From 412f7bc24dcc26c9631a17cdb205dc00a8c1b9db Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sun, 9 Apr 2017 16:18:03 +0200 Subject: [PATCH] updated documentation --- doc/Configuration.md | 54 ++++++++- doc/DesignGoals.md | 14 +++ doc/GettingStarted.md | 23 ++-- doc/Install.md | 51 ++++++--- doc/RemoteAPI.md | 124 --------------------- doc/Tuning.md | 38 ++++--- doc/Usage.md | 114 +++++++------------ doc/{ => development}/AdvancedIO.md | 0 doc/{ => development}/Development.md | 0 doc/development/RemoteAPI.md | 157 +++++++++++++++++++++++++++ doc/figures/path_states.dia | Bin 1488 -> 0 bytes 11 files changed, 334 insertions(+), 241 deletions(-) create mode 100644 doc/DesignGoals.md delete mode 100644 doc/RemoteAPI.md rename doc/{ => development}/AdvancedIO.md (100%) rename doc/{ => development}/Development.md (100%) create mode 100644 doc/development/RemoteAPI.md delete mode 100644 doc/figures/path_states.dia diff --git a/doc/Configuration.md b/doc/Configuration.md index 3fd3dd6f6..be48d347a 100644 --- a/doc/Configuration.md +++ b/doc/Configuration.md @@ -35,23 +35,65 @@ This technique, also called 'pinning', improves the determinism of the server by The `priority` setting allows to adjust the scheduling priority of the deamon processes. By default, the daemon uses a real-time optimized FIFO scheduling algorithm. -#### `name` *(integer)* +#### `name` *(string)* By default the `name` of a VILLASnode instance is equalt to the hostname of the machine it is running on. Some node types are using this name to identify themselves agains their remotes. An example is the `ngsi` node type which adds a metadata attribute `source` to its updates. +## Log + +``` +log = { + file = "/var/log/villas-node.log"; + level = 5; + facilities = "socket,log,mem"; // log socket node-type, log and memory sub-system + facilities = "nodes,!websocket"; // log all node-type but not the websocket node-type + //facilities = "all,!nodes"; // log everything but not the node-types +} +``` + +#### Available logging facilities + +| Facility | Description | +|:- |:- | +| `pool` | Memory Pool for fixed size allocations | +| `queue` | Multiple-producer / Multiple-consumer queue | +| `config` | Configuration parser | +| `hook` | Hook sub-system | +| `path` | Send / Receive path | +| `node` | Node-types | +| `mem` | Memory management | +| `web` | Web sub-system | +| `api` | Remote API | +| `log` | Logging sub-system | +| `vfio` | Virtual Function Input/Output sub-system for access to PCI devices | +| `pci` | PCI device detection | +| `xil` | Xilinx drivers for FPGA components | +| `tc` | Linux traffic control for network emulation | +| `if` | Linux network interfaces | +| `socket` | BSD Socket node-type | +| `file` | File node-type | +| `fpga` | VILLASfpga node-type | +| `ngsi` | FIWARE NGSI node-type | +| `websocket` | WebSocket node-type | +| `opal` | OPAL-RT node-type | + +## Web + ## Nodes The node section is a **directory** of nodes (clients) which are connected to the VILLASnode instance. The directory is indexed by the name of the node: - nodes = { - "sintef_node" = { - type = "socket" - .... +``` +nodes = { + "sintef_node" = { + type = "socket"; + ... } - } +} +``` There are multiple diffrent type of nodes. But all types have the following settings in common: diff --git a/doc/DesignGoals.md b/doc/DesignGoals.md new file mode 100644 index 000000000..1d42abb67 --- /dev/null +++ b/doc/DesignGoals.md @@ -0,0 +1,14 @@ +# Design Goals {#designgoals} + +VILLASnode ... + +- is written in C the programming language +- is using features of the C11 standard +- is using an object oriented programming paradigm +- can be compiled with Clang / LLVM or GCC +- is released under the LGPLv2 license +- only relies on open source software libraries and the Linux kernel +- is extensible with new node types & hooks +- is heavily multi-threaded +- follows the Unix philosophy +- is separated into a library (libvillas) and a few binaries (villas-server, villas-pipe, villas-test-*, villas-signal, villas-hook) which link against the lib. \ No newline at end of file diff --git a/doc/GettingStarted.md b/doc/GettingStarted.md index 78201b0b9..2e6d6f8ab 100644 --- a/doc/GettingStarted.md +++ b/doc/GettingStarted.md @@ -1,9 +1,14 @@ # Getting started +## Installation + We put some effort in getting you started as smooth as possible. For first tests and development you can use the Docker platform to bootstrap your environment. Docker is a software to run containers (a.k.a images in Docker's terminology) on a Linux machine. +We use for development as well as for testing or demonstrating VILLASnode's functionality. + +**Note:** Please be aware that we do not recommend to use Docker for running VILLASnode in a real-time simulation. We prepared a image which you can download and run out of the box: @@ -15,18 +20,12 @@ We prepared a image which you can download and run out of the box: 3. Start the latest VILLASnode container by running: - $ git clone --recursive git@git.rwth-aachen.de:VILLASframework/VILLASnode.git - $ cd VILLASnode - $ make docker +``` +$ docker run rwth-acs/villas-node --help +``` -### To be added -VILLASnode ... +@todo Add a ASCIIcinema screencast here + +## Guide -- is written in object-oriented C11 -- is compiled with Clang / LLVM or GCC -- is fully based on open source software -- is extensible with new node types & hooks -- is heavily multi-threaded -- follows the Unix philosophy -- is separated into a library (libvillas) and a few binaries (villas-server, villas-pipe, villas-test-*, villas-signal, villas-hook) which link against the lib. \ No newline at end of file diff --git a/doc/Install.md b/doc/Install.md index 45833121e..696fb462f 100644 --- a/doc/Install.md +++ b/doc/Install.md @@ -1,8 +1,14 @@ # Setup +VILLASnode can be installed in multiple ways: + +- Using a pre-build Docker image +- Using pre-build RPM packages for Redhat based Linux distributions +- or from source + ## Prerequisites -For all features VILLASnode currently requires the following list of dependencies: +VILLASnode currently has the following list of dependencies: - [libconfig](http://www.hyperrealm.com/libconfig/) for parsing the configuration file. - [libnl3](http://www.infradead.org/~tgr/libnl/) for the network communication & emulation support of the `socket` node-type. @@ -10,6 +16,7 @@ For all features VILLASnode currently requires the following list of dependencie - [libjansson](http://www.digip.org/jansson/) JSON parser for `websocket` and `ngsi` node-types. - [libwebsockets](http://libwebsockets.org) for the `websocket` node-type. - [libcurl](https://curl.haxx.se/libcurl/) for HTTP REST requests by the `ngsi` node-type. + - [openssl]() There are two ways to install these dependencies: @@ -17,18 +24,28 @@ There are two ways to install these dependencies: Use the following command to install the dependencies under Debian-based distributions: - $ sudo apt-get install build-essential pkg-config wget tar cmake doxygen dia graphviz libconfig-dev libnl-3-dev libnl-route-3-dev libjansson-dev libcurl4-openssl-dev +``` +$ sudo apt-get install build-essential pkg-config wget tar cmake doxygen dia graphviz libconfig-dev libnl-3-dev libnl-route-3-dev libjansson-dev libcurl4-openssl-dev +``` or the following line for Fedora / CentOS / Redhat systems: - $ sudo yum install gcc pkgconfig make wget tar cmake openssl-devel doxygen dia graphviz libconfig-devel libnl3-devel libcurl-devel jansson-devel - - 2. Alternatively, you can use the make targets `make thirdparty` and `make install-thirdparty` which will compile and install all required dependencies from source. - -## Fetching VILLASnode +``` +$ sudo yum install gcc pkgconfig make wget tar cmake openssl-devel doxygen dia graphviz libconfig-devel libnl3-devel libcurl-devel jansson-devel +``` - $ git clone --recursive git@git.rwth-aachen.de:VILLASframework/VILLASnode.git - $ cd VILLASnode + 2. Alternatively, you can use the build system to download, compile and install all dependencies: + +``` +$ make install-thirdparty +``` + +## Downloading VILLASnode + +``` +$ git clone --recursive git@git.rwth-aachen.de:VILLASframework/VILLASnode.git +$ cd VILLASnode +``` ## Compilation @@ -36,7 +53,10 @@ Checkout the `Makefile` and `include/config.h` for some options which have to be Afterwards, start the compilation with: - $ make +``` +$ make +$ make run-tests +``` Append `V=5` to `make` for a more verbose debugging output. Append `DEBUG=1` to `make` to add debug symbols. @@ -45,7 +65,10 @@ Append `DEBUG=1` to `make` to add debug symbols. Install the files to your search path: - $ make install +``` +$ make install +$ make install-doc +``` Append `PREFIX=/opt/local` to change the installation destination. @@ -53,6 +76,8 @@ Append `PREFIX=/opt/local` to change the installation destination. Verify everything is working and required node-types are compiled-in: - $ villas node +``` +$ villas node --help +``` -Will show you the current version of the server including a list of all supported node-types. +Will print the current version including a list of all supported node-types, hooks, etc. diff --git a/doc/RemoteAPI.md b/doc/RemoteAPI.md deleted file mode 100644 index 59fbd80d5..000000000 --- a/doc/RemoteAPI.md +++ /dev/null @@ -1,124 +0,0 @@ -#Remote Application Programming Interface (API) {#API} - -VILLASnode can be controlled remotely over a HTTP REST / WebSocket API. -This page documents this API. - -## Transports - -The API is accessible via multiple transports: - -- via HTTP POST requests -- via a WebSocket protocol -- via a Unix socket - -All transports use the same JSON based protocol to handle requests. - -### HTTP REST - -**Endpoint URL:** http[s]://localhost:80/api/v1 -**HTTP Method:** POST - -### WebSockets - -**WebSocket Protocol:** `api` - -### Unix socket - -_This transport is not implemented yet_ - -## Commands - -### `restart` - -Restart VILLASnode with a new configuration file. - -**Request:** _application/json_ - - { - "request": "restart", - "id": "66675eb4-6a0b-49e6-8e82-64d2b2504e7a" - "args" : { - "configuration": "smb://MY-WINDOWS-HOST/SHARE1/path/to/config.conf" - } - } - -**Response:** _application/json_ - - { - "response": "reload", - "id": "66675eb4-6a0b-49e6-8e82-64d2b2504e7a" - } - -### `config` - -Retrieve the contents of the current configuration. - -**Request:** _application/json_ - - { - "request": "config", - "id": "66675eb4-6a0b-49e6-8e82-64d2b2504e7a" - } - -**Response:** _application/json_ - - { - "response": "config", - "id": "66675eb4-6a0b-49e6-8e82-64d2b2504e7a", - "response" : { - "nodes" : { - "socket_node" : { - "type": "socket", - "layer": "udp", - ... - } - }, - "paths" : [ - { - "in": "socket_node", - "out": "socket_node" - } - ] - } - } - -### `nodes` - -Get a list of currently active nodes. - -**Request:** _application/json_ - - { - "request": "nodes", - "id": "5a786626-fbc6-4c04-98c2-48027e68c2fa" - } - -**Response:** _application/json_ - - { - "command": "nodes", - "response": [ - { - "name": "ws", - "state": 4, - "vectorize": 1, - "affinity": 0, - "id": 0, - "type": "websocket", - "description": "Demo Channel" - } - ], - "id": "5a786626-fbc6-4c04-98c2-48027e68c2fa" - } - -### `paths` - -Get a list of currently active paths. - -_This request is not implemented yet_ - -### `status` - -The the status of this VILLASnode instance. - -_This request is not implemented yet_ \ No newline at end of file diff --git a/doc/Tuning.md b/doc/Tuning.md index 6d1f2e679..a0b3338ad 100644 --- a/doc/Tuning.md +++ b/doc/Tuning.md @@ -1,12 +1,19 @@ # Tuning {#tuning} +This page is not directly related to VILLASnode. +It describes several ways to optimize the host system for running real-time applications like VILLASnode. + ## Operating System and Kernel For minimum latency several kernel and driver settings can be optimized. A [PREEMPT_RT patched Linux](https://rt.wiki.kernel.org/index.php/Main_Page) kernel is recommended. Precompiled kernels for Fedora can be found here: http://ccrma.stanford.edu/planetccrma/software/ -1. **Important:** Tune overall system performance for real-time: +1. Tune overall system performance for real-time: + +1.1. Use the `tuned` tool for imprving general real-time performance. + +Please adjust the setting `isolated_cpucores` according to your hardware. ```bash $ dnf install tuned-profiles-realtime @@ -14,25 +21,28 @@ $ echo "realtime" > /etc/tuned/active_profile $ echo "isolated_cpucores=6-7" >> /etc/tuned/realtime-variables.conf $ systemctl enable tuned && systemctl start tuned ``` - - This enables the following `tuned` profiles: - - latency-performance - - network-latency - - realtime -2. Map NIC IRQs => see setting `affinity` -3. Map Tasks => see setting `affinity` +This enables the following `tuned` profiles: -4. Increase priority of server task (nice(2)) => see setting `priority` -5. Increase BSD socket priority => see setting `priority` and node-type `socket` + - latency-performance + - network-latency + - realtime -6. Configure NIC interrupt coalescence with `ethtool`: +1.2 Install a `PREEMPT_RT` patched Linux kernel. + +2. Optimize the VILLASnode configuration +2.1. Map NIC IRQs (see global setting `affinity`) +2.2. Map Tasks (see global setting `affinity`) +2.3. Increase priority of server task (nice(2)) (see global setting `priority`) +2.4. Increase BSD socket priority (see global setting `priority` for node-type `socket`) + +3. Configure NIC interrupt coalescence with `ethtool`: ```bash -$ ethtool -C|--coalesce devname [adaptive-rx on|off] [adaptive-tx on|off] ... +$ ethtool --coalesce eth0 adaptive-rx off adaptive-tx off ``` -7. Configure NIC kernel driver in `/etc/modprobe.d/e1000e.conf`: +4. Configure NIC kernel driver in `/etc/modprobe.d/e1000e.conf`: ``` # More conservative interrupt throttling for better latency @@ -45,7 +55,7 @@ option e1000e InterruptThrottleRate= This are some proposals for the selection of appropriate server hardware: - Server-grade CPU: Intel Xeon - - A multi-core systems allows parallization of send/receive paths. + - A multi-core systems enable true paralell execution of multiple send / receive paths. - Server-grade network cards: Intel PRO/1000 - These allow offloading of UDP checksumming to the hardware diff --git a/doc/Usage.md b/doc/Usage.md index 45fb7f46b..a3ab2ca28 100644 --- a/doc/Usage.md +++ b/doc/Usage.md @@ -1,86 +1,56 @@ # Usage {#usage} -The core of VILLASnode is the `villas-node` server. +# `villas signal` + +# `villas pipe` + +# `villas hook` + +## `villas node` + +The core of VILLASnode is the `villas-node` daemon. The folling usage information is provided when called like `villas-node --help`; - Usage: villas-node [CONFIG] - CONFIG is the path to an optional configuration file - if omitted, VILLASnode will start without a configuration - and wait for provisioning over the web interface. +``` +Usage: villas-node [CONFIG] + CONFIG is the path to an optional configuration file + if omitted, VILLASnode will start without a configuration + and wait for provisioning over the web interface. - Supported node types: - - file : support for file log / replay node type - - cbuilder : RTDS CBuilder model - - socket : BSD network sockets - - fpga : VILLASfpga PCIe card (libxil) - - ngsi : OMA Next Generation Services Interface 10 (libcurl, libjansson) - - websocket : Send and receive samples of a WebSocket connection (libwebsockets) +Supported node types: + - file : support for file log / replay node type + - cbuilder : RTDS CBuilder model + - socket : BSD network sockets + - fpga : VILLASfpga PCIe card (libxil) + - ngsi : OMA Next Generation Services Interface 10 (libcurl, libjansson) + - websocket : Send and receive samples of a WebSocket connection (libwebsockets) - Supported hooks: - - restart : Call restart hooks for current path - - print : Print the message to stdout - - decimate : Downsamping by integer factor - - fix_ts : Update timestamps of sample if not set - - skip_first : Skip the first samples - - drop : Drop messages with reordered sequence numbers - - convert : Convert message from / to floating-point / integer - - shift : Shift the origin timestamp of samples - - ts : Update timestamp of message with current time - - stats : Collect statistics for the current path - - stats_send : Send path statistics to another node +Supported hooks: + - restart : Call restart hooks for current path + - print : Print the message to stdout + - decimate : Downsamping by integer factor + - fix_ts : Update timestamps of sample if not set + - skip_first : Skip the first samples + - drop : Drop messages with reordered sequence numbers + - convert : Convert message from / to floating-point / integer + - shift : Shift the origin timestamp of samples + - ts : Update timestamp of message with current time + - stats : Collect statistics for the current path + - stats_send : Send path statistics to another node - Supported API commands: - - nodes : retrieve list of all known nodes - - config : retrieve current VILLASnode configuration - - reload : restart VILLASnode with new configuration +Supported API commands: + - nodes : retrieve list of all known nodes + - config : retrieve current VILLASnode configuration + - reload : restart VILLASnode with new configuration - VILLASnode v0.7-0.2-646-g59756e7-dirty-debug (built on Mar 12 2017 21:37:40) - copyright 2014-2016, Institute for Automation of Complex Power Systems, EONERC - Steffen Vogel +VILLASnode v0.7-0.2-646-g59756e7-dirty-debug (built on Mar 12 2017 21:37:40) + copyright 2014-2016, Institute for Automation of Complex Power Systems, EONERC + Steffen Vogel +``` The server requires root privileges for: - Enable the realtime FIFO scheduler - Increase the task priority - Configure the network emulator (netem) - - Change the SMP affinity of threads and network interrupts - -## Step-by-step - -1. Start putty.exe (SSH client for Windows) - - - Should be already installed on most systems - - Or load .exe from this website (no installation required) - http://the.earth.li/~sgtatham/putty/latest/x86/putty.exe - -2. Connect to VILLASnode - -| Setting | Value | -| :------- | :-------------- | -| IP | 130.134.169.31 | -| Port | 22 | -| Protocol | SSH | -| User | root | -| Password | *please ask msv | - -3. Go to VILLASnode directory - - $ cd /villas/ - -4. Edit configuration file - - $ nano etc/loopback.conf - - - Take a look at the examples and documentation for a detailed description - - Move with: cursor keys - - Save with: Ctrl+X => y => Enter - -5. Start server - - $ villas node etc/loopback.conf - -6. Terminate server by pressing Ctrl+C - -7. Logout - - $ exit + - Change the SMP affinity of threads and network interrupts \ No newline at end of file diff --git a/doc/AdvancedIO.md b/doc/development/AdvancedIO.md similarity index 100% rename from doc/AdvancedIO.md rename to doc/development/AdvancedIO.md diff --git a/doc/Development.md b/doc/development/Development.md similarity index 100% rename from doc/Development.md rename to doc/development/Development.md diff --git a/doc/development/RemoteAPI.md b/doc/development/RemoteAPI.md new file mode 100644 index 000000000..7096cd9f1 --- /dev/null +++ b/doc/development/RemoteAPI.md @@ -0,0 +1,157 @@ +#Remote Application Programming Interface (API) {#API} + +VILLASnode can be controlled remotely with an Application Programming Interface (API). + +## Transports + +The API is accessible via multiple transports: + +- HTTP POST requests +- WebSockets +- Unix Domain sockets (planned) + +### HTTP REST + +**Endpoint URL:** `http[s]://server:port/api/v1` + +**HTTP Method:** POST only + +### WebSockets + +**Protocol:** `api` + +**Endpoint:** `ws[s]://server:port/v1` + +### Unix socket + +_This transport is not implemented yet_ + +## Protocol + +All transports use the same JSON-based protocol to encode API requests and responses. +Both requests and responses are JSON objects with the fields described below. +Per API session multiple requests can be sent to the node. +The order of the respective responses does not necessarily match the order of the requests. +Requests and responses can be interleaved. +The client must check the `id` field in order to find the matching response to a request. + +#### Request + +| Field | Description | +|:------------------------ |:---------------------- | +| `action` | The API action which is requested. See next section. | +| `id` | A unique string which identifies the request. The same id will be used for the response. | +| `request` | Any JSON data-type which will be passed as an argument to the respective API action. | + +#### Response + +The response is similar to the request object. +The `id` field of the request will be copied to allow + +| Field | Description | +|:------------------------ |:---------------------- | +| `action` | The API action which is requested. See next section. | +| `id` | A unique string which identifies the request. The same id will be used for the response. | +| `response` | Any JSON data-type which can be returned. | + +## Actions + +### `restart` + +Restart VILLASnode with a new configuration file. + +**Request:** _application/json_ + + { + "action": "restart", + "id": "66675eb4-6a0b-49e6-8e82-64d2b2504e7a" + "request" : { + "configuration": "smb://MY-WINDOWS-HOST/SHARE1/path/to/config.conf" + } + } + +**Response:** _application/json_ + + { + "action": "reload", + "id": "66675eb4-6a0b-49e6-8e82-64d2b2504e7a" + "response" : "success" + } + +### `config` + +Retrieve the contents of the current configuration. + +**Request:** _application/json_ + + { + "action": "config", + "id": "66675eb4-6a0b-49e6-8e82-64d2b2504e7a" + } + +**Response:** _application/json_ + + { + "action": "config", + "id": "66675eb4-6a0b-49e6-8e82-64d2b2504e7a", + "response" : { + "nodes" : { + "socket_node" : { + "type": "socket", + "layer": "udp", + ... + } + }, + "paths" : [ + { + "in": "socket_node", + "out": "socket_node" + } + ] + } + } + +### `nodes` + +Get a list of currently active nodes. + +**Request:** _application/json_ + + { + "action": "nodes", + "id": "5a786626-fbc6-4c04-98c2-48027e68c2fa" + } + +**Response:** _application/json_ + + { + "action": "nodes", + "response": [ + { + "name": "ws", + "state": 4, + "vectorize": 1, + "affinity": 0, + "id": 0, + "type": "websocket", + "description": "Demo Channel" + } + ], + "id": "5a786626-fbc6-4c04-98c2-48027e68c2fa" + } + +### `capabilities` + +Get a list of supported node-types, hooks and API actions. + +### `paths` + +Get a list of currently active paths. + +_This request is not implemented yet_ + +### `status` + +The the status of this VILLASnode instance. + +_This request is not implemented yet_ \ No newline at end of file diff --git a/doc/figures/path_states.dia b/doc/figures/path_states.dia deleted file mode 100644 index 804951d9fb672220d871b531bc39f8c50cc1ac35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1488 zcmV;>1uyy^iwFP!000021MOQ~Z=*OAeebV8>C3Lv5^S)6>9nJ2x1*^vyDRN<=QVP` zCEg)`jFU7k``gzhG->!qnh=<&hDc2nV(zgo_BrPo^8UlqG}Pvlr!0=H2S7Il8jZZz zXHjrH_~+r{r8D?&bN1e6V5 zg=6U`iMil}i8ACkjzdbK6{_GfdOS|*kx)*tUBhZHibp0yyr=(eI z557YYMTg5nFk?PV_e`_xHZ_qZd9&fZtQXt8HZiM11+mT`Lb9Yh5C2JCI?4mCbXv== za85ZI@dTSce|nLWc>FK*L_XS&VZ87rgbVFby9+~>$hE$4BGQL2|9bG3v7NHwAZ0|! zhNYY(@n&LK7r-ngp-drl!N=obY%YgDq9CMsd{cMP1%UGX3OG8%Py%Az!G?1jDh=s$ z^kJf?ndKA(q*rl$9j+#R3(eGCysF@)rpj<6!Nok(H(I#?vItiIPa-^zrQ zrb)%QAU_vupNq>|9fb~^&P$q2S-@=BN^YE_t&F6JyKs_bZzOvoc{(F`|L51+&-eYN zMC(pc8AuGA-br@qB(3KA5xH%`Iz-U!!bwUb$-f&XY3q)K?3SHm?KpRs&L%18o#b0| zk_9&ij65pR}fcd_QST0iEn8)sAxqTlD)$KN|&@4fd0aHujTV5Ws_~i(fo! zn=^pj*aXOw%0=1Y%6H^?a%>4==$3)50Mtzfn2HDh`tkP>5+Wuexs18^<=1{Qq4g&m zinFobL^x<=%(E`lYrwu8*3*!Za~e`|+a)C%sD~GvH7P;Ik(h6Zl;Wi4q^>y0?K$af zaMEWMRo!%Rn{tw}>KC#&Xp@x`1~Lq+a8zAzW$FN|_SPZBsO2K5VuY2q>0}E79a*Qy ztL`PmK2$x(-xog}6+bmw7*0n83~2n2KFp@=(dmEPJq%Zez;rHLHTAEcsoM_DN*L=6 zxP>pOdeQ!%X-WcmCPzluocg_SX#E+x^2F?|!y7DT05}arxh)eER?l;;>KVmQrkT|< q?weq!IOz%MI}uc0+59Nz9-_Xa56Q=F=H%K>z@fUDaU#