Compare commits

..

No commits in common. "main" and "satrs-core-v0.1.0-alpha.1" have entirely different histories.

264 changed files with 14433 additions and 186895 deletions

View File

@ -1,64 +0,0 @@
name: ci
on: [push, pull_request]
jobs:
check:
name: Check build
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo check --release
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install nextest
uses: taiki-e/install-action@nextest
- run: cargo nextest run --all-features
- run: cargo test --doc --all-features
cross-check:
name: Check Cross-Compilation
runs-on: ubuntu-latest
strategy:
matrix:
target:
- armv7-unknown-linux-gnueabihf
- thumbv7em-none-eabihf
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: "armv7-unknown-linux-gnueabihf, thumbv7em-none-eabihf"
- run: cargo check -p satrs --release --target=${{matrix.target}} --no-default-features
fmt:
name: Check formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo fmt --all -- --check
docs:
name: Check Documentation Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- run: cargo +nightly doc --all-features --config 'build.rustdocflags=["--cfg", "docs_rs"]'
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo clippy -- -D warnings

7
.gitignore vendored
View File

@ -1,10 +1,5 @@
target/ /target
output.log
/Cargo.lock /Cargo.lock
output.log
output.log
/.idea/* /.idea/*
!/.idea/runConfigurations !/.idea/runConfigurations

View File

@ -1,14 +1,12 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"satrs", "satrs-core",
"satrs-mib", "satrs-mib",
"satrs-example", "satrs-example",
"satrs-minisim",
"satrs-shared",
] ]
exclude = [ exclude = [
"embedded-examples/stm32f3-disco-rtic", "satrs-example-stm32f3-disco",
"embedded-examples/stm32h7-rtic",
] ]

View File

@ -1,23 +1,11 @@
<p align="center"> <img src="misc/satrs-logo-v2.png" width="40%"> </p> <p align="center"> <img src="misc/satrs-logo.png" width="40%"> </p>
[![sat-rs website](https://img.shields.io/badge/sat--rs-website-darkgreen?style=flat)](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/)
[![sat-rs book](https://img.shields.io/badge/sat--rs-book-darkgreen?style=flat)](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/book/)
[![Crates.io](https://img.shields.io/crates/v/satrs)](https://crates.io/crates/satrs)
[![docs.rs](https://img.shields.io/docsrs/satrs)](https://docs.rs/satrs)
sat-rs sat-rs
========= =========
This is the repository of the sat-rs library. Its primary goal is to provide re-usable components 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 to write on-board software for remote systems like rovers or satellites. It is specifically written
for the special requirements for these systems. You can find an overview of the project and the for the special requirements for these systems.
link to the [more high-level sat-rs book](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/)
at the [IRS software projects website](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/).
This is early-stage software. Important features are missing. New releases
with breaking changes are released regularly, with all changes documented inside respective
changelog files. You should only use this library if your are willing to work in this
environment.
A lot of the architecture and general design considerations are based on the 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 [FSFW](https://egit.irs.uni-stuttgart.de/fsfw/fsfw) C++ framework which has flight heritage
@ -28,64 +16,24 @@ and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-
This project currently contains following crates: This project currently contains following crates:
* [`satrs-book`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-book): * [`satrs-core`](https://egit.irs.uni-stuttgart.de/rust/satrs-launchpad/src/branch/main/satrs-core):
Primary information resource in addition to the API documentation, hosted Core components of sat-rs.
[here](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/). It can be useful to read * [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/satrs-launchpad/src/branch/main/satrs-example):
this first before delving into the example application and the API documentation.
* [`satrs`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs):
Primary crate.
* [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example):
Example of a simple example on-board software using various sat-rs components which can be run Example of a simple example on-board software using various sat-rs components which can be run
on a host computer or on any system with a standard runtime like a Raspberry Pi. on a host computer or on any system with a standard runtime like a Raspberry Pi.
* [`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-minisim): * [`satrs-mib`](https://egit.irs.uni-stuttgart.de/rust/satrs-launchpad/src/branch/main/satrs-mib):
Mini-Simulator based on [asynchronix](https://github.com/asynchronics/asynchronix) which
simulates some physical devices for the `satrs-example` application device handlers.
* [`satrs-mib`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-mib):
Components to build a mission information base from the on-board software directly. Components to build a mission information base from the on-board software directly.
* [`satrs-stm32f3-disco-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/embedded-examples/stm32f3-disco-rtic): * [`satrs-example-stm32f3-disco`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example-stm32f3-disco):
Example of a simple example using low-level sat-rs components on a bare-metal system Example of a simple example on-board software using sat-rs components on a bare-metal system
with constrained resources. This example uses the [RTIC](https://github.com/rtic-rs/rtic) with constrained resources.
framework on the STM32F3-Discovery device.
* [`satrs-stm32h-nucleo-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/embedded-examples/stm32h7-nucleo-rtic):
Example of a simple example using sat-rs components on a bare-metal system
with constrained resources. This example uses the [RTIC](https://github.com/rtic-rs/rtic)
framework on the STM32H743ZIT device.
Each project has its own `CHANGELOG.md`. Each project has its own `CHANGELOG.md`.
# Related projects # Related projects
In addition to the crates in this repository, the sat-rs project also maintains other libraries. In addition to the crates in this repository, the sat-rs project also maintains other libraries.
* [`spacepackets`](https://egit.irs.uni-stuttgart.de/rust/spacepackets): Basic ECSS and CCSDS * [`spacepackets`](https://egit.irs.uni-stuttgart.de/rust/spacepackets): Basic ECSS and CCSDS
packet protocol implementations. This repository is re-exported in the packet protocol implementations. This repository is re-exported in the
[`satrs`](https://egit.irs.uni-stuttgart.de/rust/satrs/src/branch/main/satrs) [`satrs-core`](https://egit.irs.uni-stuttgart.de/rust/satrs-launchpad/src/branch/main/satrs-core)
crate. crate.
# Flight Heritage
There is an active and continuous effort to get early flight heritage for the sat-rs library.
Currently this library has the following flight heritage:
- Submission as an [OPS-SAT experiment](https://www.esa.int/Enabling_Support/Operations/OPS-SAT)
which has also
[flown on the satellite](https://blogs.esa.int/rocketscience/2024/05/21/ops-sat-reentry-tomorrow-final-experiments-continue/).
The application is strongly based on the sat-rs example application. You can find the repository
of the experiment [here](https://egit.irs.uni-stuttgart.de/rust/ops-sat-rs).
- Development and use of a sat-rs-based [demonstration on-board software](https://egit.irs.uni-stuttgart.de/rust/eurosim-obsw)
alongside a Flight System Simulator in the context of a
[Bachelors Thesis](https://www.researchgate.net/publication/380785984_Design_and_Development_of_a_Hardware-in-the-Loop_EuroSim_Demonstrator)
at [Airbus Netherlands](https://www.airbusdefenceandspacenetherlands.nl/).
# 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` crate with coverage. You can
optionally supply the `--open` flag to open the coverage report in your webbrowser.

View File

@ -14,15 +14,8 @@ RUN rustup install nightly && \
rustup target add thumbv7em-none-eabihf armv7-unknown-linux-gnueabihf && \ rustup target add thumbv7em-none-eabihf armv7-unknown-linux-gnueabihf && \
rustup component add rustfmt clippy rustup component add rustfmt clippy
WORKDIR "/tmp" # RUN cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
# Install cargo-nextest 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 -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin
# Install mdbook and mdbook-linkcheck
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 # SSH stuff to allow deployment to doc server
RUN adduser --uid 114 jenkins RUN adduser --uid 114 jenkins

View File

@ -32,8 +32,7 @@ pipeline {
} }
stage('Test') { stage('Test') {
steps { steps {
sh 'cargo nextest r --all-features' sh 'cargo test --all-features'
sh 'cargo test --doc --all-features'
} }
} }
stage('Check with all features') { stage('Check with all features') {
@ -48,7 +47,7 @@ pipeline {
} }
stage('Check Cross Embedded Bare Metal') { stage('Check Cross Embedded Bare Metal') {
steps { steps {
sh 'cargo check -p satrs --target thumbv7em-none-eabihf --no-default-features' sh 'cargo check -p satrs-core --target thumbv7em-none-eabihf --no-default-features'
} }
} }
stage('Check Cross Embedded Linux') { stage('Check Cross Embedded Linux') {
@ -68,7 +67,7 @@ pipeline {
sh 'mdbook build' sh 'mdbook build'
sshagent(credentials: ['documentation-buildfix']) { sshagent(credentials: ['documentation-buildfix']) {
// Deploy to Apache webserver // Deploy to Apache webserver
sh 'rsync -r --delete book/html/ buildfix@documentation.irs.uni-stuttgart.de:/projects/sat-rs/book/' sh 'rsync -r --delete book/ buildfix@documentation.irs.uni-stuttgart.de:/projects/sat-rs'
} }
} }
} }

View File

@ -1,65 +0,0 @@
#!/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"
grcov_cmd = (
f"grcov . -s . --binary-path ./target/debug/ -t {format} --branch --ignore-not-existing "
f"-o {out_path}"
)
print(f"Running: {grcov_cmd}")
os.system(grcov_cmd)
if format == "lcov":
lcov_cmd = (
"genhtml -o ./target/debug/coverage/ --show-details --highlight --ignore-errors source "
"--legend ./target/debug/lcov.info"
)
print(f"Running: {lcov_cmd}")
os.system(lcov_cmd)
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", "satrs-minisim", "satrs-example"],
default="satrs",
help="Choose project to generate coverage for",
)
parser.add_argument(
"--format",
choices=["html", "lcov"],
default="html",
help="Choose report format (html or lcov)",
)
args = parser.parse_args()
generate_cov_report(args.open, args.format, args.package)
if __name__ == "__main__":
main()

View File

@ -1,37 +0,0 @@
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# uncomment ONE of these three option to make `cargo run` start a GDB session
# which option to pick depends on your system
# You can also replace openocd.gdb by jlink.gdb when using a J-Link.
# runner = "arm-none-eabi-gdb -q -x openocd.gdb"
# runner = "gdb-multiarch -q -x openocd.gdb"
# runner = "gdb -q -x openocd.gdb"
runner = "probe-rs run --chip STM32F303VCTx"
rustflags = [
"-C", "linker=flip-link",
# LLD (shipped with the Rust toolchain) is used as the default linker
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
# This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x
# See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
"-C", "link-arg=--nmagic",
# if you run into problems with LLD switch to the GNU linker by commenting out
# this line
# "-C", "linker=arm-none-eabi-ld",
# if you need to link to pre-compiled C libraries provided by a C toolchain
# use GCC as the linker by commenting out both lines above and then
# uncommenting the three lines below
# "-C", "linker=arm-none-eabi-gcc",
# "-C", "link-arg=-Wl,-Tlink.x",
# "-C", "link-arg=-nostartfiles",
]
[build]
# comment out the following line if you intend to run unit tests on host machine
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
[env]
DEFMT_LOG = "info"

View File

@ -1,84 +0,0 @@
[package]
name = "satrs-stm32f3-disco-rtic"
version = "0.1.0"
edition = "2021"
default-run = "satrs-stm32f3-disco-rtic"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7"
defmt = "0.3"
defmt-brtt = { version = "0.1", default-features = false, features = ["rtt"] }
panic-probe = { version = "0.3", features = ["print-defmt"] }
embedded-hal = "0.2.7"
cortex-m-semihosting = "0.5.0"
enumset = "1"
heapless = "0.8"
[dependencies.rtic]
version = "2"
features = ["thumbv7-backend"]
[dependencies.rtic-monotonics]
version = "1"
features = ["cortex-m-systick"]
[dependencies.cobs]
git = "https://github.com/robamu/cobs.rs.git"
branch = "all_features"
default-features = false
[dependencies.stm32f3xx-hal]
git = "https://github.com/robamu/stm32f3xx-hal"
version = "0.11.0-alpha.0"
features = ["stm32f303xc", "rt", "enumset"]
branch = "complete-dma-update"
# Can be used in workspace to develop and update HAL
# path = "../stm32f3xx-hal"
[dependencies.stm32f3-discovery]
git = "https://github.com/robamu/stm32f3-discovery"
version = "0.8.0-alpha.0"
branch = "complete-dma-update-hal"
# Can be used in workspace to develop and update BSP
# path = "../stm32f3-discovery"
[dependencies.satrs]
# path = "satrs"
version = "0.2"
default-features = false
features = ["defmt"]
[dev-dependencies]
defmt-test = "0.3"
# cargo test
[profile.test]
codegen-units = 1
debug = 2
debug-assertions = true # <-
incremental = false
opt-level = "s" # <-
overflow-checks = true # <-
# cargo build/run --release
[profile.release]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
lto = 'fat'
opt-level = "s" # <-
overflow-checks = false # <-
# cargo test --release
[profile.bench]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
lto = 'fat'
opt-level = "s" # <-
overflow-checks = false # <-

View File

@ -1,114 +0,0 @@
sat-rs example for the STM32F3-Discovery board
=======
This example application shows how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
can be used on an embedded target.
It also shows how a relatively simple OBSW could be built when no standard runtime is available.
It uses [RTIC](https://rtic.rs/2/book/en/) as the concurrency framework and the
[defmt](https://defmt.ferrous-systems.com/) framework for logging.
The STM32F3-Discovery device was picked because it is a cheap Cortex-M4 based device which is also
used by the [Rust Embedded Book](https://docs.rust-embedded.org/book/intro/hardware.html) and the
[Rust Discovery](https://docs.rust-embedded.org/discovery/f3discovery/) book as an introduction
to embedded Rust.
## Pre-Requisites
Make sure the following tools are installed:
1. [`probe-rs`](https://probe.rs/): Application used to flash and debug the MCU.
2. Optional and recommended: [VS Code](https://code.visualstudio.com/) with
[probe-rs plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger)
for debugging.
## Preparing Rust and the repository
Building an application requires the `thumbv7em-none-eabihf` cross-compiler toolchain.
If you have not installed it yet, you can do so with
```sh
rustup target add thumbv7em-none-eabihf
```
A default `.cargo` config file is provided for this project, but needs to be copied to have
the correct name. This is so that the config file can be updated or edited for custom needs
without being tracked by git.
```sh
cp def_config.toml config.toml
```
The configuration file will also set the target so it does not always have to be specified with
the `--target` argument.
## Building
After that, assuming that you have a `.cargo/config.toml` setting the correct build target,
you can simply build the application with
```sh
cargo build
```
## Flashing from the command line
You can flash the application from the command line using `probe-rs`:
```sh
probe-rs run --chip STM32F303VCTx
```
## Debugging with VS Code
The STM32F3-Discovery comes with an on-board ST-Link so all that is required to flash and debug
the board is a Mini-USB cable. The code in this repository was debugged using [`probe-rs`](https://probe.rs/docs/tools/debuggerA)
and the VS Code [`probe-rs` plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger).
Make sure to install this plugin first.
Sample configuration files are provided inside the `vscode` folder.
Use `cp vscode .vscode -r` to use them for your project.
Some sample configuration files for VS Code were provided as well. You can simply use `Run` and `Debug`
to automatically rebuild and flash your application.
The `tasks.json` and `launch.json` files are generic and you can use them immediately by opening
the folder in VS code or adding it to a workspace.
## Commanding with Python
When the SW is running on the Discovery board, you can command the MCU via a serial interface,
using COBS encoded PUS packets.
It is recommended to use a virtual environment to do this. To set up one in the command line,
you can use `python3 -m venv venv` on Unix systems or `py -m venv venv` on Windows systems.
After doing this, you can check the [venv tutorial](https://docs.python.org/3/tutorial/venv.html)
on how to activate the environment and then use the following command to install the required
dependency:
```sh
pip install -r requirements.txt
```
The packets are exchanged using a dedicated serial interface. You can use any generic USB-to-UART
converter device with the TX pin connected to the PA3 pin and the RX pin connected to the PA2 pin.
A default configuration file for the python application is provided and can be used by running
```sh
cp def_tmtc_conf.json tmtc_conf.json
```
After that, you can for example send a ping to the MCU using the following command
```sh
./main.py -p /ping
```
You can configure the blinky frequency using
```sh
./main.py -p /change_blink_freq
```
All these commands will package a PUS telecommand which will be sent to the MCU using the COBS
format as the packet framing format.

File diff suppressed because it is too large Load Diff

View File

@ -1,51 +0,0 @@
#![no_main]
#![no_std]
use cortex_m_semihosting::debug;
use defmt_brtt as _; // global logger
use stm32f3xx_hal as _; // memory layout
use panic_probe as _;
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
// this prevents the panic message being printed *twice* when `defmt::panic` is invoked
#[defmt::panic_handler]
fn panic() -> ! {
cortex_m::asm::udf()
}
/// Terminates the application and makes a semihosting-capable debug tool exit
/// with status code 0.
pub fn exit() -> ! {
loop {
debug::exit(debug::EXIT_SUCCESS);
}
}
/// Hardfault handler.
///
/// Terminates the application and makes a semihosting-capable debug tool exit
/// with an error. This seems better than the default, which is to spin in a
/// loop.
#[cortex_m_rt::exception]
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
loop {
debug::exit(debug::EXIT_FAILURE);
}
}
// defmt-test 0.3.0 has the limitation that this `#[tests]` attribute can only be used
// once within a crate. the module can be in any file but there can only be at most
// one `#[tests]` module in this library crate
#[cfg(test)]
#[defmt_test::tests]
mod unit_tests {
use defmt::assert;
#[test]
fn it_works() {
assert!(true)
}
}

View File

@ -1,684 +0,0 @@
#![no_std]
#![no_main]
use satrs::pus::verification::{
FailParams, TcStateAccepted, VerificationReportCreator, VerificationToken,
};
use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader};
use satrs::spacepackets::ecss::EcssEnumU16;
use satrs::spacepackets::CcsdsPacket;
use satrs::spacepackets::{ByteConversionError, SpHeader};
// global logger + panicking-behavior + memory layout
use satrs_stm32f3_disco_rtic as _;
use rtic::app;
use heapless::{mpmc::Q8, Vec};
#[allow(unused_imports)]
use rtic_monotonics::systick::fugit::{MillisDurationU32, TimerInstantU32};
use rtic_monotonics::systick::ExtU32;
use satrs::seq_count::SequenceCountProviderCore;
use satrs::spacepackets::{ecss::PusPacket, ecss::WritablePusPacket};
use stm32f3xx_hal::dma::dma1;
use stm32f3xx_hal::gpio::{PushPull, AF7, PA2, PA3};
use stm32f3xx_hal::pac::USART2;
use stm32f3xx_hal::serial::{Rx, RxEvent, Serial, SerialDmaRx, SerialDmaTx, Tx, TxEvent};
const UART_BAUD: u32 = 115200;
const DEFAULT_BLINK_FREQ_MS: u32 = 1000;
const TX_HANDLER_FREQ_MS: u32 = 20;
const MIN_DELAY_BETWEEN_TX_PACKETS_MS: u32 = 5;
const MAX_TC_LEN: usize = 128;
const MAX_TM_LEN: usize = 128;
pub const PUS_APID: u16 = 0x02;
type TxType = Tx<USART2, PA2<AF7<PushPull>>>;
type RxType = Rx<USART2, PA3<AF7<PushPull>>>;
type InstantFugit = TimerInstantU32<1000>;
type TxDmaTransferType = SerialDmaTx<&'static [u8], dma1::C7, TxType>;
type RxDmaTransferType = SerialDmaRx<&'static mut [u8], dma1::C6, RxType>;
// This is the predictable maximum overhead of the COBS encoding scheme.
// It is simply the maximum packet lenght dividied by 254 rounded up.
const COBS_TC_OVERHEAD: usize = (MAX_TC_LEN + 254 - 1) / 254;
const COBS_TM_OVERHEAD: usize = (MAX_TM_LEN + 254 - 1) / 254;
const TC_BUF_LEN: usize = MAX_TC_LEN + COBS_TC_OVERHEAD;
const TM_BUF_LEN: usize = MAX_TC_LEN + COBS_TM_OVERHEAD;
// This is a static buffer which should ONLY (!) be used as the TX DMA
// transfer buffer.
static mut DMA_TX_BUF: [u8; TM_BUF_LEN] = [0; TM_BUF_LEN];
// This is a static buffer which should ONLY (!) be used as the RX DMA
// transfer buffer.
static mut DMA_RX_BUF: [u8; TC_BUF_LEN] = [0; TC_BUF_LEN];
type TmPacket = Vec<u8, MAX_TM_LEN>;
type TcPacket = Vec<u8, MAX_TC_LEN>;
static TM_REQUESTS: Q8<TmPacket> = Q8::new();
use core::sync::atomic::{AtomicU16, Ordering};
pub struct SeqCountProviderAtomicRef {
atomic: AtomicU16,
ordering: Ordering,
}
impl SeqCountProviderAtomicRef {
pub const fn new(ordering: Ordering) -> Self {
Self {
atomic: AtomicU16::new(0),
ordering,
}
}
}
impl SequenceCountProviderCore<u16> for SeqCountProviderAtomicRef {
fn get(&self) -> u16 {
self.atomic.load(self.ordering)
}
fn increment(&self) {
self.atomic.fetch_add(1, self.ordering);
}
fn get_and_increment(&self) -> u16 {
self.atomic.fetch_add(1, self.ordering)
}
}
static SEQ_COUNT_PROVIDER: SeqCountProviderAtomicRef =
SeqCountProviderAtomicRef::new(Ordering::Relaxed);
pub struct TxIdle {
tx: TxType,
dma_channel: dma1::C7,
}
#[derive(Debug, defmt::Format)]
pub enum TmSendError {
ByteConversion(ByteConversionError),
Queue,
}
impl From<ByteConversionError> for TmSendError {
fn from(value: ByteConversionError) -> Self {
Self::ByteConversion(value)
}
}
fn send_tm(tm_creator: PusTmCreator) -> Result<(), TmSendError> {
if tm_creator.len_written() > MAX_TM_LEN {
return Err(ByteConversionError::ToSliceTooSmall {
expected: tm_creator.len_written(),
found: MAX_TM_LEN,
}
.into());
}
let mut tm_vec = TmPacket::new();
tm_vec
.resize(tm_creator.len_written(), 0)
.expect("vec resize failed");
tm_creator.write_to_bytes(tm_vec.as_mut_slice())?;
defmt::info!(
"Sending TM[{},{}] with size {}",
tm_creator.service(),
tm_creator.subservice(),
tm_creator.len_written()
);
TM_REQUESTS
.enqueue(tm_vec)
.map_err(|_| TmSendError::Queue)?;
Ok(())
}
fn handle_tm_send_error(error: TmSendError) {
defmt::warn!("sending tm failed with error {}", error);
}
pub enum UartTxState {
// Wrapped in an option because we need an owned type later.
Idle(Option<TxIdle>),
// Same as above
Transmitting(Option<TxDmaTransferType>),
}
pub struct UartTxShared {
last_completed: Option<InstantFugit>,
state: UartTxState,
}
pub struct RequestWithToken {
token: VerificationToken<TcStateAccepted>,
request: Request,
}
#[derive(Debug, defmt::Format)]
pub enum Request {
Ping,
ChangeBlinkFrequency(u32),
}
#[derive(Debug, defmt::Format)]
pub enum RequestError {
InvalidApid = 1,
InvalidService = 2,
InvalidSubservice = 3,
NotEnoughAppData = 4,
}
pub fn convert_pus_tc_to_request(
tc: &PusTcReader,
verif_reporter: &mut VerificationReportCreator,
src_data_buf: &mut [u8],
timestamp: &[u8],
) -> Result<RequestWithToken, RequestError> {
defmt::info!(
"Found PUS TC [{},{}] with length {}",
tc.service(),
tc.subservice(),
tc.len_packed()
);
let token = verif_reporter.add_tc(tc);
if tc.apid() != PUS_APID {
defmt::warn!("Received tc with unknown APID {}", tc.apid());
let result = send_tm(
verif_reporter
.acceptance_failure(
src_data_buf,
token,
SEQ_COUNT_PROVIDER.get_and_increment(),
0,
FailParams::new(timestamp, &EcssEnumU16::new(0), &[]),
)
.unwrap(),
);
if let Err(e) = result {
handle_tm_send_error(e);
}
return Err(RequestError::InvalidApid);
}
let (tm_creator, accepted_token) = verif_reporter
.acceptance_success(
src_data_buf,
token,
SEQ_COUNT_PROVIDER.get_and_increment(),
0,
timestamp,
)
.unwrap();
if let Err(e) = send_tm(tm_creator) {
handle_tm_send_error(e);
}
if tc.service() == 17 && tc.subservice() == 1 {
if tc.subservice() == 1 {
return Ok(RequestWithToken {
request: Request::Ping,
token: accepted_token,
});
} else {
return Err(RequestError::InvalidSubservice);
}
} else if tc.service() == 8 {
if tc.subservice() == 1 {
if tc.user_data().len() < 4 {
return Err(RequestError::NotEnoughAppData);
}
let new_freq_ms = u32::from_be_bytes(tc.user_data()[0..4].try_into().unwrap());
return Ok(RequestWithToken {
request: Request::ChangeBlinkFrequency(new_freq_ms),
token: accepted_token,
});
} else {
return Err(RequestError::InvalidSubservice);
}
} else {
return Err(RequestError::InvalidService);
}
}
#[app(device = stm32f3xx_hal::pac, peripherals = true)]
mod app {
use super::*;
use core::slice::Iter;
use rtic_monotonics::systick::Systick;
use rtic_monotonics::Monotonic;
use satrs::pus::verification::{TcStateStarted, VerificationReportCreator};
use satrs::spacepackets::{ecss::tc::PusTcReader, time::cds::P_FIELD_BASE};
#[allow(unused_imports)]
use stm32f3_discovery::leds::Direction;
use stm32f3_discovery::leds::Leds;
use stm32f3xx_hal::prelude::*;
use stm32f3_discovery::switch_hal::OutputSwitch;
use stm32f3xx_hal::Switch;
#[allow(dead_code)]
type SerialType = Serial<USART2, (PA2<AF7<PushPull>>, PA3<AF7<PushPull>>)>;
#[shared]
struct Shared {
blink_freq: MillisDurationU32,
tx_shared: UartTxShared,
rx_transfer: Option<RxDmaTransferType>,
}
#[local]
struct Local {
verif_reporter: VerificationReportCreator,
leds: Leds,
last_dir: Direction,
curr_dir: Iter<'static, Direction>,
}
#[init]
fn init(cx: init::Context) -> (Shared, Local) {
let mut rcc = cx.device.RCC.constrain();
// Initialize the systick interrupt & obtain the token to prove that we did
let systick_mono_token = rtic_monotonics::create_systick_token!();
Systick::start(cx.core.SYST, 8_000_000, systick_mono_token);
let mut flash = cx.device.FLASH.constrain();
let clocks = rcc
.cfgr
.use_hse(8.MHz())
.sysclk(8.MHz())
.pclk1(8.MHz())
.freeze(&mut flash.acr);
// Set up monotonic timer.
//let mono_timer = MonoTimer::new(cx.core.DWT, clocks, &mut cx.core.DCB);
defmt::info!("Starting sat-rs demo application for the STM32F3-Discovery");
let mut gpioe = cx.device.GPIOE.split(&mut rcc.ahb);
let leds = Leds::new(
gpioe.pe8,
gpioe.pe9,
gpioe.pe10,
gpioe.pe11,
gpioe.pe12,
gpioe.pe13,
gpioe.pe14,
gpioe.pe15,
&mut gpioe.moder,
&mut gpioe.otyper,
);
let mut gpioa = cx.device.GPIOA.split(&mut rcc.ahb);
// USART2 pins
let mut pins = (
// TX pin: PA2
gpioa
.pa2
.into_af_push_pull(&mut gpioa.moder, &mut gpioa.otyper, &mut gpioa.afrl),
// RX pin: PA3
gpioa
.pa3
.into_af_push_pull(&mut gpioa.moder, &mut gpioa.otyper, &mut gpioa.afrl),
);
pins.1.internal_pull_up(&mut gpioa.pupdr, true);
let mut usart2 = Serial::new(
cx.device.USART2,
pins,
UART_BAUD.Bd(),
clocks,
&mut rcc.apb1,
);
usart2.configure_rx_interrupt(RxEvent::Idle, Switch::On);
// This interrupt is enabled to re-schedule new transfers in the interrupt handler immediately.
usart2.configure_tx_interrupt(TxEvent::TransmissionComplete, Switch::On);
let dma1 = cx.device.DMA1.split(&mut rcc.ahb);
let (mut tx_serial, mut rx_serial) = usart2.split();
// This interrupt is immediately triggered, clear it. It will only be reset
// by the hardware when data is received on RX (RXNE event)
rx_serial.clear_event(RxEvent::Idle);
// For some reason, this is also immediately triggered..
tx_serial.clear_event(TxEvent::TransmissionComplete);
let rx_transfer = rx_serial.read_exact(unsafe { DMA_RX_BUF.as_mut_slice() }, dma1.ch6);
defmt::info!("Spawning tasks");
blink::spawn().unwrap();
serial_tx_handler::spawn().unwrap();
let verif_reporter = VerificationReportCreator::new(PUS_APID).unwrap();
(
Shared {
blink_freq: MillisDurationU32::from_ticks(DEFAULT_BLINK_FREQ_MS),
tx_shared: UartTxShared {
last_completed: None,
state: UartTxState::Idle(Some(TxIdle {
tx: tx_serial,
dma_channel: dma1.ch7,
})),
},
rx_transfer: Some(rx_transfer),
},
Local {
verif_reporter,
leds,
last_dir: Direction::North,
curr_dir: Direction::iter(),
},
)
}
#[task(local = [leds, curr_dir, last_dir], shared=[blink_freq])]
async fn blink(mut cx: blink::Context) {
let blink::LocalResources {
leds,
curr_dir,
last_dir,
..
} = cx.local;
let mut toggle_leds = |dir: &Direction| {
let last_led = leds.for_direction(*last_dir);
last_led.off().ok();
let led = leds.for_direction(*dir);
led.on().ok();
*last_dir = *dir;
};
loop {
match curr_dir.next() {
Some(dir) => {
toggle_leds(dir);
}
None => {
*curr_dir = Direction::iter();
toggle_leds(curr_dir.next().unwrap());
}
}
let current_blink_freq = cx.shared.blink_freq.lock(|current| *current);
Systick::delay(current_blink_freq).await;
}
}
#[task(
shared = [tx_shared],
)]
async fn serial_tx_handler(mut cx: serial_tx_handler::Context) {
loop {
let is_idle = cx.shared.tx_shared.lock(|tx_shared| {
if let UartTxState::Idle(_) = tx_shared.state {
return true;
}
false
});
if is_idle {
let last_completed = cx.shared.tx_shared.lock(|shared| shared.last_completed);
if let Some(last_completed) = last_completed {
let elapsed_ms = (Systick::now() - last_completed).to_millis();
if elapsed_ms < MIN_DELAY_BETWEEN_TX_PACKETS_MS {
Systick::delay((MIN_DELAY_BETWEEN_TX_PACKETS_MS - elapsed_ms).millis())
.await;
}
}
} else {
// Check for completion after 1 ms
Systick::delay(1.millis()).await;
continue;
}
if let Some(vec) = TM_REQUESTS.dequeue() {
cx.shared
.tx_shared
.lock(|tx_shared| match &mut tx_shared.state {
UartTxState::Idle(tx) => {
let encoded_len;
//debug!(target: "serial_tx_handler", "bytes: {:x?}", &buf[0..len]);
// Safety: We only copy the data into the TX DMA buffer in this task.
// If the DMA is active, another branch will be taken.
unsafe {
// 0 sentinel value as start marker
DMA_TX_BUF[0] = 0;
encoded_len =
cobs::encode(&vec[0..vec.len()], &mut DMA_TX_BUF[1..]);
// Should never panic, we accounted for the overhead.
// Write into transfer buffer directly, no need for intermediate
// encoding buffer.
// 0 end marker
DMA_TX_BUF[encoded_len + 1] = 0;
}
//debug!(target: "serial_tx_handler", "Sending {} bytes", encoded_len + 2);
//debug!("sent: {:x?}", &mut_tx_dma_buf[0..encoded_len + 2]);
let tx_idle = tx.take().unwrap();
// Transfer completion and re-scheduling of new TX transfers will be done
// by the IRQ handler.
// SAFETY: The DMA is the exclusive writer to the DMA buffer now.
let transfer = tx_idle.tx.write_all(
unsafe { &DMA_TX_BUF[0..encoded_len + 2] },
tx_idle.dma_channel,
);
tx_shared.state = UartTxState::Transmitting(Some(transfer));
// The memory block is automatically returned to the pool when it is dropped.
}
UartTxState::Transmitting(_) => (),
});
// Check for completion after 1 ms
Systick::delay(1.millis()).await;
continue;
}
// Nothing to do, and we are idle.
Systick::delay(TX_HANDLER_FREQ_MS.millis()).await;
}
}
#[task(
local = [
verif_reporter,
decode_buf: [u8; MAX_TC_LEN] = [0; MAX_TC_LEN],
src_data_buf: [u8; MAX_TM_LEN] = [0; MAX_TM_LEN],
timestamp: [u8; 7] = [0; 7],
],
shared = [blink_freq]
)]
async fn serial_rx_handler(
mut cx: serial_rx_handler::Context,
received_packet: Vec<u8, MAX_TC_LEN>,
) {
cx.local.timestamp[0] = P_FIELD_BASE;
defmt::info!("Received packet with {} bytes", received_packet.len());
let decode_buf = cx.local.decode_buf;
let packet = received_packet.as_slice();
let mut start_idx = None;
for (idx, byte) in packet.iter().enumerate() {
if *byte != 0 {
start_idx = Some(idx);
break;
}
}
if start_idx.is_none() {
defmt::warn!("decoding error, can only process cobs encoded frames, data is all 0");
return;
}
let start_idx = start_idx.unwrap();
match cobs::decode(&received_packet.as_slice()[start_idx..], decode_buf) {
Ok(len) => {
defmt::info!("Decoded packet length: {}", len);
let pus_tc = PusTcReader::new(decode_buf);
match pus_tc {
Ok((tc, _tc_len)) => {
match convert_pus_tc_to_request(
&tc,
cx.local.verif_reporter,
cx.local.src_data_buf,
cx.local.timestamp,
) {
Ok(request_with_token) => {
let started_token = handle_start_verification(
request_with_token.token,
cx.local.verif_reporter,
cx.local.src_data_buf,
cx.local.timestamp,
);
match request_with_token.request {
Request::Ping => {
handle_ping_request(cx.local.timestamp);
}
Request::ChangeBlinkFrequency(new_freq_ms) => {
defmt::info!("Received blink frequency change request with new frequncy {}", new_freq_ms);
cx.shared.blink_freq.lock(|blink_freq| {
*blink_freq =
MillisDurationU32::from_ticks(new_freq_ms);
});
}
}
handle_completion_verification(
started_token,
cx.local.verif_reporter,
cx.local.src_data_buf,
cx.local.timestamp,
);
}
Err(e) => {
// TODO: Error handling: Send verification failure based on request error.
defmt::warn!("request error {}", e);
}
}
}
Err(e) => {
defmt::warn!("Error unpacking PUS TC: {}", e);
}
}
}
Err(_) => {
defmt::warn!("decoding error, can only process cobs encoded frames")
}
}
}
fn handle_ping_request(timestamp: &[u8]) {
defmt::info!("Received PUS ping telecommand, sending ping reply TM[17,2]");
let sp_header =
SpHeader::new_for_unseg_tc(PUS_APID, SEQ_COUNT_PROVIDER.get_and_increment(), 0);
let sec_header = PusTmSecondaryHeader::new_simple(17, 2, timestamp);
let ping_reply = PusTmCreator::new(sp_header, sec_header, &[], true);
let mut tm_packet = TmPacket::new();
tm_packet
.resize(ping_reply.len_written(), 0)
.expect("vec resize failed");
ping_reply.write_to_bytes(&mut tm_packet).unwrap();
if TM_REQUESTS.enqueue(tm_packet).is_err() {
defmt::warn!("TC queue full");
return;
}
}
fn handle_start_verification(
accepted_token: VerificationToken<TcStateAccepted>,
verif_reporter: &mut VerificationReportCreator,
src_data_buf: &mut [u8],
timestamp: &[u8],
) -> VerificationToken<TcStateStarted> {
let (tm_creator, started_token) = verif_reporter
.start_success(
src_data_buf,
accepted_token,
SEQ_COUNT_PROVIDER.get(),
0,
&timestamp,
)
.unwrap();
let result = send_tm(tm_creator);
if let Err(e) = result {
handle_tm_send_error(e);
}
started_token
}
fn handle_completion_verification(
started_token: VerificationToken<TcStateStarted>,
verif_reporter: &mut VerificationReportCreator,
src_data_buf: &mut [u8],
timestamp: &[u8],
) {
let result = send_tm(
verif_reporter
.completion_success(
src_data_buf,
started_token,
SEQ_COUNT_PROVIDER.get(),
0,
timestamp,
)
.unwrap(),
);
if let Err(e) = result {
handle_tm_send_error(e);
}
}
#[task(binds = DMA1_CH6, shared = [rx_transfer])]
fn rx_dma_isr(mut cx: rx_dma_isr::Context) {
let mut tc_packet = TcPacket::new();
cx.shared.rx_transfer.lock(|rx_transfer| {
let rx_ref = rx_transfer.as_ref().unwrap();
if rx_ref.is_complete() {
let uart_rx_owned = rx_transfer.take().unwrap();
let (buf, c, rx) = uart_rx_owned.stop();
// The received data is transferred to another task now to avoid any processing overhead
// during the interrupt. There are multiple ways to do this, we use a stack allocaed vector here
// to do this.
tc_packet.resize(buf.len(), 0).expect("vec resize failed");
tc_packet.copy_from_slice(buf);
// Start the next transfer as soon as possible.
*rx_transfer = Some(rx.read_exact(buf, c));
// Send the vector to a regular task.
serial_rx_handler::spawn(tc_packet).expect("spawning rx handler task failed");
// If this happens, there is a high chance that the maximum packet length was
// exceeded. Circular mode is not used here, so data might be missed.
defmt::warn!(
"rx transfer with maximum length {}, might miss data",
TC_BUF_LEN
);
}
});
}
#[task(binds = USART2_EXTI26, shared = [rx_transfer, tx_shared])]
fn serial_isr(mut cx: serial_isr::Context) {
cx.shared
.tx_shared
.lock(|tx_shared| match &mut tx_shared.state {
UartTxState::Idle(_) => (),
UartTxState::Transmitting(transfer) => {
let transfer_ref = transfer.as_ref().unwrap();
if transfer_ref.is_complete() {
let transfer = transfer.take().unwrap();
let (_, dma_channel, mut tx) = transfer.stop();
tx.clear_event(TxEvent::TransmissionComplete);
tx_shared.state = UartTxState::Idle(Some(TxIdle { tx, dma_channel }));
// We cache the last completed time to ensure that there is a minimum delay between consecutive
// transferred packets.
tx_shared.last_completed = Some(Systick::now());
}
}
});
let mut tc_packet = TcPacket::new();
cx.shared.rx_transfer.lock(|rx_transfer| {
let rx_transfer_ref = rx_transfer.as_ref().unwrap();
// Received a partial packet.
if rx_transfer_ref.is_event_triggered(RxEvent::Idle) {
let rx_transfer_owned = rx_transfer.take().unwrap();
let (buf, ch, mut rx, rx_len) = rx_transfer_owned.stop_and_return_received_bytes();
// The received data is transferred to another task now to avoid any processing overhead
// during the interrupt. There are multiple ways to do this, we use a stack
// allocated vector to do this.
tc_packet
.resize(rx_len as usize, 0)
.expect("vec resize failed");
tc_packet[0..rx_len as usize].copy_from_slice(&buf[0..rx_len as usize]);
rx.clear_event(RxEvent::Idle);
serial_rx_handler::spawn(tc_packet).expect("spawning rx handler failed");
*rx_transfer = Some(rx.read_exact(buf, ch));
}
});
}
}

View File

@ -1,22 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"preLaunchTask": "${defaultBuildTask}",
"type": "probe-rs-debug",
"request": "launch",
"name": "probe-rs Debugging ",
"flashingConfig": {
"flashingEnabled": true
},
"chip": "STM32F303VCTx",
"coreConfigs": [
{
"programBinary": "${workspaceFolder}/target/thumbv7em-none-eabihf/debug/satrs-stm32f3-disco-rtic",
"rttEnabled": true,
"svdFile": "STM32F303.svd"
}
]
}
]
}

View File

@ -1,29 +0,0 @@
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip STM32H743ZITx"
# runner = ["probe-rs", "run", "--chip", "$CHIP", "--log-format", "{L} {s}"]
rustflags = [
"-C", "linker=flip-link",
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
# This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x
# See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
"-C", "link-arg=--nmagic",
# Can be useful for debugging.
# "-Clink-args=-Map=app.map"
]
[build]
# (`thumbv6m-*` is compatible with all ARM Cortex-M chips but using the right
# target improves performance)
# target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
# target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
[alias]
rb = "run --bin"
rrb = "run --release --bin"
[env]
DEFMT_LOG = "info"

View File

@ -1,4 +0,0 @@
/target
/.cargo/config*
/.vscode
/app.map

View File

@ -1,881 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "atomic-polyfill"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
dependencies = [
"critical-section",
]
[[package]]
name = "autocfg"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "bare-metal"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3"
dependencies = [
"rustc_version 0.2.3",
]
[[package]]
name = "bare-metal"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fe8f5a8a398345e52358e18ff07cc17a568fbca5c6f73873d3a62056309603"
[[package]]
name = "bitfield"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cobs"
version = "0.2.3"
source = "git+https://github.com/robamu/cobs.rs.git?branch=all_features#c70a7f30fd00a7cbdb7666dec12b437977385d40"
[[package]]
name = "cortex-m"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9"
dependencies = [
"bare-metal 0.2.5",
"bitfield",
"critical-section",
"embedded-hal 0.2.7",
"volatile-register",
]
[[package]]
name = "cortex-m-rt"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2722f5b7d6ea8583cffa4d247044e280ccbb9fe501bed56552e2ba48b02d5f3d"
dependencies = [
"cortex-m-rt-macros",
]
[[package]]
name = "cortex-m-rt-macros"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f6f3e36f203cfedbc78b357fb28730aa2c6dc1ab060ee5c2405e843988d3c7"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "cortex-m-semihosting"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c23234600452033cc77e4b761e740e02d2c4168e11dbf36ab14a0f58973592b0"
dependencies = [
"cortex-m",
]
[[package]]
name = "crc"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "critical-section"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216"
[[package]]
name = "defmt"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99dd22262668b887121d4672af5a64b238f026099f1a2a1b322066c9ecfe9e0"
dependencies = [
"bitflags",
"defmt-macros",
]
[[package]]
name = "defmt-brtt"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2f0ac3635d0c89d12b8101fcb44a7625f5f030a1c0491124b74467eb5a58a78"
dependencies = [
"critical-section",
"defmt",
]
[[package]]
name = "defmt-macros"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9f309eff1f79b3ebdf252954d90ae440599c26c2c553fe87a2d17195f2dcb"
dependencies = [
"defmt-parser",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.64",
]
[[package]]
name = "defmt-parser"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff4a5fefe330e8d7f31b16a318f9ce81000d8e35e69b93eae154d16d2278f70f"
dependencies = [
"thiserror",
]
[[package]]
name = "defmt-test"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290966e8c38f94b11884877242de876280d0eab934900e9642d58868e77c5df1"
dependencies = [
"cortex-m-rt",
"cortex-m-semihosting",
"defmt",
"defmt-test-macros",
]
[[package]]
name = "defmt-test-macros"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "984bc6eca246389726ac2826acc2488ca0fe5fcd6b8d9b48797021951d76a125"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.64",
]
[[package]]
name = "delegate"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee5df75c70b95bd3aacc8e2fd098797692fb1d54121019c4de481e42f04c8a1"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "derive-new"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.64",
]
[[package]]
name = "embedded-alloc"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddae17915accbac2cfbc64ea0ae6e3b330e6ea124ba108dada63646fd3c6f815"
dependencies = [
"critical-section",
"linked_list_allocator",
]
[[package]]
name = "embedded-dma"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "994f7e5b5cb23521c22304927195f236813053eb9c065dd2226a32ba64695446"
dependencies = [
"stable_deref_trait",
]
[[package]]
name = "embedded-hal"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff"
dependencies = [
"nb 0.1.3",
"void",
]
[[package]]
name = "embedded-hal"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89"
dependencies = [
"defmt",
]
[[package]]
name = "embedded-hal-async"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884"
dependencies = [
"defmt",
"embedded-hal 1.0.0",
]
[[package]]
name = "embedded-hal-bus"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57b4e6ede84339ebdb418cd986e6320a34b017cdf99b5cc3efceec6450b06886"
dependencies = [
"critical-section",
"defmt",
"embedded-hal 1.0.0",
"embedded-hal-async",
]
[[package]]
name = "embedded-storage"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "fugit"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17186ad64927d5ac8f02c1e77ccefa08ccd9eaa314d5a4772278aa204a22f7e7"
dependencies = [
"gcd",
]
[[package]]
name = "futures-core"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-task"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-util"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"pin-utils",
]
[[package]]
name = "gcd"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a"
[[package]]
name = "hash32"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
dependencies = [
"byteorder",
]
[[package]]
name = "hash32"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "heapless"
version = "0.7.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
dependencies = [
"atomic-polyfill",
"hash32 0.2.1",
"rustc_version 0.4.0",
"spin",
"stable_deref_trait",
]
[[package]]
name = "heapless"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
dependencies = [
"defmt",
"hash32 0.3.1",
"stable_deref_trait",
]
[[package]]
name = "indexmap"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "linked_list_allocator"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afa463f5405ee81cdb9cc2baf37e08ec7e4c8209442b5d72c04cfb2cd6e6286"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "managed"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d"
[[package]]
name = "nb"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f"
dependencies = [
"nb 1.1.0",
]
[[package]]
name = "nb"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "num_enum"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.64",
]
[[package]]
name = "panic-probe"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4047d9235d1423d66cc97da7d07eddb54d4f154d6c13805c6d0793956f4f25b0"
dependencies = [
"cortex-m",
"defmt",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "portable-atomic"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rtic"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c443db16326376bdd64377da268f6616d5f804aba8ce799bac7d1f7f244e9d51"
dependencies = [
"atomic-polyfill",
"bare-metal 1.0.0",
"cortex-m",
"critical-section",
"rtic-core",
"rtic-macros",
]
[[package]]
name = "rtic-common"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0786b50b81ef9d2a944a000f60405bb28bf30cd45da2d182f3fe636b2321f35c"
dependencies = [
"critical-section",
]
[[package]]
name = "rtic-core"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9369355b04d06a3780ec0f51ea2d225624db777acbc60abd8ca4832da5c1a42"
[[package]]
name = "rtic-macros"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54053598ea24b1b74937724e366558412a1777eb2680b91ef646db540982789a"
dependencies = [
"indexmap",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.64",
]
[[package]]
name = "rtic-monotonics"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "058c2397dbd5bb4c5650a0e368c3920953e458805ff5097a0511b8147b3619d7"
dependencies = [
"atomic-polyfill",
"cfg-if",
"cortex-m",
"embedded-hal 1.0.0",
"fugit",
"rtic-time",
]
[[package]]
name = "rtic-sync"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49b1200137ccb2bf272a1801fa6e27264535facd356cb2c1d5bc8e12aa211bad"
dependencies = [
"critical-section",
"defmt",
"embedded-hal 1.0.0",
"embedded-hal-async",
"embedded-hal-bus",
"heapless 0.8.0",
"portable-atomic",
"rtic-common",
]
[[package]]
name = "rtic-time"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b232e7aebc045cfea81cdd164bc2727a10aca9a4568d406d0a5661cdfd0f19"
dependencies = [
"critical-section",
"futures-util",
"rtic-common",
]
[[package]]
name = "rustc_version"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
dependencies = [
"semver 0.9.0",
]
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver 1.0.23",
]
[[package]]
name = "satrs"
version = "0.2.1"
dependencies = [
"cobs",
"crc",
"defmt",
"delegate",
"derive-new",
"heapless 0.7.17",
"num-traits",
"num_enum",
"paste",
"satrs-shared",
"smallvec",
"spacepackets",
]
[[package]]
name = "satrs-shared"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6042477018c2d43fffccaaa5099bc299a58485139b4d31c5b276889311e474f1"
dependencies = [
"spacepackets",
]
[[package]]
name = "satrs-stm32h7-nucleo-rtic"
version = "0.1.0"
dependencies = [
"cortex-m",
"cortex-m-rt",
"cortex-m-semihosting",
"defmt",
"defmt-brtt",
"defmt-test",
"embedded-alloc",
"panic-probe",
"rtic",
"rtic-monotonics",
"rtic-sync",
"satrs",
"smoltcp",
"stm32h7xx-hal",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "semver-parser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "smoltcp"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a1a996951e50b5971a2c8c0fa05a381480d70a933064245c4a223ddc87ccc97"
dependencies = [
"bitflags",
"byteorder",
"cfg-if",
"defmt",
"heapless 0.8.0",
"managed",
]
[[package]]
name = "spacepackets"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e85574d113a06312010c0ba51aadccd4ba2806231ebe9a49fc6473d0534d8696"
dependencies = [
"crc",
"defmt",
"delegate",
"num-traits",
"num_enum",
"zerocopy",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "stm32h7"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "362f288cd8341e9209587b889c385f323e82fc237b60c272868965bb879bb9b1"
dependencies = [
"bare-metal 1.0.0",
"cortex-m",
"cortex-m-rt",
"vcell",
]
[[package]]
name = "stm32h7xx-hal"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bd869329be25440b24e2b3583a1c016151b4a54bc36d96d82af7fcd9d010b98"
dependencies = [
"bare-metal 1.0.0",
"cast",
"cortex-m",
"embedded-dma",
"embedded-hal 0.2.7",
"embedded-storage",
"fugit",
"nb 1.1.0",
"paste",
"smoltcp",
"stm32h7",
"void",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.64",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "vcell"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "void"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
[[package]]
name = "volatile-register"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc"
dependencies = [
"vcell",
]
[[package]]
name = "zerocopy"
version = "0.7.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087"
dependencies = [
"byteorder",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.64",
]

View File

@ -1,85 +0,0 @@
[package]
authors = ["Robin Mueller <robin.mueller.m@gmail.com>"]
name = "satrs-stm32h7-nucleo-rtic"
edition = "2021"
version = "0.1.0"
default-run = "satrs-stm32h7-nucleo-rtic"
[lib]
harness = false
# needed for each integration test
[[test]]
name = "integration"
harness = false
[dependencies]
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7"
defmt = "0.3"
defmt-brtt = { version = "0.1", default-features = false, features = ["rtt"] }
panic-probe = { version = "0.3", features = ["print-defmt"] }
cortex-m-semihosting = "0.5.0"
stm32h7xx-hal = { version="0.16", features= ["stm32h743v", "ethernet"] }
embedded-alloc = "0.5"
rtic-sync = { version = "1", features = ["defmt-03"] }
[dependencies.smoltcp]
version = "0.11.0"
default-features = false
features = ["medium-ethernet", "proto-ipv4", "socket-raw", "socket-dhcpv4", "socket-udp", "defmt"]
[dependencies.rtic]
version = "2"
features = ["thumbv7-backend"]
[dependencies.rtic-monotonics]
version = "1"
features = ["cortex-m-systick"]
[dependencies.satrs]
path = "../../satrs"
version = "0.2"
default-features = false
features = ["defmt", "heapless"]
[dev-dependencies]
defmt-test = "0.3"
# cargo build/run
[profile.dev]
codegen-units = 1
debug = 2
debug-assertions = true # <-
incremental = false
opt-level = 's' # <-
overflow-checks = true # <-
# cargo test
[profile.test]
codegen-units = 1
debug = 2
debug-assertions = true # <-
incremental = false
opt-level = 3 # <-
overflow-checks = true # <-
# cargo build/run --release
[profile.release]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
lto = 'fat'
opt-level = 3 # <-
overflow-checks = false # <-
# cargo test --release
[profile.bench]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
lto = 'fat'
opt-level = 3 # <-
overflow-checks = false # <-

View File

@ -1,118 +0,0 @@
sat-rs example for the STM32H73ZI-Nucleo board
=======
This example application shows how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
can be used on an embedded target.
It also shows how a relatively simple OBSW could be built when no standard runtime is available.
It uses [RTIC](https://rtic.rs/2/book/en/) as the concurrency framework and the
[defmt](https://defmt.ferrous-systems.com/) framework for logging.
The STM32H743ZIT device was picked because it is one of the more powerful Cortex-M based devices
available for STM with which also has a little bit more RAM available and also allows commanding
via TCP/IP.
## Pre-Requisites
Make sure the following tools are installed:
1. [`probe-rs`](https://probe.rs/): Application used to flash and debug the MCU.
2. Optional and recommended: [VS Code](https://code.visualstudio.com/) with
[probe-rs plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger)
for debugging.
## Preparing Rust and the repository
Building an application requires the `thumbv7em-none-eabihf` cross-compiler toolchain.
If you have not installed it yet, you can do so with
```sh
rustup target add thumbv7em-none-eabihf
```
A default `.cargo` config file is provided for this project, but needs to be copied to have
the correct name. This is so that the config file can be updated or edited for custom needs
without being tracked by git.
```sh
cp def_config.toml config.toml
```
The configuration file will also set the target so it does not always have to be specified with
the `--target` argument.
## Building
After that, assuming that you have a `.cargo/config.toml` setting the correct build target,
you can simply build the application with
```sh
cargo build
```
## Flashing from the command line
You can flash the application from the command line using `probe-rs`:
```sh
probe-rs run --chip STM32H743ZITx
```
## Debugging with VS Code
The STM32F3-Discovery comes with an on-board ST-Link so all that is required to flash and debug
the board is a Mini-USB cable. The code in this repository was debugged using [`probe-rs`](https://probe.rs/docs/tools/debuggerA)
and the VS Code [`probe-rs` plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger).
Make sure to install this plugin first.
Sample configuration files are provided inside the `vscode` folder.
Use `cp vscode .vscode -r` to use them for your project.
Some sample configuration files for VS Code were provided as well. You can simply use `Run` and `Debug`
to automatically rebuild and flash your application.
The `tasks.json` and `launch.json` files are generic and you can use them immediately by opening
the folder in VS code or adding it to a workspace.
## Commanding with Python
When the SW is running on the Discovery board, you can command the MCU via a serial interface,
using COBS encoded PUS packets.
It is recommended to use a virtual environment to do this. To set up one in the command line,
you can use `python3 -m venv venv` on Unix systems or `py -m venv venv` on Windows systems.
After doing this, you can check the [venv tutorial](https://docs.python.org/3/tutorial/venv.html)
on how to activate the environment and then use the following command to install the required
dependency:
```sh
pip install -r requirements.txt
```
The packets are exchanged using a dedicated serial interface. You can use any generic USB-to-UART
converter device with the TX pin connected to the PA3 pin and the RX pin connected to the PA2 pin.
A default configuration file for the python application is provided and can be used by running
```sh
cp def_tmtc_conf.json tmtc_conf.json
```
After that, you can for example send a ping to the MCU using the following command
```sh
./main.py -p /ping
```
You can configure the blinky frequency using
```sh
./main.py -p /change_blink_freq
```
All these commands will package a PUS telecommand which will be sent to the MCU using the COBS
format as the packet framing format.
## Resources
- [STM32H743ZI Ethernet link checker example](https://github.com/stm32-rs/stm32h7xx-hal/blob/master/examples/ethernet-nucleo-h743zi2.rs)
- [smoltcp DHCP client](https://github.com/smoltcp-rs/smoltcp/blob/main/examples/dhcp_client.rs)

File diff suppressed because it is too large Load Diff

View File

@ -1,119 +0,0 @@
/* Taken from https://github.com/stm32-rs/stm32h7xx-hal/pull/299, adapted slightly to work with */
/* flip-link */
MEMORY
{
/* This file is intended for parts in the STM32H743/743v/753/753v families (RM0433), */
/* with the exception of the STM32H742/742v parts which have a different RAM layout. */
/* - FLASH and RAM are mandatory memory sections. */
/* - The sum of all non-FLASH sections must add to 1060K total device RAM. */
/* - The FLASH section size must match your device, see table below. */
/* FLASH */
/* Flash is divided in two independent banks (except 750xB). */
/* Select the appropriate FLASH size for your device. */
/* - STM32H750xB 128K (only FLASH1) */
/* - STM32H750xB 1M (512K + 512K) */
/* - STM32H743xI/753xI 2M ( 1M + 1M) */
FLASH1 : ORIGIN = 0x08000000, LENGTH = 1M
FLASH2 : ORIGIN = 0x08100000, LENGTH = 1M
/* Data TCM */
/* - Two contiguous 64KB RAMs. */
/* - Used for interrupt handlers, stacks and general RAM. */
/* - Zero wait-states. */
/* - The DTCM is taken as the origin of the base ram. (See below.) */
/* This is also where the interrupt table and such will live, */
/* which is required for deterministic performance. */
/* Need a region called RAM */
/* DTCM : ORIGIN = 0x20000000, LENGTH = 128K */
RAM : ORIGIN = 0x20000000, LENGTH = 128K
/* Instruction TCM */
/* - Used for latency-critical interrupt handlers etc. */
/* - Zero wait-states. */
ITCM : ORIGIN = 0x00000000, LENGTH = 64K
/* AXI SRAM */
/* - AXISRAM is in D1 and accessible by all system masters except BDMA. */
/* - Suitable for application data not stored in DTCM. */
/* - Zero wait-states. */
AXISRAM : ORIGIN = 0x24000000, LENGTH = 512K
/* AHB SRAM */
/* - SRAM1-3 are in D2 and accessible by all system masters except BDMA. */
/* Suitable for use as DMA buffers. */
/* - SRAM4 is in D3 and additionally accessible by the BDMA. Used for BDMA */
/* buffers, for storing application data in lower-power modes. */
/* - Zero wait-states. */
SRAM1 : ORIGIN = 0x30000000, LENGTH = 128K
SRAM2 : ORIGIN = 0x30020000, LENGTH = 128K
SRAM3 : ORIGIN = 0x30040000, LENGTH = 32K
SRAM4 : ORIGIN = 0x38000000, LENGTH = 64K
/* Backup SRAM */
BSRAM : ORIGIN = 0x38800000, LENGTH = 4K
}
/*
/* Assign the memory regions defined above for use. */
/*
/* Provide the mandatory FLASH and RAM definitions for cortex-m-rt's linker script. */
/* These do not work with flip-link */
REGION_ALIAS(FLASH, FLASH1);
/* REGION_ALIAS(RAM, DTCM); */
/* The location of the stack can be overridden using the `_stack_start` symbol. */
/* - Set the stack location at the end of RAM, using all remaining space. */
_stack_start = ORIGIN(RAM) + LENGTH(RAM);
/* The location of the .text section can be overridden using the */
/* `_stext` symbol. By default it will place after .vector_table. */
/* _stext = ORIGIN(FLASH) + 0x40c; */
/* Define sections for placing symbols into the extra memory regions above. */
/* This makes them accessible from code. */
/* - ITCM, DTCM and AXISRAM connect to a 64-bit wide bus -> align to 8 bytes. */
/* - All other memories connect to a 32-bit wide bus -> align to 4 bytes. */
SECTIONS {
.flash2 (NOLOAD) : ALIGN(4) {
*(.flash2 .flash2.*);
. = ALIGN(4);
} > FLASH2
.itcm (NOLOAD) : ALIGN(8) {
*(.itcm .itcm.*);
. = ALIGN(8);
} > ITCM
.axisram (NOLOAD) : ALIGN(8) {
*(.axisram .axisram.*);
. = ALIGN(8);
} > AXISRAM
.sram1 (NOLOAD) : ALIGN(8) {
*(.sram1 .sram1.*);
. = ALIGN(4);
} > SRAM1
.sram2 (NOLOAD) : ALIGN(8) {
*(.sram2 .sram2.*);
. = ALIGN(4);
} > SRAM2
.sram3 (NOLOAD) : ALIGN(4) {
*(.sram3 .sram3.*);
. = ALIGN(4);
} > SRAM3
.sram4 (NOLOAD) : ALIGN(4) {
*(.sram4 .sram4.*);
. = ALIGN(4);
} > SRAM4
.bsram (NOLOAD) : ALIGN(4) {
*(.bsram .bsram.*);
. = ALIGN(4);
} > BSRAM
};

View File

@ -1,4 +0,0 @@
{
"com_if": "udp",
"tcpip_udp_port": 7301
}

View File

@ -1,2 +0,0 @@
tmtccmd == 8.0.1
# -e git+https://github.com/robamu-org/tmtccmd.git@main#egg=tmtccmd

View File

@ -1,55 +0,0 @@
//! Blinks an LED
//!
//! This assumes that LD2 (blue) is connected to pb7 and LD3 (red) is connected
//! to pb14. This assumption is true for the nucleo-h743zi board.
#![no_std]
#![no_main]
use satrs_stm32h7_nucleo_rtic as _;
use stm32h7xx_hal::{block, prelude::*, timer::Timer};
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
defmt::println!("starting stm32h7 blinky example");
// Get access to the device specific peripherals from the peripheral access crate
let dp = stm32h7xx_hal::stm32::Peripherals::take().unwrap();
// Take ownership over the RCC devices and convert them into the corresponding HAL structs
let rcc = dp.RCC.constrain();
let pwr = dp.PWR.constrain();
let pwrcfg = pwr.freeze();
// Freeze the configuration of all the clocks in the system and
// retrieve the Core Clock Distribution and Reset (CCDR) object
let rcc = rcc.use_hse(8.MHz()).bypass_hse();
let ccdr = rcc.freeze(pwrcfg, &dp.SYSCFG);
// Acquire the GPIOB peripheral
let gpiob = dp.GPIOB.split(ccdr.peripheral.GPIOB);
// Configure gpio B pin 0 as a push-pull output.
let mut ld1 = gpiob.pb0.into_push_pull_output();
// Configure gpio B pin 7 as a push-pull output.
let mut ld2 = gpiob.pb7.into_push_pull_output();
// Configure gpio B pin 14 as a push-pull output.
let mut ld3 = gpiob.pb14.into_push_pull_output();
// Configure the timer to trigger an update every second
let mut timer = Timer::tim1(dp.TIM1, ccdr.peripheral.TIM1, &ccdr.clocks);
timer.start(1.Hz());
// Wait for the timer to trigger an update and change the state of the LED
loop {
ld1.toggle();
ld2.toggle();
ld3.toggle();
block!(timer.wait()).unwrap();
}
}

View File

@ -1,11 +0,0 @@
#![no_main]
#![no_std]
use satrs_stm32h7_nucleo_rtic as _; // global logger + panicking-behavior + memory layout
#[cortex_m_rt::entry]
fn main() -> ! {
defmt::println!("Hello, world!");
satrs_stm32h7_nucleo_rtic::exit()
}

View File

@ -1,52 +0,0 @@
#![no_main]
#![no_std]
use cortex_m_semihosting::debug;
use defmt_brtt as _; // global logger
// TODO(5) adjust HAL import
use stm32h7xx_hal as _; // memory layout
use panic_probe as _;
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
// this prevents the panic message being printed *twice* when `defmt::panic` is invoked
#[defmt::panic_handler]
fn panic() -> ! {
cortex_m::asm::udf()
}
/// Terminates the application and makes a semihosting-capable debug tool exit
/// with status code 0.
pub fn exit() -> ! {
loop {
debug::exit(debug::EXIT_SUCCESS);
}
}
/// Hardfault handler.
///
/// Terminates the application and makes a semihosting-capable debug tool exit
/// with an error. This seems better than the default, which is to spin in a
/// loop.
#[cortex_m_rt::exception]
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
loop {
debug::exit(debug::EXIT_FAILURE);
}
}
// defmt-test 0.3.0 has the limitation that this `#[tests]` attribute can only be used
// once within a crate. the module can be in any file but there can only be at most
// one `#[tests]` module in this library crate
#[cfg(test)]
#[defmt_test::tests]
mod unit_tests {
use defmt::assert;
#[test]
fn it_works() {
assert!(true)
}
}

View File

@ -1,528 +0,0 @@
#![no_main]
#![no_std]
extern crate alloc;
use rtic::app;
use rtic_monotonics::systick::Systick;
use rtic_monotonics::Monotonic;
use satrs::pool::{PoolAddr, PoolProvider, StaticHeaplessMemoryPool};
use satrs::static_subpool;
// global logger + panicking-behavior + memory layout
use satrs_stm32h7_nucleo_rtic as _;
use smoltcp::socket::udp::UdpMetadata;
use smoltcp::socket::{dhcpv4, udp};
use core::mem::MaybeUninit;
use embedded_alloc::Heap;
use smoltcp::iface::{Config, Interface, SocketHandle, SocketSet, SocketStorage};
use smoltcp::wire::{HardwareAddress, IpAddress, IpCidr};
use stm32h7xx_hal::ethernet;
const DEFAULT_BLINK_FREQ_MS: u32 = 1000;
const PORT: u16 = 7301;
const HEAP_SIZE: usize = 131_072;
const TC_SOURCE_CHANNEL_DEPTH: usize = 16;
pub type SharedPool = StaticHeaplessMemoryPool<3>;
pub type TcSourceChannel = rtic_sync::channel::Channel<PoolAddr, TC_SOURCE_CHANNEL_DEPTH>;
pub type TcSourceTx = rtic_sync::channel::Sender<'static, PoolAddr, TC_SOURCE_CHANNEL_DEPTH>;
pub type TcSourceRx = rtic_sync::channel::Receiver<'static, PoolAddr, TC_SOURCE_CHANNEL_DEPTH>;
#[global_allocator]
static HEAP: Heap = Heap::empty();
// We place the memory pool buffers inside the larger AXISRAM.
pub const SUBPOOL_SMALL_NUM_BLOCKS: u16 = 32;
pub const SUBPOOL_SMALL_BLOCK_SIZE: usize = 32;
pub const SUBPOOL_MEDIUM_NUM_BLOCKS: u16 = 16;
pub const SUBPOOL_MEDIUM_BLOCK_SIZE: usize = 128;
pub const SUBPOOL_LARGE_NUM_BLOCKS: u16 = 8;
pub const SUBPOOL_LARGE_BLOCK_SIZE: usize = 2048;
// This data will be held by Net through a mutable reference
pub struct NetStorageStatic<'a> {
socket_storage: [SocketStorage<'a>; 8],
}
// MaybeUninit allows us write code that is correct even if STORE is not
// initialised by the runtime
static mut STORE: MaybeUninit<NetStorageStatic> = MaybeUninit::uninit();
static mut UDP_RX_META: [udp::PacketMetadata; 12] = [udp::PacketMetadata::EMPTY; 12];
static mut UDP_RX: [u8; 2048] = [0; 2048];
static mut UDP_TX_META: [udp::PacketMetadata; 12] = [udp::PacketMetadata::EMPTY; 12];
static mut UDP_TX: [u8; 2048] = [0; 2048];
/// Locally administered MAC address
const MAC_ADDRESS: [u8; 6] = [0x02, 0x00, 0x11, 0x22, 0x33, 0x44];
pub struct Net {
iface: Interface,
ethdev: ethernet::EthernetDMA<4, 4>,
dhcp_handle: SocketHandle,
}
impl Net {
pub fn new(
sockets: &mut SocketSet<'static>,
mut ethdev: ethernet::EthernetDMA<4, 4>,
ethernet_addr: HardwareAddress,
) -> Self {
let config = Config::new(ethernet_addr);
let mut iface = Interface::new(
config,
&mut ethdev,
smoltcp::time::Instant::from_millis((Systick::now() - Systick::ZERO).to_millis()),
);
// Create sockets
let dhcp_socket = dhcpv4::Socket::new();
iface.update_ip_addrs(|addrs| {
let _ = addrs.push(IpCidr::new(IpAddress::v4(192, 168, 1, 99), 0));
});
let dhcp_handle = sockets.add(dhcp_socket);
Net {
iface,
ethdev,
dhcp_handle,
}
}
/// Polls on the ethernet interface. You should refer to the smoltcp
/// documentation for poll() to understand how to call poll efficiently
pub fn poll<'a>(&mut self, sockets: &'a mut SocketSet) -> bool {
let uptime = Systick::now() - Systick::ZERO;
let timestamp = smoltcp::time::Instant::from_millis(uptime.to_millis());
self.iface.poll(timestamp, &mut self.ethdev, sockets)
}
pub fn poll_dhcp<'a>(&mut self, sockets: &'a mut SocketSet) -> Option<dhcpv4::Event<'a>> {
let opt_event = sockets.get_mut::<dhcpv4::Socket>(self.dhcp_handle).poll();
if let Some(event) = &opt_event {
match event {
dhcpv4::Event::Deconfigured => {
defmt::info!("DHCP lost configuration");
self.iface.update_ip_addrs(|addrs| addrs.clear());
self.iface.routes_mut().remove_default_ipv4_route();
}
dhcpv4::Event::Configured(config) => {
defmt::info!("DHCP configuration acquired");
defmt::info!("IP address: {}", config.address);
self.iface.update_ip_addrs(|addrs| {
addrs.clear();
addrs.push(IpCidr::Ipv4(config.address)).unwrap();
});
if let Some(router) = config.router {
defmt::debug!("Default gateway: {}", router);
self.iface
.routes_mut()
.add_default_ipv4_route(router)
.unwrap();
} else {
defmt::debug!("Default gateway: None");
self.iface.routes_mut().remove_default_ipv4_route();
}
}
}
}
opt_event
}
}
pub struct UdpNet {
udp_handle: SocketHandle,
last_client: Option<UdpMetadata>,
tc_source_tx: TcSourceTx,
}
impl UdpNet {
pub fn new<'sockets>(sockets: &mut SocketSet<'sockets>, tc_source_tx: TcSourceTx) -> Self {
// SAFETY: The RX and TX buffers are passed here and not used anywhere else.
let udp_rx_buffer =
smoltcp::socket::udp::PacketBuffer::new(unsafe { &mut UDP_RX_META[..] }, unsafe {
&mut UDP_RX[..]
});
let udp_tx_buffer =
smoltcp::socket::udp::PacketBuffer::new(unsafe { &mut UDP_TX_META[..] }, unsafe {
&mut UDP_TX[..]
});
let udp_socket = smoltcp::socket::udp::Socket::new(udp_rx_buffer, udp_tx_buffer);
let udp_handle = sockets.add(udp_socket);
Self {
udp_handle,
last_client: None,
tc_source_tx,
}
}
pub fn poll<'sockets>(
&mut self,
sockets: &'sockets mut SocketSet,
shared_pool: &mut SharedPool,
) {
let socket = sockets.get_mut::<udp::Socket>(self.udp_handle);
if !socket.is_open() {
if let Err(e) = socket.bind(PORT) {
defmt::warn!("binding UDP socket failed: {}", e);
}
}
loop {
match socket.recv() {
Ok((data, client)) => {
match shared_pool.add(data) {
Ok(store_addr) => {
if let Err(e) = self.tc_source_tx.try_send(store_addr) {
defmt::warn!("TC source channel is full: {}", e);
}
}
Err(e) => {
defmt::warn!("could not add UDP packet to shared pool: {}", e);
}
}
self.last_client = Some(client);
// TODO: Implement packet wiretapping.
}
Err(e) => match e {
udp::RecvError::Exhausted => {
break;
}
udp::RecvError::Truncated => {
defmt::warn!("UDP packet was truncacted");
}
},
};
}
}
}
#[app(device = stm32h7xx_hal::stm32, peripherals = true)]
mod app {
use core::ptr::addr_of_mut;
use super::*;
use rtic_monotonics::systick::fugit::MillisDurationU32;
use rtic_monotonics::systick::Systick;
use satrs::spacepackets::ecss::tc::PusTcReader;
use stm32h7xx_hal::ethernet::{EthernetMAC, PHY};
use stm32h7xx_hal::gpio::{Output, Pin};
use stm32h7xx_hal::prelude::*;
use stm32h7xx_hal::stm32::Interrupt;
struct BlinkyLeds {
led1: Pin<'B', 7, Output>,
led2: Pin<'B', 14, Output>,
}
#[local]
struct Local {
leds: BlinkyLeds,
link_led: Pin<'B', 0, Output>,
net: Net,
udp: UdpNet,
tc_source_rx: TcSourceRx,
phy: ethernet::phy::LAN8742A<EthernetMAC>,
}
#[shared]
struct Shared {
blink_freq: MillisDurationU32,
eth_link_up: bool,
sockets: SocketSet<'static>,
shared_pool: SharedPool,
}
#[init]
fn init(mut cx: init::Context) -> (Shared, Local) {
defmt::println!("Starting sat-rs demo application for the STM32H743ZIT");
let pwr = cx.device.PWR.constrain();
let pwrcfg = pwr.freeze();
let rcc = cx.device.RCC.constrain();
// Try to keep the clock configuration similar to one used in STM examples:
// https://github.com/STMicroelectronics/STM32CubeH7/blob/master/Projects/NUCLEO-H743ZI/Examples/GPIO/GPIO_EXTI/Src/main.c
let ccdr = rcc
.sys_ck(400.MHz())
.hclk(200.MHz())
.use_hse(8.MHz())
.bypass_hse()
.pclk1(100.MHz())
.pclk2(100.MHz())
.pclk3(100.MHz())
.pclk4(100.MHz())
.freeze(pwrcfg, &cx.device.SYSCFG);
// Initialize the systick interrupt & obtain the token to prove that we did
let systick_mono_token = rtic_monotonics::create_systick_token!();
Systick::start(
cx.core.SYST,
ccdr.clocks.sys_ck().to_Hz(),
systick_mono_token,
);
// Those are used in the smoltcp of the stm32h7xx-hal , I am not fully sure what they are
// good for.
cx.core.SCB.enable_icache();
cx.core.DWT.enable_cycle_counter();
let gpioa = cx.device.GPIOA.split(ccdr.peripheral.GPIOA);
let gpiob = cx.device.GPIOB.split(ccdr.peripheral.GPIOB);
let gpioc = cx.device.GPIOC.split(ccdr.peripheral.GPIOC);
let gpiog = cx.device.GPIOG.split(ccdr.peripheral.GPIOG);
let link_led = gpiob.pb0.into_push_pull_output();
let mut led1 = gpiob.pb7.into_push_pull_output();
let mut led2 = gpiob.pb14.into_push_pull_output();
// Criss-cross pattern looks cooler.
led1.set_high();
led2.set_low();
let leds = BlinkyLeds { led1, led2 };
let rmii_ref_clk = gpioa.pa1.into_alternate::<11>();
let rmii_mdio = gpioa.pa2.into_alternate::<11>();
let rmii_mdc = gpioc.pc1.into_alternate::<11>();
let rmii_crs_dv = gpioa.pa7.into_alternate::<11>();
let rmii_rxd0 = gpioc.pc4.into_alternate::<11>();
let rmii_rxd1 = gpioc.pc5.into_alternate::<11>();
let rmii_tx_en = gpiog.pg11.into_alternate::<11>();
let rmii_txd0 = gpiog.pg13.into_alternate::<11>();
let rmii_txd1 = gpiob.pb13.into_alternate::<11>();
let mac_addr = smoltcp::wire::EthernetAddress::from_bytes(&MAC_ADDRESS);
/// Ethernet descriptor rings are a global singleton
#[link_section = ".sram3.eth"]
static mut DES_RING: MaybeUninit<ethernet::DesRing<4, 4>> = MaybeUninit::uninit();
let (eth_dma, eth_mac) = ethernet::new(
cx.device.ETHERNET_MAC,
cx.device.ETHERNET_MTL,
cx.device.ETHERNET_DMA,
(
rmii_ref_clk,
rmii_mdio,
rmii_mdc,
rmii_crs_dv,
rmii_rxd0,
rmii_rxd1,
rmii_tx_en,
rmii_txd0,
rmii_txd1,
),
// SAFETY: We do not move the returned DMA struct across thread boundaries, so this
// should be safe according to the docs.
unsafe { DES_RING.assume_init_mut() },
mac_addr,
ccdr.peripheral.ETH1MAC,
&ccdr.clocks,
);
// Initialise ethernet PHY...
let mut lan8742a = ethernet::phy::LAN8742A::new(eth_mac.set_phy_addr(0));
lan8742a.phy_reset();
lan8742a.phy_init();
unsafe {
ethernet::enable_interrupt();
cx.core.NVIC.set_priority(Interrupt::ETH, 196); // Mid prio
cortex_m::peripheral::NVIC::unmask(Interrupt::ETH);
}
// unsafe: mutable reference to static storage, we only do this once
let store = unsafe {
let store_ptr = STORE.as_mut_ptr();
// Initialise the socket_storage field. Using `write` instead of
// assignment via `=` to not call `drop` on the old, uninitialised
// value
addr_of_mut!((*store_ptr).socket_storage).write([SocketStorage::EMPTY; 8]);
// Now that all fields are initialised we can safely use
// assume_init_mut to return a mutable reference to STORE
STORE.assume_init_mut()
};
let (tc_source_tx, tc_source_rx) =
rtic_sync::make_channel!(PoolAddr, TC_SOURCE_CHANNEL_DEPTH);
let mut sockets = SocketSet::new(&mut store.socket_storage[..]);
let net = Net::new(&mut sockets, eth_dma, mac_addr.into());
let udp = UdpNet::new(&mut sockets, tc_source_tx);
let mut shared_pool: SharedPool = StaticHeaplessMemoryPool::new(true);
static_subpool!(
SUBPOOL_SMALL,
SUBPOOL_SMALL_SIZES,
SUBPOOL_SMALL_NUM_BLOCKS as usize,
SUBPOOL_SMALL_BLOCK_SIZE,
link_section = ".axisram"
);
static_subpool!(
SUBPOOL_MEDIUM,
SUBPOOL_MEDIUM_SIZES,
SUBPOOL_MEDIUM_NUM_BLOCKS as usize,
SUBPOOL_MEDIUM_BLOCK_SIZE,
link_section = ".axisram"
);
static_subpool!(
SUBPOOL_LARGE,
SUBPOOL_LARGE_SIZES,
SUBPOOL_LARGE_NUM_BLOCKS as usize,
SUBPOOL_LARGE_BLOCK_SIZE,
link_section = ".axisram"
);
shared_pool
.grow(
unsafe { SUBPOOL_SMALL.assume_init_mut() },
unsafe { SUBPOOL_SMALL_SIZES.assume_init_mut() },
SUBPOOL_SMALL_NUM_BLOCKS,
true,
)
.expect("growing heapless memory pool failed");
shared_pool
.grow(
unsafe { SUBPOOL_MEDIUM.assume_init_mut() },
unsafe { SUBPOOL_MEDIUM_SIZES.assume_init_mut() },
SUBPOOL_MEDIUM_NUM_BLOCKS,
true,
)
.expect("growing heapless memory pool failed");
shared_pool
.grow(
unsafe { SUBPOOL_LARGE.assume_init_mut() },
unsafe { SUBPOOL_LARGE_SIZES.assume_init_mut() },
SUBPOOL_LARGE_NUM_BLOCKS,
true,
)
.expect("growing heapless memory pool failed");
// Set up global allocator. Use AXISRAM for the heap.
#[link_section = ".axisram"]
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
eth_link_check::spawn().expect("eth link check failed");
blinky::spawn().expect("spawning blink task failed");
udp_task::spawn().expect("spawning UDP task failed");
tc_source_task::spawn().expect("spawning TC source task failed");
(
Shared {
blink_freq: MillisDurationU32::from_ticks(DEFAULT_BLINK_FREQ_MS),
eth_link_up: false,
sockets,
shared_pool,
},
Local {
link_led,
leds,
net,
udp,
tc_source_rx,
phy: lan8742a,
},
)
}
#[task(local = [leds], shared=[blink_freq])]
async fn blinky(mut cx: blinky::Context) {
let leds = cx.local.leds;
loop {
leds.led1.toggle();
leds.led2.toggle();
let current_blink_freq = cx.shared.blink_freq.lock(|current| *current);
Systick::delay(current_blink_freq).await;
}
}
/// This task checks for the network link.
#[task(local=[link_led, phy], shared=[eth_link_up])]
async fn eth_link_check(mut cx: eth_link_check::Context) {
let phy = cx.local.phy;
let link_led = cx.local.link_led;
loop {
let link_was_up = cx.shared.eth_link_up.lock(|link_up| *link_up);
if phy.poll_link() {
if !link_was_up {
link_led.set_high();
cx.shared.eth_link_up.lock(|link_up| *link_up = true);
defmt::info!("Ethernet link up");
}
} else if link_was_up {
link_led.set_low();
cx.shared.eth_link_up.lock(|link_up| *link_up = false);
defmt::info!("Ethernet link down");
}
Systick::delay(100.millis()).await;
}
}
#[task(binds=ETH, local=[net], shared=[sockets])]
fn eth_isr(mut cx: eth_isr::Context) {
// SAFETY: We do not write the register mentioned inside the docs anywhere else.
unsafe {
ethernet::interrupt_handler();
}
// Check and process ETH frames and DHCP. UDP is checked in a different task.
cx.shared.sockets.lock(|sockets| {
cx.local.net.poll(sockets);
cx.local.net.poll_dhcp(sockets);
});
}
/// This task routes UDP packets.
#[task(local=[udp], shared=[sockets, shared_pool])]
async fn udp_task(mut cx: udp_task::Context) {
loop {
cx.shared.sockets.lock(|sockets| {
cx.shared.shared_pool.lock(|pool| {
cx.local.udp.poll(sockets, pool);
})
});
Systick::delay(40.millis()).await;
}
}
/// This task handles all the incoming telecommands.
#[task(local=[read_buf: [u8; 1024] = [0; 1024], tc_source_rx], shared=[shared_pool])]
async fn tc_source_task(mut cx: tc_source_task::Context) {
loop {
let recv_result = cx.local.tc_source_rx.recv().await;
match recv_result {
Ok(pool_addr) => {
cx.shared.shared_pool.lock(|pool| {
match pool.read(&pool_addr, cx.local.read_buf.as_mut()) {
Ok(packet_len) => {
defmt::info!("received {} bytes in the TC source task", packet_len);
match PusTcReader::new(&cx.local.read_buf[0..packet_len]) {
Ok((packet, _tc_len)) => {
// TODO: Handle packet here or dispatch to dedicated PUS
// handler? Dispatching could simplify some things and make
// the software more scalable..
defmt::info!("received PUS packet: {}", packet);
}
Err(e) => {
defmt::info!("invalid TC format, not a PUS packet: {}", e);
}
}
if let Err(e) = pool.delete(pool_addr) {
defmt::warn!("deleting TC data failed: {}", e);
}
}
Err(e) => {
defmt::warn!("TC packet read failed: {}", e);
}
}
});
}
Err(e) => {
defmt::warn!("TC source reception error: {}", e);
}
};
}
}
}

View File

@ -1,16 +0,0 @@
#![no_std]
#![no_main]
use stm32h7_testapp as _; // memory layout + panic handler
// See https://crates.io/crates/defmt-test/0.3.0 for more documentation (e.g. about the 'state'
// feature)
#[defmt_test::tests]
mod tests {
use defmt::assert;
#[test]
fn it_works() {
assert!(true)
}
}

View File

@ -1,2 +0,0 @@
/settings.json
/.cortex-debug.*

View File

@ -1,12 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"rust-lang.rust",
"probe-rs.probe-rs-debugger"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []
}

View File

@ -1,22 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"preLaunchTask": "${defaultBuildTask}",
"type": "probe-rs-debug",
"request": "launch",
"name": "probe-rs Debugging ",
"flashingConfig": {
"flashingEnabled": true
},
"chip": "STM32H743ZITx",
"coreConfigs": [
{
"programBinary": "${workspaceFolder}/target/thumbv7em-none-eabihf/debug/satrs-stm32h7-nucleo-rtic",
"rttEnabled": true,
"svdFile": "STM32H743.svd"
}
]
}
]
}

View File

@ -1,20 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "cargo build",
"type": "shell",
"command": "~/.cargo/bin/cargo", // note: full path to the cargo
"args": [
"build"
],
"group": {
"kind": "build",
"isDefault": true
}
},
]
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

View File

@ -1,260 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
<!--Created by yEd 3.23.2-->
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
<key for="port" id="d1" yfiles.type="portgraphics"/>
<key for="port" id="d2" yfiles.type="portgeometry"/>
<key for="port" id="d3" yfiles.type="portuserdata"/>
<key attr.name="url" attr.type="string" for="node" id="d4"/>
<key attr.name="description" attr.type="string" for="node" id="d5"/>
<key for="node" id="d6" yfiles.type="nodegraphics"/>
<key for="graphml" id="d7" yfiles.type="resources"/>
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
<graph edgedefault="directed" id="G">
<data key="d0" xml:space="preserve"/>
<node id="n0">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="360.0" width="479.0" x="771.3047672479152" y="458.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" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="237.5" y="178.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="n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="177.64799999999997" width="200.75199999999973" x="1037.5527672479152" y="470.15200000000027"/>
<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.919921875" x="13.264464667588754" xml:space="preserve" y="8.302185845943427">Simulation<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.433926114471642" nodeRatioY="-0.45326608886143704" 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="34.0" width="84.39999999999986" x="1068.8351781652768" y="508.2800000000002"/>
<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="37.638671875" x="23.380664062499818" xml:space="preserve" y="8.015625">PCDU<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="34.0" width="120.39999999999986" x="1068.8351781652768" y="550.4800000000001"/>
<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="92.453125" x="13.973437499999818" xml:space="preserve" y="8.015625">Magnetometer<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="34.0" width="120.39999999999986" x="1068.8351781652768" y="594.9000000000001"/>
<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="88.83203125" x="15.783984374999818" xml:space="preserve" y="8.015625">Magnetorquer<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="34.0" width="120.39999999999986" x="783.4063563305535" y="545.2800000000002"/>
<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="85.931640625" x="17.234179687499932" xml:space="preserve" y="8.015625">SimController<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="34.0" width="120.39999999999986" x="840.5407126611072" y="677.8000000000002"/>
<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="105.05078125" x="7.674609374999932" xml:space="preserve" y="8.015625">UDP TC 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="n7">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="34.0" width="120.39999999999986" x="1005.2814253222144" y="677.8000000000002"/>
<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="97.111328125" x="11.644335937499932" xml:space="preserve" y="8.015625">UDP TM Sender<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="34.0" width="120.39999999999986" x="931.6174253222144" y="775.5920000000002"/>
<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="38.740234375" x="40.82988281249993" xml:space="preserve" y="8.015625">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>
<edge id="e0" source="n5" target="n3">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="-5.199999999999932"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e1" source="n5" target="n2">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="60.19999999999993" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="1023.8695890826383" y="562.2800000000002"/>
<y:Point x="1023.8695890826383" y="525.2800000000002"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e2" source="n5" target="n4">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="48.72964366944643" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="1023.8695890826383" y="562.2800000000002"/>
<y:Point x="1023.8695890826383" y="611.9000000000001"/>
</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="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.955078125" x="12.686124396959713" xml:space="preserve" y="-22.50440429687478">schedule_event<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="13.519999999999978" distanceToCenter="true" position="left" ratio="0.11621274698385183" 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="e3" source="n6" target="n5">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="-5.329643669446341" ty="0.0">
<y:Point x="900.7407126611072" y="628.5400000000002"/>
<y:Point x="838.2767126611071" y="628.5400000000002"/>
</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="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="75.923828125" x="-87.89792405764274" xml:space="preserve" y="-40.606550292968564">SimRequest<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="49.935999999999936" distanceToCenter="true" position="left" 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" target="n7">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="60.200000000000045" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="1223.8814253222142" y="611.9000000000001"/>
<y:Point x="1223.8814253222142" y="694.8000000000002"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e5" source="n3" target="n7">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="1223.8814253222142" y="567.4800000000001"/>
<y:Point x="1223.8814253222142" y="694.8000000000002"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e6" source="n2" target="n7">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="11.514125426161627" sy="-2.5781798912005343" tx="45.553752843062284" ty="0.0">
<y:Point x="1223.8814253222142" y="522.7018201087997"/>
<y:Point x="1223.8814253222142" y="694.8000000000002"/>
</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="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="60.4140625" x="-2.4087265765670054" xml:space="preserve" y="145.1356018470808">SimReply<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="17.97817989120062" distanceToCenter="true" position="right" ratio="0.679561684469248" 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="e7" source="n8" target="n6">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-25.212712661107275" sy="0.0" tx="-11.264000000000124" ty="0.0">
<y:Point x="966.6047126611071" y="731.8000000000002"/>
<y:Point x="889.4767126611071" y="731.8000000000002"/>
</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="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="119.751953125" x="-132.27600022951788" xml:space="preserve" y="-32.03587548828091">SimRequest in UDP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="20.73181250000017" distanceToCenter="true" position="left" ratio="0.9386993050513424" 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="n7" target="n8">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="29.18399999999997" sy="0.0" tx="24.28800000000001" ty="0.0">
<y:Point x="1094.6654253222143" y="731.8000000000002"/>
<y:Point x="1016.1054253222144" y="731.8000000000002"/>
</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="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="104.2421875" x="-62.15307370122309" xml:space="preserve" y="34.80927001953137">SimReply in UDP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="23.81218750000005" distanceToCenter="true" position="left" ratio="0.12769857433808468" 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="e9" source="n5" target="n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="23.921741802203996" sy="-3.0501798912007416" tx="0.0" ty="-56.27417989120056">
<y:Point x="867.5280981327575" y="502.70182010879967"/>
</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="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="29.95703125" x="73.38950633588263" xml:space="preserve" y="-62.699758016200235">step<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="11.126187499999986" distanceToCenter="true" position="left" ratio="0.5889387894625147" 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>
</graph>
<data key="d7">
<y:Resources/>
</data>
</graphml>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

View File

@ -1,650 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
<!--Created by yEd 3.23.2-->
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
<key for="port" id="d1" yfiles.type="portgraphics"/>
<key for="port" id="d2" yfiles.type="portgeometry"/>
<key for="port" id="d3" yfiles.type="portuserdata"/>
<key attr.name="url" attr.type="string" for="node" id="d4"/>
<key attr.name="description" attr.type="string" for="node" id="d5"/>
<key for="node" id="d6" yfiles.type="nodegraphics"/>
<key for="graphml" id="d7" yfiles.type="resources"/>
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
<graph edgedefault="directed" id="G">
<data key="d0"/>
<node id="n0">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="157.79999999999998" width="452.0" x="959.3461887999997" y="585.7236400000005"/>
<y:Fill color2="#000000" 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="145.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="434.0" x="9.0" xml:space="preserve" y="6.399999999999977">&lt;html&gt;
&lt;center&gt;
&lt;h4&gt;ACS Mode Tree&lt;/h4&gt;
&lt;/center&gt;
&lt;table border="1"&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;MGMs&lt;/th&gt;
&lt;th&gt;SUSs&lt;/th&gt;
&lt;th&gt;STR&lt;/th&gt;
&lt;th&gt;MGT&lt;/th&gt;
&lt;th&gt;RWs&lt;/th&gt;
&lt;th&gt;ACS CTRL&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OFF&lt;/td&gt;
&lt;td&gt;OFF&lt;/td&gt;
&lt;td&gt;OFF&lt;/td&gt;
&lt;td&gt;OFF&lt;/td&gt;
&lt;td&gt;OFF&lt;/td&gt;
&lt;td&gt;OFF&lt;/td&gt;
&lt;td&gt;OFF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAFE&lt;/td&gt;
&lt;td&gt;NORMAL&lt;/td&gt;
&lt;td&gt;NORMAL&lt;/td&gt;
&lt;td&gt;OFF&lt;/td&gt;
&lt;td&gt;OFF&lt;/td&gt;
&lt;td&gt;NORMAL&lt;/td&gt;
&lt;td&gt;SAFE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDLE&lt;/td&gt;
&lt;td&gt;NORMAL&lt;/td&gt;
&lt;td&gt;NORMAL&lt;/td&gt;
&lt;td&gt;NORMAL&lt;/td&gt;
&lt;td&gt;NORMAL&lt;/td&gt;
&lt;td&gt;NORMAL&lt;/td&gt;
&lt;td&gt;IDLE&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/html&gt;<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="-1.1102230246251565E-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="n1">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="134.60000000000014" width="452.0" x="959.3461887999997" y="757.2703199999999"/>
<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="120.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="428.0" x="12.0" xml:space="preserve" y="7.300000000000068">&lt;html&gt;
&lt;center&gt;
&lt;h4&gt;ACS IDLE Sequence&lt;/h4&gt;
&lt;/center&gt;
&lt;table border="1"&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;MGMs&lt;/th&gt;
&lt;th&gt;SUS&lt;/th&gt;
&lt;th&gt;STR&lt;/th&gt;
&lt;th&gt;MGT&lt;/th&gt;
&lt;th&gt;RWs&lt;/th&gt;
&lt;th&gt;ACS CTRL&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;NORMAL&lt;/td&gt;
&lt;td&gt;NORMAL&lt;/td&gt;
&lt;td&gt;NORMAL&lt;/td&gt;
&lt;td&gt;NORMAL&lt;/td&gt;
&lt;td&gt;NORMAL&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;SAFE&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/html&gt;<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="32.400000000000034" width="146.79999999999995" x="1128.4" y="313.94999999999993"/>
<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="113.94921875" x="16.42539062500009" xml:space="preserve" y="6.0515625000000455">ACS 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="n3">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="40.0" width="80.39999999999998" x="910.7200000000004" y="407.20000000000005"/>
<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="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="61.837890625" x="9.281054687499932" xml:space="preserve" y="4.03125">MGM
Assembly<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="32.400000000000034" width="146.79999999999995" x="1133.6000000000001" y="243.3579999999999"/>
<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="117.99609375" x="14.401953124999864" xml:space="preserve" y="6.051562499999989">Satellite System<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="40.0" width="72.40000000000009" x="1021.1200000000003" y="404.99560000000014"/>
<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="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="61.837890625" x="5.2810546875000455" xml:space="preserve" y="4.03125">SUS
Assembly<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="40.0" width="67.59999999999991" x="1124.92" y="404.99560000000014"/>
<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="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="56.599609375" x="5.5001953124999545" xml:space="preserve" y="4.03125">STR
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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="40.0" width="67.59999999999991" x="1208.74" y="407.20000000000005"/>
<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="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="56.599609375" x="5.5001953124999545" xml:space="preserve" y="4.03125">MGT
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="n8">
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="40.0" width="72.40000000000009" x="1292.56" y="407.20000000000005"/>
<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="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="61.837890625" x="5.2810546875000455" xml:space="preserve" y="4.03125">RW
Assembly<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="40.0" width="67.59999999999991" x="1381.18" y="407.20000000000005"/>
<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="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="34.732421875" x="16.433789062499955" xml:space="preserve" y="4.03125">ACS
CTRL<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="44.719999999999914" x="946.4000000000004" y="463.3000000000002"/>
<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.640625" x="1.5396874999999" xml:space="preserve" y="1.0156250000000568">MGM0<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="44.719999999999914" x="946.4000000000004" y="491.32480000000066"/>
<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.640625" x="1.5396874999999" xml:space="preserve" y="1.015625">MGM1<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="44.719999999999914" x="946.4000000000004" y="519.3496000000011"/>
<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.640625" x="1.5396874999999" xml:space="preserve" y="1.015625">MGM2<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="44.719999999999914" x="1055.2600000000007" y="463.3000000000002"/>
<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.65234375" x="4.5338281249999" xml:space="preserve" y="1.015625">SUS0<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="44.719999999999914" x="1055.2600000000007" y="491.32480000000066"/>
<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="15.443359375" x="14.6383203124999" xml:space="preserve" y="1.015625">...<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="44.719999999999914" x="1055.2600000000007" y="519.3496000000011"/>
<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.287109375" x="0.7164453124999" xml:space="preserve" y="1.015625">SUS12<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="40.0" width="50.0" x="1129.9800000000005" y="482.79120000000023"/>
<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="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="45.0390625" x="2.48046875" xml:space="preserve" y="4.03125">STR
Device<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="40.0" width="50.0" x="1216.4400000000005" y="482.7912000000002"/>
<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="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="45.0390625" x="2.48046875" xml:space="preserve" y="4.031249999999943">MGT
Device<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="44.719999999999914" x="1341.6661887999999" y="454.7884800000007"/>
<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="31.837890625" x="6.4410546874999" xml:space="preserve" y="1.015625">RW0<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="44.719999999999914" x="1341.6661887999999" y="482.3769600000013"/>
<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="31.837890625" x="6.4410546874999" xml:space="preserve" y="1.015625">RW1<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="44.719999999999914" x="1341.6661887999999" y="512.3769600000013"/>
<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="31.837890625" x="6.4410546874999" xml:space="preserve" y="1.015625">RW2<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="44.719999999999914" x="1341.6661887999999" y="542.3769600000013"/>
<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="31.837890625" x="6.4410546874999" xml:space="preserve" y="1.015625">RW3<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="32.400000000000034" width="59.75999999999999" x="1305.2" y="313.94999999999993"/>
<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="17.3505859375" x="21.20470703125011" xml:space="preserve" y="6.051562499999989">...<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="n2">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-5.199999999999818" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e1" source="n2" target="n3">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-1.1700000000000728" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="1200.63" y="379.7"/>
<y:Point x="950.9200000000003" y="379.7"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e2" source="n2" target="n5">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-1.1700000000000728" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="1200.63" y="379.7"/>
<y:Point x="1057.3200000000004" y="379.7"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e3" source="n2" target="n6">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-1.1700000000000728" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="1200.63" y="379.7"/>
<y:Point x="1158.72" y="379.7"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e4" source="n2" target="n7">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-1.1700000000000728" sy="0.0" tx="-11.29999999999859" ty="0.0">
<y:Point x="1200.63" y="379.7"/>
<y:Point x="1231.2400000000014" y="379.7"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e5" source="n2" target="n8">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-1.1700000000000728" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="1200.63" y="379.7"/>
<y:Point x="1328.76" y="379.7"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e6" source="n2" target="n9">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-1.1700000000000728" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="1200.63" y="379.7"/>
<y:Point x="1414.98" y="379.7"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e7" source="n3" target="n10">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-28.009798234586242" sy="0.0" tx="13.11999999999989" ty="0.0">
<y:Point x="922.9102017654141" y="473.3000000000002"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e8" source="n3" target="n11">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-28.009798234586242" sy="17.706497359551577" tx="0.0" ty="0.0">
<y:Point x="922.9102017654141" y="501.32480000000066"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e9" source="n3" target="n12">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-28.009798234586242" sy="12.053248679775777" tx="0.0" ty="0.0">
<y:Point x="922.9102017654141" y="529.3496000000011"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e10" source="n5" target="n13">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-24.18711646917177" sy="11.599696230174459" tx="-20.855199999999513" ty="-1.9751999999995178">
<y:Point x="1033.1328835308286" y="471.32480000000066"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e11" source="n5" target="n14">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-24.18711646917177" sy="0.0" tx="-20.855199999999513" ty="-3.024800000000596">
<y:Point x="1033.1328835308286" y="498.30000000000007"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e12" source="n6" target="n16">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="3.7399999999995543" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e13" source="n7" target="n17">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="1.0999999999994543" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e14" source="n8" target="n18">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-23.248152938343992" sy="14.026599270174415" tx="-13.026188800000227" ty="0.33167708069714763">
<y:Point x="1305.511847061656" y="465.1201570806978"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e15" source="n8" target="n19">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-23.248152938343992" sy="0.0" tx="-13.026188800000227" ty="0.0">
<y:Point x="1305.511847061656" y="492.3769600000013"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e16" source="n8" target="n20">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-23.248152938343992" sy="0.0" tx="-13.026188800000227" ty="0.0">
<y:Point x="1305.511847061656" y="522.3769600000013"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e17" source="n8" target="n21">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-23.248152938343992" sy="0.0" tx="-13.026188800000227" ty="-1.1368683772161603E-13">
<y:Point x="1305.511847061656" y="552.3769600000012"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e18" source="n4" target="n22">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-5.199999999999818" sy="16.200000000000045" tx="0.0" ty="0.0">
<y:Point x="1201.8000000000002" y="290.43031999999994"/>
<y:Point x="1335.08" y="290.43031999999994"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e19" source="n5" target="n15">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-24.18711646917177" sy="0.0" tx="-20.855199999999513" ty="0.0">
<y:Point x="1033.1328835308286" y="529.3496000000011"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
</graph>
<data key="d7">
<y:Resources/>
</data>
</graphml>

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,285 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
<!--Created by yEd 3.23.2-->
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
<key for="port" id="d1" yfiles.type="portgraphics"/>
<key for="port" id="d2" yfiles.type="portgeometry"/>
<key for="port" id="d3" yfiles.type="portuserdata"/>
<key attr.name="url" attr.type="string" for="node" id="d4"/>
<key attr.name="description" attr.type="string" for="node" id="d5"/>
<key for="node" id="d6" yfiles.type="nodegraphics"/>
<key for="graphml" id="d7" yfiles.type="resources"/>
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
<graph edgedefault="directed" id="G">
<data key="d0" xml:space="preserve"/>
<node id="n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="261.7582999999998" width="631.1152000000001" x="810.8848" y="142.00000000000003"/>
<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="26.51035059931519" xml:space="preserve" y="9.42072533356739">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="217.68500000000003" width="166.9147999999998" x="819.9999999999999" y="180.434265625"/>
<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="166.9147999999998" x="13.181757989188668" xml:space="preserve" y="6.728692946816665">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="0" rightF="0.0" top="39" topF="38.90500000000003"/>
</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="136.9147999999998" x="834.9999999999999" y="234.33926562500002"/>
<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="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="113.94921875" x="11.482790624999893" xml:space="preserve" y="4.851562500000028">ACS 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="n1::n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="136.9147999999998" x="834.9999999999999" y="270.5687968750001"/>
<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="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="111.884765625" x="12.515017187499893" xml:space="preserve" y="4.8515625">EPS 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="n1::n2">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="136.9147999999998" x="834.9999999999999" y="306.84403125000006"/>
<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="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.296875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="112.923828125" x="11.995485937499893" xml:space="preserve" y="4.8515625">TCS 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="n1::n3">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="40.0" width="136.9147999999998" x="834.9999999999999" y="343.119265625"/>
<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="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.59375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="81.259765625" x="27.827517187499893" xml:space="preserve" y="1.703125">Payload
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>
</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="261.7582999999998" width="498.25116669136776" x="971.9147999999997" y="151.36096562500023"/>
<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="247.12558334568382" 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="7" leftF="7.00529999999992" right="3" rightF="3.313607121059931" 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="217.68500000000003" width="441.0158999999999" x="993.9200999999996" y="180.434265625"/>
<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="441.0158999999999" x="16.916359570308032" xml:space="preserve" y="5.969557999968089">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="22" bottomF="22.006468749999954" left="0" leftF="0.0" right="0" rightF="0.0" 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="1151.9280499999995" y="281.84403125000006"/>
<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="58.837890625" x="33.0810546875" xml:space="preserve" y="4.8515625">TM 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="n2::n0::n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="125.0" x="1151.9280499999995" y="330.5687968750001"/>
<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="105.7119140625" x="9.64404296875" xml:space="preserve" y="4.8515625">TCP/IP Servers<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="125.0" x="1294.9359999999995" y="281.84403125000006"/>
<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::n3">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="125.0" x="1008.9200999999996" y="223.11926562500005"/>
<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::n4">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="125.0" x="1008.9200999999996" y="267.1160312500001"/>
<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="117.7021484375" x="3.64892578125" xml:space="preserve" y="4.8515625">PUS Distribution<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="40.0" width="125.0" x="1151.9280499999995" y="223.11926562500005"/>
<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.7031249999999716">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="n2::n0::n6">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="50.0" width="125.0" x="1008.9200999999996" y="311.1127968750001"/>
<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="36.59375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.12890625" x="23.435546875" xml:space="preserve" y="6.703125">Satellite
Mode Tree<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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::n7">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="30.0" width="125.0" x="1294.9359999999995" y="223.11926562500005"/>
<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="74.7861328125" x="25.10693359375" xml:space="preserve" y="4.8515625"> PUS Stack<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" 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="n3">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="57.265600000000006" width="631.1152" x="810.8847999999999" y="411.39428125"/>
<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="41.25" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="261.8125" x="166.89412267941418" xml:space="preserve" y="3.144146301369915">satrs-minisim
Simulator based on asynchronix<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="-0.028136269449041573" nodeRatioY="-0.08493150684931505" 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="50.0" width="631.1152000000002" x="810.8847999999998" y="476.2958625000002"/>
<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="41.25" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="374.8359375" x="110.3824039294143" xml:space="preserve" y="0.12842465753431043">pytmtc
Command-line interface based TMTC handling<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="-0.028136269449041573" nodeRatioY="-0.08493150684931505" 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>

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,274 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
<!--Created by yEd 3.23.2-->
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
<key for="port" id="d1" yfiles.type="portgraphics"/>
<key for="port" id="d2" yfiles.type="portgeometry"/>
<key for="port" id="d3" yfiles.type="portuserdata"/>
<key attr.name="url" attr.type="string" for="node" id="d4"/>
<key attr.name="description" attr.type="string" for="node" id="d5"/>
<key for="node" id="d6" yfiles.type="nodegraphics"/>
<key for="graphml" id="d7" yfiles.type="resources"/>
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
<graph edgedefault="directed" id="G">
<data key="d0" xml:space="preserve"/>
<node id="n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="115.8239999999999" width="93.73760000000004" x="680.0096000000001" y="128.20672000000008"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.060546875" x="4.0" xml:space="preserve" y="7.633644259571071">spacepackets<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.5" nodeRatioY="-0.4340927246548981" offsetX="4.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="38.232" width="93.73760000000004" x="680.0096000000001" y="253.37632000000008"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="30.056640625" x="31.840479687499965" xml:space="preserve" y="10.131624999999985">cfdp<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n2">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="163.40159999999997" width="284.4319999999997" x="783.8771200000003" y="128.20672000000008"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="67.896484375" x="12.842478099131654" xml:space="preserve" y="10.580257051368562">sat-rs core<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.45484868756282143" nodeRatioY="-0.4352499788780002" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n3">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="75.46560000000002" width="82.73087999999973" x="985.5782400000003" y="128.20672000000008"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="27.91796875" x="5.868388937197551" xml:space="preserve" y="7.831944999999905">HAL<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.42906652344085205" nodeRatioY="-0.39621834319213123" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n4">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="57.03359999999975" x="998.4268800000001" y="165.02472000000006"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="41.763671875" x="7.634964062499762" xml:space="preserve" y="1.015625">TCP/IP<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n5">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="62.76800000000037" x="796.3430400000003" y="236.42112000000003"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="27.63671875" x="17.56564062500013" xml:space="preserve" y="1.015625">PUS<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n6">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="62.76800000000014" x="796.3430400000004" y="212.21056000000002"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="44.62890625" x="9.069546875000015" xml:space="preserve" y="1.015625">Events<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n7">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="80.30272000000002" x="969.0342399999995" y="260.6316800000001"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="40.708984375" x="19.796867812500068" xml:space="preserve" y="1.015625">Power<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n8">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="62.76800000000014" x="796.3430400000003" y="163.78943999999998"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="42.947265625" x="9.910367187500128" xml:space="preserve" y="1.015625">Modes<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n9">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="21.39839999999998" width="62.76800000000014" x="695.4944" y="164.06207999999998"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="45.232421875" x="8.767789062500015" xml:space="preserve" y="1.7148249999999905">CCSDS<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n10">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="21.39839999999998" width="62.76800000000014" x="695.4943999999999" y="198.76255999999998"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="35.1953125" x="13.786343750000128" xml:space="preserve" y="1.7148249999999905">ECSS<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n11">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="93.73760000000004" x="867.20384" y="163.78943999999998"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.62890625" x="20.05434687500008" xml:space="preserve" y="1.015625">Thermal<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n12">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="93.73760000000004" x="867.2038400000001" y="188.43504"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="89.494140625" x="2.1217296874999647" xml:space="preserve" y="1.015625">Housekeeping<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n13">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="50.0" width="104.03008000000023" x="1082.8518400000003" y="128.20672000000008"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="63.765625" x="22.802665047002165" xml:space="preserve" y="16.015624999999986">sat-rs MIB<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.025669859592552413" nodeRatioY="2.220446049250313E-16" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n14">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="93.73760000000016" x="867.20384" y="212.21056000000002"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="73.22265625" x="10.257471875000078" xml:space="preserve" y="1.015625">Parameters<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n15">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="80.30272000000025" x="969.0342399999994" y="236.42112000000003"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="29.25390625" x="25.52440687500018" xml:space="preserve" y="1.015625">Pool<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n16">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="93.73760000000004" x="867.2038400000001" y="260.6316800000001"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.392578125" x="28.172510937499965" xml:space="preserve" y="1.015625">TMTC<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n17">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="62.76800000000037" x="796.3430400000002" y="260.6316800000001"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="32.01953125" x="15.374234375000242" xml:space="preserve" y="1.015625">FDIR<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n18">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="50.0" width="104.03008000000023" x="1082.8518400000003" y="184.90752000000006"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="71.505859375" x="18.932547859502165" xml:space="preserve" y="16.015625">sat-rs Book<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.025669859592552413" nodeRatioY="2.220446049250313E-16" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n19">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="80.30272000000002" x="969.0342399999997" y="212.21056000000002"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="70.22265625" x="5.040031875000068" xml:space="preserve" y="1.015625">Subsystem<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n20">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="93.73760000000016" x="867.20384" y="236.42112000000003"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="48.044921875" x="22.84633906250008" xml:space="preserve" y="1.015625">Actions<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n21">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="20.0" width="62.76800000000014" x="796.3430400000004" y="188.0"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="43.404296875" x="9.681851562500015" xml:space="preserve" y="1.015625">Health<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n22">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="50.0" width="104.03008000000023" x="1082.8518400000003" y="241.60832000000005"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="93.701171875" x="7.834891609502165" xml:space="preserve" y="16.015625">sat-rs Example<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.025669859592552413" nodeRatioY="2.220446049250313E-16" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
</graph>
<data key="d7">
<y:Resources/>
</data>
</graphml>

View File

@ -1,607 +0,0 @@
%PDF-1.4
%âãÏÓ
1 0 obj
<<
/Title ()
/Author ()
/Subject ()
/Keywords ()
/Creator (yExport 1.5)
/Producer (org.freehep.graphicsio.pdf.YPDFGraphics2D 1.5)
/CreationDate (D:20240129115539+01'00')
/ModDate (D:20240129115539+01'00')
/Trapped /False
>>
endobj
2 0 obj
<<
/Type /Catalog
/Pages 3 0 R
/ViewerPreferences 4 0 R
/OpenAction [5 0 R /Fit]
>>
endobj
4 0 obj
<<
/FitWindow true
/CenterWindow false
>>
endobj
5 0 obj
<<
/Parent 3 0 R
/Type /Page
/Contents 6 0 R
>>
endobj
6 0 obj
<<
/Length 7 0 R
/Filter [/ASCII85Decode /FlateDecode]
>>
stream
GauF[bH</%%P0O,YLl(tZJLY97g9BH@!gh:YlW`<fOajJs+$:F5t\]Qi,fnlDGR9B.l3M[OVCAa<;r/S
s1ZqITDp7Q@,C;HT<I_@J,N`(]c[1N=0M5I_*<tQb0OG`:]1.`YJ:(g^]';W49+\tp[`n6l[R)bs5j:V
pn[ob^]2Kls82iohuAuAh;A8"+.jVS:]D/Zl]X6!A,i@L-gaHos1]u[s/G>al/DYSN;a2iOfS98U1f!=
RtII8p?rM\oeCm\q;md^YJ<A_CRW1frY*?Ds8)&2s0">ss1^_p*lS)&qPUOsiH_9m#Q9^5NV,E+^N*Jl
8q&0LBDj^"iuc=<4Ub5JJpdP2a7K?-l[S`2fFOVocO[[<D<G96I-"CX%@b,Jp"*mN[0J)=[_BaV^LdPI
"/ntfZG4-R17UCFrYfSPRMdMLT\:DLc`ZqVQ.*<n258NM17s!oB[=t2CB8X+G5dp>kkRK:B!:],iKhj%
Ldo7**tfH;*ap-_!Id4NBNTbrOjEl.&l@_sqc=lT"]Vq8=5^YUM)BjU:Dh@KmRI3uh!K<?rLBRdJ0#%&
R\J!AF.p!Es.><Hr6"FmUPRcD!cLt]=oYs._FCke,E?oZ>"^*_Uf%Fi9f-[Ehd'ni7%huWD>pDG-p4V-
5Bo*ri)T;blTk<pib6cSl2OHO54h@sd/WKhoPaW6DK<\e-M]@]=.Be*AKM7_4+#WCNVHf"_ak^\*PZ1i
.\*J%S\L"c5p&SVXHJ2pgN%:[-fEKBQef^rl-Q9]A/i\IH&4@`jL)?\EQD!5dC[cC@XM3S#2+6U;1\6!
UjSJ8F1<3$i)V:J'rVQ3HQ^a`P\@Ie-R*RmA@^>WUs%83ao.''CA[>!?bRV!1nKt,Zh)Dln%_(*e!X`4
\bq$GHf-T-rtF":`ukh-p)@S?E(7GK^ej4\GKq@$1A7C?d[6Y@^F?H9-!m^;oDO'YjE+7.T_[qWEC2U<
+'Z*h-7XSXc)3]T/=t=[e'q*>GA4Ma<u7[UHUO:qrjp&FB[j&EDA1?h__SXmrDug0Z%o(3qGl/!HbMIl
bLD89nGD/,,FOdic3:CF8i8$5&JS+IH?2n-E#!VZg8#+gY;U(][Q)3pc'7Peh=do<o;h/_H6a1=(EVX6
]fDborO^Ni?GF(m:$N=uOS@@LP`J;9dp)@>kRcVdQ1V,OTR(_/f-ig]V)F"@p^NaOj8#'ESS/Yt78:+[
[qEkMeZ_7Wdd$G:EZrYDi+Y!.2\U?2`cmWRCr*H=cqn(6/#8ldII,P"FA^'IfVc.1KRtsI^%_qDcSk(^
Brf*ENtS6X*p''_I0_r/=1mei"3M)=T%<;d#=H^ac5/\7FPLQ-/5rD/CN__S380K=qt7L)`XEmP2#;`g
P"s5!3N\]3Ldg&p-L.+d`-7mY$k:M)I%`CiXO`s*a73b'S<;W>BurAf3)t"HKZl!m@.0+.8d#+KGh+f+
'pRCnV%Zgt^.**s$`1Qar6j@Qjb?B0NMP%'VtEp`_t>Qo-u1YT4L,B"_IslmHNQmi/^p=deDCp?Fir4@
`Ukc2UG[h>?+>Vhn'g30M#J2o>$)Rpl`k>BXB00n;[pL)_9It?O//tK\rs09S19EQHjlR-Z,gEdS]9?C
X(3@iEb!(,4fE?1N/o'F0?\e+9`()sI8D"Oo*:Q(EhAC;dG((O9o4L'2KW_]_m6Mi6EBJASIUgK$iFZb
[MEV3>g@PkF3o*r0NinUUBO88IhsOKUP[PjGrP-MFBHORhgYfpTX09&'E*(M:?2-<6gl<-Yc"n+%anQ>
[-2p@f]\$2"6g9LQFc!?7l-7`q9gMYUD>.kR;$lbqV]R=&#8)nmqkmCNe;r?_T`LAGdEj=M=4LB,O$d!
XA@2)cf"%=DoXk%bM.m)eO!<,%PC=J-!6s/_.&/CS%lf0-(2"Ar<6JUn70,`%!;7%%NZV)rH@@KVqLAl
C[I_%CfsPgY>DYSja$BXU&EuL7Cua*&$<c;&!,mj/kPgtL3n/1L:$51dr52goih/20C.'thVH+#j5!J;
.t<=ZOg=g?`AIsMWo<>Js&\\+a1D+]*VAifrR%Rn_@*\_)s?;HU]"pPV[d[&UJCh5E.bi3Dh\^;hlWT_
^G.[:I*ic,0Z1?-rlu<M/Zt+Kh-n3FTHtO3BNSLI#)>$V\).$M*P+tu^roO#U"`(Ff$T+"[9)?@-2nP=
kNUNY-/1]TUEqXHLi1kOP;98k@Dn*`iBXE`U("/<>Ep9UIh,^f*8NL+=8u^tP\R("GZTp]S!o%3gF%/_
.c13DojDKYduY*oS>g6#XE@h*Q!qpA2<sr*+U=)ST6"6_O(sq%9C=4W4Zr;\KB;25B,,,5GZTH$b;gX1
U#(HH(#P:#'u.=.Sa$f``;AjUjJ,SQ7S:kMl/6`Y;i+PM;\7g9HbQH?'L@k*'c=ZDokK2Q@n8+-f3@Q^
NRdggj(2AYmVV%,&q2T>)n!34XDSR4]3u/7IJU#d#mJ;%iQK0/n8mU$^5i)oEkO'`GFFlVrH1iqUJ^@H
<kKkB`-7"&HMjckW#jde"iY9IfF5Ho#9U-YekDQ`2Y;VWXVQ4a26g6TXr]E1_iu+f^2m!:b2+CT9O$(A
IWFoB_1f6Mn*31d2MV4eP^2b3VcCGS=0`(=LsOb\Q0U0<X4jNLl!6;in*4:aPV5'`gXJt+gq>5YmS3Kn
5-]H9:N*U`RT4*N*?cdgNc[ii/R0MK@h6/Ho:aKh'og_\%&u2O6h@H%,d33a<S;bdhGoF*HG0s(/'%:b
m*0r<6d\6pY&&QD9oi@Y7J*aShEu4nGNEpdn*4<7Q3Wnu@<:'gs#m<%D'A?eh.2Wqr6d^YqL`h04R"]S
C!Bo[klhn5'lV4>^&5$Y<tE`*DVior&p;CiB,(NEG?'jIloJdpj,i>==d.D5e7BjO[-eq"K$AUDSfDN+
[cW3#OfalZbpld:8RJ5g8+4S\<><.B(cpW$/TEA;GNIJjX-UkeoWB72r<qZGQ@60=GL_lFFVnC+eAK0]
<73oSZ9U5kM\8O^r1KCbIrL_"D'MqpOPZ6If?0-<?29u^Y5Go74oJIH?.BU6YG]-Q$i=V3F[\\_kht<_
;i*Qi.7ipMl0I2kA,!-pI!^0@p*JD/U,T2[H<2\D*%$m+XbABPd*W8tGImFf18]-]lClnTf2s,bAZm$^
LW@rh]5eVX*jP=T04e9;l$V*rX1I'j1[jVn;*I+uP']m`GT4o=>NIT-X>h5*P+5/:Y+1+9Y$V$#Np#-o
r;<<pN8kch-N4<f1'/KHQHLj3_mWigKnlk<Zo"EKH*7Z/BIt4]T._2OeNNXCpAR3"k_$6^s6sFmK9+F7
o*7JYiIg:_%snUY)LV,6If.)i3@j1e#Hc_gmu:LCXj)(6bX1Na<,qs-_.SPIcoV/7Q]:$@G7U0urh2K'
r57AY-7XN.p(+8Aa$6pP*riGp!p;?/N:j9L7[JQ$_["`H5O;PgkT<!e*rjTLqhL3r+534)5O>\"grAdp
2_Yd#l-VKi@@%/b1=>3-WjQpHUOUqB*^\6CpkR-<oFG=,".AAD0gtaHHhS,>IFl+"l^31S,N_Q,nDJcn
_qoc:g[r(:i:[\.oj.8bhWr3Hlk-:nIqY',raD.)8H7$sgASojs!]2`Bp1^Tqd>cEOaYjINN:J#k</d$
LFN31:\]<l'JiBI?2LqgZV:p3B)3FohU\Hpk=EnC[(O$n^cscT2E9"rgM^rLL(_%?ft8ntolTcYU;oS.
YV2&"Rc\dHnaY<')ZSm`nqiodrqn,JrI7#n"#Dd@<[]1;YVIur(`j8J46Ai7-6@6POJ7!,4@XNfd4C=`
ott>OHJ*nFGuOjIc+V*ebi>X\E@W9OY\ZU0Z%dWgA7Pg@7`L/,DR;?Z#1:";1@/'W+^PP)B_4M%H_77M
F48bkrA%no@-h-LbuT/;TI^V#GP\rDCkd+.TaV>YWb"!SBKBE#S!?35A8pO0'Rus6*jHs"8R^5]*M]%3
jl)tm<o:%(U_:!mB[Me(qmFGgUX6VYUf,.47Dj.kHp>F;\SY%YQ$Sb^NR*?_7S\HeDr*kXPn3FkW1\W$
XkdP<&"p%3fhN?cCB[[9bbNK'l+=jn#J5Hh72`jc:qAD;^u(Zm<q*/LhH;!,]4YH?hV;U8PU#<j0d@q,
6D&a@:Ic9?L'MuUD7%HP1Tbg3UI^Ck)rZ;*bt@e27L&'0m8/kfE?-iaDq?HtqN\78O_YCO[qE_DCDREp
K5VX0>"^IHn^9)JaLM*Xq-hDulNlcD2Oa-W7b_%":,^fRp?2Hs2O_T@+3V/f"N#X1S8DMiLd+MM0]K%c
Lh\=m!]r?th&\Fk`$QcJjqeRRqN";U.6]ggl`e[p]7]Y,$Ih4ZrD:u4AmG4qTJGcb3i]p/+l(^W]a;];
mrjc!lW;2ZhBR'=pD8Wq_e@Q"O`j4`7>GdGL,%[UN2XB;f\'WZf4,SVLLJT"\CX)dl:LDj(n//@d5nPr
br8AV1U?S)37cbY/$oJ/Ke`#,m]f+IJ5-ci$9a#rXWA2K(=mAehTQa9MXMlp*uQ4a_hCs4UY=AU?%J^_
'#!%6?1pRWETiL3Dbj>)gkcUHiASIHS'bPENTCbp7ppfgN8uP0gMQ(kr%.`^L7X+T:hOl-%A:P)HEl+Z
@AECG(<9h]3f%#m4H\Xs_ru(8[s2!A)sI)V*qE#t2APs6NouTToRDYhHh%"A5Q)Vq`X<FKT]\+c/H/P?
+q8N]$<3SDGVC??/@_E6kdqj`J'8.-SCi=F2ILWofUkF)2Z8JBjSo-+``C@q@Z$Z$Rhnp[[D7=[s.D[r
duMR+!sUM]Yh.9`N"q"GNf96;`_EfMo1&5G"I(<Eg7d_HKe6l7<m"N*aCWS8@16+RAoaQY$_>I#I$]fb
0CP%!8Gru8dqDMB(B9XeLZCN^U/GG27DZo5qE82MYsHWf&c=i&_,`";S;lo.liFLe.pJW9B[8G>?Xi2.
R;F.d&ZgFb[je"q'9%9dG>mZ^NudC%4`]TtGKfiD^R8LPq0.70nmCM4jZVV7k5FsQcUXa2NDshR%e3h%
8\Z_^W^f!r\YITG+j&cZ%u.40\3G**-['4uk<ErMGjI6Hbr%L*e(m-d_^cImE)*Fo:iTXW^<toU3mEYA
<Wk[YH`NseH+Rt&P4Kaul;9#N`=*)PUAR_XRt'%$d=5/Y>4tKbXT!s5CGanJO]"8#*F*eJQ)IiUK%)ib
q;MgcM[+_RX;L%U#KOBKc8Q8;T9&q]OjL>?%+X8rcAqPr@N<+`o-PZ3f6>uoKAL86815l=/;\,NN;Ig)
(B%;`WSSjlZ1lj]gde-jl)f2O^1H*!UgIC\).,$7s3Fa@>ST8]IO6<f>NsEN&)jb(p&$3:LM&qSC7^jD
/J9$?b5CG=Q'(rP2M`OP<=^OEB7:?=@&61#H'kBHC5f%N%&A=)SFN[i3bm2?l`fJm=XGAcXTQlr`cpA)
(m=(Vr)fW6I>83^N`PQ3FeVIRFDO+@%"VF+#=Yum!8<AU@:c_CQ\b!k0&!Y1Ue>>OTFW0Y]@_W/HJ:Oa
6^889k7pKM3F)^4MH5W\>/6Bp*mBq*7r1.@hd[I!f_iOF6qQ^ZH*Kc5GJ.1*\tih65UCl'4'aD;g0n[>
c(31@1c@_D0`_^cj(QCBYXLaWrtA`t?aYO54rZZ6<ce+%pbB:RLId4YA?fA`d6F1&7d,2BhHKc@k&b>(
")"C-L\XF9DY:^)S\CVg%Db[6mab&HkAT?@crq<r.HKE\XH;]=-4`52pq^'&jRP<:(eZWQBhnc$K&M0!
VB=;_N:J)\`Z-cB@E1Bn`*Z*i(X+L%N3Uo)R8]D#qnE5QXNDV;V!qngXYe26+sol<<9t)M=".]OG^\lo
dsW``cPIZ<)CJ8a%HET8faj9;;MR,VV86JIgWYbRCi.ru>VMZ]+ed*ml,eED*$P][Ln/bB*/8&i]Z.Xg
_hea_No#ZD*k\t4XS7s6DiAgsgE'O/brV^\ODB]*iUskJA9rW+p18:coK+bgoI%BB#F=8$hCp%Q(A=#'
*^okU.N7ECr^[>PBUAF#NoAA;Yk=cVdF\p4C8_\dc3g=k7QocH7M9Q=g5T"XZ]_;nd&NZ%agkKc&lC?%
L:$\#(GR6s3J4(e\gS$cF(=S2?T<+%[k__Zho[a?:=s/f@HHeLV7"UKVHqMMo-X@5f'2uVH&s8[Iu!(3
N\b3\0S]e#<edp*8IRbOQ,>F4c\)(hgIn25S2dk4WKPf+5)jt9HSI9-WF&ot6RO9<R&rAok1$`q)X>J1
_E;Jm372V_c6YndHjlQRYoI=W,]IquY>pO;0r.&(TT'W2Ipsru#Fd>j.$>N0T3Q$>X.?g%gh`)uEoFHH
F^GLuShr9mU[RCOn^!1@JapB,j6FR;$J.LH.l/sd4!If#m2BbF6Yk]fb\@^Kq"SZPNF+&T5&nrQqC/C&
#5s$8k(C6X14"qIs"d#2Go+7\#@gQik</d$p&+F#a-2a0<.6R3g?,=Bg/jN>'!_H<\sFJ(k!H,o_3cKN
p1A;Tcg"Jj=rVPJnZ.D/'/q(WV>:\2Bd_):UjZnpa3h1E(I$U>27KEto)-El@t4Ll%n=_O:J-tU,h=d=
;/@AWYWZ\ob!n0Kgp8@WQU[I+@FAnU]61"fP4Xs@__$W+K:!7E%]#W(1A1"%5Kp'=0"N^pfR8S]f:s!o
EkQ3tW$s\AqP,Cp[DP[k%Q;R7_TJQY/_a(NTlna`TeB9/9*Tg$ZDcOB99ePpN`h7"gm:"QFKiHXdt0uQ
Vc;kQ0V[[C6s6<@I>OkO3Zg[qik[/;h[d`l=i*XplumpT&iKQuZk@,6ZXjfPRl6=A"ekE3%\EUd/E'6(
jK`++KJHNg93=qrXROfud]`?#,#)/)Ek`i]L:LU(KZ0M!-8AJtq\$n7],tB(Y^?a>h8>R-4RFe/6F.9<
K]k(+krki%<jpa*[,<L%o2orhXJSZTPb-^VLS,Q6M435KkkR^GY-<;1ROHMS,q=>En0L2pP.j?3kn!V'
Kf3N;&fOL>&g])@o-Hd@q@`,T\\jafX:3b"B9da)?77`qX#7N\9?N"9Rp&,A99dC/47jh(k.:<%hf_A\
h5Sth4)5U%9pB5`-!VtIQ`0JLkY&S\$g`VFdsB[Bs*aBu%Gg*a,A:f4cV##rBd7A_()DgBS>KO=lBDP(
Yj8BWRsW(iodi9pgK%UMO!/;F0!7/VV]j`ZWuV<FQ-[T?Yl<q4%R\FSgQQ[iJ$ibse9b_2]V<//2E$T8
><1,lTSq9m,6aHVOEG*"ZmaRH\CBcdFKY)*GL*Rb)tV+;[Y-<qcm]Kg'N-_DFPQPDp$h_f/\O#:Q']J@
MBt5<M/Uo^ZAq+':'Wh&ZmOKM1ER>'St;\erQ.!8k$!/ak.#/DF98I`cKeV'>IqIYdC\;<HNqNK%Ujpt
i9JIqg#QhQoO%MjNZW_q+Htj+'j"Tk/0X&lVXs.AQ>MQOm^A^rk%>=X363Q/GXM&oVK(UOmi$oUh+gV.
(3<t;!p;H7pMf,N30(Dp:\-N-6?7Jj8drILXc#BiL@sIQqC@NLF+:N)S&<!fkV1hJ"-qL`Vr1;,(WAJr
Z&>S40t5@%TG+GRnVjIAW%hap-lP)VM@g,4=IXWCA(gA?*\&e/=d&jYaCRjUGM-^$Wn#gNc,H0rP<%N3
m+?fqH6!mq.*aTI^\'XclYlVj,QC5TLj\)40&;;72gn!^hWZ,nB4+l6ME3sHkfpHgNJemc,.m1:V;@RA
9$b<D11_mETK4(GZSfB$CK94+4bKo=+SQ:,/!Z\lO1[!<9TQ8+H3V7F$!=uSs,"W,nFo/PHNoe2Wu=>5
rCfWA)>*2X3G#'<?;Jn?2ie@]U-j.PC>>qlh+2i_FR=N@a?5+3]d<pFTY$MaBCM?SGQoC>^*b[o0"bT>
oGKZYJSE`)SarF?]mu^!L`(3NZpYUZlU[CdrR/1,QQ^j&7Ang"P/kmc*?5XpVaWVQ51/4,q6i]scSItZ
cS:a2+5faApu*9A8[5\FhiZeUoK-4LF%;;9P*31qXth0DI9l05;*rfTQD&j\Z*89GTVc;=TQ`U:4SqE;
em+:-dM<kj,L0/(T_X`h>3-2"VUhMiI*]O?RYa"?LOY^HEq/%nO;a+SG>ljBCq&=6"D6Qh"!(f,ec#u)
bm/"eNAT43'8p1#^+f6#n<rrtX,T<(Z\%nTj_3oOjn6:5\frW/abeJn/B[HbZ^gk;/PuEQ*3HOIe%%"*
s780rJpS[4ic"PoD1fnl-U%Kds5BR-hE!l_Gd_("jW>i3,mA1FHKA\l)?0p,q$1Qum2pT%s,H2+0p7'j
htt?<55P/5om?_E00XILr$=/FqSE,s1hu&(5*U?OlZmp;Bfe.sn9g;]U#gNY@=O7+@3kDuB[D3f(Od.H
fCS+aHe7_m7le>2A472=4X+53L!1TfiqN\*mh<_pr^RISN3`pD'@rXCV0'aBJX<T*s,$^Ms3.10jtjGk
`r>09a$0gl^r==i#JS6Irsb"Oom<>+RLMnI_\8FX-<uq_qVYuu8%U3*6gTjcm&=NbqjH^%!OMO/Lnp2M
*[ZO%L70?6ergM=Wtd,BQT>`U!6pK@+2AZkd/6[W.Q_D#rs@K>](haG-K3jQN)[j$6FTCEGgO>)/'T*0
n^adB>Mu%^3^X``RS]8E6,YC-^6:HF@"a(`H6Y^6^;R;er]A9J#A.aSLYaSr@]2=#^!g]?j9$Ht1b'76
^[g'/d;`;CPX33-TgNa--<U&4aDHq*U3)@X:$rtd.sdGa<U0NW5o5R$>I3CD@j<"WIofV/d<2?0Z?>%%
rCr:o/gKA#+48`2!V4INd':MSC^$_?^_Fk:^%#p(Zts_sT/TBeT<4cO/_'bH95aQL1JcE<Y@9FGHGnf?
hVZQTkkFitj%ZnFf$PG1FnBg2&A;EZ"W1EuIQ>q(%=k&",fqm\b>F3PYQ^<jbm&7>^Y=&-07K2JVS#DM
-0"l'k#[L<qhBYh$Ym>HpsOJriJjLd*Nm+*;GeT^-'N9*r.3W$4MEd/-g-EZXkAE':O<!R?XIWP5K0P;
_3]=Gie=XT3hP+dW5GNVipZ#.Z[!%Qg:<<_Vh$[__W"M-.6c:+K2;0@iXYWPI&d/MTAJTA%CaM<BrEtB
LAJLV<;dH%>2\T+;mY;`Soj3Cn>XbPTk!Skh@uUI]>:EA<Um&7,X,4XK24AAi7""KI&ZXbidYLLb`+;Q
WCSH4rKhmKiLdnkfUr2^rqt[7gLV?O.h/IG^CMCJ`7dE>B#SE'"^BQp9:>B6e[u6hY8S>7-\0$0'M0iX
0!&*\Gp<R%Wb6mFWYTNI_EiRYnr.Yf.S>:\O"c1LDt2V?H[NPKCjE&jHTo3j#_BC)g`/i!kRA=2M[aCX
ZZn"oJ-t1%nm.n*TlB1DZJOs'OjeLZALN"ME(<>[K$+r."hI//r"GHHp.pYA'Dm%SL#(Lr08^I8<]Lj+
`kKZNZ%$GcGG(q+EGO;k_2)CN!XtOUJ<$>6Y_U\s,OXMu-In"@Uj*QiFF=rE=7N=1)9(GWU"DjJp@6YN
5(;M5<?Uf%IGr"K(%0?np1ek;)QDQ&gGg)_HH#CdOVZ7JUD:'(GdDmI\)Fsq<M(H.aO0'?g\:Vck8QK?
`g;Y,44Zu3gInbL$`:tT-Xl1aN/JAD,XPsVT)hs#%:e9,T`uXo4mG$(6=eCtS(V!bB,aA;%5gatiZ:]:
+6&ORWY#*F+E!jL*U89:<jbdH2#j`?[kji'mn0l@>B^OUX,KEQcEkGDRdM$o3-]5kG?bJt*V[QGUPqt8
^C4l"[EpBEdS?2i.!Ig)WCRo/I00pBCM-Lji]1,b`h9tFd!Hb(W<dMbNiIn^7Vf=Z!_T7]"r^W$SE.[b
$5`]!m?^ja8fQ3M'f"#Is5;^"m_g.qap]!O`De+J928'.l`ontFj;aR>5\tgYD<KS.h3'm?)GA>Dn7@K
Z=<jCnDs+nR'#Kf'55H^3A__Q2H6VLp$BKI"NBNiPQTOL-3!%scfG"0JNMtlELfF,4QrMFfLg#3=]NIT
C8Zq+<F;gU<EOD.6I]n5;1$?tPdJ<t=.hF/rU_C+f`I1g<WsBTrL_il7;oU)>`BJ2d9\*M]t&$,BCiAq
@q>#qhYQ>P:CtSka[J9lZ=&KL78noXK)*4]r84%f=HN&8[0N<CA;'a@i$Fm)KDu%?m($jG4R`S.VKB$)
5B5bi+fk$,XPF0NmjIi.rX2kU7W"H\r/IdAdr<61nVnb#F;ukL>?R]6T_ct,49>oR(Gfgt?hjU`=pR76
AHseqP<)1$C$(;@I_2k!h9reSj4&T&PNr;%V/X92l=FVNQmN&R+p6LZ+R>_7^Y/*iVj/6;Y:q&rnQFtk
;1"YN#V#[h@8<+cW6"fGXRl74RMbi]'5)Ru1W(aOBEQ@W6]!taN?2jnHXk(LE5^Njj#q<L8]/DcK#mU0
h_9A['=@uU86CqK+aPD5[D"UinNa`9Nb+fTl/Ue#;haO(hR#BaHCq0X$c6[G*8;C0'rbf!V.#L+1[s5g
1pg8bAG/;/P%hbhr.Fq\b7^Q!2#&*8bHg:iBrl51k5#caV"3&tQW<5RJDu:]kak'goGtB?\+52h8^*hq
<iLBr4)4:e1[)9(9Wo5]GI.9`[!2OaF(+)7It<hFG,_Ldq;dN4M=/6K.o4&cYEG;t`%trCF(/tAiM\))
i$8Z"omo["c8_ru3st]kCS>9N[o5%n-IYH*=@J4Mb)M-grW1&aYke&!%]uMh5B08rhQp=$c`lX7.Xsgo
(Ks\qTO^8[;0faCXq4Xf&mSU;$30L6jXba/b%RFaZpu&!=P]PMePRSGAh_\O\E=Yj>[a.lVLEjaqd:LS
!u_-S-1Nh#5Q:t+>%nJd/7\,cqHs=q:F\SOPFB^qJ']NWb[kP2dJ)l1nC91a3Ia5)'))nCgY+;E0!o0p
HaV=(A="#Pbsq]b%NX]1BfFU_N.7R._'_Dd_rq'8PIkAoq1)-oFV]A2Pq-@SG^_bRE%p/OR31FCah4<@
r`!bQ.\8qd2KD8jkSZau?<W+1L8)(q2.:r`e(X$]rN267baWC5g'jIu([@UP,)g?+l<;"Z%4pNcdgkmf
(UG"f%LO,SgUS-r08nPcBP`ql%;d5VY_GkFZpEQ#)/<i#XnV0.ZZBuO8]^ibX-&8*(D:#,40jR.Jqt/&
oU`FJ\0a#*`jq*i?+t?bB[^*cI<<l*[*"PdKAU5[L/)]8(MEEVbnI&G2ap)BmI]E&(01P?;Joq<Cq0H&
oVWsZ&$`^@s7]^C<0_.a/lUc3X^>4+(J-f'UXCbkY+(Ne)]4uSU1j/B19BA5Got&``0S)&(E'QZgcW#b
okR`\.O1B?p;s5U3cq@Fc7A72dFjU^BYtS2-u@tfFJ5eDX2Sh]q5(&*K"iN*jP&.'4pBX`;s&+6I;[T-
25-`-bgku/"u?B<f8J<M6Wr0*3FoPmqXMueJ*.((JQjJD83CURPPMYIIf_93J,U,Yo7*)aJQf]e)[*Vp
3hD17!jWI$NTo11)]Gg7n//Ba2JWu!rkPtsLPR%8)euIGnX*+'(283Ua7P?MOf9,R7H(iH^\h.<ZJ)r]
*.djNkkek;7ZG*$33Wfnn\8Re6WQ@a4rd)-clkcG#T60[F7&Lh9YQ>sH@]OY_H<!"c&%?.r4>m))#r+m
9?qlp]B(h<89@<lr,Zge/DJ5BM1*&:@V,uuq!?*ROlor/Ok2A9DbGI5`2YrRjfmdV*/BL\B7^S$hLmZE
i.8:#X>^a\nF+ZF'/0?c'?c[8kS]"!19&#Vhon-\=6lW1Y;^;'q#50MmhG38>HSPLG4L@:hnVq/ni*!3
VXIi5^oms-d%d8l7GmPm`KM]V%ea7=XWX:]\%1C5QPI&1!NE@q4m7iT(c=qac,kUaqX-d@Nj!FmNBN_L
].]'1R%8B-VI/$6ZruhYI+NCLbS'!?)R;5N4UMlC,)jqES$#!Nh2;d<eY!P$'GS4DqgC5nln>2[-<,#e
g_a&'@-3@NB^U%*)Z%>4rHngQ>j^q=rVGiC6@<^cE!"(Q(mk**mrge`qSLj/[*N*.Z*$i9guilDe\h1f
AgOaS4Hme)PE\'pkrZF2PbJog3[&Ee+6c_,aZ?QMb$4AthF+GrZB_PRIZZRd125AGAo_GN"7oEJ;lj]j
5O3M`re`?db,X.E5g!"ScCp?r]PlYerpL\>\QYn(`F+FY`hpD;U![:53+G]q@?6(`<\6#r<rC4YoQbl:
QL68a$,!r6F")@g0%N%qNNo>/>*A[+F(HUQ2[p!"VE9PY*)h.QO)efpHG#Z\)7\8h9.U1]N%)h_\gTb(
Xb8m!UCUi;HTK.Ce[dFUIrh2&?^06fmh$'X*.^,k7sjo91,>[fV$5"@YHouYq!FT.3.dqGYc3)6YHJfG
ReQ_'-fq`/?c.@.Lqb^0qpO!;KO8kM%ZOb!T@`ASY$2:dn_6:ud5l[S&%l$g*IT+"5d&YDV<k)IS.p6-
Ho=c\\6iK"%hFOuq:5=2jN0bjrbC.o0dA42JA7i6[!VfO@?*_s<ET^Po-TJ<KtN*;q+pA's4r^PAhN:*
o8@`DljQ)J3NYJ,.uN;MrOU`dQh_:jO'Cg3YWm*TDJeO8n2VK[?G_?:;Wi=6s.9)eM7l.R6Mq\8LtKO^
mMKKTL+LMt(7kt08>hl5jVkT"U.8.N#@.m&11^BHUSaEPA(G>_2[:SIqQ<o*-qkeT_t-;J20DZTIu@bs
6!KEVlE(=d`1&/]I=7I(;025S0In%rmjCs(=1C^.G]BPFHoL8^?PSjU,>ms-n+TQH9RUJjQf8jse=#F:
rVi\'4f4d;\YVCi1N=eth`rmt@I@h+RtE^9qJK,,^P_EOkRma$kjMtL@qFjl.rYFZ2fUD;HrmetkuT[e
:9$gE]YBIe)Lk[1eTZWUqGgBa4V@p:qL5I`5h#W^<<Fr!$DftC.2t-.ECc)(s+uM6'?$=7^m4)=mJI3q
K9pHVRZj$"E9<O5]%Gcrq/</Wn!H2$k".mXS^-I8V9t.l3PDj(';AVc(V=7C[]p>Mc4G_)(2oMjGDl"m
r)Y]KXjeRm>5c]PZU(?pX,iK&I[gnJWY\XOM>NqgP+gb('miF#8+?C`BbR)!SV)9Lf^I!>G2=!a)4PcI
LVVU5oARS!H^/BR<bYLH=c\k`g'*V)9'c,;4`]&tNf!'kek.@7:.5*)dAhZ`]W][/hBQ_Jp"(G%4rUhD
Us_W)K7DLP`*[WoZ:?SEnuO9Y1h?!ph`'EIU$1R$$Z#ZM@!?0qh-q^hFO0I3)m)`T]@`g8OMA%R%8pms
GUmP[9V-[6b9]l0A)^!p&n54$Z\R`bCBRKJIK($ap$2M*qOH=]6fhCj)Llml9=F$`ZoDWI/=mn\b%cl*
L+TH:T2o!NOg_@$<8ccM(>=rI*//[;k`J[tB:@-^k4B/O')%^YV0f%*L+BA=afnWl*%@.Ai\Fba?J/Os
m*=rFc)$V&d#d$B=Jqgt8%+?Mih+jZA,\7C,J`BU`dOn`9_/l3#%m3]ZMr<(XO&_@Y%P2cObl-ep"([-
c7b_q!%0,^YpdM&+lTN`5B1rh31T<61=>Ohs6)Rg;%EWU$2>]jE7^0F&^8+UleS[tb".$DLrrP!h8dGs
RrQH`$P:Vi)c$ig/'m%si]@ff+5Md%$fu]@C;QW]RHl!O;KLg>*K`qC(!Ge<n=6Api/&7o[Q5kHg&"H8
AO#S>W=<PFF3haCAIO%XZpn+*T'n;ho4YKKr!Yf$@\eWccR,l>nLF3(+ha^&]'nl@C6IDIk('870l9$`
fLHa[R;98)/LKR`HWR:+lJF1X+4f(@4c;=kL;I8^W"j?N1fbe";4IAt\?9oLA,VH0:?R0KWu1*YcDoGt
KkaA':FEo9ZPQT?-c$ssHA,D=62Ia'B2oC<PVraWg)AC'Sa9/&QJa>`X@8*IZ7Yn*s3@_CZhLoL'B@H9
EDO!Hs%WCcNE(gIiso%c*+>W86J6S#-"WXRZ`#=[G?&qNB"DePg-M#$YB>ScAFeU)ToB9d:@"4#<Q,=!
4k9:6LV>j\j2Wf1Eq%I0IsYhJ`kHO9)#8+,q(j#j"CBgZpZFcG5Q>qOAi0'.Wj2Ic\F?l@<6_oZGLB==
M5J93[u42G)u"=gN]Q^mpW9a)\@Xf68'IGuF1SU3&DfE?Frs:ZZm;sFE(Go`>kd+H:>X*^,TU<n^>f^p
M2T"*9h8lDGNu=Gs4,41ZI@nW7=UZ]Q(2,EADZ][GS\YEH4TE4rDuf%.BGg$'WMN::;;'YGj/PIbdtQ1
nIB9aSXikPm<8baPr6LJ:5Lc_m.&S^`6J8t7i^hud@(nOR^e%)ODH>r&5TB21N=IE-JiN#EC(pIcs+$N
Iei6/@;=u;q:mX<F@rKZ7?8!aMuG'$$h#&,ej:;Xa6Z(s78'Z0]*$e><_i,%gZEGRAj-12ou&lIZ#b<7
0"]-n9\aTI\$V?IEBFn?SDsuq[WdaE'nF3llb\+O\i;lLY5QZ9RC@9pj"p-O(<8N-)/r90pG^4ara"]3
s&F'OH6f*=(ZO*<N)h%"DG1fdSE$/4jERT\o_UJPIODl\UooX99;Y-EN\A'0<ZKG-\\mr2h/SH.M>"26
IRi-.]qKT!3[lT?pcPJkSE"[7D^FYNQQ."6.-N6VNqYKSnbuhWGlLca]_=rZ]q%qf(N*QZeKZ="pl]>k
43&/g3Z#gu]ufdtr[,[(q?A?Yrqg*R,+.eHB&;CX(t6[@.=8VFcZ/7Q0kOhiDf+?eSr+cLqG??hm9H!o
3k5!<CIe'p\[-[Q*Oi`ZI1b;e,i0VenDXm%=f3J>Au0@[47&?I,OYaoN6'c5/`<puH(UC`#%e;.U<",4
@*uQ"]QnqS^M5)e]K'J"$4CblffY``pHaH/,_:6a+jW'_dc@a&I+mRd'1p"#%3hLrdJnT64;7Rc'b2T&
mmF9>=3_cK=lj>f?;B$)SVoOi2agRg^]+WLjj)V..d^']0ng?U-Z?LFXQ28;-?ADUjZ2knO*oM\msf#@
H,\#:XaK?,Fkq!2Fh5A&1c21]1fI?CiiQjI1S7Z@9t-QjY&<cGilp?c*L6nsSSo]V*VO;jr`1S(El:&j
khAm5<1UZBZo!gh-,7&U;2aA0n4Hn+/kM_q\#0LAJ]F7EeuW*EFXDLH*)5$!LA%HB`jOf`hpZ<$]tuRF
VHAP-)&8l)2,NFOD!J1G)g%*hiVakKjYudCpZbcin'8&oaU$(fT"LPFpWlQ5c^m8D-9m:[@eahkm]BN3
,p1XT\W1sMV8$12DA65$e5lRLa;p<_@0Ftbf7L3rnX_,bX3-qUhH<dg@ubLea10N"2G/h\[U[jdMiu9<
''eV<<o-(d'I+;]`_a=)F-F1$"KPcD270+<*q`:O%;IKk]-OjLSg@\;)BVGmib<%*EU9_ulQ1&S<JB+^
^8e[Zl1AenYG5+:iZp,,%Fp#f<J@u?d9:5VE`HgWqg;BuYHl0J@\2P7<Q1*(.L.W`L97!jICJ9Q<T04$
^8e\Mb>kCD$?N9u4rRd.Ras!l.SX*8X%F>l$XZn/SA\Hn-JOQ]8de:QX,:Di.W)[a[RpE\Y>rPiI'+se
`H0jK#b.AbV=AF-De7H[XX^I"3q-.Gn]hKD.P89W20kD^bh&lSfN4?(?,N.:]2-aQKu_]gWks[bI9!Nn
7]\NEn#K(0%\R!pa+m*M<3K<`7$DFe;=.#Y.ZSmV3B6V5`OBY`BrphGX%I0q6eSL!EAXgJXjSQj<J=N<
SlLK2FS`#sFQ'7J['/erb!XW8@o]e&Tef^a8%tu_k,QjmmoO-#9&%stic;t;`FE"T2d3mW[>WmVD6tY#
Wd&X8<u%Vl<u'=(>ZG,,je#":/4o5hRLi!="M)NRPiNLuo7%Q6@9.Zj&YIsbo.?`/pugT'(olOj]GRuC
3ktCohE4pTc[XkXFA$OY4;7=CX(1kt'754SbFP2J8;Qm95'SL5Jd:NKgZljpq1c1kbCXqLp[dHEJnfqG
\:KBNS)m1"8$:;J3QIIEIDLVE_7;LMK8#YE9\p:f1rG<;6Ml#ia9^B<IsdGG*WL##Dd0uFl#!Ik\a\b!
H6J<NmaK=@k!I7MFNDGi>9oJSTq+sdk/(4$l'o$I5BU5@2_@\UAk[@fc@8ka+aDRgTd%lG"3^,>)]!Lk
mfD0?Lgt;fZm)S[#X/^9*W>`E?ZAOCQXrq[j;UN"eKP?H4.a)RQ>`Wd0:q&qh=#+>YGP&&Q9BP=d=%)8
8G$etQ"Qjj$X\O)lFXR(^qQj2#W):Z*-s!OZ"]t:k&s):cgQJkZfqqRfbWUfZPKf1:j?Q1Tdol@ZBdCQ
EH`Jh08$/-:hbr7+/=-;FB]uP4@Pb&1u-",ICfeS%WIJ?V*+C&VH`884)K#bD>JJD,[^*][0mIP6$@M9
AYD";534u:F:lPOLoEIXrfEH^)3ls7;^/O=V9/1[XZV/K(Z*NYKENF@16AMo"tmPeOS'ekUMZYr5*c(P
GiG4%A67PCjZHf\pc'8>qYS,!S#9gJ3Kna3\T/e>E2&rjh'4))3o8JVVFtHEMWetZZiF`S$ZukE</);g
>HXS5!MKF`Uj,P+;nW.5EN7;,WYI%(d&$N,m7!AY>69(f1hY*5+PO&[EH9@OU3,-qc$!9s"3%U#1C#Q8
QEnd#&q:Sb#'jg:'OD!_)eoV'U+nEd\?][DfM5\JV;%k!i_mmFM%h<JYB?W"pSB4Z8s:>mRF?#]Mjg:k
d2QjtUIUTjZd^E];Z^7b<(%k4eh)m1.JI")Mpknr5.S%>*DG_`X^@`@=Ju+2*654%>,$28`bDA+lPdh;
kgQYR[VM!sY>DtrGWXt)\&RCKH#MfXXjT2?+Z)"cldrbgW@2-7)TRtUS:fcrA*_N"%R&UBE@T<.]>g9*
Tet%=`C&[!1p1heF<i_=9@WO^3"6`q8)lF?.r(T?\M@(C3-(#=Ql;l6Y&BOUg;?G@=<>CE)MdKi?/mKF
Y,$''CCsEQjNok$Y5-I)Wm)cJ;e73'LdCB#j#D4A;fTR7CE1lmOiFYOgm?$c$7b(LSE8H23^ruXNO*nZ
:9Bc<6Pn?i0=k!?[,dO<AkGU#ZS47snTPA#dh9\6)Fq5k1_02VCRdij1,KJMe48Ou<Z@.?&RD2Pl>R2"
NDhnr`U_c5`s@ZGd-E;lBcL,JP/aIL.WdLBS^_`/338+t3e\qqNlE6U]leB*Q,Z+kph1Y<m$>Y2NBrQq
dP$+50s/8r$9SE;X/<C><'0f4COc<ss2,93`@R_(^tt#d6q4R<4[HuNdC<!=\XU2b=7d1'oD6-rHnqS.
DWYK4bO:)V`LR?[QThZh;?t4EgeN_CiM`iOao+;ki\fMUbu<!S\U`^+.<:(GSlJ6!nCkNRO]?l?.5Qh6
%"%g<X4?d9oBMaQNgf5@3"Cg?mXBQAaI^rRT6M,PKs\QBLY9!5m+,4VW&&eFfE,MF(mB,i$:`ep2pIHQ
KGMsM[*r2@qm6eTAKu$>[5c\Y-$7Dq6brHoRPaA)NqQ`!$R>Cc-Ul,Y%n8])p4`oq#tb@>7"`1Ego9@+
=5>mZCr^D9>]F<#`bkr`[q?6*3RtL-c]K<V\WA7eaEh*Z\RVO>1TTD[/nAAO6K2_HbY3D0/&Rjkg9g0&
ofG_W[>>rcLnP\9f?Wk\Z'U@!_)rcl/9^;"JVMH,S;0VZ[](k@lD$j9b(U7_[1R=,4$W8IWks\!VVue`
O0K#V#7p2Pn">KnMJ]&0qjtKlp!q<WSdPiCP+r6G]c"Gio8t^*!Cs!2Gai^s2]!m]L%.g[e4ojHAMB/'
'NZ>a-H.jVaq7mV%;^E/S_!'&T6ALAn[9+WL"s6]Q+<"7@E&<%,`:7GpIG7W'ePn?)4CpgU=lC*b/;Pf
fP0M2(mQ]&0KVpV+L6`&bh14'29G[;MS,rh;*t3Geh*1do`fU%mFN#&;+$OPnd&*c`[H(>f@lfPo8,8A
6AakiZ*YL"Hb#G0cQY<dO>I][>`N**d&KSg!7AN+1@mCrD]RUH^^^pSXR,k^B=5'DqH(m3p,VV9riKhZ
r*`&t[8d(ob\3uY[r(dee^%KfPa0CY]JA%2fkYsVg-p;3lnch7SIq.T4#(C`Vbh;uiVP^8rj,aI97&>(
9\7cSh2%NAZ-Qeqmrd<gYKMr'H#N"48tKSN]&e"U.Q6(C%'h!G:o68/\1Y+re3u=(53oMJ<mJ8b)K/uL
D$I@>pBt\@%NONN^>&Y,0u!!X''%0l%u`1=lt_"%1QF1R`V;<B'KQEX@o4pubr[JB%[?-6^8t!\N\<'3
UZ`NbnMq'cSA)&p`Z]oVZeXFt,]\/p+d')%,a$i&=!Wo34XlL>\W'LCk&#=,m'9nfaUpUk2Fg>ZYr$)G
2$L+1LG!qq)chf!4G/G&=>T]pGp]0R*isr]b-@h>kUsu6`R%H0l'-<8`8(OUBI)HaBaoMmq7s*3;-B'3
UATikc]!7MGg8m#%Bg\`>)QSp7fQZEHac9l"qnOWL2-(-"_unL^o/%AVA"&QD;<<MoGRcmn+[\_nMjXZ
'dut-LNF*l>DT3pc>fRofBi<NR4-*I3`<0#ft"6>.'4:#86MdfQt/2"qKqkBB+a+"m0g0?K4?1mi([DE
[aLH.4+DB6B.-i2PDcAZ9TRflqg^X^'Qg=U]6.O]W5!<64;DV;`VEj$mbU>ZSR&PoV\Z`BbVH0N:R9@F
WL806.rTNqSCHU\ZhSce$DgLV<ZY3f-$5;d/CCgRf)Rj`nSH<1-pu[)R1c@.:TAkV8(P!=n=28PXP[es
%=\J^eT8ILQE&Iki?Wh[[5=(+EN1F5o.bC)\@aERm.p4\*$<HID_:g3s6i]eL[Vs9D3(kr*54V2+RSst
T/`GB)df5$Q?Qnpm4pS(NdhP1W]`5-s7-DTkILG:i'M'1T)Za;Qcj^Prd/R24cfSkiqU3:rO!EeDf;/C
GXf%iCcgXa?trpGXZ#h2c0/AUfbcWL[W1Jh+d7_[06RuGdC)R?XB!22#^GW/6WWN9h^p:X_XK#pF[i4q
<Oo$IC7X^pco:f>Q*n94F!"D#ZG,JsDJ+%uko#5[)hcqr6<AYjEF;X^@\iPMM[oO4TFuPb'YN]-I8de"
LY]1G#nu(&#2cl<7#Ml>Cqh(d_<-AfhXn7D[C;9:*%4:l$FC,kgZhM.Kq8"sG:tF$rgFtglB*7%a4VnJ
R.Xt\Ok+4ugl9dPRCdD-h9:,?IPip%W#btFr>!msBX@J)H"Q3cJQ@Y,#sAV&hXi,5(\\(GX,RG?gSlZS
B%m+m]QJOhf\X+/?YbjDE;/>Fg2Y4qh6RtV:97p<';M>(S%?"7RWU*0Z_FG.(</>-W"!15'h\BC;*`>3
[P0:FLq"p`9Z[(Rb3P(94<*tU54R#-rj>OlFgq/;gP`CO[2aDLN[q]!?B[J)nhH7g0`K;[2L/ls2^e"2
rAATC%c(-9$5%3>=Nd[]&\p2t7ZH$t@`C>Z+$27O_MjWJ*r.s$pH(&YYQ"Sq[ital],<Q.2Jes40SIo-
Ri0\EFkKn^b2n-5eqK38_`=@?l'uGM`\pW(g2GuXi+?$ML7>^JB<<Hr&hSfWaq&D+aq@RJA;NfREuqC%
7]SB%@.,%9'_)h7+o]C3kb-EL)FtfrDi]*#b"]'sN@mE,.U!GQa*#oKRS1tEnegAWM4,@,A>kSSH,g70
*LsDh)egB86^dn2a&j;;>:4JPi9c(fe6P1P];6_[UWggL#A5!mOU4CLTePIEN:L%Z4%[$hZQ7,hS..ie
e5,pNFQk<%aBJ;gs5ceVm<)TD+(UnOhV2,OiI!&RJO<Q60(JpA8'/Lb\2+mbM6&nZjrRq$)](E5qjMo_
09==q<gha$H@"m(J^&6qH`Kp0Sm9+i3f#EH(K.b#k9\P`(51KUZ$?t.A,A@7AH(eH0tO\dnkI4Rr/!ic
LAksYk."T2hHB-E"\*^Do"=tie@M-T/?e]o$AmPFfnW8C3;S=@<Sg*V[jjWaN;FMEWaG&sk,O%^C?9c'
-kWQ<RWM`VE$Ijr^9$uHX0BR0a#n,m,,P1DZ@Zc,+[AD1*DT$cHo`>op-5(f&]kIg?n=?9643V9HS,pU
@;3;XG5gbNdN6:K-_lKtFseQAk^Pc/D55SVK3$hK@_Y7*Q"tO0U.E^)o,SXBTm9GfJ(\%4VrPbKZRf\C
R:>&Jb;K2Vk!g>V)>e?sH,f^8/roLI^:3\,k05<+,oDGVK8*V3MKW(RUGT^Ar'QL-Cgm/:*P3,774b"e
dN4H#fUAAj[&>m,,V4\n<='n[eh$/oeC2PR2?%31%tRU3`%lY.Obgs@;d9,JQ$2cidGFj%d68=^2cKda
(d0WVKRXJ$&rbf31'c"B@aR!fR+^k3,1nN^lW`he(Y8>C<"o%A:N"#(3;U[_oVe+@+&&ZZPFQ]&fe+W5
/._/HSoKAF89nOUWXM?LCY7>Vo1SnBEgE`?>%MDC7TN0rUqA+^KMnuM^*Mg:\.;"WGO/5Q!oGWlT%?R`
03EO\Pd@U/96j*6<@Gm=T7<Y!YA;rPP\*I3Au4pWnuQXJ8pMK%d\;s>*@Z2+.G'tc8U3dJg`tp+\phta
rm(!gXM6R(2NDVo*M@^SP&eOL6coc<5q_9E+dDO6s$4^70;o=/f83HuH-,K4%fVVR;+`jPTl=/rD0.DN
RJ4=KPUdC7Xc-!?c-1UmOE.,?#V")YkX`k&elS4/=8s089^_^R7Ll:hB`I263JEE::,Z[ZFLQND,A"Be
AMV,DO<FeVI5B!d]Y)ds>?G-"QH'^c,Klnb19t18)WR1Tng#Hs+X9'"ahl-_4>O3UCP@]u`^qF#doSJl
!pb?LA>BY.OjMf2'pOj/>UCjDKfAm"7W`-rA;'0fLh"RuOnVBadFl9rOePq`AV>9MKG;59^1:b[T"1Ot
Nf7qdj2_LBI$YH.&B`(PIjcPI8OGt5NrUrPr!o&*hO].+,Wmj'aE5-T.4CAqWQRgMjCP`>#MS(bg>ar_
o=C*I_EECj87>5V,9qF4J$cO+NK%/p"*^kXQ1lVRi3W"Eq*4Ck>cB0V`Ju?,WK5kgl81c7eZKK]fn0mo
n>-R7HeG"XQZ[/Mgj8=_$9._K%Por4Z^=ISkl:lbF@]LTb4aUc7/$7Yn2d0^^9X,JZ:?Mnm-g&[h>DJ#
[D%^dFGSS(W2.Li.#=hCrd)nWa2-O(&c=h;0p4b+U[Ee!b)0q><#f-d&mJ#I&?V#!4irnq\T]j_X3o4^
XS[7V><Et?doZ$lH6a1=f8((6d1hdUNHO&!8)KJo)6us$Q[0q5U])</`6gtgGUIaB]9^ukL[4e.R_`'&
O)FfWi\9-*o+/elUMsNp5^]ZBWnchtNp!T!\io=cFF2MW@rrufs2;rACe,4?[,ndWdg*Lm+od,`r-W"d
-_Lq3)lmc2jBYR$$k7F6p]IXd)T)3^G`RWA4kmllZ.A'b0]SnY4;Q-5U#D:t`1F%@Q$*;i[MN+Ocd&$B
Xa$Z;`n)21[n>M%lCN[[)!Bo8Del&ULg4jm*tpN^Zeub5]JNXC$GG!0D83LC&Q@_s`dRai0mBG<fG/[V
en5\/c[1,WQ+a`I?bhqeOd^*bnSPTuHLlq7:@@OVh/Y=me#W@^-L&2t]&n=3,?ZI!Z`hi&1QtVp]A1*l
?VRf+e.:9=Ar%&ta^9ACcndk6=0Z^nGKP,s?g85648LJGZ*e[W.m5]%ff[QVml$M[r2MT2H?@W&'M#kI
Cc>mN3Q?6G+iL0rVTdoJq*s)!r_#4qB"aY`='6KY9TD[6HF7A=FZ?.-BZ,`6;PrZJL+'4[k5dE(6et1N
@%;B<q(&%+,tr]CrR]8:IM_0Xd0jQ!RoRuRS]<$PL3e!\)(=mZ8lD-W'm;DJ]BI+=/b@0dXjeC1IQJ!%
:';I&>%<t!d[Na41F^!#kAf&:2*>pQ.5f]mk^RjCijPmFHQ:[=kFUIX+&b#@g9iY0&^aD</EShpa#cch
EkRldFc*mP4&PiSm6mpDY!q"#ULQ9PB[oj.[1heDF;Lq;R]XcqJs$J$f02K.f9t^o:Fped],gR7D#<lU
diJ;`=3bS$r"F<9f(5NdTE!5!l8dmZMJOiXnS?!q3FNH,)aSLW$%n360+=7i?ml%aNZXeaXA+hN7b!9X
,<Tac24R^Z2ZR325+aEPPeW&Z"Ut!*9/8<QcpJ0W*eZ\;$X9jNhk\qjEP"a/3dCh?J]hFC.T"qh$OHQ7
k^NUSg$+ZMD0[sK^Vc4BL)*Tss6Z<cn3Cd.n$#X-5u&5*3c]s1ck*m_5Bbk'"It&/+GDn=--B?iLEk3Y
SlUgjkGs2,>+L2ImoiUL:_D]2S;']8`D'"q1^aN`QWUE4In-tY7k_*L7h;#0m/8"gkCCtAX#939(5aiR
pab[-$^4j*'i9/L)u4[ufD65#Fr5Muh2Q^7rT8$a,r/F[X8V\"kp?hVSF]31ZBT4aZ4:/:8J*d`c:amG
#B#I%%4GM\0Inc[bJqE&AA,N"F1CO"kg+*3QT58q$uJKQCV<n-^<=A'cMNHrUjsESrX+Y$Je9[7K)7tl
DYh`4TlHqX"^5#Y&1@pkNOMbqgnk-&UU=l$FulQ,(HA"4Q9:<W^FR%JC^oo@05R0qYKat1hsBgf]$S?G
9o#7*psLI1TtakQ^;XA,+*=m.`=kHXFM^0f\YQ!QINL[t(MA-S'9Bq%1a#5QUd"H-%c)Mm;(h-e7N/X,
[g][TDAdbeX^.BnFs>@ICQ3#mK9]Ro6n(nJlL^AEitMaF^,Vl\.d"<&"6UWNUI!.2f_[oMV5s#S3B61@
o0sgo6PbR'fd4jb_U=<$elP\m=#G$l=nAk<8*eO%``6IB$FkLHr"@R8NA`#_Ar-E/ae.\,R6(,ZPk3_F
Jhku1#Yn1&Fb<R@qO]C>T19,kA+eETB@eKiBO(DB`X4:?:)-395,M)ZgEXK(j1O?Z>fSu^X7X6aFik(:
=6Ja/s+uk]s2&@h3YL''p)L.L*2OC-nC?Sc9ep8!U7pQ0qt(!cl;pNLSj0`VFs+nsEBkuLh*hf%`RhW=
f#Bn?K/u/P^h<RJ>l)HqI]fq_R.UFSQk/F&QRKd>+7-Gcckj-4E>KpUYD4d4kO"2890*j'=Ik@)7s5cV
bJSt$Uk/:.):n^%[U,,-+[nnG[edULX_tsm,$MUH1p=9j=9QbXf3r?_5WekLrLVc4V'FkWBWUA]0qtP^
*"u?coo',:%5jJcNG84rN"MC$Q_I2FX_;o^KfY>sIF5I7r6MR6&a9ME=5o7aHi?""/\0`30-M=L6qdL`
kiiue^-pf&Z!8Y-FS?Na6s[:aXKI2P=6ho]`Ik%;W=h)W-(^[eZE7b"s,b#]>1WgC#O>+[&p6g%j`m*W
&Zk%SA^ONNSo+$EWnVmk-0c7Y_3\iWm=7US^+LqPMA1Gl0@>sL$YY5ZU5Q7'$,%cpU:KY69;V&\1KE^`
Xlsnuoj)'5=6jtC,987f02]dU]0kEWWRU$`3"<Lr.po$^<<cg)\lArAU:O>O?UFCO0;0H07]UF%ab$SZ
[c"/i8an%kep"K]M(\?F.Qp`+&qTtmNiTleq\-\sju@_pn[ElPn[ElPof%7)N@'%0l4u1'$.238Z\@MU
/mk=_ff![fMT_=t$=?-PARe87Rsp&h-"!CXV#Xjh1?U]18R&_0=t^>l?!GPupYeb9/'+%gQcbZ)oaC/A
2RH6$,&X%nYu.MK;8#mUGNd\[]q+m.5!$>Xn`pRlc/!mYHB85bgQht'#$oOG`92L6s!RO>fEaQTm5)Th
?$Gsd8Q*6WDO\E1rPanI<MPj$mRnef-<o;[2g,Q7\nnk[":6.:8?C(mkjkT&!+q%l%GPW=eD+LD>kBT?
l)NpYeN$RC>4n`Sp,[M!cZm/g`cmL=R>&BC&JqqV#A<kPgG+/:XcR&c,eK>=Sn<$7E2s/Vcg]Ia(.7#'
d>lg;l+:fQZctL)A)'6Lmk<)_a7qLN,^W9k1[;e@kNe'"3;`@kkb?Jt[7h$=G=M2i(t4R76A#E0nLSfP
c"t'E\j;BH)<JZ@[:!W_rX8DnTFFh)K8<S9=%Iar`B!luWinD"2'DgfiHD0oLrcPXjDg@*Z/4j=.t]3J
UCU$iYTAP["qOIi;ipuK$LdfcLM*UoPNUq!Q1-e4<JCDI:[HF-?2YJ/K!8ng:EU5cjF7-e]SW0o]!96B
M=%[*m=t,7Yb)+Lfh^b'R#L"JZSf1\:_M(n"QGt#_Nkt&^AL\Pg%fiuDHc^?c,ef3*cV>>?_["MnaXh'
@QgJXl%h8FB*Vf$Nh]t1:H2L#Rmg!J6r5)!3nL-5cTl,r_O2dCph#*Ulea;b7"RHj8+RelilILLU0M`2
Z,3Td^l&$aGX&HopKold@XEa(lK54fCd;GsPmF_;9WXu#eIC8N;rK<)CU*_MoogOa9m9;P4S^;XUU+JN
,!4o'Eq8,OKB3`PSA1c;ektH9:m'jNrU5:Hfq)Y=,-[V$61;GR?eS%"ZTd=>67.IGT%agVlGLGp-ZO#b
O`EQ#</PqBZ.%FAn;7%YK>#_?S][A+)hrP,D\YL?@sOe[n;IHEko.DF\P69(h1KK_#1\UcdEmd]A@\_:
laFCDgn)r>[YOP*$5*sQ24&"88q*BrpR-)`*Ygmc[ZH&XZ%Yk(+qNF2[#J#E65QK+#%&3%\\KVDM)hAl
ZJ2'rk*M:($c&;KL@(S3SN+2kl^&d<D'pNoToX$?W@)#7!?)aR%`d&oh8qHLKrpi/!(UYWB]_9/->-+Y
`X^I66(%n`aN^m`LAGF;c8^pIVr'6jlK?uZ1tJ^mqu?BGc*LjFDtPo&P@MYd$?<'oHLil;E(S=gj"iZO
UsV-PJ?"CE:p*'R!S2ZLaj2e8%iSGn!__aI`pqXEX2<jfP0(+e>&hG31TV6VKmF:&X9*6'Ti,b[\<-ge
#L8h/$\/-h1Nf0h@tIat'lo[KSd%QQhp(p):6KTt9XjWGC<Y(7Fh3sV`@@D*Wg-=MHkLNi(!b(QIFOX0
q]'DrhAu?Z5Hd_G6C[r9M2UYD1BgHUIe2K-BR-*Yl`T>.O,Z`S#EkCf0PW>Yr0<R<KsG^*4R/7JfCGkX
2I)<<%ba*MhXK<u'r_bmniHLV>$gA9Uuh!fK]TO:b6P'(LVV:%D`X#$\9fL.HslJBI,P`_a3=:SQM7^R
eFFGH5?`Ll9h&<81Po"U*4=_qdINbUcd<).Pa9@8W,ubL*G]9MM9;5Qb<'_rn!I;8hX%E,EQYL+/W]^E
Y2qUkM581opL>0p+*<X2^MqIWL/-GrKoqZ+e`P9M36flW"/0%*E]q.)c4X;%FDIch#A^/U5f7]Nr8nSS
BmE(o:7Gn?nig5@YdF='W^@(U!6P`%o(G/DT"Y(B($uQIH0]I=0)(j7lCfFVeuQ3hc<i^4eGIYa`g`s;
Q4H_*7;kA%jZ8t[/m14YUZr17[86rUP>\>*<GD>J`a;QYWq_Bh(2-)XEspgB1ErAi6Ie-b&`h<?e2;Nu
IRX&N43o$%kiJJS8nii^nhOunrdg!m;)h%p?'W>8O,KZlZHp5L=QSaZrDHq+TM0DJ;M@7'C=#\WPBFhO
/Ucc&k<a$Y3cTj7%oMm*Z'Lm>(m!bD'?Bm$288F`Mt+n%Nq&h:9T2bN>9cZ^6!hl3lQ,UHC,5Y\P#`JC
+)CfMp_)1;NUJ"?WY5leM<!-9?sf2tq+p@X1b'UX?_pU+DtkfAb(T#\d[OfS[7[3#?U#M;e)fRrSj`2$
%"En"GRR/U\G"tr;U[@qXb=-.aW&Q$1t'(9?Ai;=]DpeoOF3(@#M69#F(\b@7eb&0V0&p;PMFp>l**Wa
_fS1Q71^Z7MX'eNh*c[sQ5eH;jsO+ThaKsSAeZ"ckc.g(B-h^`PX0+]'B@"<1H`k=fqM*Q)NsM48O9@:
H5N\).VJ-a/_!K'hO<6r6f#BA*Y'cY/<9+^3EfR^q)PcIqFb"Y9:4BcH-s*#FhR/`_+:<0Cs(!`BZKPY
8\K4c'Go,07:F6R;JE(iVq!>3=Ia/Y6Zd52jr\VCjm%hho;p+o.%ctcf!cPn+r0r+am5"i'fr$&f?@MF
.+bJ&o!QVlf;Kc=CS8(ncTrF`n]#<Y>;;uQ'c^M,[!2OqW`,1c-h@EA]+ufa2<I+WYEo*CkPr=l&[d6=
c`)ok[Ll96Oe7Q(>#TO:nsil\XC:ml.\Z=[&)EltfX725+*_,>!h;1o^I,/YoB[g-nSB5L+iML$&fg;;
WQpsm(dDs;pr%r:I/XWrPlpLNpsEfF2(pQDfX4a+pV1YX/QReakGTdFROEGeY^J)$>$\n1SuRccX(sG\
G4F-YrD,"G]Bd5oB;JW:/Z/C#dUI>eCs('\.DCtX8s])W:0.%D\FEW8D4q`Gh//CrMHrC;QD(rD/Z)`*
]X7kIYQigkrLlj@?ZTP!\lW(ZO_Z\le7)DiP#IjO?WsPZ09d6Y7"k&*%IJRXZ.aG\NESF4[@e;9[.6PA
eNkXZ(39tZ(45Q^:D7ksH`k<rYL$adqmU[ebTj0;->S[h@RfltrRYPYF]#dlc_2-+EKb%@HG?gh/ZpZE
ldqsI>"j-oKGJq-T4a5Gb:00k>BhSVa&C%coOiD%<^]o1;uDP$0Nnt^9gGm^W$NNK>*Ii6[>6S.`/;rB
)N!Wll_`d`8t?4SXCoT@jjrO`r5,-bo_^'H>HMf;fhAPcX&fn>$7(+P[[N2TL99%tm15%3_36I_hhBY@
d5@V&:.3(*U-<b[YSH$$[!`GFL!?]3chqcPVs@&f]JqNH'ij"I2[7P^\\6gE>"ga;Vo>)eMTsUZ&Z<"b
;JqtFKUl:BXj-6/4.Xa81LZ!WeeeW_4G=OlG"FWep,UcQhprW/7#:q3NP6Vf0Xg,kh88bgg97`Loldp+
YrGcW[9E]HUL;+C@ooInVnFS3L8<:)N%d%Ke#)S&J=CO-(Y4Rd,LJps=-_L/95?iVYE4+1dG)1_g,A@i
9'_Ea]2ih?rR@i)#06U+-#2p=\l<^X^0A-(0hA/+2J`#!e+QdrVsS$0gD\RVm"ikA)npVYrOMoS%iH>H
SX=&dB\h]EBH8-3lJt=7[ak1mn[m_@iGl6\Dkuh+Xu[$6fs$Z/D0QJGnJ\!R1q`V>`:[i+^DkpR^?JpG
JuLK5@)hImFM[X4T^[&0,#*tX@Qp:'Qg$hCD:c%',9fa+;tA$FZo)2*F[C-iDm=HAA.feon=CE'N[p\3
\gTJl`8=^dD1bJ#hmDYU0E(HcpMT?&6+)TBqu&Y-/B:<V-ZBmqk<H49s.B%FpZCm7/%Xi[n_f9!S1IM3
_KGuO(s#mK8S)Q:1&EV,3"4G[!t7?YUg<LMDZk=nbYmm.-9*HgX*\aD=)f$_oEZ[trd%i@2*P^NNQXGB
/N4F:eJl:gkNfq20AHR21k:tBQ)mptH\,l\hF,(NTJOG7>1m>)IhU?4ik)KRj&]*@-$k7!kG,eU)&P*F
aV+l=Hf@f2O7s=bGlM?iB"<EB;r!B.@*<L*#3%W9-=X`X4.Y_e)ib#ql5BojNC^Gs!\a'RhddTrs6cbS
`inrZ'CX?"S?TbtQ'i`,^]E<'eB0]dIcYt/o%*<2BS@up?Dl`FoTts.8c6gc%UC!^dp\)fU_8%8'YNZ,
I8_+7&&7p`F,XJ#Ch06][ui&&E$d$0$7G>EO-\ng-q5>lODX6#DId5QG7PGYX.?)feK^Y>nG^FQ".P@#
[:1KCc)tFDml(+SJ$n)q&ip_FpLs2rlOSG'kTKkE.ZHgC2gn8;S63=HgobBO1R+qD0mhS"<HFWs<fL`C
n9]CbkYRdO4K^W^j8*Z_U4-+4WZI"Te_d#DcAnFGrd&YL:&jVmNMftCiGkR??XiQ8h@u%:]8$mp7AVWM
5RLiiI8N+O*%/:+L@e--4e,WbH$hqW30-j_aa'E?N*o8=ON70o\4tjD<-5hXVm8h<`U*'PESD>@>3da\
\`d,m]1(G3^RALmargT,YL+'S(.ZO9*]heT'<jtm_2H%%pr;'V2m2=S5)SBm^qLHVo:O[-s(D.E")1:R
^Q&$L[k2$<rY,2#J!8cuE=f72iH64Sj:hMQfIL42!:V(T2Vh]*E,G4hfUn5^qo&=omNu/^jRd-V(SaJS
rlRW?)ck?B$Sg?kme-P]7uoFnM>.16WEmZjfIUr0iU<2RHnZ&=TBCpSLtVf(I#UsLAMM%g:HEggfP-jQ
_=nIf7^-9uJ)^'IpE%*>kn3BcYeRD/Q/ft)e#7IZN+a^+NZ,d;"H)Z"*aUG@]Tn&[+:o(GQP\sh=mV3*
Ok54;c77\nX;GP(nM0.90Nk*.KtE3L;HCtNPa<8$-@)#>1\YPWVJZ[dGBmEGpL!p#/5GXsdd7P009N!`
jg9'(PBe4D_lS`NLe8="$;SEobtb2*/E^%Fi23(EJtP`pNE>o'"<+"YibWhd]5mT]Hb:bV>;>^2M"FI<
Zn`+N)g<],BVY'G-gicmF@J4Td@Dh;o86AkRs8bhfD4Qrgem$S.5.21qpdE'?][DhSb;R+?R!qQ@MI\1
W%0nZRP!n(NT&ckS.8#@SR\'8o&B,Qo+WP[NJ.RCI?P=M_[=#qp#eR05LdKZ?2Ej^2;Xl;dM^[pVQp='
^g$WFRJ0g4igCeXq*2*jL\KsF+)S7iX)3iqMY4im.Mo2E.*dMamil'A"s2j"5A64DWi@Td`j[@sL+HrR
qgSYnH?5)O.\ld."4i$QGNb>1s4)\c;r/aZ_#$3jpYd/88'K2nQm:-3N&`B3Rio/s[Q*6]R1T"2*<'<H
pP5?nDo9)l?LNNl#_`l@a.&X,.f:n2n"_L2S'W#f,M^7NdJHT/*TMu:IZ4j_[QT^fNEVU0i#&_<HpQ).
iqnYXM4BCcrW_6?6d'Z(h89o2-:<dHL3I<3AmFkt)+49t7?R,>S<qb][BfP&Kl0o:GS<"(ebNXg$GZ:R
>\3mX=,s*rDPtcsYK@m,af=G;ooT\0,&,-:B9CIdr,ZiLK9>@nHmh)h`BJNP(t@T)fmORGVfrP\65kQr
_2B#eqEOR;90'_H2srlWS^.CBp\#W/m@3j!)R_efM;*Wc3U?%ZR:#$rC;cenS,u8bb8T?!=)j2h;NKME
%M`'9(a?HDTfMhXERm$jTOij>nduooat6M$N:YjSmdXY<2<-i_T(!V;4n0HJ5HS1tE4Y12/m>jZ<4GoJ
I/Xf"o7'iPLf&q\c2-<&K@s7]Ac[,+=OD!0Wft;4X"(8G4c>LgMub18$7J!QZZ]sE[)19dMe>5UO>hYf
Q&FQJ-6/-Jice8'6qC=]0$N$$Lj5c@Ze(.<,ki5E=*5>T?lp;*ZrZW/n%129[L-9GdmZ3^cS&:W0f2l:
.,eM"'U>"7E/cnE\:_C5<.Z`_EOqVoY?Z>c%TS(!;n9WSd-C:Bi-FX/:)hH"Up7@0CEcA:V;-<B)K<C(
CFAK8=1;!.EZ,Q,<d=dR^cV/2lp'dSk]a&!M,Q(-Wqd'O\Z1G.d7&P'SYCcP$T,dCVKL@aLdT`Hh&G6i
jPP*Mg/kqTGog%.*;'s:4'D6J-MjcAMX#-R=qV__Oop2cLN??&/^brML5[ONU7tR"2s&"ZEqFaQZM`C)
a[VJiAGljHHSqEshO!*jOZAfWcL4.>CO<HAU3Yf@gTmP^-f/4(0db3hhYK[+Mjbo(%^2-u%kUWP89`F]
;ApfPQ&CMFX/K&8Bg0s2`OR+1e!G<JcRY!30;A_R-U3kL<?;NS6f##*',`ZdegA,TWie-`)H9X)B*_Le
L>4ZXND_kX4Rr#/-_<gseJ[VEhZocW8,_Y($eGbbhFNdkg8*Wdn;9>aF_)?X1XqKZ5W,Q7[qM)?2,!Nd
<NP5):N\b@I+3oR^"/1KAij^U=VWDZ,Hn&p#+8UQB-c"I!uL3BDX2566YYPP)ln]ai2V(^b-c"]NUlef
=__3PesB=[=#J!ec^IA87;L9X=As1MU$Zs]Q`uLDE`g^8e/;9Y%&k/#nFW.tH?uU.:m#:4eRkR]HZc:H
P-+`H,P@T"UD#IbDftHa3e^j&FQja=>$%Q)H;X=oA*F+a4qg76^CNdk$ru.errs^DnT1<%^cB$O,[=<(
CV,f`,-=tI>a31#XD=&*^'E&-oH!lQ55dMTW9940DFkSDZu)-i<ANV'G&`&9p=(*UG/5^7ANta:h(J<h
:N>V<#Q:rDlo4?=l0+]L<A&=*>ZJ<?[M88X_3m6C`Uf^(c_GMXQ?74K/on,7*8/l</e:43*1[MAalaaG
IZfaM&TpU2UFf&(X'JHB?eIjF'sAC\A0F9$PpP`.)Q1*)2mPY0??89+,@C6*g%E2190Ct8@u^jG8b$p:
PoRrt[rdd?@o?UC$6F?#'Q$0Om6]@(2>]Gl&!c1nrPA(UAjj5(B)P?FrbTfNAorjPj@]c607g,QiCd/%
&F_!Gg\C=cC3SLE^JmQL1hUqO/A6q`9rGB[DlDes^O:rBodoI+>$\V8K>Nr\bh=XKX25E_[kD1/j,Yj%
l&j#Q5\7(@Q`+fjad8rD)o@q,`L--@lB%fgKqTY/Zpq0on0Nb)QO(5lmra#73i%*t*NF6=;ft_7.sc&F
I96HteQUet83s*49@WP/DNTpnL!o?]e?I>p[T>Q)7!5O=^-m+E9YQ1++`42"b!rH!p^ZThA\hBUP):`d
p"PfE?0"YO[SQZ2Jc*-?PJ4Da02]>^Uf/M>*FGOac"$.=^Yqh!1uV),\!ZnNFHQN,Mq^\LBj-NM;2[D[
^6!^K@@%G48Gp_Xq\#%rq%AiNf.g!gc8AE_^'.?HFA1;1j\&,paHr".O=tV>(KE)fma8(9RbQuKQHH=F
%<_#B&%QMok8'>r&>;(lH^j"Y.pC2d6^ZU9A[ajW'5^.NR:pU)]?.%R?VROYk*?VE5I/$lh]LnU\OVTO
F3]TjEa,t@FJS\fZXY,\3T*L&,V?Jbg-"mF+O34<GbOB&?f6<FQZs+-Is8i;r)M92;ND#.<W*q(SSNTk
EKMgsYG_&GUY:ncH;]89=,hBaC6u!,'u#\_+ibo=8qYK4aUqj\1Y\4oJJGY>jD,7'#3Q:NA"2n]MiN`O
8K,Z#BiEcqV,l)liihG(g9WB5%&g,ZQ8FKn8D\:YB?ADone/_**P[c4\ds.TGqjqK0W`ccRH?[ll$r&g
HTkD7X,8g3Zas3-31t]5],nd<g5@LB=kc)t)R(s-HZc:@(J(s:KJU":hopfIX1GJTZB4j^0'U&?%tI1&
?_VK4Ip]<!iSp?pdJJ.DUPDn:m_cr:l/BH3J!LrC`A`'id_C.V_>Qo&l/bFlo_lKQ%ZU=SDdO8fGC>BL
Wk1_KSBpUA!S-,>lQ=%j\pJ$QJt\G8I)7hA+oT"h'g9,.*S<$7+XeBI6n`a@Z=hG-(<WiL-I">r28+=#
55LaTUIhWUApj?I6h]%u>^H4S2grHS(>-9FI&,L@mI#IEp9s,,kB=HA5jNIhT5N^7E2b1FDs+PLhn=8S
bN8i'Z:E3Z,9;fRB1MYD(0^<4(E`8m42^G<nG"CaFlT.YDd`&ohfKn]+0s1!FXiAu>O=@t?PF"l+8K"R
pi\g,2i>e:.Yi<1"Z&A*rWB;G\jfNXWIgo?VK3]kHVr1u$94;6E\8F$lT:0rK4?7!a4H+59NdD:nTjk"
k]"VU8&I9<a4H*Jn!>BRLSA_`MW49=Rb*TA>ZmQJ1F>#pV%W\"jO$`s*efAR7B.:eo/g8=Y%YoBqs/r3
H^E<.@IN)<bNnnKE1Y,=p"^h<5O--8O8f=^Z[-S;'qT84lG;qA[olWo^c:)!A>$V\c]0$/hEZ7>UXri,
.s^M>]2`OLE_&YS.RU.ZZ07JA\TuVqX`O$F(H*oUF[""(I(S`uB-n6q'@2#.0$uu+DKMu/p^bg+I!o(/
s-ODji5&>nQJ(s'.-V03#+@Y'a"t+RYu3m=l*k<l<E1%h.'[p+d85\R=6S,m>tftHgo]k>>qE2B+J*t%
N(UAKk0t:"#,F:9bU-NH/'C%\Yo_1.Vj:qbhW($$#FBF6Q_.Zn1TGNd]T&OkS`a7`;0\7=]K(!BY:G5d
1P$L?s7UQb.Fj<"rH(oun*jS2ospMPe""cpS]J2Y2t)7J/RZogC6-r2r2`jB=6e'.U'l.1%8G@WW]/pO
A&g"GK/_,uUSURmofS.2H8m-YI2JJc4/oYfR/uJ88T.TkX,s*U>'QFOTF6cOK>L#=5<#L4f?G<M?fYWW
bpC<W\tm*Fh2!m8.hXD]E3c(b;C80^&cN3,><m@tI;V[8e6-tB>HQne%#kQSn4h/%p@I6q>akGbSU5bK
(S(,'NE*\hZhSb&3&eEp<&M:-5p"p;EA=nqc+18F^,<P]"\630B2Et2U>D,o@`DPkE;a6`KX[P:rWPh-
,nb54X]?FBEYp+9Fs2lE##bD<a"a0VW&6eYS?)>W5B.JeS]ZdiLM'IcWk+ErWUoCQ2C=tqC`%/Me)uH`
\MV5C[t].klBPup:a$hql.TAMfVqB5;,QAF?d<mqFVu2UIUbTXQ$MXNi=7lg"Z_ip\99b.a_SG/Rbc7H
NMoVDr;,hfdDBohGCN\Kg<EDkUU>BD3kSTuUEI:O)IrWob4E40OcZ;U(LI$f+?u7#piel6eUJ<F"/8:J
X6KTUPFOfT[;)L`;fmiaMG!`qTI>,.dhd)b4nd@h>?*Y-8$V`T<6X^(C0Br(&a6%"SRgXXDe7P'ABCiV
rV;?MJe>3G@EOa]jiCK0EFnbjo_AFl2=$C/pIrN9Z=[.TY(?e>9T!*N>b$Hd*Te`/cABhQS9*[.=<i&?
YQEG;N$M38jq/;__/iFD>aWp:r[$T*rS0ndablLHkB?<;:Q+qd.St%gg:W?P(355]ALVWd=b70YS1S*g
fc#3;YMtr0(S%1`^7tTg51/SMHBKfqA&Eg;NK2(G0`cC9&W.gl+en)I'dJDc^5PO*PTa*7:rK5\rc"*5
P\*-O&W$6-U<sR*aEb-,OXM9.F<a?U_N)t'RU;7L]4XNTd\84DDe4<A9+d$FBQW0?6'GOjXZcq80g?A3
%N1.P8aXQ4#G][t`5P\nA@W6r)BD?\g6+Bb)q!l0n`8o/Kjh$U2chh^o^7s$'hL0nU<t"QWG(q3(pD1t
oUXp2DrKPdltH#sS!`_iI+"eb4-#&idA%G;,$<MbBW5[[o-E\Z[At!SDN%&/FK8J7bf`L4"Q./-W\<Gr
)V5a%[+#=8`em6Hq9?:-Se4t4_tGPu4aNl:oT1fBfN9a`@VkYVM+1jqHu;$CHnFq6U^K1$h3cURCRN_H
kq-.?N^fkkNgh+sF4@1\n(,#:h,Pr5N#Cs<dHK-gN(3OmAMj7aIShpO]_$f87a\/]<X_F#iZ,O'X\#$F
QPi]?aYU,J479DuLr)hTT0F=U9Diq:U3D..$g-o5Zj]_-8Zi3GfZ*&sFC;7b[VBJlVtr@%%\1C7hg[AD
oJr5,gMpZEU[<L#Mc$nR0'?N.X8XD@l?#KA*h?&RFF#!s(\:$^>,NUSf;6rAHc8$/GAHu/&WNLC58eO1
YL4a,&"V##1e4J8^6`tg`]$-JoK^i1JMFq39BgQTj>o=b\"CJj!jmt_7ujoKV_c3^k]HK'3ZU6Y6Ds"?
odlZZ3)Jr5N,pH6oK\_cO=n_&mPm6Jq;*`pc3RS=FP\ORf&f]/UXRaTQSrXSFJKU3S[ZJq\^Z^H;CXma
L69:"kttYG>u-VYf6]i^Wtj(IC8&"XI_;0#>Q-8Bh@2:PgTApN([6$P'+CEQaNl_u:=h.qdi"?rFLpVD
F?9!LKhduI6`6tDU!jlHD4N;i8>l&;S9;DQCT5d6P')'nl1Dbt)r--Fr3G3@?2#\BdriiR7Ji]]VmCSE
g9(BfXS\dAW>2M`WA1L'DuE"nY+&eXg'\l]Wg$SU7;OS:fDc:=T2l0Q=[=sol-L^8*+LV8<,>S$07t5.
m&LdoUX.C!EU<pQp.O#PEU`VYc*W\=/kXQ=QScXdN<5M"flbdU93,EAY1rFE,O=K1pt>h97c5#>>OQ\s
\0oe%))o\/+tRWFmpA2l@=$su0iouK9l2/F:D3EEh)ACodeO0E1;)c&dk\Su;9HS7\P<h,L`Kc!VFCfZ
VQ<u0+a7=5m0-Nq:t,^$?9AO!HU-)Fi]em@\oP'pQQ<4%LX%LMk[6qJ1LP"&dZ-LYh59%;bY-Kh^_Tr7
'!5YPE:(Ut1Sa[:>^<h#%Jmi*^/oFpYBF0=5LWUn[4]9<;jK8`o-2)r2J@hUZGPDg7r*R"Dl&^BSB9p/
2j%kd8/u:6Q8lDJd,rMMA.O]#;5ga*C("R:1D@a8(\8>GHSL:3D)e4%]tN.'28Xc/][kt">-caF*O9',
-HNA+3q+(aft"=rlhoFse<)?N)O%a]c;TU:IH%X=U*eqA`RMa@hK!F8r<%P(;"6X`%W-nnBYS'I4pU5@
XjiN0Kj>?PeUmN5\=XA6b?!S4Dl8_.;.&D(;HQqaG#\1JY=!.,^BsAuOn\Z<]L?EPrMfaV3;P#(rpb_H
o^B%ZXSt@$?2!`lSVnOGcVO5;]p-O)a<7K9r@=$Uqc@<JZ@>@3?[:IEC-FQ1_+3hW0&.C(=!H?2>`$i`
jpk%l=qe9D:ZP+NC=iB#a)3lBMUG"s@/mk$X7S19[.-d-d'+jD?#P3-Ii]K.`)IKiXb)CgfZ[m/s,R+6
VQubnA&l*fW[Q+\`r%`>a*][blu*aHht<E!kJ=VG1Ond142TE>=NCS`nY%*"7X`8Q`4et6aL_V$h&(u8
YPJF#ZNoCV-\k5_bV)NgIi\IVp),hMa:=CIGlJkkj=KBq+I^N!_"dZD08tbc[,g9#BV2;nSS)RZGfD1$
TXK)nOR8a)ouHkh$9<[!M#`Ci^f7`P'":TnanG"d\:5OF<CXQElXETk[7FF9e6^sKNo!1g,hhGYQG_^q
acI-q@7#R(m?N&8Dp0+a\l""3`;AkfT!ZP!o%m`oS.?:8i+s:TEq*\H"X*uf`L!uZVjTE,f4(+?#T4o=
h,f&RhcrLlb-K)i<r50+mprl@_V>ho>XIa=='S(:lSOq>ns(K,#=75>E!\ND`@9VMVHRQ.$Hcd5oo324
%:=i9_:H]XJ'W%\T+I?=")\"?K:>Ud1GdV[!Dd;?M#+<"9'72,^DJ5F@r$mu0LJT>N'0KSJRTBHj:MqA
bTeg#5#48)"4T;4oi+.0Y?M>qT>@:M"r#n9*di\;7(*3B9&qH>33R1W@k'<\F#%_,IrI5na->J\s,&!"
YIF&s;D+*Gr/4q?,oZLkKMTosruJ`NogJ=J`=p3H't?dF]Dal%?E1sTiPFZbo7sEBmIn@m0G\O//M._o
j<a%4@WU;&#Fep?Y:jCi\]$5<jqc9Kc7jbK2p\?pIX,WY\(>DCJ(AP9FBQ67Qi(;7ceTNtB>@&KjDQ,L
Esq(&)L$5tka2(/*mY9U/FO..m/=I8$/Y7O]Ho0g2uG^lL=gEZTj)2T*R1>f"6;FG4B)pm=eOu0n759$
rGn#^?dH+ooiC<S[7E>Zm0sN;%nPt7-#9`7M,[."kX^L!XneYCA;cCUL'Y+ErZupVk?8@AX/)O,8V/Vd
Ac;r*5I@S0<Pl#d$8,fcL0Hmq&VRW"0#P:^R&5+tBA4<b>8eQ20G]f*[XmsYnZ%*NH)O1mV9DB8In@)L
V30n]a_93GAs6n9WR>[,cnoZAbBJGTnF7G-4lkM8A)'m)m[T!%Dj)6t*6/6)N%ks+qU?sBn4f$ZaE+t7
?$gMN7tiAQNM##6jcQ2U+4d!K3r#R/U(:>r4hg($ceR8I&m'OZVCKfu5do[lN#(EjDrf!>mF<,V\0,0p
YZB?rbQX(q>CM/7^\:Z14JJ>2XCgb&aIn(AEEK$\5bRWNPC=XW\JcuH1P44P)']C`7[&"+F`n&FQ-:N$
*m4bu$RhuOe[J/A4Y<#Eh_*%T/KDt75n:F`%\J74qVCn'.MC>"[>iB=JKu%?gm^cV!bib!HZ9#JXcc;*
G1hc-!=ebPh4'fb>u\p2InJ/q4&X`T_TJt0J?gD71:M0HP'net-<n4.bI21"2s,lDgh#t#NQ)TL^#`$/
6nN55=WIi^l=0)pjBk\sV7`/XQM/")8M600A"Jb$]"&n>rS532n5+rkZThtEH4RD]di^[UfcR_^9[;S<
))02Zmku"WN&aR-Z)o7;JZn='=_Ba]-r\e(SbY7Hm4\*&1EN&VjR1[s7rRA_Uh6^hUMOJVYKZMcSPmR-
(E03'0]=LPdRVMKP0"$ASB4,K*G^[!MR:ijIea6%jml/&Rh!'#n6qYV_If!Y_bpFe]__hU-SM:cf^*WL
KI?\?YW.S"gKX[kNr!]rhf25nF&i5\n)phKqS!o@qJ>U^Nq[BC2L`GFb2+1a0oC!`kl4^HJTkGP/Et#<
!B4<d,"l0Qjs<'9Z]GJMJP^/<9t(Zoe@(ol8f,nL4PU?ckZ(Z$@X]:I9^b!0.WQc<c[c@?[mAec_E,r/
4?25Co3gr54d8b%H\BW/[SMWU+j1lY>AmIkgRjIK?/@8:l$(G]J%<,O1A=kI+8pj$';bE)!9X9^98W*Q
)(?IL0Q[MJ8[L9EoBL[/SST,[NW+pBZV;LgD7%?\5UQrEDu4M&V)[8M0+2=#Nu;##hXQ2GH/hJA:UX]p
^ODS8TAhZ^8*k"Hp(B?[rh,5?X?i.<nD<-G_\,b\S!e.9`t.l>4_R<Lp^Ttuks!==pra)Q5KU.KP"atA
'BHTXG0XUDBP1ItBg:4jA=Fs[$?.4b@rNY&N`ZN.j3W)Q>?YGpk;V8NYk>hF0oI[3Sd0P>Y40=V<R0Ys
[.A8b`!<O7jK,O&D(+'nCs%Wqq'LI7V.Af]Waso$=YC/p:eKes`:4OBHb*'ph-MXfY;\J(V(d0Q;dU.Z
IDdN2Nq:)P.imuB49\93pd%.>XOPbCb4R4_auRd_*F@FLr0@41jh:&7K@$UqA5!O3#r]puND[f?R5HJS
7cdM'r'3Sq(@"ih_C<fAi'^n/=2FZHhBfI4;O30pPW9Vp8/bf&@,,OI_2[K^aWc!"'WdX+A)i]A-J)@h
GZ/HlNU\dS=^r_b32J]aD2F)_/tAN.rb1[s:5_mdPXO\<SP*u)IMf?'J")6!a.lKNFY]KT%g;d0XP3jq
j(n*Qc_\YdpX@K0Lbst:L!O*6Qch<ke.Gr6(!!9mK?5fMmr!urb[[q5P&=^Ekhe2.nnFA<Sa`'#XT+<U
rdTI%ERV9G,cU]a0G;?fC%H%g^n"i7UAc\"F-F&Q_%4'n[s+Ql>^7*M5Vl#EcHcAiba_?'Y;e\GdAP_B
r,luN<:p>(C,02dHr:&s'9[Wg;C6V(`nL6t2QEGmpV(p7p`lV>U]1j+_)i/&WW"u6r4h+."!bb>3dVY'
rT.5XoP>Tt[,3Q<GeGia!B)F_(UqthK8^OsRCh9[NohCbD3/N\r0@GEC8"qBHKCh7=dAO#%eQuap-^F<
&Wd34,M<`7U.1>ZDo,S*kh!N,1["`6GML/sgu(E4Y2J%U=tAJ@fa'\>6go<pWN[*8Y9;TH3c9oqc)\Lb
CXm@62Qi77\%Y#T!)W3X(c,;7IJ7(n8fm#Znbjua_i1<AdrIA,..L^g?,ioENa;Z<(eE,mI>m%VZMeq8
j*XK-[nf'1b(.S2]iF/`XK,P>$XZiE11Vq`C$iJF\%:E`L:ag3(G&0u(S]s"iZ$j*8Un[oZ>g,h/Z7.K
#(3`d[&WGTIs4CYDoB9l<G2@q;0SFh(N$*]+(%De,AZ+Q^Goi3jW_G25tQ1idfM*Vq5XD'MpZ6LDq'=,
'g.T>64@I5TFA!ZoYatX3t9D3h!nLVQSQX6ZJ48r,k$&EU?<aUV[K=XP4NrMh"HTf,*lU^8ZCu8Q6C3S
o_a5pH-PlT9KqBS7Uq8c`:@9J3?'PDCB#tKnrH](l0<@Z2O]$:]]@M++(jJ$7u*pg+[:0aCiGRk&8k!O
N6Q_3pIsH>>e$uiVoAr.mDHF#1n06kC2"1,`4&tUDZNbQS4f"oB.\![Y,WAq0(eXdk/(+ol<%%bDrI/6
>qsI9n]%$K*+DBI^,Oq6;=@cTQ&XOBP"VS<$a$(kAC;<SGBn-*_Dla//T`\[JFg9aAWb.p929"[c70LV
$R98NnH*BCI+!ugW@ps.e\>AZ(<oMB`Fc55nFSCM896@=&g:jrAm@un<m@OFLq6[*-i:Kb]#>_[\ao>X
,ME]M@R/8sE[&s?H#'C3IK^e/=ir,)j1YRrXa3U8>+>0Ja%3d?P8Vb'Gg@lrg-s^?1U\QNMaf-fPF`.o
;oX3D,\FToQIe7]b0h,HDD.!B+smBSne&Y/bePg<4r>:\j4hNnbZWH;fZD2fU.`qM?T/`:@,Ri#i\&jK
)<&W+I>g>B44iJ]<pXRh0kR]R8T%C_Ln+FeFCtA:*uOt8*_7pBUq0r=A:..%JtY8?-&_Rb[@>g$PMTJ"
G&1*;2P$,(R<_W0?DAQ&_0pI*[0ufMX]rI7*_EJiHb*Gp^#lSA!cGekILgI+#ii!%N0q^JjTRl[?0h@U
GhQaB?KBQ]?[+M&q=K[^I*0XEM)iBK$:aN&oQf/qbaCbEXha&+d+s^"40/.ZRH)3_en"/<4,3L>q-cbV
^<";E1sJ4LCmV^4?*j/<L]!;mp+%b-/A9(km)R]#M0u#0fl'pi$SO8q7f@P$bZ@AR='BmaLcMLj7c]-n
\fu32ItEN'H2Pj@q,4nho!0gbcTAOt=??+UE;F*L]4QK![0thD`n9oYN?^:()FJnlQ4Y>0pmSPLD,(Vd
fi!bha(ICaCZ5epHBu*lfdHAYF,k!@8iEqhH11b_eNX!Q.0nV[_;cb0Ui?f8dT^a8h&&A9kbD9M?c(I0
7-8?"q%C5?6Y*RB2SPP#9=+%k/.M(,)qiAr?7f1EA81ShW'nn47,=S?<]`sNgXD.hY6i[En1X$!Cd`4#
M7T3-?-hVM1`"h=$Vh,_`V*UNpT/5L4=D$!!K!c2<W\\<j\'DXqZ9qEOQH%(P<G*'#[_&9Eqp?[%HD-!
@S6baHDtP,='sQZ>3pI/OAbidS!3YsDO*0,`TOld?U?`"$.Y=pTdRsKn7^I@Yl(=LUP7s/rNb?R]e-a'
.qpl8p2PHda[Np3B&nSlM1b`]VQh"b'!2shFI&dE8ToaK2gpNfCaZ)-g)IbZB9Rb#NEH`D>fh]66tUgD
p_'"B8+Ir]j+ZPe(tk68\EZXjn,=Wbgsb\66smfH:-,7OBf]rNp!0C?fjskqH>[VlZgo^!7q`>-&hj+C
dk')*VdpD27R$@o_)3q$q2QQO$0:jZ%<nE*7K%EM(3U_%(3^e&(-1.HNnn%2D76cDJ8_bb)cUgU[TAB]
fcN/uZ4-<b)qNa_k?Im1>pNIUqIY$'okJc[lohWLrT0lgh7lI@f(!P&UFFO!M4^!<pCWT<b7:j+I3<RK
gAcoB(MMUJY4hTn^\tiXYQ';^f)?`d6`gmQI+9!L^6C87,j`r2r"hQk8NJSV(4)-='3[RJ1jNL)HafN`
m$T./c0csoNZFb]NmI'-lh:),])=ecj7mt<WpUS=l8_Mknp0d"3(d'%Xl762HM`&Ych.!Z@Q1^V/S'A"
i%2=lFNoaslRU;.B#rlY/<[b<hU^`U8M8uj"dZn>3,+m8J$7PRPhp:d?&I>.o&qp:__,Zp[T^a7CHGKO
1Angtbl_0;]p6@a]'@Gq'+TMqft)sRf6fVX%NG!/pR85.,hCdm"shLMUUScTPs2V\iU;oboD!Z.^AU4>
>YF^Xa'ql9[LB,KSrf]a4P;rX-@%MOomp6^Zb!'HNBNi'B%QG,[?=WBeUtc@fpd+Y00XMJh<M\Y*7XdA
g\@j4Ec&PNNit&g\;:0KDh=OQ2Cd\h=D-u0\]dQ!h,;jbh/0?"DL>=tHRZ(6mF[RVZWi@P[!1c3BoB@1
mX3D5j41>]]\_/Q^dY>lrUfQ`DlWe"_gWZ:34lg:p;!Sd.l_qF;Yq!kTD]#hYM"+5O775`Y=X)NJ*hA[
I-R?ng!u\f7i]7te'q.Z$BIt`iO_8!X7$gPK(&F*gNS#@RBMsY>\Xc^1S<Ht]uAha]kSHU1q_buod7,p
m_*(uD6tE]AI3P+r&[(Tkl#Y]n((>@Z`Om7Y!,()/_o/41&(Fh;2qE=^Y7j2F$e]*9;L=OKuH+>Oim8;
5!srD).ED+#7Yaq$d2`*04j>4\LU_:ne[GZi>-\tm.9S'lJ&sDLX$0;$TX_?\9fUB%c.(H-O[n<B`ri*
g,+E)?aJ==fjV>o*^n89\>6'geE?A"J\mmW&"]iOhu*8mDYpuS#OQhQpV+6L$Af.S=i\9V/eC0iH.USN
QY)FW?;cdV*d`m[M:,s"0c`9EkPBjWWH;Dt]&ZM@&+1"^H'/C1Rd@[M_rp@6,,;ecdJN[Fn7J!C/VW).
N<cTUdJo$AiB6ID^hXoW*jG;'(p^fIPK)+r_i?iAB,MGrnafX.Rr->WHM5P2T'>sdVn]%C8"\IOi!Q"J
GK<UTg4S`THI>@F;P*kcTKRJNH:;_+BHbsS\CMYb:@5eI*BmO.l=W;.;ML]7fGTE\::+^0/"ZGKiD)$m
^U-20NFm#@>-3:-oVJJ*I'lVebF..EoD2&sC/;TU8DP:BUshrHk7?aS7]/<q7pu*ce)(b7hFjDV*U$GQ
//b)KpSS8E$KJF7d9D%$-q*Ih2BeKD[ed4(%6IHD\jp\BpA]\DqJSUlIGF;kVO)f3prC]fhpqc(a$9Oo
f73b>^ZUZms6l$@VeZ8'pHS]V?iT6fpTE+4"O=(<bknC"k9$q.mCrSSO8o,qn_\fn*uFoMFo~>
endstream
endobj
7 0 obj
40412
endobj
3 0 obj
<<
/Parent null
/Type /Pages
/MediaBox [0.0000 0.0000 537.00 194.00]
/Resources 8 0 R
/Kids [5 0 R]
/Count 1
>>
endobj
9 0 obj
[/PDF /Text /ImageC]
endobj
10 0 obj
<<
/S /Transparency
/CS /DeviceRGB
/I true
/K false
>>
endobj
11 0 obj
<<
/Alpha1
<<
/ca 1.0000
/CA 1.0000
/BM /Normal
/AIS false
>>
>>
endobj
8 0 obj
<<
/ProcSet 9 0 R
/ExtGState 11 0 R
>>
endobj
xref
0 12
0000000000 65535 f
0000000015 00000 n
0000000315 00000 n
0000041155 00000 n
0000000445 00000 n
0000000521 00000 n
0000000609 00000 n
0000041131 00000 n
0000041609 00000 n
0000041325 00000 n
0000041364 00000 n
0000041466 00000 n
trailer
<<
/Size 12
/Root 2 0 R
/Info 1 0 R
>>
startxref
41682
%%EOF

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

@ -1,19 +0,0 @@
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 the pre-requisites first:
```sh
cargo install mdbook --locked
cargo install mdbook-linkcheck --locked
```
After that, you can build the book with:
```sh
mdbook build
```

View File

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

View File

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

View File

@ -15,7 +15,7 @@ action commanding could look like.
2. Target ID and Action String based. The target ID is the same as in the first proposal, but 2. Target ID and Action String based. The target ID is the same as in the first proposal, but
the unique action is identified by a string. the unique action is identified by a string.
The library provides an `ActionRequest` abstraction to model both of these cases. The framework provides an `ActionRequest` abstraction to model both of these cases.
## Commanding with ECSS PUS 8 ## Commanding with ECSS PUS 8

View File

@ -1,5 +1,3 @@
<div id="communication-chapter"/>
# Communication with sat-rs based software # Communication with sat-rs based software
Communication is a vital topic for remote system which are usually not (directly) Communication is a vital topic for remote system which are usually not (directly)
@ -17,16 +15,10 @@ it is still centered around small packets. `sat-rs` provides support for these E
standards and also attempts to fill the gap to the internet protocol by providing the following standards and also attempts to fill the gap to the internet protocol by providing the following
components. components.
1. [UDP TMTC Server](https://docs.rs/satrs/latest/satrs/hal/std/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. UDP is already packet based which makes it an excellent fit for exchanging space packets.
2. [TCP TMTC Server Components](https://docs.rs/satrs/latest/satrs/hal/std/tcp_server/index.html). 2. TCP TMTC Server. This is a stream based protocol, so the server uses the COBS framing protocol
TCP is a stream based protocol, so the library provides building blocks to parse telemetry to always deliver complete packets.
from an arbitrary bytestream. Two concrete implementations are provided:
- [TCP spacepackets server](https://docs.rs/satrs/latest/satrs/hal/std/tcp_server/struct.TcpSpacepacketsServer.html)
to parse tightly packed CCSDS Spacepackets.
- [TCP COBS server](https://docs.rs/satrs/latest/satrs/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) # Working with telemetry and telecommands (TMTC)
@ -39,12 +31,8 @@ task might be to store all arriving telemetry persistently. This is especially i
space systems which do not have permanent contact like low-earth-orbit (LEO) satellites. space systems which do not have permanent contact like low-earth-orbit (LEO) satellites.
The most important task of a TC source is to deliver the telecommands to the correct recipients. The most important task of a TC source is to deliver the telecommands to the correct recipients.
For component oriented software using message passing, this usually includes staged demultiplexing For modern component oriented software using message passing, this usually includes staged
components to determine where a command needs to be sent. demultiplexing components to determine where a command needs to be sent.
Using a generic concept of a TC source and a TM sink as part of the software design simplifies
the flexibility of the TMTC infrastructure: Newly added TM generators and TC receiver only have to
forward their generated or received packets to those handler objects.
# Low-level protocols and the bridge to the communcation subsystem # Low-level protocols and the bridge to the communcation subsystem

View File

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

View File

@ -1,14 +1,13 @@
# Library Design # Framework Design
Satellites and space systems in general are complex systems with a wide range of requirements for Satellites and space systems in general are complex systems with a wide range of requirements for
both the hardware and the software. Consequently, the general design of the library is centered both the hardware and the software. Consequently, the general design of the framework is centered
around many light-weight components which try to impose as few restrictions as possible on how to around many light-weight components which try to impose as few restrictions as possible on how to
solve certain problems. This is also the reason why sat-rs is explicitely called a library solve certain problems.
instead of a framework.
There are still a lot of common patterns and architectures across these systems where guidance There are still a lot of common patterns and architectures across these systems where guidance
of how to solve a problem and a common structure would still be extremely useful to avoid pitfalls of how to solve a problem and a common structure would still be extremely useful to avoid pitfalls
which were already solved and to avoid boilerplate code. This library tries to provide this which were already solved and to avoid boilerplate code. This framework tries to provide this
structure and guidance the following way: structure and guidance the following way:
1. Providing this book which explains the architecture and design patterns in respect to common 1. Providing this book which explains the architecture and design patterns in respect to common
@ -19,7 +18,7 @@ structure and guidance the following way:
3. Providing a good test suite. This includes both unittests and integration tests. The integration 3. Providing a good test suite. This includes both unittests and integration tests. The integration
tests can also serve as smaller usage examples than the large `satrs-example` application. tests can also serve as smaller usage examples than the large `satrs-example` application.
This library has special support for standards used in the space industry. This especially This framework has special support for standards used in the space industry. This especially
includes standards provided by Consultative Committee for Space Data Systems (CCSDS) and European includes standards provided by Consultative Committee for Space Data Systems (CCSDS) and European
Cooperation for Space Standardization (ECSS). It does not enforce using any of those standards, Cooperation for Space Standardization (ECSS). It does not enforce using any of those standards,
but it is always recommended to use some sort of standard for interoperability. but it is always recommended to use some sort of standard for interoperability.
@ -31,10 +30,10 @@ Flying Laptop Project by the University of Stuttgart with Airbus Defence and Spa
It has flight heritage through the 2 mssions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/) It has flight heritage through the 2 mssions [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/). and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/EIVE/).
Therefore, a lot of the design concepts were ported more or less unchanged to the `sat-rs` Therefore, a lot of the design concepts were ported more or less unchanged to the `sat-rs`
library. framework.
FLP is a medium-size small satellite with a higher budget and longer development time than EIVE, FLP is a medium-size small satellite with a higher budget and longer development time than EIVE,
which allowed to build a highly reliable system while EIVE is a smaller 6U+ cubesat which had a which allowed to build a highly reliable system while EIVE is a smaller 6U+ cubesat which had a
shorter development cycle and was built using cheaper COTS components. This library also tries shorter development cycle and was built using cheaper COTS components. This framework also tries
to accumulate the knowledge of developing the OBSW and operating the satellite for both these to accumulate the knowledge of developing the OBSW and operating the satellite for both these
different systems and provide a solution for a wider range of small satellite systems. different systems and provide a solution for a wider range of small satellite systems.

View File

@ -1,24 +1,16 @@
# Events # Events
Events are an important mechanism used for remote systems to monitor unexpected Events can be an extremely important mechanism used for remote systems to monitor unexpected
or expected anomalies and events occuring on these systems. or expected anomalies and events occuring on these systems. They are oftentimes tied to
One common use case for events on remote 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. They can also be tied to
Fault Detection, Isolation and Recovery (FDIR) operations, which need to happen autonomously. Fault Detection, Isolation and Recovery (FDIR) operations, which need to happen autonomously.
The PUS Service 5 standardizes how the ground interface for events might look like, but does not Events can also be used as a convenient Inter-Process Communication (IPC) mechansism, which is
specify how other software components might react to those events. There is the PUS Service 19, also observable for the Ground segment. The PUS Service 5 standardizes how the ground interface
which might be used for that purpose, but the event components recommended by this framework do not for events might look like, but does not specify how other software components might react
rely on the present of this service. to those events. There is the PUS Service 19, which might be used for that purpose, but the
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 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: can generate events, and where other system components might be interested in those events:
![Event flow](images/events/event_man_arch.png) ![Event flow](images/event_man_arch.png)
For the concrete implementation of your own event management and/or event routing system, you
can have a look at the event management documentation inside the
[API documentation](https://docs.rs/satrs/latest/satrs/event_man/index.html) where you can also
find references to all examples.

View File

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

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?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"> <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--> <!--Created by yEd 3.22-->
<key attr.name="Description" attr.type="string" for="graph" id="d0"/> <key attr.name="Description" attr.type="string" for="graph" id="d0"/>
<key for="port" id="d1" yfiles.type="portgraphics"/> <key for="port" id="d1" yfiles.type="portgraphics"/>
<key for="port" id="d2" yfiles.type="portgeometry"/> <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:Geometry height="509.9999999999999" width="768.7000000000003" x="579.3105418719211" y="304.7"/>
<y:Fill hasColor="false" transparent="false"/> <y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<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: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:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
</data> </data>
@ -31,7 +31,7 @@
<y:Geometry height="60.0" width="203.0" x="814.0" y="506.6799999999999"/> <y:Geometry height="60.0" width="203.0" x="814.0" y="506.6799999999999"/>
<y:Fill color="#FFFF00" transparent="false"/> <y:Fill color="#FFFF00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<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: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:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
</data> </data>
@ -39,10 +39,10 @@
<node id="n2"> <node id="n2">
<data key="d6"> <data key="d6">
<y:ShapeNode> <y:ShapeNode>
<y:Geometry height="60.0" width="92.24000000000001" x="607.36" y="413.23"/> <y:Geometry height="60.0" width="82.0" x="617.6" y="413.23"/>
<y:Fill color="#FF9900" transparent="false"/> <y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <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="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 <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> 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:Shape type="rectangle"/>
</y:ShapeNode> </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:Geometry height="60.0" width="76.55999999999995" x="988.5" y="335.62999999999994"/>
<y:Fill color="#FF9900" transparent="false"/> <y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <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="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 <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> 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:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
@ -63,10 +63,10 @@ Creator 2<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:
<node id="n4"> <node id="n4">
<data key="d6"> <data key="d6">
<y:ShapeNode> <y:ShapeNode>
<y:Geometry height="60.0" width="87.27999999999997" x="845.9410837438425" y="335.62999999999994"/> <y:Geometry height="60.0" width="72.55999999999983" x="860.6610837438426" y="335.62999999999994"/>
<y:Fill color="#FF9900" transparent="false"/> <y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <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="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 <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> 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:Shape type="rectangle"/>
</y:ShapeNode> </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:Geometry height="60.0" width="87.27999999999997" x="1112.52" y="335.62999999999994"/>
<y:Fill color="#FF9900" transparent="false"/> <y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <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="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 <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> 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:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
@ -87,10 +87,10 @@ Creator 3<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:
<node id="n6"> <node id="n6">
<data key="d6"> <data key="d6">
<y:ShapeNode> <y:ShapeNode>
<y:Geometry height="60.0" width="145.19999999999993" x="734.2060377358491" y="620.26"/> <y:Geometry height="60.0" width="126.0" x="781.0" y="620.26"/>
<y:Fill color="#FFCC00" transparent="false"/> <y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <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="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 <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 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:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/> <y:Shape type="rectangle"/>
@ -100,10 +100,10 @@ Event Reporting
<node id="n7"> <node id="n7">
<data key="d6"> <data key="d6">
<y:ShapeNode> <y:ShapeNode>
<y:Geometry height="60.0" width="136.8599999999999" x="901.8" y="620.26"/> <y:Geometry height="60.0" width="118.63999999999987" x="928.2" y="620.26"/>
<y:Fill color="#FFCC00" transparent="false"/> <y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <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="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 <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> 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:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
@ -112,10 +112,10 @@ Event Action<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel>
<node id="n8"> <node id="n8">
<data key="d6"> <data key="d6">
<y:ShapeNode> <y:ShapeNode>
<y:Geometry height="60.0" width="97.27999999999997" x="787.1260377358491" y="733.8400000000001"/> <y:Geometry height="60.0" width="87.27999999999997" x="792.1260377358491" y="733.8400000000001"/>
<y:Fill color="#FFCC99" transparent="false"/> <y:Fill color="#FFCC99" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <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="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 <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> 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:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
@ -124,10 +124,10 @@ Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Model
<node id="n9"> <node id="n9">
<data key="d6"> <data key="d6">
<y:ShapeNode> <y:ShapeNode>
<y:Geometry height="218.79999999999995" width="256.8800000000001" x="1068.6599999999999" y="575.0400000000002"/> <y:Geometry height="170.79999999999995" width="210.80000000000018" x="1076.84" y="601.88"/>
<y:Fill hasColor="false" transparent="false"/> <y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<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 <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 1. Event Creator 0 subscribes
for event 0 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"> <edge id="e0" source="n4" target="n1">
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="15.418916256157559" sy="0.0" tx="-10.5" ty="0.0"/> <y:Path sx="8.058916256157545" sy="0.0" tx="-10.5" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<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 <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> (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:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
@ -161,7 +161,7 @@ Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Model
</y:Path> </y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<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 <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> (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:BendStyle smoothed="false"/>
</y:PolyLineEdge> </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:Path sx="-23.719999999999914" sy="5.5" tx="87.56000000000006" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<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 <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> (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:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
@ -187,8 +187,8 @@ Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Model
</y:Path> </y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<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) <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="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> 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:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
</data> </data>
@ -196,10 +196,10 @@ event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false
<edge id="e4" source="n1" target="n6"> <edge id="e4" source="n1" target="n6">
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="-80.36000000000001" sy="0.0" tx="28.333962264150955" ty="0.0"/> <y:Path sx="-65.0" sy="0.0" tx="6.5" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="19.693931579589844" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="96.35763549804688" x="-105.378832397461" xml:space="preserve" y="15.634602566150534">&lt;&lt;all events&gt;&gt;<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="57.20000000000004" distanceToCenter="true" position="right" ratio="0.4441995640590947" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel> <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.16456604003906" x="-98.78228302001958" xml:space="preserve" y="16.63042580701972">&lt;&lt;all events&gt;&gt;<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="57.20000000000004" distanceToCenter="true" position="right" ratio="0.4441995640590947" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/> <y:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
</data> </data>
@ -207,10 +207,10 @@ event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false
<edge id="e5" source="n1" target="n7"> <edge id="e5" source="n1" target="n7">
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="32.38107215104537" sy="0.0" tx="-22.34892784895453" ty="0.0"/> <y:Path sx="42.660000000000196" sy="0.0" tx="-29.359999999999786" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="19.693931579589844" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" underlinedText="true" verticalTextPosition="bottom" visible="true" width="96.35763549804688" x="13.821211921553186" xml:space="preserve" y="16.782337120427428">&lt;&lt;all events&gt;&gt;<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="62.0" distanceToCenter="true" position="left" ratio="0.492249939452652" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel> <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.16456604003906" x="20.4177438354493" xml:space="preserve" y="17.885881494816203">&lt;&lt;all events&gt;&gt;<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="62.0" distanceToCenter="true" position="left" ratio="0.492249939452652" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/> <y:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
</data> </data>
@ -219,11 +219,11 @@ event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="653.48" y="536.6799999999998"/> <y:Point x="658.6" y="536.6799999999998"/>
</y:Path> </y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<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 <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> 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:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
@ -232,10 +232,10 @@ event 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultA
<edge id="e7" source="n1" target="n4"> <edge id="e7" source="n1" target="n4">
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="-35.69940886699487" sy="0.0" tx="-9.780492610837314" ty="1.5"/> <y:Path sx="-35.69940886699487" sy="0.0" tx="-17.140492610837327" ty="1.5"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<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: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:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
</data> </data>
@ -243,10 +243,10 @@ event 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultA
<edge id="e8" source="n6" target="n8"> <edge id="e8" source="n6" target="n8">
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="28.960000000000036" sy="0.0" tx="0.0" ty="-21.423522388059723"/> <y:Path sx="0.0" sy="0.0" tx="8.233962264150945" ty="-21.42352238805968"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<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 <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> 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:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

View File

@ -1,49 +1,23 @@
The sat-rs book The sat-rs book
====== ======
This book is the primary information resource for the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs) This book is the primary information resource for the [sat-rs framework](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
in addition to the regular API documentation. It contains the following resources: in addition to the regular API documentation. It contains the following resources:
1. Architecture informations and consideration which would exceeds the scope of the regular API. 1. Architecture informations and consideration which would exceeds the scope of the regular API.
2. General information on how to build on-board Software and how `sat-rs` can help to fulfill 2. General information on how to build On-Board Software and how `sat-rs` can help to fulfill
the unique requirements of writing software for remote systems. the unique requirements of writing software for remote systems.
2. A Getting-Started workshop where a small On-Board Software is built from scratch using
sat-rs components.
# Introduction # Introduction
The primary goal of the sat-rs library is to provide re-usable components The primary goal of the sat-rs framework is to provide re-usable components
to write on-board software for remote systems like rovers or satellites. It is specifically written 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.
It should be noted that sat-rs is early-stage software. Important features are missing. New releases
with breaking changes are released regularly, with all changes documented inside respective
changelog files. You should only use this library if your are willing to work in this
environment.
A lot of the architecture and general design considerations are based on the 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 [FSFW](https://egit.irs.uni-stuttgart.de/fsfw/fsfw) C++ framework which has flight heritage
through the 2 missions [FLP](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-instruments/smallsatelliteprogram/flying-laptop/) 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/). 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. The [`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-minisim)
applicatin complements the example application and can be used to simulate some physical devices
for the `satrs-example` device handlers.
# Flight Heritage
There is an active and continuous effort to get early flight heritage for the sat-rs library.
Currently this library has the following flight heritage:
- Submission as an [OPS-SAT experiment](https://www.esa.int/Enabling_Support/Operations/OPS-SAT)
which has also
[flown on the satellite](https://blogs.esa.int/rocketscience/2024/05/21/ops-sat-reentry-tomorrow-final-experiments-continue/).
The application is strongly based on the sat-rs example application. You can find the repository
of the experiment [here](https://egit.irs.uni-stuttgart.de/rust/ops-sat-rs).
- Development and use of a sat-rs-based [demonstration on-board software](https://egit.irs.uni-stuttgart.de/rust/eurosim-obsw)
alongside a Flight System Simulator in the context of a
[Bachelors Thesis](https://www.researchgate.net/publication/380785984_Design_and_Development_of_a_Hardware-in-the-Loop_EuroSim_Demonstrator)
at [Airbus Netherlands](https://www.airbusdefenceandspacenetherlands.nl/).

View File

@ -1,11 +1,11 @@
# Modes # Modes
Modes are an extremely useful concept to model complex systems. They allow simplified Modes are an extremely useful concept for complex system in general. They also allow simplified
system reasoning for both system operators and OBSW developers. They also provide a way to alter system reasoning for both system operators and OBSW developers. They model the behaviour of a
the behaviour of a component and also provide observability of a system. A few examples of how to component and also provide observability of a system. A few examples of how to model
model the mode of different components within a space system with modes will be given. different components of a space system with modes will be given.
## Pyhsical device component with modes ## Modelling a pyhsical devices with modes
The following simple mode scheme with the following three mode The following simple mode scheme with the following three mode
@ -13,8 +13,7 @@ The following simple mode scheme with the following three mode
- `ON` - `ON`
- `NORMAL` - `NORMAL`
can be applied to a large number of simpler device controllers of a remote system, for example can be applied to a large number of simpler devices of a remote system, for example sensors.
sensors.
1. `OFF` means that a device is physically switched off, and the corresponding software component 1. `OFF` means that a device is physically switched off, and the corresponding software component
does not poll the device regularly. does not poll the device regularly.
@ -32,7 +31,7 @@ for the majority of devices:
2. `NORMAL` or `ON` to `OFF`: Any important shutdown configuration or handling must be performed 2. `NORMAL` or `ON` to `OFF`: Any important shutdown configuration or handling must be performed
before powering off the device. before powering off the device.
## Controller components with modes ## Modelling a controller with modes
Controller components are not modelling physical devices, but a mode scheme is still the best Controller components are not modelling physical devices, but a mode scheme is still the best
way to model most of these components. way to model most of these components.

9
satrs-core/CHANGELOG.md Normal file
View File

@ -0,0 +1,9 @@
Change Log
=======
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
# [unreleased]

View File

@ -1,57 +1,49 @@
[package] [package]
name = "satrs" name = "satrs-core"
version = "0.2.1" version = "0.1.0-alpha.1"
edition = "2021" edition = "2021"
rust-version = "1.71.1" rust-version = "1.61"
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"] authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
description = "A framework to build software for remote systems" description = "Core components of the sat-rs framework to build software for remote systems"
homepage = "https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/" homepage = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs" repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
license = "Apache-2.0" license = "Apache-2.0"
keywords = ["no-std", "space", "aerospace"] keywords = ["no-std", "space", "aerospace"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
categories = ["aerospace", "aerospace::space-protocols", "no-std", "hardware-support", "embedded"] categories = ["aerospace", "aerospace::space-protocols", "no-std", "hardware-support", "embedded"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
delegate = ">0.7, <=0.13" delegate = ">0.7, <=0.10"
paste = "1" paste = "1"
derive-new = ">=0.6, <=0.7" # TODO: Remove this as soon as the image including the description was moved to the satrs-book.
smallvec = "1" embed-doc-image = "0.1"
crc = "3"
[dependencies.satrs-shared] [dependencies.smallvec]
version = ">=0.1.3, <=0.2" version = "1"
[dependencies.num_enum] [dependencies.num_enum]
version = ">0.5, <=0.7" version = ">0.5, <=0.7"
default-features = false default-features = false
[dependencies.spacepackets] [dependencies.crc]
version = "0.13" version = "3"
default-features = false
[dependencies.cobs]
git = "https://github.com/robamu/cobs.rs.git"
version = "0.2.3"
branch = "all_features"
default-features = false
[dependencies.num-traits]
version = "0.2"
default-features = false
[dependencies.dyn-clone] [dependencies.dyn-clone]
version = "1" version = "1"
optional = true optional = true
[dependencies.hashbrown] [dependencies.hashbrown]
version = ">=0.14, <=0.15" version = "0.14"
optional = true optional = true
[dependencies.heapless] [dependencies.heapless]
version = "0.8" version = "0.7"
optional = true optional = true
[dependencies.num-traits]
version = "0.2"
default-features = false
[dependencies.downcast-rs] [dependencies.downcast-rs]
version = "1.2" version = "1.2"
default-features = false default-features = false
@ -67,8 +59,8 @@ default-features = false
optional = true optional = true
[dependencies.thiserror] [dependencies.thiserror]
version = "2" version = "1"
default-features = false optional = true
[dependencies.serde] [dependencies.serde]
version = "1" version = "1"
@ -80,22 +72,25 @@ version = "0.5.4"
features = ["all"] features = ["all"]
optional = true optional = true
[dependencies.mio] [dependencies.spacepackets]
version = "1" version = "0.7.0-beta.2"
features = ["os-poll", "net"] default-features = false
optional = true # git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git"
# rev = "79d26e1a6"
# branch = ""
[dependencies.defmt] [dependencies.cobs]
version = "0.3" git = "https://github.com/robamu/cobs.rs.git"
optional = true version = "0.2.3"
branch = "all_features"
default-features = false
[dev-dependencies] [dev-dependencies]
serde = "1" serde = "1"
zerocopy = "0.8" zerocopy = "0.7"
once_cell = "1" once_cell = "1.13"
serde_json = "1" serde_json = "1"
rand = "0.8" rand = "0.8"
tempfile = "3"
[dev-dependencies.postcard] [dev-dependencies.postcard]
version = "1" version = "1"
@ -111,9 +106,8 @@ std = [
"serde/std", "serde/std",
"spacepackets/std", "spacepackets/std",
"num_enum/std", "num_enum/std",
"thiserror/std", "thiserror",
"socket2", "socket2"
"mio"
] ]
alloc = [ alloc = [
"serde/alloc", "serde/alloc",
@ -122,12 +116,11 @@ alloc = [
"dyn-clone", "dyn-clone",
"downcast-rs" "downcast-rs"
] ]
serde = ["dep:serde", "spacepackets/serde", "satrs-shared/serde"] serde = ["dep:serde", "spacepackets/serde"]
crossbeam = ["crossbeam-channel"] crossbeam = ["crossbeam-channel"]
heapless = ["dep:heapless"] heapless = ["dep:heapless"]
defmt = ["dep:defmt", "spacepackets/defmt"] doc-images = []
test_util = []
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
rustdoc-args = ["--generate-link-to-definition"] rustdoc-args = ["--cfg", "doc_cfg"]

9
satrs-core/README.md Normal file
View File

@ -0,0 +1,9 @@
[![Crates.io](https://img.shields.io/crates/v/satrs-core)](https://crates.io/crates/satrs-core)
[![docs.rs](https://img.shields.io/docsrs/satrs-core)](https://docs.rs/satrs-core)
satrs-core
======
This crate contains the core components of the sat-rs framework.
You can find more information on [homepage](https://egit.irs.uni-stuttgart.de/rust/sat-rs).

View File

@ -0,0 +1,259 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
<!--Created by yEd 3.22-->
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
<key for="port" id="d1" yfiles.type="portgraphics"/>
<key for="port" id="d2" yfiles.type="portgeometry"/>
<key for="port" id="d3" yfiles.type="portuserdata"/>
<key attr.name="url" attr.type="string" for="node" id="d4"/>
<key attr.name="description" attr.type="string" for="node" id="d5"/>
<key for="node" id="d6" yfiles.type="nodegraphics"/>
<key for="graphml" id="d7" yfiles.type="resources"/>
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
<graph edgedefault="directed" id="G">
<data key="d0" xml:space="preserve"/>
<node id="n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="509.9999999999999" width="768.7000000000003" x="579.3105418719211" y="304.7"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="21.936037063598633" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="150.1282958984375" x="26.197490701913352" xml:space="preserve" y="24.234711021505348">Example Event Flow<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.46591974671274444" nodeRatioY="-0.452480958781362" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="203.0" x="814.0" y="506.6799999999999"/>
<y:Fill color="#FFFF00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.21258544921875" x="58.393707275390625" xml:space="preserve" y="21.27395296096796">Event Manager<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n2">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="82.0" x="617.6" y="413.23"/>
<y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="13.4398193359375" xml:space="preserve" y="14.547905921936035">Event
Creator 0<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n3">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="76.55999999999995" x="988.5" y="335.62999999999994"/>
<y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="10.719819335937473" xml:space="preserve" y="14.547905921936035">Event
Creator 2<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n4">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="72.55999999999983" x="860.6610837438426" y="335.62999999999994"/>
<y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="8.719819335937359" xml:space="preserve" y="14.547905921936035">Event
Creator 1<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n5">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="87.27999999999997" x="1112.52" y="335.62999999999994"/>
<y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="55.120361328125" x="16.079819335937373" xml:space="preserve" y="14.547905921936035">Event
Creator 3<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n6">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="126.0" x="781.0" y="620.26"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="92.78865051269531" x="16.605674743652344" xml:space="preserve" y="14.547905921936035">PUS Service 5
Event Reporting
<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n7">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="118.63999999999987" x="928.2" y="620.26"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.08859252929688" x="17.2757037353515" xml:space="preserve" y="14.547905921936035">PUS Service 19
Event Action<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n8">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="60.0" width="87.27999999999997" x="792.1260377358491" y="733.8400000000001"/>
<y:Fill color="#FFCC99" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="59.932403564453125" x="13.673798217773424" xml:space="preserve" y="14.547905921936035">Telemetry
Sink<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n9">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="170.79999999999995" width="210.80000000000018" x="1076.84" y="601.88"/>
<y:Fill hasColor="false" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="left" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="143.6875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="181.591796875" x="8.373079774614325" xml:space="preserve" y="7.444138124199753">Subscriptions
1. Event Creator 0 subscribes
for event 0
2. Event Creator 1 subscribes
for event group 2
3. PUS Service 5 handler
subscribes for all events
4. PUS Service 19 handler
subscribes for all events<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.4602795077105583" nodeRatioY="-0.45641605313700395" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<edge id="e0" source="n4" target="n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="8.058916256157545" sy="0.0" tx="-10.5" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.92036437988281" x="8.639817810058275" xml:space="preserve" y="29.00100609374465">event 1
(group 1)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="35.59999999999969" distanceToCenter="true" position="left" ratio="0.34252387409930674" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e1" source="n2" target="n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="11.93999999999994" tx="-83.5" ty="0.0">
<y:Point x="832.0" y="455.16999999999996"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.92036437988281" x="25.334655000000453" xml:space="preserve" y="-40.972107505798476">event 0
(group 0)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="25.520000000000095" distanceToCenter="true" position="left" ratio="0.20267159489379444" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e2" source="n3" target="n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-23.719999999999914" sy="5.5" tx="87.56000000000006" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.92036437988281" x="5.6761352539062955" xml:space="preserve" y="27.551854405966765">event 2
(group 3)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="5.676132812499983" distanceToCenter="false" position="left" ratio="0.3219761157957032" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e3" source="n5" target="n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-6.275467980295616" sy="0.0" tx="57.5" ty="8.5">
<y:Point x="1149.8845320197042" y="545.1799999999998"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.38468933105469" x="26.667665869801795" xml:space="preserve" y="43.287014528669715">event 3 (group 2)
event 4 (group 2)<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="75.3599999999999" distanceToCenter="true" position="left" ratio="0.2967848459873102" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e4" source="n1" target="n6">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-65.0" sy="0.0" tx="6.5" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.16456604003906" x="-98.78228302001958" xml:space="preserve" y="16.63042580701972">&lt;&lt;all events&gt;&gt;<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="57.20000000000004" distanceToCenter="true" position="right" ratio="0.4441995640590947" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e5" source="n1" target="n7">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="42.660000000000196" sy="0.0" tx="-29.359999999999786" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="83.16456604003906" x="20.4177438354493" xml:space="preserve" y="17.885881494816203">&lt;&lt;all events&gt;&gt;<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="62.0" distanceToCenter="true" position="left" ratio="0.492249939452652" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e6" source="n1" target="n2">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="658.6" y="536.6799999999998"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="44.69230651855469" x="-131.99129340961497" xml:space="preserve" y="-45.45208675384538">event 1
event 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.6426904695623505" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e7" source="n1" target="n4">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-35.69940886699487" sy="0.0" tx="-17.140492610837327" ty="1.5"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.452094078063965" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="46.14430236816406" x="-54.352158195608126" xml:space="preserve" y="-79.29459128622307">group 2<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="31.279999999999973" distanceToCenter="true" position="left" ratio="0.6800790648728832" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e8" source="n6" target="n8">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="8.233962264150945" ty="-21.42352238805968"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Ubuntu" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="30.90418815612793" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="87.40060424804688" x="-100.50030212402339" xml:space="preserve" y="11.337896156311103">enabled Events
as PUS 5 TM<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="56.79999999999995" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
</graph>
<data key="d7">
<y:Resources/>
</data>
</graphml>

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -3,11 +3,12 @@ Checklist for new releases
# Pre-Release # Pre-Release
1. Make sure any new modules are documented sufficiently enough and check docs by running `docs.sh`. 1. Make sure any new modules are documented sufficiently enough and check docs with
`cargo +nightly doc --all-features --config 'rustdocflags=["--cfg", "doc_cfg"]' --open`.
2. Bump version specifier in `Cargo.toml`. 2. Bump version specifier in `Cargo.toml`.
3. Update `CHANGELOG.md`: Convert `unreleased` section into version section with date and add new 3. Update `CHANGELOG.md`: Convert `unreleased` section into version section with date and add new
`unreleased` section. `unreleased` section.
4. Run `cargo test --all-features` or `cargo nextest r --all-features` and `cargo test --doc`. 4. Run `cargo test --all-features`.
5. Run `cargo fmt` and `cargo clippy`. Check `cargo msrv` against MSRV in `Cargo.toml`. 5. Run `cargo fmt` and `cargo clippy`. Check `cargo msrv` against MSRV in `Cargo.toml`.
6. Wait for CI/CD results for EGit and Github. These also check cross-compilation for bare-metal 6. Wait for CI/CD results for EGit and Github. These also check cross-compilation for bare-metal
targets. targets.
@ -18,5 +19,7 @@ Checklist for new releases
# Post-Release # Post-Release
1. Create a new release on `EGit` with the name `satrs-<version>`. 1. Create a new annotaged tag and push it with `git tag -a satrs-core-<version>` and
`git push -u origin satrs-core-<version>`
2. Create a new release on `EGit` based on the tag.

871
satrs-core/src/cfdp/dest.rs Normal file
View File

@ -0,0 +1,871 @@
use core::str::{from_utf8, Utf8Error};
use std::{
fs::{metadata, File},
io::{BufReader, Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
};
use crate::cfdp::user::TransactionFinishedParams;
use super::{
user::{CfdpUser, MetadataReceivedParams},
PacketInfo, PacketTarget, State, TransactionId, TransactionStep, CRC_32,
};
use smallvec::SmallVec;
use spacepackets::{
cfdp::{
pdu::{
eof::EofPdu,
file_data::FileDataPdu,
finished::{DeliveryCode, FileStatus, FinishedPdu},
metadata::{MetadataGenericParams, MetadataPdu},
CommonPduConfig, FileDirectiveType, PduError, PduHeader,
},
tlv::{msg_to_user::MsgToUserTlv, EntityIdTlv, TlvType},
ConditionCode, PduType, TransmissionMode,
},
util::UnsignedByteField,
};
use thiserror::Error;
pub struct DestinationHandler {
id: UnsignedByteField,
step: TransactionStep,
state: State,
tparams: TransactionParams,
packets_to_send_ctx: PacketsToSendContext,
}
#[derive(Debug, Default)]
struct PacketsToSendContext {
packet_available: bool,
directive: Option<FileDirectiveType>,
}
#[derive(Debug)]
struct FileProperties {
src_file_name: [u8; u8::MAX as usize],
src_file_name_len: usize,
dest_file_name: [u8; u8::MAX as usize],
dest_file_name_len: usize,
dest_path_buf: PathBuf,
}
#[derive(Debug)]
struct TransferState {
transaction_id: Option<TransactionId>,
progress: usize,
condition_code: ConditionCode,
delivery_code: DeliveryCode,
file_status: FileStatus,
metadata_params: MetadataGenericParams,
}
impl Default for TransferState {
fn default() -> Self {
Self {
transaction_id: None,
progress: Default::default(),
condition_code: ConditionCode::NoError,
delivery_code: DeliveryCode::Incomplete,
file_status: FileStatus::Unreported,
metadata_params: Default::default(),
}
}
}
#[derive(Debug)]
struct TransactionParams {
tstate: TransferState,
pdu_conf: CommonPduConfig,
file_properties: FileProperties,
cksum_buf: [u8; 1024],
msgs_to_user_size: usize,
msgs_to_user_buf: [u8; 1024],
}
impl Default for FileProperties {
fn default() -> Self {
Self {
src_file_name: [0; u8::MAX as usize],
src_file_name_len: Default::default(),
dest_file_name: [0; u8::MAX as usize],
dest_file_name_len: Default::default(),
dest_path_buf: Default::default(),
}
}
}
impl TransactionParams {
fn file_size(&self) -> usize {
self.tstate.metadata_params.file_size as usize
}
fn metadata_params(&self) -> &MetadataGenericParams {
&self.tstate.metadata_params
}
}
impl Default for TransactionParams {
fn default() -> Self {
Self {
pdu_conf: Default::default(),
cksum_buf: [0; 1024],
msgs_to_user_size: 0,
msgs_to_user_buf: [0; 1024],
tstate: Default::default(),
file_properties: Default::default(),
}
}
}
impl TransactionParams {
fn reset(&mut self) {
self.tstate.condition_code = ConditionCode::NoError;
self.tstate.delivery_code = DeliveryCode::Incomplete;
}
}
#[derive(Debug, Error)]
pub enum DestError {
/// File directive expected, but none specified
#[error("expected file directive")]
DirectiveExpected,
#[error("can not process packet type {0:?}")]
CantProcessPacketType(FileDirectiveType),
#[error("can not process file data PDUs in current state")]
WrongStateForFileDataAndEof,
// Received new metadata PDU while being already being busy with a file transfer.
#[error("busy with transfer")]
RecvdMetadataButIsBusy,
#[error("empty source file field")]
EmptySrcFileField,
#[error("empty dest file field")]
EmptyDestFileField,
#[error("pdu error {0}")]
Pdu(#[from] PduError),
#[error("io error {0}")]
Io(#[from] std::io::Error),
#[error("path conversion error {0}")]
PathConversion(#[from] Utf8Error),
#[error("error building dest path from source file name and dest folder")]
PathConcatError,
}
impl DestinationHandler {
pub fn new(id: impl Into<UnsignedByteField>) -> Self {
Self {
id: id.into(),
step: TransactionStep::Idle,
state: State::Idle,
tparams: Default::default(),
packets_to_send_ctx: Default::default(),
}
}
pub fn state_machine(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> {
match self.state {
State::Idle => todo!(),
State::BusyClass1Nacked => self.fsm_nacked(cfdp_user),
State::BusyClass2Acked => todo!("acknowledged mode not implemented yet"),
}
}
pub fn insert_packet(&mut self, packet_info: &PacketInfo) -> Result<(), DestError> {
if packet_info.target() != PacketTarget::DestEntity {
// Unwrap is okay here, a PacketInfo for a file data PDU should always have the
// destination as the target.
return Err(DestError::CantProcessPacketType(
packet_info.pdu_directive().unwrap(),
));
}
match packet_info.pdu_type {
PduType::FileDirective => {
if packet_info.pdu_directive.is_none() {
return Err(DestError::DirectiveExpected);
}
self.handle_file_directive(
packet_info.pdu_directive.unwrap(),
packet_info.raw_packet,
)
}
PduType::FileData => self.handle_file_data(packet_info.raw_packet),
}
}
pub fn packet_to_send_ready(&self) -> bool {
self.packets_to_send_ctx.packet_available
}
pub fn get_next_packet_to_send(
&self,
buf: &mut [u8],
) -> Result<Option<(FileDirectiveType, usize)>, DestError> {
if !self.packet_to_send_ready() {
return Ok(None);
}
let directive = self.packets_to_send_ctx.directive.unwrap();
let written_size = match directive {
FileDirectiveType::FinishedPdu => {
let pdu_header = PduHeader::new_no_file_data(self.tparams.pdu_conf, 0);
let finished_pdu = if self.tparams.tstate.condition_code == ConditionCode::NoError
|| self.tparams.tstate.condition_code == ConditionCode::UnsupportedChecksumType
{
FinishedPdu::new_default(
pdu_header,
self.tparams.tstate.delivery_code,
self.tparams.tstate.file_status,
)
} else {
// TODO: Are there cases where this ID is actually the source entity ID?
let entity_id = EntityIdTlv::new(self.id);
FinishedPdu::new_with_error(
pdu_header,
self.tparams.tstate.condition_code,
self.tparams.tstate.delivery_code,
self.tparams.tstate.file_status,
entity_id,
)
};
finished_pdu.write_to_bytes(buf)?
}
FileDirectiveType::AckPdu => todo!(),
FileDirectiveType::NakPdu => todo!(),
FileDirectiveType::KeepAlivePdu => todo!(),
_ => {
// This should never happen and is considered an internal impl error
panic!("invalid file directive {directive:?} for dest handler send packet");
}
};
Ok(Some((directive, written_size)))
}
pub fn handle_file_directive(
&mut self,
pdu_directive: FileDirectiveType,
raw_packet: &[u8],
) -> Result<(), DestError> {
match pdu_directive {
FileDirectiveType::EofPdu => self.handle_eof_pdu(raw_packet)?,
FileDirectiveType::FinishedPdu
| FileDirectiveType::NakPdu
| FileDirectiveType::KeepAlivePdu => {
return Err(DestError::CantProcessPacketType(pdu_directive));
}
FileDirectiveType::AckPdu => {
todo!(
"check whether ACK pdu handling is applicable by checking the acked directive field"
)
}
FileDirectiveType::MetadataPdu => self.handle_metadata_pdu(raw_packet)?,
FileDirectiveType::PromptPdu => self.handle_prompt_pdu(raw_packet)?,
};
Ok(())
}
pub fn handle_metadata_pdu(&mut self, raw_packet: &[u8]) -> Result<(), DestError> {
if self.state != State::Idle {
return Err(DestError::RecvdMetadataButIsBusy);
}
let metadata_pdu = MetadataPdu::from_bytes(raw_packet)?;
self.tparams.reset();
self.tparams.tstate.metadata_params = *metadata_pdu.metadata_params();
let src_name = metadata_pdu.src_file_name();
if src_name.is_empty() {
return Err(DestError::EmptySrcFileField);
}
self.tparams.file_properties.src_file_name[..src_name.len_value()]
.copy_from_slice(src_name.value());
self.tparams.file_properties.src_file_name_len = src_name.len_value();
let dest_name = metadata_pdu.dest_file_name();
if dest_name.is_empty() {
return Err(DestError::EmptyDestFileField);
}
self.tparams.file_properties.dest_file_name[..dest_name.len_value()]
.copy_from_slice(dest_name.value());
self.tparams.file_properties.dest_file_name_len = dest_name.len_value();
self.tparams.pdu_conf = *metadata_pdu.pdu_header().common_pdu_conf();
self.tparams.msgs_to_user_size = 0;
if metadata_pdu.options().is_some() {
for option_tlv in metadata_pdu.options_iter().unwrap() {
if option_tlv.is_standard_tlv()
&& option_tlv.tlv_type().unwrap() == TlvType::MsgToUser
{
self.tparams
.msgs_to_user_buf
.copy_from_slice(option_tlv.raw_data().unwrap());
self.tparams.msgs_to_user_size += option_tlv.len_full();
}
}
}
if self.tparams.pdu_conf.trans_mode == TransmissionMode::Unacknowledged {
self.state = State::BusyClass1Nacked;
} else {
self.state = State::BusyClass2Acked;
}
self.step = TransactionStep::TransactionStart;
Ok(())
}
pub fn handle_file_data(&mut self, raw_packet: &[u8]) -> Result<(), DestError> {
if self.state == State::Idle || self.step != TransactionStep::ReceivingFileDataPdus {
return Err(DestError::WrongStateForFileDataAndEof);
}
let fd_pdu = FileDataPdu::from_bytes(raw_packet)?;
let mut dest_file = File::options()
.write(true)
.open(&self.tparams.file_properties.dest_path_buf)?;
dest_file.seek(SeekFrom::Start(fd_pdu.offset()))?;
dest_file.write_all(fd_pdu.file_data())?;
Ok(())
}
#[allow(clippy::needless_if)]
pub fn handle_eof_pdu(&mut self, raw_packet: &[u8]) -> Result<(), DestError> {
if self.state == State::Idle || self.step != TransactionStep::ReceivingFileDataPdus {
return Err(DestError::WrongStateForFileDataAndEof);
}
let eof_pdu = EofPdu::from_bytes(raw_packet)?;
let checksum = eof_pdu.file_checksum();
// For a standard disk based file system, which is assumed to be used for now, the file
// will always be retained. This might change in the future.
self.tparams.tstate.file_status = FileStatus::Retained;
if self.checksum_check(checksum)? {
self.tparams.tstate.condition_code = ConditionCode::NoError;
self.tparams.tstate.delivery_code = DeliveryCode::Complete;
} else {
self.tparams.tstate.condition_code = ConditionCode::FileChecksumFailure;
}
// TODO: Check progress, and implement transfer completion timer as specified in the
// standard. This timer protects against out of order arrival of packets.
if self.tparams.tstate.progress != self.tparams.file_size() {}
if self.state == State::BusyClass1Nacked {
self.step = TransactionStep::TransferCompletion;
} else {
self.step = TransactionStep::SendingAckPdu;
}
Ok(())
}
pub fn handle_prompt_pdu(&mut self, _raw_packet: &[u8]) -> Result<(), DestError> {
todo!();
}
fn checksum_check(&mut self, expected_checksum: u32) -> Result<bool, DestError> {
let mut digest = CRC_32.digest();
let file_to_check = File::open(&self.tparams.file_properties.dest_path_buf)?;
let mut buf_reader = BufReader::new(file_to_check);
loop {
let bytes_read = buf_reader.read(&mut self.tparams.cksum_buf)?;
if bytes_read == 0 {
break;
}
digest.update(&self.tparams.cksum_buf[0..bytes_read]);
}
if digest.finalize() == expected_checksum {
return Ok(true);
}
Ok(false)
}
fn fsm_nacked(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> {
if self.step == TransactionStep::TransactionStart {
self.transaction_start(cfdp_user)?;
}
if self.step == TransactionStep::TransferCompletion {
self.transfer_completion(cfdp_user)?;
}
if self.step == TransactionStep::SendingAckPdu {
todo!("no support for acknowledged mode yet");
}
if self.step == TransactionStep::SendingFinishedPdu {
self.reset();
}
Ok(())
}
/// Get the step, which denotes the exact step of a pending CFDP transaction when applicable.
pub fn step(&self) -> TransactionStep {
self.step
}
/// Get the step, which denotes whether the CFDP handler is active, and which CFDP class
/// is used if it is active.
pub fn state(&self) -> State {
self.state
}
fn transaction_start(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> {
let dest_name = from_utf8(
&self.tparams.file_properties.dest_file_name
[..self.tparams.file_properties.dest_file_name_len],
)?;
let dest_path = Path::new(dest_name);
self.tparams.file_properties.dest_path_buf = dest_path.to_path_buf();
let source_id = self.tparams.pdu_conf.source_id();
let id = TransactionId::new(source_id, self.tparams.pdu_conf.transaction_seq_num);
let src_name = from_utf8(
&self.tparams.file_properties.src_file_name
[0..self.tparams.file_properties.src_file_name_len],
)?;
let mut msgs_to_user = SmallVec::<[MsgToUserTlv<'_>; 16]>::new();
let mut num_msgs_to_user = 0;
if self.tparams.msgs_to_user_size > 0 {
let mut index = 0;
while index < self.tparams.msgs_to_user_size {
// This should never panic as the validity of the options was checked beforehand.
let msgs_to_user_tlv =
MsgToUserTlv::from_bytes(&self.tparams.msgs_to_user_buf[index..])
.expect("message to user creation failed unexpectedly");
msgs_to_user.push(msgs_to_user_tlv);
index += msgs_to_user_tlv.len_full();
num_msgs_to_user += 1;
}
}
let metadata_recvd_params = MetadataReceivedParams {
id,
source_id,
file_size: self.tparams.tstate.metadata_params.file_size,
src_file_name: src_name,
dest_file_name: dest_name,
msgs_to_user: &msgs_to_user[..num_msgs_to_user],
};
self.tparams.tstate.transaction_id = Some(id);
cfdp_user.metadata_recvd_indication(&metadata_recvd_params);
if dest_path.exists() {
let dest_metadata = metadata(dest_path)?;
if dest_metadata.is_dir() {
// Create new destination path by concatenating the last part of the source source
// name and the destination folder. For example, for a source file of /tmp/hello.txt
// and a destination name of /home/test, the resulting file name should be
// /home/test/hello.txt
let source_path = Path::new(from_utf8(
&self.tparams.file_properties.src_file_name
[..self.tparams.file_properties.src_file_name_len],
)?);
let source_name = source_path.file_name();
if source_name.is_none() {
return Err(DestError::PathConcatError);
}
let source_name = source_name.unwrap();
self.tparams.file_properties.dest_path_buf.push(source_name);
}
}
// This function does exactly what we require: Create a new file if it does not exist yet
// and trucate an existing one.
File::create(&self.tparams.file_properties.dest_path_buf)?;
self.step = TransactionStep::ReceivingFileDataPdus;
Ok(())
}
fn transfer_completion(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> {
let transaction_finished_params = TransactionFinishedParams {
id: self.tparams.tstate.transaction_id.unwrap(),
condition_code: self.tparams.tstate.condition_code,
delivery_code: self.tparams.tstate.delivery_code,
file_status: self.tparams.tstate.file_status,
};
cfdp_user.transaction_finished_indication(&transaction_finished_params);
// This function should never be called with metadata parameters not set
if self.tparams.metadata_params().closure_requested {
self.prepare_finished_pdu()?;
self.step = TransactionStep::SendingFinishedPdu;
} else {
self.reset();
self.state = State::Idle;
self.step = TransactionStep::Idle;
}
Ok(())
}
fn reset(&mut self) {
self.step = TransactionStep::Idle;
self.state = State::Idle;
self.packets_to_send_ctx.packet_available = false;
self.tparams.reset();
}
fn prepare_finished_pdu(&mut self) -> Result<(), DestError> {
self.packets_to_send_ctx.packet_available = true;
self.packets_to_send_ctx.directive = Some(FileDirectiveType::FinishedPdu);
self.step = TransactionStep::SendingFinishedPdu;
Ok(())
}
}
#[cfg(test)]
mod tests {
use core::sync::atomic::{AtomicU8, Ordering};
#[allow(unused_imports)]
use std::println;
use std::{env::temp_dir, fs};
use alloc::{format, string::String};
use rand::Rng;
use spacepackets::{
cfdp::{lv::Lv, ChecksumType},
util::{UbfU16, UnsignedByteFieldU16},
};
use super::*;
const LOCAL_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(1);
const REMOTE_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(2);
const SRC_NAME: &str = "__cfdp__source-file";
const DEST_NAME: &str = "__cfdp__dest-file";
static ATOMIC_COUNTER: AtomicU8 = AtomicU8::new(0);
#[derive(Default)]
struct TestCfdpUser {
next_expected_seq_num: u64,
expected_full_src_name: String,
expected_full_dest_name: String,
expected_file_size: usize,
}
impl TestCfdpUser {
fn generic_id_check(&self, id: &crate::cfdp::TransactionId) {
assert_eq!(id.source_id, LOCAL_ID.into());
assert_eq!(id.seq_num().value(), self.next_expected_seq_num);
}
}
impl CfdpUser for TestCfdpUser {
fn transaction_indication(&mut self, id: &crate::cfdp::TransactionId) {
self.generic_id_check(id);
}
fn eof_sent_indication(&mut self, id: &crate::cfdp::TransactionId) {
self.generic_id_check(id);
}
fn transaction_finished_indication(
&mut self,
finished_params: &crate::cfdp::user::TransactionFinishedParams,
) {
self.generic_id_check(&finished_params.id);
}
fn metadata_recvd_indication(
&mut self,
md_recvd_params: &crate::cfdp::user::MetadataReceivedParams,
) {
self.generic_id_check(&md_recvd_params.id);
assert_eq!(
String::from(md_recvd_params.src_file_name),
self.expected_full_src_name
);
assert_eq!(
String::from(md_recvd_params.dest_file_name),
self.expected_full_dest_name
);
assert_eq!(md_recvd_params.msgs_to_user.len(), 0);
assert_eq!(md_recvd_params.source_id, LOCAL_ID.into());
assert_eq!(md_recvd_params.file_size as usize, self.expected_file_size);
}
fn file_segment_recvd_indication(
&mut self,
_segment_recvd_params: &crate::cfdp::user::FileSegmentRecvdParams,
) {
}
fn report_indication(&mut self, _id: &crate::cfdp::TransactionId) {}
fn suspended_indication(
&mut self,
_id: &crate::cfdp::TransactionId,
_condition_code: ConditionCode,
) {
panic!("unexpected suspended indication");
}
fn resumed_indication(&mut self, _id: &crate::cfdp::TransactionId, _progresss: u64) {}
fn fault_indication(
&mut self,
_id: &crate::cfdp::TransactionId,
_condition_code: ConditionCode,
_progress: u64,
) {
panic!("unexpected fault indication");
}
fn abandoned_indication(
&mut self,
_id: &crate::cfdp::TransactionId,
_condition_code: ConditionCode,
_progress: u64,
) {
panic!("unexpected abandoned indication");
}
fn eof_recvd_indication(&mut self, id: &crate::cfdp::TransactionId) {
self.generic_id_check(id);
}
}
fn init_check(handler: &DestinationHandler) {
assert_eq!(handler.state(), State::Idle);
assert_eq!(handler.step(), TransactionStep::Idle);
}
fn init_full_filenames() -> (PathBuf, PathBuf) {
let mut file_path = temp_dir();
let mut src_path = file_path.clone();
// Atomic counter used to allow concurrent tests.
let unique_counter = ATOMIC_COUNTER.fetch_add(1, Ordering::Relaxed);
// Create unique test filenames.
let src_name_unique = format!("{SRC_NAME}{}.txt", unique_counter);
let dest_name_unique = format!("{DEST_NAME}{}.txt", unique_counter);
src_path.push(src_name_unique);
file_path.push(dest_name_unique);
(src_path, file_path)
}
#[test]
fn test_basic() {
let dest_handler = DestinationHandler::new(REMOTE_ID);
init_check(&dest_handler);
}
fn create_pdu_header(seq_num: impl Into<UnsignedByteField>) -> PduHeader {
let mut pdu_conf =
CommonPduConfig::new_with_byte_fields(LOCAL_ID, REMOTE_ID, seq_num).unwrap();
pdu_conf.trans_mode = TransmissionMode::Unacknowledged;
PduHeader::new_no_file_data(pdu_conf, 0)
}
fn create_metadata_pdu<'filename>(
pdu_header: &PduHeader,
src_name: &'filename Path,
dest_name: &'filename Path,
file_size: u64,
) -> MetadataPdu<'filename, 'filename, 'static> {
let metadata_params = MetadataGenericParams::new(false, ChecksumType::Crc32, file_size);
MetadataPdu::new(
*pdu_header,
metadata_params,
Lv::new_from_str(src_name.as_os_str().to_str().unwrap()).unwrap(),
Lv::new_from_str(dest_name.as_os_str().to_str().unwrap()).unwrap(),
None,
)
}
fn insert_metadata_pdu(
metadata_pdu: &MetadataPdu,
buf: &mut [u8],
dest_handler: &mut DestinationHandler,
) {
let written_len = metadata_pdu
.write_to_bytes(buf)
.expect("writing metadata PDU failed");
let packet_info =
PacketInfo::new(&buf[..written_len]).expect("generating packet info failed");
let insert_result = dest_handler.insert_packet(&packet_info);
if let Err(e) = insert_result {
panic!("insert result error: {e}");
}
}
fn insert_eof_pdu(
file_data: &[u8],
pdu_header: &PduHeader,
buf: &mut [u8],
dest_handler: &mut DestinationHandler,
) {
let mut digest = CRC_32.digest();
digest.update(file_data);
let crc32 = digest.finalize();
let eof_pdu = EofPdu::new_no_error(*pdu_header, crc32, file_data.len() as u64);
let result = eof_pdu.write_to_bytes(buf);
assert!(result.is_ok());
let packet_info = PacketInfo::new(&buf).expect("generating packet info failed");
let result = dest_handler.insert_packet(&packet_info);
assert!(result.is_ok());
}
#[test]
fn test_empty_file_transfer() {
let (src_name, dest_name) = init_full_filenames();
assert!(!Path::exists(&dest_name));
let mut buf: [u8; 512] = [0; 512];
let mut test_user = TestCfdpUser {
next_expected_seq_num: 0,
expected_full_src_name: src_name.to_string_lossy().into(),
expected_full_dest_name: dest_name.to_string_lossy().into(),
expected_file_size: 0,
};
// We treat the destination handler like it is a remote entity.
let mut dest_handler = DestinationHandler::new(REMOTE_ID);
init_check(&dest_handler);
let seq_num = UbfU16::new(0);
let pdu_header = create_pdu_header(seq_num);
let metadata_pdu =
create_metadata_pdu(&pdu_header, src_name.as_path(), dest_name.as_path(), 0);
insert_metadata_pdu(&metadata_pdu, &mut buf, &mut dest_handler);
let result = dest_handler.state_machine(&mut test_user);
if let Err(e) = result {
panic!("dest handler fsm error: {e}");
}
assert_ne!(dest_handler.state(), State::Idle);
assert_eq!(dest_handler.step(), TransactionStep::ReceivingFileDataPdus);
insert_eof_pdu(&[], &pdu_header, &mut buf, &mut dest_handler);
let result = dest_handler.state_machine(&mut test_user);
assert!(result.is_ok());
assert_eq!(dest_handler.state(), State::Idle);
assert_eq!(dest_handler.step(), TransactionStep::Idle);
assert!(Path::exists(&dest_name));
let read_content = fs::read(&dest_name).expect("reading back string failed");
assert_eq!(read_content.len(), 0);
assert!(fs::remove_file(dest_name).is_ok());
}
#[test]
fn test_small_file_transfer() {
let (src_name, dest_name) = init_full_filenames();
assert!(!Path::exists(&dest_name));
let file_data_str = "Hello World!";
let file_data = file_data_str.as_bytes();
let mut buf: [u8; 512] = [0; 512];
let mut test_user = TestCfdpUser {
next_expected_seq_num: 0,
expected_full_src_name: src_name.to_string_lossy().into(),
expected_full_dest_name: dest_name.to_string_lossy().into(),
expected_file_size: file_data.len(),
};
// We treat the destination handler like it is a remote entity.
let mut dest_handler = DestinationHandler::new(REMOTE_ID);
init_check(&dest_handler);
let seq_num = UbfU16::new(0);
let pdu_header = create_pdu_header(seq_num);
let metadata_pdu = create_metadata_pdu(
&pdu_header,
src_name.as_path(),
dest_name.as_path(),
file_data.len() as u64,
);
insert_metadata_pdu(&metadata_pdu, &mut buf, &mut dest_handler);
let result = dest_handler.state_machine(&mut test_user);
if let Err(e) = result {
panic!("dest handler fsm error: {e}");
}
assert_ne!(dest_handler.state(), State::Idle);
assert_eq!(dest_handler.step(), TransactionStep::ReceivingFileDataPdus);
let offset = 0;
let filedata_pdu = FileDataPdu::new_no_seg_metadata(pdu_header, offset, file_data);
filedata_pdu
.write_to_bytes(&mut buf)
.expect("writing file data PDU failed");
let packet_info = PacketInfo::new(&buf).expect("creating packet info failed");
let result = dest_handler.insert_packet(&packet_info);
if let Err(e) = result {
panic!("destination handler packet insertion error: {e}");
}
let result = dest_handler.state_machine(&mut test_user);
assert!(result.is_ok());
insert_eof_pdu(file_data, &pdu_header, &mut buf, &mut dest_handler);
let result = dest_handler.state_machine(&mut test_user);
assert!(result.is_ok());
assert_eq!(dest_handler.state(), State::Idle);
assert_eq!(dest_handler.step(), TransactionStep::Idle);
assert!(Path::exists(&dest_name));
let read_content = fs::read_to_string(&dest_name).expect("reading back string failed");
assert_eq!(read_content, file_data_str);
assert!(fs::remove_file(dest_name).is_ok());
}
#[test]
fn test_segmented_file_transfer() {
let (src_name, dest_name) = init_full_filenames();
assert!(!Path::exists(&dest_name));
let mut rng = rand::thread_rng();
let mut random_data = [0u8; 512];
rng.fill(&mut random_data);
let mut buf: [u8; 512] = [0; 512];
let mut test_user = TestCfdpUser {
next_expected_seq_num: 0,
expected_full_src_name: src_name.to_string_lossy().into(),
expected_full_dest_name: dest_name.to_string_lossy().into(),
expected_file_size: random_data.len(),
};
// We treat the destination handler like it is a remote entity.
let mut dest_handler = DestinationHandler::new(REMOTE_ID);
init_check(&dest_handler);
let seq_num = UbfU16::new(0);
let pdu_header = create_pdu_header(seq_num);
let metadata_pdu = create_metadata_pdu(
&pdu_header,
src_name.as_path(),
dest_name.as_path(),
random_data.len() as u64,
);
insert_metadata_pdu(&metadata_pdu, &mut buf, &mut dest_handler);
let result = dest_handler.state_machine(&mut test_user);
if let Err(e) = result {
panic!("dest handler fsm error: {e}");
}
assert_ne!(dest_handler.state(), State::Idle);
assert_eq!(dest_handler.step(), TransactionStep::ReceivingFileDataPdus);
// First file data PDU
let mut offset: usize = 0;
let segment_len = 256;
let filedata_pdu = FileDataPdu::new_no_seg_metadata(
pdu_header,
offset as u64,
&random_data[0..segment_len],
);
filedata_pdu
.write_to_bytes(&mut buf)
.expect("writing file data PDU failed");
let packet_info = PacketInfo::new(&buf).expect("creating packet info failed");
let result = dest_handler.insert_packet(&packet_info);
if let Err(e) = result {
panic!("destination handler packet insertion error: {e}");
}
let result = dest_handler.state_machine(&mut test_user);
assert!(result.is_ok());
// Second file data PDU
offset += segment_len;
let filedata_pdu = FileDataPdu::new_no_seg_metadata(
pdu_header,
offset as u64,
&random_data[segment_len..],
);
filedata_pdu
.write_to_bytes(&mut buf)
.expect("writing file data PDU failed");
let packet_info = PacketInfo::new(&buf).expect("creating packet info failed");
let result = dest_handler.insert_packet(&packet_info);
if let Err(e) = result {
panic!("destination handler packet insertion error: {e}");
}
let result = dest_handler.state_machine(&mut test_user);
assert!(result.is_ok());
insert_eof_pdu(&random_data, &pdu_header, &mut buf, &mut dest_handler);
let result = dest_handler.state_machine(&mut test_user);
assert!(result.is_ok());
assert_eq!(dest_handler.state(), State::Idle);
assert_eq!(dest_handler.step(), TransactionStep::Idle);
// Clean up
assert!(Path::exists(&dest_name));
let read_content = fs::read(&dest_name).expect("reading back string failed");
assert_eq!(read_content, random_data);
assert!(fs::remove_file(dest_name).is_ok());
}
}

320
satrs-core/src/cfdp/mod.rs Normal file
View File

@ -0,0 +1,320 @@
use crc::{Crc, CRC_32_CKSUM};
use spacepackets::{
cfdp::{
pdu::{FileDirectiveType, PduError, PduHeader},
ChecksumType, PduType, TransmissionMode,
},
util::UnsignedByteField,
};
#[cfg(feature = "alloc")]
use alloc::boxed::Box;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "std")]
pub mod dest;
#[cfg(feature = "std")]
pub mod source;
pub mod user;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntityType {
Sending,
Receiving,
}
/// Generic abstraction for a check timer which has different functionality depending on whether
/// the using entity is the sending entity or the receiving entity for the unacknowledged
/// transmission mode.
///
/// For the sending entity, this timer determines the expiry period for declaring a check limit
/// fault after sending an EOF PDU with requested closure. This allows a timeout of the transfer.
/// Also see 4.6.3.2 of the CFDP standard.
///
/// 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 {
fn has_expired(&self) -> bool;
}
/// 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.
///
/// 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.
#[cfg(feature = "alloc")]
pub trait CheckTimerCreator {
fn get_check_timer_provider(
local_id: &UnsignedByteField,
remote_id: &UnsignedByteField,
entity_type: EntityType,
) -> Box<dyn CheckTimerProvider>;
}
/// Simple implementation of the [CheckTimerProvider] trait assuming a standard runtime.
/// It also assumes that a second accuracy of the check timer period is sufficient.
#[cfg(feature = "std")]
pub struct StdCheckTimer {
expiry_time_seconds: u64,
start_time: std::time::Instant,
}
#[cfg(feature = "std")]
impl StdCheckTimer {
pub fn new(expiry_time_seconds: u64) -> Self {
Self {
expiry_time_seconds,
start_time: std::time::Instant::now(),
}
}
}
#[cfg(feature = "std")]
impl CheckTimerProvider for StdCheckTimer {
fn has_expired(&self) -> bool {
let elapsed_time = self.start_time.elapsed();
if elapsed_time.as_secs() > self.expiry_time_seconds {
return true;
}
false
}
}
#[derive(Debug)]
pub struct RemoteEntityConfig {
pub entity_id: UnsignedByteField,
pub max_file_segment_len: usize,
pub closure_requeted_by_default: bool,
pub crc_on_transmission_by_default: bool,
pub default_transmission_mode: TransmissionMode,
pub default_crc_type: ChecksumType,
pub check_limit: u32,
}
pub trait RemoteEntityConfigProvider {
fn get_remote_config(&self, remote_id: &UnsignedByteField) -> Option<&RemoteEntityConfig>;
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TransactionId {
source_id: UnsignedByteField,
seq_num: UnsignedByteField,
}
impl TransactionId {
pub fn new(source_id: UnsignedByteField, seq_num: UnsignedByteField) -> Self {
Self { source_id, seq_num }
}
pub fn source_id(&self) -> &UnsignedByteField {
&self.source_id
}
pub fn seq_num(&self) -> &UnsignedByteField {
&self.seq_num
}
}
#[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,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum State {
Idle = 0,
BusyClass1Nacked = 2,
BusyClass2Acked = 3,
}
pub const CRC_32: Crc<u32> = Crc::<u32>::new(&CRC_32_CKSUM);
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum PacketTarget {
SourceEntity,
DestEntity,
}
/// This is a helper struct which contains base information about a particular PDU packet.
/// This is also necessary information for CFDP packet routing. For example, some packet types
/// like file data PDUs can only be used by CFDP source entities.
pub struct PacketInfo<'raw_packet> {
pdu_type: PduType,
pdu_directive: Option<FileDirectiveType>,
target: PacketTarget,
raw_packet: &'raw_packet [u8],
}
impl<'raw> PacketInfo<'raw> {
pub fn new(raw_packet: &'raw [u8]) -> Result<Self, PduError> {
let (pdu_header, header_len) = PduHeader::from_bytes(raw_packet)?;
if pdu_header.pdu_type() == PduType::FileData {
return Ok(Self {
pdu_type: pdu_header.pdu_type(),
pdu_directive: None,
target: PacketTarget::DestEntity,
raw_packet,
});
}
if pdu_header.pdu_datafield_len() < 1 {
return Err(PduError::FormatError);
}
// Route depending on PDU type and directive type if applicable. Retrieve directive type
// from the raw stream for better performance (with sanity and directive code check).
// The routing is based on section 4.5 of the CFDP standard which specifies the PDU forwarding
// procedure.
let directive = FileDirectiveType::try_from(raw_packet[header_len]).map_err(|_| {
PduError::InvalidDirectiveType {
found: raw_packet[header_len],
expected: None,
}
})?;
let packet_target = match directive {
// Section c) of 4.5.3: These PDUs should always be targeted towards the file sender a.k.a.
// the source handler
FileDirectiveType::NakPdu
| FileDirectiveType::FinishedPdu
| FileDirectiveType::KeepAlivePdu => PacketTarget::SourceEntity,
// Section b) of 4.5.3: These PDUs should always be targeted towards the file receiver a.k.a.
// the destination handler
FileDirectiveType::MetadataPdu
| FileDirectiveType::EofPdu
| FileDirectiveType::PromptPdu => PacketTarget::DestEntity,
// Section a): Recipient depends of the type of PDU that is being acknowledged. We can simply
// extract the PDU type from the raw stream. If it is an EOF PDU, this packet is passed to
// the source handler, for a Finished PDU, it is passed to the destination handler.
FileDirectiveType::AckPdu => {
let acked_directive = FileDirectiveType::try_from(raw_packet[header_len + 1])
.map_err(|_| PduError::InvalidDirectiveType {
found: raw_packet[header_len],
expected: None,
})?;
if acked_directive == FileDirectiveType::EofPdu {
PacketTarget::SourceEntity
} else if acked_directive == FileDirectiveType::FinishedPdu {
PacketTarget::DestEntity
} else {
// TODO: Maybe a better error? This might be confusing..
return Err(PduError::InvalidDirectiveType {
found: raw_packet[header_len + 1],
expected: None,
});
}
}
};
Ok(Self {
pdu_type: pdu_header.pdu_type(),
pdu_directive: Some(directive),
target: packet_target,
raw_packet,
})
}
pub fn pdu_type(&self) -> PduType {
self.pdu_type
}
pub fn pdu_directive(&self) -> Option<FileDirectiveType> {
self.pdu_directive
}
pub fn target(&self) -> PacketTarget {
self.target
}
pub fn raw_packet(&self) -> &[u8] {
self.raw_packet
}
}
#[cfg(test)]
mod tests {
use spacepackets::cfdp::{
lv::Lv,
pdu::{
eof::EofPdu,
file_data::FileDataPdu,
metadata::{MetadataGenericParams, MetadataPdu},
CommonPduConfig, FileDirectiveType, PduHeader,
},
PduType,
};
use crate::cfdp::PacketTarget;
use super::PacketInfo;
fn generic_pdu_header() -> PduHeader {
let pdu_conf = CommonPduConfig::default();
PduHeader::new_no_file_data(pdu_conf, 0)
}
#[test]
fn test_metadata_pdu_info() {
let mut buf: [u8; 128] = [0; 128];
let pdu_header = generic_pdu_header();
let metadata_params = MetadataGenericParams::default();
let src_file_name = "hello.txt";
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);
metadata_pdu
.write_to_bytes(&mut buf)
.expect("writing metadata PDU failed");
let packet_info = PacketInfo::new(&buf).expect("creating packet info failed");
assert_eq!(packet_info.pdu_type(), PduType::FileDirective);
assert!(packet_info.pdu_directive().is_some());
assert_eq!(
packet_info.pdu_directive().unwrap(),
FileDirectiveType::MetadataPdu
);
assert_eq!(packet_info.target(), PacketTarget::DestEntity);
}
#[test]
fn test_filedata_pdu_info() {
let mut buf: [u8; 128] = [0; 128];
let pdu_header = generic_pdu_header();
let file_data_pdu = FileDataPdu::new_no_seg_metadata(pdu_header, 0, &[]);
file_data_pdu
.write_to_bytes(&mut buf)
.expect("writing file data PDU failed");
let packet_info = PacketInfo::new(&buf).expect("creating packet info failed");
assert_eq!(packet_info.pdu_type(), PduType::FileData);
assert!(packet_info.pdu_directive().is_none());
assert_eq!(packet_info.target(), PacketTarget::DestEntity);
}
#[test]
fn test_eof_pdu_info() {
let mut buf: [u8; 128] = [0; 128];
let pdu_header = generic_pdu_header();
let eof_pdu = EofPdu::new_no_error(pdu_header, 0, 0);
eof_pdu
.write_to_bytes(&mut buf)
.expect("writing file data PDU failed");
let packet_info = PacketInfo::new(&buf).expect("creating packet info failed");
assert_eq!(packet_info.pdu_type(), PduType::FileDirective);
assert!(packet_info.pdu_directive().is_some());
assert_eq!(
packet_info.pdu_directive().unwrap(),
FileDirectiveType::EofPdu
);
}
}

View File

@ -0,0 +1,15 @@
#![allow(dead_code)]
use spacepackets::util::UnsignedByteField;
pub struct SourceHandler {
id: UnsignedByteField,
}
impl SourceHandler {
pub fn new(id: impl Into<UnsignedByteField>) -> Self {
Self { id: id.into() }
}
}
#[cfg(test)]
mod tests {}

View File

@ -0,0 +1,65 @@
use spacepackets::{
cfdp::{
pdu::{
file_data::RecordContinuationState,
finished::{DeliveryCode, FileStatus},
},
tlv::msg_to_user::MsgToUserTlv,
ConditionCode,
},
util::UnsignedByteField,
};
use super::TransactionId;
#[derive(Debug, Copy, Clone)]
pub struct TransactionFinishedParams {
pub id: TransactionId,
pub condition_code: ConditionCode,
pub delivery_code: DeliveryCode,
pub file_status: FileStatus,
}
#[derive(Debug)]
pub struct MetadataReceivedParams<'src_file, 'dest_file, 'msgs_to_user> {
pub id: TransactionId,
pub source_id: UnsignedByteField,
pub file_size: u64,
pub src_file_name: &'src_file str,
pub dest_file_name: &'dest_file str,
pub msgs_to_user: &'msgs_to_user [MsgToUserTlv<'msgs_to_user>],
}
#[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 trait CfdpUser {
fn transaction_indication(&mut self, id: &TransactionId);
fn eof_sent_indication(&mut self, id: &TransactionId);
fn transaction_finished_indication(&mut self, finished_params: &TransactionFinishedParams);
fn metadata_recvd_indication(&mut self, md_recvd_params: &MetadataReceivedParams);
fn file_segment_recvd_indication(&mut self, segment_recvd_params: &FileSegmentRecvdParams);
// TODO: The standard does not strictly specify how the report information looks..
fn report_indication(&mut self, id: &TransactionId);
fn suspended_indication(&mut self, id: &TransactionId, condition_code: ConditionCode);
fn resumed_indication(&mut self, id: &TransactionId, progress: u64);
fn fault_indication(
&mut self,
id: &TransactionId,
condition_code: ConditionCode,
progress: u64,
);
fn abandoned_indication(
&mut self,
id: &TransactionId,
condition_code: ConditionCode,
progress: u64,
);
fn eof_recvd_indication(&mut self, id: &TransactionId);
}

View File

@ -0,0 +1,269 @@
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
#[cfg(feature = "alloc")]
use hashbrown::HashSet;
use spacepackets::PacketId;
use crate::tmtc::ReceivesTcCore;
pub trait PacketIdLookup {
fn validate(&self, packet_id: u16) -> bool;
}
#[cfg(feature = "alloc")]
impl PacketIdLookup for Vec<u16> {
fn validate(&self, packet_id: u16) -> bool {
self.contains(&packet_id)
}
}
#[cfg(feature = "alloc")]
impl PacketIdLookup for HashSet<u16> {
fn validate(&self, packet_id: u16) -> bool {
self.contains(&packet_id)
}
}
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 {
self.contains(&PacketId::from(packet_id))
}
}
#[cfg(feature = "alloc")]
impl PacketIdLookup for HashSet<PacketId> {
fn validate(&self, packet_id: u16) -> bool {
self.contains(&PacketId::from(packet_id))
}
}
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.
///
/// This function is also able to deal with broken tail packets at the end as long a the parser
/// can read the full 7 bytes which constitue a space packet header plus one byte minimal size.
/// If broken tail packets are detected, they are moved to the front of the buffer, and the write
/// index for future write operations will be written to the `next_write_idx` argument.
///
/// The parser will write all packets which were decoded successfully to the given `tc_receiver`
/// and return the number of packets found. If the [ReceivesTcCore::pass_tc] calls fails, the
/// error will be returned.
pub fn parse_buffer_for_ccsds_space_packets<E>(
buf: &mut [u8],
packet_id_lookup: &(impl PacketIdLookup + ?Sized),
tc_receiver: &mut (impl ReceivesTcCore<Error = E> + ?Sized),
next_write_idx: &mut usize,
) -> Result<u32, E> {
*next_write_idx = 0;
let mut packets_found = 0;
let mut current_idx = 0;
let buf_len = buf.len();
loop {
if current_idx + 7 >= buf.len() {
break;
}
let packet_id = u16::from_be_bytes(buf[current_idx..current_idx + 2].try_into().unwrap());
if packet_id_lookup.validate(packet_id) {
let length_field =
u16::from_be_bytes(buf[current_idx + 4..current_idx + 6].try_into().unwrap());
let packet_size = length_field + 7;
if (current_idx + packet_size as usize) <= buf_len {
tc_receiver.pass_tc(&buf[current_idx..current_idx + packet_size as usize])?;
packets_found += 1;
} else {
// Move packet to start of buffer if applicable.
if current_idx > 0 {
buf.copy_within(current_idx.., 0);
*next_write_idx = buf.len() - current_idx;
}
}
current_idx += packet_size as usize;
continue;
}
current_idx += 1;
}
Ok(packets_found)
}
#[cfg(test)]
mod tests {
use spacepackets::{
ecss::{tc::PusTcCreator, SerializablePusPacket},
PacketId, SpHeader,
};
use crate::encoding::tests::TcCacher;
use super::parse_buffer_for_ccsds_space_packets;
const TEST_APID_0: u16 = 0x02;
const TEST_APID_1: u16 = 0x10;
const TEST_PACKET_ID_0: PacketId = PacketId::const_tc(true, TEST_APID_0);
const TEST_PACKET_ID_1: PacketId = PacketId::const_tc(true, TEST_APID_1);
#[test]
fn test_basic() {
let mut sph = SpHeader::tc_unseg(TEST_APID_0, 0, 0).unwrap();
let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true);
let mut buffer: [u8; 32] = [0; 32];
let packet_len = ping_tc
.write_to_bytes(&mut buffer)
.expect("writing packet failed");
let valid_packet_ids = [TEST_PACKET_ID_0];
let mut tc_cacher = TcCacher::default();
let mut next_write_idx = 0;
let parse_result = parse_buffer_for_ccsds_space_packets(
&mut buffer,
valid_packet_ids.as_slice(),
&mut tc_cacher,
&mut next_write_idx,
);
assert!(parse_result.is_ok());
let parsed_packets = parse_result.unwrap();
assert_eq!(parsed_packets, 1);
assert_eq!(tc_cacher.tc_queue.len(), 1);
assert_eq!(
tc_cacher.tc_queue.pop_front().unwrap(),
buffer[..packet_len]
);
}
#[test]
fn test_multi_packet() {
let mut sph = SpHeader::tc_unseg(TEST_APID_0, 0, 0).unwrap();
let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true);
let action_tc = PusTcCreator::new_simple(&mut sph, 8, 0, None, true);
let mut buffer: [u8; 32] = [0; 32];
let packet_len_ping = ping_tc
.write_to_bytes(&mut buffer)
.expect("writing packet failed");
let packet_len_action = action_tc
.write_to_bytes(&mut buffer[packet_len_ping..])
.expect("writing packet failed");
let valid_packet_ids = [TEST_PACKET_ID_0];
let mut tc_cacher = TcCacher::default();
let mut next_write_idx = 0;
let parse_result = parse_buffer_for_ccsds_space_packets(
&mut buffer,
valid_packet_ids.as_slice(),
&mut tc_cacher,
&mut next_write_idx,
);
assert!(parse_result.is_ok());
let parsed_packets = parse_result.unwrap();
assert_eq!(parsed_packets, 2);
assert_eq!(tc_cacher.tc_queue.len(), 2);
assert_eq!(
tc_cacher.tc_queue.pop_front().unwrap(),
buffer[..packet_len_ping]
);
assert_eq!(
tc_cacher.tc_queue.pop_front().unwrap(),
buffer[packet_len_ping..packet_len_ping + packet_len_action]
);
}
#[test]
fn test_multi_apid() {
let mut sph = SpHeader::tc_unseg(TEST_APID_0, 0, 0).unwrap();
let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true);
sph = SpHeader::tc_unseg(TEST_APID_1, 0, 0).unwrap();
let action_tc = PusTcCreator::new_simple(&mut sph, 8, 0, None, true);
let mut buffer: [u8; 32] = [0; 32];
let packet_len_ping = ping_tc
.write_to_bytes(&mut buffer)
.expect("writing packet failed");
let packet_len_action = action_tc
.write_to_bytes(&mut buffer[packet_len_ping..])
.expect("writing packet failed");
let valid_packet_ids = [TEST_PACKET_ID_0, TEST_PACKET_ID_1];
let mut tc_cacher = TcCacher::default();
let mut next_write_idx = 0;
let parse_result = parse_buffer_for_ccsds_space_packets(
&mut buffer,
valid_packet_ids.as_slice(),
&mut tc_cacher,
&mut next_write_idx,
);
assert!(parse_result.is_ok());
let parsed_packets = parse_result.unwrap();
assert_eq!(parsed_packets, 2);
assert_eq!(tc_cacher.tc_queue.len(), 2);
assert_eq!(
tc_cacher.tc_queue.pop_front().unwrap(),
buffer[..packet_len_ping]
);
assert_eq!(
tc_cacher.tc_queue.pop_front().unwrap(),
buffer[packet_len_ping..packet_len_ping + packet_len_action]
);
}
#[test]
fn test_split_packet_multi() {
let mut sph = SpHeader::tc_unseg(TEST_APID_0, 0, 0).unwrap();
let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true);
sph = SpHeader::tc_unseg(TEST_APID_1, 0, 0).unwrap();
let action_tc = PusTcCreator::new_simple(&mut sph, 8, 0, None, true);
let mut buffer: [u8; 32] = [0; 32];
let packet_len_ping = ping_tc
.write_to_bytes(&mut buffer)
.expect("writing packet failed");
let packet_len_action = action_tc
.write_to_bytes(&mut buffer[packet_len_ping..])
.expect("writing packet failed");
let valid_packet_ids = [TEST_PACKET_ID_0, TEST_PACKET_ID_1];
let mut tc_cacher = TcCacher::default();
let mut next_write_idx = 0;
let parse_result = parse_buffer_for_ccsds_space_packets(
&mut buffer[..packet_len_ping + packet_len_action - 4],
valid_packet_ids.as_slice(),
&mut tc_cacher,
&mut next_write_idx,
);
assert!(parse_result.is_ok());
let parsed_packets = parse_result.unwrap();
assert_eq!(parsed_packets, 1);
assert_eq!(tc_cacher.tc_queue.len(), 1);
// The broken packet was moved to the start, so the next write index should be after the
// last segment missing 4 bytes.
assert_eq!(next_write_idx, packet_len_action - 4);
}
#[test]
fn test_one_split_packet() {
let mut sph = SpHeader::tc_unseg(TEST_APID_0, 0, 0).unwrap();
let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true);
let mut buffer: [u8; 32] = [0; 32];
let packet_len_ping = ping_tc
.write_to_bytes(&mut buffer)
.expect("writing packet failed");
let valid_packet_ids = [TEST_PACKET_ID_0, TEST_PACKET_ID_1];
let mut tc_cacher = TcCacher::default();
let mut next_write_idx = 0;
let parse_result = parse_buffer_for_ccsds_space_packets(
&mut buffer[..packet_len_ping - 4],
valid_packet_ids.as_slice(),
&mut tc_cacher,
&mut next_write_idx,
);
assert_eq!(next_write_idx, 0);
assert!(parse_result.is_ok());
let parsed_packets = parse_result.unwrap();
assert_eq!(parsed_packets, 0);
assert_eq!(tc_cacher.tc_queue.len(), 0);
}
}

View File

@ -1,4 +1,4 @@
use crate::{tmtc::PacketSenderRaw, ComponentId}; use crate::tmtc::ReceivesTcCore;
use cobs::{decode_in_place, encode, max_encoding_length}; use cobs::{decode_in_place, encode, max_encoding_length};
/// This function encodes the given packet with COBS and also wraps the encoded packet with /// This function encodes the given packet with COBS and also wraps the encoded packet with
@ -13,7 +13,7 @@ use cobs::{decode_in_place, encode, max_encoding_length};
/// ///
/// ``` /// ```
/// use cobs::decode_in_place_report; /// use cobs::decode_in_place_report;
/// use satrs::encoding::{encode_packet_with_cobs}; /// use satrs_core::encoding::{encode_packet_with_cobs};
// //
/// const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5]; /// const SIMPLE_PACKET: [u8; 5] = [1, 2, 3, 4, 5];
/// const INVERTED_PACKET: [u8; 5] = [5, 4, 3, 2, 1]; /// const INVERTED_PACKET: [u8; 5] = [5, 4, 3, 2, 1];
@ -55,12 +55,11 @@ pub fn encode_packet_with_cobs(
/// future write operations will be written to the `next_write_idx` argument. /// future write operations will be written to the `next_write_idx` argument.
/// ///
/// The parser will write all packets which were decoded successfully to the given `tc_receiver`. /// The parser will write all packets which were decoded successfully to the given `tc_receiver`.
pub fn parse_buffer_for_cobs_encoded_packets<SendError>( pub fn parse_buffer_for_cobs_encoded_packets<E>(
buf: &mut [u8], buf: &mut [u8],
sender_id: ComponentId, tc_receiver: &mut dyn ReceivesTcCore<Error = E>,
packet_sender: &(impl PacketSenderRaw<Error = SendError> + ?Sized),
next_write_idx: &mut usize, next_write_idx: &mut usize,
) -> Result<u32, SendError> { ) -> Result<u32, E> {
let mut start_index_packet = 0; let mut start_index_packet = 0;
let mut start_found = false; let mut start_found = false;
let mut last_byte = false; let mut last_byte = false;
@ -79,10 +78,8 @@ pub fn parse_buffer_for_cobs_encoded_packets<SendError>(
let decode_result = decode_in_place(&mut buf[start_index_packet..i]); let decode_result = decode_in_place(&mut buf[start_index_packet..i]);
if let Ok(packet_len) = decode_result { if let Ok(packet_len) = decode_result {
packets_found += 1; packets_found += 1;
packet_sender.send_packet( tc_receiver
sender_id, .pass_tc(&buf[start_index_packet..start_index_packet + packet_len])?;
&buf[start_index_packet..start_index_packet + packet_len],
)?;
} }
start_found = false; start_found = false;
} else { } else {
@ -103,39 +100,32 @@ pub fn parse_buffer_for_cobs_encoded_packets<SendError>(
pub(crate) mod tests { pub(crate) mod tests {
use cobs::encode; use cobs::encode;
use crate::{ use crate::encoding::tests::{encode_simple_packet, TcCacher, INVERTED_PACKET, SIMPLE_PACKET};
encoding::tests::{encode_simple_packet, TcCacher, INVERTED_PACKET, SIMPLE_PACKET},
ComponentId,
};
use super::parse_buffer_for_cobs_encoded_packets; use super::parse_buffer_for_cobs_encoded_packets;
const PARSER_ID: ComponentId = 0x05;
#[test] #[test]
fn test_parsing_simple_packet() { fn test_parsing_simple_packet() {
let test_sender = TcCacher::default(); let mut test_sender = TcCacher::default();
let mut encoded_buf: [u8; 16] = [0; 16]; let mut encoded_buf: [u8; 16] = [0; 16];
let mut current_idx = 0; let mut current_idx = 0;
encode_simple_packet(&mut encoded_buf, &mut current_idx); encode_simple_packet(&mut encoded_buf, &mut current_idx);
let mut next_read_idx = 0; let mut next_read_idx = 0;
let packets = parse_buffer_for_cobs_encoded_packets( let packets = parse_buffer_for_cobs_encoded_packets(
&mut encoded_buf[0..current_idx], &mut encoded_buf[0..current_idx],
PARSER_ID, &mut test_sender,
&test_sender,
&mut next_read_idx, &mut next_read_idx,
) )
.unwrap(); .unwrap();
assert_eq!(packets, 1); assert_eq!(packets, 1);
let queue = test_sender.tc_queue.borrow(); assert_eq!(test_sender.tc_queue.len(), 1);
assert_eq!(queue.len(), 1); let packet = &test_sender.tc_queue[0];
let packet = &queue[0]; assert_eq!(packet, &SIMPLE_PACKET);
assert_eq!(packet.packet, &SIMPLE_PACKET);
} }
#[test] #[test]
fn test_parsing_consecutive_packets() { fn test_parsing_consecutive_packets() {
let test_sender = TcCacher::default(); let mut test_sender = TcCacher::default();
let mut encoded_buf: [u8; 16] = [0; 16]; let mut encoded_buf: [u8; 16] = [0; 16];
let mut current_idx = 0; let mut current_idx = 0;
encode_simple_packet(&mut encoded_buf, &mut current_idx); encode_simple_packet(&mut encoded_buf, &mut current_idx);
@ -149,23 +139,21 @@ pub(crate) mod tests {
let mut next_read_idx = 0; let mut next_read_idx = 0;
let packets = parse_buffer_for_cobs_encoded_packets( let packets = parse_buffer_for_cobs_encoded_packets(
&mut encoded_buf[0..current_idx], &mut encoded_buf[0..current_idx],
PARSER_ID, &mut test_sender,
&test_sender,
&mut next_read_idx, &mut next_read_idx,
) )
.unwrap(); .unwrap();
assert_eq!(packets, 2); assert_eq!(packets, 2);
let queue = test_sender.tc_queue.borrow(); assert_eq!(test_sender.tc_queue.len(), 2);
assert_eq!(queue.len(), 2); let packet0 = &test_sender.tc_queue[0];
let packet0 = &queue[0]; assert_eq!(packet0, &SIMPLE_PACKET);
assert_eq!(packet0.packet, &SIMPLE_PACKET); let packet1 = &test_sender.tc_queue[1];
let packet1 = &queue[1]; assert_eq!(packet1, &INVERTED_PACKET);
assert_eq!(packet1.packet, &INVERTED_PACKET);
} }
#[test] #[test]
fn test_split_tail_packet_only() { fn test_split_tail_packet_only() {
let test_sender = TcCacher::default(); let mut test_sender = TcCacher::default();
let mut encoded_buf: [u8; 16] = [0; 16]; let mut encoded_buf: [u8; 16] = [0; 16];
let mut current_idx = 0; let mut current_idx = 0;
encode_simple_packet(&mut encoded_buf, &mut current_idx); encode_simple_packet(&mut encoded_buf, &mut current_idx);
@ -173,19 +161,17 @@ pub(crate) mod tests {
let packets = parse_buffer_for_cobs_encoded_packets( let packets = parse_buffer_for_cobs_encoded_packets(
// Cut off the sentinel byte at the end. // Cut off the sentinel byte at the end.
&mut encoded_buf[0..current_idx - 1], &mut encoded_buf[0..current_idx - 1],
PARSER_ID, &mut test_sender,
&test_sender,
&mut next_read_idx, &mut next_read_idx,
) )
.unwrap(); .unwrap();
assert_eq!(packets, 0); assert_eq!(packets, 0);
let queue = test_sender.tc_queue.borrow(); assert_eq!(test_sender.tc_queue.len(), 0);
assert_eq!(queue.len(), 0);
assert_eq!(next_read_idx, 0); assert_eq!(next_read_idx, 0);
} }
fn generic_test_split_packet(cut_off: usize) { fn generic_test_split_packet(cut_off: usize) {
let test_sender = TcCacher::default(); let mut test_sender = TcCacher::default();
let mut encoded_buf: [u8; 16] = [0; 16]; let mut encoded_buf: [u8; 16] = [0; 16];
assert!(cut_off < INVERTED_PACKET.len() + 1); assert!(cut_off < INVERTED_PACKET.len() + 1);
let mut current_idx = 0; let mut current_idx = 0;
@ -207,15 +193,13 @@ pub(crate) mod tests {
let packets = parse_buffer_for_cobs_encoded_packets( let packets = parse_buffer_for_cobs_encoded_packets(
// Cut off the sentinel byte at the end. // Cut off the sentinel byte at the end.
&mut encoded_buf[0..current_idx - cut_off], &mut encoded_buf[0..current_idx - cut_off],
PARSER_ID, &mut test_sender,
&test_sender,
&mut next_write_idx, &mut next_write_idx,
) )
.unwrap(); .unwrap();
assert_eq!(packets, 1); assert_eq!(packets, 1);
let queue = test_sender.tc_queue.borrow(); assert_eq!(test_sender.tc_queue.len(), 1);
assert_eq!(queue.len(), 1); assert_eq!(&test_sender.tc_queue[0], &SIMPLE_PACKET);
assert_eq!(&queue[0].packet, &SIMPLE_PACKET);
assert_eq!(next_write_idx, next_expected_write_idx); assert_eq!(next_write_idx, next_expected_write_idx);
assert_eq!(encoded_buf[..next_expected_write_idx], expected_at_start); assert_eq!(encoded_buf[..next_expected_write_idx], expected_at_start);
} }
@ -237,7 +221,7 @@ pub(crate) mod tests {
#[test] #[test]
fn test_zero_at_end() { fn test_zero_at_end() {
let test_sender = TcCacher::default(); let mut test_sender = TcCacher::default();
let mut encoded_buf: [u8; 16] = [0; 16]; let mut encoded_buf: [u8; 16] = [0; 16];
let mut next_write_idx = 0; let mut next_write_idx = 0;
let mut current_idx = 0; let mut current_idx = 0;
@ -249,35 +233,31 @@ pub(crate) mod tests {
let packets = parse_buffer_for_cobs_encoded_packets( let packets = parse_buffer_for_cobs_encoded_packets(
// Cut off the sentinel byte at the end. // Cut off the sentinel byte at the end.
&mut encoded_buf[0..current_idx], &mut encoded_buf[0..current_idx],
PARSER_ID, &mut test_sender,
&test_sender,
&mut next_write_idx, &mut next_write_idx,
) )
.unwrap(); .unwrap();
assert_eq!(packets, 1); assert_eq!(packets, 1);
let queue = test_sender.tc_queue.borrow_mut(); assert_eq!(test_sender.tc_queue.len(), 1);
assert_eq!(queue.len(), 1); assert_eq!(&test_sender.tc_queue[0], &SIMPLE_PACKET);
assert_eq!(&queue[0].packet, &SIMPLE_PACKET);
assert_eq!(next_write_idx, 1); assert_eq!(next_write_idx, 1);
assert_eq!(encoded_buf[0], 0); assert_eq!(encoded_buf[0], 0);
} }
#[test] #[test]
fn test_all_zeroes() { fn test_all_zeroes() {
let test_sender = TcCacher::default(); let mut test_sender = TcCacher::default();
let mut all_zeroes: [u8; 5] = [0; 5]; let mut all_zeroes: [u8; 5] = [0; 5];
let mut next_write_idx = 0; let mut next_write_idx = 0;
let packets = parse_buffer_for_cobs_encoded_packets( let packets = parse_buffer_for_cobs_encoded_packets(
// Cut off the sentinel byte at the end. // Cut off the sentinel byte at the end.
&mut all_zeroes, &mut all_zeroes,
PARSER_ID, &mut test_sender,
&test_sender,
&mut next_write_idx, &mut next_write_idx,
) )
.unwrap(); .unwrap();
assert_eq!(packets, 0); assert_eq!(packets, 0);
let queue = test_sender.tc_queue.borrow(); assert!(test_sender.tc_queue.is_empty());
assert!(queue.is_empty());
assert_eq!(next_write_idx, 0); assert_eq!(next_write_idx, 0);
} }
} }

View File

@ -6,14 +6,9 @@ pub use crate::encoding::cobs::{encode_packet_with_cobs, parse_buffer_for_cobs_e
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use core::cell::RefCell; use alloc::{collections::VecDeque, vec::Vec};
use alloc::collections::VecDeque; use crate::tmtc::ReceivesTcCore;
use crate::{
tmtc::{PacketAsVec, PacketSenderRaw},
ComponentId,
};
use super::cobs::encode_packet_with_cobs; use super::cobs::encode_packet_with_cobs;
@ -22,15 +17,14 @@ pub(crate) mod tests {
#[derive(Default)] #[derive(Default)]
pub(crate) struct TcCacher { pub(crate) struct TcCacher {
pub(crate) tc_queue: RefCell<VecDeque<PacketAsVec>>, pub(crate) tc_queue: VecDeque<Vec<u8>>,
} }
impl PacketSenderRaw for TcCacher { impl ReceivesTcCore for TcCacher {
type Error = (); type Error = ();
fn send_packet(&self, sender_id: ComponentId, tc_raw: &[u8]) -> Result<(), Self::Error> { fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> {
let mut mut_queue = self.tc_queue.borrow_mut(); self.tc_queue.push_back(tc_raw.to_vec());
mut_queue.push_back(PacketAsVec::new(sender_id, tc_raw.to_vec()));
Ok(()) Ok(())
} }
} }

49
satrs-core/src/error.rs Normal file
View File

@ -0,0 +1,49 @@
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
);
}
}

720
satrs-core/src/event_man.rs Normal file
View File

@ -0,0 +1,720 @@
//! Event management and forwarding
//!
//! 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.
//!
//! The following graph shows how the event flow for such a setup could look like:
//!
//! ![Event flow][event_man_arch]
//!
//! The event manager has a listener table abstracted by the [ListenerTable], which maps
//! listener groups identified by [ListenerKey]s to a [sender ID][ChannelId].
//! It also contains a sender table abstracted by the [SenderTable] which maps these sender IDs
//! to a concrete [SendEventProvider]s. A simple approach would be to use one send event provider
//! for each OBSW thread and then subscribe for all interesting events for a particular thread
//! using the send event provider ID.
//!
//! This can be done with the [EventManager] like this:
//!
//! 1. Provide a concrete [EventReceiver] implementation. This abstraction allow to use different
//! message queue backends. A straightforward implementation where dynamic memory allocation is
//! not a big concern could use [std::sync::mpsc::channel] to do this and is provided in
//! form of the [MpscEventReceiver].
//! 2. To set up event creators, create channel pairs using some message queue implementation.
//! Each event creator gets a (cloned) sender component which allows it to send events to the
//! manager.
//! 3. The event manager receives the receiver component as part of a [EventReceiver]
//! implementation so all events are routed to the manager.
//! 4. Create the [send event providers][SendEventProvider]s which allow routing events to
//! subscribers. You can now use their [sender IDs][SendEventProvider::id] to subscribe for
//! event groups, for example by using the [EventManager::subscribe_single] method.
//! 5. Add the send provider as well using the [EventManager::add_sender] call so the event
//! manager can route listener groups to a the send provider.
//!
//! Some components like a PUS Event Service or PUS Event Action Service might require all
//! events to package them as telemetry or start actions where applicable.
//! Other components might only be interested in certain events. For example, a thermal system
//! handler might only be interested in temperature events generated by a thermal sensor component.
//!
//! # Examples
//!
//! You can check [integration test](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-core/tests/pus_events.rs)
//! for a concrete example using multi-threading where events are routed to
//! different threads.
use crate::events::{EventU16, EventU32, GenericEvent, LargestEventRaw, LargestGroupIdRaw};
use crate::params::{Params, ParamsHeapless};
#[cfg(feature = "alloc")]
use alloc::boxed::Box;
#[cfg(feature = "alloc")]
use alloc::vec;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use core::slice::Iter;
#[cfg(feature = "alloc")]
use hashbrown::HashMap;
use crate::ChannelId;
#[cfg(feature = "std")]
pub use stdmod::*;
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
pub enum ListenerKey {
Single(LargestEventRaw),
Group(LargestGroupIdRaw),
All,
}
pub type EventWithHeaplessAuxData<Event> = (Event, Option<ParamsHeapless>);
pub type EventU32WithHeaplessAuxData = EventWithHeaplessAuxData<EventU32>;
pub type EventU16WithHeaplessAuxData = EventWithHeaplessAuxData<EventU16>;
pub type EventWithAuxData<Event> = (Event, Option<Params>);
pub type EventU32WithAuxData = EventWithAuxData<EventU32>;
pub type EventU16WithAuxData = EventWithAuxData<EventU16>;
pub trait SendEventProvider<Provider: GenericEvent, AuxDataProvider = Params> {
type Error;
fn id(&self) -> ChannelId;
fn send_no_data(&self, event: Provider) -> Result<(), Self::Error> {
self.send(event, None)
}
fn send(&self, event: Provider, aux_data: Option<AuxDataProvider>) -> Result<(), Self::Error>;
}
/// Generic abstraction for an event receiver.
pub trait EventReceiver<Event: GenericEvent, AuxDataProvider = Params> {
/// This function has to be provided by any event receiver. A receive call may or may not return
/// an event.
///
/// To allow returning arbitrary additional auxiliary data, a mutable slice is passed to the
/// [Self::receive] call as well. Receivers can write data to this slice, but care must be taken
/// to avoid panics due to size missmatches or out of bound writes.
fn receive(&self) -> Option<(Event, Option<AuxDataProvider>)>;
}
pub trait ListenerTable {
fn get_listeners(&self) -> Vec<ListenerKey>;
fn contains_listener(&self, key: &ListenerKey) -> bool;
fn get_listener_ids(&self, key: &ListenerKey) -> Option<Iter<ChannelId>>;
fn add_listener(&mut self, key: ListenerKey, sender_id: ChannelId) -> bool;
fn remove_duplicates(&mut self, key: &ListenerKey);
}
pub trait SenderTable<SendProviderError, Event: GenericEvent = EventU32, AuxDataProvider = Params> {
fn contains_send_event_provider(&self, id: &ChannelId) -> bool;
fn get_send_event_provider(
&self,
id: &ChannelId,
) -> Option<&dyn SendEventProvider<Event, AuxDataProvider, Error = SendProviderError>>;
fn add_send_event_provider(
&mut self,
send_provider: Box<
dyn SendEventProvider<Event, AuxDataProvider, Error = SendProviderError>,
>,
) -> bool;
}
/// Generic event manager implementation.
///
/// # Generics
///
/// * `SendProviderError`: [SendEventProvider] error type
/// * `Event`: Concrete event provider, currently either [EventU32] or [EventU16]
/// * `AuxDataProvider`: Concrete auxiliary data provider, currently either [Params] or
/// [ParamsHeapless]
pub struct EventManager<SendProviderError, Event: GenericEvent = EventU32, AuxDataProvider = Params>
{
listener_table: Box<dyn ListenerTable>,
sender_table: Box<dyn SenderTable<SendProviderError, Event, AuxDataProvider>>,
event_receiver: Box<dyn EventReceiver<Event, AuxDataProvider>>,
}
/// Safety: It is safe to implement [Send] because all fields in the [EventManager] are [Send]
/// as well
#[cfg(feature = "std")]
unsafe impl<E, Event: GenericEvent + Send, AuxDataProvider: Send> Send
for EventManager<E, Event, AuxDataProvider>
{
}
#[cfg(feature = "std")]
pub type EventManagerWithMpscQueue<Event, AuxDataProvider> = EventManager<
std::sync::mpsc::SendError<(Event, Option<AuxDataProvider>)>,
Event,
AuxDataProvider,
>;
#[derive(Debug)]
pub enum EventRoutingResult<Event: GenericEvent, AuxDataProvider> {
/// No event was received
Empty,
/// An event was received and routed.
/// The first tuple entry will contain the number of recipients.
Handled(u32, Event, Option<AuxDataProvider>),
}
#[derive(Debug)]
pub enum EventRoutingError<E> {
SendError(E),
NoSendersForKey(ListenerKey),
NoSenderForId(ChannelId),
}
#[derive(Debug)]
pub struct EventRoutingErrorsWithResult<Event: GenericEvent, AuxDataProvider, E> {
pub result: EventRoutingResult<Event, AuxDataProvider>,
pub errors: [Option<EventRoutingError<E>>; 3],
}
impl<E, Event: GenericEvent + Copy> EventManager<E, Event> {
pub fn remove_duplicates(&mut self, key: &ListenerKey) {
self.listener_table.remove_duplicates(key)
}
/// Subscribe for a unique event.
pub fn subscribe_single(&mut self, event: &Event, sender_id: ChannelId) {
self.update_listeners(ListenerKey::Single(event.raw_as_largest_type()), sender_id);
}
/// Subscribe for an event group.
pub fn subscribe_group(&mut self, group_id: LargestGroupIdRaw, sender_id: ChannelId) {
self.update_listeners(ListenerKey::Group(group_id), sender_id);
}
/// Subscribe for all events received by the manager.
///
/// For example, this can be useful for a handler component which sends every event as
/// a telemetry packet.
pub fn subscribe_all(&mut self, sender_id: ChannelId) {
self.update_listeners(ListenerKey::All, sender_id);
}
}
impl<E: 'static, Event: GenericEvent + Copy + 'static, AuxDataProvider: Clone + 'static>
EventManager<E, Event, AuxDataProvider>
{
/// Create an event manager where the sender table will be the [DefaultSenderTableProvider]
/// and the listener table will be the [DefaultListenerTableProvider].
pub fn new(event_receiver: Box<dyn EventReceiver<Event, AuxDataProvider>>) -> Self {
let listener_table: Box<DefaultListenerTableProvider> = Box::default();
let sender_table: Box<DefaultSenderTableProvider<E, Event, AuxDataProvider>> =
Box::default();
Self::new_custom_tables(listener_table, sender_table, event_receiver)
}
}
impl<E, Event: GenericEvent + Copy, AuxDataProvider: Clone>
EventManager<E, Event, AuxDataProvider>
{
pub fn new_custom_tables(
listener_table: Box<dyn ListenerTable>,
sender_table: Box<dyn SenderTable<E, Event, AuxDataProvider>>,
event_receiver: Box<dyn EventReceiver<Event, AuxDataProvider>>,
) -> Self {
EventManager {
listener_table,
sender_table,
event_receiver,
}
}
pub fn add_sender(
&mut self,
send_provider: impl SendEventProvider<Event, AuxDataProvider, Error = E> + 'static,
) {
if !self
.sender_table
.contains_send_event_provider(&send_provider.id())
{
self.sender_table
.add_send_event_provider(Box::new(send_provider));
}
}
fn update_listeners(&mut self, key: ListenerKey, sender_id: ChannelId) {
self.listener_table.add_listener(key, sender_id);
}
/// This function will use the cached event receiver and try to receive one event.
/// If an event was received, it will try to route that event to all subscribed event listeners.
/// If this works without any issues, the [EventRoutingResult] will contain context information
/// about the routed event.
///
/// This function will track up to 3 errors returned as part of the
/// [EventRoutingErrorsWithResult] error struct.
pub fn try_event_handling(
&self,
) -> Result<
EventRoutingResult<Event, AuxDataProvider>,
EventRoutingErrorsWithResult<Event, AuxDataProvider, E>,
> {
let mut err_idx = 0;
let mut err_slice = [None, None, None];
let mut num_recipients = 0;
let mut add_error = |error: EventRoutingError<E>| {
if err_idx < 3 {
err_slice[err_idx] = Some(error);
err_idx += 1;
}
};
let mut send_handler =
|key: &ListenerKey, event: Event, aux_data: &Option<AuxDataProvider>| {
if self.listener_table.contains_listener(key) {
if let Some(ids) = self.listener_table.get_listener_ids(key) {
for id in ids {
if let Some(sender) = self.sender_table.get_send_event_provider(id) {
if let Err(e) = sender.send(event, aux_data.clone()) {
add_error(EventRoutingError::SendError(e));
} else {
num_recipients += 1;
}
} else {
add_error(EventRoutingError::NoSenderForId(*id));
}
}
} else {
add_error(EventRoutingError::NoSendersForKey(*key));
}
}
};
if let Some((event, aux_data)) = self.event_receiver.receive() {
let single_key = ListenerKey::Single(event.raw_as_largest_type());
send_handler(&single_key, event, &aux_data);
let group_key = ListenerKey::Group(event.group_id_as_largest_type());
send_handler(&group_key, event, &aux_data);
send_handler(&ListenerKey::All, event, &aux_data);
if err_idx > 0 {
return Err(EventRoutingErrorsWithResult {
result: EventRoutingResult::Handled(num_recipients, event, aux_data),
errors: err_slice,
});
}
return Ok(EventRoutingResult::Handled(num_recipients, event, aux_data));
}
Ok(EventRoutingResult::Empty)
}
}
#[derive(Default)]
pub struct DefaultListenerTableProvider {
listeners: HashMap<ListenerKey, Vec<ChannelId>>,
}
pub struct DefaultSenderTableProvider<
SendProviderError,
Event: GenericEvent = EventU32,
AuxDataProvider = Params,
> {
senders: HashMap<
ChannelId,
Box<dyn SendEventProvider<Event, AuxDataProvider, Error = SendProviderError>>,
>,
}
impl<SendProviderError, Event: GenericEvent, AuxDataProvider> Default
for DefaultSenderTableProvider<SendProviderError, Event, AuxDataProvider>
{
fn default() -> Self {
Self {
senders: HashMap::new(),
}
}
}
impl ListenerTable for DefaultListenerTableProvider {
fn get_listeners(&self) -> Vec<ListenerKey> {
let mut key_list = Vec::new();
for key in self.listeners.keys() {
key_list.push(*key);
}
key_list
}
fn contains_listener(&self, key: &ListenerKey) -> bool {
self.listeners.contains_key(key)
}
fn get_listener_ids(&self, key: &ListenerKey) -> Option<Iter<ChannelId>> {
self.listeners.get(key).map(|vec| vec.iter())
}
fn add_listener(&mut self, key: ListenerKey, sender_id: ChannelId) -> bool {
if let Some(existing_list) = self.listeners.get_mut(&key) {
existing_list.push(sender_id);
} else {
let new_list = vec![sender_id];
self.listeners.insert(key, new_list);
}
true
}
fn remove_duplicates(&mut self, key: &ListenerKey) {
if let Some(list) = self.listeners.get_mut(key) {
list.sort_unstable();
list.dedup();
}
}
}
impl<SendProviderError, Event: GenericEvent, AuxDataProvider>
SenderTable<SendProviderError, Event, AuxDataProvider>
for DefaultSenderTableProvider<SendProviderError, Event, AuxDataProvider>
{
fn contains_send_event_provider(&self, id: &ChannelId) -> bool {
self.senders.contains_key(id)
}
fn get_send_event_provider(
&self,
id: &ChannelId,
) -> Option<&dyn SendEventProvider<Event, AuxDataProvider, Error = SendProviderError>> {
self.senders
.get(id)
.filter(|sender| sender.id() == *id)
.map(|v| v.as_ref())
}
fn add_send_event_provider(
&mut self,
send_provider: Box<
dyn SendEventProvider<Event, AuxDataProvider, Error = SendProviderError>,
>,
) -> bool {
let id = send_provider.id();
if self.senders.contains_key(&id) {
return false;
}
self.senders.insert(id, send_provider).is_none()
}
}
#[cfg(feature = "std")]
pub mod stdmod {
use super::*;
use crate::event_man::{EventReceiver, EventWithAuxData};
use crate::events::{EventU16, EventU32, GenericEvent};
use crate::params::Params;
use std::sync::mpsc::{Receiver, SendError, Sender};
pub struct MpscEventReceiver<Event: GenericEvent + Send = EventU32> {
mpsc_receiver: Receiver<(Event, Option<Params>)>,
}
impl<Event: GenericEvent + Send> MpscEventReceiver<Event> {
pub fn new(receiver: Receiver<(Event, Option<Params>)>) -> Self {
Self {
mpsc_receiver: receiver,
}
}
}
impl<Event: GenericEvent + Send> EventReceiver<Event> for MpscEventReceiver<Event> {
fn receive(&self) -> Option<EventWithAuxData<Event>> {
if let Ok(event_and_data) = self.mpsc_receiver.try_recv() {
return Some(event_and_data);
}
None
}
}
pub type MpscEventU32Receiver = MpscEventReceiver<EventU32>;
pub type MpscEventU16Receiver = MpscEventReceiver<EventU16>;
#[derive(Clone)]
pub struct MpscEventSendProvider<Event: GenericEvent + Send> {
id: u32,
sender: Sender<(Event, Option<Params>)>,
}
impl<Event: GenericEvent + Send> MpscEventSendProvider<Event> {
pub fn new(id: u32, sender: Sender<(Event, Option<Params>)>) -> Self {
Self { id, sender }
}
}
impl<Event: GenericEvent + Send> SendEventProvider<Event> for MpscEventSendProvider<Event> {
type Error = SendError<(Event, Option<Params>)>;
fn id(&self) -> u32 {
self.id
}
fn send(&self, event: Event, aux_data: Option<Params>) -> Result<(), Self::Error> {
self.sender.send((event, aux_data))
}
}
pub type MpscEventU32SendProvider = MpscEventSendProvider<EventU32>;
pub type MpscEventU16SendProvider = MpscEventSendProvider<EventU16>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event_man::EventManager;
use crate::events::{EventU32, GenericEvent, Severity};
use crate::params::ParamsRaw;
use alloc::boxed::Box;
use std::format;
use std::sync::mpsc::{channel, Receiver, SendError, Sender};
#[derive(Clone)]
struct MpscEventSenderQueue {
id: u32,
mpsc_sender: Sender<EventU32WithAuxData>,
}
impl MpscEventSenderQueue {
fn new(id: u32, mpsc_sender: Sender<EventU32WithAuxData>) -> Self {
Self { id, mpsc_sender }
}
}
impl SendEventProvider<EventU32> for MpscEventSenderQueue {
type Error = SendError<EventU32WithAuxData>;
fn id(&self) -> u32 {
self.id
}
fn send(&self, event: EventU32, aux_data: Option<Params>) -> Result<(), Self::Error> {
self.mpsc_sender.send((event, aux_data))
}
}
fn check_next_event(
expected: EventU32,
receiver: &Receiver<EventU32WithAuxData>,
) -> Option<Params> {
if let Ok(event) = receiver.try_recv() {
assert_eq!(event.0, expected);
return event.1;
}
None
}
fn check_handled_event(
res: EventRoutingResult<EventU32, Params>,
expected: EventU32,
expected_num_sent: u32,
) {
assert!(matches!(res, EventRoutingResult::Handled { .. }));
if let EventRoutingResult::Handled(num_recipients, event, _aux_data) = res {
assert_eq!(event, expected);
assert_eq!(num_recipients, expected_num_sent);
}
}
fn generic_event_man() -> (
Sender<EventU32WithAuxData>,
EventManager<SendError<EventU32WithAuxData>>,
) {
let (event_sender, manager_queue) = channel();
let event_man_receiver = MpscEventReceiver::new(manager_queue);
(
event_sender,
EventManager::new(Box::new(event_man_receiver)),
)
}
#[test]
fn test_basic() {
let (event_sender, mut event_man) = generic_event_man();
let event_grp_0 = EventU32::new(Severity::INFO, 0, 0).unwrap();
let event_grp_1_0 = EventU32::new(Severity::HIGH, 1, 0).unwrap();
let (single_event_sender, single_event_receiver) = channel();
let single_event_listener = MpscEventSenderQueue::new(0, single_event_sender);
event_man.subscribe_single(&event_grp_0, single_event_listener.id());
event_man.add_sender(single_event_listener);
let (group_event_sender_0, group_event_receiver_0) = channel();
let group_event_listener = MpscEventSenderQueue {
id: 1,
mpsc_sender: group_event_sender_0,
};
event_man.subscribe_group(event_grp_1_0.group_id(), group_event_listener.id());
event_man.add_sender(group_event_listener);
// Test event with one listener
event_sender
.send((event_grp_0, None))
.expect("Sending single error failed");
let res = event_man.try_event_handling();
assert!(res.is_ok());
check_handled_event(res.unwrap(), event_grp_0, 1);
check_next_event(event_grp_0, &single_event_receiver);
// Test event which is sent to all group listeners
event_sender
.send((event_grp_1_0, None))
.expect("Sending group error failed");
let res = event_man.try_event_handling();
assert!(res.is_ok());
check_handled_event(res.unwrap(), event_grp_1_0, 1);
check_next_event(event_grp_1_0, &group_event_receiver_0);
}
#[test]
fn test_with_basic_aux_data() {
let (event_sender, mut event_man) = generic_event_man();
let event_grp_0 = EventU32::new(Severity::INFO, 0, 0).unwrap();
let (single_event_sender, single_event_receiver) = channel();
let single_event_listener = MpscEventSenderQueue::new(0, single_event_sender);
event_man.subscribe_single(&event_grp_0, single_event_listener.id());
event_man.add_sender(single_event_listener);
event_sender
.send((event_grp_0, Some(Params::Heapless((2_u32, 3_u32).into()))))
.expect("Sending group error failed");
let res = event_man.try_event_handling();
assert!(res.is_ok());
check_handled_event(res.unwrap(), event_grp_0, 1);
let aux = check_next_event(event_grp_0, &single_event_receiver);
assert!(aux.is_some());
let aux = aux.unwrap();
if let Params::Heapless(ParamsHeapless::Raw(ParamsRaw::U32Pair(pair))) = aux {
assert_eq!(pair.0, 2);
assert_eq!(pair.1, 3);
} else {
panic!("{}", format!("Unexpected auxiliary value type {:?}", aux));
}
}
/// Test listening for multiple groups
#[test]
fn test_multi_group() {
let (event_sender, mut event_man) = generic_event_man();
let res = event_man.try_event_handling();
assert!(res.is_ok());
let hres = res.unwrap();
assert!(matches!(hres, EventRoutingResult::Empty));
let event_grp_0 = EventU32::new(Severity::INFO, 0, 0).unwrap();
let event_grp_1_0 = EventU32::new(Severity::HIGH, 1, 0).unwrap();
let (event_grp_0_sender, event_grp_0_receiver) = channel();
let event_grp_0_and_1_listener = MpscEventSenderQueue {
id: 0,
mpsc_sender: event_grp_0_sender,
};
event_man.subscribe_group(event_grp_0.group_id(), event_grp_0_and_1_listener.id());
event_man.subscribe_group(event_grp_1_0.group_id(), event_grp_0_and_1_listener.id());
event_man.add_sender(event_grp_0_and_1_listener);
event_sender
.send((event_grp_0, None))
.expect("Sending Event Group 0 failed");
event_sender
.send((event_grp_1_0, None))
.expect("Sendign Event Group 1 failed");
let res = event_man.try_event_handling();
assert!(res.is_ok());
check_handled_event(res.unwrap(), event_grp_0, 1);
let res = event_man.try_event_handling();
assert!(res.is_ok());
check_handled_event(res.unwrap(), event_grp_1_0, 1);
check_next_event(event_grp_0, &event_grp_0_receiver);
check_next_event(event_grp_1_0, &event_grp_0_receiver);
}
/// Test listening to the same event from multiple listeners. Also test listening
/// to both group and single events from one listener
#[test]
fn test_listening_to_same_event_and_multi_type() {
let (event_sender, mut event_man) = generic_event_man();
let event_0 = EventU32::new(Severity::INFO, 0, 5).unwrap();
let event_1 = EventU32::new(Severity::HIGH, 1, 0).unwrap();
let (event_0_tx_0, event_0_rx_0) = channel();
let (event_0_tx_1, event_0_rx_1) = channel();
let event_listener_0 = MpscEventSenderQueue {
id: 0,
mpsc_sender: event_0_tx_0,
};
let event_listener_1 = MpscEventSenderQueue {
id: 1,
mpsc_sender: event_0_tx_1,
};
let event_listener_0_sender_id = event_listener_0.id();
event_man.subscribe_single(&event_0, event_listener_0_sender_id);
event_man.add_sender(event_listener_0);
let event_listener_1_sender_id = event_listener_1.id();
event_man.subscribe_single(&event_0, event_listener_1_sender_id);
event_man.add_sender(event_listener_1);
event_sender
.send((event_0, None))
.expect("Triggering Event 0 failed");
let res = event_man.try_event_handling();
assert!(res.is_ok());
check_handled_event(res.unwrap(), event_0, 2);
check_next_event(event_0, &event_0_rx_0);
check_next_event(event_0, &event_0_rx_1);
event_man.subscribe_group(event_1.group_id(), event_listener_0_sender_id);
event_sender
.send((event_0, None))
.expect("Triggering Event 0 failed");
event_sender
.send((event_1, None))
.expect("Triggering Event 1 failed");
// 3 Events messages will be sent now
let res = event_man.try_event_handling();
assert!(res.is_ok());
check_handled_event(res.unwrap(), event_0, 2);
let res = event_man.try_event_handling();
assert!(res.is_ok());
check_handled_event(res.unwrap(), event_1, 1);
// Both the single event and the group event should arrive now
check_next_event(event_0, &event_0_rx_0);
check_next_event(event_1, &event_0_rx_0);
// Do double insertion and then remove duplicates
event_man.subscribe_group(event_1.group_id(), event_listener_0_sender_id);
event_man.remove_duplicates(&ListenerKey::Group(event_1.group_id()));
event_sender
.send((event_1, None))
.expect("Triggering Event 1 failed");
let res = event_man.try_event_handling();
assert!(res.is_ok());
check_handled_event(res.unwrap(), event_1, 1);
}
#[test]
fn test_all_events_listener() {
let (event_sender, manager_queue) = channel();
let event_man_receiver = MpscEventReceiver::new(manager_queue);
let mut event_man: EventManager<SendError<EventU32WithAuxData>> =
EventManager::new(Box::new(event_man_receiver));
let event_0 = EventU32::new(Severity::INFO, 0, 5).unwrap();
let event_1 = EventU32::new(Severity::HIGH, 1, 0).unwrap();
let (event_0_tx_0, all_events_rx) = channel();
let all_events_listener = MpscEventSenderQueue {
id: 0,
mpsc_sender: event_0_tx_0,
};
event_man.subscribe_all(all_events_listener.id());
event_man.add_sender(all_events_listener);
event_sender
.send((event_0, None))
.expect("Triggering event 0 failed");
event_sender
.send((event_1, None))
.expect("Triggering event 1 failed");
let res = event_man.try_event_handling();
assert!(res.is_ok());
check_handled_event(res.unwrap(), event_0, 1);
let res = event_man.try_event_handling();
assert!(res.is_ok());
check_handled_event(res.unwrap(), event_1, 1);
check_next_event(event_0, &all_events_rx);
check_next_event(event_1, &all_events_rx);
}
}

View File

@ -18,14 +18,14 @@
//! # Examples //! # Examples
//! //!
//! ``` //! ```
//! use satrs::events::{EventU16, EventU32, EventU32TypedSev, Severity, SeverityHigh, SeverityInfo}; //! use satrs_core::events::{EventU16, EventU32, EventU32TypedSev, Severity, SeverityHigh, SeverityInfo};
//! //!
//! const MSG_RECVD: EventU32TypedSev<SeverityInfo> = EventU32TypedSev::new(1, 0); //! const MSG_RECVD: EventU32TypedSev<SeverityInfo> = EventU32TypedSev::const_new(1, 0);
//! const MSG_FAILED: EventU32 = EventU32::new(Severity::Low, 1, 1); //! const MSG_FAILED: EventU32 = EventU32::const_new(Severity::LOW, 1, 1);
//! //!
//! const TEMPERATURE_HIGH: EventU32TypedSev<SeverityHigh> = EventU32TypedSev::new(2, 0); //! const TEMPERATURE_HIGH: EventU32TypedSev<SeverityHigh> = EventU32TypedSev::const_new(2, 0);
//! //!
//! let small_event = EventU16::new(Severity::Info, 3, 0); //! let small_event = EventU16::new(Severity::INFO, 3, 0);
//! ``` //! ```
use core::fmt::Debug; use core::fmt::Debug;
use core::hash::Hash; use core::hash::Hash;
@ -40,17 +40,12 @@ pub type LargestEventRaw = u32;
/// Using a type definition allows to change this to u32 in the future more easily /// Using a type definition allows to change this to u32 in the future more easily
pub type LargestGroupIdRaw = u16; pub type LargestGroupIdRaw = u16;
pub const MAX_GROUP_ID_U32_EVENT: u16 = 2_u16.pow(14) - 1;
pub const MAX_GROUP_ID_U16_EVENT: u16 = 2_u16.pow(6) - 1;
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Severity { pub enum Severity {
Info = 0, INFO = 0,
Low = 1, LOW = 1,
Medium = 2, MEDIUM = 2,
High = 3, HIGH = 3,
} }
pub trait HasSeverity: Debug + PartialEq + Eq + Copy + Clone { pub trait HasSeverity: Debug + PartialEq + Eq + Copy + Clone {
@ -61,31 +56,31 @@ pub trait HasSeverity: Debug + PartialEq + Eq + Copy + Clone {
#[derive(Debug, PartialEq, Eq, Copy, Clone)] #[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub struct SeverityInfo {} pub struct SeverityInfo {}
impl HasSeverity for SeverityInfo { impl HasSeverity for SeverityInfo {
const SEVERITY: Severity = Severity::Info; const SEVERITY: Severity = Severity::INFO;
} }
/// Type level support struct /// Type level support struct
#[derive(Debug, PartialEq, Eq, Copy, Clone)] #[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub struct SeverityLow {} pub struct SeverityLow {}
impl HasSeverity for SeverityLow { impl HasSeverity for SeverityLow {
const SEVERITY: Severity = Severity::Low; const SEVERITY: Severity = Severity::LOW;
} }
/// Type level support struct /// Type level support struct
#[derive(Debug, PartialEq, Eq, Copy, Clone)] #[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub struct SeverityMedium {} pub struct SeverityMedium {}
impl HasSeverity for SeverityMedium { impl HasSeverity for SeverityMedium {
const SEVERITY: Severity = Severity::Medium; const SEVERITY: Severity = Severity::MEDIUM;
} }
/// Type level support struct /// Type level support struct
#[derive(Debug, PartialEq, Eq, Copy, Clone)] #[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub struct SeverityHigh {} pub struct SeverityHigh {}
impl HasSeverity for SeverityHigh { impl HasSeverity for SeverityHigh {
const SEVERITY: Severity = Severity::High; const SEVERITY: Severity = Severity::HIGH;
} }
pub trait GenericEvent: EcssEnumeration + Copy + Clone { pub trait GenericEvent: EcssEnumeration {
type Raw; type Raw;
type GroupId; type GroupId;
type UniqueId; type UniqueId;
@ -104,29 +99,27 @@ impl TryFrom<u8> for Severity {
fn try_from(value: u8) -> Result<Self, Self::Error> { fn try_from(value: u8) -> Result<Self, Self::Error> {
match value { match value {
x if x == Severity::Info as u8 => Ok(Severity::Info), x if x == Severity::INFO as u8 => Ok(Severity::INFO),
x if x == Severity::Low as u8 => Ok(Severity::Low), x if x == Severity::LOW as u8 => Ok(Severity::LOW),
x if x == Severity::Medium as u8 => Ok(Severity::Medium), x if x == Severity::MEDIUM as u8 => Ok(Severity::MEDIUM),
x if x == Severity::High as u8 => Ok(Severity::High), x if x == Severity::HIGH as u8 => Ok(Severity::HIGH),
_ => Err(()), _ => Err(()),
} }
} }
} }
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] struct EventBase<RAW, GID, UID> {
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
struct EventBase<Raw, GroupId, UniqueId> {
severity: Severity, severity: Severity,
group_id: GroupId, group_id: GID,
unique_id: UniqueId, unique_id: UID,
phantom: PhantomData<Raw>, phantom: PhantomData<RAW>,
} }
impl<Raw: ToBeBytes, GroupId, UniqueId> EventBase<Raw, GroupId, UniqueId> { impl<RAW: ToBeBytes, GID, UID> EventBase<RAW, GID, UID> {
fn write_to_bytes( fn write_to_bytes(
&self, &self,
raw: Raw, raw: RAW,
buf: &mut [u8], buf: &mut [u8],
width: usize, width: usize,
) -> Result<usize, ByteConversionError> { ) -> Result<usize, ByteConversionError> {
@ -274,7 +267,6 @@ macro_rules! const_from_fn {
} }
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EventU32 { pub struct EventU32 {
base: EventBase<u32, u16, u16>, base: EventBase<u32, u16, u16>,
} }
@ -317,12 +309,12 @@ impl EventU32 {
/// next 14 bits after the severity. Therefore, the size is limited by dec 16383 hex 0x3FFF. /// next 14 bits after the severity. Therefore, the size is limited by dec 16383 hex 0x3FFF.
/// * `unique_id`: Each event has a unique 16 bit ID occupying the last 16 bits of the /// * `unique_id`: Each event has a unique 16 bit ID occupying the last 16 bits of the
/// raw event ID /// raw event ID
pub fn new_checked( pub fn new(
severity: Severity, severity: Severity,
group_id: <Self as GenericEvent>::GroupId, group_id: <Self as GenericEvent>::GroupId,
unique_id: <Self as GenericEvent>::UniqueId, unique_id: <Self as GenericEvent>::UniqueId,
) -> Option<Self> { ) -> Option<Self> {
if group_id > MAX_GROUP_ID_U32_EVENT { if group_id > (2u16.pow(14) - 1) {
return None; return None;
} }
Some(Self { Some(Self {
@ -334,14 +326,12 @@ impl EventU32 {
}, },
}) })
} }
pub const fn const_new(
/// This constructor will panic if the passed group is is larger than [MAX_GROUP_ID_U32_EVENT].
pub const fn new(
severity: Severity, severity: Severity,
group_id: <Self as GenericEvent>::GroupId, group_id: <Self as GenericEvent>::GroupId,
unique_id: <Self as GenericEvent>::UniqueId, unique_id: <Self as GenericEvent>::UniqueId,
) -> Self { ) -> Self {
if group_id > MAX_GROUP_ID_U32_EVENT { if group_id > (2u16.pow(14) - 1) {
panic!("Group ID too large"); panic!("Group ID too large");
} }
Self { Self {
@ -354,67 +344,32 @@ impl EventU32 {
} }
} }
pub fn from_be_bytes(bytes: [u8; 4]) -> Self {
Self::from(u32::from_be_bytes(bytes))
}
const_from_fn!(const_from_info, EventU32TypedSev, SeverityInfo); const_from_fn!(const_from_info, EventU32TypedSev, SeverityInfo);
const_from_fn!(const_from_low, EventU32TypedSev, SeverityLow); const_from_fn!(const_from_low, EventU32TypedSev, SeverityLow);
const_from_fn!(const_from_medium, EventU32TypedSev, SeverityMedium); const_from_fn!(const_from_medium, EventU32TypedSev, SeverityMedium);
const_from_fn!(const_from_high, EventU32TypedSev, SeverityHigh); const_from_fn!(const_from_high, EventU32TypedSev, SeverityHigh);
} }
impl From<u32> for EventU32 {
fn from(raw: u32) -> Self {
// Severity conversion from u8 should never fail
let severity = Severity::try_from(((raw >> 30) & 0b11) as u8).unwrap();
let group_id = ((raw >> 16) & 0x3FFF) as u16;
let unique_id = (raw & 0xFFFF) as u16;
// Sanitized input, should never fail
Self::new(severity, group_id, unique_id)
}
}
impl UnsignedEnum for EventU32 {
fn size(&self) -> usize {
core::mem::size_of::<u32>()
}
fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
self.base.write_to_bytes(self.raw(), buf, self.size())
}
fn value(&self) -> u64 {
self.raw().into()
}
}
impl EcssEnumeration for EventU32 {
fn pfc(&self) -> u8 {
u32::BITS as u8
}
}
impl<SEVERITY: HasSeverity> EventU32TypedSev<SEVERITY> { impl<SEVERITY: HasSeverity> EventU32TypedSev<SEVERITY> {
/// This is similar to [EventU32::new] but the severity is a type generic, which allows to /// This is similar to [EventU32::new] but the severity is a type generic, which allows to
/// have distinct types for events with different severities /// have distinct types for events with different severities
pub fn new_checked( pub fn new(
group_id: <Self as GenericEvent>::GroupId, group_id: <Self as GenericEvent>::GroupId,
unique_id: <Self as GenericEvent>::UniqueId, unique_id: <Self as GenericEvent>::UniqueId,
) -> Option<Self> { ) -> Option<Self> {
let event = EventU32::new_checked(SEVERITY::SEVERITY, group_id, unique_id)?; let event = EventU32::new(SEVERITY::SEVERITY, group_id, unique_id)?;
Some(Self { Some(Self {
event, event,
phantom: PhantomData, phantom: PhantomData,
}) })
} }
/// This constructor will panic if the `group_id` is larger than [MAX_GROUP_ID_U32_EVENT]. /// Const version of [Self::new], but panics on invalid group ID input values.
pub const fn new( pub const fn const_new(
group_id: <Self as GenericEvent>::GroupId, group_id: <Self as GenericEvent>::GroupId,
unique_id: <Self as GenericEvent>::UniqueId, unique_id: <Self as GenericEvent>::UniqueId,
) -> Self { ) -> Self {
let event = EventU32::new(SEVERITY::SEVERITY, group_id, unique_id); let event = EventU32::const_new(SEVERITY::SEVERITY, group_id, unique_id);
Self { Self {
event, event,
phantom: PhantomData, phantom: PhantomData,
@ -426,24 +381,50 @@ impl<SEVERITY: HasSeverity> EventU32TypedSev<SEVERITY> {
if severity != expected { if severity != expected {
return Err(severity); return Err(severity);
} }
Ok(Self::new( Ok(Self::const_new(
((raw >> 16) & 0x3FFF) as u16, ((raw >> 16) & 0x3FFF) as u16,
(raw & 0xFFFF) as u16, (raw & 0xFFFF) as u16,
)) ))
} }
} }
try_from_impls!(SeverityInfo, Severity::Info, u32, EventU32TypedSev); impl From<u32> for EventU32 {
try_from_impls!(SeverityLow, Severity::Low, u32, EventU32TypedSev); fn from(raw: u32) -> Self {
try_from_impls!(SeverityMedium, Severity::Medium, u32, EventU32TypedSev); // Severity conversion from u8 should never fail
try_from_impls!(SeverityHigh, Severity::High, u32, EventU32TypedSev); let severity = Severity::try_from(((raw >> 30) & 0b11) as u8).unwrap();
let group_id = ((raw >> 16) & 0x3FFF) as u16;
let unique_id = (raw & 0xFFFF) as u16;
// Sanitized input, should never fail
Self::const_new(severity, group_id, unique_id)
}
}
try_from_impls!(SeverityInfo, Severity::INFO, u32, EventU32TypedSev);
try_from_impls!(SeverityLow, Severity::LOW, u32, EventU32TypedSev);
try_from_impls!(SeverityMedium, Severity::MEDIUM, u32, EventU32TypedSev);
try_from_impls!(SeverityHigh, Severity::HIGH, u32, EventU32TypedSev);
impl UnsignedEnum for EventU32 {
fn size(&self) -> usize {
core::mem::size_of::<u32>()
}
fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
self.base.write_to_bytes(self.raw(), buf, self.size())
}
}
impl EcssEnumeration for EventU32 {
fn pfc(&self) -> u8 {
u32::BITS as u8
}
}
//noinspection RsTraitImplementation //noinspection RsTraitImplementation
impl<SEVERITY: HasSeverity> UnsignedEnum for EventU32TypedSev<SEVERITY> { impl<SEVERITY: HasSeverity> UnsignedEnum for EventU32TypedSev<SEVERITY> {
delegate!(to self.event { delegate!(to self.event {
fn size(&self) -> usize; fn size(&self) -> usize;
fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError>; fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError>;
fn value(&self) -> u64;
}); });
} }
@ -455,8 +436,6 @@ impl<SEVERITY: HasSeverity> EcssEnumeration for EventU32TypedSev<SEVERITY> {
} }
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct EventU16 { pub struct EventU16 {
base: EventBase<u16, u8, u8>, base: EventBase<u16, u8, u8>,
} }
@ -491,7 +470,7 @@ impl EventU16 {
/// next 6 bits after the severity. Therefore, the size is limited by dec 63 hex 0x3F. /// next 6 bits after the severity. Therefore, the size is limited by dec 63 hex 0x3F.
/// * `unique_id`: Each event has a unique 8 bit ID occupying the last 8 bits of the /// * `unique_id`: Each event has a unique 8 bit ID occupying the last 8 bits of the
/// raw event ID /// raw event ID
pub fn new_checked( pub fn new(
severity: Severity, severity: Severity,
group_id: <Self as GenericEvent>::GroupId, group_id: <Self as GenericEvent>::GroupId,
unique_id: <Self as GenericEvent>::UniqueId, unique_id: <Self as GenericEvent>::UniqueId,
@ -509,8 +488,8 @@ impl EventU16 {
}) })
} }
/// This constructor will panic if the `group_id` is larger than [MAX_GROUP_ID_U16_EVENT]. /// Const version of [Self::new], but panics on invalid group ID input values.
pub const fn new( pub const fn const_new(
severity: Severity, severity: Severity,
group_id: <Self as GenericEvent>::GroupId, group_id: <Self as GenericEvent>::GroupId,
unique_id: <Self as GenericEvent>::UniqueId, unique_id: <Self as GenericEvent>::UniqueId,
@ -527,66 +506,32 @@ impl EventU16 {
}, },
} }
} }
pub fn from_be_bytes(bytes: [u8; 2]) -> Self {
Self::from(u16::from_be_bytes(bytes))
}
const_from_fn!(const_from_info, EventU16TypedSev, SeverityInfo); const_from_fn!(const_from_info, EventU16TypedSev, SeverityInfo);
const_from_fn!(const_from_low, EventU16TypedSev, SeverityLow); const_from_fn!(const_from_low, EventU16TypedSev, SeverityLow);
const_from_fn!(const_from_medium, EventU16TypedSev, SeverityMedium); const_from_fn!(const_from_medium, EventU16TypedSev, SeverityMedium);
const_from_fn!(const_from_high, EventU16TypedSev, SeverityHigh); const_from_fn!(const_from_high, EventU16TypedSev, SeverityHigh);
} }
impl From<u16> for EventU16 {
fn from(raw: <Self as GenericEvent>::Raw) -> Self {
let severity = Severity::try_from(((raw >> 14) & 0b11) as u8).unwrap();
let group_id = ((raw >> 8) & 0x3F) as u8;
let unique_id = (raw & 0xFF) as u8;
// Sanitized input, new call should never fail
Self::new(severity, group_id, unique_id)
}
}
impl UnsignedEnum for EventU16 {
fn size(&self) -> usize {
core::mem::size_of::<u16>()
}
fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
self.base.write_to_bytes(self.raw(), buf, self.size())
}
fn value(&self) -> u64 {
self.raw().into()
}
}
impl EcssEnumeration for EventU16 {
#[inline]
fn pfc(&self) -> u8 {
u16::BITS as u8
}
}
impl<SEVERITY: HasSeverity> EventU16TypedSev<SEVERITY> { impl<SEVERITY: HasSeverity> EventU16TypedSev<SEVERITY> {
/// This is similar to [EventU16::new] but the severity is a type generic, which allows to /// This is similar to [EventU16::new] but the severity is a type generic, which allows to
/// have distinct types for events with different severities /// have distinct types for events with different severities
pub fn new_checked( pub fn new(
group_id: <Self as GenericEvent>::GroupId, group_id: <Self as GenericEvent>::GroupId,
unique_id: <Self as GenericEvent>::UniqueId, unique_id: <Self as GenericEvent>::UniqueId,
) -> Option<Self> { ) -> Option<Self> {
let event = EventU16::new_checked(SEVERITY::SEVERITY, group_id, unique_id)?; let event = EventU16::new(SEVERITY::SEVERITY, group_id, unique_id)?;
Some(Self { Some(Self {
event, event,
phantom: PhantomData, phantom: PhantomData,
}) })
} }
/// This constructor will panic if the `group_id` is larger than [MAX_GROUP_ID_U16_EVENT]. /// Const version of [Self::new], but panics on invalid group ID input values.
pub const fn new( pub const fn const_new(
group_id: <Self as GenericEvent>::GroupId, group_id: <Self as GenericEvent>::GroupId,
unique_id: <Self as GenericEvent>::UniqueId, unique_id: <Self as GenericEvent>::UniqueId,
) -> Self { ) -> Self {
let event = EventU16::new(SEVERITY::SEVERITY, group_id, unique_id); let event = EventU16::const_new(SEVERITY::SEVERITY, group_id, unique_id);
Self { Self {
event, event,
phantom: PhantomData, phantom: PhantomData,
@ -598,18 +543,36 @@ impl<SEVERITY: HasSeverity> EventU16TypedSev<SEVERITY> {
if severity != expected { if severity != expected {
return Err(severity); return Err(severity);
} }
Ok(Self::new(((raw >> 8) & 0x3F) as u8, (raw & 0xFF) as u8)) Ok(Self::const_new(
((raw >> 8) & 0x3F) as u8,
(raw & 0xFF) as u8,
))
} }
} }
impl_event_provider!(EventU16, EventU16TypedSev, u16, u8, u8); impl_event_provider!(EventU16, EventU16TypedSev, u16, u8, u8);
impl UnsignedEnum for EventU16 {
fn size(&self) -> usize {
core::mem::size_of::<u16>()
}
fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
self.base.write_to_bytes(self.raw(), buf, self.size())
}
}
impl EcssEnumeration for EventU16 {
#[inline]
fn pfc(&self) -> u8 {
u16::BITS as u8
}
}
//noinspection RsTraitImplementation //noinspection RsTraitImplementation
impl<SEVERITY: HasSeverity> UnsignedEnum for EventU16TypedSev<SEVERITY> { impl<SEVERITY: HasSeverity> UnsignedEnum for EventU16TypedSev<SEVERITY> {
delegate!(to self.event { delegate!(to self.event {
fn size(&self) -> usize; fn size(&self) -> usize;
fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError>; fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError>;
fn value(&self) -> u64;
}); });
} }
@ -620,10 +583,20 @@ impl<SEVERITY: HasSeverity> EcssEnumeration for EventU16TypedSev<SEVERITY> {
}); });
} }
try_from_impls!(SeverityInfo, Severity::Info, u16, EventU16TypedSev); impl From<u16> for EventU16 {
try_from_impls!(SeverityLow, Severity::Low, u16, EventU16TypedSev); fn from(raw: <Self as GenericEvent>::Raw) -> Self {
try_from_impls!(SeverityMedium, Severity::Medium, u16, EventU16TypedSev); let severity = Severity::try_from(((raw >> 14) & 0b11) as u8).unwrap();
try_from_impls!(SeverityHigh, Severity::High, u16, EventU16TypedSev); let group_id = ((raw >> 8) & 0x3F) as u8;
let unique_id = (raw & 0xFF) as u8;
// Sanitized input, new call should never fail
Self::const_new(severity, group_id, unique_id)
}
}
try_from_impls!(SeverityInfo, Severity::INFO, u16, EventU16TypedSev);
try_from_impls!(SeverityLow, Severity::LOW, u16, EventU16TypedSev);
try_from_impls!(SeverityMedium, Severity::MEDIUM, u16, EventU16TypedSev);
try_from_impls!(SeverityHigh, Severity::HIGH, u16, EventU16TypedSev);
impl<Severity: HasSeverity> PartialEq<EventU32> for EventU32TypedSev<Severity> { impl<Severity: HasSeverity> PartialEq<EventU32> for EventU32TypedSev<Severity> {
#[inline] #[inline]
@ -664,10 +637,12 @@ mod tests {
assert_eq!(size_of::<T>(), val); assert_eq!(size_of::<T>(), val);
} }
const INFO_EVENT: EventU32TypedSev<SeverityInfo> = EventU32TypedSev::new(0, 0); const INFO_EVENT: EventU32TypedSev<SeverityInfo> = EventU32TypedSev::const_new(0, 0);
const INFO_EVENT_SMALL: EventU16TypedSev<SeverityInfo> = EventU16TypedSev::new(0, 0); const INFO_EVENT_SMALL: EventU16TypedSev<SeverityInfo> = EventU16TypedSev::const_new(0, 0);
const HIGH_SEV_EVENT: EventU32TypedSev<SeverityHigh> = EventU32TypedSev::new(0x3FFF, 0xFFFF); const HIGH_SEV_EVENT: EventU32TypedSev<SeverityHigh> =
const HIGH_SEV_EVENT_SMALL: EventU16TypedSev<SeverityHigh> = EventU16TypedSev::new(0x3F, 0xff); EventU32TypedSev::const_new(0x3FFF, 0xFFFF);
const HIGH_SEV_EVENT_SMALL: EventU16TypedSev<SeverityHigh> =
EventU16TypedSev::const_new(0x3F, 0xff);
/// This working is a test in itself. /// This working is a test in itself.
const INFO_REDUCED: EventU32 = EventU32::const_from_info(INFO_EVENT); const INFO_REDUCED: EventU32 = EventU32::const_from_info(INFO_EVENT);
@ -698,7 +673,7 @@ mod tests {
#[test] #[test]
fn test_normal_event_getters() { fn test_normal_event_getters() {
assert_eq!(INFO_EVENT.severity(), Severity::Info); assert_eq!(INFO_EVENT.severity(), Severity::INFO);
assert_eq!(INFO_EVENT.unique_id(), 0); assert_eq!(INFO_EVENT.unique_id(), 0);
assert_eq!(INFO_EVENT.group_id(), 0); assert_eq!(INFO_EVENT.group_id(), 0);
let raw_event = INFO_EVENT.raw(); let raw_event = INFO_EVENT.raw();
@ -707,7 +682,7 @@ mod tests {
#[test] #[test]
fn test_small_event_getters() { fn test_small_event_getters() {
assert_eq!(INFO_EVENT_SMALL.severity(), Severity::Info); assert_eq!(INFO_EVENT_SMALL.severity(), Severity::INFO);
assert_eq!(INFO_EVENT_SMALL.unique_id(), 0); assert_eq!(INFO_EVENT_SMALL.unique_id(), 0);
assert_eq!(INFO_EVENT_SMALL.group_id(), 0); assert_eq!(INFO_EVENT_SMALL.group_id(), 0);
let raw_event = INFO_EVENT_SMALL.raw(); let raw_event = INFO_EVENT_SMALL.raw();
@ -716,7 +691,7 @@ mod tests {
#[test] #[test]
fn all_ones_event_regular() { fn all_ones_event_regular() {
assert_eq!(HIGH_SEV_EVENT.severity(), Severity::High); assert_eq!(HIGH_SEV_EVENT.severity(), Severity::HIGH);
assert_eq!(HIGH_SEV_EVENT.group_id(), 0x3FFF); assert_eq!(HIGH_SEV_EVENT.group_id(), 0x3FFF);
assert_eq!(HIGH_SEV_EVENT.unique_id(), 0xFFFF); assert_eq!(HIGH_SEV_EVENT.unique_id(), 0xFFFF);
let raw_event = HIGH_SEV_EVENT.raw(); let raw_event = HIGH_SEV_EVENT.raw();
@ -725,7 +700,7 @@ mod tests {
#[test] #[test]
fn all_ones_event_small() { fn all_ones_event_small() {
assert_eq!(HIGH_SEV_EVENT_SMALL.severity(), Severity::High); assert_eq!(HIGH_SEV_EVENT_SMALL.severity(), Severity::HIGH);
assert_eq!(HIGH_SEV_EVENT_SMALL.group_id(), 0x3F); assert_eq!(HIGH_SEV_EVENT_SMALL.group_id(), 0x3F);
assert_eq!(HIGH_SEV_EVENT_SMALL.unique_id(), 0xFF); assert_eq!(HIGH_SEV_EVENT_SMALL.unique_id(), 0xFF);
let raw_event = HIGH_SEV_EVENT_SMALL.raw(); let raw_event = HIGH_SEV_EVENT_SMALL.raw();
@ -734,19 +709,18 @@ mod tests {
#[test] #[test]
fn invalid_group_id_normal() { fn invalid_group_id_normal() {
assert!(EventU32TypedSev::<SeverityMedium>::new_checked(2_u16.pow(14), 0).is_none()); assert!(EventU32TypedSev::<SeverityMedium>::new(2_u16.pow(14), 0).is_none());
} }
#[test] #[test]
fn invalid_group_id_small() { fn invalid_group_id_small() {
assert!(EventU16TypedSev::<SeverityMedium>::new_checked(2_u8.pow(6), 0).is_none()); assert!(EventU16TypedSev::<SeverityMedium>::new(2_u8.pow(6), 0).is_none());
} }
#[test] #[test]
fn regular_new() { fn regular_new() {
assert_eq!( assert_eq!(
EventU32TypedSev::<SeverityInfo>::new_checked(0, 0) EventU32TypedSev::<SeverityInfo>::new(0, 0).expect("Creating regular event failed"),
.expect("Creating regular event failed"),
INFO_EVENT INFO_EVENT
); );
} }
@ -754,8 +728,7 @@ mod tests {
#[test] #[test]
fn small_new() { fn small_new() {
assert_eq!( assert_eq!(
EventU16TypedSev::<SeverityInfo>::new_checked(0, 0) EventU16TypedSev::<SeverityInfo>::new(0, 0).expect("Creating regular event failed"),
.expect("Creating regular event failed"),
INFO_EVENT_SMALL INFO_EVENT_SMALL
); );
} }
@ -794,8 +767,6 @@ mod tests {
assert!(HIGH_SEV_EVENT.write_to_be_bytes(&mut buf).is_ok()); assert!(HIGH_SEV_EVENT.write_to_be_bytes(&mut buf).is_ok());
let val_from_raw = u32::from_be_bytes(buf); let val_from_raw = u32::from_be_bytes(buf);
assert_eq!(val_from_raw, 0xFFFFFFFF); assert_eq!(val_from_raw, 0xFFFFFFFF);
let event_read_back = EventU32::from_be_bytes(buf);
assert_eq!(event_read_back, HIGH_SEV_EVENT);
} }
#[test] #[test]
@ -804,8 +775,6 @@ mod tests {
assert!(HIGH_SEV_EVENT_SMALL.write_to_be_bytes(&mut buf).is_ok()); assert!(HIGH_SEV_EVENT_SMALL.write_to_be_bytes(&mut buf).is_ok());
let val_from_raw = u16::from_be_bytes(buf); let val_from_raw = u16::from_be_bytes(buf);
assert_eq!(val_from_raw, 0xFFFF); assert_eq!(val_from_raw, 0xFFFF);
let event_read_back = EventU16::from_be_bytes(buf);
assert_eq!(event_read_back, HIGH_SEV_EVENT_SMALL);
} }
#[test] #[test]
@ -836,13 +805,13 @@ mod tests {
fn severity_from_invalid_raw_val() { fn severity_from_invalid_raw_val() {
let invalid = 0xFF; let invalid = 0xFF;
assert!(Severity::try_from(invalid).is_err()); assert!(Severity::try_from(invalid).is_err());
let invalid = Severity::High as u8 + 1; let invalid = Severity::HIGH as u8 + 1;
assert!(Severity::try_from(invalid).is_err()); assert!(Severity::try_from(invalid).is_err());
} }
#[test] #[test]
fn reduction() { fn reduction() {
let event = EventU32TypedSev::<SeverityInfo>::new(1, 1); let event = EventU32TypedSev::<SeverityInfo>::const_new(1, 1);
let raw = event.raw(); let raw = event.raw();
let reduced: EventU32 = event.into(); let reduced: EventU32 = event.into();
assert_eq!(reduced.group_id(), 1); assert_eq!(reduced.group_id(), 1);

View File

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

View File

@ -1,3 +1,4 @@
//! # Hardware Abstraction Layer module //! # Hardware Abstraction Layer module
#[cfg(feature = "std")] #[cfg(feature = "std")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))]
pub mod std; pub mod std;

View File

@ -1,25 +1,20 @@
use alloc::sync::Arc; use alloc::boxed::Box;
use alloc::vec; use alloc::vec;
use cobs::encode; use cobs::encode;
use core::sync::atomic::AtomicBool;
use core::time::Duration;
use delegate::delegate; use delegate::delegate;
use mio::net::{TcpListener, TcpStream};
use std::io::Write; use std::io::Write;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::net::TcpListener;
use std::net::TcpStream;
use std::vec::Vec; use std::vec::Vec;
use crate::encoding::parse_buffer_for_cobs_encoded_packets; use crate::encoding::parse_buffer_for_cobs_encoded_packets;
use crate::tmtc::PacketSenderRaw; use crate::tmtc::ReceivesTc;
use crate::tmtc::PacketSource; use crate::tmtc::TmPacketSource;
use crate::hal::std::tcp_server::{ use crate::hal::std::tcp_server::{
ConnectionResult, ServerConfig, TcpTcParser, TcpTmSender, TcpTmtcError, TcpTmtcGenericServer, ConnectionResult, ServerConfig, TcpTcParser, TcpTmSender, TcpTmtcError, TcpTmtcGenericServer,
}; };
use crate::ComponentId;
use super::tcp_server::HandledConnectionHandler;
use super::tcp_server::HandledConnectionInfo;
/// Concrete [TcpTcParser] implementation for the [TcpTmtcInCobsServer]. /// Concrete [TcpTcParser] implementation for the [TcpTmtcInCobsServer].
#[derive(Default)] #[derive(Default)]
@ -29,16 +24,15 @@ impl<TmError, TcError: 'static> TcpTcParser<TmError, TcError> for CobsTcParser {
fn handle_tc_parsing( fn handle_tc_parsing(
&mut self, &mut self,
tc_buffer: &mut [u8], tc_buffer: &mut [u8],
sender_id: ComponentId, tc_receiver: &mut (impl ReceivesTc<Error = TcError> + ?Sized),
tc_sender: &(impl PacketSenderRaw<Error = TcError> + ?Sized), conn_result: &mut ConnectionResult,
conn_result: &mut HandledConnectionInfo,
current_write_idx: usize, current_write_idx: usize,
next_write_idx: &mut usize, next_write_idx: &mut usize,
) -> Result<(), TcpTmtcError<TmError, TcError>> { ) -> Result<(), TcpTmtcError<TmError, TcError>> {
// Reader vec full, need to parse for packets.
conn_result.num_received_tcs += parse_buffer_for_cobs_encoded_packets( conn_result.num_received_tcs += parse_buffer_for_cobs_encoded_packets(
&mut tc_buffer[..current_write_idx], &mut tc_buffer[..current_write_idx],
sender_id, tc_receiver.upcast_mut(),
tc_sender,
next_write_idx, next_write_idx,
) )
.map_err(|e| TcpTmtcError::TcError(e))?; .map_err(|e| TcpTmtcError::TcError(e))?;
@ -65,8 +59,8 @@ impl<TmError, TcError> TcpTmSender<TmError, TcError> for CobsTmSender {
fn handle_tm_sending( fn handle_tm_sending(
&mut self, &mut self,
tm_buffer: &mut [u8], tm_buffer: &mut [u8],
tm_source: &mut (impl PacketSource<Error = TmError> + ?Sized), tm_source: &mut (impl TmPacketSource<Error = TmError> + ?Sized),
conn_result: &mut HandledConnectionInfo, conn_result: &mut ConnectionResult,
stream: &mut TcpStream, stream: &mut TcpStream,
) -> Result<bool, TcpTmtcError<TmError, TcError>> { ) -> Result<bool, TcpTmtcError<TmError, TcError>> {
let mut tm_was_sent = false; let mut tm_was_sent = false;
@ -104,7 +98,7 @@ impl<TmError, TcError> TcpTmSender<TmError, TcError> for CobsTmSender {
/// Telemetry will be encoded with the COBS protocol using [cobs::encode] in addition to being /// Telemetry will be encoded with the COBS protocol using [cobs::encode] in addition to being
/// wrapped with the sentinel value 0 as the packet delimiter as well before being sent back to /// wrapped with the sentinel value 0 as the packet delimiter as well before being sent back to
/// the client. Please note that the server will send as much data as it can retrieve from the /// the client. Please note that the server will send as much data as it can retrieve from the
/// [PacketSource] in its current implementation. /// [TmPacketSource] in its current implementation.
/// ///
/// Using a framing protocol like COBS imposes minimal restrictions on the type of TMTC data /// Using a framing protocol like COBS imposes minimal restrictions on the type of TMTC data
/// exchanged while also allowing packets with flexible size and a reliable way to reconstruct full /// exchanged while also allowing packets with flexible size and a reliable way to reconstruct full
@ -115,34 +109,13 @@ impl<TmError, TcError> TcpTmSender<TmError, TcError> for CobsTmSender {
/// ///
/// ## Example /// ## Example
/// ///
/// The [TCP integration tests](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs/tests/tcp_servers.rs) /// 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. /// test also serves as the example application for this module.
pub struct TcpTmtcInCobsServer< pub struct TcpTmtcInCobsServer<TmError, TcError: 'static> {
TmSource: PacketSource<Error = TmError>, generic_server: TcpTmtcGenericServer<TmError, TcError, CobsTmSender, CobsTcParser>,
TcSender: PacketSenderRaw<Error = SendError>,
HandledConnection: HandledConnectionHandler,
TmError,
SendError: 'static,
> {
pub generic_server: TcpTmtcGenericServer<
TmSource,
TcSender,
CobsTmSender,
CobsTcParser,
HandledConnection,
TmError,
SendError,
>,
} }
impl< impl<TmError: 'static, TcError: 'static> TcpTmtcInCobsServer<TmError, TcError> {
TmSource: PacketSource<Error = TmError>,
TcReceiver: PacketSenderRaw<Error = TcError>,
HandledConnection: HandledConnectionHandler,
TmError: 'static,
TcError: 'static,
> TcpTmtcInCobsServer<TmSource, TcReceiver, HandledConnection, TmError, TcError>
{
/// Create a new TCP TMTC server which exchanges TMTC packets encoded with /// Create a new TCP TMTC server which exchanges TMTC packets encoded with
/// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing). /// [COBS protocol](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing).
/// ///
@ -155,11 +128,9 @@ impl<
/// forwarded to this TC receiver. /// forwarded to this TC receiver.
pub fn new( pub fn new(
cfg: ServerConfig, cfg: ServerConfig,
tm_source: TmSource, tm_source: Box<dyn TmPacketSource<Error = TmError>>,
tc_receiver: TcReceiver, tc_receiver: Box<dyn ReceivesTc<Error = TcError>>,
handled_connection: HandledConnection, ) -> Result<Self, TcpTmtcError<TmError, TcError>> {
stop_signal: Option<Arc<AtomicBool>>,
) -> Result<Self, std::io::Error> {
Ok(Self { Ok(Self {
generic_server: TcpTmtcGenericServer::new( generic_server: TcpTmtcGenericServer::new(
cfg, cfg,
@ -167,8 +138,6 @@ impl<
CobsTmSender::new(cfg.tm_buffer_size), CobsTmSender::new(cfg.tm_buffer_size),
tm_source, tm_source,
tc_receiver, tc_receiver,
handled_connection,
stop_signal,
)?, )?,
}) })
} }
@ -181,10 +150,9 @@ impl<
/// useful if using the port number 0 for OS auto-assignment. /// useful if using the port number 0 for OS auto-assignment.
pub fn local_addr(&self) -> std::io::Result<SocketAddr>; pub fn local_addr(&self) -> std::io::Result<SocketAddr>;
/// Delegation to the [TcpTmtcGenericServer::handle_all_connections] call. /// Delegation to the [TcpTmtcGenericServer::handle_next_connection] call.
pub fn handle_all_connections( pub fn handle_next_connection(
&mut self, &mut self,
poll_duration: Option<Duration>,
) -> Result<ConnectionResult, TcpTmtcError<TmError, TcError>>; ) -> Result<ConnectionResult, TcpTmtcError<TmError, TcError>>;
} }
} }
@ -199,29 +167,21 @@ mod tests {
use std::{ use std::{
io::{Read, Write}, io::{Read, Write},
net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream}, net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream},
panic,
sync::mpsc,
thread, thread,
time::Instant,
}; };
use crate::{ use crate::{
encoding::tests::{INVERTED_PACKET, SIMPLE_PACKET}, encoding::tests::{INVERTED_PACKET, SIMPLE_PACKET},
hal::std::tcp_server::{ hal::std::tcp_server::{
tests::{ConnectionFinishedHandler, SyncTmSource}, tests::{SyncTcCacher, SyncTmSource},
ConnectionResult, ServerConfig, ServerConfig,
}, },
queue::GenericSendError,
tmtc::PacketAsVec,
ComponentId,
}; };
use alloc::sync::Arc; use alloc::{boxed::Box, sync::Arc};
use cobs::encode; use cobs::encode;
use super::TcpTmtcInCobsServer; use super::TcpTmtcInCobsServer;
const TCP_SERVER_ID: ComponentId = 0x05;
fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) { fn encode_simple_packet(encoded_buf: &mut [u8], current_idx: &mut usize) {
encode_packet(&SIMPLE_PACKET, encoded_buf, current_idx) encode_packet(&SIMPLE_PACKET, encoded_buf, current_idx)
} }
@ -240,22 +200,13 @@ mod tests {
fn generic_tmtc_server( fn generic_tmtc_server(
addr: &SocketAddr, addr: &SocketAddr,
tc_sender: mpsc::Sender<PacketAsVec>, tc_receiver: SyncTcCacher,
tm_source: SyncTmSource, tm_source: SyncTmSource,
stop_signal: Option<Arc<AtomicBool>>, ) -> TcpTmtcInCobsServer<(), ()> {
) -> TcpTmtcInCobsServer<
SyncTmSource,
mpsc::Sender<PacketAsVec>,
ConnectionFinishedHandler,
(),
GenericSendError,
> {
TcpTmtcInCobsServer::new( TcpTmtcInCobsServer::new(
ServerConfig::new(TCP_SERVER_ID, *addr, Duration::from_millis(2), 1024, 1024), ServerConfig::new(*addr, Duration::from_millis(2), 1024, 1024),
tm_source, Box::new(tm_source),
tc_sender, Box::new(tc_receiver),
ConnectionFinishedHandler::default(),
stop_signal,
) )
.expect("TCP server generation failed") .expect("TCP server generation failed")
} }
@ -263,10 +214,9 @@ mod tests {
#[test] #[test]
fn test_server_basic_no_tm() { fn test_server_basic_no_tm() {
let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0);
let (tc_sender, tc_receiver) = mpsc::channel(); let tc_receiver = SyncTcCacher::default();
let tm_source = SyncTmSource::default(); let tm_source = SyncTmSource::default();
let mut tcp_server = let mut tcp_server = generic_tmtc_server(&auto_port_addr, tc_receiver.clone(), tm_source);
generic_tmtc_server(&auto_port_addr, tc_sender.clone(), tm_source, None);
let dest_addr = tcp_server let dest_addr = tcp_server
.local_addr() .local_addr()
.expect("retrieving dest addr failed"); .expect("retrieving dest addr failed");
@ -274,20 +224,13 @@ mod tests {
let set_if_done = conn_handled.clone(); let set_if_done = conn_handled.clone();
// Call the connection handler in separate thread, does block. // Call the connection handler in separate thread, does block.
thread::spawn(move || { thread::spawn(move || {
let result = tcp_server.handle_all_connections(Some(Duration::from_millis(100))); let result = tcp_server.handle_next_connection();
if result.is_err() { if result.is_err() {
panic!("handling connection failed: {:?}", result.unwrap_err()); panic!("handling connection failed: {:?}", result.unwrap_err());
} }
let result = result.unwrap(); let conn_result = result.unwrap();
assert_eq!(result, ConnectionResult::HandledConnections(1)); assert_eq!(conn_result.num_received_tcs, 1);
tcp_server assert_eq!(conn_result.num_sent_tms, 0);
.generic_server
.finished_handler
.check_last_connection(0, 1);
tcp_server
.generic_server
.finished_handler
.check_no_connections_left();
set_if_done.store(true, Ordering::Relaxed); set_if_done.store(true, Ordering::Relaxed);
}); });
// Send TC to server now. // Send TC to server now.
@ -309,20 +252,24 @@ mod tests {
panic!("connection was not handled properly"); panic!("connection was not handled properly");
} }
// Check that the packet was received and decoded successfully. // Check that the packet was received and decoded successfully.
let packet_with_sender = tc_receiver.recv().expect("receiving TC failed"); let mut tc_queue = tc_receiver
assert_eq!(packet_with_sender.packet, &SIMPLE_PACKET); .tc_queue
matches!(tc_receiver.try_recv(), Err(mpsc::TryRecvError::Empty)); .lock()
.expect("locking tc queue failed");
assert_eq!(tc_queue.len(), 1);
assert_eq!(tc_queue.pop_front().unwrap(), &SIMPLE_PACKET);
drop(tc_queue);
} }
#[test] #[test]
fn test_server_basic_multi_tm_multi_tc() { fn test_server_basic_multi_tm_multi_tc() {
let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0);
let (tc_sender, tc_receiver) = mpsc::channel(); let tc_receiver = SyncTcCacher::default();
let mut tm_source = SyncTmSource::default(); let mut tm_source = SyncTmSource::default();
tm_source.add_tm(&INVERTED_PACKET); tm_source.add_tm(&INVERTED_PACKET);
tm_source.add_tm(&SIMPLE_PACKET); tm_source.add_tm(&SIMPLE_PACKET);
let mut tcp_server = let mut tcp_server =
generic_tmtc_server(&auto_port_addr, tc_sender.clone(), tm_source.clone(), None); generic_tmtc_server(&auto_port_addr, tc_receiver.clone(), tm_source.clone());
let dest_addr = tcp_server let dest_addr = tcp_server
.local_addr() .local_addr()
.expect("retrieving dest addr failed"); .expect("retrieving dest addr failed");
@ -330,20 +277,13 @@ mod tests {
let set_if_done = conn_handled.clone(); let set_if_done = conn_handled.clone();
// Call the connection handler in separate thread, does block. // Call the connection handler in separate thread, does block.
thread::spawn(move || { thread::spawn(move || {
let result = tcp_server.handle_all_connections(Some(Duration::from_millis(100))); let result = tcp_server.handle_next_connection();
if result.is_err() { if result.is_err() {
panic!("handling connection failed: {:?}", result.unwrap_err()); panic!("handling connection failed: {:?}", result.unwrap_err());
} }
let result = result.unwrap(); let conn_result = result.unwrap();
assert_eq!(result, ConnectionResult::HandledConnections(1)); assert_eq!(conn_result.num_received_tcs, 2, "Not enough TCs received");
tcp_server assert_eq!(conn_result.num_sent_tms, 2, "Not enough TMs received");
.generic_server
.finished_handler
.check_last_connection(2, 2);
tcp_server
.generic_server
.finished_handler
.check_no_connections_left();
set_if_done.store(true, Ordering::Relaxed); set_if_done.store(true, Ordering::Relaxed);
}); });
// Send TC to server now. // Send TC to server now.
@ -417,78 +357,13 @@ mod tests {
panic!("connection was not handled properly"); panic!("connection was not handled properly");
} }
// Check that the packet was received and decoded successfully. // Check that the packet was received and decoded successfully.
let packet_with_sender = tc_receiver.recv().expect("receiving TC failed"); let mut tc_queue = tc_receiver
let packet = &packet_with_sender.packet; .tc_queue
assert_eq!(packet, &SIMPLE_PACKET); .lock()
let packet_with_sender = tc_receiver.recv().expect("receiving TC failed"); .expect("locking tc queue failed");
let packet = &packet_with_sender.packet; assert_eq!(tc_queue.len(), 2);
assert_eq!(packet, &INVERTED_PACKET); assert_eq!(tc_queue.pop_front().unwrap(), &SIMPLE_PACKET);
matches!(tc_receiver.try_recv(), Err(mpsc::TryRecvError::Empty)); assert_eq!(tc_queue.pop_front().unwrap(), &INVERTED_PACKET);
} drop(tc_queue);
#[test]
fn test_server_accept_timeout() {
let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0);
let (tc_sender, _tc_receiver) = mpsc::channel();
let tm_source = SyncTmSource::default();
let mut tcp_server =
generic_tmtc_server(&auto_port_addr, tc_sender.clone(), tm_source, None);
let start = Instant::now();
// Call the connection handler in separate thread, does block.
let thread_jh = thread::spawn(move || loop {
let result = tcp_server.handle_all_connections(Some(Duration::from_millis(20)));
if result.is_err() {
panic!("handling connection failed: {:?}", result.unwrap_err());
}
let result = result.unwrap();
if result == ConnectionResult::AcceptTimeout {
break;
}
if Instant::now() - start > Duration::from_millis(100) {
panic!("regular stop signal handling failed");
}
});
thread_jh.join().expect("thread join failed");
}
#[test]
fn test_server_stop_signal() {
let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0);
let (tc_sender, _tc_receiver) = mpsc::channel();
let tm_source = SyncTmSource::default();
let stop_signal = Arc::new(AtomicBool::new(false));
let mut tcp_server = generic_tmtc_server(
&auto_port_addr,
tc_sender.clone(),
tm_source,
Some(stop_signal.clone()),
);
let dest_addr = tcp_server
.local_addr()
.expect("retrieving dest addr failed");
let stop_signal_copy = stop_signal.clone();
let start = Instant::now();
// Call the connection handler in separate thread, does block.
let thread_jh = thread::spawn(move || loop {
let result = tcp_server.handle_all_connections(Some(Duration::from_millis(20)));
if result.is_err() {
panic!("handling connection failed: {:?}", result.unwrap_err());
}
let result = result.unwrap();
if result == ConnectionResult::AcceptTimeout {
panic!("unexpected accept timeout");
}
if stop_signal_copy.load(Ordering::Relaxed) {
break;
}
if Instant::now() - start > Duration::from_millis(100) {
panic!("regular stop signal handling failed");
}
});
// We connect but do not do anything.
let _stream = TcpStream::connect(dest_addr).expect("connecting to TCP server failed");
stop_signal.store(true, Ordering::Relaxed);
// No need to drop the connection, the stop signal should take take of everything.
thread_jh.join().expect("thread join failed");
} }
} }

View File

@ -1,23 +1,21 @@
//! Generic TCP TMTC servers with different TMTC format flavours. //! Generic TCP TMTC servers with different TMTC format flavours.
use alloc::sync::Arc;
use alloc::vec; use alloc::vec;
use alloc::vec::Vec; use alloc::{boxed::Box, vec::Vec};
use core::sync::atomic::AtomicBool;
use core::time::Duration; use core::time::Duration;
use mio::net::{TcpListener, TcpStream};
use mio::{Events, Interest, Poll, Token};
use socket2::{Domain, Socket, Type}; use socket2::{Domain, Socket, Type};
use std::io::{self, Read}; use std::io::Read;
use std::net::SocketAddr; use std::net::TcpListener;
use std::net::{SocketAddr, TcpStream};
use std::thread; use std::thread;
use crate::tmtc::{PacketSenderRaw, PacketSource}; use crate::tmtc::{ReceivesTc, TmPacketSource};
use crate::ComponentId;
use thiserror::Error; use thiserror::Error;
// Re-export the TMTC in COBS server. // Re-export the TMTC in COBS server.
pub use crate::hal::std::tcp_cobs_server::{CobsTcParser, CobsTmSender, TcpTmtcInCobsServer}; pub use crate::hal::std::tcp_cobs_server::{CobsTcParser, CobsTmSender, TcpTmtcInCobsServer};
pub use crate::hal::std::tcp_spacepackets_server::{SpacepacketsTmSender, TcpSpacepacketsServer}; pub use crate::hal::std::tcp_spacepackets_server::{
SpacepacketsTcParser, SpacepacketsTmSender, TcpSpacepacketsServer,
};
/// Configuration struct for the generic TCP TMTC server /// Configuration struct for the generic TCP TMTC server
/// ///
@ -27,7 +25,7 @@ pub use crate::hal::std::tcp_spacepackets_server::{SpacepacketsTmSender, TcpSpac
/// * `inner_loop_delay` - If a client connects for a longer period, but no TC is received or /// * `inner_loop_delay` - If a client connects for a longer period, but no TC is received or
/// no TM needs to be sent, the TCP server will delay for the specified amount of time /// no TM needs to be sent, the TCP server will delay for the specified amount of time
/// to reduce CPU load. /// to reduce CPU load.
/// * `tm_buffer_size` - Size of the TM buffer used to read TM from the [PacketSource] and /// * `tm_buffer_size` - Size of the TM buffer used to read TM from the [TmPacketSource] and
/// encoding of that data. This buffer should at large enough to hold the maximum expected /// encoding of that data. This buffer should at large enough to hold the maximum expected
/// TM size read from the packet source. /// TM size read from the packet source.
/// * `tc_buffer_size` - Size of the TC buffer used to read encoded telecommands sent from /// * `tc_buffer_size` - Size of the TC buffer used to read encoded telecommands sent from
@ -43,7 +41,6 @@ pub use crate::hal::std::tcp_spacepackets_server::{SpacepacketsTmSender, TcpSpac
/// default. /// default.
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct ServerConfig { pub struct ServerConfig {
pub id: ComponentId,
pub addr: SocketAddr, pub addr: SocketAddr,
pub inner_loop_delay: Duration, pub inner_loop_delay: Duration,
pub tm_buffer_size: usize, pub tm_buffer_size: usize,
@ -54,20 +51,18 @@ pub struct ServerConfig {
impl ServerConfig { impl ServerConfig {
pub fn new( pub fn new(
id: ComponentId,
addr: SocketAddr, addr: SocketAddr,
inner_loop_delay: Duration, inner_loop_delay: Duration,
tm_buffer_size: usize, tm_buffer_size: usize,
tc_buffer_size: usize, tc_buffer_size: usize,
) -> Self { ) -> Self {
Self { Self {
id,
addr, addr,
inner_loop_delay, inner_loop_delay,
tm_buffer_size, tm_buffer_size,
tc_buffer_size, tc_buffer_size,
reuse_addr: true, reuse_addr: false,
reuse_port: true, reuse_port: false,
} }
} }
} }
@ -84,62 +79,37 @@ pub enum TcpTmtcError<TmError, TcError> {
/// Result of one connection attempt. Contains the client address if a connection was established, /// Result of one connection attempt. Contains the client address if a connection was established,
/// in addition to the number of telecommands and telemetry packets exchanged. /// in addition to the number of telecommands and telemetry packets exchanged.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, Default)]
pub enum ConnectionResult { pub struct ConnectionResult {
AcceptTimeout, pub addr: Option<SocketAddr>,
HandledConnections(u32),
}
#[derive(Debug)]
pub struct HandledConnectionInfo {
pub addr: SocketAddr,
pub num_received_tcs: u32, pub num_received_tcs: u32,
pub num_sent_tms: u32, pub num_sent_tms: u32,
/// The generic TCP server can be stopped using an external signal. If this happened, this
/// boolean will be set to true.
pub stopped_by_signal: bool,
}
impl HandledConnectionInfo {
pub fn new(addr: SocketAddr) -> Self {
Self {
addr,
num_received_tcs: 0,
num_sent_tms: 0,
stopped_by_signal: false,
}
}
}
pub trait HandledConnectionHandler {
fn handled_connection(&mut self, info: HandledConnectionInfo);
} }
/// Generic parser abstraction for an object which can parse for telecommands given a raw /// Generic parser abstraction for an object which can parse for telecommands given a raw
/// bytestream received from a TCP socket and send them using a generic [PacketSenderRaw] /// bytestream received from a TCP socket and send them to a generic [ReceivesTc] telecommand
/// implementation. This allows different encoding schemes for telecommands. /// receiver. This allows different encoding schemes for telecommands.
pub trait TcpTcParser<TmError, SendError> { pub trait TcpTcParser<TmError, TcError> {
fn handle_tc_parsing( fn handle_tc_parsing(
&mut self, &mut self,
tc_buffer: &mut [u8], tc_buffer: &mut [u8],
sender_id: ComponentId, tc_receiver: &mut (impl ReceivesTc<Error = TcError> + ?Sized),
tc_sender: &(impl PacketSenderRaw<Error = SendError> + ?Sized), conn_result: &mut ConnectionResult,
conn_result: &mut HandledConnectionInfo,
current_write_idx: usize, current_write_idx: usize,
next_write_idx: &mut usize, next_write_idx: &mut usize,
) -> Result<(), TcpTmtcError<TmError, SendError>>; ) -> Result<(), TcpTmtcError<TmError, TcError>>;
} }
/// Generic sender abstraction for an object which can pull telemetry from a given TM source /// Generic sender abstraction for an object which can pull telemetry from a given TM source
/// using a [PacketSource] and then send them back to a client using a given [TcpStream]. /// using a [TmPacketSource] and then send them back to a client using a given [TcpStream].
/// The concrete implementation can also perform any encoding steps which are necessary before /// The concrete implementation can also perform any encoding steps which are necessary before
/// sending back the data to a client. /// sending back the data to a client.
pub trait TcpTmSender<TmError, TcError> { pub trait TcpTmSender<TmError, TcError> {
fn handle_tm_sending( fn handle_tm_sending(
&mut self, &mut self,
tm_buffer: &mut [u8], tm_buffer: &mut [u8],
tm_source: &mut (impl PacketSource<Error = TmError> + ?Sized), tm_source: &mut (impl TmPacketSource<Error = TmError> + ?Sized),
conn_result: &mut HandledConnectionInfo, conn_result: &mut ConnectionResult,
stream: &mut TcpStream, stream: &mut TcpStream,
) -> Result<bool, TcpTmtcError<TmError, TcError>>; ) -> Result<bool, TcpTmtcError<TmError, TcError>>;
} }
@ -151,9 +121,9 @@ pub trait TcpTmSender<TmError, TcError> {
/// through the following 4 core abstractions: /// through the following 4 core abstractions:
/// ///
/// 1. [TcpTcParser] to parse for telecommands from the raw bytestream received from a client. /// 1. [TcpTcParser] to parse for telecommands from the raw bytestream received from a client.
/// 2. Parsed telecommands will be sent using the [PacketSenderRaw] object. /// 2. Parsed telecommands will be sent to the [ReceivesTc] telecommand receiver.
/// 3. [TcpTmSender] to send telemetry pulled from a TM source back to the client. /// 3. [TcpTmSender] to send telemetry pulled from a TM source back to the client.
/// 4. [PacketSource] as a generic TM source used by the [TcpTmSender]. /// 4. [TmPacketSource] as a generic TM source used by the [TcpTmSender].
/// ///
/// It is possible to specify custom abstractions to build a dedicated TCP TMTC server without /// It is possible to specify custom abstractions to build a dedicated TCP TMTC server without
/// having to re-implement common logic. /// having to re-implement common logic.
@ -161,49 +131,23 @@ pub trait TcpTmSender<TmError, TcError> {
/// Currently, this framework offers the following concrete implementations: /// Currently, this framework offers the following concrete implementations:
/// ///
/// 1. [TcpTmtcInCobsServer] to exchange TMTC wrapped inside the COBS framing protocol. /// 1. [TcpTmtcInCobsServer] to exchange TMTC wrapped inside the COBS framing protocol.
/// 2. [TcpSpacepacketsServer] to exchange space packets via TCP.
pub struct TcpTmtcGenericServer< pub struct TcpTmtcGenericServer<
TmSource: PacketSource<Error = TmError>,
TcSender: PacketSenderRaw<Error = TcSendError>,
TmSender: TcpTmSender<TmError, TcSendError>,
TcParser: TcpTcParser<TmError, TcSendError>,
HandledConnection: HandledConnectionHandler,
TmError, TmError,
TcSendError, TcError,
TmHandler: TcpTmSender<TmError, TcError>,
TcHandler: TcpTcParser<TmError, TcError>,
> { > {
pub id: ComponentId, base: TcpTmtcServerBase<TmError, TcError>,
pub finished_handler: HandledConnection, tc_handler: TcHandler,
pub(crate) listener: TcpListener, tm_handler: TmHandler,
pub(crate) inner_loop_delay: Duration,
pub(crate) tm_source: TmSource,
pub(crate) tm_buffer: Vec<u8>,
pub(crate) tc_sender: TcSender,
pub(crate) tc_buffer: Vec<u8>,
poll: Poll,
events: Events,
pub tc_handler: TcParser,
pub tm_handler: TmSender,
stop_signal: Option<Arc<AtomicBool>>,
} }
impl< impl<
TmSource: PacketSource<Error = TmError>,
TcSender: PacketSenderRaw<Error = TcSendError>,
TmSender: TcpTmSender<TmError, TcSendError>,
TcParser: TcpTcParser<TmError, TcSendError>,
HandledConnection: HandledConnectionHandler,
TmError: 'static, TmError: 'static,
TcSendError: 'static, TcError: 'static,
> TmSender: TcpTmSender<TmError, TcError>,
TcpTmtcGenericServer< TcParser: TcpTcParser<TmError, TcError>,
TmSource, > TcpTmtcGenericServer<TmError, TcError, TmSender, TcParser>
TcSender,
TmSender,
TcParser,
HandledConnection,
TmError,
TcSendError,
>
{ {
/// Create a new generic TMTC server instance. /// Create a new generic TMTC server instance.
/// ///
@ -215,77 +159,38 @@ impl<
/// * `tm_sender` - Sends back telemetry to the client using the specified TM source. /// * `tm_sender` - Sends back telemetry to the client using the specified TM source.
/// * `tm_source` - Generic TM source used by the server to pull telemetry packets which are /// * `tm_source` - Generic TM source used by the server to pull telemetry packets which are
/// then sent back to the client. /// then sent back to the client.
/// * `tc_sender` - Any received telecommand which was decoded successfully will be forwarded /// * `tc_receiver` - Any received telecommand which was decoded successfully will be forwarded
/// using this TC sender. /// to this TC receiver.
/// * `stop_signal` - Can be used to stop the server even if a connection is ongoing.
pub fn new( pub fn new(
cfg: ServerConfig, cfg: ServerConfig,
tc_parser: TcParser, tc_parser: TcParser,
tm_sender: TmSender, tm_sender: TmSender,
tm_source: TmSource, tm_source: Box<dyn TmPacketSource<Error = TmError>>,
tc_receiver: TcSender, tc_receiver: Box<dyn ReceivesTc<Error = TcError>>,
finished_handler: HandledConnection, ) -> Result<TcpTmtcGenericServer<TmError, TcError, TmSender, TcParser>, std::io::Error> {
stop_signal: Option<Arc<AtomicBool>>,
) -> 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)?;
// MIO does not do this for us. We want the accept calls to be non-blocking.
socket.set_nonblocking(true)?;
let addr = (cfg.addr).into();
socket.bind(&addr)?;
socket.listen(128)?;
// Create a poll instance.
let poll = Poll::new()?;
// Create storage for events.
let events = Events::with_capacity(32);
let listener: std::net::TcpListener = socket.into();
let mut mio_listener = TcpListener::from_std(listener);
// Start listening for incoming connections.
poll.registry().register(
&mut mio_listener,
Token(0),
Interest::READABLE | Interest::WRITABLE,
)?;
Ok(Self { Ok(Self {
id: cfg.id, base: TcpTmtcServerBase::new(cfg, tm_source, tc_receiver)?,
tc_handler: tc_parser, tc_handler: tc_parser,
tm_handler: tm_sender, tm_handler: tm_sender,
poll,
events,
listener: mio_listener,
inner_loop_delay: cfg.inner_loop_delay,
tm_source,
tm_buffer: vec![0; cfg.tm_buffer_size],
tc_sender: tc_receiver,
tc_buffer: vec![0; cfg.tc_buffer_size],
stop_signal,
finished_handler,
}) })
} }
/// Retrieve the internal [TcpListener] class. /// Retrieve the internal [TcpListener] class.
pub fn listener(&mut self) -> &mut TcpListener { pub fn listener(&mut self) -> &mut TcpListener {
&mut self.listener self.base.listener()
} }
/// Can be used to retrieve the local assigned address of the TCP server. This is especially /// 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. /// useful if using the port number 0 for OS auto-assignment.
pub fn local_addr(&self) -> std::io::Result<SocketAddr> { pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
self.listener.local_addr() self.base.local_addr()
} }
/// This call is used to handle all connection from clients. Right now, it performs /// This call is used to handle the next connection to a client. Right now, it performs
/// the following steps: /// the following steps:
/// ///
/// 1. It calls the [std::net::TcpListener::accept] method until a client connects. An optional /// 1. It calls the [std::net::TcpListener::accept] method internally using the blocking API
/// timeout can be specified for non-blocking acceptance. /// until a client connects.
/// 2. It reads all the telecommands from the client and parses all received data using the /// 2. It reads all the telecommands from the client and parses all received data using the
/// user specified [TcpTcParser]. /// user specified [TcpTcParser].
/// 3. After reading and parsing all telecommands, it sends back all telemetry using the /// 3. After reading and parsing all telecommands, it sends back all telemetry using the
@ -294,78 +199,26 @@ impl<
/// The server will delay for a user-specified period if the client connects to the server /// The server will delay for a user-specified period if the client connects to the server
/// for prolonged periods and there is no traffic for the server. This is the case if the /// for prolonged periods and there is no traffic for the server. This is the case if the
/// client does not send any telecommands and no telemetry needs to be sent back to the client. /// client does not send any telecommands and no telemetry needs to be sent back to the client.
pub fn handle_all_connections( pub fn handle_next_connection(
&mut self, &mut self,
poll_timeout: Option<Duration>, ) -> Result<ConnectionResult, TcpTmtcError<TmError, TcError>> {
) -> Result<ConnectionResult, TcpTmtcError<TmError, TcSendError>> { let mut connection_result = ConnectionResult::default();
let mut handled_connections = 0;
// Poll Mio for events.
self.poll.poll(&mut self.events, poll_timeout)?;
let mut acceptable_connection = false;
// Process each event.
for event in self.events.iter() {
if event.token() == Token(0) {
acceptable_connection = true;
} else {
// Should never happen..
panic!("unexpected TCP event token");
}
}
// I'd love to do this in the loop above, but there are issues with multiple borrows.
if acceptable_connection {
// There might be mutliple connections available. Accept until all of them have
// been handled.
loop {
match self.listener.accept() {
Ok((stream, addr)) => {
if let Err(e) = self.handle_accepted_connection(stream, addr) {
self.reregister_poll_interest()?;
return Err(e);
}
handled_connections += 1;
}
Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => break,
Err(err) => {
self.reregister_poll_interest()?;
return Err(TcpTmtcError::Io(err));
}
}
}
}
if handled_connections > 0 {
return Ok(ConnectionResult::HandledConnections(handled_connections));
}
Ok(ConnectionResult::AcceptTimeout)
}
fn reregister_poll_interest(&mut self) -> io::Result<()> {
self.poll.registry().reregister(
&mut self.listener,
Token(0),
Interest::READABLE | Interest::WRITABLE,
)
}
fn handle_accepted_connection(
&mut self,
mut stream: TcpStream,
addr: SocketAddr,
) -> Result<(), TcpTmtcError<TmError, TcSendError>> {
let mut current_write_idx; let mut current_write_idx;
let mut next_write_idx = 0; let mut next_write_idx = 0;
let mut connection_result = HandledConnectionInfo::new(addr); let (mut stream, addr) = self.base.listener.accept()?;
stream.set_nonblocking(true)?;
connection_result.addr = Some(addr);
current_write_idx = next_write_idx; current_write_idx = next_write_idx;
loop { loop {
let read_result = stream.read(&mut self.tc_buffer[current_write_idx..]); let read_result = stream.read(&mut self.base.tc_buffer[current_write_idx..]);
match read_result { match read_result {
Ok(0) => { Ok(0) => {
// Connection closed by client. If any TC was read, parse for complete packets. // Connection closed by client. If any TC was read, parse for complete packets.
// After that, break the outer loop. // After that, break the outer loop.
if current_write_idx > 0 { if current_write_idx > 0 {
self.tc_handler.handle_tc_parsing( self.tc_handler.handle_tc_parsing(
&mut self.tc_buffer, &mut self.base.tc_buffer,
self.id, self.base.tc_receiver.as_mut(),
&self.tc_sender,
&mut connection_result, &mut connection_result,
current_write_idx, current_write_idx,
&mut next_write_idx, &mut next_write_idx,
@ -376,11 +229,10 @@ impl<
Ok(read_len) => { Ok(read_len) => {
current_write_idx += read_len; current_write_idx += read_len;
// TC buffer is full, we must parse for complete packets now. // TC buffer is full, we must parse for complete packets now.
if current_write_idx == self.tc_buffer.capacity() { if current_write_idx == self.base.tc_buffer.capacity() {
self.tc_handler.handle_tc_parsing( self.tc_handler.handle_tc_parsing(
&mut self.tc_buffer, &mut self.base.tc_buffer,
self.id, self.base.tc_receiver.as_mut(),
&self.tc_sender,
&mut connection_result, &mut connection_result,
current_write_idx, current_write_idx,
&mut next_write_idx, &mut next_write_idx,
@ -393,9 +245,8 @@ impl<
// both UNIX and Windows. // both UNIX and Windows.
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => { std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => {
self.tc_handler.handle_tc_parsing( self.tc_handler.handle_tc_parsing(
&mut self.tc_buffer, &mut self.base.tc_buffer,
self.id, self.base.tc_receiver.as_mut(),
&self.tc_sender,
&mut connection_result, &mut connection_result,
current_write_idx, current_write_idx,
&mut next_write_idx, &mut next_write_idx,
@ -403,26 +254,14 @@ impl<
current_write_idx = next_write_idx; current_write_idx = next_write_idx;
if !self.tm_handler.handle_tm_sending( if !self.tm_handler.handle_tm_sending(
&mut self.tm_buffer, &mut self.base.tm_buffer,
&mut self.tm_source, self.base.tm_source.as_mut(),
&mut connection_result, &mut connection_result,
&mut stream, &mut stream,
)? { )? {
// No TC read, no TM was sent, but the client has not disconnected. // No TC read, no TM was sent, but the client has not disconnected.
// Perform an inner delay to avoid burning CPU time. // Perform an inner delay to avoid burning CPU time.
thread::sleep(self.inner_loop_delay); thread::sleep(self.base.inner_loop_delay);
// Optional stop signal handling.
if self.stop_signal.is_some()
&& self
.stop_signal
.as_ref()
.unwrap()
.load(std::sync::atomic::Ordering::Relaxed)
{
connection_result.stopped_by_signal = true;
self.finished_handler.handled_connection(connection_result);
return Ok(());
}
} }
} }
_ => { _ => {
@ -432,13 +271,53 @@ impl<
} }
} }
self.tm_handler.handle_tm_sending( self.tm_handler.handle_tm_sending(
&mut self.tm_buffer, &mut self.base.tm_buffer,
&mut self.tm_source, self.base.tm_source.as_mut(),
&mut connection_result, &mut connection_result,
&mut stream, &mut stream,
)?; )?;
self.finished_handler.handled_connection(connection_result); Ok(connection_result)
Ok(()) }
}
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()
} }
} }
@ -448,9 +327,21 @@ pub(crate) mod tests {
use alloc::{collections::VecDeque, sync::Arc, vec::Vec}; use alloc::{collections::VecDeque, sync::Arc, vec::Vec};
use crate::tmtc::PacketSource; use crate::tmtc::{ReceivesTcCore, TmPacketSourceCore};
use super::*; #[derive(Default, Clone)]
pub(crate) struct SyncTcCacher {
pub(crate) tc_queue: Arc<Mutex<VecDeque<Vec<u8>>>>,
}
impl ReceivesTcCore for SyncTcCacher {
type Error = ();
fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> {
let mut tc_queue = self.tc_queue.lock().expect("tc forwarder failed");
tc_queue.push_back(tc_raw.to_vec());
Ok(())
}
}
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub(crate) struct SyncTmSource { pub(crate) struct SyncTmSource {
@ -464,7 +355,7 @@ pub(crate) mod tests {
} }
} }
impl PacketSource for SyncTmSource { impl TmPacketSourceCore for SyncTmSource {
type Error = (); type Error = ();
fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result<usize, Self::Error> { fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result<usize, Self::Error> {
@ -484,30 +375,4 @@ pub(crate) mod tests {
Ok(0) Ok(0)
} }
} }
#[derive(Default)]
pub struct ConnectionFinishedHandler {
connection_info: VecDeque<HandledConnectionInfo>,
}
impl HandledConnectionHandler for ConnectionFinishedHandler {
fn handled_connection(&mut self, info: HandledConnectionInfo) {
self.connection_info.push_back(info);
}
}
impl ConnectionFinishedHandler {
pub fn check_last_connection(&mut self, num_tms: u32, num_tcs: u32) {
let last_conn_result = self
.connection_info
.pop_back()
.expect("no connection info available");
assert_eq!(last_conn_result.num_received_tcs, num_tcs);
assert_eq!(last_conn_result.num_sent_tms, num_tms);
}
pub fn check_no_connections_left(&self) {
assert!(self.connection_info.is_empty());
}
}
} }

View File

@ -1,44 +1,48 @@
use alloc::sync::Arc;
use core::{sync::atomic::AtomicBool, time::Duration};
use delegate::delegate; use delegate::delegate;
use mio::net::{TcpListener, TcpStream}; use std::{
use std::{io::Write, net::SocketAddr}; io::Write,
net::{SocketAddr, TcpListener, TcpStream},
};
use alloc::boxed::Box;
use crate::{ use crate::{
encoding::{ccsds::SpacePacketValidator, parse_buffer_for_ccsds_space_packets}, encoding::{ccsds::PacketIdLookup, parse_buffer_for_ccsds_space_packets},
tmtc::{PacketSenderRaw, PacketSource}, tmtc::{ReceivesTc, TmPacketSource},
ComponentId,
}; };
use super::tcp_server::{ use super::tcp_server::{
ConnectionResult, HandledConnectionHandler, HandledConnectionInfo, ServerConfig, TcpTcParser, ConnectionResult, ServerConfig, TcpTcParser, TcpTmSender, TcpTmtcError, TcpTmtcGenericServer,
TcpTmSender, TcpTmtcError, TcpTmtcGenericServer,
}; };
impl<T: SpacePacketValidator, TmError, TcError: 'static> TcpTcParser<TmError, TcError> for T { /// Concrete [TcpTcParser] implementation for the [TcpSpacepacketsServer].
pub struct SpacepacketsTcParser {
packet_id_lookup: Box<dyn PacketIdLookup + Send>,
}
impl SpacepacketsTcParser {
pub fn new(packet_id_lookup: Box<dyn PacketIdLookup + Send>) -> Self {
Self { packet_id_lookup }
}
}
impl<TmError, TcError: 'static> TcpTcParser<TmError, TcError> for SpacepacketsTcParser {
fn handle_tc_parsing( fn handle_tc_parsing(
&mut self, &mut self,
tc_buffer: &mut [u8], tc_buffer: &mut [u8],
sender_id: ComponentId, tc_receiver: &mut (impl ReceivesTc<Error = TcError> + ?Sized),
tc_sender: &(impl PacketSenderRaw<Error = TcError> + ?Sized), conn_result: &mut ConnectionResult,
conn_result: &mut HandledConnectionInfo,
current_write_idx: usize, current_write_idx: usize,
next_write_idx: &mut usize, next_write_idx: &mut usize,
) -> Result<(), TcpTmtcError<TmError, TcError>> { ) -> Result<(), TcpTmtcError<TmError, TcError>> {
// Reader vec full, need to parse for packets. // Reader vec full, need to parse for packets.
let parse_result = parse_buffer_for_ccsds_space_packets( conn_result.num_received_tcs += parse_buffer_for_ccsds_space_packets(
&tc_buffer[..current_write_idx], &mut tc_buffer[..current_write_idx],
self, self.packet_id_lookup.as_ref(),
sender_id, tc_receiver.upcast_mut(),
tc_sender, next_write_idx,
) )
.map_err(|e| TcpTmtcError::TcError(e))?; .map_err(|e| TcpTmtcError::TcError(e))?;
if let Some(broken_tail_start) = parse_result.incomplete_tail_start {
// Copy broken tail to front of buffer.
tc_buffer.copy_within(broken_tail_start..current_write_idx, 0);
*next_write_idx = current_write_idx - broken_tail_start;
}
conn_result.num_received_tcs += parse_result.packets_found;
Ok(()) Ok(())
} }
} }
@ -51,8 +55,8 @@ impl<TmError, TcError> TcpTmSender<TmError, TcError> for SpacepacketsTmSender {
fn handle_tm_sending( fn handle_tm_sending(
&mut self, &mut self,
tm_buffer: &mut [u8], tm_buffer: &mut [u8],
tm_source: &mut (impl PacketSource<Error = TmError> + ?Sized), tm_source: &mut (impl TmPacketSource<Error = TmError> + ?Sized),
conn_result: &mut HandledConnectionInfo, conn_result: &mut ConnectionResult,
stream: &mut TcpStream, stream: &mut TcpStream,
) -> Result<bool, TcpTmtcError<TmError, TcError>> { ) -> Result<bool, TcpTmtcError<TmError, TcError>> {
let mut tm_was_sent = false; let mut tm_was_sent = false;
@ -79,73 +83,44 @@ impl<TmError, TcError> TcpTmSender<TmError, TcError> for SpacepacketsTmSender {
/// ///
/// This serves only works if /// This serves only works if
/// [CCSDS 133.0-B-2 space packets](https://public.ccsds.org/Pubs/133x0b2e1.pdf) are the only /// [CCSDS 133.0-B-2 space packets](https://public.ccsds.org/Pubs/133x0b2e1.pdf) are the only
/// packet type being exchanged. It uses the CCSDS space packet header [spacepackets::SpHeader] and /// packet type being exchanged. It uses the CCSDS [spacepackets::PacketId] as the packet delimiter
/// a user specified [SpacePacketValidator] to determine the space packets relevant for further /// and start marker when parsing for packets. The user specifies a set of expected
/// processing. /// [spacepackets::PacketId]s as part of the server configuration for that purpose.
/// ///
/// ## Example /// ## Example
/// ///
/// The [TCP server integration tests](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs/tests/tcp_servers.rs) /// 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. /// also serves as the example application for this module.
pub struct TcpSpacepacketsServer< pub struct TcpSpacepacketsServer<TmError, TcError: 'static> {
TmSource: PacketSource<Error = TmError>, generic_server:
TcSender: PacketSenderRaw<Error = SendError>, TcpTmtcGenericServer<TmError, TcError, SpacepacketsTmSender, SpacepacketsTcParser>,
Validator: SpacePacketValidator,
HandledConnection: HandledConnectionHandler,
TmError,
SendError: 'static,
> {
pub generic_server: TcpTmtcGenericServer<
TmSource,
TcSender,
SpacepacketsTmSender,
Validator,
HandledConnection,
TmError,
SendError,
>,
} }
impl< impl<TmError: 'static, TcError: 'static> TcpSpacepacketsServer<TmError, TcError> {
TmSource: PacketSource<Error = TmError>, /// Create a new TCP TMTC server which exchanges CCSDS space packets.
TcSender: PacketSenderRaw<Error = TcError>,
Validator: SpacePacketValidator,
HandledConnection: HandledConnectionHandler,
TmError: 'static,
TcError: 'static,
> TcpSpacepacketsServer<TmSource, TcSender, Validator, HandledConnection, TmError, TcError>
{
/// ///
/// ## Parameter /// ## Parameter
/// ///
/// * `cfg` - Configuration of the server. /// * `cfg` - Configuration of the server.
/// * `tm_source` - Generic TM source used by the server to pull telemetry packets which are /// * `tm_source` - Generic TM source used by the server to pull telemetry packets which are
/// then sent back to the client. /// then sent back to the client.
/// * `tc_sender` - Any received telecommands which were decoded successfully will be /// * `tc_receiver` - Any received telecommands which were decoded successfully will be
/// forwarded using this [PacketSenderRaw]. /// forwarded to this TC receiver.
/// * `validator` - Used to determine the space packets relevant for further processing and /// * `packet_id_lookup` - This lookup table contains the relevant packets IDs for packet
/// to detect broken space packets. /// parsing. This mechanism is used to have a start marker for finding CCSDS packets.
/// * `handled_connection_hook` - Called to notify the user about a succesfully handled
/// connection.
/// * `stop_signal` - Can be used to shut down the TCP server even for longer running
/// connections.
pub fn new( pub fn new(
cfg: ServerConfig, cfg: ServerConfig,
tm_source: TmSource, tm_source: Box<dyn TmPacketSource<Error = TmError>>,
tc_sender: TcSender, tc_receiver: Box<dyn ReceivesTc<Error = TcError>>,
validator: Validator, packet_id_lookup: Box<dyn PacketIdLookup + Send>,
handled_connection_hook: HandledConnection, ) -> Result<Self, TcpTmtcError<TmError, TcError>> {
stop_signal: Option<Arc<AtomicBool>>,
) -> Result<Self, std::io::Error> {
Ok(Self { Ok(Self {
generic_server: TcpTmtcGenericServer::new( generic_server: TcpTmtcGenericServer::new(
cfg, cfg,
validator, SpacepacketsTcParser::new(packet_id_lookup),
SpacepacketsTmSender::default(), SpacepacketsTmSender::default(),
tm_source, tm_source,
tc_sender, tc_receiver,
handled_connection_hook,
stop_signal,
)?, )?,
}) })
} }
@ -158,10 +133,9 @@ impl<
/// useful if using the port number 0 for OS auto-assignment. /// useful if using the port number 0 for OS auto-assignment.
pub fn local_addr(&self) -> std::io::Result<SocketAddr>; pub fn local_addr(&self) -> std::io::Result<SocketAddr>;
/// Delegation to the [TcpTmtcGenericServer::handle_all_connections] call. /// Delegation to the [TcpTmtcGenericServer::handle_next_connection] call.
pub fn handle_all_connections( pub fn handle_next_connection(
&mut self, &mut self,
poll_timeout: Option<Duration>
) -> Result<ConnectionResult, TcpTmtcError<TmError, TcError>>; ) -> Result<ConnectionResult, TcpTmtcError<TmError, TcError>>;
} }
} }
@ -178,70 +152,39 @@ mod tests {
use std::{ use std::{
io::{Read, Write}, io::{Read, Write},
net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream}, net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream},
sync::mpsc,
thread, thread,
}; };
use alloc::sync::Arc; use alloc::{boxed::Box, sync::Arc};
use hashbrown::HashSet; use hashbrown::HashSet;
use spacepackets::{ use spacepackets::{
ecss::{tc::PusTcCreator, WritablePusPacket}, ecss::{tc::PusTcCreator, SerializablePusPacket},
CcsdsPacket, PacketId, SpHeader, PacketId, SpHeader,
}; };
use crate::{ use crate::hal::std::tcp_server::{
encoding::ccsds::{SpValidity, SpacePacketValidator}, tests::{SyncTcCacher, SyncTmSource},
hal::std::tcp_server::{ ServerConfig,
tests::{ConnectionFinishedHandler, SyncTmSource},
ConnectionResult, ServerConfig,
},
queue::GenericSendError,
tmtc::PacketAsVec,
ComponentId,
}; };
use super::TcpSpacepacketsServer; use super::TcpSpacepacketsServer;
const TCP_SERVER_ID: ComponentId = 0x05;
const TEST_APID_0: u16 = 0x02; const TEST_APID_0: u16 = 0x02;
const TEST_PACKET_ID_0: PacketId = PacketId::new_for_tc(true, TEST_APID_0); const TEST_PACKET_ID_0: PacketId = PacketId::const_tc(true, TEST_APID_0);
const TEST_APID_1: u16 = 0x10; const TEST_APID_1: u16 = 0x10;
const TEST_PACKET_ID_1: PacketId = PacketId::new_for_tc(true, TEST_APID_1); const TEST_PACKET_ID_1: PacketId = PacketId::const_tc(true, TEST_APID_1);
#[derive(Default)]
pub struct SimpleValidator(pub HashSet<PacketId>);
impl SpacePacketValidator for SimpleValidator {
fn validate(&self, sp_header: &SpHeader, _raw_buf: &[u8]) -> SpValidity {
if self.0.contains(&sp_header.packet_id()) {
return SpValidity::Valid;
}
// Simple case: Assume that the interface always contains valid space packets.
SpValidity::Skip
}
}
fn generic_tmtc_server( fn generic_tmtc_server(
addr: &SocketAddr, addr: &SocketAddr,
tc_sender: mpsc::Sender<PacketAsVec>, tc_receiver: SyncTcCacher,
tm_source: SyncTmSource, tm_source: SyncTmSource,
validator: SimpleValidator, packet_id_lookup: HashSet<PacketId>,
stop_signal: Option<Arc<AtomicBool>>, ) -> TcpSpacepacketsServer<(), ()> {
) -> TcpSpacepacketsServer<
SyncTmSource,
mpsc::Sender<PacketAsVec>,
SimpleValidator,
ConnectionFinishedHandler,
(),
GenericSendError,
> {
TcpSpacepacketsServer::new( TcpSpacepacketsServer::new(
ServerConfig::new(TCP_SERVER_ID, *addr, Duration::from_millis(2), 1024, 1024), ServerConfig::new(*addr, Duration::from_millis(2), 1024, 1024),
tm_source, Box::new(tm_source),
tc_sender, Box::new(tc_receiver),
validator, Box::new(packet_id_lookup),
ConnectionFinishedHandler::default(),
stop_signal,
) )
.expect("TCP server generation failed") .expect("TCP server generation failed")
} }
@ -249,16 +192,15 @@ mod tests {
#[test] #[test]
fn test_basic_tc_only() { fn test_basic_tc_only() {
let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0);
let (tc_sender, tc_receiver) = mpsc::channel(); let tc_receiver = SyncTcCacher::default();
let tm_source = SyncTmSource::default(); let tm_source = SyncTmSource::default();
let mut validator = SimpleValidator::default(); let mut packet_id_lookup = HashSet::new();
validator.0.insert(TEST_PACKET_ID_0); packet_id_lookup.insert(TEST_PACKET_ID_0);
let mut tcp_server = generic_tmtc_server( let mut tcp_server = generic_tmtc_server(
&auto_port_addr, &auto_port_addr,
tc_sender.clone(), tc_receiver.clone(),
tm_source, tm_source,
validator, packet_id_lookup,
None,
); );
let dest_addr = tcp_server let dest_addr = tcp_server
.local_addr() .local_addr()
@ -267,24 +209,17 @@ mod tests {
let set_if_done = conn_handled.clone(); let set_if_done = conn_handled.clone();
// Call the connection handler in separate thread, does block. // Call the connection handler in separate thread, does block.
thread::spawn(move || { thread::spawn(move || {
let result = tcp_server.handle_all_connections(Some(Duration::from_millis(100))); let result = tcp_server.handle_next_connection();
if result.is_err() { if result.is_err() {
panic!("handling connection failed: {:?}", result.unwrap_err()); panic!("handling connection failed: {:?}", result.unwrap_err());
} }
let conn_result = result.unwrap(); let conn_result = result.unwrap();
matches!(conn_result, ConnectionResult::HandledConnections(1)); assert_eq!(conn_result.num_received_tcs, 1);
tcp_server assert_eq!(conn_result.num_sent_tms, 0);
.generic_server
.finished_handler
.check_last_connection(0, 1);
tcp_server
.generic_server
.finished_handler
.check_no_connections_left();
set_if_done.store(true, Ordering::Relaxed); set_if_done.store(true, Ordering::Relaxed);
}); });
let ping_tc = let mut sph = SpHeader::tc_unseg(TEST_APID_0, 0, 0).unwrap();
PusTcCreator::new_simple(SpHeader::new_from_apid(TEST_APID_0), 17, 1, &[], true); let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true);
let tc_0 = ping_tc.to_vec().expect("packet generation failed"); let tc_0 = ping_tc.to_vec().expect("packet generation failed");
let mut stream = TcpStream::connect(dest_addr).expect("connecting to TCP server failed"); let mut stream = TcpStream::connect(dest_addr).expect("connecting to TCP server failed");
stream stream
@ -301,40 +236,40 @@ mod tests {
if !conn_handled.load(Ordering::Relaxed) { if !conn_handled.load(Ordering::Relaxed) {
panic!("connection was not handled properly"); panic!("connection was not handled properly");
} }
let packet = tc_receiver.try_recv().expect("receiving TC failed"); // Check that TC has arrived.
assert_eq!(packet.packet, tc_0); let mut tc_queue = tc_receiver.tc_queue.lock().unwrap();
matches!(tc_receiver.try_recv(), Err(mpsc::TryRecvError::Empty)); assert_eq!(tc_queue.len(), 1);
assert_eq!(tc_queue.pop_front().unwrap(), tc_0);
} }
#[test] #[test]
fn test_multi_tc_multi_tm() { fn test_multi_tc_multi_tm() {
let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0);
let (tc_sender, tc_receiver) = mpsc::channel(); let tc_receiver = SyncTcCacher::default();
let mut tm_source = SyncTmSource::default(); let mut tm_source = SyncTmSource::default();
// Add telemetry // Add telemetry
let mut total_tm_len = 0; let mut total_tm_len = 0;
let verif_tm = let mut sph = SpHeader::tc_unseg(TEST_APID_0, 0, 0).unwrap();
PusTcCreator::new_simple(SpHeader::new_from_apid(TEST_APID_0), 1, 1, &[], true); let verif_tm = PusTcCreator::new_simple(&mut sph, 1, 1, None, true);
let tm_0 = verif_tm.to_vec().expect("writing packet failed"); let tm_0 = verif_tm.to_vec().expect("writing packet failed");
total_tm_len += tm_0.len(); total_tm_len += tm_0.len();
tm_source.add_tm(&tm_0); tm_source.add_tm(&tm_0);
let verif_tm = let mut sph = SpHeader::tc_unseg(TEST_APID_1, 0, 0).unwrap();
PusTcCreator::new_simple(SpHeader::new_from_apid(TEST_APID_1), 1, 3, &[], true); let verif_tm = PusTcCreator::new_simple(&mut sph, 1, 3, None, true);
let tm_1 = verif_tm.to_vec().expect("writing packet failed"); let tm_1 = verif_tm.to_vec().expect("writing packet failed");
total_tm_len += tm_1.len(); total_tm_len += tm_1.len();
tm_source.add_tm(&tm_1); tm_source.add_tm(&tm_1);
// Set up server // Set up server
let mut validator = SimpleValidator::default(); let mut packet_id_lookup = HashSet::new();
validator.0.insert(TEST_PACKET_ID_0); packet_id_lookup.insert(TEST_PACKET_ID_0);
validator.0.insert(TEST_PACKET_ID_1); packet_id_lookup.insert(TEST_PACKET_ID_1);
let mut tcp_server = generic_tmtc_server( let mut tcp_server = generic_tmtc_server(
&auto_port_addr, &auto_port_addr,
tc_sender.clone(), tc_receiver.clone(),
tm_source, tm_source,
validator, packet_id_lookup,
None,
); );
let dest_addr = tcp_server let dest_addr = tcp_server
.local_addr() .local_addr()
@ -344,20 +279,16 @@ mod tests {
// Call the connection handler in separate thread, does block. // Call the connection handler in separate thread, does block.
thread::spawn(move || { thread::spawn(move || {
let result = tcp_server.handle_all_connections(Some(Duration::from_millis(100))); let result = tcp_server.handle_next_connection();
if result.is_err() { if result.is_err() {
panic!("handling connection failed: {:?}", result.unwrap_err()); panic!("handling connection failed: {:?}", result.unwrap_err());
} }
let conn_result = result.unwrap(); let conn_result = result.unwrap();
matches!(conn_result, ConnectionResult::HandledConnections(1)); assert_eq!(
tcp_server conn_result.num_received_tcs, 2,
.generic_server "wrong number of received TCs"
.finished_handler );
.check_last_connection(2, 2); assert_eq!(conn_result.num_sent_tms, 2, "wrong number of sent TMs");
tcp_server
.generic_server
.finished_handler
.check_no_connections_left();
set_if_done.store(true, Ordering::Relaxed); set_if_done.store(true, Ordering::Relaxed);
}); });
let mut stream = TcpStream::connect(dest_addr).expect("connecting to TCP server failed"); let mut stream = TcpStream::connect(dest_addr).expect("connecting to TCP server failed");
@ -366,14 +297,14 @@ mod tests {
.expect("setting reas timeout failed"); .expect("setting reas timeout failed");
// Send telecommands // Send telecommands
let ping_tc = let mut sph = SpHeader::tc_unseg(TEST_APID_0, 0, 0).unwrap();
PusTcCreator::new_simple(SpHeader::new_from_apid(TEST_APID_0), 17, 1, &[], true); let ping_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true);
let tc_0 = ping_tc.to_vec().expect("ping tc creation failed"); let tc_0 = ping_tc.to_vec().expect("ping tc creation failed");
stream stream
.write_all(&tc_0) .write_all(&tc_0)
.expect("writing to TCP server failed"); .expect("writing to TCP server failed");
let action_tc = let mut sph = SpHeader::tc_unseg(TEST_APID_1, 0, 0).unwrap();
PusTcCreator::new_simple(SpHeader::new_from_apid(TEST_APID_1), 8, 0, &[], true); let action_tc = PusTcCreator::new_simple(&mut sph, 8, 0, None, true);
let tc_1 = action_tc.to_vec().expect("action tc creation failed"); let tc_1 = action_tc.to_vec().expect("action tc creation failed");
stream stream
.write_all(&tc_1) .write_all(&tc_1)
@ -408,10 +339,9 @@ mod tests {
panic!("connection was not handled properly"); panic!("connection was not handled properly");
} }
// Check that TC has arrived. // Check that TC has arrived.
let packet_0 = tc_receiver.try_recv().expect("receiving TC failed"); let mut tc_queue = tc_receiver.tc_queue.lock().unwrap();
assert_eq!(packet_0.packet, tc_0); assert_eq!(tc_queue.len(), 2);
let packet_1 = tc_receiver.try_recv().expect("receiving TC failed"); assert_eq!(tc_queue.pop_front().unwrap(), tc_0);
assert_eq!(packet_1.packet, tc_1); assert_eq!(tc_queue.pop_front().unwrap(), tc_1);
matches!(tc_receiver.try_recv(), Err(mpsc::TryRecvError::Empty));
} }
} }

View File

@ -1,8 +1,7 @@
//! Generic UDP TC server. //! Generic UDP TC server.
use crate::tmtc::PacketSenderRaw; use crate::tmtc::{ReceivesTc, ReceivesTcCore};
use crate::ComponentId; use std::boxed::Box;
use core::fmt::Debug; use std::io::{Error, ErrorKind};
use std::io::{self, ErrorKind};
use std::net::{SocketAddr, ToSocketAddrs, UdpSocket}; use std::net::{SocketAddr, ToSocketAddrs, UdpSocket};
use std::vec; use std::vec;
use std::vec::Vec; use std::vec::Vec;
@ -12,46 +11,45 @@ use std::vec::Vec;
/// ///
/// It caches all received telecomands into a vector. The maximum expected telecommand size should /// It caches all received telecomands into a vector. The maximum expected telecommand size should
/// be declared upfront. This avoids dynamic allocation during run-time. The user can specify a TC /// be declared upfront. This avoids dynamic allocation during run-time. The user can specify a TC
/// sender in form of a special trait object which implements [PacketSenderRaw]. For example, this /// receiver in form of a special trait object which implements [ReceivesTc]. Please note that the
/// can be used to send the telecommands to a centralized TC source component for further /// receiver should copy out the received data if it the data is required past the
/// processing and routing. /// [ReceivesTcCore::pass_tc] call.
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; /// use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
/// use std::sync::mpsc; /// use spacepackets::ecss::SerializablePusPacket;
/// use spacepackets::ecss::WritablePusPacket; /// use satrs_core::hal::std::udp_server::UdpTcServer;
/// use satrs::hal::std::udp_server::UdpTcServer; /// use satrs_core::tmtc::{ReceivesTc, ReceivesTcCore};
/// use satrs::ComponentId;
/// use satrs::tmtc::PacketSenderRaw;
/// use spacepackets::SpHeader; /// use spacepackets::SpHeader;
/// use spacepackets::ecss::tc::PusTcCreator; /// use spacepackets::ecss::tc::PusTcCreator;
/// ///
/// const UDP_SERVER_ID: ComponentId = 0x05; /// #[derive (Default)]
/// struct PingReceiver {}
/// impl ReceivesTcCore for PingReceiver {
/// type Error = ();
/// fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> {
/// assert_eq!(tc_raw.len(), 13);
/// Ok(())
/// }
/// }
/// ///
/// let (packet_sender, packet_receiver) = mpsc::channel(); /// let mut buf = [0; 32];
/// let dest_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 7777); /// let dest_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 7777);
/// let mut udp_tc_server = UdpTcServer::new(UDP_SERVER_ID, dest_addr, 2048, packet_sender) /// let ping_receiver = PingReceiver::default();
/// let mut udp_tc_server = UdpTcServer::new(dest_addr, 2048, Box::new(ping_receiver))
/// .expect("Creating UDP TMTC server failed"); /// .expect("Creating UDP TMTC server failed");
/// let sph = SpHeader::new_from_apid(0x02); /// let mut sph = SpHeader::tc_unseg(0x02, 0, 0).unwrap();
/// let pus_tc = PusTcCreator::new_simple(sph, 17, 1, &[], true); /// let pus_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true);
/// // Can not fail. /// let len = pus_tc
/// let ping_tc_raw = pus_tc.to_vec().unwrap(); /// .write_to_bytes(&mut buf)
/// /// .expect("Error writing PUS TC packet");
/// // Now create a UDP client and send the ping telecommand to the server. /// assert_eq!(len, 13);
/// let client = UdpSocket::bind("127.0.0.1:0").expect("creating UDP client failed"); /// let client = UdpSocket::bind("127.0.0.1:7778").expect("Connecting to UDP server failed");
/// client /// client
/// .send_to(&ping_tc_raw, dest_addr) /// .send_to(&buf[0..len], dest_addr)
/// .expect("Error sending PUS TC via UDP"); /// .expect("Error sending PUS TC via UDP");
/// let recv_result = udp_tc_server.try_recv_tc();
/// assert!(recv_result.is_ok());
/// // The packet is received by the UDP TC server and sent via the mpsc channel.
/// let sent_packet_with_sender = packet_receiver.try_recv().expect("expected telecommand");
/// assert_eq!(sent_packet_with_sender.packet, ping_tc_raw);
/// assert_eq!(sent_packet_with_sender.sender_id, UDP_SERVER_ID);
/// // No more packets received.
/// matches!(packet_receiver.try_recv(), Err(mpsc::TryRecvError::Empty));
/// ``` /// ```
/// ///
/// The [satrs-example crate](https://egit.irs.uni-stuttgart.de/rust/fsrc-launchpad/src/branch/main/satrs-example) /// The [satrs-example crate](https://egit.irs.uni-stuttgart.de/rust/fsrc-launchpad/src/branch/main/satrs-example)
@ -59,45 +57,65 @@ use std::vec::Vec;
/// [example code](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example/src/tmtc.rs#L67) /// [example code](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example/src/tmtc.rs#L67)
/// on how to use this TC server. It uses the server to receive PUS telecommands on a specific port /// on how to use this TC server. It uses the server to receive PUS telecommands on a specific port
/// and then forwards them to a generic CCSDS packet receiver. /// and then forwards them to a generic CCSDS packet receiver.
pub struct UdpTcServer<TcSender: PacketSenderRaw<Error = SendError>, SendError> { pub struct UdpTcServer<E> {
pub id: ComponentId,
pub socket: UdpSocket, pub socket: UdpSocket,
recv_buf: Vec<u8>, recv_buf: Vec<u8>,
sender_addr: Option<SocketAddr>, sender_addr: Option<SocketAddr>,
pub tc_sender: TcSender, tc_receiver: Box<dyn ReceivesTc<Error = E>>,
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug)]
pub enum ReceiveResult<SendError: Debug + 'static> { pub enum ReceiveResult<E> {
#[error("nothing was received")]
NothingReceived, NothingReceived,
#[error(transparent)] IoError(Error),
Io(#[from] io::Error), ReceiverError(E),
#[error(transparent)]
Send(SendError),
} }
impl<TcSender: PacketSenderRaw<Error = SendError>, SendError: Debug + 'static> impl<E> From<Error> for ReceiveResult<E> {
UdpTcServer<TcSender, SendError> fn from(e: Error) -> Self {
{ ReceiveResult::IoError(e)
}
}
impl<E: PartialEq> PartialEq for ReceiveResult<E> {
fn eq(&self, other: &Self) -> bool {
use ReceiveResult::*;
match (self, other) {
(IoError(ref e), IoError(ref other_e)) => e.kind() == other_e.kind(),
(NothingReceived, NothingReceived) => true,
(ReceiverError(e), ReceiverError(other_e)) => e == other_e,
_ => false,
}
}
}
impl<E: Eq + PartialEq> Eq for ReceiveResult<E> {}
impl<E: 'static> ReceivesTcCore for UdpTcServer<E> {
type Error = E;
fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> {
self.tc_receiver.pass_tc(tc_raw)
}
}
impl<E: 'static> UdpTcServer<E> {
pub fn new<A: ToSocketAddrs>( pub fn new<A: ToSocketAddrs>(
id: ComponentId,
addr: A, addr: A,
max_recv_size: usize, max_recv_size: usize,
tc_sender: TcSender, tc_receiver: Box<dyn ReceivesTc<Error = E>>,
) -> Result<Self, io::Error> { ) -> Result<Self, Error> {
let server = Self { let server = Self {
id,
socket: UdpSocket::bind(addr)?, socket: UdpSocket::bind(addr)?,
recv_buf: vec![0; max_recv_size], recv_buf: vec![0; max_recv_size],
sender_addr: None, sender_addr: None,
tc_sender, tc_receiver,
}; };
server.socket.set_nonblocking(true)?; server.socket.set_nonblocking(true)?;
Ok(server) Ok(server)
} }
pub fn try_recv_tc(&mut self) -> Result<(usize, SocketAddr), ReceiveResult<SendError>> { pub fn try_recv_tc(&mut self) -> Result<(usize, SocketAddr), ReceiveResult<E>> {
let res = match self.socket.recv_from(&mut self.recv_buf) { let res = match self.socket.recv_from(&mut self.recv_buf) {
Ok(res) => res, Ok(res) => res,
Err(e) => { Err(e) => {
@ -110,9 +128,9 @@ impl<TcSender: PacketSenderRaw<Error = SendError>, SendError: Debug + 'static>
}; };
let (num_bytes, from) = res; let (num_bytes, from) = res;
self.sender_addr = Some(from); self.sender_addr = Some(from);
self.tc_sender self.tc_receiver
.send_packet(self.id, &self.recv_buf[0..num_bytes]) .pass_tc(&self.recv_buf[0..num_bytes])
.map_err(ReceiveResult::Send)?; .map_err(|e| ReceiveResult::ReceiverError(e))?;
Ok(res) Ok(res)
} }
@ -124,35 +142,29 @@ impl<TcSender: PacketSenderRaw<Error = SendError>, SendError: Debug + 'static>
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::hal::std::udp_server::{ReceiveResult, UdpTcServer}; use crate::hal::std::udp_server::{ReceiveResult, UdpTcServer};
use crate::queue::GenericSendError; use crate::tmtc::ReceivesTcCore;
use crate::tmtc::PacketSenderRaw;
use crate::ComponentId;
use core::cell::RefCell;
use spacepackets::ecss::tc::PusTcCreator; use spacepackets::ecss::tc::PusTcCreator;
use spacepackets::ecss::WritablePusPacket; use spacepackets::ecss::SerializablePusPacket;
use spacepackets::SpHeader; use spacepackets::SpHeader;
use std::boxed::Box;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
use std::vec::Vec; use std::vec::Vec;
fn is_send<T: Send>(_: &T) {} fn is_send<T: Send>(_: &T) {}
const UDP_SERVER_ID: ComponentId = 0x05;
#[derive(Default)] #[derive(Default)]
struct PingReceiver { struct PingReceiver {
pub sent_cmds: RefCell<VecDeque<Vec<u8>>>, pub sent_cmds: VecDeque<Vec<u8>>,
} }
impl PacketSenderRaw for PingReceiver { impl ReceivesTcCore for PingReceiver {
type Error = GenericSendError; type Error = ();
fn send_packet(&self, sender_id: ComponentId, tc_raw: &[u8]) -> Result<(), Self::Error> { fn pass_tc(&mut self, tc_raw: &[u8]) -> Result<(), Self::Error> {
assert_eq!(sender_id, UDP_SERVER_ID);
let mut sent_data = Vec::new(); let mut sent_data = Vec::new();
sent_data.extend_from_slice(tc_raw); sent_data.extend_from_slice(tc_raw);
let mut queue = self.sent_cmds.borrow_mut(); self.sent_cmds.push_back(sent_data);
queue.push_back(sent_data);
Ok(()) Ok(())
} }
} }
@ -163,11 +175,11 @@ mod tests {
let dest_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 7777); let dest_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 7777);
let ping_receiver = PingReceiver::default(); let ping_receiver = PingReceiver::default();
is_send(&ping_receiver); is_send(&ping_receiver);
let mut udp_tc_server = UdpTcServer::new(UDP_SERVER_ID, dest_addr, 2048, ping_receiver) let mut udp_tc_server = UdpTcServer::new(dest_addr, 2048, Box::new(ping_receiver))
.expect("Creating UDP TMTC server failed"); .expect("Creating UDP TMTC server failed");
is_send(&udp_tc_server); is_send(&udp_tc_server);
let sph = SpHeader::new_from_apid(0x02); let mut sph = SpHeader::tc_unseg(0x02, 0, 0).unwrap();
let pus_tc = PusTcCreator::new_simple(sph, 17, 1, &[], true); let pus_tc = PusTcCreator::new_simple(&mut sph, 17, 1, None, true);
let len = pus_tc let len = pus_tc
.write_to_bytes(&mut buf) .write_to_bytes(&mut buf)
.expect("Error writing PUS TC packet"); .expect("Error writing PUS TC packet");
@ -183,10 +195,9 @@ mod tests {
udp_tc_server.last_sender().expect("No sender set"), udp_tc_server.last_sender().expect("No sender set"),
local_addr local_addr
); );
let ping_receiver = &mut udp_tc_server.tc_sender; let ping_receiver: &mut PingReceiver = udp_tc_server.tc_receiver.downcast_mut().unwrap();
let mut queue = ping_receiver.sent_cmds.borrow_mut(); assert_eq!(ping_receiver.sent_cmds.len(), 1);
assert_eq!(queue.len(), 1); let sent_cmd = ping_receiver.sent_cmds.pop_front().unwrap();
let sent_cmd = queue.pop_front().unwrap();
assert_eq!(sent_cmd, buf[0..len]); assert_eq!(sent_cmd, buf[0..len]);
} }
@ -194,11 +205,11 @@ mod tests {
fn test_nothing_received() { fn test_nothing_received() {
let dest_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 7779); let dest_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 7779);
let ping_receiver = PingReceiver::default(); let ping_receiver = PingReceiver::default();
let mut udp_tc_server = UdpTcServer::new(UDP_SERVER_ID, dest_addr, 2048, ping_receiver) let mut udp_tc_server = UdpTcServer::new(dest_addr, 2048, Box::new(ping_receiver))
.expect("Creating UDP TMTC server failed"); .expect("Creating UDP TMTC server failed");
let res = udp_tc_server.try_recv_tc(); let res = udp_tc_server.try_recv_tc();
assert!(res.is_err()); assert!(res.is_err());
let err = res.unwrap_err(); let err = res.unwrap_err();
matches!(err, ReceiveResult::NothingReceived); assert_eq!(err, ReceiveResult::NothingReceived);
} }
} }

16
satrs-core/src/hk.rs Normal file
View File

@ -0,0 +1,16 @@
pub type CollectionIntervalFactor = u32;
pub type UniqueId = u32;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum HkRequest {
OneShot(UniqueId),
Enable(UniqueId),
Disable(UniqueId),
ModifyCollectionInterval(UniqueId, CollectionIntervalFactor),
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct TargetedHkRequest {
target: u32,
hk_request: HkRequest,
}

49
satrs-core/src/lib.rs Normal file
View File

@ -0,0 +1,49 @@
//! # Core components of the sat-rs framework
//!
//! You can find more information about the sat-rs framework on the
//! [homepage](https://egit.irs.uni-stuttgart.de/rust/sat-rs).
//!
//! ## Overview
//!
//! The core modules of this crate include
//!
//! - The [event manager][event_man] module which provides a publish and
//! and subscribe to route events.
//! - The [pus] module which provides special support for projects using
//! the [ECSS PUS C standard](https://ecss.nl/standard/ecss-e-st-70-41c-space-engineering-telemetry-and-telecommand-packet-utilization-15-april-2016/).
#![no_std]
#![cfg_attr(doc_cfg, feature(doc_cfg))]
#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(feature = "alloc")]
extern crate downcast_rs;
#[cfg(any(feature = "std", test))]
extern crate std;
pub mod cfdp;
pub mod encoding;
pub mod error;
#[cfg(feature = "alloc")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
pub mod event_man;
pub mod events;
#[cfg(feature = "std")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))]
pub mod executable;
pub mod hal;
pub mod hk;
pub mod mode;
pub mod objects;
pub mod params;
pub mod pool;
pub mod power;
pub mod pus;
pub mod request;
pub mod res_code;
pub mod seq_count;
pub mod tmtc;
pub use spacepackets;
// Generic channel ID type.
pub type ChannelId = u32;

94
satrs-core/src/mode.rs Normal file
View File

@ -0,0 +1,94 @@
use crate::tmtc::TargetId;
use core::mem::size_of;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use spacepackets::ByteConversionError;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ModeAndSubmode {
mode: u32,
submode: u16,
}
impl ModeAndSubmode {
pub const fn new_mode_only(mode: u32) -> Self {
Self { mode, submode: 0 }
}
pub const fn new(mode: u32, submode: u16) -> Self {
Self { mode, submode }
}
pub fn raw_len() -> usize {
size_of::<u32>() + size_of::<u16>()
}
pub fn from_be_bytes(buf: &[u8]) -> Result<Self, ByteConversionError> {
if buf.len() < 6 {
return Err(ByteConversionError::FromSliceTooSmall {
expected: 6,
found: buf.len(),
});
}
Ok(Self {
mode: u32::from_be_bytes(buf[0..4].try_into().unwrap()),
submode: u16::from_be_bytes(buf[4..6].try_into().unwrap()),
})
}
pub fn mode(&self) -> u32 {
self.mode
}
pub fn submode(&self) -> u16 {
self.submode
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ModeCommand {
pub address: TargetId,
pub mode_submode: ModeAndSubmode,
}
impl ModeCommand {
pub const fn new(address: TargetId, mode_submode: ModeAndSubmode) -> Self {
Self {
address,
mode_submode,
}
}
pub fn address(&self) -> TargetId {
self.address
}
pub fn mode_submode(&self) -> ModeAndSubmode {
self.mode_submode
}
pub fn mode(&self) -> u32 {
self.mode_submode.mode
}
pub fn submode(&self) -> u16 {
self.mode_submode.submode
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ModeRequest {
SetMode(ModeAndSubmode),
ReadMode,
AnnounceMode,
AnnounceModeRecursive,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TargetedModeRequest {
target_id: TargetId,
mode_request: ModeRequest,
}

307
satrs-core/src/objects.rs Normal file
View File

@ -0,0 +1,307 @@
//! # Module providing addressable object support and a manager for them
//!
//! Each addressable object can be identified using an [object ID][ObjectId].
//! The [system object][ManagedSystemObject] trait also allows storing these objects into the
//! [object manager][ObjectManager]. They can then be retrieved and casted back to a known type
//! using the object ID.
//!
//! # Examples
//!
//! ```rust
//! use std::any::Any;
//! use std::error::Error;
//! use satrs_core::objects::{ManagedSystemObject, ObjectId, ObjectManager, SystemObject};
//!
//! struct ExampleSysObj {
//! id: ObjectId,
//! dummy: u32,
//! was_initialized: bool,
//! }
//!
//! impl ExampleSysObj {
//! fn new(id: ObjectId, dummy: u32) -> ExampleSysObj {
//! ExampleSysObj {
//! id,
//! dummy,
//! was_initialized: false,
//! }
//! }
//! }
//!
//! impl SystemObject for ExampleSysObj {
//! type Error = ();
//! fn get_object_id(&self) -> &ObjectId {
//! &self.id
//! }
//!
//! fn initialize(&mut self) -> Result<(), Self::Error> {
//! self.was_initialized = true;
//! Ok(())
//! }
//! }
//!
//! impl ManagedSystemObject for ExampleSysObj {}
//!
//! let mut obj_manager = ObjectManager::default();
//! let obj_id = ObjectId { id: 0, name: "Example 0"};
//! let example_obj = ExampleSysObj::new(obj_id, 42);
//! obj_manager.insert(Box::new(example_obj));
//! let obj_back_casted: Option<&ExampleSysObj> = obj_manager.get_ref(&obj_id);
//! let example_obj = obj_back_casted.unwrap();
//! assert_eq!(example_obj.id, obj_id);
//! assert_eq!(example_obj.dummy, 42);
//! ```
use crate::tmtc::TargetId;
#[cfg(feature = "alloc")]
use alloc::boxed::Box;
#[cfg(feature = "alloc")]
pub use alloc_mod::*;
#[cfg(feature = "alloc")]
use downcast_rs::Downcast;
#[cfg(feature = "alloc")]
use hashbrown::HashMap;
#[cfg(feature = "std")]
use std::error::Error;
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
pub struct ObjectId {
pub id: TargetId,
pub name: &'static str,
}
#[cfg(feature = "alloc")]
pub mod alloc_mod {
use super::*;
/// Each object which is stored inside the [object manager][ObjectManager] needs to implemented
/// this trait
pub trait SystemObject: Downcast {
type Error;
fn get_object_id(&self) -> &ObjectId;
fn initialize(&mut self) -> Result<(), Self::Error>;
}
downcast_rs::impl_downcast!(SystemObject assoc Error);
pub trait ManagedSystemObject: SystemObject + Send {}
downcast_rs::impl_downcast!(ManagedSystemObject assoc Error);
/// Helper module to manage multiple [ManagedSystemObjects][ManagedSystemObject] by mapping them
/// using an [object ID][ObjectId]
#[cfg(feature = "alloc")]
pub struct ObjectManager<E> {
obj_map: HashMap<ObjectId, Box<dyn ManagedSystemObject<Error = E>>>,
}
#[cfg(feature = "alloc")]
impl<E: 'static> Default for ObjectManager<E> {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "alloc")]
impl<E: 'static> ObjectManager<E> {
pub fn new() -> Self {
ObjectManager {
obj_map: HashMap::new(),
}
}
pub fn insert(&mut self, sys_obj: Box<dyn ManagedSystemObject<Error = E>>) -> bool {
let obj_id = sys_obj.get_object_id();
if self.obj_map.contains_key(obj_id) {
return false;
}
self.obj_map.insert(*obj_id, sys_obj).is_none()
}
/// Initializes all System Objects in the hash map and returns the number of successful
/// initializations
pub fn initialize(&mut self) -> Result<u32, Box<dyn Error>> {
let mut init_success = 0;
for val in self.obj_map.values_mut() {
if val.initialize().is_ok() {
init_success += 1
}
}
Ok(init_success)
}
/// Retrieve a reference to an object stored inside the manager. The type to retrieve needs to
/// be explicitly passed as a generic parameter or specified on the left hand side of the
/// expression.
pub fn get_ref<T: ManagedSystemObject<Error = E>>(&self, key: &ObjectId) -> Option<&T> {
self.obj_map.get(key).and_then(|o| o.downcast_ref::<T>())
}
/// Retrieve a mutable reference to an object stored inside the manager. The type to retrieve
/// needs to be explicitly passed as a generic parameter or specified on the left hand side
/// of the expression.
pub fn get_mut<T: ManagedSystemObject<Error = E>>(
&mut self,
key: &ObjectId,
) -> Option<&mut T> {
self.obj_map
.get_mut(key)
.and_then(|o| o.downcast_mut::<T>())
}
}
}
#[cfg(test)]
mod tests {
use crate::objects::{ManagedSystemObject, ObjectId, ObjectManager, SystemObject};
use std::boxed::Box;
use std::string::String;
use std::sync::{Arc, Mutex};
use std::thread;
struct ExampleSysObj {
id: ObjectId,
dummy: u32,
was_initialized: bool,
}
impl ExampleSysObj {
fn new(id: ObjectId, dummy: u32) -> ExampleSysObj {
ExampleSysObj {
id,
dummy,
was_initialized: false,
}
}
}
impl SystemObject for ExampleSysObj {
type Error = ();
fn get_object_id(&self) -> &ObjectId {
&self.id
}
fn initialize(&mut self) -> Result<(), Self::Error> {
self.was_initialized = true;
Ok(())
}
}
impl ManagedSystemObject for ExampleSysObj {}
struct OtherExampleObject {
id: ObjectId,
string: String,
was_initialized: bool,
}
impl SystemObject for OtherExampleObject {
type Error = ();
fn get_object_id(&self) -> &ObjectId {
&self.id
}
fn initialize(&mut self) -> Result<(), Self::Error> {
self.was_initialized = true;
Ok(())
}
}
impl ManagedSystemObject for OtherExampleObject {}
#[test]
fn test_obj_manager_simple() {
let mut obj_manager = ObjectManager::default();
let expl_obj_id = ObjectId {
id: 0,
name: "Example 0",
};
let example_obj = ExampleSysObj::new(expl_obj_id, 42);
assert!(obj_manager.insert(Box::new(example_obj)));
let res = obj_manager.initialize();
assert!(res.is_ok());
assert_eq!(res.unwrap(), 1);
let obj_back_casted: Option<&ExampleSysObj> = obj_manager.get_ref(&expl_obj_id);
assert!(obj_back_casted.is_some());
let expl_obj_back_casted = obj_back_casted.unwrap();
assert_eq!(expl_obj_back_casted.dummy, 42);
assert!(expl_obj_back_casted.was_initialized);
let second_obj_id = ObjectId {
id: 12,
name: "Example 1",
};
let second_example_obj = OtherExampleObject {
id: second_obj_id,
string: String::from("Hello Test"),
was_initialized: false,
};
assert!(obj_manager.insert(Box::new(second_example_obj)));
let res = obj_manager.initialize();
assert!(res.is_ok());
assert_eq!(res.unwrap(), 2);
let obj_back_casted: Option<&OtherExampleObject> = obj_manager.get_ref(&second_obj_id);
assert!(obj_back_casted.is_some());
let expl_obj_back_casted = obj_back_casted.unwrap();
assert_eq!(expl_obj_back_casted.string, String::from("Hello Test"));
assert!(expl_obj_back_casted.was_initialized);
let existing_obj_id = ObjectId {
id: 12,
name: "Example 1",
};
let invalid_obj = OtherExampleObject {
id: existing_obj_id,
string: String::from("Hello Test"),
was_initialized: false,
};
assert!(!obj_manager.insert(Box::new(invalid_obj)));
}
#[test]
fn object_man_threaded() {
let obj_manager = Arc::new(Mutex::new(ObjectManager::new()));
let expl_obj_id = ObjectId {
id: 0,
name: "Example 0",
};
let example_obj = ExampleSysObj::new(expl_obj_id, 42);
let second_obj_id = ObjectId {
id: 12,
name: "Example 1",
};
let second_example_obj = OtherExampleObject {
id: second_obj_id,
string: String::from("Hello Test"),
was_initialized: false,
};
let mut obj_man_handle = obj_manager.lock().expect("Mutex lock failed");
assert!(obj_man_handle.insert(Box::new(example_obj)));
assert!(obj_man_handle.insert(Box::new(second_example_obj)));
let res = obj_man_handle.initialize();
std::mem::drop(obj_man_handle);
assert!(res.is_ok());
assert_eq!(res.unwrap(), 2);
let obj_man_0 = obj_manager.clone();
let jh0 = thread::spawn(move || {
let locked_man = obj_man_0.lock().expect("Mutex lock failed");
let obj_back_casted: Option<&ExampleSysObj> = locked_man.get_ref(&expl_obj_id);
assert!(obj_back_casted.is_some());
let expl_obj_back_casted = obj_back_casted.unwrap();
assert_eq!(expl_obj_back_casted.dummy, 42);
assert!(expl_obj_back_casted.was_initialized);
std::mem::drop(locked_man)
});
let jh1 = thread::spawn(move || {
let locked_man = obj_manager.lock().expect("Mutex lock failed");
let obj_back_casted: Option<&OtherExampleObject> = locked_man.get_ref(&second_obj_id);
assert!(obj_back_casted.is_some());
let expl_obj_back_casted = obj_back_casted.unwrap();
assert_eq!(expl_obj_back_casted.string, String::from("Hello Test"));
assert!(expl_obj_back_casted.was_initialized);
std::mem::drop(locked_man)
});
jh0.join().expect("Joining thread 0 failed");
jh1.join().expect("Joining thread 1 failed");
}
}

679
satrs-core/src/params.rs Normal file
View File

@ -0,0 +1,679 @@
//! Parameter types and enums.
//!
//! This module contains various helper types.
//!
//! # Primtive Parameter Wrappers and Enumeration
//!
//! This module includes wrapper for primitive rust types using the newtype pattern.
//! This was also done for pairs and triplets of these primitive types.
//! The [WritableToBeBytes] was implemented for all those types as well, which allows to easily
//! convert them into a network friendly raw byte format. The [ParamsRaw] enumeration groups
//! all newtypes and implements the [WritableToBeBytes] trait itself.
//!
//! ## Example for primitive type wrapper
//!
//! ```
//! use satrs_core::params::{ParamsRaw, ToBeBytes, U32Pair, WritableToBeBytes};
//!
//! let u32_pair = U32Pair(0x1010, 25);
//! assert_eq!(u32_pair.0, 0x1010);
//! assert_eq!(u32_pair.1, 25);
//! // Convert to raw stream
//! let raw_buf = u32_pair.to_be_bytes();
//! assert_eq!(raw_buf, [0, 0, 0x10, 0x10, 0, 0, 0, 25]);
//!
//! // Convert to enum variant
//! let params_raw: ParamsRaw = u32_pair.into();
//! assert_eq!(params_raw, (0x1010_u32, 25_u32).into());
//!
//! // Convert to stream using the enum variant
//! let mut other_raw_buf: [u8; 8] = [0; 8];
//! params_raw.write_to_be_bytes(&mut other_raw_buf).expect("Writing parameter to buffer failed");
//! assert_eq!(other_raw_buf, [0, 0, 0x10, 0x10, 0, 0, 0, 25]);
//!
//! // Create a pair from a raw stream
//! let u32_pair_from_stream: U32Pair = raw_buf.as_slice().try_into().unwrap();
//! assert_eq!(u32_pair_from_stream.0, 0x1010);
//! assert_eq!(u32_pair_from_stream.1, 25);
//! ```
//!
//! # Generic Parameter Enumeration
//!
//! The module also contains generic parameter enumerations.
//! This includes the [ParamsHeapless] enumeration for contained values which do not require heap
//! allocation, and the [Params] which enumerates [ParamsHeapless] and some additional types which
//! require [alloc] support but allow for more flexbility.
#[cfg(feature = "alloc")]
use crate::pool::StoreAddr;
#[cfg(feature = "alloc")]
use alloc::string::{String, ToString};
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use core::fmt::Debug;
use core::mem::size_of;
use paste::paste;
use spacepackets::ecss::{EcssEnumU16, EcssEnumU32, EcssEnumU64, EcssEnumU8};
use spacepackets::util::UnsignedEnum;
use spacepackets::ByteConversionError;
#[cfg(feature = "alloc")]
pub use alloc_mod::*;
pub use spacepackets::util::ToBeBytes;
/// Generic trait which is used for objects which can be converted into a raw network (big) endian
/// byte format.
pub trait WritableToBeBytes {
fn raw_len(&self) -> usize;
/// Writes the object to a raw buffer in network endianness (big)
fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError>;
}
macro_rules! param_to_be_bytes_impl {
($Newtype: ident) => {
impl WritableToBeBytes for $Newtype {
#[inline]
fn raw_len(&self) -> usize {
size_of::<<Self as ToBeBytes>::ByteArray>()
}
fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
let raw_len = self.raw_len();
if buf.len() < raw_len {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: raw_len,
});
}
buf[0..raw_len].copy_from_slice(&self.to_be_bytes());
Ok(raw_len)
}
}
};
}
macro_rules! primitive_newtypes_with_eq {
($($ty: ty,)+) => {
$(
paste! {
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct [<$ty:upper>](pub $ty);
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct [<$ty:upper Pair>](pub $ty, pub $ty);
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct [<$ty:upper Triplet>](pub $ty, pub $ty, pub $ty);
param_to_be_bytes_impl!([<$ty:upper>]);
param_to_be_bytes_impl!([<$ty:upper Pair>]);
param_to_be_bytes_impl!([<$ty:upper Triplet>]);
impl From<$ty> for [<$ty:upper>] {
fn from(v: $ty) -> Self {
Self(v)
}
}
impl From<($ty, $ty)> for [<$ty:upper Pair>] {
fn from(v: ($ty, $ty)) -> Self {
Self(v.0, v.1)
}
}
impl From<($ty, $ty, $ty)> for [<$ty:upper Triplet>] {
fn from(v: ($ty, $ty, $ty)) -> Self {
Self(v.0, v.1, v.2)
}
}
}
)+
}
}
macro_rules! primitive_newtypes {
($($ty: ty,)+) => {
$(
paste! {
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct [<$ty:upper>](pub $ty);
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct [<$ty:upper Pair>](pub $ty, pub $ty);
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct [<$ty:upper Triplet>](pub $ty, pub $ty, pub $ty);
param_to_be_bytes_impl!([<$ty:upper>]);
param_to_be_bytes_impl!([<$ty:upper Pair>]);
param_to_be_bytes_impl!([<$ty:upper Triplet>]);
impl From<$ty> for [<$ty:upper>] {
fn from(v: $ty) -> Self {
Self(v)
}
}
impl From<($ty, $ty)> for [<$ty:upper Pair>] {
fn from(v: ($ty, $ty)) -> Self {
Self(v.0, v.1)
}
}
impl From<($ty, $ty, $ty)> for [<$ty:upper Triplet>] {
fn from(v: ($ty, $ty, $ty)) -> Self {
Self(v.0, v.1, v.2)
}
}
}
)+
}
}
primitive_newtypes_with_eq!(u8, u16, u32, u64, i8, i16, i32, i64,);
primitive_newtypes!(f32, f64,);
macro_rules! scalar_byte_conversions_impl {
($($ty: ty,)+) => {
$(
paste! {
impl ToBeBytes for [<$ty:upper>] {
type ByteArray = [u8; size_of::<$ty>()];
fn written_len(&self) -> usize {
size_of::<Self::ByteArray>()
}
fn to_be_bytes(&self) -> Self::ByteArray {
self.0.to_be_bytes()
}
}
impl TryFrom<&[u8]> for [<$ty:upper>] {
type Error = ByteConversionError;
fn try_from(v: &[u8]) -> Result<Self, Self::Error> {
if v.len() < size_of::<$ty>() {
return Err(ByteConversionError::FromSliceTooSmall{
expected: size_of::<$ty>(),
found: v.len()
});
}
Ok([<$ty:upper>]($ty::from_be_bytes(v[0..size_of::<$ty>()].try_into().unwrap())))
}
}
}
)+
}
}
macro_rules! pair_byte_conversions_impl {
($($ty: ty,)+) => {
$(
paste! {
impl ToBeBytes for [<$ty:upper Pair>] {
type ByteArray = [u8; size_of::<$ty>() * 2];
fn written_len(&self) -> usize {
size_of::<Self::ByteArray>()
}
fn to_be_bytes(&self) -> Self::ByteArray {
let mut array = [0; size_of::<$ty>() * 2];
array[0..size_of::<$ty>()].copy_from_slice(&self.0.to_be_bytes());
array[
size_of::<$ty>()..2 * size_of::<$ty>()
].copy_from_slice(&self.1.to_be_bytes());
array
}
}
impl TryFrom<&[u8]> for [<$ty:upper Pair>] {
type Error = ByteConversionError;
fn try_from(v: &[u8]) -> Result<Self, Self::Error> {
if v.len() < 2 * size_of::<$ty>() {
return Err(ByteConversionError::FromSliceTooSmall{
expected: 2 * size_of::<$ty>(),
found: v.len()
});
}
Ok([<$ty:upper Pair>](
$ty::from_be_bytes(v[0..size_of::<$ty>()].try_into().unwrap()),
$ty::from_be_bytes(v[size_of::<$ty>()..2 * size_of::<$ty>()].try_into().unwrap())
))
}
}
}
)+
}
}
macro_rules! triplet_to_be_bytes_impl {
($($ty: ty,)+) => {
$(
paste! {
impl ToBeBytes for [<$ty:upper Triplet>] {
type ByteArray = [u8; size_of::<$ty>() * 3];
fn written_len(&self) -> usize {
size_of::<Self::ByteArray>()
}
fn to_be_bytes(&self) -> Self::ByteArray {
let mut array = [0; size_of::<$ty>() * 3];
array[0..size_of::<$ty>()].copy_from_slice(&self.0.to_be_bytes());
array[
size_of::<$ty>()..2 * size_of::<$ty>()
].copy_from_slice(&self.1.to_be_bytes());
array[
2 * size_of::<$ty>()..3 * size_of::<$ty>()
].copy_from_slice(&self.2.to_be_bytes());
array
}
}
impl TryFrom<&[u8]> for [<$ty:upper Triplet>] {
type Error = ByteConversionError;
fn try_from(v: &[u8]) -> Result<Self, Self::Error> {
if v.len() < 3 * size_of::<$ty>() {
return Err(ByteConversionError::FromSliceTooSmall{
expected: 3 * size_of::<$ty>(),
found: v.len()
});
}
Ok([<$ty:upper Triplet>](
$ty::from_be_bytes(v[0..size_of::<$ty>()].try_into().unwrap()),
$ty::from_be_bytes(v[size_of::<$ty>()..2 * size_of::<$ty>()].try_into().unwrap()),
$ty::from_be_bytes(v[2 * size_of::<$ty>()..3 * size_of::<$ty>()].try_into().unwrap())
))
}
}
}
)+
}
}
scalar_byte_conversions_impl!(u8, u16, u32, u64, i8, i16, i32, i64, f32, f64,);
impl ToBeBytes for U8Pair {
type ByteArray = [u8; 2];
fn written_len(&self) -> usize {
size_of::<Self::ByteArray>()
}
fn to_be_bytes(&self) -> Self::ByteArray {
let mut array = [0; 2];
array[0] = self.0;
array[1] = self.1;
array
}
}
impl ToBeBytes for I8Pair {
type ByteArray = [u8; 2];
fn written_len(&self) -> usize {
size_of::<Self::ByteArray>()
}
fn to_be_bytes(&self) -> Self::ByteArray {
let mut array = [0; 2];
array[0] = self.0 as u8;
array[1] = self.1 as u8;
array
}
}
impl ToBeBytes for U8Triplet {
type ByteArray = [u8; 3];
fn written_len(&self) -> usize {
size_of::<Self::ByteArray>()
}
fn to_be_bytes(&self) -> Self::ByteArray {
let mut array = [0; 3];
array[0] = self.0;
array[1] = self.1;
array[2] = self.2;
array
}
}
impl ToBeBytes for I8Triplet {
type ByteArray = [u8; 3];
fn written_len(&self) -> usize {
size_of::<Self::ByteArray>()
}
fn to_be_bytes(&self) -> Self::ByteArray {
let mut array = [0; 3];
array[0] = self.0 as u8;
array[1] = self.1 as u8;
array[2] = self.2 as u8;
array
}
}
pair_byte_conversions_impl!(u16, u32, u64, i16, i32, i64, f32, f64,);
triplet_to_be_bytes_impl!(u16, u32, u64, i16, i32, i64, f32, f64,);
/// Generic enumeration for additonal parameters only consisting of primitive data types.
///
/// All contained variants and the enum itself implement the [WritableToBeBytes] trait, which
/// allows to easily convert them into a network-friendly format.
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum ParamsRaw {
U8(U8),
U8Pair(U8Pair),
U8Triplet(U8Triplet),
I8(I8),
I8Pair(I8Pair),
I8Triplet(I8Triplet),
U16(U16),
U16Pair(U16Pair),
U16Triplet(U16Triplet),
I16(I16),
I16Pair(I16Pair),
I16Triplet(I16Triplet),
U32(U32),
U32Pair(U32Pair),
U32Triplet(U32Triplet),
I32(I32),
I32Pair(I32Pair),
I32Triplet(I32Triplet),
F32(F32),
F32Pair(F32Pair),
F32Triplet(F32Triplet),
U64(U64),
I64(I64),
F64(F64),
}
impl WritableToBeBytes for ParamsRaw {
fn raw_len(&self) -> usize {
match self {
ParamsRaw::U8(v) => v.raw_len(),
ParamsRaw::U8Pair(v) => v.raw_len(),
ParamsRaw::U8Triplet(v) => v.raw_len(),
ParamsRaw::I8(v) => v.raw_len(),
ParamsRaw::I8Pair(v) => v.raw_len(),
ParamsRaw::I8Triplet(v) => v.raw_len(),
ParamsRaw::U16(v) => v.raw_len(),
ParamsRaw::U16Pair(v) => v.raw_len(),
ParamsRaw::U16Triplet(v) => v.raw_len(),
ParamsRaw::I16(v) => v.raw_len(),
ParamsRaw::I16Pair(v) => v.raw_len(),
ParamsRaw::I16Triplet(v) => v.raw_len(),
ParamsRaw::U32(v) => v.raw_len(),
ParamsRaw::U32Pair(v) => v.raw_len(),
ParamsRaw::U32Triplet(v) => v.raw_len(),
ParamsRaw::I32(v) => v.raw_len(),
ParamsRaw::I32Pair(v) => v.raw_len(),
ParamsRaw::I32Triplet(v) => v.raw_len(),
ParamsRaw::F32(v) => v.raw_len(),
ParamsRaw::F32Pair(v) => v.raw_len(),
ParamsRaw::F32Triplet(v) => v.raw_len(),
ParamsRaw::U64(v) => v.raw_len(),
ParamsRaw::I64(v) => v.raw_len(),
ParamsRaw::F64(v) => v.raw_len(),
}
}
fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
match self {
ParamsRaw::U8(v) => v.write_to_be_bytes(buf),
ParamsRaw::U8Pair(v) => v.write_to_be_bytes(buf),
ParamsRaw::U8Triplet(v) => v.write_to_be_bytes(buf),
ParamsRaw::I8(v) => v.write_to_be_bytes(buf),
ParamsRaw::I8Pair(v) => v.write_to_be_bytes(buf),
ParamsRaw::I8Triplet(v) => v.write_to_be_bytes(buf),
ParamsRaw::U16(v) => v.write_to_be_bytes(buf),
ParamsRaw::U16Pair(v) => v.write_to_be_bytes(buf),
ParamsRaw::U16Triplet(v) => v.write_to_be_bytes(buf),
ParamsRaw::I16(v) => v.write_to_be_bytes(buf),
ParamsRaw::I16Pair(v) => v.write_to_be_bytes(buf),
ParamsRaw::I16Triplet(v) => v.write_to_be_bytes(buf),
ParamsRaw::U32(v) => v.write_to_be_bytes(buf),
ParamsRaw::U32Pair(v) => v.write_to_be_bytes(buf),
ParamsRaw::U32Triplet(v) => v.write_to_be_bytes(buf),
ParamsRaw::I32(v) => v.write_to_be_bytes(buf),
ParamsRaw::I32Pair(v) => v.write_to_be_bytes(buf),
ParamsRaw::I32Triplet(v) => v.write_to_be_bytes(buf),
ParamsRaw::F32(v) => v.write_to_be_bytes(buf),
ParamsRaw::F32Pair(v) => v.write_to_be_bytes(buf),
ParamsRaw::F32Triplet(v) => v.write_to_be_bytes(buf),
ParamsRaw::U64(v) => v.write_to_be_bytes(buf),
ParamsRaw::I64(v) => v.write_to_be_bytes(buf),
ParamsRaw::F64(v) => v.write_to_be_bytes(buf),
}
}
}
macro_rules! params_raw_from_newtype {
($($newtype: ident,)+) => {
$(
impl From<$newtype> for ParamsRaw {
fn from(v: $newtype) -> Self {
Self::$newtype(v)
}
}
)+
}
}
params_raw_from_newtype!(
U8, U8Pair, U8Triplet, U16, U16Pair, U16Triplet, U32, U32Pair, U32Triplet, I8, I8Pair,
I8Triplet, I16, I16Pair, I16Triplet, I32, I32Pair, I32Triplet, F32, F32Pair, F32Triplet, U64,
I64, F64,
);
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum EcssEnumParams {
U8(EcssEnumU8),
U16(EcssEnumU16),
U32(EcssEnumU32),
U64(EcssEnumU64),
}
macro_rules! writable_as_be_bytes_ecss_enum_impl {
($EnumIdent: ident) => {
impl WritableToBeBytes for $EnumIdent {
fn raw_len(&self) -> usize {
self.size()
}
fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
<Self as UnsignedEnum>::write_to_be_bytes(self, buf).map(|_| self.raw_len())
}
}
};
}
writable_as_be_bytes_ecss_enum_impl!(EcssEnumU8);
writable_as_be_bytes_ecss_enum_impl!(EcssEnumU16);
writable_as_be_bytes_ecss_enum_impl!(EcssEnumU32);
writable_as_be_bytes_ecss_enum_impl!(EcssEnumU64);
impl WritableToBeBytes for EcssEnumParams {
fn raw_len(&self) -> usize {
match self {
EcssEnumParams::U8(e) => e.raw_len(),
EcssEnumParams::U16(e) => e.raw_len(),
EcssEnumParams::U32(e) => e.raw_len(),
EcssEnumParams::U64(e) => e.raw_len(),
}
}
fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
match self {
EcssEnumParams::U8(e) => WritableToBeBytes::write_to_be_bytes(e, buf),
EcssEnumParams::U16(e) => WritableToBeBytes::write_to_be_bytes(e, buf),
EcssEnumParams::U32(e) => WritableToBeBytes::write_to_be_bytes(e, buf),
EcssEnumParams::U64(e) => WritableToBeBytes::write_to_be_bytes(e, buf),
}
}
}
/// Generic enumeration for parameters which do not rely on heap allocations.
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum ParamsHeapless {
Raw(ParamsRaw),
EcssEnum(EcssEnumParams),
}
macro_rules! from_conversions_for_raw {
($(($raw_ty: ty, $TargetPath: path),)+) => {
$(
impl From<$raw_ty> for ParamsRaw {
fn from(val: $raw_ty) -> Self {
$TargetPath(val.into())
}
}
impl From<$raw_ty> for ParamsHeapless {
fn from(val: $raw_ty) -> Self {
ParamsHeapless::Raw(val.into())
}
}
)+
};
}
from_conversions_for_raw!(
(u8, Self::U8),
((u8, u8), Self::U8Pair),
((u8, u8, u8), Self::U8Triplet),
(i8, Self::I8),
((i8, i8), Self::I8Pair),
((i8, i8, i8), Self::I8Triplet),
(u16, Self::U16),
((u16, u16), Self::U16Pair),
((u16, u16, u16), Self::U16Triplet),
(i16, Self::I16),
((i16, i16), Self::I16Pair),
((i16, i16, i16), Self::I16Triplet),
(u32, Self::U32),
((u32, u32), Self::U32Pair),
((u32, u32, u32), Self::U32Triplet),
(i32, Self::I32),
((i32, i32), Self::I32Pair),
((i32, i32, i32), Self::I32Triplet),
(f32, Self::F32),
((f32, f32), Self::F32Pair),
((f32, f32, f32), Self::F32Triplet),
(u64, Self::U64),
(f64, Self::F64),
);
#[cfg(feature = "alloc")]
mod alloc_mod {
use super::*;
/// Generic enumeration for additional parameters, including parameters which rely on heap
/// allocations.
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
#[derive(Debug, Clone)]
pub enum Params {
Heapless(ParamsHeapless),
Store(StoreAddr),
Vec(Vec<u8>),
String(String),
}
impl From<StoreAddr> for Params {
fn from(x: StoreAddr) -> Self {
Self::Store(x)
}
}
impl From<ParamsHeapless> for Params {
fn from(x: ParamsHeapless) -> Self {
Self::Heapless(x)
}
}
impl From<Vec<u8>> for Params {
fn from(val: Vec<u8>) -> Self {
Self::Vec(val)
}
}
/// Converts a byte slice into the [Params::Vec] variant
impl From<&[u8]> for Params {
fn from(val: &[u8]) -> Self {
Self::Vec(val.to_vec())
}
}
impl From<String> for Params {
fn from(val: String) -> Self {
Self::String(val)
}
}
/// Converts a string slice into the [Params::String] variant
impl From<&str> for Params {
fn from(val: &str) -> Self {
Self::String(val.to_string())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_u32_pair() {
let u32_pair = U32Pair(4, 8);
assert_eq!(u32_pair.0, 4);
assert_eq!(u32_pair.1, 8);
let raw = u32_pair.to_be_bytes();
let mut u32_conv_back = u32::from_be_bytes(raw[0..4].try_into().unwrap());
assert_eq!(u32_conv_back, 4);
u32_conv_back = u32::from_be_bytes(raw[4..8].try_into().unwrap());
assert_eq!(u32_conv_back, 8);
}
#[test]
fn basic_signed_test_pair() {
let i8_pair = I8Pair(-3, -16);
assert_eq!(i8_pair.0, -3);
assert_eq!(i8_pair.1, -16);
let raw = i8_pair.to_be_bytes();
let mut i8_conv_back = i8::from_be_bytes(raw[0..1].try_into().unwrap());
assert_eq!(i8_conv_back, -3);
i8_conv_back = i8::from_be_bytes(raw[1..2].try_into().unwrap());
assert_eq!(i8_conv_back, -16);
}
#[test]
fn basic_signed_test_triplet() {
let i8_triplet = I8Triplet(-3, -16, -126);
assert_eq!(i8_triplet.0, -3);
assert_eq!(i8_triplet.1, -16);
assert_eq!(i8_triplet.2, -126);
let raw = i8_triplet.to_be_bytes();
let mut i8_conv_back = i8::from_be_bytes(raw[0..1].try_into().unwrap());
assert_eq!(i8_conv_back, -3);
i8_conv_back = i8::from_be_bytes(raw[1..2].try_into().unwrap());
assert_eq!(i8_conv_back, -16);
i8_conv_back = i8::from_be_bytes(raw[2..3].try_into().unwrap());
assert_eq!(i8_conv_back, -126);
}
#[test]
fn conversion_test_string() {
let param: Params = "Test String".into();
if let Params::String(str) = param {
assert_eq!(str, String::from("Test String"));
} else {
panic!("Params type is not String")
}
}
#[test]
fn conversion_from_slice() {
let test_slice: [u8; 5] = [0; 5];
let vec_param: Params = test_slice.as_slice().into();
if let Params::Vec(vec) = vec_param {
assert_eq!(vec, test_slice.to_vec());
} else {
panic!("Params type is not a vector")
}
}
}

Some files were not shown because too many files have changed in this diff Show More