From 2d7d18b34d027883546e6358e93c6ae9ba4f98f9 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Tue, 28 Jan 2014 08:47:16 +0000 Subject: [PATCH 01/59] Added coding framework for X11 Former-commit-id: 2915defe12e23d328716eade251849d40d098a8a --- CMakeLists.txt | 11 ++++--- src/CMakeLists.txt | 11 +++++-- src/hyperion-x11/CMakeLists.txt | 55 +++++++++++++++++++++++++++++++++ src/hyperion-x11/X11Grabber.cpp | 14 +++++++++ src/hyperion-x11/X11Grabber.h | 0 5 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 src/hyperion-x11/CMakeLists.txt create mode 100644 src/hyperion-x11/X11Grabber.cpp create mode 100644 src/hyperion-x11/X11Grabber.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b6b9ed3c..36fc2e56 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,15 +7,18 @@ cmake_minimum_required(VERSION 2.8) #set(CMAKE_TOOLCHAIN_FILE /opt/raspberrypi/Toolchain-RaspberryPi.cmake) # set the build options -option (ENABLE_DISPMANX "Enable the RPi dispmanx grabber" ON) -option (ENABLE_SPIDEV "Enable the SPIDEV device" ON) - +option(ENABLE_DISPMANX "Enable the RPi dispmanx grabber" ON) message(STATUS "ENABLE_DISPMANX = " ${ENABLE_DISPMANX}) + +option(ENABLE_SPIDEV "Enable the SPIDEV device" ON) message(STATUS "ENABLE_SPIDEV = " ${ENABLE_SPIDEV}) -option (ENABLE_V4L2 "Enable the V4L2 grabber" ON) +option(ENABLE_V4L2 "Enable the V4L2 grabber" ON) message(STATUS "ENABLE_V4L2 = " ${ENABLE_V4L2}) +option(ENABLE_X11 "Enable the X11 grabber" ON) +message(STATUS "ENABLE_X11 = " ${ENABLE_X11}) + # Createt the configuration file # configure a header file to pass some of the CMake settings # to the source code diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b06b4ee7..16c88d77 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,5 +1,12 @@ add_subdirectory(hyperiond) add_subdirectory(hyperion-remote) -if (ENABLE_V4L2) + +# Add the 'Video 4 Linux' grabber if it is enabled +if(ENABLE_V4L2) add_subdirectory(hyperion-v4l2) -endif (ENABLE_V4L2) +endif(ENABLE_V4L2) + +# Add the X11 grabber if it is enabled +if(ENABLE_X11) + add_subdirectory(hyperion-x11) +endif(ENABLE_X11) diff --git a/src/hyperion-x11/CMakeLists.txt b/src/hyperion-x11/CMakeLists.txt new file mode 100644 index 00000000..5b4fd821 --- /dev/null +++ b/src/hyperion-x11/CMakeLists.txt @@ -0,0 +1,55 @@ +# Configure minimum CMAKE version +cmake_minimum_required(VERSION 2.8) + +# Set the project name +project(hyperion-x11) + +# add protocol buffers +find_package(Protobuf REQUIRED) + +# find Qt4 +find_package(Qt4 REQUIRED QtCore QtGui QtNetwork) + +include_directories( + ${CMAKE_CURRENT_BINARY_DIR} + ${PROTOBUF_INCLUDE_DIRS} + ${QT_INCLUDES} +) + +set(Hyperion_X11_HEADERS + X11Grabber.h + ../hyperion-v4l2/ProtoConnection.h +) + +set(Hyperion_X11_SOURCES + X11Grabber.cpp + ../hyperion-v4l2/ProtoConnection.cpp +) + +set(Hyperion_X11_PROTOS + ${CMAKE_CURRENT_SOURCE_DIR}/../../libsrc/protoserver/message.proto +) + +protobuf_generate_cpp(Hyperion_X11_PROTO_SRCS Hyperion_X11_PROTO_HDRS + ${Hyperion_X11_PROTOS} +) + +add_executable(hyperion-x11 + ${Hyperion_X11_HEADERS} + ${Hyperion_X11_SOURCES} + ${Hyperion_X11_PROTO_SRCS} + ${Hyperion_X11_PROTO_HDRS} +) + +target_link_libraries(hyperion-x11 + getoptPlusPlus + blackborder + hyperion-utils + ${PROTOBUF_LIBRARIES} + pthread +) + +qt4_use_modules(hyperion-x11 + Core + Gui + Network) diff --git a/src/hyperion-x11/X11Grabber.cpp b/src/hyperion-x11/X11Grabber.cpp new file mode 100644 index 00000000..d1331743 --- /dev/null +++ b/src/hyperion-x11/X11Grabber.cpp @@ -0,0 +1,14 @@ + +#include + +int main(int argc, char ** argv) +{ + Display * dspl; + Drawable drwbl; + int x = 0; + int y = 0; + int width = 0; + int height = 0; + + XImage * xImage = XGetImage(dspl, drwbl, x, y, width, height, AllPlanes, ZPixmap); +} diff --git a/src/hyperion-x11/X11Grabber.h b/src/hyperion-x11/X11Grabber.h new file mode 100644 index 00000000..e69de29b From 8e446bf8835c1126ab39c1d45b0d9d1466d7d726 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Thu, 30 Jan 2014 12:35:29 +0000 Subject: [PATCH 02/59] Implemented the X11-grabber (based on v4l code). Former-commit-id: 17ad66ff7e2a1e3fae2d853e5360f791934b6563 --- src/hyperion-x11/CMakeLists.txt | 19 ++++- src/hyperion-x11/ProtoWrapper.cpp | 17 +++++ src/hyperion-x11/ProtoWrapper.h | 23 ++++++ src/hyperion-x11/X11Grabber.cpp | 121 +++++++++++++++++++++++++++--- src/hyperion-x11/X11Grabber.h | 37 +++++++++ src/hyperion-x11/X11Wrapper.cpp | 33 ++++++++ src/hyperion-x11/X11Wrapper.h | 39 ++++++++++ src/hyperion-x11/hyperion-x11.cpp | 90 ++++++++++++++++++++++ 8 files changed, 366 insertions(+), 13 deletions(-) create mode 100644 src/hyperion-x11/ProtoWrapper.cpp create mode 100644 src/hyperion-x11/ProtoWrapper.h create mode 100644 src/hyperion-x11/X11Wrapper.cpp create mode 100644 src/hyperion-x11/X11Wrapper.h create mode 100644 src/hyperion-x11/hyperion-x11.cpp diff --git a/src/hyperion-x11/CMakeLists.txt b/src/hyperion-x11/CMakeLists.txt index 5b4fd821..43cc9a1d 100644 --- a/src/hyperion-x11/CMakeLists.txt +++ b/src/hyperion-x11/CMakeLists.txt @@ -10,19 +10,30 @@ find_package(Protobuf REQUIRED) # find Qt4 find_package(Qt4 REQUIRED QtCore QtGui QtNetwork) +# Find X11 +find_package(X11 REQUIRED) + include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${PROTOBUF_INCLUDE_DIRS} ${QT_INCLUDES} + ${X11_INCLUDES} ) +set(Hyperion_X11_QT_HEADERS + ProtoWrapper.h + X11Wrapper.h) + set(Hyperion_X11_HEADERS X11Grabber.h ../hyperion-v4l2/ProtoConnection.h ) set(Hyperion_X11_SOURCES + hyperion-x11.cpp + ProtoWrapper.cpp X11Grabber.cpp + X11Wrapper.cpp ../hyperion-v4l2/ProtoConnection.cpp ) @@ -30,15 +41,16 @@ set(Hyperion_X11_PROTOS ${CMAKE_CURRENT_SOURCE_DIR}/../../libsrc/protoserver/message.proto ) -protobuf_generate_cpp(Hyperion_X11_PROTO_SRCS Hyperion_X11_PROTO_HDRS - ${Hyperion_X11_PROTOS} -) +QT4_WRAP_CPP(Hyperion_X11_HEADERS_MOC ${Hyperion_X11_QT_HEADERS}) + +protobuf_generate_cpp(Hyperion_X11_PROTO_SRCS Hyperion_X11_PROTO_HDRS ${Hyperion_X11_PROTOS}) add_executable(hyperion-x11 ${Hyperion_X11_HEADERS} ${Hyperion_X11_SOURCES} ${Hyperion_X11_PROTO_SRCS} ${Hyperion_X11_PROTO_HDRS} + ${Hyperion_X11_HEADERS_MOC} ) target_link_libraries(hyperion-x11 @@ -46,6 +58,7 @@ target_link_libraries(hyperion-x11 blackborder hyperion-utils ${PROTOBUF_LIBRARIES} + ${X11_LIBRARIES} pthread ) diff --git a/src/hyperion-x11/ProtoWrapper.cpp b/src/hyperion-x11/ProtoWrapper.cpp new file mode 100644 index 00000000..89710257 --- /dev/null +++ b/src/hyperion-x11/ProtoWrapper.cpp @@ -0,0 +1,17 @@ + +// +#include "ProtoWrapper.h" + +ProtoWrapper::ProtoWrapper(const std::string & protoAddress, const bool skipReply) : + _priority(200), + _duration_ms(2000), + _connection(protoAddress) +{ + _connection.setSkipReply(skipReply); +} + +void ProtoWrapper::process(const Image & image) +{ + std::cout << "Attempt to send screenshot" << std::endl; + _connection.setImage(image, _priority, _duration_ms); +} diff --git a/src/hyperion-x11/ProtoWrapper.h b/src/hyperion-x11/ProtoWrapper.h new file mode 100644 index 00000000..4d184868 --- /dev/null +++ b/src/hyperion-x11/ProtoWrapper.h @@ -0,0 +1,23 @@ + +// QT includes +#include + + +#include "../hyperion-v4l2/ProtoConnection.h" + +class ProtoWrapper : public QObject +{ + Q_OBJECT +public: + ProtoWrapper(const std::string & protoAddress, const bool skipReply); + +public slots: + void process(const Image & image); + +private: + + int _priority; + int _duration_ms; + + ProtoConnection _connection; +}; diff --git a/src/hyperion-x11/X11Grabber.cpp b/src/hyperion-x11/X11Grabber.cpp index d1331743..cd6d0fda 100644 --- a/src/hyperion-x11/X11Grabber.cpp +++ b/src/hyperion-x11/X11Grabber.cpp @@ -1,14 +1,115 @@ +// STL includes +#include +#include -#include +// X11 includes +#include -int main(int argc, char ** argv) +// X11Grabber includes +#include "X11Grabber.h" + +X11Grabber::X11Grabber(const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation) : + _pixelDecimation(pixelDecimation), + _cropWidth(cropHorizontal), + _cropHeight(cropVertical), + _x11Display(nullptr), + _screenWidth(0), + _screenHeight(0), + _image(0,0) { - Display * dspl; - Drawable drwbl; - int x = 0; - int y = 0; - int width = 0; - int height = 0; - - XImage * xImage = XGetImage(dspl, drwbl, x, y, width, height, AllPlanes, ZPixmap); + // empty +} + +X11Grabber::~X11Grabber() +{ + if (_x11Display != nullptr) + { + XCloseDisplay(_x11Display); + } +} + +int X11Grabber::open() +{ + const char * display_name = nullptr; + _x11Display = XOpenDisplay(display_name); + + if (_x11Display == nullptr) + { + std::cerr << "Failed to open the default X11-display" << std::endl; + return -1; + } + + return 0; +} + +Image & X11Grabber::grab() +{ + if (_x11Display == nullptr) + { + open(); + } + + updateScreenDimensions(); + + const int croppedWidth = _screenWidth - 2*_cropWidth; + const int croppedHeight = _screenHeight - 2*_cropHeight; + + // Capture the current screen + XImage * xImage = XGetImage(_x11Display, DefaultRootWindow(_x11Display), _cropWidth, _cropHeight, croppedWidth, croppedHeight, AllPlanes, ZPixmap); + if (xImage == nullptr) + { + std::cerr << "Grab failed" << std::endl; + return _image; + } + + // Copy the capture XImage to the local image (and apply required decimation) + ColorRgb * outputPtr = _image.memptr(); + for (int iY=(_pixelDecimation/2); iYred = uint8_t((pixel >> 16) & 0xff); + outputPtr->green = uint8_t((pixel >> 8) & 0xff); + outputPtr->blue = uint8_t((pixel >> 0) & 0xff); + + // Move to the next output pixel + ++outputPtr; + } + } + // Cleanup allocated resources of the X11 grab + XDestroyImage(xImage); + + return _image; +} + +int X11Grabber::updateScreenDimensions() +{ + XWindowAttributes window_attributes_return; + const Status status = XGetWindowAttributes(_x11Display, DefaultRootWindow(_x11Display), &window_attributes_return); + if (status == 0) + { + std::cerr << "Failed to obtain window attributes" << std::endl; + return -1; + } + + if (_screenWidth == unsigned(window_attributes_return.width) && _screenHeight == unsigned(window_attributes_return.height)) + { + // No update required + return 0; + } + std::cout << "Update of screen resolution: [" << _screenWidth << "x" << _screenHeight <<"] => "; + _screenWidth = window_attributes_return.width; + _screenHeight = window_attributes_return.height; + std::cout << "[" << _screenWidth << "x" << _screenHeight <<"]" << std::endl; + + // Update the size of the buffer used to transfer the screenshot + int width = (_screenWidth - 2 * _cropWidth + _pixelDecimation/2) / _pixelDecimation; + int height = (_screenHeight - 2 * _cropHeight + _pixelDecimation/2) / _pixelDecimation; + _image.resize(width, height); + + return 0; } diff --git a/src/hyperion-x11/X11Grabber.h b/src/hyperion-x11/X11Grabber.h index e69de29b..21d36c47 100644 --- a/src/hyperion-x11/X11Grabber.h +++ b/src/hyperion-x11/X11Grabber.h @@ -0,0 +1,37 @@ + +// Hyperion-utils includes +#include +#include + +// X11 includes +#include + +class X11Grabber +{ +public: + + X11Grabber(const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation); + + virtual ~X11Grabber(); + + int open(); + + Image & grab(); + +private: + + const unsigned _pixelDecimation; + + const unsigned _cropWidth; + const unsigned _cropHeight; + + /// Reference to the X11 display (nullptr if not opened) + Display * _x11Display; + + unsigned _screenWidth; + unsigned _screenHeight; + + Image _image; + + int updateScreenDimensions(); +}; diff --git a/src/hyperion-x11/X11Wrapper.cpp b/src/hyperion-x11/X11Wrapper.cpp new file mode 100644 index 00000000..5bf4a97f --- /dev/null +++ b/src/hyperion-x11/X11Wrapper.cpp @@ -0,0 +1,33 @@ + +// Hyperion-X11 includes +#include "X11Wrapper.h" + +X11Wrapper::X11Wrapper(const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation) : + _timer(this), + _grabber(cropHorizontal, cropVertical, pixelDecimation) +{ + // Connect capturing to the timeout signal of the timer + connect(&_timer, SIGNAL(timeout()), this, SLOT(capture())); +} + +const Image & X11Wrapper::getScreenshot() +{ + const Image & screenshot = _grabber.grab(); + return screenshot; +} + +void X11Wrapper::start() +{ + _timer.start(200); +} + +void X11Wrapper::stop() +{ + _timer.stop(); +} + +void X11Wrapper::capture() +{ + const Image & screenshot = _grabber.grab(); + emit sig_screenshot(screenshot); +} diff --git a/src/hyperion-x11/X11Wrapper.h b/src/hyperion-x11/X11Wrapper.h new file mode 100644 index 00000000..405cbce9 --- /dev/null +++ b/src/hyperion-x11/X11Wrapper.h @@ -0,0 +1,39 @@ + +// QT includes +#include + +// Hyperion-X11 includes +#include "X11Grabber.h" + +class X11Wrapper : public QObject +{ + Q_OBJECT +public: + X11Wrapper(const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation); + + const Image & getScreenshot(); + + /// + /// Starts the timed capturing of screenshots + /// + void start(); + + void stop(); + +signals: + void sig_screenshot(const Image & screenshot); + +private slots: + /// + /// Performs a single screenshot capture and publishes the capture screenshot on the screenshot + /// signal. + /// + void capture(); + +private: + /// The QT timer to generate capture-publish events + QTimer _timer; + + /// The grabber for creating screenshots + X11Grabber _grabber; +}; diff --git a/src/hyperion-x11/hyperion-x11.cpp b/src/hyperion-x11/hyperion-x11.cpp new file mode 100644 index 00000000..ef9b5ab4 --- /dev/null +++ b/src/hyperion-x11/hyperion-x11.cpp @@ -0,0 +1,90 @@ + +// QT includes +#include +#include + +// getoptPlusPLus includes +#include + +#include "ProtoWrapper.h" +#include "X11Wrapper.h" + +using namespace vlofgren; + +// save the image as screenshot +void saveScreenshot(const char * filename, const Image & image) +{ + // store as PNG + QImage pngImage((const uint8_t *) image.memptr(), image.width(), image.height(), 3*image.width(), QImage::Format_RGB888); + pngImage.save(filename); +} + +int main(int argc, char ** argv) +{ + QCoreApplication app(argc, argv); + + try + { + // create the option parser and initialize all parameters + OptionsParser optionParser("X11 capture application for Hyperion"); + ParameterSet & parameters = optionParser.getParameters(); + + IntParameter & argCropWidth = parameters.add (0x0, "crop-width", "Number of pixels to crop from the left and right sides in the picture before decimation [default=0]"); + IntParameter & argCropHeight = parameters.add (0x0, "crop-height", "Number of pixels to crop from the top and the bottom in the picture before decimation [default=0]"); + IntParameter & argSizeDecimation = parameters.add ('s', "size-decimator", "Decimation factor for the output size [default=1]"); + SwitchParameter<> & argScreenshot = parameters.add> (0x0, "screenshot", "Take a single screenshot, save it to file and quit"); + StringParameter & argAddress = parameters.add ('a', "address", "Set the address of the hyperion server [default: 127.0.0.1:19445]"); + IntParameter & argPriority = parameters.add ('p', "priority", "Use the provided priority channel (the lower the number, the higher the priority) [default: 800]"); + SwitchParameter<> & argSkipReply = parameters.add> (0x0, "skip-reply", "Do not receive and check reply messages from Hyperion"); + SwitchParameter<> & argHelp = parameters.add> ('h', "help", "Show this help message and exit"); + + // set defaults + argCropWidth.setDefault(0); + argCropHeight.setDefault(0); + argSizeDecimation.setDefault(1); + argAddress.setDefault("127.0.0.1:19445"); + argPriority.setDefault(800); + + // parse all options + optionParser.parse(argc, const_cast(argv)); + + // check if we need to display the usage. exit if we do. + if (argHelp.isSet()) + { + optionParser.usage(); + return 0; + } + + // Create the X11 grabbing stuff + X11Wrapper x11Wrapper(argCropWidth.getValue(), argCropHeight.getValue(), argSizeDecimation.getValue()); + + if (argScreenshot.isSet()) + { + // Capture a single screenshot and finish + const Image & screenshot = x11Wrapper.getScreenshot(); + saveScreenshot("screenshot.png", screenshot); + } + else + { + // Create the Proto-connection with hyperiond + ProtoWrapper protoWrapper("127.0.0.1:19445", argSkipReply.isSet()); + + // Connect the screen capturing to the proto processing + QObject::connect(&x11Wrapper, SIGNAL(sig_screenshot(const Image &)), &protoWrapper, SLOT(process(Image))); + + // Start the capturing + x11Wrapper.start(); + + // Start the application + app.exec(); + } + } + catch (const std::runtime_error & e) + { + // An error occured. Display error and quit + std::cerr << e.what() << std::endl; + return -1; + } + + return 0; +} From 2fa46fdd982ce4817935d313dec3b6a463bb5862 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Sun, 2 Feb 2014 16:58:46 +0100 Subject: [PATCH 03/59] Fixed correct addres configuration Former-commit-id: b001df5968f009cf9686a3741170790d82eaba1f --- src/hyperion-x11/hyperion-x11.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hyperion-x11/hyperion-x11.cpp b/src/hyperion-x11/hyperion-x11.cpp index ef9b5ab4..6e6ee9ba 100644 --- a/src/hyperion-x11/hyperion-x11.cpp +++ b/src/hyperion-x11/hyperion-x11.cpp @@ -67,7 +67,7 @@ int main(int argc, char ** argv) else { // Create the Proto-connection with hyperiond - ProtoWrapper protoWrapper("127.0.0.1:19445", argSkipReply.isSet()); + ProtoWrapper protoWrapper(argAddress.getValue(), argSkipReply.isSet()); // Connect the screen capturing to the proto processing QObject::connect(&x11Wrapper, SIGNAL(sig_screenshot(const Image &)), &protoWrapper, SLOT(process(Image))); From 591a071856bf2df439c72248c7329f5a8f70e9c6 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Tue, 25 Feb 2014 22:08:26 +0100 Subject: [PATCH 04/59] Fixed missing include Former-commit-id: 53dab1a29d04b9cf083461caa20249b78600440d --- libsrc/hyperion/ImageProcessorFactory.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libsrc/hyperion/ImageProcessorFactory.cpp b/libsrc/hyperion/ImageProcessorFactory.cpp index 70987845..a47e532f 100644 --- a/libsrc/hyperion/ImageProcessorFactory.cpp +++ b/libsrc/hyperion/ImageProcessorFactory.cpp @@ -1,4 +1,7 @@ +// STL includes +#include + // Hyperion includes #include #include From 6b63b57f17f299f36c67dc69552d825f80f8532f Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Tue, 25 Feb 2014 22:08:54 +0100 Subject: [PATCH 05/59] Added performance test for x11 grabbing Former-commit-id: b8c60cf9984c6961675b41002bed40d251bff9fa --- test/CMakeLists.txt | 8 +++ test/TestX11Performance.cpp | 139 ++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 test/TestX11Performance.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bd46d059..8bf17456 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -53,6 +53,14 @@ add_executable(test_qtscreenshot TestQtScreenshot.cpp) target_link_libraries(test_qtscreenshot ${QT_LIBRARIES}) +if(ENABLE_X11) + # Find X11 + find_package(X11 REQUIRED) + + add_executable(test_x11performance TestX11Performance.cpp) + target_link_libraries(test_x11performance ${X11_LIBRARIES} ${QT_LIBRARIES}) +endif(ENABLE_X11) + add_executable(determineWs2811Timing DetermineWs2811Timing.cpp) add_executable(test_rs232highspeed diff --git a/test/TestX11Performance.cpp b/test/TestX11Performance.cpp new file mode 100644 index 00000000..9b7fb402 --- /dev/null +++ b/test/TestX11Performance.cpp @@ -0,0 +1,139 @@ + +// X11 includes +#include +#include + +#include + +#include +#include + +void foo_1(int pixelDecimation) +{ + int cropWidth = 0; + int cropHeight = 0; + + Image image(64, 64); + + /// Reference to the X11 display (nullptr if not opened) + Display * x11Display; + + const char * display_name = nullptr; + x11Display = XOpenDisplay(display_name); + + std::cout << "Opened display: " << x11Display << std::endl; + + XWindowAttributes window_attributes_return; + XGetWindowAttributes(x11Display, DefaultRootWindow(x11Display), &window_attributes_return); + + int screenWidth = window_attributes_return.width; + int screenHeight = window_attributes_return.height; + std::cout << "[" << screenWidth << "x" << screenHeight <<"]" << std::endl; + + // Update the size of the buffer used to transfer the screenshot + int width = (screenWidth - 2 * cropWidth + pixelDecimation/2) / pixelDecimation; + int height = (screenHeight - 2 * cropHeight + pixelDecimation/2) / pixelDecimation; + image.resize(width, height); + + const int croppedWidth = screenWidth - 2*cropWidth; + const int croppedHeight = screenHeight - 2*cropHeight; + + QElapsedTimer timer; + timer.start(); + + XImage * xImage = XGetImage(x11Display, DefaultRootWindow(x11Display), cropWidth, cropHeight, croppedWidth, croppedHeight, AllPlanes, ZPixmap); + + std::cout << "Captured image: " << xImage << std::endl; + + // Copy the capture XImage to the local image (and apply required decimation) + ColorRgb * outputPtr = image.memptr(); + for (int iY=(pixelDecimation/2); iYred = uint8_t((pixel >> 16) & 0xff); + outputPtr->green = uint8_t((pixel >> 8) & 0xff); + outputPtr->blue = uint8_t((pixel >> 0) & 0xff); + + // Move to the next output pixel + ++outputPtr; + } + } + + // Cleanup allocated resources of the X11 grab + XDestroyImage(xImage); + + std::cout << "Time required: " << timer.elapsed() << " us" << std::endl; + + XCloseDisplay(x11Display); +} + +void foo_2(int pixelDecimation) +{ + int cropWidth = 0; + int cropHeight = 0; + + Image image(64, 64); + + /// Reference to the X11 display (nullptr if not opened) + Display * x11Display; + + const char * display_name = nullptr; + x11Display = XOpenDisplay(display_name); + + XWindowAttributes window_attributes_return; + XGetWindowAttributes(x11Display, DefaultRootWindow(x11Display), &window_attributes_return); + + int screenWidth = window_attributes_return.width; + int screenHeight = window_attributes_return.height; + std::cout << "[" << screenWidth << "x" << screenHeight <<"]" << std::endl; + + // Update the size of the buffer used to transfer the screenshot + int width = (screenWidth - 2 * cropWidth + pixelDecimation/2) / pixelDecimation; + int height = (screenHeight - 2 * cropHeight + pixelDecimation/2) / pixelDecimation; + image.resize(width, height); + + const int croppedWidth = screenWidth - 2*cropWidth; + const int croppedHeight = screenHeight - 2*cropHeight; + + QElapsedTimer timer; + timer.start(); + + // Copy the capture XImage to the local image (and apply required decimation) + ColorRgb * outputPtr = image.memptr(); + for (int iY=(pixelDecimation/2); iYred = uint8_t((pixel >> 16) & 0xff); + outputPtr->green = uint8_t((pixel >> 8) & 0xff); + outputPtr->blue = uint8_t((pixel >> 0) & 0xff); + + // Move to the next output pixel + ++outputPtr; + + // Cleanup allocated resources of the X11 grab + XDestroyImage(xImage); + } + } + std::cout << "Time required: " << timer.elapsed() << " us" << std::endl; + + + XCloseDisplay(x11Display); +} + +int main() +{ + foo_1(10); + foo_2(10); + return 0; +} From 5f457fd9e3eb32b1beebc26921e4963e1935742c Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Thu, 6 Mar 2014 21:01:14 +0100 Subject: [PATCH 06/59] Added qt screenshot test Former-commit-id: c28e098ded780a6597952c83007fea1fe8da36fa --- test/TestQtScreenshot.cpp | 56 +++++++++++++++++++++++++++++++++++-- test/TestX11Performance.cpp | 4 +-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/test/TestQtScreenshot.cpp b/test/TestQtScreenshot.cpp index 27e03b0f..d2bed9e7 100644 --- a/test/TestQtScreenshot.cpp +++ b/test/TestQtScreenshot.cpp @@ -6,15 +6,67 @@ #include #include #include +#include +#include + +#include + +// Utils includes +#include +#include + +void createScreenshot(const int cropHorizontal, const int cropVertical, const int decimation, Image & image) +{ + // Create the full size screenshot + const QRect screenSize = QApplication::desktop()->screenGeometry(); + const int croppedWidth = screenSize.width() - 2*cropVertical; + const int croppedHeight = screenSize.height() - 2*cropHorizontal; + const QPixmap fullSizeScreenshot = QPixmap::grabWindow(QApplication::desktop()->winId(), cropVertical, cropHorizontal, croppedWidth, croppedHeight); + + // Scale the screenshot to the required size + const int width = fullSizeScreenshot.width()/decimation; + const int height = fullSizeScreenshot.height()/decimation; + const QPixmap scaledScreenshot = fullSizeScreenshot.scaled(width, height, Qt::IgnoreAspectRatio, Qt::FastTransformation); + + // Convert the QPixmap to QImage in order to get out RGB values + const QImage qImage = scaledScreenshot.toImage(); + + // Make sure that the output image has the right size + image.resize(width, height); + + // Copy the data into the output image + for (int y=0; y> 16; + outPixel.green = (inPixel & 0x0000ff00) >> 8; + outPixel.blue = (inPixel & 0x000000ff); + } + } +} int main(int argc, char** argv) { + int decimation = 10; QApplication app(argc, argv); + QElapsedTimer timer; - QPixmap originalPixmap = QPixmap::grabWindow(QApplication::desktop()->winId()); + Image screenshot(64,64); - std::cout << "Grabbed image: [" << originalPixmap.width() << "; " << originalPixmap.height() << "]" << std::endl; + int loopCnt = 100; + timer.start(); + for (int i=0; i Date: Mon, 8 Sep 2014 15:39:15 +0200 Subject: [PATCH 07/59] cmake: do not cache libusb library if libusb-1.0 headers not found Library found might be an older libusb version. Closes #158 Former-commit-id: 12783e1cfd3902954a9513ca1baf53433add742a --- cmake/Findlibusb-1.0.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/Findlibusb-1.0.cmake b/cmake/Findlibusb-1.0.cmake index 26a82c4f..77474e9a 100644 --- a/cmake/Findlibusb-1.0.cmake +++ b/cmake/Findlibusb-1.0.cmake @@ -87,6 +87,7 @@ else (LIBUSB_1_LIBRARIES AND LIBUSB_1_INCLUDE_DIRS) message(STATUS " - Libraries: ${LIBUSB_1_LIBRARIES}") endif (NOT libusb_1_FIND_QUIETLY) else (LIBUSB_1_FOUND) + unset(LIBUSB_1_LIBRARY CACHE) if (libusb_1_FIND_REQUIRED) message(FATAL_ERROR "Could not find libusb") endif (libusb_1_FIND_REQUIRED) From 3c65b82ac1ee3b6c104cb0483f014e785681709a Mon Sep 17 00:00:00 2001 From: Floris Bos Date: Mon, 8 Sep 2014 16:16:02 +0200 Subject: [PATCH 08/59] Allow disabling PROTOBUF support One dependency less for users that only use DISPMANX grabbing Former-commit-id: 24ea0480e3798bab692e75d82ddb9f5eccfa03c5 --- CMakeLists.txt | 21 ++++++++++----- HyperionConfig.h.in | 3 +++ libsrc/CMakeLists.txt | 34 ++++++++++++----------- src/hyperion-remote/CMakeLists.txt | 3 --- src/hyperiond/CMakeLists.txt | 43 ++++++++++++++++-------------- src/hyperiond/hyperiond.cpp | 6 +++++ 6 files changed, 66 insertions(+), 44 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f4ff4f6..29c9476b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,13 @@ message(STATUS "ENABLE_V4L2 = " ${ENABLE_V4L2}) option(ENABLE_TINKERFORGE "Enable the TINKERFORGE device" ON) message(STATUS "ENABLE_TINKERFORGE = " ${ENABLE_TINKERFORGE}) +option(ENABLE_PROTOBUF "Enable PROTOBUF server" ON) +message(STATUS "ENABLE_PROTOBUF = " ${ENABLE_PROTOBUF}) + +if (ENABLE_V4L2 AND NOT ENABLE_PROTOBUF) + message(FATAL_ERROR "V4L2 grabber requires PROTOBUF. Disable V4L2 or enable PROTOBUF") +endif (ENABLE_V4L2 AND NOT ENABLE_PROTOBUF) + # Createt the configuration file # configure a header file to pass some of the CMake settings # to the source code @@ -54,12 +61,14 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -Wall") # Configure the use of QT4 find_package(Qt4 COMPONENTS QtCore QtGui QtNetwork REQUIRED QUIET) -# add protocol buffers (make sure to find the static version) -set(CMAKE_FIND_LIBRARY_SUFFIXES_OLD ${CMAKE_FIND_LIBRARY_SUFFIXES}) -set(CMAKE_FIND_LIBRARY_SUFFIXES ".a") -find_package(Protobuf REQUIRED) -set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES_OLD}) -set(CMAKE_FIND_LIBRARY_SUFFIXES_OLD) +if (ENABLE_PROTOBUF) + # add protocol buffers (make sure to find the static version) + set(CMAKE_FIND_LIBRARY_SUFFIXES_OLD ${CMAKE_FIND_LIBRARY_SUFFIXES}) + set(CMAKE_FIND_LIBRARY_SUFFIXES ".a") + find_package(Protobuf REQUIRED) + set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES_OLD}) + set(CMAKE_FIND_LIBRARY_SUFFIXES_OLD) +endif (ENABLE_PROTOBUF) #add libusb and pthreads find_package(libusb-1.0 REQUIRED) diff --git a/HyperionConfig.h.in b/HyperionConfig.h.in index 17f040c0..ff2f4f82 100644 --- a/HyperionConfig.h.in +++ b/HyperionConfig.h.in @@ -11,3 +11,6 @@ // Define to enable the spi-device #cmakedefine ENABLE_TINKERFORGE + +// Define to enable PROTOBUF server +#cmakedefine ENABLE_PROTOBUF diff --git a/libsrc/CMakeLists.txt b/libsrc/CMakeLists.txt index 62911dd7..680e7a53 100644 --- a/libsrc/CMakeLists.txt +++ b/libsrc/CMakeLists.txt @@ -1,15 +1,19 @@ - -# Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc) - -add_subdirectory(hyperion) -add_subdirectory(blackborder) -add_subdirectory(jsonserver) -add_subdirectory(protoserver) -add_subdirectory(boblightserver) -add_subdirectory(leddevice) -add_subdirectory(utils) -add_subdirectory(xbmcvideochecker) -add_subdirectory(effectengine) -add_subdirectory(grabber) + +# Define the current source locations +SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include) +SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc) + +add_subdirectory(hyperion) +add_subdirectory(blackborder) +add_subdirectory(jsonserver) + +if (ENABLE_PROTOBUF) + add_subdirectory(protoserver) +endif (ENABLE_PROTOBUF) + +add_subdirectory(boblightserver) +add_subdirectory(leddevice) +add_subdirectory(utils) +add_subdirectory(xbmcvideochecker) +add_subdirectory(effectengine) +add_subdirectory(grabber) diff --git a/src/hyperion-remote/CMakeLists.txt b/src/hyperion-remote/CMakeLists.txt index 676dc7d3..c934be21 100644 --- a/src/hyperion-remote/CMakeLists.txt +++ b/src/hyperion-remote/CMakeLists.txt @@ -2,9 +2,6 @@ cmake_minimum_required(VERSION 2.8) project(hyperion-remote) -# add protocol buffers -find_package(Protobuf REQUIRED) - # find Qt4 find_package(Qt4 REQUIRED QtCore QtGui QtNetwork) diff --git a/src/hyperiond/CMakeLists.txt b/src/hyperiond/CMakeLists.txt index f843b15f..49df4bfe 100644 --- a/src/hyperiond/CMakeLists.txt +++ b/src/hyperiond/CMakeLists.txt @@ -1,20 +1,23 @@ - -add_executable(hyperiond - hyperiond.cpp) - -target_link_libraries(hyperiond - hyperion - xbmcvideochecker - effectengine - jsonserver - protoserver - boblightserver -) - -if (ENABLE_DISPMANX) - target_link_libraries(hyperiond dispmanx-grabber) -endif (ENABLE_DISPMANX) - -if (ENABLE_V4L2) - target_link_libraries(hyperiond v4l2-grabber) -endif (ENABLE_V4L2) + +add_executable(hyperiond + hyperiond.cpp) + +target_link_libraries(hyperiond + hyperion + xbmcvideochecker + effectengine + jsonserver + boblightserver +) + +if (ENABLE_DISPMANX) + target_link_libraries(hyperiond dispmanx-grabber) +endif (ENABLE_DISPMANX) + +if (ENABLE_V4L2) + target_link_libraries(hyperiond v4l2-grabber) +endif (ENABLE_V4L2) + +if (ENABLE_PROTOBUF) + target_link_libraries(hyperiond protoserver) +endif (ENABLE_PROTOBUF) diff --git a/src/hyperiond/hyperiond.cpp b/src/hyperiond/hyperiond.cpp index b6365019..e7591343 100644 --- a/src/hyperiond/hyperiond.cpp +++ b/src/hyperiond/hyperiond.cpp @@ -36,8 +36,10 @@ // JsonServer includes #include +#ifdef ENABLE_PROTOBUF // ProtoServer includes #include +#endif // BoblightServer includes #include @@ -233,6 +235,7 @@ int main(int argc, char** argv) std::cout << "Json server created and started on port " << jsonServer->getPort() << std::endl; } +#ifdef ENABLE_PROTOBUF // Create Proto server if configuration is present ProtoServer * protoServer = nullptr; if (config.isMember("protoServer")) @@ -241,6 +244,7 @@ int main(int argc, char** argv) protoServer = new ProtoServer(&hyperion, protoServerConfig["port"].asUInt()); std::cout << "Proto server created and started on port " << protoServer->getPort() << std::endl; } +#endif // Create Boblight server if configuration is present BoblightServer * boblightServer = nullptr; @@ -264,7 +268,9 @@ int main(int argc, char** argv) #endif delete xbmcVideoChecker; delete jsonServer; +#ifdef ENABLE_PROTOBUF delete protoServer; +#endif delete boblightServer; // leave application From 905f8565438480ea6676feba94cef1869e581d8a Mon Sep 17 00:00:00 2001 From: Floris Bos Date: Mon, 8 Sep 2014 18:59:29 +0200 Subject: [PATCH 09/59] Fix linking with static Qt Embedded builds Make sure pthreads and dl get linked when building Hyperion using a static Qt Embedded library instead of Qt X11 Former-commit-id: d1e57e9192dfb6c3c42261a9ee57ebdc85bf03cd --- cmake/qt4/Qt4ConfigDependentSettings.cmake | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cmake/qt4/Qt4ConfigDependentSettings.cmake b/cmake/qt4/Qt4ConfigDependentSettings.cmake index 6db5da18..6df28f47 100644 --- a/cmake/qt4/Qt4ConfigDependentSettings.cmake +++ b/cmake/qt4/Qt4ConfigDependentSettings.cmake @@ -270,6 +270,16 @@ if(Q_WS_X11) endif() +if(Q_WS_QWS) + set(CMAKE_THREAD_PREFER_PTHREADS 1) + find_package(Threads) + if(CMAKE_USE_PTHREADS_INIT) + set(QT_QTCORE_LIB_DEPENDENCIES ${QT_QTCORE_LIB_DEPENDENCIES} ${CMAKE_THREAD_LIBS_INIT}) + endif() + + set (QT_QTCORE_LIB_DEPENDENCIES ${QT_QTCORE_LIB_DEPENDENCIES} ${CMAKE_DL_LIBS}) + +endif() if(Q_WS_WIN) set(QT_QTGUI_LIB_DEPENDENCIES ${QT_QTGUI_LIB_DEPENDENCIES} imm32 winmm) From 042d4b6e91f48bc4938168eb207599cdc9eadb8f Mon Sep 17 00:00:00 2001 From: David Brodski Date: Tue, 16 Sep 2014 00:17:23 +0200 Subject: [PATCH 10/59] Added LedDevice for "ws2812s" leds To activate: use led device "ws2812s" in the hyperion configuration Former-commit-id: 0b5ee38679fe353f43bb4a347882d056ca237128 --- libsrc/leddevice/CMakeLists.txt | 9 + libsrc/leddevice/LedDeviceFactory.cpp | 6 + libsrc/leddevice/LedDeviceWS2812s.cpp | 529 ++++++++++++++++++++++++++ libsrc/leddevice/LedDeviceWS2812s.h | 451 ++++++++++++++++++++++ 4 files changed, 995 insertions(+) create mode 100644 libsrc/leddevice/LedDeviceWS2812s.cpp create mode 100644 libsrc/leddevice/LedDeviceWS2812s.h diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt index 441618d2..0efbd18c 100755 --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -68,6 +68,15 @@ if(ENABLE_SPIDEV) ) endif(ENABLE_SPIDEV) +SET(Leddevice_HEADERS + ${Leddevice_HEADERS} + ${CURRENT_SOURCE_DIR}/LedDeviceWS2812s.h + ) +SET(Leddevice_SOURCES + ${Leddevice_SOURCES} + ${CURRENT_SOURCE_DIR}/LedDeviceWS2812s.cpp +) + if(ENABLE_TINKERFORGE) SET(Leddevice_HEADERS ${Leddevice_HEADERS} diff --git a/libsrc/leddevice/LedDeviceFactory.cpp b/libsrc/leddevice/LedDeviceFactory.cpp index c7797c3d..abc0a2c7 100755 --- a/libsrc/leddevice/LedDeviceFactory.cpp +++ b/libsrc/leddevice/LedDeviceFactory.cpp @@ -31,6 +31,8 @@ #include "LedDevicePhilipsHue.h" #include "LedDeviceTpm2.h" +#include "LedDeviceWS2812s.h" + LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) { std::cout << "Device configuration: " << deviceConfig << std::endl; @@ -181,6 +183,10 @@ LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) LedDeviceTpm2* deviceTpm2 = new LedDeviceTpm2(output, rate); deviceTpm2->open(); device = deviceTpm2; + }else if (type == "ws2812s") + { + LedDeviceWS2812s * ledDeviceWS2812s = new LedDeviceWS2812s(); + device = ledDeviceWS2812s; } else { diff --git a/libsrc/leddevice/LedDeviceWS2812s.cpp b/libsrc/leddevice/LedDeviceWS2812s.cpp new file mode 100644 index 00000000..545519b0 --- /dev/null +++ b/libsrc/leddevice/LedDeviceWS2812s.cpp @@ -0,0 +1,529 @@ +// For license and other informations see LedDeviceWS2812s.h +// To activate: use led device "ws2812s" in the hyperion configuration + +// STL includes +#include +#include +#include +#include + +// Linux includes +#include +//#include + +// hyperion local includes +#include "LedDeviceWS2812s.h" + +LedDeviceWS2812s::LedDeviceWS2812s() : + LedDevice(), + mLedCount(0) +{ + // Init PWM generator and clear LED buffer + initHardware(); + //clearLEDBuffer(); +} + + +int LedDeviceWS2812s::write(const std::vector &ledValues) +{ + mLedCount = ledValues.size(); + //printf("Set leds, number: %d\n", mLedCount); + +// const unsigned dataLen = ledValues.size() * sizeof(ColorRgb); +// const uint8_t * dataPtr = reinterpret_cast(ledValues.data()); + + // Clear out the PWM buffer + // Disabled, because we will overwrite the buffer anyway. + + // Read data from LEDBuffer[], translate it into wire format, and write to PWMWaveform +// unsigned int LEDBuffeWordPos = 0; +// unsigned int PWMWaveformBitPos = 0; + unsigned int colorBits = 0; // Holds the GRB color before conversion to wire bit pattern + unsigned char colorBit = 0; // Holds current bit out of colorBits to be processed + unsigned int wireBit = 0; // Holds the current bit we will set in PWMWaveform +// Color_t color; + + for(size_t i=0; i=0; j--) { + colorBit = (colorBits & (1 << j)) ? 1 : 0; + switch(colorBit) { + case 1: + //wireBits = 0b110; // High, High, Low + setPWMBit(wireBit++, 1); + setPWMBit(wireBit++, 1); + setPWMBit(wireBit++, 0); + break; + case 0: + //wireBits = 0b100; // High, Low, Low + setPWMBit(wireBit++, 1); + setPWMBit(wireBit++, 0); + setPWMBit(wireBit++, 0); + break; + } + } + } + + // Copy PWM waveform to DMA's data buffer + //printf("Copying %d words to DMA data buffer\n", NUM_DATA_WORDS); + ctl = (struct control_data_s *)virtbase; + dma_cb_t *cbp = ctl->cb; + + // 72 bits per pixel / 32 bits per word = 2.25 words per pixel + // Add 1 to make sure the PWM FIFO gets the message: "we're sending zeroes" + // Times 4 because DMA works in bytes, not words + cbp->length = ((mLedCount * 2.25) + 1) * 4; + if(cbp->length > NUM_DATA_WORDS * 4) { + cbp->length = NUM_DATA_WORDS * 4; + } + + // This block is a major CPU hog when there are lots of pixels to be transmitted. + // It would go quicker with DMA. + for(unsigned int i = 0; i < (cbp->length / 4); i++) { + ctl->sample[i] = PWMWaveform[i]; + } + + + // Enable DMA and PWM engines, which should now send the data + startTransfer(); + + // Wait long enough for the DMA transfer to finish + // 3 RAM bits per wire bit, so 72 bits to send one color command. + //float bitTimeUSec = (float)(NUM_DATA_WORDS * 32) * 0.4; // Bits sent * time to transmit one bit, which is 0.4μSec + //printf("Delay for %d μSec\n", (int)bitTimeUSec); + //usleep((int)bitTimeUSec); + + return 0; +} + +int LedDeviceWS2812s::switchOff() +{ + return write(std::vector(mLedCount, ColorRgb{0,0,0})); +} + +LedDeviceWS2812s::~LedDeviceWS2812s() +{ + // Exit cleanly, freeing memory and stopping the DMA & PWM engines + // We trap all signals (including Ctrl+C), so even if you don't get here, it terminates correctly + terminate(0); +} + + +// ================================================================================================= +// ________ .__ +// / _____/ ____ ____ ________________ | | +// / \ ____/ __ \ / \_/ __ \_ __ \__ \ | | +// \ \_\ \ ___/| | \ ___/| | \// __ \| |__ +// \______ /\___ >___| /\___ >__| (____ /____/ +// \/ \/ \/ \/ \/ +// ================================================================================================= + +// Convenience functions +// -------------------------------------------------------------------------------------------------- +// Print some bits of a binary number (2nd arg is how many bits) +void LedDeviceWS2812s::printBinary(unsigned int i, unsigned int bits) { + int x; + for(x=bits-1; x>=0; x--) { + printf("%d", (i & (1 << x)) ? 1 : 0); + if(x % 16 == 0 && x > 0) { + printf(" "); + } else if(x % 4 == 0 && x > 0) { + printf(":"); + } + } +} + +// Reverse the bits in a word +unsigned int reverseWord(unsigned int word) { + unsigned int output = 0; + //unsigned char bit; + int i; + for(i=0; i<32; i++) { + //bit = word & (1 << i) ? 1 : 0; + output |= word & (1 << i) ? 1 : 0; + if(i<31) { + output <<= 1; + } + } + return output; +} + +// Not sure how this is better than usleep...? +/* +static void udelay(int us) { + struct timespec ts = { 0, us * 1000 }; + nanosleep(&ts, NULL); +} +*/ + + +// Shutdown functions +// -------------------------------------------------------------------------------------------------- +void LedDeviceWS2812s::terminate(int dummy) { + // Shut down the DMA controller + if(dma_reg) { + CLRBIT(dma_reg[DMA_CS], DMA_CS_ACTIVE); + usleep(100); + SETBIT(dma_reg[DMA_CS], DMA_CS_RESET); + usleep(100); + } + + // Shut down PWM + if(pwm_reg) { + CLRBIT(pwm_reg[PWM_CTL], PWM_CTL_PWEN1); + usleep(100); + pwm_reg[PWM_CTL] = (1 << PWM_CTL_CLRF1); + } + + // Free the allocated memory + if(page_map != 0) { + free(page_map); + } + + //exit(1); +} + +void LedDeviceWS2812s::fatal(char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + terminate(0); +} + + +// Memory management +// -------------------------------------------------------------------------------------------------- +// Translate from virtual address to physical +unsigned int LedDeviceWS2812s::mem_virt_to_phys(void *virt) { + unsigned int offset = (uint8_t *)virt - virtbase; + return page_map[offset >> PAGE_SHIFT].physaddr + (offset % PAGE_SIZE); +} + +// Translate from physical address to virtual +unsigned int LedDeviceWS2812s::mem_phys_to_virt(uint32_t phys) { + unsigned int pg_offset = phys & (PAGE_SIZE - 1); + unsigned int pg_addr = phys - pg_offset; + + for (unsigned int i = 0; i < NUM_PAGES; i++) { + if (page_map[i].physaddr == pg_addr) { + return (uint32_t)virtbase + i * PAGE_SIZE + pg_offset; + } + } + fatal("Failed to reverse map phys addr %08x\n", phys); + + return 0; +} + +// Map a peripheral's IO memory into our virtual memory, so we can read/write it directly +void * LedDeviceWS2812s::map_peripheral(uint32_t base, uint32_t len) { + int fd = open("/dev/mem", O_RDWR); + void * vaddr; + + if (fd < 0) + fatal("Failed to open /dev/mem: %m\n"); + vaddr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, base); + if (vaddr == MAP_FAILED) + fatal("Failed to map peripheral at 0x%08x: %m\n", base); + close(fd); + + return vaddr; +} + +// Zero out the PWM waveform buffer +void LedDeviceWS2812s::clearPWMBuffer() { + memset(PWMWaveform, 0, NUM_DATA_WORDS * 4); // Times four because memset deals in bytes. +} + +// Set an individual bit in the PWM output array, accounting for word boundaries +// The (31 - bitIdx) is so that we write the data backwards, correcting its endianness +// This means getPWMBit will return something other than what was written, so it would be nice +// if the logic that calls this function would figure it out instead. (However, that's trickier) +void LedDeviceWS2812s::setPWMBit(unsigned int bitPos, unsigned char bit) { + + // Fetch word the bit is in + unsigned int wordOffset = (int)(bitPos / 32); + unsigned int bitIdx = bitPos - (wordOffset * 32); + + //printf("bitPos=%d wordOffset=%d bitIdx=%d value=%d\n", bitPos, wordOffset, bitIdx, bit); + + switch(bit) { + case 1: + PWMWaveform[wordOffset] |= (1 << (31 - bitIdx)); +// PWMWaveform[wordOffset] |= (1 << bitIdx); + break; + case 0: + PWMWaveform[wordOffset] &= ~(1 << (31 - bitIdx)); +// PWMWaveform[wordOffset] &= ~(1 << bitIdx); + break; + } +} + +// ================================================================================================= +// .___ .__ __ ___ ___ .___ +// | | ____ |__|/ |_ / | \_____ _______ __| _/_ _ _______ _______ ____ +// | |/ \| \ __\ / ~ \__ \\_ __ \/ __ |\ \/ \/ /\__ \\_ __ \_/ __ \ +// | | | \ || | \ Y // __ \| | \/ /_/ | \ / / __ \| | \/\ ___/ +// |___|___| /__||__| \___|_ /(____ /__| \____ | \/\_/ (____ /__| \___ > +// \/ \/ \/ \/ \/ \/ +// ================================================================================================= + +void LedDeviceWS2812s::initHardware() { + int pid; + int fd; + char pagemap_fn[64]; + + // Clear the PWM buffer + // --------------------------------------------------------------- + clearPWMBuffer(); + + // Set up peripheral access + // --------------------------------------------------------------- + dma_reg = (unsigned int *) map_peripheral(DMA_BASE, DMA_LEN); + dma_reg += 0x000; + pwm_reg = (unsigned int *) map_peripheral(PWM_BASE, PWM_LEN); + clk_reg = (unsigned int *) map_peripheral(CLK_BASE, CLK_LEN); + gpio_reg = (unsigned int *) map_peripheral(GPIO_BASE, GPIO_LEN); + + + // Set PWM alternate function for GPIO18 + // --------------------------------------------------------------- + //gpio_reg[1] &= ~(7 << 24); + //usleep(100); + //gpio_reg[1] |= (2 << 24); + //usleep(100); + SET_GPIO_ALT(18, 5); + + + // Allocate memory for the DMA control block & data to be sent + // --------------------------------------------------------------- + virtbase = (uint8_t *) mmap( + NULL, // Address + NUM_PAGES * PAGE_SIZE, // Length + PROT_READ | PROT_WRITE, // Protection + MAP_SHARED | // Shared + MAP_ANONYMOUS | // Not file-based, init contents to 0 + MAP_NORESERVE | // Don't reserve swap space + MAP_LOCKED, // Lock in RAM (don't swap) + -1, // File descriptor + 0); // Offset + + if (virtbase == MAP_FAILED) { + fatal("Failed to mmap physical pages: %m\n"); + } + + if ((unsigned long)virtbase & (PAGE_SIZE-1)) { + fatal("Virtual address is not page aligned\n"); + } + + //printf("virtbase mapped 0x%x bytes at 0x%x\n", NUM_PAGES * PAGE_SIZE, virtbase); + + // Allocate page map (pointers to the control block(s) and data for each CB + page_map = (page_map_t *) malloc(NUM_PAGES * sizeof(*page_map)); + if (page_map == 0) { + fatal("Failed to malloc page_map: %m\n"); + } else { + //printf("Allocated 0x%x bytes for page_map at 0x%x\n", NUM_PAGES * sizeof(*page_map), page_map); + } + + // Use /proc/self/pagemap to figure out the mapping between virtual and physical addresses + pid = getpid(); + sprintf(pagemap_fn, "/proc/%d/pagemap", pid); + fd = open(pagemap_fn, O_RDONLY); + + if (fd < 0) { + fatal("Failed to open %s: %m\n", pagemap_fn); + } + + if (lseek(fd, (unsigned long)virtbase >> 9, SEEK_SET) != (unsigned long)virtbase >> 9) { + fatal("Failed to seek on %s: %m\n", pagemap_fn); + } + + printf("Page map: %d pages\n", NUM_PAGES); + for (unsigned int i = 0; i < NUM_PAGES; i++) { + uint64_t pfn; + page_map[i].virtaddr = virtbase + i * PAGE_SIZE; + + // Following line forces page to be allocated + // (Note: Copied directly from Hirst's code... page_map[i].virtaddr[0] was just set...?) + page_map[i].virtaddr[0] = 0; + + if (read(fd, &pfn, sizeof(pfn)) != sizeof(pfn)) { + fatal("Failed to read %s: %m\n", pagemap_fn); + } + + if (((pfn >> 55) & 0xfbf) != 0x10c) { // pagemap bits: https://www.kernel.org/doc/Documentation/vm/pagemap.txt + fatal("Page %d not present (pfn 0x%016llx)\n", i, pfn); + } + + page_map[i].physaddr = (unsigned int)pfn << PAGE_SHIFT | 0x40000000; + //printf("Page map #%2d: virtual %8p ==> physical 0x%08x [0x%016llx]\n", i, page_map[i].virtaddr, page_map[i].physaddr, pfn); + } + + + // Set up control block + // --------------------------------------------------------------- + ctl = (struct control_data_s *)virtbase; + dma_cb_t *cbp = ctl->cb; + // FIXME: Change this to use DEFINEs + unsigned int phys_pwm_fifo_addr = 0x7e20c000 + 0x18; + + // No wide bursts, source increment, dest DREQ on line 5, wait for response, enable interrupt + cbp->info = DMA_TI_CONFIGWORD; + + // Source is our allocated memory + cbp->src = mem_virt_to_phys(ctl->sample); + + // Destination is the PWM controller + cbp->dst = phys_pwm_fifo_addr; + + // 72 bits per pixel / 32 bits per word = 2.25 words per pixel + // Add 1 to make sure the PWM FIFO gets the message: "we're sending zeroes" + // Times 4 because DMA works in bytes, not words + cbp->length = ((mLedCount * 2.25) + 1) * 4; + if(cbp->length > NUM_DATA_WORDS * 4) { + cbp->length = NUM_DATA_WORDS * 4; + } + + // We don't use striding + cbp->stride = 0; + + // These are reserved + cbp->pad[0] = 0; + cbp->pad[1] = 0; + + // Pointer to next block - 0 shuts down the DMA channel when transfer is complete + cbp->next = 0; + + // Testing + /* + ctl = (struct control_data_s *)virtbase; + ctl->sample[0] = 0x00000000; + ctl->sample[1] = 0x000000FA; + ctl->sample[2] = 0x0000FFFF; + ctl->sample[3] = 0xAAAAAAAA; + ctl->sample[4] = 0xF0F0F0F0; + ctl->sample[5] = 0x0A0A0A0A; + ctl->sample[6] = 0xF00F0000; + */ + + + // Stop any existing DMA transfers + // --------------------------------------------------------------- + dma_reg[DMA_CS] |= (1 << DMA_CS_ABORT); + usleep(100); + dma_reg[DMA_CS] = (1 << DMA_CS_RESET); + usleep(100); + + + // PWM Clock + // --------------------------------------------------------------- + // Kill the clock + // FIXME: Change this to use a DEFINE + clk_reg[PWM_CLK_CNTL] = 0x5A000000 | (1 << 5); + usleep(100); + + // Disable DMA requests + CLRBIT(pwm_reg[PWM_DMAC], PWM_DMAC_ENAB); + usleep(100); + + // The fractional part is quantized to a range of 0-1024, so multiply the decimal part by 1024. + // E.g., 0.25 * 1024 = 256. + // So, if you want a divisor of 400.5, set idiv to 400 and fdiv to 512. + unsigned int idiv = 400; + unsigned short fdiv = 0; // Should be 16 bits, but the value must be <= 1024 + clk_reg[PWM_CLK_DIV] = 0x5A000000 | (idiv << 12) | fdiv; // Set clock multiplier + usleep(100); + + // Enable the clock. Next-to-last digit means "enable clock". Last digit is 1 (oscillator), + // 4 (PLLA), 5 (PLLC), or 6 (PLLD) (according to the docs) although PLLA doesn't seem to work. + // FIXME: Change this to use a DEFINE + clk_reg[PWM_CLK_CNTL] = 0x5A000015; + usleep(100); + + + // PWM + // --------------------------------------------------------------- + // Clear any preexisting crap from the control & status register + pwm_reg[PWM_CTL] = 0; + + // Set transmission range (32 bytes, or 1 word) + // <32: Truncate. >32: Pad with SBIT1. As it happens, 32 is perfect. + pwm_reg[PWM_RNG1] = 32; + usleep(100); + + // Send DMA requests to fill the FIFO + pwm_reg[PWM_DMAC] = + (1 << PWM_DMAC_ENAB) | + (8 << PWM_DMAC_PANIC) | + (8 << PWM_DMAC_DREQ); + usleep(1000); + + // Clear the FIFO + SETBIT(pwm_reg[PWM_CTL], PWM_CTL_CLRF1); + usleep(100); + + // Don't repeat last FIFO contents if it runs dry + CLRBIT(pwm_reg[PWM_CTL], PWM_CTL_RPTL1); + usleep(100); + + // Silence (default) bit is 0 + CLRBIT(pwm_reg[PWM_CTL], PWM_CTL_SBIT1); + usleep(100); + + // Polarity = default (low = 0, high = 1) + CLRBIT(pwm_reg[PWM_CTL], PWM_CTL_POLA1); + usleep(100); + + // Enable serializer mode + SETBIT(pwm_reg[PWM_CTL], PWM_CTL_MODE1); + usleep(100); + + // Use FIFO rather than DAT1 + SETBIT(pwm_reg[PWM_CTL], PWM_CTL_USEF1); + usleep(100); + + // Disable MSEN1 + CLRBIT(pwm_reg[PWM_CTL], PWM_CTL_MSEN1); + usleep(100); + + + // DMA + // --------------------------------------------------------------- + // Raise an interrupt when transfer is complete, which will set the INT flag in the CS register + SETBIT(dma_reg[DMA_CS], DMA_CS_INT); + usleep(100); + + // Clear the END flag (by setting it - this is a "write 1 to clear", or W1C, bit) + SETBIT(dma_reg[DMA_CS], DMA_CS_END); + usleep(100); + + // Send the physical address of the control block into the DMA controller + dma_reg[DMA_CONBLK_AD] = mem_virt_to_phys(ctl->cb); + usleep(100); + + // Clear error flags, if any (these are also W1C bits) + // FIXME: Use a define instead of this + dma_reg[DMA_DEBUG] = 7; + usleep(100); +} + +// Begin the transfer +void LedDeviceWS2812s::startTransfer() { + // Enable DMA + dma_reg[DMA_CONBLK_AD] = mem_virt_to_phys(ctl->cb); + dma_reg[DMA_CS] = DMA_CS_CONFIGWORD | (1 << DMA_CS_ACTIVE); + usleep(100); + + // Enable PWM + SETBIT(pwm_reg[PWM_CTL], PWM_CTL_PWEN1); + +// dumpPWM(); +// dumpDMA(); +} diff --git a/libsrc/leddevice/LedDeviceWS2812s.h b/libsrc/leddevice/LedDeviceWS2812s.h new file mode 100644 index 00000000..2dca0fb0 --- /dev/null +++ b/libsrc/leddevice/LedDeviceWS2812s.h @@ -0,0 +1,451 @@ +#ifndef LEDDEVICEWS2812S_H_ +#define LEDDEVICEWS2812S_H_ + +#pragma once + + +// Set tabs to 4 spaces. + +// ================================================================================================= +// +// __ __ _________________ ______ ____________ ____________________.__ +// / \ / \/ _____/\_____ \ / __ \/_ \_____ \ \______ \______ \__| +// \ \/\/ /\_____ \ / ____/ > < | |/ ____/ | _/| ___/ | +// \ / / \/ \/ -- \| / \ | | \| | | | +// \__/\ / /_______ /\_______ \______ /|___\_______ \ |____|_ /|____| |__| +// \/ \/ \/ \/ \/ \/ +// +// WS2812 NeoPixel driver +// Based on code by Richard G. Hirst and others +// Adapted for the WS2812 by 626Pilot, April/May 2014 +// See: https://github.com/626Pilot/RaspberryPi-NeoPixel-WS2812 +// Version: https://github.com/626Pilot/RaspberryPi-NeoPixel-WS2812/blob/1d43407d9e6eba19bff24330bc09a27963b55751/ws2812-RPi.c +// Huge ASCII art section labels are from http://patorjk.com/software/taag/ +// +// LED driver adaptation by Kammerjaeger () +// mostly code removed that was not needed +// +// License: GPL +// +// You are using this at your OWN RISK. I believe this software is reasonably safe to use (aside +// from the intrinsic risk to those who are photosensitive - see below), although I can't be certain +// that it won't trash your hardware or cause property damage. +// +// Speaking of risk, WS2812 pixels are bright enough to cause eye pain and (for all I know) possibly +// retina damage when run at full strength. It's a good idea to set the brightness at 0.2 or so for +// direct viewing (whether you're looking directly at the pixels or not), or to put some diffuse +// material between you and the LEDs. +// +// PHOTOSENSITIVITY WARNING: +// Patterns of light and darkness (stationary or moving), flashing lights, patterns and backgrounds +// on screens, and the like, may cause epilleptic seizures in some people. This is a danger EVEN IF +// THE PERSON (WHICH MAY BE *YOU*) HAS NEVER KNOWINGLY HAD A PHOTOSENSITIVE EPISODE BEFORE. It's up +// to you to learn the warning signs, but symptoms may include dizziness, nausea, vision changes, +// convlusions, disorientation, involuntary movements, and eye twitching. (This list is not +// necessarily exhaustive.) +// +// NEOPIXEL BEST PRACTICES: https://learn.adafruit.com/adafruit-neopixel-uberguide/best-practices +// +// Connections: +// Positive to Raspberry Pi's 3.3v, for better separation connect only ground and data directly +// (5v can be used then without a problem, at least it worked for me, Kammerjaeger) +// Negative to Raspberry Pi's ground +// Data to GPIO18 (Pin 12) (through a resistor, which you should know from the Best +// Practices guide!) +// +// Buy WS2812-based stuff from: http://adafruit.com +// +// To activate: use led device "ws2812s" in the hyperion configuration +// (it needs to be root so it can map the peripherals' registers) +// +// ================================================================================================= + +// This is for the WS2812 LEDs. It won't work with the older WS2811s, although it could be modified +// for that without too much trouble. Preliminary driver used Frank Buss' servo driver, but I moved +// to Richard Hirst's memory mapping/access model because his code already works with DMA, and has +// what I think is a slightly cleaner way of accessing the registers: register[name] rather than +// *(register + name). + +// At the time of writing, there's a lot of confusing "PWM DMA" code revolving around simulating +// an FM signal. Usually this is done without properly initializing certain registers, which is +// OK for their purpose, but I needed to be able to transfer actual coherent data and have it wind +// up in a proper state once it was transferred. This has proven to be a somewhat painful task. +// The PWM controller likes to ignore the RPTL1 bit when the data is in a regular, repeating +// pattern. I'M NOT MAKING IT UP! It really does that. It's bizarre. There are lots of other +// strange irregularities as well, which had to be figured out through trial and error. It doesn't +// help that the BCM2835 ARM Peripherals manual contains outright errors and omissions! + +// Many examples of this kind of code have magic numbers in them. If you don't know, a magic number +// is one that either lacks an obvious structure (e.g. 0x2020C000) or purpose. Please don't use +// that stuff in any code you release! All magic numbers found in reference code have been changed +// to DEFINEs. That way, instead of seeing some inscrutable number, you see (e.g.) PWM_CTL. + +// References - BCM2835 ARM Peripherals: +// http://www.raspberrypi.org/wp-content/uploads/2012/02/BCM2835-ARM-Peripherals.pdf +// +// Raspberry Pi low-level peripherals: +// http://elinux.org/RPi_Low-level_peripherals +// +// Richard Hirst's nice, clean code: +// https://github.com/richardghirst/PiBits/blob/master/PiFmDma/PiFmDma.c +// +// PWM clock register: +// http://www.raspberrypi.org/forums/viewtopic.php?t=8467&p=124620 +// +// Simple (because it's in assembly) PWM+DMA setup: +// https://github.com/mikedurso/rpi-projects/blob/master/asm-nyancat/rpi-nyancat.s +// +// Adafruit's NeoPixel driver: +// https://github.com/adafruit/Adafruit_NeoPixel/blob/master/Adafruit_NeoPixel.cpp + + +// ================================================================================================= +// .___ .__ .___ +// | | ____ ____ | | __ __ __| _/____ ______ +// | |/ \_/ ___\| | | | \/ __ |/ __ \ / ___/ +// | | | \ \___| |_| | / /_/ \ ___/ \___ \ +// |___|___| /\___ >____/____/\____ |\___ >____ > +// \/ \/ \/ \/ \/ +// ================================================================================================= + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Hyperion includes +#include + + +// ================================================================================================= +// ________ _____.__ ____ ____ ____ +// \______ \ _____/ ____\__| ____ ____ ______ / _ \ \ \ / /____ _______ ______ +// | | \_/ __ \ __\| |/ \_/ __ \ / ___/ > _ __| |__|___| /\___ >____ > \_____\ \ \___/ (____ /__| /____ > +// \/ \/ \/ \/ \/ \/ \/ \/ +// ================================================================================================= + +// Base addresses for GPIO, PWM, PWM clock, and DMA controllers (physical, not bus!) +// These will be "memory mapped" into virtual RAM so that they can be written and read directly. +// ------------------------------------------------------------------------------------------------- +#define DMA_BASE 0x20007000 +#define DMA_LEN 0x24 +#define PWM_BASE 0x2020C000 +#define PWM_LEN 0x28 +#define CLK_BASE 0x20101000 +#define CLK_LEN 0xA8 +#define GPIO_BASE 0x20200000 +#define GPIO_LEN 0xB4 + +// GPIO +// ------------------------------------------------------------------------------------------------- +#define GPFSEL0 0x20200000 // GPIO function select, pins 0-9 (bits 30-31 reserved) +#define GPFSEL1 0x20200004 // Pins 10-19 +#define GPFSEL2 0x20200008 // Pins 20-29 +#define GPFSEL3 0x2020000C // Pins 30-39 +#define GPFSEL4 0x20200010 // Pins 40-49 +#define GPFSEL5 0x20200014 // Pins 50-53 +#define GPSET0 0x2020001C // Set (turn on) pin +#define GPCLR0 0x20200028 // Clear (turn off) pin +#define GPPUD 0x20200094 // Internal pullup/pulldown resistor control +#define GPPUDCLK0 0x20200098 // PUD clock for pins 0-31 +#define GPPUDCLK1 0x2020009C // PUD clock for pins 32-53 + +// Memory offsets for the PWM clock register, which is undocumented! Please fix that, Broadcom! +// ------------------------------------------------------------------------------------------------- +#define PWM_CLK_CNTL 40 // Control (on/off) +#define PWM_CLK_DIV 41 // Divisor (bits 11:0 are *quantized* floating part, 31:12 integer part) + +// PWM Register Addresses (page 141) +// These are divided by 4 because the register offsets in the guide are in bytes (8 bits) but +// the pointers we use in this program are in words (32 bits). Buss' original defines are in +// word offsets, e.g. PWM_RNG1 was 4 and PWM_DAT1 was 5. This is functionally the same, but it +// matches the numbers supplied in the guide. +// ------------------------------------------------------------------------------------------------- +#define PWM_CTL 0x00 // Control Register +#define PWM_STA (0x04 / 4) // Status Register +#define PWM_DMAC (0x08 / 4) // DMA Control Register +#define PWM_RNG1 (0x10 / 4) // Channel 1 Range +#define PWM_DAT1 (0x14 / 4) // Channel 1 Data +#define PWM_FIF1 (0x18 / 4) // FIFO (for both channels - bytes are interleaved if both active) +#define PWM_RNG2 (0x20 / 4) // Channel 2 Range +#define PWM_DAT2 (0x24 / 4) // Channel 2 Data + +// PWM_CTL register bit offsets +// Note: Don't use MSEN1/2 for this purpose. It will screw things up. +// ------------------------------------------------------------------------------------------------- +#define PWM_CTL_MSEN2 15 // Channel 2 - 0: Use PWM algorithm. 1: Use M/S (serial) algorithm. +#define PWM_CTL_USEF2 13 // Channel 2 - 0: Use PWM_DAT2. 1: Use FIFO. +#define PWM_CTL_POLA2 12 // Channel 2 - Invert output polarity (if set, 0=high and 1=low) +#define PWM_CTL_SBIT2 11 // Channel 2 - Silence bit (default line state when not transmitting) +#define PWM_CTL_RPTL2 10 // Channel 2 - Repeat last data in FIFO +#define PWM_CTL_MODE2 9 // Channel 2 - Mode. 0=PWM, 1=Serializer +#define PWM_CTL_PWEN2 8 // Channel 2 - Enable PWM +#define PWM_CTL_CLRF1 6 // Clear FIFO +#define PWM_CTL_MSEN1 7 // Channel 1 - 0: Use PWM algorithm. 1: Use M/S (serial) algorithm. +#define PWM_CTL_USEF1 5 // Channel 1 - 0: Use PWM_DAT1. 1: Use FIFO. +#define PWM_CTL_POLA1 4 // Channel 1 - Invert output polarity (if set, 0=high and 1=low) +#define PWM_CTL_SBIT1 3 // Channel 1 - Silence bit (default line state when not transmitting) +#define PWM_CTL_RPTL1 2 // Channel 1 - Repeat last data in FIFO +#define PWM_CTL_MODE1 1 // Channel 1 - Mode. 0=PWM, 1=Serializer +#define PWM_CTL_PWEN1 0 // Channel 1 - Enable PWM + +// PWM_STA register bit offsets +// ------------------------------------------------------------------------------------------------- +#define PWM_STA_STA4 12 // Channel 4 State +#define PWM_STA_STA3 11 // Channel 3 State +#define PWM_STA_STA2 10 // Channel 2 State +#define PWM_STA_STA1 9 // Channel 1 State +#define PWM_STA_BERR 8 // Bus Error +#define PWM_STA_GAPO4 7 // Gap Occurred on Channel 4 +#define PWM_STA_GAPO3 6 // Gap Occurred on Channel 3 +#define PWM_STA_GAPO2 5 // Gap Occurred on Channel 2 +#define PWM_STA_GAPO1 4 // Gap Occurred on Channel 1 +#define PWM_STA_RERR1 3 // FIFO Read Error +#define PWM_STA_WERR1 2 // FIFO Write Error +#define PWM_STA_EMPT1 1 // FIFO Empty +#define PWM_STA_FULL1 0 // FIFO Full + +// PWM_DMAC bit offsets +// ------------------------------------------------------------------------------------------------- +#define PWM_DMAC_ENAB 31 // 0: DMA Disabled. 1: DMA Enabled. +#define PWM_DMAC_PANIC 8 // Bits 15:8. Threshold for PANIC signal. Default 7. +#define PWM_DMAC_DREQ 0 // Bits 7:0. Threshold for DREQ signal. Default 7. + +// PWM_RNG1, PWM_RNG2 +// -------------------------------------------------------------------------------------------------- +// Defines the transmission range. In PWM mode, evenly spaced pulses are sent within a period +// of length defined in these registers. In serial mode, serialized data is sent within the +// same period. The value is normally 32. If less, data will be truncated. If more, data will +// be padded with zeros. + +// DAT1, DAT2 +// -------------------------------------------------------------------------------------------------- +// NOTE: These registers are not useful for our purposes - we will use the FIFO instead! +// Stores 32 bits of data to be sent when USEF1/USEF2 is 0. In PWM mode, defines how many +// pulses will be sent within the period specified in PWM_RNG1/PWM_RNG2. In serializer mode, +// defines a 32-bit word to be transmitted. + +// FIF1 +// -------------------------------------------------------------------------------------------------- +// 32-bit-wide register used to "stuff" the FIFO, which has 16 32-bit words. (So, if you write +// it 16 times, it will fill the FIFO.) +// See also: PWM_STA_EMPT1 (FIFO empty) +// PWM_STA_FULL1 (FIFO full) +// PWM_CTL_CLRF1 (Clear FIFO) + +// DMA +// -------------------------------------------------------------------------------------------------- +// DMA registers (divided by four to convert form word to byte offsets, as with the PWM registers) +#define DMA_CS (0x00 / 4) // Control & Status register +#define DMA_CONBLK_AD (0x04 / 4) // Address of Control Block (must be 256-BYTE ALIGNED!!!) +#define DMA_TI (0x08 / 4) // Transfer Information (populated from CB) +#define DMA_SOURCE_AD (0x0C / 4) // Source address, populated from CB. Physical address. +#define DMA_DEST_AD (0x10 / 4) // Destination address, populated from CB. Bus address. +#define DMA_TXFR_LEN (0x14 / 4) // Transfer length, populated from CB +#define DMA_STRIDE (0x18 / 4) // Stride, populated from CB +#define DMA_NEXTCONBK (0x1C / 4) // Next control block address, populated from CB +#define DMA_DEBUG (0x20 / 4) // Debug settings + +// DMA Control & Status register bit offsets +#define DMA_CS_RESET 31 // Reset the controller for this channel +#define DMA_CS_ABORT 30 // Set to abort transfer +#define DMA_CS_DISDEBUG 29 // Disable debug pause signal +#define DMA_CS_WAIT_FOR 28 // Wait for outstanding writes +#define DMA_CS_PANIC_PRI 20 // Panic priority (bits 23:20), default 7 +#define DMA_CS_PRIORITY 16 // AXI priority level (bits 19:16), default 7 +#define DMA_CS_ERROR 8 // Set when there's been an error +#define DMA_CS_WAITING_FOR 6 // Set when the channel's waiting for a write to be accepted +#define DMA_CS_DREQ_STOPS_DMA 5 // Set when the DMA is paused because DREQ is inactive +#define DMA_CS_PAUSED 4 // Set when the DMA is paused (active bit cleared, etc.) +#define DMA_CS_DREQ 3 // Set when DREQ line is high +#define DMA_CS_INT 2 // If INTEN is set, this will be set on CB transfer end +#define DMA_CS_END 1 // Set when the current control block is finished +#define DMA_CS_ACTIVE 0 // Enable DMA (CB_ADDR must not be 0) +// Default CS word +#define DMA_CS_CONFIGWORD (8 << DMA_CS_PANIC_PRI) | \ + (8 << DMA_CS_PRIORITY) | \ + (1 << DMA_CS_WAIT_FOR) + +// DREQ lines (page 61, most DREQs omitted) +#define DMA_DREQ_ALWAYS 0 +#define DMA_DREQ_PCM_TX 2 +#define DMA_DREQ_PCM_RX 3 +#define DMA_DREQ_PWM 5 +#define DMA_DREQ_SPI_TX 6 +#define DMA_DREQ_SPI_RX 7 +#define DMA_DREQ_BSC_TX 8 +#define DMA_DREQ_BSC_RX 9 + +// DMA Transfer Information register bit offsets +// We don't write DMA_TI directly. It's populated from the TI field in a control block. +#define DMA_TI_NO_WIDE_BURSTS 26 // Don't do wide writes in 2-beat bursts +#define DMA_TI_WAITS 21 // Wait this many cycles after end of each read/write +#define DMA_TI_PERMAP 16 // Peripheral # whose ready signal controls xfer rate (pwm=5) +#define DMA_TI_BURST_LENGTH 12 // Length of burst in words (bits 15:12) +#define DMA_TI_SRC_IGNORE 11 // Don't perform source reads (for fast cache fill) +#define DMA_TI_SRC_DREQ 10 // Peripheral in PERMAP gates source reads +#define DMA_TI_SRC_WIDTH 9 // Source transfer width - 0=32 bits, 1=128 bits +#define DMA_TI_SRC_INC 8 // Source address += SRC_WITH after each read +#define DMA_TI_DEST_IGNORE 7 // Don't perform destination writes +#define DMA_TI_DEST_DREQ 6 // Peripheral in PERMAP gates destination writes +#define DMA_TI_DEST_WIDTH 5 // Destination transfer width - 0=32 bits, 1=128 bits +#define DMA_TI_DEST_INC 4 // Dest address += DEST_WIDTH after each read +#define DMA_TI_WAIT_RESP 3 // Wait for write response +#define DMA_TI_TDMODE 1 // 2D striding mode +#define DMA_TI_INTEN 0 // Interrupt enable +// Default TI word +#define DMA_TI_CONFIGWORD (1 << DMA_TI_NO_WIDE_BURSTS) | \ + (1 << DMA_TI_SRC_INC) | \ + (1 << DMA_TI_DEST_DREQ) | \ + (1 << DMA_TI_WAIT_RESP) | \ + (1 << DMA_TI_INTEN) | \ + (DMA_DREQ_PWM << DMA_TI_PERMAP) + +// DMA Debug register bit offsets +#define DMA_DEBUG_LITE 28 // Whether the controller is "Lite" +#define DMA_DEBUG_VERSION 25 // DMA Version (bits 27:25) +#define DMA_DEBUG_DMA_STATE 16 // DMA State (bits 24:16) +#define DMA_DEBUG_DMA_ID 8 // DMA controller's AXI bus ID (bits 15:8) +#define DMA_DEBUG_OUTSTANDING_WRITES 4 // Outstanding writes (bits 7:4) +#define DMA_DEBUG_READ_ERROR 2 // Slave read response error (clear by setting) +#define DMA_DEBUG_FIFO_ERROR 1 // Operational read FIFO error (clear by setting) +#define DMA_DEBUG_READ_LAST_NOT_SET 0 // AXI bus read last signal not set (clear by setting) + +// Control Block (CB) - this tells the DMA controller what to do. +typedef struct { + unsigned int + info, // Transfer Information (TI) + src, // Source address (physical) + dst, // Destination address (bus) + length, // Length in bytes (not words!) + stride, // We don't care about this + next, // Pointer to next control block + pad[2]; // These are "reserved" (unused) +} dma_cb_t; + +// The page map contains pointers to memory that we will allocate below. It uses two pointers +// per address. This is because the software (this program) deals only in virtual addresses, +// whereas the DMA controller can only access RAM via physical address. (If that's not confusing +// enough, it writes to peripherals by their bus addresses.) +typedef struct { + uint8_t *virtaddr; + uint32_t physaddr; +} page_map_t; + + +#define PAGE_SIZE 4096 // Size of a RAM page to be allocated +#define PAGE_SHIFT 12 // This is used for address translation +#define NUM_PAGES ((sizeof(struct control_data_s) + PAGE_SIZE - 1) >> PAGE_SHIFT) + +#define SETBIT(word, bit) word |= 1< &ledValues); + + /// Switch the leds off + virtual int switchOff(); + +private: + + /// the number of leds (needed when switching off) + size_t mLedCount; + + page_map_t *page_map; // This will hold the page map, which we'll allocate below + uint8_t *virtbase; // Pointer to some virtual memory that will be allocated + + volatile unsigned int *pwm_reg; // PWM controller register set + volatile unsigned int *clk_reg; // PWM clock manager register set + volatile unsigned int *dma_reg; // DMA controller register set + volatile unsigned int *gpio_reg; // GPIO pin controller register set + + // Contains arrays of control blocks and their related samples. + // One pixel needs 72 bits (24 bits for the color * 3 to represent them on the wire). + // 768 words = 341.3 pixels + // 1024 words = 455.1 pixels + // The highest I can make this number is 1016. Any higher, and it will start copying garbage to the + // PWM controller. I think it might be because of the virtual->physical memory mapping not being + // contiguous, so *pointer+1016 isn't "next door" to *pointer+1017 for some weird reason. + // However, that's still enough for 451.5 color instructions! If someone has more pixels than that + // to control, they can figure it out. I tried Hirst's message of having one CB per word, which + // seems like it might fix that, but I couldn't figure it out. + #define NUM_DATA_WORDS 1016 + struct control_data_s { + dma_cb_t cb[1]; + uint32_t sample[NUM_DATA_WORDS]; + }; + + struct control_data_s *ctl; + + // PWM waveform buffer (in words), 16 32-bit words are enough to hold 170 wire bits. + // That's OK if we only transmit from the FIFO, but for DMA, we will use a much larger size. + // 1024 (4096 bytes) should be enough for over 400 elements. It can be bumped up if you need more! + unsigned int PWMWaveform[NUM_DATA_WORDS]; + + void initHardware(); + void startTransfer(); + + void clearPWMBuffer(); + void setPWMBit(unsigned int bitPos, unsigned char bit); + + unsigned int mem_phys_to_virt(uint32_t phys); + unsigned int mem_virt_to_phys(void *virt); + void terminate(int dummy); + void fatal(char *fmt, ...); + void * map_peripheral(uint32_t base, uint32_t len); + void printBinary(unsigned int i, unsigned int bits); +}; + + + + + + + + + + + + +#endif /* LEDDEVICEWS2812S_H_ */ From 61da05e1081777ae4708f593669968159b0fe8a5 Mon Sep 17 00:00:00 2001 From: David Brodski Date: Wed, 17 Sep 2014 21:12:46 +0200 Subject: [PATCH 11/59] Moved defines into cpp file make include of h file smaller removed not needed includes fixed warnings (removed some ascii art for that) Former-commit-id: 71b16cf7e73a9463462820238d12069e4d1e6d6e --- libsrc/leddevice/LedDeviceWS2812s.cpp | 227 +++++++++++++++++++++- libsrc/leddevice/LedDeviceWS2812s.h | 260 +------------------------- 2 files changed, 226 insertions(+), 261 deletions(-) diff --git a/libsrc/leddevice/LedDeviceWS2812s.cpp b/libsrc/leddevice/LedDeviceWS2812s.cpp index 545519b0..d7fd19b4 100644 --- a/libsrc/leddevice/LedDeviceWS2812s.cpp +++ b/libsrc/leddevice/LedDeviceWS2812s.cpp @@ -9,11 +9,224 @@ // Linux includes #include +#include +#include +//#include //#include // hyperion local includes #include "LedDeviceWS2812s.h" +// ==== Defines and Vars ==== + +// Base addresses for GPIO, PWM, PWM clock, and DMA controllers (physical, not bus!) +// These will be "memory mapped" into virtual RAM so that they can be written and read directly. +// ------------------------------------------------------------------------------------------------- +#define DMA_BASE 0x20007000 +#define DMA_LEN 0x24 +#define PWM_BASE 0x2020C000 +#define PWM_LEN 0x28 +#define CLK_BASE 0x20101000 +#define CLK_LEN 0xA8 +#define GPIO_BASE 0x20200000 +#define GPIO_LEN 0xB4 + +// GPIO +// ------------------------------------------------------------------------------------------------- +#define GPFSEL0 0x20200000 // GPIO function select, pins 0-9 (bits 30-31 reserved) +#define GPFSEL1 0x20200004 // Pins 10-19 +#define GPFSEL2 0x20200008 // Pins 20-29 +#define GPFSEL3 0x2020000C // Pins 30-39 +#define GPFSEL4 0x20200010 // Pins 40-49 +#define GPFSEL5 0x20200014 // Pins 50-53 +#define GPSET0 0x2020001C // Set (turn on) pin +#define GPCLR0 0x20200028 // Clear (turn off) pin +#define GPPUD 0x20200094 // Internal pullup/pulldown resistor control +#define GPPUDCLK0 0x20200098 // PUD clock for pins 0-31 +#define GPPUDCLK1 0x2020009C // PUD clock for pins 32-53 + +// Memory offsets for the PWM clock register, which is undocumented! Please fix that, Broadcom! +// ------------------------------------------------------------------------------------------------- +#define PWM_CLK_CNTL 40 // Control (on/off) +#define PWM_CLK_DIV 41 // Divisor (bits 11:0 are *quantized* floating part, 31:12 integer part) + +// PWM Register Addresses (page 141) +// These are divided by 4 because the register offsets in the guide are in bytes (8 bits) but +// the pointers we use in this program are in words (32 bits). Buss' original defines are in +// word offsets, e.g. PWM_RNG1 was 4 and PWM_DAT1 was 5. This is functionally the same, but it +// matches the numbers supplied in the guide. +// ------------------------------------------------------------------------------------------------- +#define PWM_CTL 0x00 // Control Register +#define PWM_STA (0x04 / 4) // Status Register +#define PWM_DMAC (0x08 / 4) // DMA Control Register +#define PWM_RNG1 (0x10 / 4) // Channel 1 Range +#define PWM_DAT1 (0x14 / 4) // Channel 1 Data +#define PWM_FIF1 (0x18 / 4) // FIFO (for both channels - bytes are interleaved if both active) +#define PWM_RNG2 (0x20 / 4) // Channel 2 Range +#define PWM_DAT2 (0x24 / 4) // Channel 2 Data + +// PWM_CTL register bit offsets +// Note: Don't use MSEN1/2 for this purpose. It will screw things up. +// ------------------------------------------------------------------------------------------------- +#define PWM_CTL_MSEN2 15 // Channel 2 - 0: Use PWM algorithm. 1: Use M/S (serial) algorithm. +#define PWM_CTL_USEF2 13 // Channel 2 - 0: Use PWM_DAT2. 1: Use FIFO. +#define PWM_CTL_POLA2 12 // Channel 2 - Invert output polarity (if set, 0=high and 1=low) +#define PWM_CTL_SBIT2 11 // Channel 2 - Silence bit (default line state when not transmitting) +#define PWM_CTL_RPTL2 10 // Channel 2 - Repeat last data in FIFO +#define PWM_CTL_MODE2 9 // Channel 2 - Mode. 0=PWM, 1=Serializer +#define PWM_CTL_PWEN2 8 // Channel 2 - Enable PWM +#define PWM_CTL_CLRF1 6 // Clear FIFO +#define PWM_CTL_MSEN1 7 // Channel 1 - 0: Use PWM algorithm. 1: Use M/S (serial) algorithm. +#define PWM_CTL_USEF1 5 // Channel 1 - 0: Use PWM_DAT1. 1: Use FIFO. +#define PWM_CTL_POLA1 4 // Channel 1 - Invert output polarity (if set, 0=high and 1=low) +#define PWM_CTL_SBIT1 3 // Channel 1 - Silence bit (default line state when not transmitting) +#define PWM_CTL_RPTL1 2 // Channel 1 - Repeat last data in FIFO +#define PWM_CTL_MODE1 1 // Channel 1 - Mode. 0=PWM, 1=Serializer +#define PWM_CTL_PWEN1 0 // Channel 1 - Enable PWM + +// PWM_STA register bit offsets +// ------------------------------------------------------------------------------------------------- +#define PWM_STA_STA4 12 // Channel 4 State +#define PWM_STA_STA3 11 // Channel 3 State +#define PWM_STA_STA2 10 // Channel 2 State +#define PWM_STA_STA1 9 // Channel 1 State +#define PWM_STA_BERR 8 // Bus Error +#define PWM_STA_GAPO4 7 // Gap Occurred on Channel 4 +#define PWM_STA_GAPO3 6 // Gap Occurred on Channel 3 +#define PWM_STA_GAPO2 5 // Gap Occurred on Channel 2 +#define PWM_STA_GAPO1 4 // Gap Occurred on Channel 1 +#define PWM_STA_RERR1 3 // FIFO Read Error +#define PWM_STA_WERR1 2 // FIFO Write Error +#define PWM_STA_EMPT1 1 // FIFO Empty +#define PWM_STA_FULL1 0 // FIFO Full + +// PWM_DMAC bit offsets +// ------------------------------------------------------------------------------------------------- +#define PWM_DMAC_ENAB 31 // 0: DMA Disabled. 1: DMA Enabled. +#define PWM_DMAC_PANIC 8 // Bits 15:8. Threshold for PANIC signal. Default 7. +#define PWM_DMAC_DREQ 0 // Bits 7:0. Threshold for DREQ signal. Default 7. + +// PWM_RNG1, PWM_RNG2 +// -------------------------------------------------------------------------------------------------- +// Defines the transmission range. In PWM mode, evenly spaced pulses are sent within a period +// of length defined in these registers. In serial mode, serialized data is sent within the +// same period. The value is normally 32. If less, data will be truncated. If more, data will +// be padded with zeros. + +// DAT1, DAT2 +// -------------------------------------------------------------------------------------------------- +// NOTE: These registers are not useful for our purposes - we will use the FIFO instead! +// Stores 32 bits of data to be sent when USEF1/USEF2 is 0. In PWM mode, defines how many +// pulses will be sent within the period specified in PWM_RNG1/PWM_RNG2. In serializer mode, +// defines a 32-bit word to be transmitted. + +// FIF1 +// -------------------------------------------------------------------------------------------------- +// 32-bit-wide register used to "stuff" the FIFO, which has 16 32-bit words. (So, if you write +// it 16 times, it will fill the FIFO.) +// See also: PWM_STA_EMPT1 (FIFO empty) +// PWM_STA_FULL1 (FIFO full) +// PWM_CTL_CLRF1 (Clear FIFO) + +// DMA +// -------------------------------------------------------------------------------------------------- +// DMA registers (divided by four to convert form word to byte offsets, as with the PWM registers) +#define DMA_CS (0x00 / 4) // Control & Status register +#define DMA_CONBLK_AD (0x04 / 4) // Address of Control Block (must be 256-BYTE ALIGNED!!!) +#define DMA_TI (0x08 / 4) // Transfer Information (populated from CB) +#define DMA_SOURCE_AD (0x0C / 4) // Source address, populated from CB. Physical address. +#define DMA_DEST_AD (0x10 / 4) // Destination address, populated from CB. Bus address. +#define DMA_TXFR_LEN (0x14 / 4) // Transfer length, populated from CB +#define DMA_STRIDE (0x18 / 4) // Stride, populated from CB +#define DMA_NEXTCONBK (0x1C / 4) // Next control block address, populated from CB +#define DMA_DEBUG (0x20 / 4) // Debug settings + +// DMA Control & Status register bit offsets +#define DMA_CS_RESET 31 // Reset the controller for this channel +#define DMA_CS_ABORT 30 // Set to abort transfer +#define DMA_CS_DISDEBUG 29 // Disable debug pause signal +#define DMA_CS_WAIT_FOR 28 // Wait for outstanding writes +#define DMA_CS_PANIC_PRI 20 // Panic priority (bits 23:20), default 7 +#define DMA_CS_PRIORITY 16 // AXI priority level (bits 19:16), default 7 +#define DMA_CS_ERROR 8 // Set when there's been an error +#define DMA_CS_WAITING_FOR 6 // Set when the channel's waiting for a write to be accepted +#define DMA_CS_DREQ_STOPS_DMA 5 // Set when the DMA is paused because DREQ is inactive +#define DMA_CS_PAUSED 4 // Set when the DMA is paused (active bit cleared, etc.) +#define DMA_CS_DREQ 3 // Set when DREQ line is high +#define DMA_CS_INT 2 // If INTEN is set, this will be set on CB transfer end +#define DMA_CS_END 1 // Set when the current control block is finished +#define DMA_CS_ACTIVE 0 // Enable DMA (CB_ADDR must not be 0) +// Default CS word +#define DMA_CS_CONFIGWORD (8 << DMA_CS_PANIC_PRI) | \ + (8 << DMA_CS_PRIORITY) | \ + (1 << DMA_CS_WAIT_FOR) + +// DREQ lines (page 61, most DREQs omitted) +#define DMA_DREQ_ALWAYS 0 +#define DMA_DREQ_PCM_TX 2 +#define DMA_DREQ_PCM_RX 3 +#define DMA_DREQ_PWM 5 +#define DMA_DREQ_SPI_TX 6 +#define DMA_DREQ_SPI_RX 7 +#define DMA_DREQ_BSC_TX 8 +#define DMA_DREQ_BSC_RX 9 + +// DMA Transfer Information register bit offsets +// We don't write DMA_TI directly. It's populated from the TI field in a control block. +#define DMA_TI_NO_WIDE_BURSTS 26 // Don't do wide writes in 2-beat bursts +#define DMA_TI_WAITS 21 // Wait this many cycles after end of each read/write +#define DMA_TI_PERMAP 16 // Peripheral # whose ready signal controls xfer rate (pwm=5) +#define DMA_TI_BURST_LENGTH 12 // Length of burst in words (bits 15:12) +#define DMA_TI_SRC_IGNORE 11 // Don't perform source reads (for fast cache fill) +#define DMA_TI_SRC_DREQ 10 // Peripheral in PERMAP gates source reads +#define DMA_TI_SRC_WIDTH 9 // Source transfer width - 0=32 bits, 1=128 bits +#define DMA_TI_SRC_INC 8 // Source address += SRC_WITH after each read +#define DMA_TI_DEST_IGNORE 7 // Don't perform destination writes +#define DMA_TI_DEST_DREQ 6 // Peripheral in PERMAP gates destination writes +#define DMA_TI_DEST_WIDTH 5 // Destination transfer width - 0=32 bits, 1=128 bits +#define DMA_TI_DEST_INC 4 // Dest address += DEST_WIDTH after each read +#define DMA_TI_WAIT_RESP 3 // Wait for write response +#define DMA_TI_TDMODE 1 // 2D striding mode +#define DMA_TI_INTEN 0 // Interrupt enable +// Default TI word +#define DMA_TI_CONFIGWORD (1 << DMA_TI_NO_WIDE_BURSTS) | \ + (1 << DMA_TI_SRC_INC) | \ + (1 << DMA_TI_DEST_DREQ) | \ + (1 << DMA_TI_WAIT_RESP) | \ + (1 << DMA_TI_INTEN) | \ + (DMA_DREQ_PWM << DMA_TI_PERMAP) + +// DMA Debug register bit offsets +#define DMA_DEBUG_LITE 28 // Whether the controller is "Lite" +#define DMA_DEBUG_VERSION 25 // DMA Version (bits 27:25) +#define DMA_DEBUG_DMA_STATE 16 // DMA State (bits 24:16) +#define DMA_DEBUG_DMA_ID 8 // DMA controller's AXI bus ID (bits 15:8) +#define DMA_DEBUG_OUTSTANDING_WRITES 4 // Outstanding writes (bits 7:4) +#define DMA_DEBUG_READ_ERROR 2 // Slave read response error (clear by setting) +#define DMA_DEBUG_FIFO_ERROR 1 // Operational read FIFO error (clear by setting) +#define DMA_DEBUG_READ_LAST_NOT_SET 0 // AXI bus read last signal not set (clear by setting) + + + +#define PAGE_SIZE 4096 // Size of a RAM page to be allocated +#define PAGE_SHIFT 12 // This is used for address translation +#define NUM_PAGES ((sizeof(struct control_data_s) + PAGE_SIZE - 1) >> PAGE_SHIFT) + +#define SETBIT(word, bit) word |= 1< -// \/ \/ \/ \/ \/ \/ -// ================================================================================================= +// ==== Init Hardware ==== void LedDeviceWS2812s::initHardware() { int pid; @@ -341,7 +547,8 @@ void LedDeviceWS2812s::initHardware() { fatal("Failed to open %s: %m\n", pagemap_fn); } - if (lseek(fd, (unsigned long)virtbase >> 9, SEEK_SET) != (unsigned long)virtbase >> 9) { + off_t newOffset = (unsigned long)virtbase >> 9; + if (lseek(fd, newOffset, SEEK_SET) != newOffset) { fatal("Failed to seek on %s: %m\n", pagemap_fn); } diff --git a/libsrc/leddevice/LedDeviceWS2812s.h b/libsrc/leddevice/LedDeviceWS2812s.h index 2dca0fb0..d92ef39f 100644 --- a/libsrc/leddevice/LedDeviceWS2812s.h +++ b/libsrc/leddevice/LedDeviceWS2812s.h @@ -98,233 +98,18 @@ // Adafruit's NeoPixel driver: // https://github.com/adafruit/Adafruit_NeoPixel/blob/master/Adafruit_NeoPixel.cpp - -// ================================================================================================= -// .___ .__ .___ -// | | ____ ____ | | __ __ __| _/____ ______ -// | |/ \_/ ___\| | | | \/ __ |/ __ \ / ___/ -// | | | \ \___| |_| | / /_/ \ ___/ \___ \ -// |___|___| /\___ >____/____/\____ |\___ >____ > -// \/ \/ \/ \/ \/ -// ================================================================================================= - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - // Hyperion includes #include -// ================================================================================================= -// ________ _____.__ ____ ____ ____ -// \______ \ _____/ ____\__| ____ ____ ______ / _ \ \ \ / /____ _______ ______ -// | | \_/ __ \ __\| |/ \_/ __ \ / ___/ > _ __| |__|___| /\___ >____ > \_____\ \ \___/ (____ /__| /____ > -// \/ \/ \/ \/ \/ \/ \/ \/ -// ================================================================================================= - -// Base addresses for GPIO, PWM, PWM clock, and DMA controllers (physical, not bus!) -// These will be "memory mapped" into virtual RAM so that they can be written and read directly. -// ------------------------------------------------------------------------------------------------- -#define DMA_BASE 0x20007000 -#define DMA_LEN 0x24 -#define PWM_BASE 0x2020C000 -#define PWM_LEN 0x28 -#define CLK_BASE 0x20101000 -#define CLK_LEN 0xA8 -#define GPIO_BASE 0x20200000 -#define GPIO_LEN 0xB4 - -// GPIO -// ------------------------------------------------------------------------------------------------- -#define GPFSEL0 0x20200000 // GPIO function select, pins 0-9 (bits 30-31 reserved) -#define GPFSEL1 0x20200004 // Pins 10-19 -#define GPFSEL2 0x20200008 // Pins 20-29 -#define GPFSEL3 0x2020000C // Pins 30-39 -#define GPFSEL4 0x20200010 // Pins 40-49 -#define GPFSEL5 0x20200014 // Pins 50-53 -#define GPSET0 0x2020001C // Set (turn on) pin -#define GPCLR0 0x20200028 // Clear (turn off) pin -#define GPPUD 0x20200094 // Internal pullup/pulldown resistor control -#define GPPUDCLK0 0x20200098 // PUD clock for pins 0-31 -#define GPPUDCLK1 0x2020009C // PUD clock for pins 32-53 - -// Memory offsets for the PWM clock register, which is undocumented! Please fix that, Broadcom! -// ------------------------------------------------------------------------------------------------- -#define PWM_CLK_CNTL 40 // Control (on/off) -#define PWM_CLK_DIV 41 // Divisor (bits 11:0 are *quantized* floating part, 31:12 integer part) - -// PWM Register Addresses (page 141) -// These are divided by 4 because the register offsets in the guide are in bytes (8 bits) but -// the pointers we use in this program are in words (32 bits). Buss' original defines are in -// word offsets, e.g. PWM_RNG1 was 4 and PWM_DAT1 was 5. This is functionally the same, but it -// matches the numbers supplied in the guide. -// ------------------------------------------------------------------------------------------------- -#define PWM_CTL 0x00 // Control Register -#define PWM_STA (0x04 / 4) // Status Register -#define PWM_DMAC (0x08 / 4) // DMA Control Register -#define PWM_RNG1 (0x10 / 4) // Channel 1 Range -#define PWM_DAT1 (0x14 / 4) // Channel 1 Data -#define PWM_FIF1 (0x18 / 4) // FIFO (for both channels - bytes are interleaved if both active) -#define PWM_RNG2 (0x20 / 4) // Channel 2 Range -#define PWM_DAT2 (0x24 / 4) // Channel 2 Data - -// PWM_CTL register bit offsets -// Note: Don't use MSEN1/2 for this purpose. It will screw things up. -// ------------------------------------------------------------------------------------------------- -#define PWM_CTL_MSEN2 15 // Channel 2 - 0: Use PWM algorithm. 1: Use M/S (serial) algorithm. -#define PWM_CTL_USEF2 13 // Channel 2 - 0: Use PWM_DAT2. 1: Use FIFO. -#define PWM_CTL_POLA2 12 // Channel 2 - Invert output polarity (if set, 0=high and 1=low) -#define PWM_CTL_SBIT2 11 // Channel 2 - Silence bit (default line state when not transmitting) -#define PWM_CTL_RPTL2 10 // Channel 2 - Repeat last data in FIFO -#define PWM_CTL_MODE2 9 // Channel 2 - Mode. 0=PWM, 1=Serializer -#define PWM_CTL_PWEN2 8 // Channel 2 - Enable PWM -#define PWM_CTL_CLRF1 6 // Clear FIFO -#define PWM_CTL_MSEN1 7 // Channel 1 - 0: Use PWM algorithm. 1: Use M/S (serial) algorithm. -#define PWM_CTL_USEF1 5 // Channel 1 - 0: Use PWM_DAT1. 1: Use FIFO. -#define PWM_CTL_POLA1 4 // Channel 1 - Invert output polarity (if set, 0=high and 1=low) -#define PWM_CTL_SBIT1 3 // Channel 1 - Silence bit (default line state when not transmitting) -#define PWM_CTL_RPTL1 2 // Channel 1 - Repeat last data in FIFO -#define PWM_CTL_MODE1 1 // Channel 1 - Mode. 0=PWM, 1=Serializer -#define PWM_CTL_PWEN1 0 // Channel 1 - Enable PWM - -// PWM_STA register bit offsets -// ------------------------------------------------------------------------------------------------- -#define PWM_STA_STA4 12 // Channel 4 State -#define PWM_STA_STA3 11 // Channel 3 State -#define PWM_STA_STA2 10 // Channel 2 State -#define PWM_STA_STA1 9 // Channel 1 State -#define PWM_STA_BERR 8 // Bus Error -#define PWM_STA_GAPO4 7 // Gap Occurred on Channel 4 -#define PWM_STA_GAPO3 6 // Gap Occurred on Channel 3 -#define PWM_STA_GAPO2 5 // Gap Occurred on Channel 2 -#define PWM_STA_GAPO1 4 // Gap Occurred on Channel 1 -#define PWM_STA_RERR1 3 // FIFO Read Error -#define PWM_STA_WERR1 2 // FIFO Write Error -#define PWM_STA_EMPT1 1 // FIFO Empty -#define PWM_STA_FULL1 0 // FIFO Full - -// PWM_DMAC bit offsets -// ------------------------------------------------------------------------------------------------- -#define PWM_DMAC_ENAB 31 // 0: DMA Disabled. 1: DMA Enabled. -#define PWM_DMAC_PANIC 8 // Bits 15:8. Threshold for PANIC signal. Default 7. -#define PWM_DMAC_DREQ 0 // Bits 7:0. Threshold for DREQ signal. Default 7. - -// PWM_RNG1, PWM_RNG2 -// -------------------------------------------------------------------------------------------------- -// Defines the transmission range. In PWM mode, evenly spaced pulses are sent within a period -// of length defined in these registers. In serial mode, serialized data is sent within the -// same period. The value is normally 32. If less, data will be truncated. If more, data will -// be padded with zeros. - -// DAT1, DAT2 -// -------------------------------------------------------------------------------------------------- -// NOTE: These registers are not useful for our purposes - we will use the FIFO instead! -// Stores 32 bits of data to be sent when USEF1/USEF2 is 0. In PWM mode, defines how many -// pulses will be sent within the period specified in PWM_RNG1/PWM_RNG2. In serializer mode, -// defines a 32-bit word to be transmitted. - -// FIF1 -// -------------------------------------------------------------------------------------------------- -// 32-bit-wide register used to "stuff" the FIFO, which has 16 32-bit words. (So, if you write -// it 16 times, it will fill the FIFO.) -// See also: PWM_STA_EMPT1 (FIFO empty) -// PWM_STA_FULL1 (FIFO full) -// PWM_CTL_CLRF1 (Clear FIFO) - -// DMA -// -------------------------------------------------------------------------------------------------- -// DMA registers (divided by four to convert form word to byte offsets, as with the PWM registers) -#define DMA_CS (0x00 / 4) // Control & Status register -#define DMA_CONBLK_AD (0x04 / 4) // Address of Control Block (must be 256-BYTE ALIGNED!!!) -#define DMA_TI (0x08 / 4) // Transfer Information (populated from CB) -#define DMA_SOURCE_AD (0x0C / 4) // Source address, populated from CB. Physical address. -#define DMA_DEST_AD (0x10 / 4) // Destination address, populated from CB. Bus address. -#define DMA_TXFR_LEN (0x14 / 4) // Transfer length, populated from CB -#define DMA_STRIDE (0x18 / 4) // Stride, populated from CB -#define DMA_NEXTCONBK (0x1C / 4) // Next control block address, populated from CB -#define DMA_DEBUG (0x20 / 4) // Debug settings - -// DMA Control & Status register bit offsets -#define DMA_CS_RESET 31 // Reset the controller for this channel -#define DMA_CS_ABORT 30 // Set to abort transfer -#define DMA_CS_DISDEBUG 29 // Disable debug pause signal -#define DMA_CS_WAIT_FOR 28 // Wait for outstanding writes -#define DMA_CS_PANIC_PRI 20 // Panic priority (bits 23:20), default 7 -#define DMA_CS_PRIORITY 16 // AXI priority level (bits 19:16), default 7 -#define DMA_CS_ERROR 8 // Set when there's been an error -#define DMA_CS_WAITING_FOR 6 // Set when the channel's waiting for a write to be accepted -#define DMA_CS_DREQ_STOPS_DMA 5 // Set when the DMA is paused because DREQ is inactive -#define DMA_CS_PAUSED 4 // Set when the DMA is paused (active bit cleared, etc.) -#define DMA_CS_DREQ 3 // Set when DREQ line is high -#define DMA_CS_INT 2 // If INTEN is set, this will be set on CB transfer end -#define DMA_CS_END 1 // Set when the current control block is finished -#define DMA_CS_ACTIVE 0 // Enable DMA (CB_ADDR must not be 0) -// Default CS word -#define DMA_CS_CONFIGWORD (8 << DMA_CS_PANIC_PRI) | \ - (8 << DMA_CS_PRIORITY) | \ - (1 << DMA_CS_WAIT_FOR) - -// DREQ lines (page 61, most DREQs omitted) -#define DMA_DREQ_ALWAYS 0 -#define DMA_DREQ_PCM_TX 2 -#define DMA_DREQ_PCM_RX 3 -#define DMA_DREQ_PWM 5 -#define DMA_DREQ_SPI_TX 6 -#define DMA_DREQ_SPI_RX 7 -#define DMA_DREQ_BSC_TX 8 -#define DMA_DREQ_BSC_RX 9 - -// DMA Transfer Information register bit offsets -// We don't write DMA_TI directly. It's populated from the TI field in a control block. -#define DMA_TI_NO_WIDE_BURSTS 26 // Don't do wide writes in 2-beat bursts -#define DMA_TI_WAITS 21 // Wait this many cycles after end of each read/write -#define DMA_TI_PERMAP 16 // Peripheral # whose ready signal controls xfer rate (pwm=5) -#define DMA_TI_BURST_LENGTH 12 // Length of burst in words (bits 15:12) -#define DMA_TI_SRC_IGNORE 11 // Don't perform source reads (for fast cache fill) -#define DMA_TI_SRC_DREQ 10 // Peripheral in PERMAP gates source reads -#define DMA_TI_SRC_WIDTH 9 // Source transfer width - 0=32 bits, 1=128 bits -#define DMA_TI_SRC_INC 8 // Source address += SRC_WITH after each read -#define DMA_TI_DEST_IGNORE 7 // Don't perform destination writes -#define DMA_TI_DEST_DREQ 6 // Peripheral in PERMAP gates destination writes -#define DMA_TI_DEST_WIDTH 5 // Destination transfer width - 0=32 bits, 1=128 bits -#define DMA_TI_DEST_INC 4 // Dest address += DEST_WIDTH after each read -#define DMA_TI_WAIT_RESP 3 // Wait for write response -#define DMA_TI_TDMODE 1 // 2D striding mode -#define DMA_TI_INTEN 0 // Interrupt enable -// Default TI word -#define DMA_TI_CONFIGWORD (1 << DMA_TI_NO_WIDE_BURSTS) | \ - (1 << DMA_TI_SRC_INC) | \ - (1 << DMA_TI_DEST_DREQ) | \ - (1 << DMA_TI_WAIT_RESP) | \ - (1 << DMA_TI_INTEN) | \ - (DMA_DREQ_PWM << DMA_TI_PERMAP) - -// DMA Debug register bit offsets -#define DMA_DEBUG_LITE 28 // Whether the controller is "Lite" -#define DMA_DEBUG_VERSION 25 // DMA Version (bits 27:25) -#define DMA_DEBUG_DMA_STATE 16 // DMA State (bits 24:16) -#define DMA_DEBUG_DMA_ID 8 // DMA controller's AXI bus ID (bits 15:8) -#define DMA_DEBUG_OUTSTANDING_WRITES 4 // Outstanding writes (bits 7:4) -#define DMA_DEBUG_READ_ERROR 2 // Slave read response error (clear by setting) -#define DMA_DEBUG_FIFO_ERROR 1 // Operational read FIFO error (clear by setting) -#define DMA_DEBUG_READ_LAST_NOT_SET 0 // AXI bus read last signal not set (clear by setting) +// The page map contains pointers to memory that we will allocate below. It uses two pointers +// per address. This is because the software (this program) deals only in virtual addresses, +// whereas the DMA controller can only access RAM via physical address. (If that's not confusing +// enough, it writes to peripherals by their bus addresses.) +typedef struct { + uint8_t *virtaddr; + uint32_t physaddr; +} page_map_t; // Control Block (CB) - this tells the DMA controller what to do. typedef struct { @@ -338,33 +123,6 @@ typedef struct { pad[2]; // These are "reserved" (unused) } dma_cb_t; -// The page map contains pointers to memory that we will allocate below. It uses two pointers -// per address. This is because the software (this program) deals only in virtual addresses, -// whereas the DMA controller can only access RAM via physical address. (If that's not confusing -// enough, it writes to peripherals by their bus addresses.) -typedef struct { - uint8_t *virtaddr; - uint32_t physaddr; -} page_map_t; - - -#define PAGE_SIZE 4096 // Size of a RAM page to be allocated -#define PAGE_SHIFT 12 // This is used for address translation -#define NUM_PAGES ((sizeof(struct control_data_s) + PAGE_SIZE - 1) >> PAGE_SHIFT) - -#define SETBIT(word, bit) word |= 1< Date: Wed, 17 Sep 2014 21:20:36 +0200 Subject: [PATCH 12/59] Rename WS2812s to WS2812b (spelling error) Former-commit-id: 83c92c9ef99d45b8683860f199a14952c05d0f5d --- libsrc/leddevice/CMakeLists.txt | 4 +-- libsrc/leddevice/LedDeviceFactory.cpp | 8 ++--- ...DeviceWS2812s.cpp => LedDeviceWS2812b.cpp} | 32 +++++++++---------- ...{LedDeviceWS2812s.h => LedDeviceWS2812b.h} | 12 +++---- 4 files changed, 28 insertions(+), 28 deletions(-) rename libsrc/leddevice/{LedDeviceWS2812s.cpp => LedDeviceWS2812b.cpp} (97%) rename libsrc/leddevice/{LedDeviceWS2812s.h => LedDeviceWS2812b.h} (98%) diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt index 0efbd18c..abe28bf5 100755 --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -70,11 +70,11 @@ endif(ENABLE_SPIDEV) SET(Leddevice_HEADERS ${Leddevice_HEADERS} - ${CURRENT_SOURCE_DIR}/LedDeviceWS2812s.h + ${CURRENT_SOURCE_DIR}/LedDeviceWS2812b.h ) SET(Leddevice_SOURCES ${Leddevice_SOURCES} - ${CURRENT_SOURCE_DIR}/LedDeviceWS2812s.cpp + ${CURRENT_SOURCE_DIR}/LedDeviceWS2812b.cpp ) if(ENABLE_TINKERFORGE) diff --git a/libsrc/leddevice/LedDeviceFactory.cpp b/libsrc/leddevice/LedDeviceFactory.cpp index abc0a2c7..0915b4fe 100755 --- a/libsrc/leddevice/LedDeviceFactory.cpp +++ b/libsrc/leddevice/LedDeviceFactory.cpp @@ -31,7 +31,7 @@ #include "LedDevicePhilipsHue.h" #include "LedDeviceTpm2.h" -#include "LedDeviceWS2812s.h" +#include "LedDeviceWS2812b.h" LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) { @@ -183,10 +183,10 @@ LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) LedDeviceTpm2* deviceTpm2 = new LedDeviceTpm2(output, rate); deviceTpm2->open(); device = deviceTpm2; - }else if (type == "ws2812s") + }else if (type == "ws2812b") { - LedDeviceWS2812s * ledDeviceWS2812s = new LedDeviceWS2812s(); - device = ledDeviceWS2812s; + LedDeviceWS2812b * ledDeviceWS2812b = new LedDeviceWS2812b(); + device = ledDeviceWS2812b; } else { diff --git a/libsrc/leddevice/LedDeviceWS2812s.cpp b/libsrc/leddevice/LedDeviceWS2812b.cpp similarity index 97% rename from libsrc/leddevice/LedDeviceWS2812s.cpp rename to libsrc/leddevice/LedDeviceWS2812b.cpp index d7fd19b4..2abbb1b9 100644 --- a/libsrc/leddevice/LedDeviceWS2812s.cpp +++ b/libsrc/leddevice/LedDeviceWS2812b.cpp @@ -1,4 +1,4 @@ -// For license and other informations see LedDeviceWS2812s.h +// For license and other informations see LedDeviceWS2812b.h // To activate: use led device "ws2812s" in the hyperion configuration // STL includes @@ -15,7 +15,7 @@ //#include // hyperion local includes -#include "LedDeviceWS2812s.h" +#include "LedDeviceWS2812b.h" // ==== Defines and Vars ==== @@ -227,7 +227,7 @@ -LedDeviceWS2812s::LedDeviceWS2812s() : +LedDeviceWS2812b::LedDeviceWS2812b() : LedDevice(), mLedCount(0) { @@ -237,7 +237,7 @@ LedDeviceWS2812s::LedDeviceWS2812s() : } -int LedDeviceWS2812s::write(const std::vector &ledValues) +int LedDeviceWS2812b::write(const std::vector &ledValues) { mLedCount = ledValues.size(); //printf("Set leds, number: %d\n", mLedCount); @@ -315,12 +315,12 @@ int LedDeviceWS2812s::write(const std::vector &ledValues) return 0; } -int LedDeviceWS2812s::switchOff() +int LedDeviceWS2812b::switchOff() { return write(std::vector(mLedCount, ColorRgb{0,0,0})); } -LedDeviceWS2812s::~LedDeviceWS2812s() +LedDeviceWS2812b::~LedDeviceWS2812b() { // Exit cleanly, freeing memory and stopping the DMA & PWM engines // We trap all signals (including Ctrl+C), so even if you don't get here, it terminates correctly @@ -340,7 +340,7 @@ LedDeviceWS2812s::~LedDeviceWS2812s() // Convenience functions // -------------------------------------------------------------------------------------------------- // Print some bits of a binary number (2nd arg is how many bits) -void LedDeviceWS2812s::printBinary(unsigned int i, unsigned int bits) { +void LedDeviceWS2812b::printBinary(unsigned int i, unsigned int bits) { int x; for(x=bits-1; x>=0; x--) { printf("%d", (i & (1 << x)) ? 1 : 0); @@ -378,7 +378,7 @@ static void udelay(int us) { // Shutdown functions // -------------------------------------------------------------------------------------------------- -void LedDeviceWS2812s::terminate(int dummy) { +void LedDeviceWS2812b::terminate(int dummy) { // Shut down the DMA controller if(dma_reg) { CLRBIT(dma_reg[DMA_CS], DMA_CS_ACTIVE); @@ -402,7 +402,7 @@ void LedDeviceWS2812s::terminate(int dummy) { //exit(1); } -void LedDeviceWS2812s::fatal(const char *fmt, ...) { +void LedDeviceWS2812b::fatal(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); @@ -414,13 +414,13 @@ void LedDeviceWS2812s::fatal(const char *fmt, ...) { // Memory management // -------------------------------------------------------------------------------------------------- // Translate from virtual address to physical -unsigned int LedDeviceWS2812s::mem_virt_to_phys(void *virt) { +unsigned int LedDeviceWS2812b::mem_virt_to_phys(void *virt) { unsigned int offset = (uint8_t *)virt - virtbase; return page_map[offset >> PAGE_SHIFT].physaddr + (offset % PAGE_SIZE); } // Translate from physical address to virtual -unsigned int LedDeviceWS2812s::mem_phys_to_virt(uint32_t phys) { +unsigned int LedDeviceWS2812b::mem_phys_to_virt(uint32_t phys) { unsigned int pg_offset = phys & (PAGE_SIZE - 1); unsigned int pg_addr = phys - pg_offset; @@ -435,7 +435,7 @@ unsigned int LedDeviceWS2812s::mem_phys_to_virt(uint32_t phys) { } // Map a peripheral's IO memory into our virtual memory, so we can read/write it directly -void * LedDeviceWS2812s::map_peripheral(uint32_t base, uint32_t len) { +void * LedDeviceWS2812b::map_peripheral(uint32_t base, uint32_t len) { int fd = open("/dev/mem", O_RDWR); void * vaddr; @@ -450,7 +450,7 @@ void * LedDeviceWS2812s::map_peripheral(uint32_t base, uint32_t len) { } // Zero out the PWM waveform buffer -void LedDeviceWS2812s::clearPWMBuffer() { +void LedDeviceWS2812b::clearPWMBuffer() { memset(PWMWaveform, 0, NUM_DATA_WORDS * 4); // Times four because memset deals in bytes. } @@ -458,7 +458,7 @@ void LedDeviceWS2812s::clearPWMBuffer() { // The (31 - bitIdx) is so that we write the data backwards, correcting its endianness // This means getPWMBit will return something other than what was written, so it would be nice // if the logic that calls this function would figure it out instead. (However, that's trickier) -void LedDeviceWS2812s::setPWMBit(unsigned int bitPos, unsigned char bit) { +void LedDeviceWS2812b::setPWMBit(unsigned int bitPos, unsigned char bit) { // Fetch word the bit is in unsigned int wordOffset = (int)(bitPos / 32); @@ -480,7 +480,7 @@ void LedDeviceWS2812s::setPWMBit(unsigned int bitPos, unsigned char bit) { // ==== Init Hardware ==== -void LedDeviceWS2812s::initHardware() { +void LedDeviceWS2812b::initHardware() { int pid; int fd; char pagemap_fn[64]; @@ -722,7 +722,7 @@ void LedDeviceWS2812s::initHardware() { } // Begin the transfer -void LedDeviceWS2812s::startTransfer() { +void LedDeviceWS2812b::startTransfer() { // Enable DMA dma_reg[DMA_CONBLK_AD] = mem_virt_to_phys(ctl->cb); dma_reg[DMA_CS] = DMA_CS_CONFIGWORD | (1 << DMA_CS_ACTIVE); diff --git a/libsrc/leddevice/LedDeviceWS2812s.h b/libsrc/leddevice/LedDeviceWS2812b.h similarity index 98% rename from libsrc/leddevice/LedDeviceWS2812s.h rename to libsrc/leddevice/LedDeviceWS2812b.h index d92ef39f..65351056 100644 --- a/libsrc/leddevice/LedDeviceWS2812s.h +++ b/libsrc/leddevice/LedDeviceWS2812b.h @@ -1,5 +1,5 @@ -#ifndef LEDDEVICEWS2812S_H_ -#define LEDDEVICEWS2812S_H_ +#ifndef LEDDEVICEWS2812B_H_ +#define LEDDEVICEWS2812B_H_ #pragma once @@ -126,14 +126,14 @@ typedef struct { /// /// Implementation of the LedDevice interface for writing to Ws2801 led device. /// -class LedDeviceWS2812s : public LedDevice +class LedDeviceWS2812b : public LedDevice { public: /// /// Constructs the LedDevice for a string containing leds of the type WS2812 - LedDeviceWS2812s(); + LedDeviceWS2812b(); - ~LedDeviceWS2812s(); + ~LedDeviceWS2812b(); /// /// Writes the led color values to the led-device /// @@ -206,4 +206,4 @@ private: -#endif /* LEDDEVICEWS2812S_H_ */ +#endif /* LEDDEVICEWS2812B_H_ */ From a92967fa7c345d4e9b0ed7207194fe884c3b31fd Mon Sep 17 00:00:00 2001 From: David Brodski Date: Thu, 18 Sep 2014 10:28:23 +0200 Subject: [PATCH 13/59] Added benchmark define some code cleanup and speedups Former-commit-id: 8254c34e1d10c598e127f46635ae6bafcb97087a --- libsrc/leddevice/LedDeviceWS2812b.cpp | 68 ++++++++++++++++++++------- libsrc/leddevice/LedDeviceWS2812b.h | 11 ++++- 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/libsrc/leddevice/LedDeviceWS2812b.cpp b/libsrc/leddevice/LedDeviceWS2812b.cpp index 2abbb1b9..bcd9f48d 100644 --- a/libsrc/leddevice/LedDeviceWS2812b.cpp +++ b/libsrc/leddevice/LedDeviceWS2812b.cpp @@ -14,6 +14,10 @@ //#include //#include +#ifdef BENCHMARK + #include +#endif + // hyperion local includes #include "LedDeviceWS2812b.h" @@ -230,31 +234,40 @@ LedDeviceWS2812b::LedDeviceWS2812b() : LedDevice(), mLedCount(0) + +#ifdef BENCHMARK + , + runCount(0), + combinedNseconds(0), + shortestNseconds(2147483647) +#endif + { + //shortestNseconds = 2147483647; // Init PWM generator and clear LED buffer initHardware(); //clearLEDBuffer(); + printf("WS2812b init finished \n"); } int LedDeviceWS2812b::write(const std::vector &ledValues) { +#ifdef BENCHMARK + timespec timeStart; + timespec timeEnd; + clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &timeStart); +#endif + mLedCount = ledValues.size(); //printf("Set leds, number: %d\n", mLedCount); -// const unsigned dataLen = ledValues.size() * sizeof(ColorRgb); -// const uint8_t * dataPtr = reinterpret_cast(ledValues.data()); - // Clear out the PWM buffer // Disabled, because we will overwrite the buffer anyway. // Read data from LEDBuffer[], translate it into wire format, and write to PWMWaveform -// unsigned int LEDBuffeWordPos = 0; -// unsigned int PWMWaveformBitPos = 0; unsigned int colorBits = 0; // Holds the GRB color before conversion to wire bit pattern - unsigned char colorBit = 0; // Holds current bit out of colorBits to be processed unsigned int wireBit = 0; // Holds the current bit we will set in PWMWaveform -// Color_t color; for(size_t i=0; i &ledValues) // Iterate through color bits to get wire bits for(int j=23; j>=0; j--) { - colorBit = (colorBits & (1 << j)) ? 1 : 0; + unsigned char colorBit = (colorBits & (1 << j)) ? 1 : 0; // Holds current bit out of colorBits to be processed + + setPWMBit(wireBit++, 1); + setPWMBit(wireBit++, colorBit); + setPWMBit(wireBit++, 0); + /* old code for better understanding switch(colorBit) { case 1: //wireBits = 0b110; // High, High, Low @@ -279,13 +297,13 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) setPWMBit(wireBit++, 0); setPWMBit(wireBit++, 0); break; - } + }*/ } } // Copy PWM waveform to DMA's data buffer //printf("Copying %d words to DMA data buffer\n", NUM_DATA_WORDS); - ctl = (struct control_data_s *)virtbase; + struct control_data_s *ctl = (struct control_data_s *)virtbase; dma_cb_t *cbp = ctl->cb; // 72 bits per pixel / 32 bits per word = 2.25 words per pixel @@ -298,10 +316,10 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) // This block is a major CPU hog when there are lots of pixels to be transmitted. // It would go quicker with DMA. - for(unsigned int i = 0; i < (cbp->length / 4); i++) { - ctl->sample[i] = PWMWaveform[i]; - } - +// for(unsigned int i = 0; i < (cbp->length / 4); i++) { +// ctl->sample[i] = PWMWaveform[i]; +// } + memcpy ( ctl->sample, PWMWaveform, cbp->length ); // memcpy does the same and is potentially faster // Enable DMA and PWM engines, which should now send the data startTransfer(); @@ -312,6 +330,20 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) //printf("Delay for %d μSec\n", (int)bitTimeUSec); //usleep((int)bitTimeUSec); +#ifdef BENCHMARK + clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &timeEnd); + timespec result; + + result.tv_sec = timeEnd.tv_sec - timeStart.tv_sec; + result.tv_nsec = timeEnd.tv_nsec - timeStart.tv_nsec; + if (result.tv_nsec < 0) { + result.tv_nsec = 1e9 - result.tv_nsec; + result.tv_sec -= 1; + } + runCount ++; + combinedNseconds += result.tv_nsec; + shortestNseconds = result.tv_nsec < shortestNseconds ? result.tv_nsec : shortestNseconds; +#endif return 0; } @@ -325,6 +357,10 @@ LedDeviceWS2812b::~LedDeviceWS2812b() // Exit cleanly, freeing memory and stopping the DMA & PWM engines // We trap all signals (including Ctrl+C), so even if you don't get here, it terminates correctly terminate(0); +#ifdef BENCHMARK + printf("WS2812b Benchmark results: Runs %d - Avarage %lu (n) - Minimum %ld (n)\n", + runCount, (runCount > 0 ? combinedNseconds / runCount : 0), shortestNseconds); +#endif } @@ -576,7 +612,7 @@ void LedDeviceWS2812b::initHardware() { // Set up control block // --------------------------------------------------------------- - ctl = (struct control_data_s *)virtbase; + struct control_data_s *ctl = (struct control_data_s *)virtbase; dma_cb_t *cbp = ctl->cb; // FIXME: Change this to use DEFINEs unsigned int phys_pwm_fifo_addr = 0x7e20c000 + 0x18; @@ -724,7 +760,7 @@ void LedDeviceWS2812b::initHardware() { // Begin the transfer void LedDeviceWS2812b::startTransfer() { // Enable DMA - dma_reg[DMA_CONBLK_AD] = mem_virt_to_phys(ctl->cb); + dma_reg[DMA_CONBLK_AD] = mem_virt_to_phys(((struct control_data_s *) virtbase)->cb); dma_reg[DMA_CS] = DMA_CS_CONFIGWORD | (1 << DMA_CS_ACTIVE); usleep(100); diff --git a/libsrc/leddevice/LedDeviceWS2812b.h b/libsrc/leddevice/LedDeviceWS2812b.h index 65351056..79216b9a 100644 --- a/libsrc/leddevice/LedDeviceWS2812b.h +++ b/libsrc/leddevice/LedDeviceWS2812b.h @@ -101,6 +101,7 @@ // Hyperion includes #include +//#define BENCHMARK // The page map contains pointers to memory that we will allocate below. It uses two pointers // per address. This is because the software (this program) deals only in virtual addresses, @@ -150,7 +151,7 @@ private: /// the number of leds (needed when switching off) size_t mLedCount; - page_map_t *page_map; // This will hold the page map, which we'll allocate below + page_map_t *page_map; // This will hold the page map, which we'll allocate uint8_t *virtbase; // Pointer to some virtual memory that will be allocated volatile unsigned int *pwm_reg; // PWM controller register set @@ -174,7 +175,7 @@ private: uint32_t sample[NUM_DATA_WORDS]; }; - struct control_data_s *ctl; + //struct control_data_s *ctl; // PWM waveform buffer (in words), 16 32-bit words are enough to hold 170 wire bits. // That's OK if we only transmit from the FIFO, but for DMA, we will use a much larger size. @@ -193,6 +194,12 @@ private: void fatal(const char *fmt, ...); void * map_peripheral(uint32_t base, uint32_t len); void printBinary(unsigned int i, unsigned int bits); + +#ifdef BENCHMARK + unsigned int runCount; + long combinedNseconds; + long shortestNseconds; +#endif }; From 5ff05d58fdfe86760acf1da764dcada75bc80601 Mon Sep 17 00:00:00 2001 From: David Brodski Date: Thu, 18 Sep 2014 20:56:32 +0200 Subject: [PATCH 14/59] fixed potential buffer overflow Former-commit-id: 2c9ea902fd563b909e6d0457c4957f80be86f93f --- libsrc/leddevice/LedDeviceWS2812b.cpp | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/libsrc/leddevice/LedDeviceWS2812b.cpp b/libsrc/leddevice/LedDeviceWS2812b.cpp index bcd9f48d..10df666a 100644 --- a/libsrc/leddevice/LedDeviceWS2812b.cpp +++ b/libsrc/leddevice/LedDeviceWS2812b.cpp @@ -269,6 +269,20 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) unsigned int colorBits = 0; // Holds the GRB color before conversion to wire bit pattern unsigned int wireBit = 0; // Holds the current bit we will set in PWMWaveform + // Copy PWM waveform to DMA's data buffer + //printf("Copying %d words to DMA data buffer\n", NUM_DATA_WORDS); + struct control_data_s *ctl = (struct control_data_s *)virtbase; + dma_cb_t *cbp = ctl->cb; + + // 72 bits per pixel / 32 bits per word = 2.25 words per pixel + // Add 1 to make sure the PWM FIFO gets the message: "we're sending zeroes" + // Times 4 because DMA works in bytes, not words + cbp->length = ((mLedCount * 2.25) + 1) * 4; + if(cbp->length > NUM_DATA_WORDS * 4) { + cbp->length = NUM_DATA_WORDS * 4; + mLedCount = (NUM_DATA_WORDS - 1) / 2.25; + } + for(size_t i=0; i &ledValues) } } - // Copy PWM waveform to DMA's data buffer - //printf("Copying %d words to DMA data buffer\n", NUM_DATA_WORDS); - struct control_data_s *ctl = (struct control_data_s *)virtbase; - dma_cb_t *cbp = ctl->cb; - - // 72 bits per pixel / 32 bits per word = 2.25 words per pixel - // Add 1 to make sure the PWM FIFO gets the message: "we're sending zeroes" - // Times 4 because DMA works in bytes, not words - cbp->length = ((mLedCount * 2.25) + 1) * 4; - if(cbp->length > NUM_DATA_WORDS * 4) { - cbp->length = NUM_DATA_WORDS * 4; - } - // This block is a major CPU hog when there are lots of pixels to be transmitted. // It would go quicker with DMA. // for(unsigned int i = 0; i < (cbp->length / 4); i++) { From d41857e626053de54f4295ab987106ac6fec0fa0 Mon Sep 17 00:00:00 2001 From: David Brodski Date: Thu, 18 Sep 2014 23:41:46 +0200 Subject: [PATCH 15/59] Pre initialized bit pattern to speed things up Former-commit-id: 08dc5ee53854997060af0257b5cff324d29f87b5 --- libsrc/leddevice/LedDeviceWS2812b.cpp | 54 ++++++++++++++++++++++++--- libsrc/leddevice/LedDeviceWS2812b.h | 2 +- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/libsrc/leddevice/LedDeviceWS2812b.cpp b/libsrc/leddevice/LedDeviceWS2812b.cpp index 10df666a..827a7f3d 100644 --- a/libsrc/leddevice/LedDeviceWS2812b.cpp +++ b/libsrc/leddevice/LedDeviceWS2812b.cpp @@ -247,6 +247,16 @@ LedDeviceWS2812b::LedDeviceWS2812b() : // Init PWM generator and clear LED buffer initHardware(); //clearLEDBuffer(); + + // init bit pattern, it is always 1X0 + unsigned int wireBit = 0; + + while ((wireBit + 3) < ((NUM_DATA_WORDS) * 4 * 8)){ + setPWMBit(wireBit++, 1); + setPWMBit(wireBit++, 0); // just init it with 0 + setPWMBit(wireBit++, 0); + } + printf("WS2812b init finished \n"); } @@ -267,7 +277,7 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) // Read data from LEDBuffer[], translate it into wire format, and write to PWMWaveform unsigned int colorBits = 0; // Holds the GRB color before conversion to wire bit pattern - unsigned int wireBit = 0; // Holds the current bit we will set in PWMWaveform + unsigned int wireBit = 1; // Holds the current bit we will set in PWMWaveform, start with 1 and skip the other two for speed // Copy PWM waveform to DMA's data buffer //printf("Copying %d words to DMA data buffer\n", NUM_DATA_WORDS); @@ -277,12 +287,16 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) // 72 bits per pixel / 32 bits per word = 2.25 words per pixel // Add 1 to make sure the PWM FIFO gets the message: "we're sending zeroes" // Times 4 because DMA works in bytes, not words - cbp->length = ((mLedCount * 2.25) + 1) * 4; + cbp->length = (mLedCount * 2.25) * 4; + //cbp->length = ((mLedCount * 2.25) + 1) * 4; if(cbp->length > NUM_DATA_WORDS * 4) { cbp->length = NUM_DATA_WORDS * 4; - mLedCount = (NUM_DATA_WORDS - 1) / 2.25; + mLedCount = NUM_DATA_WORDS / 2.25; + //mLedCount = (NUM_DATA_WORDS - 1) / 2.25; } + + for(size_t i=0; i &ledValues) for(int j=23; j>=0; j--) { unsigned char colorBit = (colorBits & (1 << j)) ? 1 : 0; // Holds current bit out of colorBits to be processed - setPWMBit(wireBit++, 1); - setPWMBit(wireBit++, colorBit); - setPWMBit(wireBit++, 0); + setPWMBit(wireBit, colorBit); + wireBit +=3; /* old code for better understanding switch(colorBit) { case 1: @@ -315,6 +328,25 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) } } + //remove one to undo optimization + wireBit --; + + // fill up the bytes + int rest = 32 - wireBit % 32; + unsigned int oldwireBitValue = wireBit; + +// printBinary(PWMWaveform[(int)(oldwireBitValue / 32)], 32); +// printf(" pre\n"); + + // zero rest of the 4 bytes / int so that output is 0 (no data is send) + for (int i = 0; i < rest; i += 3){ + setPWMBit(wireBit, 0); + wireBit += 3; + } + +// printBinary(PWMWaveform[(int)(oldwireBitValue / 32)], 32); +// printf(" post\n"); + // This block is a major CPU hog when there are lots of pixels to be transmitted. // It would go quicker with DMA. // for(unsigned int i = 0; i < (cbp->length / 4); i++) { @@ -325,6 +357,16 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) // Enable DMA and PWM engines, which should now send the data startTransfer(); + // restore bit pattern + wireBit = oldwireBitValue; + for (int i = 0; i < rest; i += 3){ + setPWMBit(wireBit, 1); + wireBit += 3; + } + +// printBinary(PWMWaveform[(int)(oldwireBitValue / 32)], 32); +// printf(" restored\n"); + // Wait long enough for the DMA transfer to finish // 3 RAM bits per wire bit, so 72 bits to send one color command. //float bitTimeUSec = (float)(NUM_DATA_WORDS * 32) * 0.4; // Bits sent * time to transmit one bit, which is 0.4μSec diff --git a/libsrc/leddevice/LedDeviceWS2812b.h b/libsrc/leddevice/LedDeviceWS2812b.h index 79216b9a..55ce250a 100644 --- a/libsrc/leddevice/LedDeviceWS2812b.h +++ b/libsrc/leddevice/LedDeviceWS2812b.h @@ -101,7 +101,7 @@ // Hyperion includes #include -//#define BENCHMARK +#define BENCHMARK // The page map contains pointers to memory that we will allocate below. It uses two pointers // per address. This is because the software (this program) deals only in virtual addresses, From 961bef22f598c5c2bba00540ac793739470443e5 Mon Sep 17 00:00:00 2001 From: David Brodski Date: Fri, 19 Sep 2014 02:00:32 +0200 Subject: [PATCH 16/59] Assembler version 1: use roll and bit clear instructions Former-commit-id: 4f27d34dd63c635a65ee33f2c368978d5b162974 --- libsrc/leddevice/LedDeviceWS2812b.cpp | 53 ++++++++++++++++++++++++++- libsrc/leddevice/LedDeviceWS2812b.h | 1 + 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/libsrc/leddevice/LedDeviceWS2812b.cpp b/libsrc/leddevice/LedDeviceWS2812b.cpp index 827a7f3d..84b7ac97 100644 --- a/libsrc/leddevice/LedDeviceWS2812b.cpp +++ b/libsrc/leddevice/LedDeviceWS2812b.cpp @@ -260,6 +260,31 @@ LedDeviceWS2812b::LedDeviceWS2812b() : printf("WS2812b init finished \n"); } +#ifdef WS2812_ASM_OPTI + +// rotate register, used to move the 1 around :-) +static inline __attribute__((always_inline)) +uint32_t arm_ror_imm(uint32_t v, uint32_t sh) { + uint32_t d; + asm ("ROR %[Rd], %[Rm], %[Is]" : [Rd] "=r" (d) : [Rm] "r" (v), [Is] "i" (sh)); + return d; +} + +static inline __attribute__((always_inline)) +uint32_t arm_ror(uint32_t v, uint32_t sh) { + uint32_t d; + asm ("ROR %[Rd], %[Rm], %[Rs]" : [Rd] "=r" (d) : [Rm] "r" (v), [Rs] "r" (sh)); + return d; +} + + +static inline __attribute__((always_inline)) +uint32_t arm_Bit_Clear_imm(uint32_t v, uint32_t v2) { + uint32_t d; + asm ("BIC %[Rd], %[Rm], %[Rs]" : [Rd] "=r" (d) : [Rm] "r" (v), [Rs] "r" (v2)); + return d; +} +#endif int LedDeviceWS2812b::write(const std::vector &ledValues) { @@ -295,6 +320,9 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) //mLedCount = (NUM_DATA_WORDS - 1) / 2.25; } +#ifdef WS2812_ASM_OPTI + unsigned int startbitPattern = 0x40000000; // = 0100 0000 0000 0000 0000 0000 0000 0000 pattern +#endif for(size_t i=0; i &ledValues) // Iterate through color bits to get wire bits for(int j=23; j>=0; j--) { - unsigned char colorBit = (colorBits & (1 << j)) ? 1 : 0; // Holds current bit out of colorBits to be processed +#ifdef WS2812_ASM_OPTI + // Fetch word the bit is in + unsigned int wordOffset = (int)(wireBit / 32); + wireBit +=3; +// printBinary(startbitPattern, 32); +// printf(" %d\n", j); + if (colorBits & (1 << j)) { + PWMWaveform[wordOffset] |= startbitPattern; + } else { + PWMWaveform[wordOffset] = arm_Bit_Clear_imm(PWMWaveform[wordOffset], startbitPattern); + } + + startbitPattern = arm_ror_imm(startbitPattern, 3); + +#else + unsigned char colorBit = (colorBits & (1 << j)) ? 1 : 0; // Holds current bit out of colorBits to be processed setPWMBit(wireBit, colorBit); wireBit +=3; +#endif /* old code for better understanding switch(colorBit) { case 1: @@ -328,6 +372,11 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) } } +#ifdef WS2812_ASM_OPTI + // calculate the bits manually since it is not needed with asm + //wireBit += mLedCount * 24 *3; + //printf(" %d\n", wireBit); +#endif //remove one to undo optimization wireBit --; @@ -344,6 +393,8 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) wireBit += 3; } +// printBinary(PWMWaveform[(int)(oldwireBitValue / 32) -1 ], 32); +// printf(" post\n"); // printBinary(PWMWaveform[(int)(oldwireBitValue / 32)], 32); // printf(" post\n"); diff --git a/libsrc/leddevice/LedDeviceWS2812b.h b/libsrc/leddevice/LedDeviceWS2812b.h index 55ce250a..d95b7912 100644 --- a/libsrc/leddevice/LedDeviceWS2812b.h +++ b/libsrc/leddevice/LedDeviceWS2812b.h @@ -102,6 +102,7 @@ #include #define BENCHMARK +#define WS2812_ASM_OPTI // The page map contains pointers to memory that we will allocate below. It uses two pointers // per address. This is because the software (this program) deals only in virtual addresses, From 0e55169f42c5efc8ef3000bf245d3489c5a99c46 Mon Sep 17 00:00:00 2001 From: David Brodski Date: Fri, 19 Sep 2014 03:16:15 +0200 Subject: [PATCH 17/59] fixed strange bug with last led and pwm needs one more int Former-commit-id: c5e20fed3d84c19032afb64e66a5934fc3db701c --- libsrc/leddevice/LedDeviceWS2812b.cpp | 61 ++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/libsrc/leddevice/LedDeviceWS2812b.cpp b/libsrc/leddevice/LedDeviceWS2812b.cpp index 84b7ac97..8a084b9c 100644 --- a/libsrc/leddevice/LedDeviceWS2812b.cpp +++ b/libsrc/leddevice/LedDeviceWS2812b.cpp @@ -270,6 +270,16 @@ uint32_t arm_ror_imm(uint32_t v, uint32_t sh) { return d; } +// rotate register, used to move the 1 around, add 1 to int counter on carry +static inline __attribute__((always_inline)) +uint32_t arm_ror_imm_add_on_carry(uint32_t v, uint32_t sh, uint32_t inc) { + uint32_t d; + asm ("RORS %[Rd], %[Rm], %[Is]\n\t" + "ADDCS %[Rd1], %[Rd1], #1" + : [Rd] "=r" (d), [Rd1] "+r" (inc): [Rm] "r" (v), [Is] "i" (sh)); + return d; +} + static inline __attribute__((always_inline)) uint32_t arm_ror(uint32_t v, uint32_t sh) { uint32_t d; @@ -312,12 +322,12 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) // 72 bits per pixel / 32 bits per word = 2.25 words per pixel // Add 1 to make sure the PWM FIFO gets the message: "we're sending zeroes" // Times 4 because DMA works in bytes, not words - cbp->length = (mLedCount * 2.25) * 4; - //cbp->length = ((mLedCount * 2.25) + 1) * 4; +// cbp->length = (mLedCount * 2.25) * 4; + cbp->length = ((mLedCount * 2.25) + 1) * 4; if(cbp->length > NUM_DATA_WORDS * 4) { cbp->length = NUM_DATA_WORDS * 4; - mLedCount = NUM_DATA_WORDS / 2.25; - //mLedCount = (NUM_DATA_WORDS - 1) / 2.25; +// mLedCount = NUM_DATA_WORDS / 2.25; + mLedCount = (NUM_DATA_WORDS - 1) / 2.25; } #ifdef WS2812_ASM_OPTI @@ -380,19 +390,39 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) //remove one to undo optimization wireBit --; - // fill up the bytes - int rest = 32 - wireBit % 32; - unsigned int oldwireBitValue = wireBit; - -// printBinary(PWMWaveform[(int)(oldwireBitValue / 32)], 32); +// printBinary(PWMWaveform[(int)(wireBit / 32)], 32); // printf(" pre\n"); +#ifdef WS2812_ASM_OPTI + int rest = 32 - wireBit % 32; // 64: 32 - used Bits + startbitPattern = (1 << (rest-1)); // set new bitpattern to start at the benigining of one bit (3 bit in wave form) + rest += 32; // add one int extra for pwm + +// printBinary(startbitPattern, 32); +// printf(" startbit\n"); + + unsigned int oldwireBitValue = wireBit; + unsigned int oldbitPattern = startbitPattern; + + // zero rest of the 4 bytes / int so that output is 0 (no data is send) + for (int i = 0; i < rest; i += 3){ + unsigned int wordOffset = (int)(wireBit / 32); + wireBit += 3; + PWMWaveform[wordOffset] = arm_Bit_Clear_imm(PWMWaveform[wordOffset], startbitPattern); + startbitPattern = arm_ror_imm(startbitPattern, 3); + } + +#else + // fill up the bytes + int rest = 32 - wireBit % 32 + 32; // 64: 32 - used Bits + 32 (one int extra for pwm) + unsigned int oldwireBitValue = wireBit; + // zero rest of the 4 bytes / int so that output is 0 (no data is send) for (int i = 0; i < rest; i += 3){ setPWMBit(wireBit, 0); wireBit += 3; } - +#endif // printBinary(PWMWaveform[(int)(oldwireBitValue / 32) -1 ], 32); // printf(" post\n"); // printBinary(PWMWaveform[(int)(oldwireBitValue / 32)], 32); @@ -410,10 +440,21 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) // restore bit pattern wireBit = oldwireBitValue; + +#ifdef WS2812_ASM_OPTI + startbitPattern = oldbitPattern; + for (int i = 0; i < rest; i += 3){ + unsigned int wordOffset = (int)(wireBit / 32); + wireBit += 3; + PWMWaveform[wordOffset] |= startbitPattern; + startbitPattern = arm_ror_imm(startbitPattern, 3); + } +#else for (int i = 0; i < rest; i += 3){ setPWMBit(wireBit, 1); wireBit += 3; } +#endif // printBinary(PWMWaveform[(int)(oldwireBitValue / 32)], 32); // printf(" restored\n"); From f41a0fc977ecf3275de48ab62fb0149d03258ea7 Mon Sep 17 00:00:00 2001 From: David Brodski Date: Fri, 19 Sep 2014 03:18:26 +0200 Subject: [PATCH 18/59] removed benchmark Former-commit-id: e716fa337c4995a258c34200e14fd56ee8dae05c --- libsrc/leddevice/LedDeviceWS2812b.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libsrc/leddevice/LedDeviceWS2812b.h b/libsrc/leddevice/LedDeviceWS2812b.h index d95b7912..dd92065c 100644 --- a/libsrc/leddevice/LedDeviceWS2812b.h +++ b/libsrc/leddevice/LedDeviceWS2812b.h @@ -101,7 +101,7 @@ // Hyperion includes #include -#define BENCHMARK +//#define BENCHMARK #define WS2812_ASM_OPTI // The page map contains pointers to memory that we will allocate below. It uses two pointers From c0f0837b925b95c0b1aa7fb2409a27976714372f Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Fri, 19 Sep 2014 16:20:01 +0200 Subject: [PATCH 19/59] Added missing include Former-commit-id: 1cf2f19a0a7e3fc9bef55979ce7e3221f3e66fec --- libsrc/leddevice/LedDeviceWS2812b.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libsrc/leddevice/LedDeviceWS2812b.cpp b/libsrc/leddevice/LedDeviceWS2812b.cpp index 8a084b9c..70f7e708 100644 --- a/libsrc/leddevice/LedDeviceWS2812b.cpp +++ b/libsrc/leddevice/LedDeviceWS2812b.cpp @@ -11,6 +11,7 @@ #include #include #include +#include //#include //#include From 8ad0e88e2f6b843a550a055f2996f50853e50439 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Fri, 19 Sep 2014 16:37:58 +0200 Subject: [PATCH 20/59] Minor reformat of code to match hyperion-style. Added ws2812b as option to disable for devices without dma-pwm Former-commit-id: 579f4426285fb537706a22af446f5280748f2ab7 --- CMakeLists.txt | 7 +- HyperionConfig.h.in | 3 + libsrc/leddevice/CMakeLists.txt | 2 + libsrc/leddevice/LedDeviceFactory.cpp | 11 +- libsrc/leddevice/LedDeviceWS2812b.cpp | 249 ++++++++++++-------------- libsrc/leddevice/LedDeviceWS2812b.h | 25 +-- 6 files changed, 143 insertions(+), 154 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 29c9476b..64cf1050 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,9 @@ message(STATUS "ENABLE_DISPMANX = " ${ENABLE_DISPMANX}) option(ENABLE_SPIDEV "Enable the SPIDEV device" ON) message(STATUS "ENABLE_SPIDEV = " ${ENABLE_SPIDEV}) +option(ENABLE_WS2812BPWM "Enable the WS2812b-PWM device" ON) +message(STATUS "ENABLE_WS2812BPWM = " ${ENABLE_WS2812BPWM}) + option(ENABLE_V4L2 "Enable the V4L2 grabber" ON) message(STATUS "ENABLE_V4L2 = " ${ENABLE_V4L2}) @@ -22,9 +25,9 @@ message(STATUS "ENABLE_TINKERFORGE = " ${ENABLE_TINKERFORGE}) option(ENABLE_PROTOBUF "Enable PROTOBUF server" ON) message(STATUS "ENABLE_PROTOBUF = " ${ENABLE_PROTOBUF}) -if (ENABLE_V4L2 AND NOT ENABLE_PROTOBUF) +if(ENABLE_V4L2 AND NOT ENABLE_PROTOBUF) message(FATAL_ERROR "V4L2 grabber requires PROTOBUF. Disable V4L2 or enable PROTOBUF") -endif (ENABLE_V4L2 AND NOT ENABLE_PROTOBUF) +endif(ENABLE_V4L2 AND NOT ENABLE_PROTOBUF) # Createt the configuration file # configure a header file to pass some of the CMake settings diff --git a/HyperionConfig.h.in b/HyperionConfig.h.in index ff2f4f82..3739bba8 100644 --- a/HyperionConfig.h.in +++ b/HyperionConfig.h.in @@ -9,6 +9,9 @@ // Define to enable the spi-device #cmakedefine ENABLE_SPIDEV +// Define to enable the ws2812b-pwm-device +#cmakedefine ENABLE_WS2812BPWM + // Define to enable the spi-device #cmakedefine ENABLE_TINKERFORGE diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt index abe28bf5..4304c874 100755 --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -68,6 +68,7 @@ if(ENABLE_SPIDEV) ) endif(ENABLE_SPIDEV) +if(ENABLE_WS2812BPWM) SET(Leddevice_HEADERS ${Leddevice_HEADERS} ${CURRENT_SOURCE_DIR}/LedDeviceWS2812b.h @@ -76,6 +77,7 @@ SET(Leddevice_SOURCES ${Leddevice_SOURCES} ${CURRENT_SOURCE_DIR}/LedDeviceWS2812b.cpp ) +endif(ENABLE_WS2812BPWM) if(ENABLE_TINKERFORGE) SET(Leddevice_HEADERS diff --git a/libsrc/leddevice/LedDeviceFactory.cpp b/libsrc/leddevice/LedDeviceFactory.cpp index 0915b4fe..fb602b02 100755 --- a/libsrc/leddevice/LedDeviceFactory.cpp +++ b/libsrc/leddevice/LedDeviceFactory.cpp @@ -31,7 +31,9 @@ #include "LedDevicePhilipsHue.h" #include "LedDeviceTpm2.h" -#include "LedDeviceWS2812b.h" +#ifdef ENABLE_WS2812BPWM + #include "LedDeviceWS2812b.h" +#endif LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) { @@ -180,14 +182,17 @@ LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) const std::string output = deviceConfig["output"].asString(); const unsigned rate = deviceConfig["rate"].asInt(); - LedDeviceTpm2* deviceTpm2 = new LedDeviceTpm2(output, rate); + LedDeviceTpm2 * deviceTpm2 = new LedDeviceTpm2(output, rate); deviceTpm2->open(); device = deviceTpm2; - }else if (type == "ws2812b") + } +#ifdef ENABLE_WS2812BPWM + else if (type == "ws2812b") { LedDeviceWS2812b * ledDeviceWS2812b = new LedDeviceWS2812b(); device = ledDeviceWS2812b; } +#endif else { std::cout << "Unable to create device " << type << std::endl; diff --git a/libsrc/leddevice/LedDeviceWS2812b.cpp b/libsrc/leddevice/LedDeviceWS2812b.cpp index 70f7e708..dc6bbe70 100644 --- a/libsrc/leddevice/LedDeviceWS2812b.cpp +++ b/libsrc/leddevice/LedDeviceWS2812b.cpp @@ -252,7 +252,8 @@ LedDeviceWS2812b::LedDeviceWS2812b() : // init bit pattern, it is always 1X0 unsigned int wireBit = 0; - while ((wireBit + 3) < ((NUM_DATA_WORDS) * 4 * 8)){ + while ((wireBit + 3) < ((NUM_DATA_WORDS) * 4 * 8)) + { setPWMBit(wireBit++, 1); setPWMBit(wireBit++, 0); // just init it with 0 setPWMBit(wireBit++, 0); @@ -264,36 +265,36 @@ LedDeviceWS2812b::LedDeviceWS2812b() : #ifdef WS2812_ASM_OPTI // rotate register, used to move the 1 around :-) -static inline __attribute__((always_inline)) -uint32_t arm_ror_imm(uint32_t v, uint32_t sh) { - uint32_t d; - asm ("ROR %[Rd], %[Rm], %[Is]" : [Rd] "=r" (d) : [Rm] "r" (v), [Is] "i" (sh)); - return d; +static inline __attribute__((always_inline)) uint32_t arm_ror_imm(uint32_t v, uint32_t sh) +{ + uint32_t d; + asm ("ROR %[Rd], %[Rm], %[Is]" : [Rd] "=r" (d) : [Rm] "r" (v), [Is] "i" (sh)); + return d; } // rotate register, used to move the 1 around, add 1 to int counter on carry -static inline __attribute__((always_inline)) -uint32_t arm_ror_imm_add_on_carry(uint32_t v, uint32_t sh, uint32_t inc) { - uint32_t d; - asm ("RORS %[Rd], %[Rm], %[Is]\n\t" - "ADDCS %[Rd1], %[Rd1], #1" - : [Rd] "=r" (d), [Rd1] "+r" (inc): [Rm] "r" (v), [Is] "i" (sh)); - return d; +static inline __attribute__((always_inline)) uint32_t arm_ror_imm_add_on_carry(uint32_t v, uint32_t sh, uint32_t inc) +{ + uint32_t d; + asm ("RORS %[Rd], %[Rm], %[Is]\n\t" + "ADDCS %[Rd1], %[Rd1], #1" + : [Rd] "=r" (d), [Rd1] "+r" (inc): [Rm] "r" (v), [Is] "i" (sh)); + return d; } -static inline __attribute__((always_inline)) -uint32_t arm_ror(uint32_t v, uint32_t sh) { - uint32_t d; - asm ("ROR %[Rd], %[Rm], %[Rs]" : [Rd] "=r" (d) : [Rm] "r" (v), [Rs] "r" (sh)); - return d; +static inline __attribute__((always_inline)) uint32_t arm_ror(uint32_t v, uint32_t sh) +{ + uint32_t d; + asm ("ROR %[Rd], %[Rm], %[Rs]" : [Rd] "=r" (d) : [Rm] "r" (v), [Rs] "r" (sh)); + return d; } -static inline __attribute__((always_inline)) -uint32_t arm_Bit_Clear_imm(uint32_t v, uint32_t v2) { - uint32_t d; - asm ("BIC %[Rd], %[Rm], %[Rs]" : [Rd] "=r" (d) : [Rm] "r" (v), [Rs] "r" (v2)); - return d; +static inline __attribute__((always_inline)) uint32_t arm_Bit_Clear_imm(uint32_t v, uint32_t v2) +{ + uint32_t d; + asm ("BIC %[Rd], %[Rm], %[Rs]" : [Rd] "=r" (d) : [Rm] "r" (v), [Rs] "r" (v2)); + return d; } #endif @@ -306,10 +307,6 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) #endif mLedCount = ledValues.size(); - //printf("Set leds, number: %d\n", mLedCount); - - // Clear out the PWM buffer - // Disabled, because we will overwrite the buffer anyway. // Read data from LEDBuffer[], translate it into wire format, and write to PWMWaveform unsigned int colorBits = 0; // Holds the GRB color before conversion to wire bit pattern @@ -323,11 +320,10 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) // 72 bits per pixel / 32 bits per word = 2.25 words per pixel // Add 1 to make sure the PWM FIFO gets the message: "we're sending zeroes" // Times 4 because DMA works in bytes, not words -// cbp->length = (mLedCount * 2.25) * 4; cbp->length = ((mLedCount * 2.25) + 1) * 4; - if(cbp->length > NUM_DATA_WORDS * 4) { + if(cbp->length > NUM_DATA_WORDS * 4) + { cbp->length = NUM_DATA_WORDS * 4; -// mLedCount = NUM_DATA_WORDS / 2.25; mLedCount = (NUM_DATA_WORDS - 1) / 2.25; } @@ -336,7 +332,8 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) #endif - for(size_t i=0; i &ledValues) for(int j=23; j>=0; j--) { #ifdef WS2812_ASM_OPTI // Fetch word the bit is in - unsigned int wordOffset = (int)(wireBit / 32); - wireBit +=3; + unsigned int wordOffset = (int)(wireBit / 32); + wireBit +=3; -// printBinary(startbitPattern, 32); -// printf(" %d\n", j); - if (colorBits & (1 << j)) { - PWMWaveform[wordOffset] |= startbitPattern; - } else { - PWMWaveform[wordOffset] = arm_Bit_Clear_imm(PWMWaveform[wordOffset], startbitPattern); - } - - startbitPattern = arm_ror_imm(startbitPattern, 3); + if (colorBits & (1 << j)) { + PWMWaveform[wordOffset] |= startbitPattern; + } else { + PWMWaveform[wordOffset] = arm_Bit_Clear_imm(PWMWaveform[wordOffset], startbitPattern); + } + startbitPattern = arm_ror_imm(startbitPattern, 3); #else unsigned char colorBit = (colorBits & (1 << j)) ? 1 : 0; // Holds current bit out of colorBits to be processed setPWMBit(wireBit, colorBit); @@ -391,9 +385,6 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) //remove one to undo optimization wireBit --; -// printBinary(PWMWaveform[(int)(wireBit / 32)], 32); -// printf(" pre\n"); - #ifdef WS2812_ASM_OPTI int rest = 32 - wireBit % 32; // 64: 32 - used Bits startbitPattern = (1 << (rest-1)); // set new bitpattern to start at the benigining of one bit (3 bit in wave form) @@ -406,7 +397,8 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) unsigned int oldbitPattern = startbitPattern; // zero rest of the 4 bytes / int so that output is 0 (no data is send) - for (int i = 0; i < rest; i += 3){ + for (int i = 0; i < rest; i += 3) + { unsigned int wordOffset = (int)(wireBit / 32); wireBit += 3; PWMWaveform[wordOffset] = arm_Bit_Clear_imm(PWMWaveform[wordOffset], startbitPattern); @@ -419,22 +411,14 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) unsigned int oldwireBitValue = wireBit; // zero rest of the 4 bytes / int so that output is 0 (no data is send) - for (int i = 0; i < rest; i += 3){ + for (int i = 0; i < rest; i += 3) + { setPWMBit(wireBit, 0); wireBit += 3; } #endif -// printBinary(PWMWaveform[(int)(oldwireBitValue / 32) -1 ], 32); -// printf(" post\n"); -// printBinary(PWMWaveform[(int)(oldwireBitValue / 32)], 32); -// printf(" post\n"); - // This block is a major CPU hog when there are lots of pixels to be transmitted. - // It would go quicker with DMA. -// for(unsigned int i = 0; i < (cbp->length / 4); i++) { -// ctl->sample[i] = PWMWaveform[i]; -// } - memcpy ( ctl->sample, PWMWaveform, cbp->length ); // memcpy does the same and is potentially faster + memcpy ( ctl->sample, PWMWaveform, cbp->length ); // Enable DMA and PWM engines, which should now send the data startTransfer(); @@ -444,35 +428,29 @@ int LedDeviceWS2812b::write(const std::vector &ledValues) #ifdef WS2812_ASM_OPTI startbitPattern = oldbitPattern; - for (int i = 0; i < rest; i += 3){ + for (int i = 0; i < rest; i += 3) + { unsigned int wordOffset = (int)(wireBit / 32); wireBit += 3; PWMWaveform[wordOffset] |= startbitPattern; startbitPattern = arm_ror_imm(startbitPattern, 3); } #else - for (int i = 0; i < rest; i += 3){ + for (int i = 0; i < rest; i += 3) + { setPWMBit(wireBit, 1); wireBit += 3; } #endif -// printBinary(PWMWaveform[(int)(oldwireBitValue / 32)], 32); -// printf(" restored\n"); - - // Wait long enough for the DMA transfer to finish - // 3 RAM bits per wire bit, so 72 bits to send one color command. - //float bitTimeUSec = (float)(NUM_DATA_WORDS * 32) * 0.4; // Bits sent * time to transmit one bit, which is 0.4μSec - //printf("Delay for %d μSec\n", (int)bitTimeUSec); - //usleep((int)bitTimeUSec); - #ifdef BENCHMARK clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &timeEnd); timespec result; result.tv_sec = timeEnd.tv_sec - timeStart.tv_sec; result.tv_nsec = timeEnd.tv_nsec - timeStart.tv_nsec; - if (result.tv_nsec < 0) { + if (result.tv_nsec < 0) + { result.tv_nsec = 1e9 - result.tv_nsec; result.tv_sec -= 1; } @@ -491,8 +469,7 @@ int LedDeviceWS2812b::switchOff() LedDeviceWS2812b::~LedDeviceWS2812b() { // Exit cleanly, freeing memory and stopping the DMA & PWM engines - // We trap all signals (including Ctrl+C), so even if you don't get here, it terminates correctly - terminate(0); + terminate(0); #ifdef BENCHMARK printf("WS2812b Benchmark results: Runs %d - Avarage %lu (n) - Minimum %ld (n)\n", runCount, (runCount > 0 ? combinedNseconds / runCount : 0), shortestNseconds); @@ -512,47 +489,46 @@ LedDeviceWS2812b::~LedDeviceWS2812b() // Convenience functions // -------------------------------------------------------------------------------------------------- // Print some bits of a binary number (2nd arg is how many bits) -void LedDeviceWS2812b::printBinary(unsigned int i, unsigned int bits) { +void LedDeviceWS2812b::printBinary(unsigned int i, unsigned int bits) +{ int x; - for(x=bits-1; x>=0; x--) { + for(x=bits-1; x>=0; x--) + { printf("%d", (i & (1 << x)) ? 1 : 0); - if(x % 16 == 0 && x > 0) { + if(x % 16 == 0 && x > 0) + { printf(" "); - } else if(x % 4 == 0 && x > 0) { + } + else if(x % 4 == 0 && x > 0) + { printf(":"); } } } // Reverse the bits in a word -unsigned int reverseWord(unsigned int word) { +unsigned int reverseWord(unsigned int word) +{ unsigned int output = 0; //unsigned char bit; int i; - for(i=0; i<32; i++) { - //bit = word & (1 << i) ? 1 : 0; + for(i=0; i<32; i++) + { output |= word & (1 << i) ? 1 : 0; - if(i<31) { + if(i<31) + { output <<= 1; } } return output; } -// Not sure how this is better than usleep...? -/* -static void udelay(int us) { - struct timespec ts = { 0, us * 1000 }; - nanosleep(&ts, NULL); -} -*/ - - // Shutdown functions // -------------------------------------------------------------------------------------------------- void LedDeviceWS2812b::terminate(int dummy) { // Shut down the DMA controller - if(dma_reg) { + if(dma_reg) + { CLRBIT(dma_reg[DMA_CS], DMA_CS_ACTIVE); usleep(100); SETBIT(dma_reg[DMA_CS], DMA_CS_RESET); @@ -560,21 +536,22 @@ void LedDeviceWS2812b::terminate(int dummy) { } // Shut down PWM - if(pwm_reg) { + if(pwm_reg) + { CLRBIT(pwm_reg[PWM_CTL], PWM_CTL_PWEN1); usleep(100); pwm_reg[PWM_CTL] = (1 << PWM_CTL_CLRF1); } // Free the allocated memory - if(page_map != 0) { + if(page_map != 0) + { free(page_map); } - - //exit(1); } -void LedDeviceWS2812b::fatal(const char *fmt, ...) { +void LedDeviceWS2812b::fatal(const char *fmt, ...) +{ va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); @@ -586,18 +563,22 @@ void LedDeviceWS2812b::fatal(const char *fmt, ...) { // Memory management // -------------------------------------------------------------------------------------------------- // Translate from virtual address to physical -unsigned int LedDeviceWS2812b::mem_virt_to_phys(void *virt) { +unsigned int LedDeviceWS2812b::mem_virt_to_phys(void *virt) +{ unsigned int offset = (uint8_t *)virt - virtbase; return page_map[offset >> PAGE_SHIFT].physaddr + (offset % PAGE_SIZE); } // Translate from physical address to virtual -unsigned int LedDeviceWS2812b::mem_phys_to_virt(uint32_t phys) { +unsigned int LedDeviceWS2812b::mem_phys_to_virt(uint32_t phys) +{ unsigned int pg_offset = phys & (PAGE_SIZE - 1); unsigned int pg_addr = phys - pg_offset; - for (unsigned int i = 0; i < NUM_PAGES; i++) { - if (page_map[i].physaddr == pg_addr) { + for (unsigned int i = 0; i < NUM_PAGES; i++) + { + if (page_map[i].physaddr == pg_addr) + { return (uint32_t)virtbase + i * PAGE_SIZE + pg_offset; } } @@ -607,22 +588,28 @@ unsigned int LedDeviceWS2812b::mem_phys_to_virt(uint32_t phys) { } // Map a peripheral's IO memory into our virtual memory, so we can read/write it directly -void * LedDeviceWS2812b::map_peripheral(uint32_t base, uint32_t len) { +void * LedDeviceWS2812b::map_peripheral(uint32_t base, uint32_t len) +{ int fd = open("/dev/mem", O_RDWR); void * vaddr; if (fd < 0) + { fatal("Failed to open /dev/mem: %m\n"); + } vaddr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, base); if (vaddr == MAP_FAILED) + { fatal("Failed to map peripheral at 0x%08x: %m\n", base); + } close(fd); return vaddr; } // Zero out the PWM waveform buffer -void LedDeviceWS2812b::clearPWMBuffer() { +void LedDeviceWS2812b::clearPWMBuffer() +{ memset(PWMWaveform, 0, NUM_DATA_WORDS * 4); // Times four because memset deals in bytes. } @@ -630,29 +617,26 @@ void LedDeviceWS2812b::clearPWMBuffer() { // The (31 - bitIdx) is so that we write the data backwards, correcting its endianness // This means getPWMBit will return something other than what was written, so it would be nice // if the logic that calls this function would figure it out instead. (However, that's trickier) -void LedDeviceWS2812b::setPWMBit(unsigned int bitPos, unsigned char bit) { - +void LedDeviceWS2812b::setPWMBit(unsigned int bitPos, unsigned char bit) +{ // Fetch word the bit is in unsigned int wordOffset = (int)(bitPos / 32); unsigned int bitIdx = bitPos - (wordOffset * 32); - //printf("bitPos=%d wordOffset=%d bitIdx=%d value=%d\n", bitPos, wordOffset, bitIdx, bit); - - switch(bit) { + switch(bit) + { case 1: PWMWaveform[wordOffset] |= (1 << (31 - bitIdx)); -// PWMWaveform[wordOffset] |= (1 << bitIdx); break; case 0: PWMWaveform[wordOffset] &= ~(1 << (31 - bitIdx)); -// PWMWaveform[wordOffset] &= ~(1 << bitIdx); break; } } // ==== Init Hardware ==== - -void LedDeviceWS2812b::initHardware() { +void LedDeviceWS2812b::initHardware() +{ int pid; int fd; char pagemap_fn[64]; @@ -692,22 +676,24 @@ void LedDeviceWS2812b::initHardware() { -1, // File descriptor 0); // Offset - if (virtbase == MAP_FAILED) { + if (virtbase == MAP_FAILED) + { fatal("Failed to mmap physical pages: %m\n"); + return; } - if ((unsigned long)virtbase & (PAGE_SIZE-1)) { + if ((unsigned long)virtbase & (PAGE_SIZE-1)) + { fatal("Virtual address is not page aligned\n"); + return; } - //printf("virtbase mapped 0x%x bytes at 0x%x\n", NUM_PAGES * PAGE_SIZE, virtbase); - // Allocate page map (pointers to the control block(s) and data for each CB page_map = (page_map_t *) malloc(NUM_PAGES * sizeof(*page_map)); - if (page_map == 0) { + if (page_map == 0) + { fatal("Failed to malloc page_map: %m\n"); - } else { - //printf("Allocated 0x%x bytes for page_map at 0x%x\n", NUM_PAGES * sizeof(*page_map), page_map); + return; } // Use /proc/self/pagemap to figure out the mapping between virtual and physical addresses @@ -715,17 +701,20 @@ void LedDeviceWS2812b::initHardware() { sprintf(pagemap_fn, "/proc/%d/pagemap", pid); fd = open(pagemap_fn, O_RDONLY); - if (fd < 0) { + if (fd < 0) + { fatal("Failed to open %s: %m\n", pagemap_fn); } off_t newOffset = (unsigned long)virtbase >> 9; - if (lseek(fd, newOffset, SEEK_SET) != newOffset) { + if (lseek(fd, newOffset, SEEK_SET) != newOffset) + { fatal("Failed to seek on %s: %m\n", pagemap_fn); } printf("Page map: %d pages\n", NUM_PAGES); - for (unsigned int i = 0; i < NUM_PAGES; i++) { + for (unsigned int i = 0; i < NUM_PAGES; i++) + { uint64_t pfn; page_map[i].virtaddr = virtbase + i * PAGE_SIZE; @@ -766,7 +755,8 @@ void LedDeviceWS2812b::initHardware() { // Add 1 to make sure the PWM FIFO gets the message: "we're sending zeroes" // Times 4 because DMA works in bytes, not words cbp->length = ((mLedCount * 2.25) + 1) * 4; - if(cbp->length > NUM_DATA_WORDS * 4) { + if(cbp->length > NUM_DATA_WORDS * 4) + { cbp->length = NUM_DATA_WORDS * 4; } @@ -780,19 +770,6 @@ void LedDeviceWS2812b::initHardware() { // Pointer to next block - 0 shuts down the DMA channel when transfer is complete cbp->next = 0; - // Testing - /* - ctl = (struct control_data_s *)virtbase; - ctl->sample[0] = 0x00000000; - ctl->sample[1] = 0x000000FA; - ctl->sample[2] = 0x0000FFFF; - ctl->sample[3] = 0xAAAAAAAA; - ctl->sample[4] = 0xF0F0F0F0; - ctl->sample[5] = 0x0A0A0A0A; - ctl->sample[6] = 0xF00F0000; - */ - - // Stop any existing DMA transfers // --------------------------------------------------------------- dma_reg[DMA_CS] |= (1 << DMA_CS_ABORT); @@ -894,7 +871,8 @@ void LedDeviceWS2812b::initHardware() { } // Begin the transfer -void LedDeviceWS2812b::startTransfer() { +void LedDeviceWS2812b::startTransfer() +{ // Enable DMA dma_reg[DMA_CONBLK_AD] = mem_virt_to_phys(((struct control_data_s *) virtbase)->cb); dma_reg[DMA_CS] = DMA_CS_CONFIGWORD | (1 << DMA_CS_ACTIVE); @@ -902,7 +880,4 @@ void LedDeviceWS2812b::startTransfer() { // Enable PWM SETBIT(pwm_reg[PWM_CTL], PWM_CTL_PWEN1); - -// dumpPWM(); -// dumpDMA(); } diff --git a/libsrc/leddevice/LedDeviceWS2812b.h b/libsrc/leddevice/LedDeviceWS2812b.h index dd92065c..d945e20d 100644 --- a/libsrc/leddevice/LedDeviceWS2812b.h +++ b/libsrc/leddevice/LedDeviceWS2812b.h @@ -108,22 +108,23 @@ // per address. This is because the software (this program) deals only in virtual addresses, // whereas the DMA controller can only access RAM via physical address. (If that's not confusing // enough, it writes to peripherals by their bus addresses.) -typedef struct { +struct page_map_t +{ uint8_t *virtaddr; uint32_t physaddr; -} page_map_t; +}; // Control Block (CB) - this tells the DMA controller what to do. -typedef struct { - unsigned int - info, // Transfer Information (TI) - src, // Source address (physical) - dst, // Destination address (bus) - length, // Length in bytes (not words!) - stride, // We don't care about this - next, // Pointer to next control block - pad[2]; // These are "reserved" (unused) -} dma_cb_t; +struct dma_cb_t +{ + unsigned info; // Transfer Information (TI) + unsigned src; // Source address (physical) + unsigned dst; // Destination address (bus) + unsigned length; // Length in bytes (not words!) + unsigned stride; // We don't care about this + unsigned next; // Pointer to next control block + unsigned pad[2]; // These are "reserved" (unused) +}; /// /// Implementation of the LedDevice interface for writing to Ws2801 led device. From c05c32b67f48dfe0b6baedfa4d228d528b7c5f17 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Fri, 19 Sep 2014 16:40:06 +0200 Subject: [PATCH 21/59] Updated binary-release Former-commit-id: 34dbccafe66ba60435c1a88c8ddde98f2fcccf17 --- deploy/hyperion.tar.gz.REMOVED.git-id | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/hyperion.tar.gz.REMOVED.git-id b/deploy/hyperion.tar.gz.REMOVED.git-id index ab29ef88..88c050e0 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -9546e335179f732ff68ea9bc47020a19e4b6f44c \ No newline at end of file +7db53c6c615dd3570277a253f1daff28e97e7abb \ No newline at end of file From b4d7410520a28c671d48ad5a9e4e2620c0e80776 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Mon, 22 Sep 2014 20:19:58 +0200 Subject: [PATCH 22/59] Started on adding a configurable delay in case leds are ahead of picture Former-commit-id: 9eedf27c9cb51d05fca2ec2f0f9edae4726ac54d --- config/hyperion.config.json | 3 ++- libsrc/hyperion/Hyperion.cpp | 1 + src/hyperiond/hyperiond.cpp | 16 +++++++--------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/config/hyperion.config.json b/config/hyperion.config.json index fa08d80d..61fb7865 100644 --- a/config/hyperion.config.json +++ b/config/hyperion.config.json @@ -80,7 +80,8 @@ { "type" : "none", "time_ms" : 200, - "updateFrequency" : 20.0000 + "updateFrequency" : 20.0000, + "framesDelay" : 0 } }, diff --git a/libsrc/hyperion/Hyperion.cpp b/libsrc/hyperion/Hyperion.cpp index a81e4f3c..9b5eb9b9 100644 --- a/libsrc/hyperion/Hyperion.cpp +++ b/libsrc/hyperion/Hyperion.cpp @@ -243,6 +243,7 @@ LedDevice * Hyperion::createColorSmoothing(const Json::Value & smoothingConfig, } else { +// const unsigned framesDelay = smoothingConfig.get("framesDelay", Json::Value(0u)).asUInt(); std::cout << "Creating linear smoothing" << std::endl; return new LinearColorSmoothing(ledDevice, smoothingConfig["updateFrequency"].asDouble(), smoothingConfig["time_ms"].asInt()); } diff --git a/src/hyperiond/hyperiond.cpp b/src/hyperiond/hyperiond.cpp index e7591343..38d2aa42 100644 --- a/src/hyperiond/hyperiond.cpp +++ b/src/hyperiond/hyperiond.cpp @@ -119,18 +119,16 @@ int main(int argc, char** argv) { const Json::Value effectConfigArgs = effectConfig["args"]; if (hyperion.setEffect(effectName, effectConfigArgs, priority, duration_ms) == 0) - { - std::cout << "Boot sequence(" << effectName << ") with user-defined arguments created and started" << std::endl; - } - else - { - std::cout << "Failed to start boot sequence: " << effectName << " with user-defined arguments" << std::endl; - } - + { + std::cout << "Boot sequence(" << effectName << ") with user-defined arguments created and started" << std::endl; + } + else + { + std::cout << "Failed to start boot sequence: " << effectName << " with user-defined arguments" << std::endl; + } } else { - if (hyperion.setEffect(effectName, priority, duration_ms) == 0) { std::cout << "Boot sequence(" << effectName << ") created and started" << std::endl; From b92ea3a5d62fa45a469b23944ceb76f6e963489f Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Mon, 22 Sep 2014 20:21:17 +0200 Subject: [PATCH 23/59] Set the DMA ws2812b to default OFF in build Former-commit-id: 1e8775cf07bd33b61429e65ce309f42a8f464828 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 64cf1050..6f0c0ed5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ message(STATUS "ENABLE_DISPMANX = " ${ENABLE_DISPMANX}) option(ENABLE_SPIDEV "Enable the SPIDEV device" ON) message(STATUS "ENABLE_SPIDEV = " ${ENABLE_SPIDEV}) -option(ENABLE_WS2812BPWM "Enable the WS2812b-PWM device" ON) +option(ENABLE_WS2812BPWM "Enable the WS2812b-PWM device" OFF) message(STATUS "ENABLE_WS2812BPWM = " ${ENABLE_WS2812BPWM}) option(ENABLE_V4L2 "Enable the V4L2 grabber" ON) From a86e451db9d21a048c99c1af3a22b75085abf21b Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Mon, 22 Sep 2014 20:24:33 +0200 Subject: [PATCH 24/59] Added black color before boot-sequence, to make sure that the boot-sequence is the first thing to start Former-commit-id: 575f1dfa4d2047c109d179c209c683c37028e721 --- src/hyperiond/hyperiond.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/hyperiond/hyperiond.cpp b/src/hyperiond/hyperiond.cpp index 38d2aa42..4c21b39b 100644 --- a/src/hyperiond/hyperiond.cpp +++ b/src/hyperiond/hyperiond.cpp @@ -115,6 +115,8 @@ int main(int argc, char** argv) const unsigned duration_ms = effectConfig["duration_ms"].asUInt(); const int priority = 0; + hyperion.setColor(priority+1, ColorRgb::BLACK, duration_ms, false); + if (effectConfig.isMember("args")) { const Json::Value effectConfigArgs = effectConfig["args"]; From 068782d419966c5b0a6dd030d538262e40c098fd Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Mon, 22 Sep 2014 20:25:06 +0200 Subject: [PATCH 25/59] Updated RPi release Former-commit-id: 248e4c3d5dd5c2f8dc1d795ccf0a86bda4bc316b --- deploy/hyperion.tar.gz.REMOVED.git-id | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/hyperion.tar.gz.REMOVED.git-id b/deploy/hyperion.tar.gz.REMOVED.git-id index 88c050e0..f71fa321 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -7db53c6c615dd3570277a253f1daff28e97e7abb \ No newline at end of file +7dbfa7fe80db4f8e65357ff45ff92a1321dfde49 \ No newline at end of file From 30cc14d9e8983707f9ca7cc229dfdf897821f992 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Mon, 22 Sep 2014 21:28:38 +0200 Subject: [PATCH 26/59] Added delay to smoothing and changed the switch off to continue sending black Former-commit-id: fa5a7f14b0fdf3a0a74169cfefbf5b625330b753 --- config/hyperion.config.json | 2 +- libsrc/hyperion/Hyperion.cpp | 8 +++- libsrc/hyperion/LinearColorSmoothing.cpp | 52 +++++++++++++++++------- libsrc/hyperion/LinearColorSmoothing.h | 16 +++++++- 4 files changed, 60 insertions(+), 18 deletions(-) diff --git a/config/hyperion.config.json b/config/hyperion.config.json index 61fb7865..8e361ed0 100644 --- a/config/hyperion.config.json +++ b/config/hyperion.config.json @@ -81,7 +81,7 @@ "type" : "none", "time_ms" : 200, "updateFrequency" : 20.0000, - "framesDelay" : 0 + "updateDelay" : 0 } }, diff --git a/libsrc/hyperion/Hyperion.cpp b/libsrc/hyperion/Hyperion.cpp index 9b5eb9b9..5e76d8e8 100644 --- a/libsrc/hyperion/Hyperion.cpp +++ b/libsrc/hyperion/Hyperion.cpp @@ -243,9 +243,13 @@ LedDevice * Hyperion::createColorSmoothing(const Json::Value & smoothingConfig, } else { -// const unsigned framesDelay = smoothingConfig.get("framesDelay", Json::Value(0u)).asUInt(); + const unsigned updateDelay = smoothingConfig.get("updateDelay", Json::Value(0u)).asUInt(); std::cout << "Creating linear smoothing" << std::endl; - return new LinearColorSmoothing(ledDevice, smoothingConfig["updateFrequency"].asDouble(), smoothingConfig["time_ms"].asInt()); + return new LinearColorSmoothing( + ledDevice, + smoothingConfig["updateFrequency"].asDouble(), + smoothingConfig["time_ms"].asInt(), + updateDelay); } } else diff --git a/libsrc/hyperion/LinearColorSmoothing.cpp b/libsrc/hyperion/LinearColorSmoothing.cpp index 0fe3e208..47974c90 100644 --- a/libsrc/hyperion/LinearColorSmoothing.cpp +++ b/libsrc/hyperion/LinearColorSmoothing.cpp @@ -3,13 +3,18 @@ #include "LinearColorSmoothing.h" -LinearColorSmoothing::LinearColorSmoothing(LedDevice *ledDevice, double ledUpdateFrequency_hz, int settlingTime_ms) : +LinearColorSmoothing::LinearColorSmoothing( + LedDevice *ledDevice, + double ledUpdateFrequency_hz, + int settlingTime_ms, + unsigned updateDelay) : QObject(), LedDevice(), _ledDevice(ledDevice), _updateInterval(1000 / ledUpdateFrequency_hz), _settlingTime(settlingTime_ms), - _timer() + _timer(), + _outputDelay(updateDelay) { _timer.setSingleShot(false); _timer.setInterval(_updateInterval); @@ -25,7 +30,7 @@ LinearColorSmoothing::~LinearColorSmoothing() int LinearColorSmoothing::write(const std::vector &ledValues) { // received a new target color - if (_previousValues.size() == 0) + if (_previousValues.empty()) { // not initialized yet _targetTime = QDateTime::currentMSecsSinceEpoch() + _settlingTime; @@ -46,17 +51,19 @@ int LinearColorSmoothing::write(const std::vector &ledValues) int LinearColorSmoothing::switchOff() { - // stop smoothing filter - _timer.stop(); - - // return to uninitialized state - _previousValues.clear(); - _previousTime = 0; - _targetValues.clear(); + // Clear the smoothing parameters + std::fill(_targetValues.begin(), _targetValues.end(), ColorRgb::BLACK); _targetTime = 0; - // finally switch off all leds - return _ledDevice->switchOff(); + // Erase the output-queue + for (unsigned i=0; i<_outputQueue.size(); ++i) + { + _outputQueue.push_back(_targetValues); + _outputQueue.pop_front(); + } + + + return 0; } void LinearColorSmoothing::updateLeds() @@ -69,7 +76,7 @@ void LinearColorSmoothing::updateLeds() memcpy(_previousValues.data(), _targetValues.data(), _targetValues.size() * sizeof(ColorRgb)); _previousTime = now; - _ledDevice->write(_previousValues); + queueColors(_previousValues); } else { @@ -86,6 +93,23 @@ void LinearColorSmoothing::updateLeds() } _previousTime = now; - _ledDevice->write(_previousValues); + queueColors(_previousValues); + } +} + +void LinearColorSmoothing::queueColors(const std::vector & ledColors) +{ + if (_outputDelay == 0) + { + _ledDevice->write(ledColors); + } + else + { + _outputQueue.push_back(ledColors); + if (_outputQueue.size() > _outputDelay) + { + _ledDevice->write(_outputQueue.front()); + _outputQueue.pop_front(); + } } } diff --git a/libsrc/hyperion/LinearColorSmoothing.h b/libsrc/hyperion/LinearColorSmoothing.h index 5c27862d..2b75059e 100644 --- a/libsrc/hyperion/LinearColorSmoothing.h +++ b/libsrc/hyperion/LinearColorSmoothing.h @@ -23,7 +23,8 @@ public: /// @param LedDevice the led device /// @param LedUpdatFrequency The frequency at which the leds will be updated (Hz) /// @param settingTime The time after which the updated led values have been fully applied (sec) - LinearColorSmoothing(LedDevice *ledDevice, double ledUpdateFrequency, int settlingTime); + /// @param updateDelay The number of frames to delay outgoing led updates + LinearColorSmoothing(LedDevice *ledDevice, double ledUpdateFrequency, int settlingTime, unsigned updateDelay); /// Destructor virtual ~LinearColorSmoothing(); @@ -43,6 +44,13 @@ private slots: void updateLeds(); private: + /** + * Pushes the colors into the output queue and popping the head to the led-device + * + * @param ledColors The colors to queue + */ + void queueColors(const std::vector & ledColors); + /// The led device LedDevice * _ledDevice; @@ -66,4 +74,10 @@ private: /// The previously written led data std::vector _previousValues; + + /** The number of updates to keep in the output queue (delayed) before being output */ + const unsigned _outputDelay; + /** The output queue */ + std::list > _outputQueue; + }; From fbe8e0ad4e29ea4e278ac2831b259cc3f368e8aa Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Thu, 25 Sep 2014 22:05:01 +0200 Subject: [PATCH 27/59] Added some code comments Former-commit-id: 58c1ec1fd2dcc210b6d162d1e8b6c40a502f30fa --- libsrc/hyperion/LinearColorSmoothing.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libsrc/hyperion/LinearColorSmoothing.cpp b/libsrc/hyperion/LinearColorSmoothing.cpp index 47974c90..888b1c2f 100644 --- a/libsrc/hyperion/LinearColorSmoothing.cpp +++ b/libsrc/hyperion/LinearColorSmoothing.cpp @@ -4,7 +4,7 @@ #include "LinearColorSmoothing.h" LinearColorSmoothing::LinearColorSmoothing( - LedDevice *ledDevice, + LedDevice * ledDevice, double ledUpdateFrequency_hz, int settlingTime_ms, unsigned updateDelay) : @@ -51,6 +51,8 @@ int LinearColorSmoothing::write(const std::vector &ledValues) int LinearColorSmoothing::switchOff() { + // We will keep updating the leds (but with pure-black) + // Clear the smoothing parameters std::fill(_targetValues.begin(), _targetValues.end(), ColorRgb::BLACK); _targetTime = 0; @@ -62,7 +64,6 @@ int LinearColorSmoothing::switchOff() _outputQueue.pop_front(); } - return 0; } @@ -101,11 +102,14 @@ void LinearColorSmoothing::queueColors(const std::vector & ledColors) { if (_outputDelay == 0) { + // No output delay => immediate write _ledDevice->write(ledColors); } else { + // Push the new colors in the delay-buffer _outputQueue.push_back(ledColors); + // If the delay-buffer is filled pop the front and write to device if (_outputQueue.size() > _outputDelay) { _ledDevice->write(_outputQueue.front()); From 11d29d02c565151821fa045cc67522b06cc94c75 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Thu, 25 Sep 2014 22:31:20 +0200 Subject: [PATCH 28/59] Added debug statement to standard out Former-commit-id: daaeb2a07931942bbeaf272775da81902169ad04 --- libsrc/hyperion/LinearColorSmoothing.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libsrc/hyperion/LinearColorSmoothing.cpp b/libsrc/hyperion/LinearColorSmoothing.cpp index 888b1c2f..da932e9e 100644 --- a/libsrc/hyperion/LinearColorSmoothing.cpp +++ b/libsrc/hyperion/LinearColorSmoothing.cpp @@ -20,6 +20,8 @@ LinearColorSmoothing::LinearColorSmoothing( _timer.setInterval(_updateInterval); connect(&_timer, SIGNAL(timeout()), this, SLOT(updateLeds())); + + std::cout << "Created linear-smoothing(interval_ms=" << _updateInterval << ";settlingTime_ms=" << settlingTime_ms << ";updateDelay=" << _outputDelay << std::endl; } LinearColorSmoothing::~LinearColorSmoothing() From 7c1811091aba5b16b65d94d51d183570835ba4b1 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Thu, 25 Sep 2014 22:33:03 +0200 Subject: [PATCH 29/59] Updated RPi release Former-commit-id: 971008c4e31aaf67e7cfe28d163e3305f426a8e6 --- deploy/hyperion.tar.gz.REMOVED.git-id | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/hyperion.tar.gz.REMOVED.git-id b/deploy/hyperion.tar.gz.REMOVED.git-id index f71fa321..687dfb80 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -7dbfa7fe80db4f8e65357ff45ff92a1321dfde49 \ No newline at end of file +f8d592fd55a3ca744f582c960dcde3aaa0afe5c0 \ No newline at end of file From 27a8e8bca4a63d156ea95a0d9093312dd6a59566 Mon Sep 17 00:00:00 2001 From: "T.van der Zwan" Date: Fri, 26 Sep 2014 18:24:38 +0000 Subject: [PATCH 30/59] Created a preliminary binary release for the HummingBoard (or cubox) Former-commit-id: e572c1f0d610e4509f01f6523e95852bd06b7975 --- deploy/hummingboard_prerelease.tar.gz.REMOVED.git-id | 1 + 1 file changed, 1 insertion(+) create mode 100644 deploy/hummingboard_prerelease.tar.gz.REMOVED.git-id diff --git a/deploy/hummingboard_prerelease.tar.gz.REMOVED.git-id b/deploy/hummingboard_prerelease.tar.gz.REMOVED.git-id new file mode 100644 index 00000000..6069fe6c --- /dev/null +++ b/deploy/hummingboard_prerelease.tar.gz.REMOVED.git-id @@ -0,0 +1 @@ +fb7e80dc922d44ce6d88cfcc2bea3e634edb3d07 \ No newline at end of file From c9d3a91d3607fd084f09f32035344cb6a97787d4 Mon Sep 17 00:00:00 2001 From: "T.van der Zwan" Date: Fri, 26 Sep 2014 18:26:46 +0000 Subject: [PATCH 31/59] Updated the HummingBoard release Former-commit-id: bc32fd4c66e721930c183cafd5a2bbc765c55cfc --- deploy/hummingboard_prerelease.tar.gz.REMOVED.git-id | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/hummingboard_prerelease.tar.gz.REMOVED.git-id b/deploy/hummingboard_prerelease.tar.gz.REMOVED.git-id index 6069fe6c..25e27e13 100644 --- a/deploy/hummingboard_prerelease.tar.gz.REMOVED.git-id +++ b/deploy/hummingboard_prerelease.tar.gz.REMOVED.git-id @@ -1 +1 @@ -fb7e80dc922d44ce6d88cfcc2bea3e634edb3d07 \ No newline at end of file +63b2cc4bf190a0c0c996be6db30fcf03109c9625 \ No newline at end of file From 654f6416ea04f401eca501cdeb2bf9f28b623c80 Mon Sep 17 00:00:00 2001 From: "T.van der Zwan" Date: Fri, 26 Sep 2014 18:27:30 +0000 Subject: [PATCH 32/59] Updated proto-definition due to changes in newest protobuf library Former-commit-id: 2e5a2de1a05bdd5c5ccc8668d6a6d050b5fbb1de --- libsrc/protoserver/message.proto | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libsrc/protoserver/message.proto b/libsrc/protoserver/message.proto index ebc7cba2..33c995b3 100644 --- a/libsrc/protoserver/message.proto +++ b/libsrc/protoserver/message.proto @@ -17,7 +17,7 @@ message HyperionRequest { message ColorRequest { extend HyperionRequest { - required ColorRequest colorRequest = 10; + optional ColorRequest colorRequest = 10; } // priority to use when setting the color @@ -32,7 +32,7 @@ message ColorRequest { message ImageRequest { extend HyperionRequest { - required ImageRequest imageRequest = 11; + optional ImageRequest imageRequest = 11; } // priority to use when setting the image @@ -53,7 +53,7 @@ message ImageRequest { message ClearRequest { extend HyperionRequest { - required ClearRequest clearRequest = 12; + optional ClearRequest clearRequest = 12; } // priority which need to be cleared From d1f043f23328bcb924b428753a80d8bf3d529840 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Tue, 30 Sep 2014 21:00:42 +0200 Subject: [PATCH 33/59] Fixed switch-off on shutdown Former-commit-id: 25438c11a3fdb14c89f946d02893a257aada67ea --- deploy/hyperion.tar.gz.REMOVED.git-id | 2 +- libsrc/hyperion/LinearColorSmoothing.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/deploy/hyperion.tar.gz.REMOVED.git-id b/deploy/hyperion.tar.gz.REMOVED.git-id index 687dfb80..e7c9bf76 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -f8d592fd55a3ca744f582c960dcde3aaa0afe5c0 \ No newline at end of file +e1ca70a63f1ce9b6975aab98b8eb6ae1a353df14 \ No newline at end of file diff --git a/libsrc/hyperion/LinearColorSmoothing.cpp b/libsrc/hyperion/LinearColorSmoothing.cpp index da932e9e..eef5569d 100644 --- a/libsrc/hyperion/LinearColorSmoothing.cpp +++ b/libsrc/hyperion/LinearColorSmoothing.cpp @@ -26,6 +26,8 @@ LinearColorSmoothing::LinearColorSmoothing( LinearColorSmoothing::~LinearColorSmoothing() { + // Make sure to switch off the underlying led-device (because switchOff is no longer forwarded) + _ledDevice->switchOff(); delete _ledDevice; } From bbc6fe389d07309057827125c9068903183f3a52 Mon Sep 17 00:00:00 2001 From: Gamadril Date: Sat, 25 Oct 2014 22:35:53 +0200 Subject: [PATCH 34/59] Added very basic WebSocket support to json server Former-commit-id: 5d62331eecdbd10287ba0520bbb06814bca0bca7 --- libsrc/jsonserver/JsonClientConnection.cpp | 150 ++++++++++++++++++++- libsrc/jsonserver/JsonClientConnection.h | 3 + 2 files changed, 152 insertions(+), 1 deletion(-) diff --git a/libsrc/jsonserver/JsonClientConnection.cpp b/libsrc/jsonserver/JsonClientConnection.cpp index d478a4d2..b82b51d7 100644 --- a/libsrc/jsonserver/JsonClientConnection.cpp +++ b/libsrc/jsonserver/JsonClientConnection.cpp @@ -10,6 +10,8 @@ // Qt includes #include #include +#include +#include // hyperion util includes #include @@ -27,6 +29,7 @@ JsonClientConnection::JsonClientConnection(QTcpSocket *socket, Hyperion * hyperi _hyperion(hyperion), _receiveBuffer() { + _webSocketHandshakeDone = false; // connect internal signals and slots connect(_socket, SIGNAL(disconnected()), this, SLOT(socketClosed())); connect(_socket, SIGNAL(readyRead()), this, SLOT(readData())); @@ -41,7 +44,126 @@ JsonClientConnection::~JsonClientConnection() void JsonClientConnection::readData() { _receiveBuffer += _socket->readAll(); + + if (_webSocketHandshakeDone) { // websocket mode, data frame + quint8 opCode = 0; + quint64 payloadLength = 0; + bool isMasked = false; + quint32 index = 0; + quint8 maskKey[4]; + + if ((_receiveBuffer.at(0) & 0x80) == 0x80) { // final bit + opCode = _receiveBuffer.at(0) & 0x0F; + isMasked = (_receiveBuffer.at(1) & 0x80) == 0x80; + payloadLength = _receiveBuffer.at(1) & 0x7F; + index = 2; + + switch (payloadLength) { + case 126: + payloadLength = ((_receiveBuffer.at(2) << 8) & 0xFF00) | (_receiveBuffer.at(3) & 0xFF); + index += 2; + break; + case 127: { + payloadLength = 0; + for (int i=0; i < 8; i++) { + payloadLength |= ((quint64)(_receiveBuffer.at(index+i) & 0xFF)) << (8*(7-i)); + } + index += 8; + } + break; + default: + break; + } + + if (isMasked) { // if the data is masked we need to get the key for unmasking + for (int i=0; i < 4; i++) { + maskKey[i] = _receiveBuffer.at(index + i); + } + index += 4; + } + + // check the type of data frame + switch (opCode) { + case 0x01: { // text + QByteArray result = _receiveBuffer.mid(index, payloadLength); + _receiveBuffer.clear(); + + // unmask data if necessary + if (isMasked) { + for (uint i=0 ; i < payloadLength; i++) { + result[i] = (result[i] ^ maskKey[i % 4]); + } + } + + handleMessage(QString(result).toStdString()); + } + break; + case 0x08: { // close + quint8 close[]={0x88, 0}; + _socket->write((const char*)close, 2); + _socket->flush(); + _socket->close(); + } + break; + case 0x09: { // ping, send pong + quint8 close[]={0x0A, 0}; + _socket->write((const char*)close, 2); + _socket->flush(); + } + break; + } + } else { + std::cout << "Someone is sending very big messages over several frames... it's not supported yet" << std::endl; + quint8 close[]={0x88, 0}; + _socket->write((const char*)close, 2); + _socket->flush(); + _socket->close(); + } + } else { // might be a handshake request or raw socket data + if(_receiveBuffer.contains("Upgrade: websocket")){ // http header, might not be a very reliable check... + std::cout << "Websocket handshake" << std::endl; + + // get the key to tprepare an answer + int start = _receiveBuffer.indexOf("Sec-WebSocket-Key") + 19; + std::string value(_receiveBuffer.mid(start, _receiveBuffer.indexOf("\r\n", start) - start).data()); + _receiveBuffer.clear(); + // must be always appended + value += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + // generate sha1 hash + QByteArray hash = QCryptographicHash::hash(value.c_str(), QCryptographicHash::Sha1); + + // prepare an answer + std::ostringstream h; + h << "HTTP/1.1 101 Switching Protocols\r\n" << + "Upgrade: websocket\r\n" << + "Connection: Upgrade\r\n" << + "Sec-WebSocket-Accept: " << QString(hash.toBase64()).toStdString() << "\r\n\r\n"; + + _socket->write(h.str().c_str()); + _socket->flush(); + _webSocketHandshakeDone = true; // we are in WebSocket mode, data frames should follow next + } else { // raw socket data, handling as usual + int bytes = _receiveBuffer.indexOf('\n') + 1; + while(bytes > 0) + { + // create message string + std::string message(_receiveBuffer.data(), bytes); + + // remove message data from buffer + _receiveBuffer = _receiveBuffer.mid(bytes); + + // handle message + handleMessage(message); + + // try too look up '\n' again + bytes = _receiveBuffer.indexOf('\n') + 1; + } + } + } + + /* int bytes = _receiveBuffer.indexOf('\n') + 1; while(bytes > 0) { @@ -57,10 +179,12 @@ void JsonClientConnection::readData() // try too look up '\n' again bytes = _receiveBuffer.indexOf('\n') + 1; } + */ } void JsonClientConnection::socketClosed() { + _webSocketHandshakeDone = false; emit connectionClosed(this); } @@ -205,6 +329,9 @@ void JsonClientConnection::handleServerInfoCommand(const Json::Value &) Json::Value result; result["success"] = true; Json::Value & info = result["info"]; + + // add host name for remote clients + info["hostname"] = QHostInfo::localHostName().toStdString(); // collect priority information Json::Value & priorities = info["priorities"] = Json::Value(Json::arrayValue); @@ -362,7 +489,28 @@ void JsonClientConnection::sendMessage(const Json::Value &message) { Json::FastWriter writer; std::string serializedReply = writer.write(message); - _socket->write(serializedReply.data(), serializedReply.length()); + + if (!_webSocketHandshakeDone) { // raw tcp socket mode + _socket->write(serializedReply.data(), serializedReply.length()); + } else { // websocket mode + quint32 size = serializedReply.length(); + + // prepare data frame + QByteArray response; + response.append(0x81); + if (size > 125) { + response.append(0x7E); + response.append((size >> 8) & 0xFF); + response.append(size & 0xFF); + } else { + response.append(size); + } + + QByteArray data(serializedReply.c_str(), serializedReply.length()); + response.append(data); + + _socket->write(response.data(), response.length()); + } } void JsonClientConnection::sendSuccessReply() diff --git a/libsrc/jsonserver/JsonClientConnection.h b/libsrc/jsonserver/JsonClientConnection.h index dc80deba..b036e638 100644 --- a/libsrc/jsonserver/JsonClientConnection.h +++ b/libsrc/jsonserver/JsonClientConnection.h @@ -161,4 +161,7 @@ private: /// The buffer used for reading data from the socket QByteArray _receiveBuffer; + + /// used for WebSocket detection and connection handling + bool _webSocketHandshakeDone; }; From 6a9e75243d9bfa383381227549cbe37dea064abe Mon Sep 17 00:00:00 2001 From: Gamadril Date: Sun, 26 Oct 2014 10:55:40 +0100 Subject: [PATCH 35/59] Update JsonClientConnection.cpp small code cleanup Former-commit-id: 82029cd23f77b393b2e6688ed6e65424cb676452 --- libsrc/jsonserver/JsonClientConnection.cpp | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/libsrc/jsonserver/JsonClientConnection.cpp b/libsrc/jsonserver/JsonClientConnection.cpp index b82b51d7..d50ed951 100644 --- a/libsrc/jsonserver/JsonClientConnection.cpp +++ b/libsrc/jsonserver/JsonClientConnection.cpp @@ -65,7 +65,7 @@ void JsonClientConnection::readData() break; case 127: { payloadLength = 0; - for (int i=0; i < 8; i++) { + for (uint i=0; i < 8; i++) { payloadLength |= ((quint64)(_receiveBuffer.at(index+i) & 0xFF)) << (8*(7-i)); } index += 8; @@ -76,7 +76,7 @@ void JsonClientConnection::readData() } if (isMasked) { // if the data is masked we need to get the key for unmasking - for (int i=0; i < 4; i++) { + for (uint i=0; i < 4; i++) { maskKey[i] = _receiveBuffer.at(index + i); } index += 4; @@ -162,24 +162,6 @@ void JsonClientConnection::readData() } } } - - /* - int bytes = _receiveBuffer.indexOf('\n') + 1; - while(bytes > 0) - { - // create message string - std::string message(_receiveBuffer.data(), bytes); - - // remove message data from buffer - _receiveBuffer = _receiveBuffer.mid(bytes); - - // handle message - handleMessage(message); - - // try too look up '\n' again - bytes = _receiveBuffer.indexOf('\n') + 1; - } - */ } void JsonClientConnection::socketClosed() From 9168ee5098ccd5498181f5a500e8ca8020afb805 Mon Sep 17 00:00:00 2001 From: Gamadril Date: Sat, 8 Nov 2014 21:01:46 +0100 Subject: [PATCH 36/59] code refactoring Former-commit-id: 157f424e83c58fca4ed1e3283899cbbdfadcffe1 --- libsrc/jsonserver/JsonClientConnection.cpp | 253 ++++++++++++--------- libsrc/jsonserver/JsonClientConnection.h | 10 + 2 files changed, 156 insertions(+), 107 deletions(-) diff --git a/libsrc/jsonserver/JsonClientConnection.cpp b/libsrc/jsonserver/JsonClientConnection.cpp index d50ed951..87fb7c1f 100644 --- a/libsrc/jsonserver/JsonClientConnection.cpp +++ b/libsrc/jsonserver/JsonClientConnection.cpp @@ -27,9 +27,9 @@ JsonClientConnection::JsonClientConnection(QTcpSocket *socket, Hyperion * hyperi _socket(socket), _imageProcessor(ImageProcessorFactory::getInstance().newImageProcessor()), _hyperion(hyperion), - _receiveBuffer() + _receiveBuffer(), + _webSocketHandshakeDone(false) { - _webSocketHandshakeDone = false; // connect internal signals and slots connect(_socket, SIGNAL(disconnected()), this, SLOT(socketClosed())); connect(_socket, SIGNAL(readyRead()), this, SLOT(readData())); @@ -45,106 +45,19 @@ void JsonClientConnection::readData() { _receiveBuffer += _socket->readAll(); - if (_webSocketHandshakeDone) { // websocket mode, data frame - quint8 opCode = 0; - quint64 payloadLength = 0; - bool isMasked = false; - quint32 index = 0; - quint8 maskKey[4]; - - if ((_receiveBuffer.at(0) & 0x80) == 0x80) { // final bit - opCode = _receiveBuffer.at(0) & 0x0F; - isMasked = (_receiveBuffer.at(1) & 0x80) == 0x80; - payloadLength = _receiveBuffer.at(1) & 0x7F; - index = 2; - - switch (payloadLength) { - case 126: - payloadLength = ((_receiveBuffer.at(2) << 8) & 0xFF00) | (_receiveBuffer.at(3) & 0xFF); - index += 2; - break; - case 127: { - payloadLength = 0; - for (uint i=0; i < 8; i++) { - payloadLength |= ((quint64)(_receiveBuffer.at(index+i) & 0xFF)) << (8*(7-i)); - } - index += 8; - } - break; - default: - break; - } - - if (isMasked) { // if the data is masked we need to get the key for unmasking - for (uint i=0; i < 4; i++) { - maskKey[i] = _receiveBuffer.at(index + i); - } - index += 4; - } - - // check the type of data frame - switch (opCode) { - case 0x01: { // text - QByteArray result = _receiveBuffer.mid(index, payloadLength); - _receiveBuffer.clear(); - - // unmask data if necessary - if (isMasked) { - for (uint i=0 ; i < payloadLength; i++) { - result[i] = (result[i] ^ maskKey[i % 4]); - } - } - - handleMessage(QString(result).toStdString()); - } - break; - case 0x08: { // close - quint8 close[]={0x88, 0}; - _socket->write((const char*)close, 2); - _socket->flush(); - _socket->close(); - } - break; - case 0x09: { // ping, send pong - quint8 close[]={0x0A, 0}; - _socket->write((const char*)close, 2); - _socket->flush(); - } - break; - } - } else { - std::cout << "Someone is sending very big messages over several frames... it's not supported yet" << std::endl; - quint8 close[]={0x88, 0}; - _socket->write((const char*)close, 2); - _socket->flush(); - _socket->close(); - } - } else { // might be a handshake request or raw socket data - if(_receiveBuffer.contains("Upgrade: websocket")){ // http header, might not be a very reliable check... - std::cout << "Websocket handshake" << std::endl; - - // get the key to tprepare an answer - int start = _receiveBuffer.indexOf("Sec-WebSocket-Key") + 19; - std::string value(_receiveBuffer.mid(start, _receiveBuffer.indexOf("\r\n", start) - start).data()); - _receiveBuffer.clear(); - - // must be always appended - value += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - - // generate sha1 hash - QByteArray hash = QCryptographicHash::hash(value.c_str(), QCryptographicHash::Sha1); - - // prepare an answer - std::ostringstream h; - h << "HTTP/1.1 101 Switching Protocols\r\n" << - "Upgrade: websocket\r\n" << - "Connection: Upgrade\r\n" << - "Sec-WebSocket-Accept: " << QString(hash.toBase64()).toStdString() << "\r\n\r\n"; - - _socket->write(h.str().c_str()); - _socket->flush(); - _webSocketHandshakeDone = true; // we are in WebSocket mode, data frames should follow next - } else { // raw socket data, handling as usual + if (_webSocketHandshakeDone) + { + // websocket mode, data frame + handleWebSocketFrame(); + } else + { + // might be a handshake request or raw socket data + if(_receiveBuffer.contains("Upgrade: websocket")) + { + doWebSocketHandshake(); + } else + { + // raw socket data, handling as usual int bytes = _receiveBuffer.indexOf('\n') + 1; while(bytes > 0) { @@ -164,6 +77,128 @@ void JsonClientConnection::readData() } } +void JsonClientConnection::handleWebSocketFrame() +{ + if ((_receiveBuffer.at(0) & 0x80) == 0x80) + { + // final bit found, frame complete + quint8 * maskKey = NULL; + quint8 opCode = _receiveBuffer.at(0) & 0x0F; + bool isMasked = (_receiveBuffer.at(1) & 0x80) == 0x80; + quint64 payloadLength = _receiveBuffer.at(1) & 0x7F; + quint32 index = 2; + + switch (payloadLength) + { + case 126: + payloadLength = ((_receiveBuffer.at(2) << 8) & 0xFF00) | (_receiveBuffer.at(3) & 0xFF); + index += 2; + break; + case 127: + payloadLength = 0; + for (uint i=0; i < 8; i++) { + payloadLength |= ((quint64)(_receiveBuffer.at(index+i) & 0xFF)) << (8*(7-i)); + } + index += 8; + break; + default: + break; + } + + if (isMasked) + { + // if the data is masked we need to get the key for unmasking + maskKey = new quint8[4]; + for (uint i=0; i < 4; i++) + { + maskKey[i] = _receiveBuffer.at(index + i); + } + index += 4; + } + + // check the type of data frame + switch (opCode) + { + case 0x01: + { + // frame contains text, extract it + QByteArray result = _receiveBuffer.mid(index, payloadLength); + _receiveBuffer.clear(); + + // unmask data if necessary + if (isMasked) + { + for (uint i=0; i < payloadLength; i++) + { + result[i] = (result[i] ^ maskKey[i % 4]); + } + if (maskKey != NULL) + { + delete[] maskKey; + maskKey = NULL; + } + } + + handleMessage(QString(result).toStdString()); + } + break; + case 0x08: + { + // close request, confirm + quint8 close[] = {0x88, 0}; + _socket->write((const char*)close, 2); + _socket->flush(); + _socket->close(); + } + break; + case 0x09: + { + // ping received, send pong + quint8 pong[] = {0x0A, 0}; + _socket->write((const char*)pong, 2); + _socket->flush(); + } + break; + } + } else + { + std::cout << "Someone is sending very big messages over several frames... it's not supported yet" << std::endl; + quint8 close[] = {0x88, 0}; + _socket->write((const char*)close, 2); + _socket->flush(); + _socket->close(); + } +} + +void JsonClientConnection::doWebSocketHandshake() +{ + // http header, might not be a very reliable check... + std::cout << "Websocket handshake" << std::endl; + + // get the key to prepare an answer + int start = _receiveBuffer.indexOf("Sec-WebSocket-Key") + 19; + std::string value(_receiveBuffer.mid(start, _receiveBuffer.indexOf("\r\n", start) - start).data()); + _receiveBuffer.clear(); + + // must be always appended + value += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + // generate sha1 hash + QByteArray hash = QCryptographicHash::hash(value.c_str(), QCryptographicHash::Sha1); + + // prepare an answer + std::ostringstream h; + h << "HTTP/1.1 101 Switching Protocols\r\n" << + "Upgrade: websocket\r\n" << + "Connection: Upgrade\r\n" << + "Sec-WebSocket-Accept: " << QString(hash.toBase64()).toStdString() << "\r\n\r\n"; + + _socket->write(h.str().c_str()); + _socket->flush(); + // we are in WebSocket mode, data frames should follow next + _webSocketHandshakeDone = true; +} + void JsonClientConnection::socketClosed() { _webSocketHandshakeDone = false; @@ -472,15 +507,20 @@ void JsonClientConnection::sendMessage(const Json::Value &message) Json::FastWriter writer; std::string serializedReply = writer.write(message); - if (!_webSocketHandshakeDone) { // raw tcp socket mode + if (!_webSocketHandshakeDone) + { + // raw tcp socket mode _socket->write(serializedReply.data(), serializedReply.length()); - } else { // websocket mode + } else + { + // websocket mode quint32 size = serializedReply.length(); // prepare data frame QByteArray response; response.append(0x81); - if (size > 125) { + if (size > 125) + { response.append(0x7E); response.append((size >> 8) & 0xFF); response.append(size & 0xFF); @@ -488,8 +528,7 @@ void JsonClientConnection::sendMessage(const Json::Value &message) response.append(size); } - QByteArray data(serializedReply.c_str(), serializedReply.length()); - response.append(data); + response.append(serializedReply.c_str(), serializedReply.length()); _socket->write(response.data(), response.length()); } diff --git a/libsrc/jsonserver/JsonClientConnection.h b/libsrc/jsonserver/JsonClientConnection.h index b036e638..6575388a 100644 --- a/libsrc/jsonserver/JsonClientConnection.h +++ b/libsrc/jsonserver/JsonClientConnection.h @@ -136,6 +136,16 @@ private: /// @param error String describing the error /// void sendErrorReply(const std::string & error); + + /// + /// Do handshake for a websocket connection + /// + void doWebSocketHandshake(); + + /// + /// Handle incoming websocket data frame + /// + void handleWebSocketFrame(); private: /// From 0ea9c87c1a0e2b219fbbb4ab78fc2644854961b2 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Sun, 9 Nov 2014 15:56:21 +0100 Subject: [PATCH 37/59] Updated RPi release Former-commit-id: e25fabd5625c4c260a81766503bd2f9c67080a36 --- deploy/hyperion.tar.gz.REMOVED.git-id | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/hyperion.tar.gz.REMOVED.git-id b/deploy/hyperion.tar.gz.REMOVED.git-id index e7c9bf76..874b3ca0 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -e1ca70a63f1ce9b6975aab98b8eb6ae1a353df14 \ No newline at end of file +c2332ae026dcd9b5ededbbe2db493ae13e5208c5 \ No newline at end of file From d89f504d83f5565abf1861473fdf6ca0159c9148 Mon Sep 17 00:00:00 2001 From: poljvd Date: Fri, 21 Nov 2014 21:24:33 +0100 Subject: [PATCH 38/59] Refactor V4L2 and X11 grabbers to share more code Former-commit-id: 46176e53d1acf39f9bd0c0ecbb8e5fb5ab4d45be --- .../grabber}/X11Grabber.h | 0 include/protoserver/ProtoConnection.h | 102 +++++++ include/protoserver/ProtoConnectionWrapper.h | 34 +++ libsrc/grabber/CMakeLists.txt | 5 +- libsrc/grabber/x11/CMakeLists.txt | 37 +++ libsrc/grabber/x11/X11Grabber.cpp | 115 ++++++++ libsrc/protoserver/CMakeLists.txt | 8 +- libsrc/protoserver/ProtoConnection.cpp | 188 +++++++++++++ libsrc/protoserver/ProtoConnectionWrapper.cpp | 19 ++ src/hyperion-v4l2/CMakeLists.txt | 21 +- src/hyperion-v4l2/ImageHandler.cpp | 18 -- src/hyperion-v4l2/ImageHandler.h | 31 --- src/hyperion-v4l2/ProtoConnection.cpp | 188 ------------- src/hyperion-v4l2/ProtoConnection.h | 102 ------- src/hyperion-v4l2/hyperion-v4l2.cpp | 250 +++++++++--------- src/hyperion-x11/CMakeLists.txt | 23 +- src/hyperion-x11/ProtoWrapper.cpp | 17 -- src/hyperion-x11/ProtoWrapper.h | 23 -- src/hyperion-x11/X11Grabber.cpp | 115 -------- src/hyperion-x11/X11Wrapper.cpp | 20 +- src/hyperion-x11/X11Wrapper.h | 38 +-- src/hyperion-x11/hyperion-x11.cpp | 118 ++++----- 22 files changed, 724 insertions(+), 748 deletions(-) rename {src/hyperion-x11 => include/grabber}/X11Grabber.h (100%) create mode 100644 include/protoserver/ProtoConnection.h create mode 100644 include/protoserver/ProtoConnectionWrapper.h create mode 100644 libsrc/grabber/x11/CMakeLists.txt create mode 100644 libsrc/grabber/x11/X11Grabber.cpp create mode 100644 libsrc/protoserver/ProtoConnection.cpp create mode 100644 libsrc/protoserver/ProtoConnectionWrapper.cpp delete mode 100644 src/hyperion-v4l2/ImageHandler.cpp delete mode 100644 src/hyperion-v4l2/ImageHandler.h delete mode 100644 src/hyperion-v4l2/ProtoConnection.cpp delete mode 100644 src/hyperion-v4l2/ProtoConnection.h delete mode 100644 src/hyperion-x11/ProtoWrapper.cpp delete mode 100644 src/hyperion-x11/ProtoWrapper.h delete mode 100644 src/hyperion-x11/X11Grabber.cpp diff --git a/src/hyperion-x11/X11Grabber.h b/include/grabber/X11Grabber.h similarity index 100% rename from src/hyperion-x11/X11Grabber.h rename to include/grabber/X11Grabber.h diff --git a/include/protoserver/ProtoConnection.h b/include/protoserver/ProtoConnection.h new file mode 100644 index 00000000..bb41e1b7 --- /dev/null +++ b/include/protoserver/ProtoConnection.h @@ -0,0 +1,102 @@ +#pragma once + +// stl includes +#include + +// Qt includes +#include +#include +#include +#include + +// hyperion util +#include +#include + +// jsoncpp includes +#include + +/// +/// Connection class to setup an connection to the hyperion server and execute commands +/// +class ProtoConnection +{ +public: + /// + /// Constructor + /// + /// @param address The address of the Hyperion server (for example "192.168.0.32:19444) + /// + ProtoConnection(const std::string & address); + + /// + /// Destructor + /// + ~ProtoConnection(); + + /// Do not read reply messages from Hyperion if set to true + void setSkipReply(bool skip); + + /// + /// Set all leds to the specified color + /// + /// @param color The color + /// @param priority The priority + /// @param duration The duration in milliseconds + /// + void setColor(const ColorRgb & color, int priority, int duration = 1); + + /// + /// Set the leds according to the given image (assume the image is stretched to the display size) + /// + /// @param image The image + /// @param priority The priority + /// @param duration The duration in milliseconds + /// + void setImage(const Image & image, int priority, int duration = -1); + + /// + /// Clear the given priority channel + /// + /// @param priority The priority + /// + void clear(int priority); + + /// + /// Clear all priority channels + /// + void clearAll(); + +private: + /// Try to connect to the Hyperion host + void connectToHost(); + + /// + /// Send a command message and receive its reply + /// + /// @param message The message to send + /// + void sendMessage(const proto::HyperionRequest & message); + + /// + /// Parse a reply message + /// + /// @param reply The received reply + /// + /// @return true if the reply indicates success + /// + bool parseReply(const proto::HyperionReply & reply); + +private: + /// The TCP-Socket with the connection to the server + QTcpSocket _socket; + + /// Host address + QString _host; + + /// Host port + uint16_t _port; + + /// Skip receiving reply messages from Hyperion if set + bool _skipReply; +}; diff --git a/include/protoserver/ProtoConnectionWrapper.h b/include/protoserver/ProtoConnectionWrapper.h new file mode 100644 index 00000000..f5b072ea --- /dev/null +++ b/include/protoserver/ProtoConnectionWrapper.h @@ -0,0 +1,34 @@ +// Qt includes +#include + +// hyperion includes +#include +#include + +// hyperion proto includes +#include "protoserver/ProtoConnection.h" + +/// This class handles callbacks from the V4L2 grabber +class ProtoConnectionWrapper : public QObject +{ + Q_OBJECT + +public: + ProtoConnectionWrapper(const std::string & address, int priority, int duration_ms, bool skipProtoReply); + virtual ~ProtoConnectionWrapper(); + +public slots: + /// Handle a single image + /// @param image The image to process + void receiveImage(const Image & image); + +private: + /// Priority for calls to Hyperion + const int _priority; + + /// Duration for color calls to Hyperion + const int _duration_ms; + + /// Hyperion proto connection object + ProtoConnection _connection; +}; diff --git a/libsrc/grabber/CMakeLists.txt b/libsrc/grabber/CMakeLists.txt index 322a5a98..85279243 100644 --- a/libsrc/grabber/CMakeLists.txt +++ b/libsrc/grabber/CMakeLists.txt @@ -1,4 +1,3 @@ - if (ENABLE_DISPMANX) add_subdirectory(dispmanx) endif (ENABLE_DISPMANX) @@ -6,3 +5,7 @@ endif (ENABLE_DISPMANX) if (ENABLE_V4L2) add_subdirectory(v4l2) endif (ENABLE_V4L2) + +if (ENABLE_X11) + add_subdirectory(x11) +endif() diff --git a/libsrc/grabber/x11/CMakeLists.txt b/libsrc/grabber/x11/CMakeLists.txt new file mode 100644 index 00000000..869fb4bf --- /dev/null +++ b/libsrc/grabber/x11/CMakeLists.txt @@ -0,0 +1,37 @@ +# Define the current source locations +SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber) +SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/x11) + +# Find X11 +find_package(X11 REQUIRED) + +include_directories( + ${QT_INCLUDES} + ${X11_INCLUDES} +) + +SET(X11_QT_HEADERS + ${CURRENT_HEADER_DIR}/X11Grabber.h +) + +SET(X11_HEADERS + ${CURRENT_HEADER_DIR}/X11Grabber.h +) + +SET(X11_SOURCES + ${CURRENT_SOURCE_DIR}/X11Grabber.cpp +) + +QT4_WRAP_CPP(X11_HEADERS_MOC ${X11_QT_HEADERS}) + +add_library(x11-grabber + ${X11_HEADERS} + ${X11_SOURCES} + ${X11_QT_HEADERS} + ${X11_HEADERS_MOC} +) + +target_link_libraries(x11-grabber + hyperion + ${QT_LIBRARIES} +) diff --git a/libsrc/grabber/x11/X11Grabber.cpp b/libsrc/grabber/x11/X11Grabber.cpp new file mode 100644 index 00000000..93f6e434 --- /dev/null +++ b/libsrc/grabber/x11/X11Grabber.cpp @@ -0,0 +1,115 @@ +// STL includes +#include +#include + +// X11 includes +#include + +// X11Grabber includes +#include + +X11Grabber::X11Grabber(const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation) : + _pixelDecimation(pixelDecimation), + _cropWidth(cropHorizontal), + _cropHeight(cropVertical), + _x11Display(nullptr), + _screenWidth(0), + _screenHeight(0), + _image(0,0) +{ + // empty +} + +X11Grabber::~X11Grabber() +{ + if (_x11Display != nullptr) + { + XCloseDisplay(_x11Display); + } +} + +int X11Grabber::open() +{ + const char * display_name = nullptr; + _x11Display = XOpenDisplay(display_name); + + if (_x11Display == nullptr) + { + std::cerr << "Failed to open the default X11-display" << std::endl; + return -1; + } + + return 0; +} + +Image & X11Grabber::grab() +{ + if (_x11Display == nullptr) + { + open(); + } + + updateScreenDimensions(); + + const int croppedWidth = _screenWidth - 2*_cropWidth; + const int croppedHeight = _screenHeight - 2*_cropHeight; + + // Capture the current screen + XImage * xImage = XGetImage(_x11Display, DefaultRootWindow(_x11Display), _cropWidth, _cropHeight, croppedWidth, croppedHeight, AllPlanes, ZPixmap); + if (xImage == nullptr) + { + std::cerr << "Grab failed" << std::endl; + return _image; + } + + // Copy the capture XImage to the local image (and apply required decimation) + ColorRgb * outputPtr = _image.memptr(); + for (int iY=(_pixelDecimation/2); iYred = uint8_t((pixel >> 16) & 0xff); + outputPtr->green = uint8_t((pixel >> 8) & 0xff); + outputPtr->blue = uint8_t((pixel >> 0) & 0xff); + + // Move to the next output pixel + ++outputPtr; + } + } + // Cleanup allocated resources of the X11 grab + XDestroyImage(xImage); + + return _image; +} + +int X11Grabber::updateScreenDimensions() +{ + XWindowAttributes window_attributes_return; + const Status status = XGetWindowAttributes(_x11Display, DefaultRootWindow(_x11Display), &window_attributes_return); + if (status == 0) + { + std::cerr << "Failed to obtain window attributes" << std::endl; + return -1; + } + + if (_screenWidth == unsigned(window_attributes_return.width) && _screenHeight == unsigned(window_attributes_return.height)) + { + // No update required + return 0; + } + std::cout << "Update of screen resolution: [" << _screenWidth << "x" << _screenHeight <<"] => "; + _screenWidth = window_attributes_return.width; + _screenHeight = window_attributes_return.height; + std::cout << "[" << _screenWidth << "x" << _screenHeight <<"]" << std::endl; + + // Update the size of the buffer used to transfer the screenshot + int width = (_screenWidth - 2 * _cropWidth + _pixelDecimation/2) / _pixelDecimation; + int height = (_screenHeight - 2 * _cropHeight + _pixelDecimation/2) / _pixelDecimation; + _image.resize(width, height); + + return 0; +} diff --git a/libsrc/protoserver/CMakeLists.txt b/libsrc/protoserver/CMakeLists.txt index 9e1992a3..f13c6a83 100644 --- a/libsrc/protoserver/CMakeLists.txt +++ b/libsrc/protoserver/CMakeLists.txt @@ -10,7 +10,9 @@ include_directories( # Group the headers that go through the MOC compiler set(ProtoServer_QT_HEADERS ${CURRENT_HEADER_DIR}/ProtoServer.h + ${CURRENT_HEADER_DIR}/ProtoConnection.h ${CURRENT_SOURCE_DIR}/ProtoClientConnection.h + ${CURRENT_HEADER_DIR}/ProtoConnectionWrapper.h ) set(ProtoServer_HEADERS @@ -19,6 +21,8 @@ set(ProtoServer_HEADERS set(ProtoServer_SOURCES ${CURRENT_SOURCE_DIR}/ProtoServer.cpp ${CURRENT_SOURCE_DIR}/ProtoClientConnection.cpp + ${CURRENT_SOURCE_DIR}/ProtoConnection.cpp + ${CURRENT_SOURCE_DIR}/ProtoConnectionWrapper.cpp ) set(ProtoServer_PROTOS @@ -44,5 +48,5 @@ add_library(protoserver target_link_libraries(protoserver hyperion hyperion-utils - ${PROTOBUF_LIBRARIES} - ${QT_LIBRARIES}) + ${PROTOBUF_LIBRARIES} + ${QT_LIBRARIES}) diff --git a/libsrc/protoserver/ProtoConnection.cpp b/libsrc/protoserver/ProtoConnection.cpp new file mode 100644 index 00000000..d7a6037e --- /dev/null +++ b/libsrc/protoserver/ProtoConnection.cpp @@ -0,0 +1,188 @@ +// stl includes +#include + +// Qt includes +#include + +// protoserver includes +#include "protoserver/ProtoConnection.h" + +ProtoConnection::ProtoConnection(const std::string & a) : + _socket(), + _skipReply(false) +{ + QString address(a.c_str()); + QStringList parts = address.split(":"); + if (parts.size() != 2) + { + throw std::runtime_error(QString("Wrong address: unable to parse address (%1)").arg(address).toStdString()); + } + _host = parts[0]; + + bool ok; + _port = parts[1].toUShort(&ok); + if (!ok) + { + throw std::runtime_error(QString("Wrong address: Unable to parse the port number (%1)").arg(parts[1]).toStdString()); + } + + // try to connect to host + std::cout << "Connecting to Hyperion: " << _host.toStdString() << ":" << _port << std::endl; + connectToHost(); +} + +ProtoConnection::~ProtoConnection() +{ + _socket.close(); +} + +void ProtoConnection::setSkipReply(bool skip) +{ + _skipReply = skip; +} + +void ProtoConnection::setColor(const ColorRgb & color, int priority, int duration) +{ + proto::HyperionRequest request; + request.set_command(proto::HyperionRequest::COLOR); + proto::ColorRequest * colorRequest = request.MutableExtension(proto::ColorRequest::colorRequest); + colorRequest->set_rgbcolor((color.red << 16) | (color.green << 8) | color.blue); + colorRequest->set_priority(priority); + colorRequest->set_duration(duration); + + // send command message + sendMessage(request); +} + +void ProtoConnection::setImage(const Image &image, int priority, int duration) +{ + proto::HyperionRequest request; + request.set_command(proto::HyperionRequest::IMAGE); + proto::ImageRequest * imageRequest = request.MutableExtension(proto::ImageRequest::imageRequest); + imageRequest->set_imagedata(image.memptr(), image.width() * image.height() * 3); + imageRequest->set_imagewidth(image.width()); + imageRequest->set_imageheight(image.height()); + imageRequest->set_priority(priority); + imageRequest->set_duration(duration); + + // send command message + sendMessage(request); +} + +void ProtoConnection::clear(int priority) +{ + proto::HyperionRequest request; + request.set_command(proto::HyperionRequest::CLEAR); + proto::ClearRequest * clearRequest = request.MutableExtension(proto::ClearRequest::clearRequest); + clearRequest->set_priority(priority); + + // send command message + sendMessage(request); +} + +void ProtoConnection::clearAll() +{ + proto::HyperionRequest request; + request.set_command(proto::HyperionRequest::CLEARALL); + + // send command message + sendMessage(request); +} + +void ProtoConnection::connectToHost() +{ + _socket.connectToHost(_host, _port); + if (_socket.waitForConnected()) { + std::cout << "Connected to Hyperion host" << std::endl; + } +} + +void ProtoConnection::sendMessage(const proto::HyperionRequest &message) +{ + if (_socket.state() == QAbstractSocket::UnconnectedState) + { + std::cout << "Currently disconnected: trying to connect to host" << std::endl; + connectToHost(); + } + + if (_socket.state() != QAbstractSocket::ConnectedState) + { + return; + } + + // We only get here if we are connected + + // serialize message (FastWriter already appends a newline) + std::string serializedMessage = message.SerializeAsString(); + + int length = serializedMessage.size(); + const uint8_t header[] = { + uint8_t((length >> 24) & 0xFF), + uint8_t((length >> 16) & 0xFF), + uint8_t((length >> 8) & 0xFF), + uint8_t((length ) & 0xFF)}; + + // write message + int count = 0; + count += _socket.write(reinterpret_cast(header), 4); + count += _socket.write(reinterpret_cast(serializedMessage.data()), length); + if (!_socket.waitForBytesWritten()) + { + std::cerr << "Error while writing data to host" << std::endl; + return; + } + + if (!_skipReply) + { + // read reply data + QByteArray serializedReply; + length = -1; + while (length < 0 && serializedReply.size() < length+4) + { + // receive reply + if (!_socket.waitForReadyRead()) + { + std::cerr << "Error while reading data from host" << std::endl; + return; + } + + serializedReply += _socket.readAll(); + + if (length < 0 && serializedReply.size() >= 4) + { + // read the message size + length = + ((serializedReply[0]<<24) & 0xFF000000) | + ((serializedReply[1]<<16) & 0x00FF0000) | + ((serializedReply[2]<< 8) & 0x0000FF00) | + ((serializedReply[3] ) & 0x000000FF); + } + } + + // parse reply data + proto::HyperionReply reply; + reply.ParseFromArray(serializedReply.constData()+4, length); + + // parse reply message + parseReply(reply); + } +} + +bool ProtoConnection::parseReply(const proto::HyperionReply &reply) +{ + bool success = false; + + if (!reply.success()) + { + if (reply.has_error()) + { + throw std::runtime_error("Error: " + reply.error()); + } + else + { + throw std::runtime_error("Error: No error info"); + } + } + + return success; +} diff --git a/libsrc/protoserver/ProtoConnectionWrapper.cpp b/libsrc/protoserver/ProtoConnectionWrapper.cpp new file mode 100644 index 00000000..7cf88f43 --- /dev/null +++ b/libsrc/protoserver/ProtoConnectionWrapper.cpp @@ -0,0 +1,19 @@ +// protoserver includes +#include "protoserver/ProtoConnectionWrapper.h" + +ProtoConnectionWrapper::ProtoConnectionWrapper(const std::string & address, int priority, int duration_ms, bool skipProtoReply) : + _priority(priority), + _duration_ms(duration_ms), + _connection(address) +{ + _connection.setSkipReply(skipProtoReply); +} + +ProtoConnectionWrapper::~ProtoConnectionWrapper() +{ +} + +void ProtoConnectionWrapper::receiveImage(const Image & image) +{ + _connection.setImage(image, _priority, _duration_ms); +} diff --git a/src/hyperion-v4l2/CMakeLists.txt b/src/hyperion-v4l2/CMakeLists.txt index 7d13baf8..6607c04f 100644 --- a/src/hyperion-v4l2/CMakeLists.txt +++ b/src/hyperion-v4l2/CMakeLists.txt @@ -2,44 +2,29 @@ cmake_minimum_required(VERSION 2.8) project(hyperion-v4l2) -# add protocol buffers -find_package(Protobuf REQUIRED) - # find Qt4 find_package(Qt4 REQUIRED QtCore QtGui QtNetwork) include_directories( - ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_CURRENT_BINARY_DIR}/../../libsrc/protoserver ${PROTOBUF_INCLUDE_DIRS} ${QT_INCLUDES} ) set(Hyperion_V4L2_QT_HEADERS - ImageHandler.h ScreenshotHandler.h ) set(Hyperion_V4L2_HEADERS VideoStandardParameter.h PixelFormatParameter.h - ProtoConnection.h ) set(Hyperion_V4L2_SOURCES hyperion-v4l2.cpp - ProtoConnection.cpp - ImageHandler.cpp ScreenshotHandler.cpp ) -set(Hyperion_V4L2_PROTOS - ${CMAKE_CURRENT_SOURCE_DIR}/../../libsrc/protoserver/message.proto -) - -protobuf_generate_cpp(Hyperion_V4L2_PROTO_SRCS Hyperion_V4L2_PROTO_HDRS - ${Hyperion_V4L2_PROTOS} -) - QT4_WRAP_CPP(Hyperion_V4L2_MOC_SOURCES ${Hyperion_V4L2_QT_HEADERS}) add_executable(hyperion-v4l2 @@ -47,8 +32,6 @@ add_executable(hyperion-v4l2 ${Hyperion_V4L2_SOURCES} ${Hyperion_V4L2_QT_HEADERS} ${Hyperion_V4L2_MOC_SOURCES} - ${Hyperion_V4L2_PROTO_SRCS} - ${Hyperion_V4L2_PROTO_HDRS} ) target_link_libraries(hyperion-v4l2 @@ -56,7 +39,7 @@ target_link_libraries(hyperion-v4l2 getoptPlusPlus blackborder hyperion-utils - ${PROTOBUF_LIBRARIES} + protoserver pthread ${QT_LIBRARIES} ) diff --git a/src/hyperion-v4l2/ImageHandler.cpp b/src/hyperion-v4l2/ImageHandler.cpp deleted file mode 100644 index 19a1da80..00000000 --- a/src/hyperion-v4l2/ImageHandler.cpp +++ /dev/null @@ -1,18 +0,0 @@ -// hyperion-v4l2 includes -#include "ImageHandler.h" - -ImageHandler::ImageHandler(const std::string & address, int priority, bool skipProtoReply) : - _priority(priority), - _connection(address) -{ - _connection.setSkipReply(skipProtoReply); -} - -ImageHandler::~ImageHandler() -{ -} - -void ImageHandler::receiveImage(const Image & image) -{ - _connection.setImage(image, _priority, 1000); -} diff --git a/src/hyperion-v4l2/ImageHandler.h b/src/hyperion-v4l2/ImageHandler.h deleted file mode 100644 index 8730989d..00000000 --- a/src/hyperion-v4l2/ImageHandler.h +++ /dev/null @@ -1,31 +0,0 @@ -// Qt includes -#include - -// hyperion includes -#include -#include - -// hyperion v4l2 includes -#include "ProtoConnection.h" - -/// This class handles callbacks from the V4L2 grabber -class ImageHandler : public QObject -{ - Q_OBJECT - -public: - ImageHandler(const std::string & address, int priority, bool skipProtoReply); - virtual ~ImageHandler(); - -public slots: - /// Handle a single image - /// @param image The image to process - void receiveImage(const Image & image); - -private: - /// Priority for calls to Hyperion - const int _priority; - - /// Hyperion proto connection object - ProtoConnection _connection; -}; diff --git a/src/hyperion-v4l2/ProtoConnection.cpp b/src/hyperion-v4l2/ProtoConnection.cpp deleted file mode 100644 index 9f59eddd..00000000 --- a/src/hyperion-v4l2/ProtoConnection.cpp +++ /dev/null @@ -1,188 +0,0 @@ -// stl includes -#include - -// Qt includes -#include - -// hyperion-v4l2 includes -#include "ProtoConnection.h" - -ProtoConnection::ProtoConnection(const std::string & a) : - _socket(), - _skipReply(false) -{ - QString address(a.c_str()); - QStringList parts = address.split(":"); - if (parts.size() != 2) - { - throw std::runtime_error(QString("Wrong address: unable to parse address (%1)").arg(address).toStdString()); - } - _host = parts[0]; - - bool ok; - _port = parts[1].toUShort(&ok); - if (!ok) - { - throw std::runtime_error(QString("Wrong address: Unable to parse the port number (%1)").arg(parts[1]).toStdString()); - } - - // try to connect to host - std::cout << "Connecting to Hyperion: " << _host.toStdString() << ":" << _port << std::endl; - connectToHost(); -} - -ProtoConnection::~ProtoConnection() -{ - _socket.close(); -} - -void ProtoConnection::setSkipReply(bool skip) -{ - _skipReply = skip; -} - -void ProtoConnection::setColor(const ColorRgb & color, int priority, int duration) -{ - proto::HyperionRequest request; - request.set_command(proto::HyperionRequest::COLOR); - proto::ColorRequest * colorRequest = request.MutableExtension(proto::ColorRequest::colorRequest); - colorRequest->set_rgbcolor((color.red << 16) | (color.green << 8) | color.blue); - colorRequest->set_priority(priority); - colorRequest->set_duration(duration); - - // send command message - sendMessage(request); -} - -void ProtoConnection::setImage(const Image &image, int priority, int duration) -{ - proto::HyperionRequest request; - request.set_command(proto::HyperionRequest::IMAGE); - proto::ImageRequest * imageRequest = request.MutableExtension(proto::ImageRequest::imageRequest); - imageRequest->set_imagedata(image.memptr(), image.width() * image.height() * 3); - imageRequest->set_imagewidth(image.width()); - imageRequest->set_imageheight(image.height()); - imageRequest->set_priority(priority); - imageRequest->set_duration(duration); - - // send command message - sendMessage(request); -} - -void ProtoConnection::clear(int priority) -{ - proto::HyperionRequest request; - request.set_command(proto::HyperionRequest::CLEAR); - proto::ClearRequest * clearRequest = request.MutableExtension(proto::ClearRequest::clearRequest); - clearRequest->set_priority(priority); - - // send command message - sendMessage(request); -} - -void ProtoConnection::clearAll() -{ - proto::HyperionRequest request; - request.set_command(proto::HyperionRequest::CLEARALL); - - // send command message - sendMessage(request); -} - -void ProtoConnection::connectToHost() -{ - _socket.connectToHost(_host, _port); - if (_socket.waitForConnected()) { - std::cout << "Connected to Hyperion host" << std::endl; - } -} - -void ProtoConnection::sendMessage(const proto::HyperionRequest &message) -{ - if (_socket.state() == QAbstractSocket::UnconnectedState) - { - std::cout << "Currently disconnected: trying to connect to host" << std::endl; - connectToHost(); - } - - if (_socket.state() != QAbstractSocket::ConnectedState) - { - return; - } - - // We only get here if we are connected - - // serialize message (FastWriter already appends a newline) - std::string serializedMessage = message.SerializeAsString(); - - int length = serializedMessage.size(); - const uint8_t header[] = { - uint8_t((length >> 24) & 0xFF), - uint8_t((length >> 16) & 0xFF), - uint8_t((length >> 8) & 0xFF), - uint8_t((length ) & 0xFF)}; - - // write message - int count = 0; - count += _socket.write(reinterpret_cast(header), 4); - count += _socket.write(reinterpret_cast(serializedMessage.data()), length); - if (!_socket.waitForBytesWritten()) - { - std::cerr << "Error while writing data to host" << std::endl; - return; - } - - if (!_skipReply) - { - // read reply data - QByteArray serializedReply; - length = -1; - while (length < 0 && serializedReply.size() < length+4) - { - // receive reply - if (!_socket.waitForReadyRead()) - { - std::cerr << "Error while reading data from host" << std::endl; - return; - } - - serializedReply += _socket.readAll(); - - if (length < 0 && serializedReply.size() >= 4) - { - // read the message size - length = - ((serializedReply[0]<<24) & 0xFF000000) | - ((serializedReply[1]<<16) & 0x00FF0000) | - ((serializedReply[2]<< 8) & 0x0000FF00) | - ((serializedReply[3] ) & 0x000000FF); - } - } - - // parse reply data - proto::HyperionReply reply; - reply.ParseFromArray(serializedReply.constData()+4, length); - - // parse reply message - parseReply(reply); - } -} - -bool ProtoConnection::parseReply(const proto::HyperionReply &reply) -{ - bool success = false; - - if (!reply.success()) - { - if (reply.has_error()) - { - throw std::runtime_error("Error: " + reply.error()); - } - else - { - throw std::runtime_error("Error: No error info"); - } - } - - return success; -} diff --git a/src/hyperion-v4l2/ProtoConnection.h b/src/hyperion-v4l2/ProtoConnection.h deleted file mode 100644 index a220c794..00000000 --- a/src/hyperion-v4l2/ProtoConnection.h +++ /dev/null @@ -1,102 +0,0 @@ -#pragma once - -// stl includes -#include - -// Qt includes -#include -#include -#include -#include - -// hyperion util -#include -#include - -// jsoncpp includes -#include - -/// -/// Connection class to setup an connection to the hyperion server and execute commands -/// -class ProtoConnection -{ -public: - /// - /// Constructor - /// - /// @param address The address of the Hyperion server (for example "192.168.0.32:19444) - /// - ProtoConnection(const std::string & address); - - /// - /// Destructor - /// - ~ProtoConnection(); - - /// Do not read reply messages from Hyperion if set to true - void setSkipReply(bool skip); - - /// - /// Set all leds to the specified color - /// - /// @param color The color - /// @param priority The priority - /// @param duration The duration in milliseconds - /// - void setColor(const ColorRgb & color, int priority, int duration = 1); - - /// - /// Set the leds according to the given image (assume the image is stretched to the display size) - /// - /// @param image The image - /// @param priority The priority - /// @param duration The duration in milliseconds - /// - void setImage(const Image & image, int priority, int duration = -1); - - /// - /// Clear the given priority channel - /// - /// @param priority The priority - /// - void clear(int priority); - - /// - /// Clear all priority channels - /// - void clearAll(); - -private: - /// Try to connect to the Hyperion host - void connectToHost(); - - /// - /// Send a command message and receive its reply - /// - /// @param message The message to send - /// - void sendMessage(const proto::HyperionRequest & message); - - /// - /// Parse a reply message - /// - /// @param reply The received reply - /// - /// @return true if the reply indicates success - /// - bool parseReply(const proto::HyperionReply & reply); - -private: - /// The TCP-Socket with the connection to the server - QTcpSocket _socket; - - /// Host address - QString _host; - - /// Host port - uint16_t _port; - - /// Skip receiving reply messages from Hyperion if set - bool _skipReply; -}; diff --git a/src/hyperion-v4l2/hyperion-v4l2.cpp b/src/hyperion-v4l2/hyperion-v4l2.cpp index 86c23781..aacd1c19 100644 --- a/src/hyperion-v4l2/hyperion-v4l2.cpp +++ b/src/hyperion-v4l2/hyperion-v4l2.cpp @@ -15,11 +15,13 @@ // grabber includes #include "grabber/V4L2Grabber.h" +// proto includes +#include "protoserver/ProtoConnection.h" +#include "protoserver/ProtoConnectionWrapper.h" + // hyperion-v4l2 includes -#include "ProtoConnection.h" #include "VideoStandardParameter.h" #include "PixelFormatParameter.h" -#include "ImageHandler.h" #include "ScreenshotHandler.h" using namespace vlofgren; @@ -27,144 +29,144 @@ using namespace vlofgren; // save the image as screenshot void saveScreenshot(void *, const Image & image) { - // store as PNG - QImage pngImage((const uint8_t *) image.memptr(), image.width(), image.height(), 3*image.width(), QImage::Format_RGB888); - pngImage.save("screenshot.png"); + // store as PNG + QImage pngImage((const uint8_t *) image.memptr(), image.width(), image.height(), 3*image.width(), QImage::Format_RGB888); + pngImage.save("screenshot.png"); } int main(int argc, char** argv) { - QCoreApplication app(argc, argv); + QCoreApplication app(argc, argv); - // force the locale - setlocale(LC_ALL, "C"); - QLocale::setDefault(QLocale::c()); + // force the locale + setlocale(LC_ALL, "C"); + QLocale::setDefault(QLocale::c()); - // register the image type to use in signals - qRegisterMetaType>("Image"); + // register the image type to use in signals + qRegisterMetaType>("Image"); - try - { - // create the option parser and initialize all parameters - OptionsParser optionParser("V4L capture application for Hyperion"); - ParameterSet & parameters = optionParser.getParameters(); + try + { + // create the option parser and initialize all parameters + OptionsParser optionParser("V4L capture application for Hyperion"); + ParameterSet & parameters = optionParser.getParameters(); - StringParameter & argDevice = parameters.add ('d', "device", "The device to use [default=/dev/video0]"); - VideoStandardParameter & argVideoStandard = parameters.add('v', "video-standard", "The used video standard. Valid values are PAL or NTSC (optional)"); - PixelFormatParameter & argPixelFormat = parameters.add (0x0, "pixel-format", "The use pixel format. Valid values are YUYV, UYVY, and RGB32 (optional)"); - IntParameter & argInput = parameters.add (0x0, "input", "Input channel (optional)"); - IntParameter & argWidth = parameters.add (0x0, "width", "Try to set the width of the video input (optional)"); - IntParameter & argHeight = parameters.add (0x0, "height", "Try to set the height of the video input (optional)"); - IntParameter & argCropWidth = parameters.add (0x0, "crop-width", "Number of pixels to crop from the left and right sides of the picture before decimation [default=0]"); - IntParameter & argCropHeight = parameters.add (0x0, "crop-height", "Number of pixels to crop from the top and the bottom of the picture before decimation [default=0]"); - IntParameter & argCropLeft = parameters.add (0x0, "crop-left", "Number of pixels to crop from the left of the picture before decimation (overrides --crop-width)"); - IntParameter & argCropRight = parameters.add (0x0, "crop-right", "Number of pixels to crop from the right of the picture before decimation (overrides --crop-width)"); - IntParameter & argCropTop = parameters.add (0x0, "crop-top", "Number of pixels to crop from the top of the picture before decimation (overrides --crop-height)"); - IntParameter & argCropBottom = parameters.add (0x0, "crop-bottom", "Number of pixels to crop from the bottom of the picture before decimation (overrides --crop-height)"); - IntParameter & argSizeDecimation = parameters.add ('s', "size-decimator", "Decimation factor for the output size [default=1]"); - IntParameter & argFrameDecimation = parameters.add ('f', "frame-decimator", "Decimation factor for the video frames [default=1]"); - SwitchParameter<> & argScreenshot = parameters.add> (0x0, "screenshot", "Take a single screenshot, save it to file and quit"); - DoubleParameter & argSignalThreshold = parameters.add ('t', "signal-threshold", "The signal threshold for detecting the presence of a signal. Value should be between 0.0 and 1.0."); - DoubleParameter & argRedSignalThreshold = parameters.add (0x0, "red-threshold", "The red signal threshold. Value should be between 0.0 and 1.0. (overrides --signal-threshold)"); - DoubleParameter & argGreenSignalThreshold = parameters.add (0x0, "green-threshold", "The green signal threshold. Value should be between 0.0 and 1.0. (overrides --signal-threshold)"); - DoubleParameter & argBlueSignalThreshold = parameters.add (0x0, "blue-threshold", "The blue signal threshold. Value should be between 0.0 and 1.0. (overrides --signal-threshold)"); - SwitchParameter<> & arg3DSBS = parameters.add> (0x0, "3DSBS", "Interpret the incoming video stream as 3D side-by-side"); - SwitchParameter<> & arg3DTAB = parameters.add> (0x0, "3DTAB", "Interpret the incoming video stream as 3D top-and-bottom"); - StringParameter & argAddress = parameters.add ('a', "address", "Set the address of the hyperion server [default: 127.0.0.1:19445]"); - IntParameter & argPriority = parameters.add ('p', "priority", "Use the provided priority channel (the lower the number, the higher the priority) [default: 800]"); - SwitchParameter<> & argSkipReply = parameters.add> (0x0, "skip-reply", "Do not receive and check reply messages from Hyperion"); - SwitchParameter<> & argHelp = parameters.add> ('h', "help", "Show this help message and exit"); + StringParameter & argDevice = parameters.add ('d', "device", "The device to use [default=/dev/video0]"); + VideoStandardParameter & argVideoStandard = parameters.add('v', "video-standard", "The used video standard. Valid values are PAL or NTSC (optional)"); + PixelFormatParameter & argPixelFormat = parameters.add (0x0, "pixel-format", "The use pixel format. Valid values are YUYV, UYVY, and RGB32 (optional)"); + IntParameter & argInput = parameters.add (0x0, "input", "Input channel (optional)"); + IntParameter & argWidth = parameters.add (0x0, "width", "Try to set the width of the video input (optional)"); + IntParameter & argHeight = parameters.add (0x0, "height", "Try to set the height of the video input (optional)"); + IntParameter & argCropWidth = parameters.add (0x0, "crop-width", "Number of pixels to crop from the left and right sides of the picture before decimation [default=0]"); + IntParameter & argCropHeight = parameters.add (0x0, "crop-height", "Number of pixels to crop from the top and the bottom of the picture before decimation [default=0]"); + IntParameter & argCropLeft = parameters.add (0x0, "crop-left", "Number of pixels to crop from the left of the picture before decimation (overrides --crop-width)"); + IntParameter & argCropRight = parameters.add (0x0, "crop-right", "Number of pixels to crop from the right of the picture before decimation (overrides --crop-width)"); + IntParameter & argCropTop = parameters.add (0x0, "crop-top", "Number of pixels to crop from the top of the picture before decimation (overrides --crop-height)"); + IntParameter & argCropBottom = parameters.add (0x0, "crop-bottom", "Number of pixels to crop from the bottom of the picture before decimation (overrides --crop-height)"); + IntParameter & argSizeDecimation = parameters.add ('s', "size-decimator", "Decimation factor for the output size [default=1]"); + IntParameter & argFrameDecimation = parameters.add ('f', "frame-decimator", "Decimation factor for the video frames [default=1]"); + SwitchParameter<> & argScreenshot = parameters.add> (0x0, "screenshot", "Take a single screenshot, save it to file and quit"); + DoubleParameter & argSignalThreshold = parameters.add ('t', "signal-threshold", "The signal threshold for detecting the presence of a signal. Value should be between 0.0 and 1.0."); + DoubleParameter & argRedSignalThreshold = parameters.add (0x0, "red-threshold", "The red signal threshold. Value should be between 0.0 and 1.0. (overrides --signal-threshold)"); + DoubleParameter & argGreenSignalThreshold = parameters.add (0x0, "green-threshold", "The green signal threshold. Value should be between 0.0 and 1.0. (overrides --signal-threshold)"); + DoubleParameter & argBlueSignalThreshold = parameters.add (0x0, "blue-threshold", "The blue signal threshold. Value should be between 0.0 and 1.0. (overrides --signal-threshold)"); + SwitchParameter<> & arg3DSBS = parameters.add> (0x0, "3DSBS", "Interpret the incoming video stream as 3D side-by-side"); + SwitchParameter<> & arg3DTAB = parameters.add> (0x0, "3DTAB", "Interpret the incoming video stream as 3D top-and-bottom"); + StringParameter & argAddress = parameters.add ('a', "address", "Set the address of the hyperion server [default: 127.0.0.1:19445]"); + IntParameter & argPriority = parameters.add ('p', "priority", "Use the provided priority channel (the lower the number, the higher the priority) [default: 800]"); + SwitchParameter<> & argSkipReply = parameters.add> (0x0, "skip-reply", "Do not receive and check reply messages from Hyperion"); + SwitchParameter<> & argHelp = parameters.add> ('h', "help", "Show this help message and exit"); - // set defaults - argDevice.setDefault("/dev/video0"); - argVideoStandard.setDefault(VIDEOSTANDARD_NO_CHANGE); - argPixelFormat.setDefault(PIXELFORMAT_NO_CHANGE); - argInput.setDefault(-1); - argWidth.setDefault(-1); - argHeight.setDefault(-1); - argCropWidth.setDefault(0); - argCropHeight.setDefault(0); - argSizeDecimation.setDefault(1); - argFrameDecimation.setDefault(1); - argAddress.setDefault("127.0.0.1:19445"); - argPriority.setDefault(800); - argSignalThreshold.setDefault(-1); + // set defaults + argDevice.setDefault("/dev/video0"); + argVideoStandard.setDefault(VIDEOSTANDARD_NO_CHANGE); + argPixelFormat.setDefault(PIXELFORMAT_NO_CHANGE); + argInput.setDefault(-1); + argWidth.setDefault(-1); + argHeight.setDefault(-1); + argCropWidth.setDefault(0); + argCropHeight.setDefault(0); + argSizeDecimation.setDefault(1); + argFrameDecimation.setDefault(1); + argAddress.setDefault("127.0.0.1:19445"); + argPriority.setDefault(800); + argSignalThreshold.setDefault(-1); - // parse all options - optionParser.parse(argc, const_cast(argv)); + // parse all options + optionParser.parse(argc, const_cast(argv)); - // check if we need to display the usage. exit if we do. - if (argHelp.isSet()) - { - optionParser.usage(); - return 0; - } + // check if we need to display the usage. exit if we do. + if (argHelp.isSet()) + { + optionParser.usage(); + return 0; + } - if (!argCropLeft.isSet()) argCropLeft.setDefault(argCropWidth.getValue()); - if (!argCropRight.isSet()) argCropRight.setDefault(argCropWidth.getValue()); - if (!argCropTop.isSet()) argCropTop.setDefault(argCropHeight.getValue()); - if (!argCropBottom.isSet()) argCropBottom.setDefault(argCropHeight.getValue()); + if (!argCropLeft.isSet()) argCropLeft.setDefault(argCropWidth.getValue()); + if (!argCropRight.isSet()) argCropRight.setDefault(argCropWidth.getValue()); + if (!argCropTop.isSet()) argCropTop.setDefault(argCropHeight.getValue()); + if (!argCropBottom.isSet()) argCropBottom.setDefault(argCropHeight.getValue()); - // initialize the grabber - V4L2Grabber grabber( - argDevice.getValue(), - argInput.getValue(), - argVideoStandard.getValue(), - argPixelFormat.getValue(), - argWidth.getValue(), - argHeight.getValue(), - std::max(1, argFrameDecimation.getValue()), - std::max(1, argSizeDecimation.getValue()), - std::max(1, argSizeDecimation.getValue())); + // initialize the grabber + V4L2Grabber grabber( + argDevice.getValue(), + argInput.getValue(), + argVideoStandard.getValue(), + argPixelFormat.getValue(), + argWidth.getValue(), + argHeight.getValue(), + std::max(1, argFrameDecimation.getValue()), + std::max(1, argSizeDecimation.getValue()), + std::max(1, argSizeDecimation.getValue())); - // set signal detection - grabber.setSignalThreshold( - std::min(1.0, std::max(0.0, argRedSignalThreshold.isSet() ? argRedSignalThreshold.getValue() : argSignalThreshold.getValue())), - std::min(1.0, std::max(0.0, argGreenSignalThreshold.isSet() ? argGreenSignalThreshold.getValue() : argSignalThreshold.getValue())), - std::min(1.0, std::max(0.0, argBlueSignalThreshold.isSet() ? argBlueSignalThreshold.getValue() : argSignalThreshold.getValue())), - 50); + // set signal detection + grabber.setSignalThreshold( + std::min(1.0, std::max(0.0, argRedSignalThreshold.isSet() ? argRedSignalThreshold.getValue() : argSignalThreshold.getValue())), + std::min(1.0, std::max(0.0, argGreenSignalThreshold.isSet() ? argGreenSignalThreshold.getValue() : argSignalThreshold.getValue())), + std::min(1.0, std::max(0.0, argBlueSignalThreshold.isSet() ? argBlueSignalThreshold.getValue() : argSignalThreshold.getValue())), + 50); - // set cropping values - grabber.setCropping( - std::max(0, argCropLeft.getValue()), - std::max(0, argCropRight.getValue()), - std::max(0, argCropTop.getValue()), - std::max(0, argCropBottom.getValue())); + // set cropping values + grabber.setCropping( + std::max(0, argCropLeft.getValue()), + std::max(0, argCropRight.getValue()), + std::max(0, argCropTop.getValue()), + std::max(0, argCropBottom.getValue())); - // set 3D mode if applicable - if (arg3DSBS.isSet()) - { - grabber.set3D(VIDEO_3DSBS); - } - else if (arg3DTAB.isSet()) - { - grabber.set3D(VIDEO_3DTAB); - } + // set 3D mode if applicable + if (arg3DSBS.isSet()) + { + grabber.set3D(VIDEO_3DSBS); + } + else if (arg3DTAB.isSet()) + { + grabber.set3D(VIDEO_3DTAB); + } - // run the grabber - if (argScreenshot.isSet()) - { - ScreenshotHandler handler("screenshot.png"); - QObject::connect(&grabber, SIGNAL(newFrame(Image)), &handler, SLOT(receiveImage(Image))); - grabber.start(); - QCoreApplication::exec(); - grabber.stop(); - } - else - { - ImageHandler handler(argAddress.getValue(), argPriority.getValue(), argSkipReply.isSet()); - QObject::connect(&grabber, SIGNAL(newFrame(Image)), &handler, SLOT(receiveImage(Image))); - grabber.start(); - QCoreApplication::exec(); - grabber.stop(); - } - } - catch (const std::runtime_error & e) - { - // An error occured. Display error and quit - std::cerr << e.what() << std::endl; - return 1; - } + // run the grabber + if (argScreenshot.isSet()) + { + ScreenshotHandler handler("screenshot.png"); + QObject::connect(&grabber, SIGNAL(newFrame(Image)), &handler, SLOT(receiveImage(Image))); + grabber.start(); + QCoreApplication::exec(); + grabber.stop(); + } + else + { + ProtoConnectionWrapper handler(argAddress.getValue(), argPriority.getValue(), 1000, argSkipReply.isSet()); + QObject::connect(&grabber, SIGNAL(newFrame(Image)), &handler, SLOT(receiveImage(Image))); + grabber.start(); + QCoreApplication::exec(); + grabber.stop(); + } + } + catch (const std::runtime_error & e) + { + // An error occured. Display error and quit + std::cerr << e.what() << std::endl; + return 1; + } - return 0; + return 0; } diff --git a/src/hyperion-x11/CMakeLists.txt b/src/hyperion-x11/CMakeLists.txt index 43cc9a1d..7841232c 100644 --- a/src/hyperion-x11/CMakeLists.txt +++ b/src/hyperion-x11/CMakeLists.txt @@ -4,9 +4,6 @@ cmake_minimum_required(VERSION 2.8) # Set the project name project(hyperion-x11) -# add protocol buffers -find_package(Protobuf REQUIRED) - # find Qt4 find_package(Qt4 REQUIRED QtCore QtGui QtNetwork) @@ -14,42 +11,27 @@ find_package(Qt4 REQUIRED QtCore QtGui QtNetwork) find_package(X11 REQUIRED) include_directories( - ${CMAKE_CURRENT_BINARY_DIR} - ${PROTOBUF_INCLUDE_DIRS} + ${CMAKE_CURRENT_BINARY_DIR}/../../libsrc/protoserver ${QT_INCLUDES} ${X11_INCLUDES} ) set(Hyperion_X11_QT_HEADERS - ProtoWrapper.h X11Wrapper.h) set(Hyperion_X11_HEADERS - X11Grabber.h - ../hyperion-v4l2/ProtoConnection.h ) set(Hyperion_X11_SOURCES hyperion-x11.cpp - ProtoWrapper.cpp - X11Grabber.cpp X11Wrapper.cpp - ../hyperion-v4l2/ProtoConnection.cpp -) - -set(Hyperion_X11_PROTOS - ${CMAKE_CURRENT_SOURCE_DIR}/../../libsrc/protoserver/message.proto ) QT4_WRAP_CPP(Hyperion_X11_HEADERS_MOC ${Hyperion_X11_QT_HEADERS}) -protobuf_generate_cpp(Hyperion_X11_PROTO_SRCS Hyperion_X11_PROTO_HDRS ${Hyperion_X11_PROTOS}) - add_executable(hyperion-x11 ${Hyperion_X11_HEADERS} ${Hyperion_X11_SOURCES} - ${Hyperion_X11_PROTO_SRCS} - ${Hyperion_X11_PROTO_HDRS} ${Hyperion_X11_HEADERS_MOC} ) @@ -57,7 +39,8 @@ target_link_libraries(hyperion-x11 getoptPlusPlus blackborder hyperion-utils - ${PROTOBUF_LIBRARIES} + protoserver + x11-grabber ${X11_LIBRARIES} pthread ) diff --git a/src/hyperion-x11/ProtoWrapper.cpp b/src/hyperion-x11/ProtoWrapper.cpp deleted file mode 100644 index c45fed7b..00000000 --- a/src/hyperion-x11/ProtoWrapper.cpp +++ /dev/null @@ -1,17 +0,0 @@ - -// -#include "ProtoWrapper.h" - -ProtoWrapper::ProtoWrapper(const std::string & protoAddress, const bool skipReply) : - _priority(10), - _duration_ms(2000), - _connection(protoAddress) -{ - _connection.setSkipReply(skipReply); -} - -void ProtoWrapper::process(const Image & image) -{ - std::cout << "Attempt to send screenshot" << std::endl; - _connection.setImage(image, _priority, _duration_ms); -} diff --git a/src/hyperion-x11/ProtoWrapper.h b/src/hyperion-x11/ProtoWrapper.h deleted file mode 100644 index 4d184868..00000000 --- a/src/hyperion-x11/ProtoWrapper.h +++ /dev/null @@ -1,23 +0,0 @@ - -// QT includes -#include - - -#include "../hyperion-v4l2/ProtoConnection.h" - -class ProtoWrapper : public QObject -{ - Q_OBJECT -public: - ProtoWrapper(const std::string & protoAddress, const bool skipReply); - -public slots: - void process(const Image & image); - -private: - - int _priority; - int _duration_ms; - - ProtoConnection _connection; -}; diff --git a/src/hyperion-x11/X11Grabber.cpp b/src/hyperion-x11/X11Grabber.cpp deleted file mode 100644 index cd6d0fda..00000000 --- a/src/hyperion-x11/X11Grabber.cpp +++ /dev/null @@ -1,115 +0,0 @@ -// STL includes -#include -#include - -// X11 includes -#include - -// X11Grabber includes -#include "X11Grabber.h" - -X11Grabber::X11Grabber(const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation) : - _pixelDecimation(pixelDecimation), - _cropWidth(cropHorizontal), - _cropHeight(cropVertical), - _x11Display(nullptr), - _screenWidth(0), - _screenHeight(0), - _image(0,0) -{ - // empty -} - -X11Grabber::~X11Grabber() -{ - if (_x11Display != nullptr) - { - XCloseDisplay(_x11Display); - } -} - -int X11Grabber::open() -{ - const char * display_name = nullptr; - _x11Display = XOpenDisplay(display_name); - - if (_x11Display == nullptr) - { - std::cerr << "Failed to open the default X11-display" << std::endl; - return -1; - } - - return 0; -} - -Image & X11Grabber::grab() -{ - if (_x11Display == nullptr) - { - open(); - } - - updateScreenDimensions(); - - const int croppedWidth = _screenWidth - 2*_cropWidth; - const int croppedHeight = _screenHeight - 2*_cropHeight; - - // Capture the current screen - XImage * xImage = XGetImage(_x11Display, DefaultRootWindow(_x11Display), _cropWidth, _cropHeight, croppedWidth, croppedHeight, AllPlanes, ZPixmap); - if (xImage == nullptr) - { - std::cerr << "Grab failed" << std::endl; - return _image; - } - - // Copy the capture XImage to the local image (and apply required decimation) - ColorRgb * outputPtr = _image.memptr(); - for (int iY=(_pixelDecimation/2); iYred = uint8_t((pixel >> 16) & 0xff); - outputPtr->green = uint8_t((pixel >> 8) & 0xff); - outputPtr->blue = uint8_t((pixel >> 0) & 0xff); - - // Move to the next output pixel - ++outputPtr; - } - } - // Cleanup allocated resources of the X11 grab - XDestroyImage(xImage); - - return _image; -} - -int X11Grabber::updateScreenDimensions() -{ - XWindowAttributes window_attributes_return; - const Status status = XGetWindowAttributes(_x11Display, DefaultRootWindow(_x11Display), &window_attributes_return); - if (status == 0) - { - std::cerr << "Failed to obtain window attributes" << std::endl; - return -1; - } - - if (_screenWidth == unsigned(window_attributes_return.width) && _screenHeight == unsigned(window_attributes_return.height)) - { - // No update required - return 0; - } - std::cout << "Update of screen resolution: [" << _screenWidth << "x" << _screenHeight <<"] => "; - _screenWidth = window_attributes_return.width; - _screenHeight = window_attributes_return.height; - std::cout << "[" << _screenWidth << "x" << _screenHeight <<"]" << std::endl; - - // Update the size of the buffer used to transfer the screenshot - int width = (_screenWidth - 2 * _cropWidth + _pixelDecimation/2) / _pixelDecimation; - int height = (_screenHeight - 2 * _cropHeight + _pixelDecimation/2) / _pixelDecimation; - _image.resize(width, height); - - return 0; -} diff --git a/src/hyperion-x11/X11Wrapper.cpp b/src/hyperion-x11/X11Wrapper.cpp index 5bf4a97f..303a03b4 100644 --- a/src/hyperion-x11/X11Wrapper.cpp +++ b/src/hyperion-x11/X11Wrapper.cpp @@ -3,31 +3,31 @@ #include "X11Wrapper.h" X11Wrapper::X11Wrapper(const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation) : - _timer(this), - _grabber(cropHorizontal, cropVertical, pixelDecimation) + _timer(this), + _grabber(cropHorizontal, cropVertical, pixelDecimation) { - // Connect capturing to the timeout signal of the timer - connect(&_timer, SIGNAL(timeout()), this, SLOT(capture())); + // Connect capturing to the timeout signal of the timer + connect(&_timer, SIGNAL(timeout()), this, SLOT(capture())); } const Image & X11Wrapper::getScreenshot() { - const Image & screenshot = _grabber.grab(); - return screenshot; + const Image & screenshot = _grabber.grab(); + return screenshot; } void X11Wrapper::start() { - _timer.start(200); + _timer.start(100); } void X11Wrapper::stop() { - _timer.stop(); + _timer.stop(); } void X11Wrapper::capture() { - const Image & screenshot = _grabber.grab(); - emit sig_screenshot(screenshot); + const Image & screenshot = _grabber.grab(); + emit sig_screenshot(screenshot); } diff --git a/src/hyperion-x11/X11Wrapper.h b/src/hyperion-x11/X11Wrapper.h index 405cbce9..986dceaf 100644 --- a/src/hyperion-x11/X11Wrapper.h +++ b/src/hyperion-x11/X11Wrapper.h @@ -3,37 +3,37 @@ #include // Hyperion-X11 includes -#include "X11Grabber.h" +#include class X11Wrapper : public QObject { - Q_OBJECT + Q_OBJECT public: - X11Wrapper(const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation); + X11Wrapper(const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation); - const Image & getScreenshot(); + const Image & getScreenshot(); - /// - /// Starts the timed capturing of screenshots - /// - void start(); + /// + /// Starts the timed capturing of screenshots + /// + void start(); - void stop(); + void stop(); signals: - void sig_screenshot(const Image & screenshot); + void sig_screenshot(const Image & screenshot); private slots: - /// - /// Performs a single screenshot capture and publishes the capture screenshot on the screenshot - /// signal. - /// - void capture(); + /// + /// Performs a single screenshot capture and publishes the capture screenshot on the screenshot + /// signal. + /// + void capture(); private: - /// The QT timer to generate capture-publish events - QTimer _timer; + /// The QT timer to generate capture-publish events + QTimer _timer; - /// The grabber for creating screenshots - X11Grabber _grabber; + /// The grabber for creating screenshots + X11Grabber _grabber; }; diff --git a/src/hyperion-x11/hyperion-x11.cpp b/src/hyperion-x11/hyperion-x11.cpp index aed83cc3..0a494516 100644 --- a/src/hyperion-x11/hyperion-x11.cpp +++ b/src/hyperion-x11/hyperion-x11.cpp @@ -6,7 +6,7 @@ // getoptPlusPLus includes #include -#include "ProtoWrapper.h" +#include "protoserver/ProtoConnectionWrapper.h" #include "X11Wrapper.h" using namespace vlofgren; @@ -14,77 +14,77 @@ using namespace vlofgren; // save the image as screenshot void saveScreenshot(const char * filename, const Image & image) { - // store as PNG - QImage pngImage((const uint8_t *) image.memptr(), image.width(), image.height(), 3*image.width(), QImage::Format_RGB888); - pngImage.save(filename); + // store as PNG + QImage pngImage((const uint8_t *) image.memptr(), image.width(), image.height(), 3*image.width(), QImage::Format_RGB888); + pngImage.save(filename); } int main(int argc, char ** argv) { - QCoreApplication app(argc, argv); + QCoreApplication app(argc, argv); - try - { - // create the option parser and initialize all parameters - OptionsParser optionParser("X11 capture application for Hyperion"); - ParameterSet & parameters = optionParser.getParameters(); + try + { + // create the option parser and initialize all parameters + OptionsParser optionParser("X11 capture application for Hyperion"); + ParameterSet & parameters = optionParser.getParameters(); - IntParameter & argCropWidth = parameters.add (0x0, "crop-width", "Number of pixels to crop from the left and right sides in the picture before decimation [default=0]"); - IntParameter & argCropHeight = parameters.add (0x0, "crop-height", "Number of pixels to crop from the top and the bottom in the picture before decimation [default=0]"); - IntParameter & argSizeDecimation = parameters.add ('s', "size-decimator", "Decimation factor for the output size [default=16]"); - SwitchParameter<> & argScreenshot = parameters.add> (0x0, "screenshot", "Take a single screenshot, save it to file and quit"); - StringParameter & argAddress = parameters.add ('a', "address", "Set the address of the hyperion server [default: 127.0.0.1:19445]"); - IntParameter & argPriority = parameters.add ('p', "priority", "Use the provided priority channel (the lower the number, the higher the priority) [default: 800]"); - SwitchParameter<> & argSkipReply = parameters.add> (0x0, "skip-reply", "Do not receive and check reply messages from Hyperion"); - SwitchParameter<> & argHelp = parameters.add> ('h', "help", "Show this help message and exit"); + IntParameter & argCropWidth = parameters.add (0x0, "crop-width", "Number of pixels to crop from the left and right sides in the picture before decimation [default=0]"); + IntParameter & argCropHeight = parameters.add (0x0, "crop-height", "Number of pixels to crop from the top and the bottom in the picture before decimation [default=0]"); + IntParameter & argSizeDecimation = parameters.add ('s', "size-decimator", "Decimation factor for the output size [default=16]"); + SwitchParameter<> & argScreenshot = parameters.add> (0x0, "screenshot", "Take a single screenshot, save it to file and quit"); + StringParameter & argAddress = parameters.add ('a', "address", "Set the address of the hyperion server [default: 127.0.0.1:19445]"); + IntParameter & argPriority = parameters.add ('p', "priority", "Use the provided priority channel (the lower the number, the higher the priority) [default: 800]"); + SwitchParameter<> & argSkipReply = parameters.add> (0x0, "skip-reply", "Do not receive and check reply messages from Hyperion"); + SwitchParameter<> & argHelp = parameters.add> ('h', "help", "Show this help message and exit"); - // set defaults - argCropWidth.setDefault(0); - argCropHeight.setDefault(0); - argSizeDecimation.setDefault(16); - argAddress.setDefault("127.0.0.1:19445"); - argPriority.setDefault(800); + // set defaults + argCropWidth.setDefault(0); + argCropHeight.setDefault(0); + argSizeDecimation.setDefault(16); + argAddress.setDefault("127.0.0.1:19445"); + argPriority.setDefault(800); - // parse all options - optionParser.parse(argc, const_cast(argv)); + // parse all options + optionParser.parse(argc, const_cast(argv)); - // check if we need to display the usage. exit if we do. - if (argHelp.isSet()) - { - optionParser.usage(); - return 0; - } + // check if we need to display the usage. exit if we do. + if (argHelp.isSet()) + { + optionParser.usage(); + return 0; + } - // Create the X11 grabbing stuff - X11Wrapper x11Wrapper(argCropWidth.getValue(), argCropHeight.getValue(), argSizeDecimation.getValue()); + // Create the X11 grabbing stuff + X11Wrapper x11Wrapper(argCropWidth.getValue(), argCropHeight.getValue(), argSizeDecimation.getValue()); - if (argScreenshot.isSet()) - { - // Capture a single screenshot and finish - const Image & screenshot = x11Wrapper.getScreenshot(); - saveScreenshot("screenshot.png", screenshot); - } - else - { - // Create the Proto-connection with hyperiond - ProtoWrapper protoWrapper(argAddress.getValue(), argSkipReply.isSet()); + if (argScreenshot.isSet()) + { + // Capture a single screenshot and finish + const Image & screenshot = x11Wrapper.getScreenshot(); + saveScreenshot("screenshot.png", screenshot); + } + else + { + // Create the Proto-connection with hyperiond + ProtoConnectionWrapper protoWrapper(argAddress.getValue(), argPriority.getValue(), 1000, argSkipReply.isSet()); - // Connect the screen capturing to the proto processing - QObject::connect(&x11Wrapper, SIGNAL(sig_screenshot(const Image &)), &protoWrapper, SLOT(process(Image))); + // Connect the screen capturing to the proto processing + QObject::connect(&x11Wrapper, SIGNAL(sig_screenshot(const Image &)), &protoWrapper, SLOT(process(Image))); - // Start the capturing - x11Wrapper.start(); + // Start the capturing + x11Wrapper.start(); - // Start the application - app.exec(); - } - } - catch (const std::runtime_error & e) - { - // An error occured. Display error and quit - std::cerr << e.what() << std::endl; - return -1; - } + // Start the application + app.exec(); + } + } + catch (const std::runtime_error & e) + { + // An error occured. Display error and quit + std::cerr << e.what() << std::endl; + return -1; + } - return 0; + return 0; } From c3e86a320a15602b7f373fd6ac9f53f84a7b47a8 Mon Sep 17 00:00:00 2001 From: wayland Date: Mon, 1 Dec 2014 10:03:17 +0100 Subject: [PATCH 39/59] Added APA102 Spec sheet Former-commit-id: 0076d6090a649f0183d2295e7716301edfb35449 --- doc/datasheets/APA102_LED.pdf | Bin 0 -> 282134 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/datasheets/APA102_LED.pdf diff --git a/doc/datasheets/APA102_LED.pdf b/doc/datasheets/APA102_LED.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2a45763169a8ae37e74cd36b31a8da2916c7f69b GIT binary patch literal 282134 zcmeFZ1yoy4*Dri1T8b2kTX1NRprvS_6feb#Ln#i$U4s-TZUtJL7HQE^+}(@2OK@vK zkrWAE`hV{GKF{;Ncim6#T6cZxO)@#jIg^>$GBbPs_C8@#e!T+zc6o{`wiCz>|L*1qJ`xQS={2A>n@3sl$(2*a0$tME6Dgnh@V%~7_4dO=H%gO{zq7k(bqH?(Xw!KNBfz@_7A+D_|vuKfUYPTDaRV^79LbfE6umt!>;H1qDUW4Ws#^I)A6N-J zI#*|>H)x?Uy1RNmRFJoFfafB1N{VU7XeuS2MZeq8w&>q z8wVE`2M_-~KK|Xi_+&)H1otV)sHiB(C@5&?*+4Y3EOZnUjNFVY>>LjtJ){Ql@bPf+ zv2i}+{L=^qE-o%U9{vM-{0E#g6f~Uww;#9d00|z329`A@1~YJn1Ot-< zLM9NLn1{#mw6cR-a~#ep_}Vq(EE-R?8~QdZJmOtsR8sQ$l+?5jAJad5&Mzn|`tr5-dsTH! zZC!msV`o=)Pj6rUz~IE>)bz~k-2B4&25fU{duMlVA93>Q^z8fs`TOz@T^Io7KgdGA z|AVl<(M5u$>kbwcCKm1=x-jl|p$jGn7B&+<4ylX=u9?dNW`STlvZsl8l^u6k1U2F0 zuU*ISDOiQp*${t7`-`&wYlMaTKcehE3Hv8q3jhHo23mNSBme}sER5pJz4Kq?zj*M! zyAFi*ki|{}5ln11Kj?HucJ;P0BfLXOfwp-vXPJG!9hftL}ecS@h7*t28pm1Mx+#2gl&Dqqx4MKI)SVdmOJmkB4^3F|0 zY^#;R@TBm(zf4R0>gui=D47-6CBmL05(;r4Uoa@b*=D`LMaewe%#(&8rT6I4A zEWC9K;MRcdBeQDM$n8Z0Sz zP;fs#ul2Q^Yv(00u1fL&l>vV3qWMc)gZ|VU(bs~oRiymc!D5z|Ttltk!oJ$w z&CnRQ=&2CK;b}%hnWeX(PJn&aW#DJY(puN0rxk3?#X6rI_hoiM)8VTW*K)IEfvjr9 z^TzdX$CR%!1;+BZ4vBLjAC?6>MgxbWLC>sXChk0nD-%@6TN%Y%PhDQ7@?=uNsm}@W z9r?UL99~NkSLMOBe3dysNMaX3}d(Y;|^KPmMO?1)>OF?K4m~r@Hk5tRYISi z{0u`}k7O*~2-F!QsU=jic_D?147R_gR(SoazP?eWfw{3Ne#mWC?a6hC+d4S52oad* zL-EzIwsGa7XiaOFFD|HCGLCqI5b$~xIyMn!KZwHf4%L~J;M%ayGB`KoTza_(4bb2YWE;1=g%B6!d)_dnFOyn5gjzrrNvet zvth>iA$V`UaR%zX7LGk!2gY|Twx-o;eZpBb4^4vyZcLRfCr(Yz?|;3-J?k&8jL z>-{`7agcbeW#f#lVOFhh7KRt`pOkmVdSW%H$xT`LJd_LK_;mJfgUh4M;{f=O&2F zj-n}(aNBSqxYnsvWw0%raNss?Y5J~?-|RQou$TQdfZwE|+-4^(D#5~E-wsHA zd*9^v=DxE_k5@WwSAZR?XGV$%vHC8&%B5&?ez6OP;+#VZjb2T5@(UdzY8C0XHsAd4 z3o>=vu)3b6+b}Ib-!dFo+&>LWIE7I=&Z`4@l0-_IB z^p`VtJP%PH$|HKxwd}L0r`_}4Kb@_ekY?pdD0m+OYcl|Lr0xZMQrT+5p*~laWbG)Jwc`kpF#S-4yPFyv{w0vr#d@4D)!(kb7yAB;Q=P5&w zZoK}r@T5nDV7Douzv-9VnTzs?*NZ%uPgp?-DUvd4I}0u4dJ}r{tm^&kB@gND`$nBO zK-OoY{5d3lm|!7zYHAaO!Zpml7*apu_B}Ei242FCTVUY3$Xh@e-WCfk^N5zzXT737 z)p+Y+LEQ!5)>UYX8t}Aj?jGL)hd7Y*nVoPHXg>#@x`@o^^x^ zi`md2{23Hds226hyX)xP>!f!5n~Wjx;C}Hs>%~G&%s8ap%SjltJ42-tduo&nfwONu zAmL{=(c)w4bKj zy;ah@wQ=dlizygKi8;K2c+$}cvi@Sl+XRa3o$j}IbD?QcmdE+4%2D^M4`1qIg#+Se zP}@4kgu5io%!W8UMel|vd#&EUFLRl&7`OIbsPb`0m~=58(2iLH_@s4?lGMFJ5k_*n zcH%y=tg0uRlCY5;ZJZ&Pk_hVRDtEK`vMM)Sf6muHzOq-WjQ%%*#8lr3iz}g9^C@W zy3uHD5%IbOKEW!4;Yp9<;ay!)f^{!f^A3`tLt0du3v~KUK(T(KU8YznYd6FOliSTI zsZ-#>W}?cJIeHyWm;}th+S_ow=v=MyNTrkVBGMf!0o!4W$A5ba%V_=lW__5j-vA%$UJ& ztEqMBdAMQO+knl5&sfPpl2_@fBI}#yP}~BzQd{h{w?)L!Q)V(J*ZIX9r~v>&(IaVl zyihn}<)wj4`xF$D2J(o7f^>oIeCOQb9rArCys;Vw{E-cMr*09N!|#gJZ0RJ_G3K<= zSU2_|6H=h-XA?qA#t=2cQT5Bz@pqrp4yVPn2{4k}jN^k(O(|@>MASCoN##nrcd-;x zLqI%L1F&*Az(=GDg4Y34dy8V9WMvl5D>YKFg?JaLHyQ5DT|Lk@VMVy3SjMkO%u7Ql z-?_(=$k;+31Z)gCgHYJo_HWH950AE^48Q!4d%OJ@TMg^gnmI%nm_jChLK^mp)n=_Pn_Su(2k4ZSnk=P! z#C0u;t^Y0NvC$x@(g`8)2Nf-udi(W`mMkvhrIem=y8b8@kbZ5*5wBA|y8#Y@u0L#? ztT7hUNK-=KWGdA($9fSk6b~(VT#uHSts-SNcxzr?dRD*)rD*TI?-SS8J4%(4AW5D? z@0Xm&)$Sa4THtWn<@(#F7oNo?e6Hj(9xwZ9wKu*=33Rde--QRXzx1=W`kuNjy`QZ? zhho(&us6X7frcd^!HOA&wWz0Ebl-VT_|}K+^Cu~CAj;ldgKb^byd^|RI2&szY;AdQ zL>$ybh-DKK_oQh(b@`SJWL+Hh;mRS3%W4Gq79>dB_-jXxCS(|VQmy*U{=Ki!F<>@k z-KX7GW@GodlKm?C zY~~b+N*87aMeQ@ZmU`H(+SgnHTIB17n*vyfyFOEL*I`_Y02KvesL@ig3j9dIbj?9# z@rqJo(uwy}KNH>%c_Eu)x(`{X(1harSS5drfG$i9s!GfUpcmK35iiHly(lRzB++7D zD=lg?Nyf?NK_pMP=kdv6Jxe`Hh*b7_i~%J6PwASQ>}pHIo39MqgexK9`pf4^kNS|^UA;fbRfO`1}Q6j@@@HY5C# z0NLTNsy{yXz|D~_nI$GP-Xtr+B$RwFrN>_k4!LHovh%vQj9DnJ`C9MSG@#CV=&qwN zMv!yDRrCLmZ~yQv=GQwx6j9+5+xPmP%_Pf7JWF7BOpqTUL(TA(1gV`LJQXZ`Pfpg} z!^z7rd107r48q0whT3pfP9<@l6T>FI|NJw{0=j|!0$lzRaQZHxmfu=a6cFijeE8av z!Ob9{d~X@koGV9T*nWcHJw~TLQ6v-6Jim{slV#~q%no3rh;4nXfZ%Cash|w~H0^NO zC3e??Yk)&9=J5{lev*{eUY|^|F>mntKs6YbH?qi%jY9^U=4poYSt925+zTQR^9s8b zpY;}+OH+u(MD@YYr(B`Qh0j&5%)erd;U*)+yLD$kc-MD{!;iVMPqxn`rJO`Aq8W}h z!+}kbjy|IRd*6kN-U}(lnQN)^kq_VZ#^v1Ww!4oL6lvcCnjB)he^bXnRc6exKAoMm zehU!Urd~WMQfyI~`N)XxXe>+o0dtlB$7h!-a8%mRiOim6bm5-VXD2Kio)35|t0QhB zUa0pN9dq7o-PugZ9dO%Zy}5x>+p49de)z{{4z!F*(TUkbA%0gnqW&5%#b&OG17c`F z@cQD{vaJ|{2j}Y#?&xK$HVr253HD2zmv?oUV8_cgz)wp8e?iUXhAnctDThe~YGI>P zi<8;ad)LHtt&|A)7^h6$1{cXLl`Vp+2ZeOPt2N3|x5cIvUDfS?@3`0tkp zh0H_shO_n_G7KIv4QcHED*xaCdHnjava!6ev6Z%lt?&6jHG}pex*X_-nI3>lLk=K& zDSTeUQx_$aB}z^plCY$BD6>QmXu^LB1fILf7ERSHaF4KUR}~SDx|enfMCTIi zN3E-M%LB=4KdFMml8eA-*G{{-BAUXtnON%MdOZ1ZXPmi!?5one2#u_pFPP)I<&-E2 zTIg5?40KKTr?$d1_v2e2Pox)uQOFwpN?0DcRgFjQ}WzAu=CpBi#ldLh4V0rbjyeJA4 z=G{Lb5LEGHKf>l^lu`TS>2|Gj8h#f$;og~{qBCNd7xwwZwBLE?0?0{7PE z+QFjdV>qa4nv6{m9q8$qHlXPsy@5N2T5`KvDot zDT?X0U(am~cR=phXOmwynv1DTsAm*M@J}n`#w%t};})Z%adQ0W+M1Z@2LC?8a}lbf z;4itj8mcR8E1)de*{jYVdN~o_TfkeR*>?={OW{w3Y9~_-X`)XVEde$wt<}K(!=~Ap z*n@*Vry2rmy@(9}gNGu-2I$wnerTF~GAiU`Y5;hUp%Gs8v$MZCY3Th&8=Rkl@c#!z z;s7?^n!Mg~mim$4yNd4+L0pt#6jPel8+Tgun5${0`889+Yq7d-Q%os@`EO-e^6uzm z-X(vZ{duR4A!WgP67}rH3)ReV3yeFM_^mISc2C9=+ep!6sMN(lh!;(o(-NA)B7gHe z{&q@+G49QRQl{*NTwbAH2p z&YIXOel|>#b+p@$x_QM)N^b!56zr&-9G5dsl`!XX%z|+ctO3$oth0TpiB*x(MD6iuXYxl^Fkmgzx!9kQoj*JOIWPzL^a%mg zf1eNkFTT4;;e+-VSZ{%E)oqu&VtXZ7@UZ4FwGB)mHL5hNq-RU@O9d=IyhcZP)L$wm z8f%VHxCIEIvo$|aBpMC8g{-FUr#xSDNKjG43HPz8~C(ll3zMA6}elzdc+J3PwDK<71F_)QqSb; zC~+BydtxTno-F+N(>`I*a>?<{BIv`*04BNsyKb)rTd#A!hFKLNG+YYjTx)8 zn+f|$E$Kcqu=}6;x%{3{X8Ef92U3^ji;MEgMqMsw-4QQkIZUqonYz&qYk)1=QvMtH z`LjSnW4*Bpoj1k7r*n`L6(&2saLH4jcNx`E z4bIQ~m7t3oUqIkR-;U6BSD9(_G0(tAcwK!T17oy|mFcxnoT_ModD#=fPYILvu~}Nc zjB&PJ__D(Q%R|T0Q)~qT^>&R!X?#}&GWmQdMOJk>%oj}gvRjEcW$Z$7G`*8md@P8f z@A5~%*ZZI5jc~b*2(HGst(J2{sBZzgq@|)ti{dzPC5XYhka7W+BhAId0b0u?lC!Vu z@01IIsmYDH=Ss~y9p*Pu6h@k}t4r!ROx_pY0%C}jU>daQV*a&qSxn%4@}ax_au1YS zG(l&c4xCgF_`eyKP=$%B5${om99plUlKJ$e95h$GShKUI_dgeSi@YN-zbS5(Vv$pS zbGMxeb=Q7HK^fSTVn8bK$5}+_;NGnlHdMEvl4dc>UYO9w!xwd0v8Fa zGvXbRzH6)+zzcG}l@Ek%m(=02o~69eFHq{W-(mLt`*CLypPI={tj7v~P#ujr9r(#x zEq+7Q2TrXtArJ}$j_HU*`_@fJ5OKE05NoG9L+*Mb6N|?@Q>YDT$aCF^M$_w12gpGZ zlu4R;<%vT!!EENKvD8}5)%kSCwU}nMz=ZZ00|^?991m}5W4HzSV>iH8xJI|YZecm7 z9dshT2R(mi0Nw49rj9JILop#g!eZH1+${5yTX|!K$bChT35Nh8d{!^A1pY8 zm1me^XT2p^TxbtwiNqn7KIDpn%R|=9{AF)}iYCYnYg|*~KeoBtOkI8$G3BDScZV&3 zz&_3NDn)ugn}7;x9JNn$=uL*v=I7&J?!YsAh%j927LoY=O-Ey2X`|i^R&wMkAJLvJ zlpbXbTF`yFIA772B%*W+V1Q!Tv(`#lJokCjHn8@feB$d98ytt8D<4gN+pHdmFoZOX zZ^D+!%L|q^mQ!=@)PCvw;Oto6vqV*hddT>~C8Y1%^5rei8iadRss-P2zXftS(@|BQo?KPlMNK*(dw742MY+@> zql^5<_Qi0%ha!0y{dAp>q^U+IvMvO^1M3kYE#i!x92e&ZccDySM)5=!g@p|WpZ%mn z+CY00^5x2G4`yxw1uW&y(?@eVsNd$Nuy292!OT`qWNOkM9 z2x?YNm%^GR9oL#_17GSHiREf5E|ETkJv*j%0vC!r>Ry6Wf&xpPPs;7O)? zFW0;AWJo7M8c|9zZTjzqd2x~5`-rmff$&MDRhl!#VHqfuv~AE)vm9f;_>j~f3Pt4C5W${*w!>6uCeZA ziqvlv5e0laC7}lyvv;Vik#Q@m3WRwI8`yQ|*aPmzb3vl(OP`nrQZ*uv`tk|=bS7$A z9(Q%~8~ip%hVH}vj9 zg*4)#?pdM3&!_%!Xwjfonc1T>REnH2-<~vG-~6@NEg)lz4rX9iK@pb{@l?<;U1_wp zX0du5_zlH0FIVin(~<76IC?IMTCj|JxWaQ~{# z#3+Z*KH0n@noIbz>fh8TAfp#_Ne_&ENT8+y)z@4^{-n2cXL`{bjJRw`7E@uHPHvlvE;P<13K;)SdDNoi@pn!Q zAXTAdc4D3t>`j}3>_u*54s^pzw3e@Q6ZlGkD5rdNeg1n*`FACoRh0wWAXbR@8|**I zGmKWA{rXk}Ma~92!>CT833?1V02-ZUS&K75<40&5|&i1r&KYE8(ADRH!Tjs<-hZ9%OHhbO|H15#~ zMtr`a?anzBM&mC%DB;O*^i(|;iG=LhL6G3+Yg;V^#C;F&M#vzM@=XYTBHCB7R@sbRnPoN5Q`_GZrm|Ln{Egta2`fMw zAuhA3|K7X}suz8Y5bYCSBcQSFXR#af9_~(+;JM=ptL6_KWRE+%DDXxCe__Oa@f*X@ zd7Ivw1G^^)QJ)Uab8@p4ntMDz@=`vANVj5DcvhFt{_E(MYuO3Zc!8kj|Z4ODZI9pMPsUPCqhN%WWY$v5NO;rtkQx`UPCFn^=X^z91r-Z^wcM6DdYt ze=Fs|Jgx>^6rU7)qb#fV=pC3+L>D5BCp?OX%3d(Pi$xmnb?N!Cyv3_Q#W!&KR`_-y zxNKSvb%uj?Rv}*Z;d$;w$)yKuZn^Y}BB&d;z~Gf{pd62OWur=yMGDpZt{THa3^_9O zKjn^ytWiP#9b2PczjAhr4c*IMK$AAqm+w8eoVde|Y5k!u)Tl4IW_t246Z4mOfqPP; zgBc69M*lzs7#Z#5*x$+V(%&5?a%8`Q%*_)&z1d6GD=D!mp(uO1g?PR8`x4TG>2*#iudbyWwIP| zkT2jcxbeFFXc%2LA(kAu1;}I{@UfZN^!R1o-QxBUH3yk?HfHy7xv@NZ)R@(Nxn0|N zN1C2kI6D6eWgU^E>YL5jd`;;-S6m^2biss&x@6s)1-R+5N~gKzR}vyT8FaJoo*!+D{SuGi?+_`j#K7S34*LiFEB zB+kNx-4vG6^WO!&lIZ|x=!@Ffq=p~($C12nTltXh^EvyBwd6x*5xOrMnQib)jUhG7 zl!hGNUk;A!WL$N%I&O%`H9KN!Hu^P!3pv=oKlU(=dbo%)6CH2%QKLyf9G^>O1(R#61c!sF_1 z_Rupn^ZARQf&JyHSt$HU2|*E?Kjg5HWyNCv`+-Bv{Pr0sU#>$trbOsyi-)#44rM%a zQyxKi6Iu>E)zT$iBmPZ)+IB@&201k=pWI;iZGwT|f{D8irS_FG+x=<*Yb1ANvyIo> ztuC`VA5kM%Smdsy5HJGPqwjmhJ;^1i=m4Flw=$v4ime<}l7jKHPh01*oUjw6_sONW zUz0`Z@hpjP=h$FAf)g9)Y8EU6F!Q0s@-Q)!vO?u&kj~F)k=Qp@3Ijw&Qkkr;;851o$wv= z@6{t1ETwUN%x4_%+`In98;fAVQa-ZO3+)DR2lp$pM%(zw@0XQpFR>U%rv0@KykJ)q zr+_<2y*6GT%w!7@%lXf)kcQj^?e>b)!xI*V-t~tdqkh2;>(FDXy zBP1#$r@45C+~`L2j!Jnx)z-zBF^AoG6#wc`&ns`*?wJ){_;h)dHNiJoXCtgM+VB~! z#klFV!~*e-Nsk!{9F>_`UB1GI>k0@2`?;yTUw?KY3>8B1GITN%NSVT204nE($Ouu`z(Y ztNvJh^m5ser+_ZBLu>y1Ir_?Gvq;H41J%qrye-O+X8YlZP?^pxu!UAhb2O-yo$G~S z`uPGrQw3dLb_Cz^E;JWSgUtJ8zKQP!}TDX6zAB|`{+v;x~Ykn#{dz!?ORT=C?eU)5MMbsK0JU@o<@9vAo*}r)= z>NslMIlsvy1aAQ|2lSf)o5$r!s9EWU^b-ofr%G`)`8N6SlAetTU6Q}Io4dNRIikCL z)gpw3MU-b_JMBD4t5gbA>FgpOQM}%A3BnMquplEg7grcBt@iT_?H@>8^XG^O5F` zjh>_6!%qIKqtnGn@*+7dTtWWoHe%b$H{15!7J?}!-Br&|8jCJuUMrfK_1J%5{Pp5f zGPJ8+_(2^5!=(4qxjtlW>$ID^(F_z@Uw|3v1wJgy?Iw?`q zB}ST)>OG^iJ)c@O_%^wq7PgbrWZISp>QWQE3ELmLTsuYFz3~YnF)`f6>q05YurU zVHGgp9`nMMNN%#)yT<|wEDTc^prOIw99Oz~anDZmVQ>9Hw!4gDx zj5Kp_&yYrjrmy>Hjmrf~v+y2T1v(`Hm*0Nh?&CZ$5|K?_m6#d5W?G>@o3U-6dG%-yXy8LY_bFYzlB`dQrQ?S@ERNzI9`R{27v)emajH^XUmg5P$5UtNPXEW_dU#M}!A0m69>-6JLI1U)a9*>l5=J_{GQt6~|MFy(A6q!Ea?~TKi57V%|L&h;oe-6r`!T~)?sKE(deJ! z`kRKAoF71b|25ZlBxGE$&>PRYaGOo$?X`qbBFOk=$tpbD+Y}WxOnD0cuh9Wo`v;n1 z?y(%bCS=Jf6Q$hwe$ippwRMU^#8v&y%E5`!^uvEkV!+rl>I=UGHs2teZu}Q-fju$Z z7rwgu>utYjB@Ax?)5qu>%oqRkr!amXxlmI6XxGHd7J6#rF{vx~ch>~ljRCzGBu00F z&PmXrO(hO=gd&~{jhgnX6JOz>BhUHCV;BLq02?t{<-P^1q9O?0&wi|;cSqTA=po1p zt&Cm9!vBmMy$x2;TVa_d%CO#Mq@6pW&b?PxH)gMKF~$RZ^({cP1$c(3)KMUPC>fW0(Et=0~g1b9<5xRiwUCOXhZl z_WHbq+HXC6syJa9Jty%V)iO5D=QA|qR(}QBI2&fn`tww$dY4=;Iq5i8Jq719}j^|DmlQoTR$WuAE02*dV_p;vck+|+W-?&qiR z>M;>ieF$BWGTYVp*`Ig;`@{xm_4gNkm^<0cXyW0P+usdL`E32r0yr;x>mfxoO<2$$ zF3(K)f7ce*`qg-9wjZu^A~$r+{-l&9kbC8 zlmF~X`^0S7^E!sO)sY-&Q8fx@7KcAjB^6tb()! z(w2ssCp zCH7O73inBgOA}Z2VDB3nwdf%;MN(4Yz`~Z3<*YK;BeLt9iDtv+-(u{CU6?IZ9$e0T zpHocw!dFhlP9&95_msu{W25@;=9=Gj&DfT-WC=(mpz$p#bEcl^k?q}M3F{Wl&&*L+ zJe-A527q@yNBRrSPa9;FB_uF2j%1x!{UFjJa$cB(TaU&=MELBcwFHVhzdNLbKSsxi zY;O|U`Qidh{fJY^NL4J~5rv~U)O*Y4OpywBDPYFF~=&z%rmGmM3;xdS{)q5~GjmS@2`UCfB z{^j)d{}F)sTliSCEfR9_;#%t#cvVtyLptr&(S>@*Ly2mBg?9SwwNYraVL%+Ab`w(G zcB-N?iVN*8_ynDe0bTOgU}nM5wq$e|;vTB?@D?~t5<4ylgDYIW2fu@!U7(k){KQ!g zX>6aRuju=;BD&*wS_3G%xTz}O!~R|I6{$s1Kc;7pO?vTND4MAn{qUC)IPbw1@92oT z*8+t)Lm7%zjNQOcq11plGrq-*?(9ve??oF~*gT2Fy z=HN)a{AkVxjBDXv6-Uw$D2m}e7O*`AH4MZHDZqSsEfKxpnYX_C8E*9rCksb9M11#t zPmpStJc{BcP@FE~3( zwBj{t7%9A9N)%tPa|>8XGbk~{Bf^@^SReZF&xS=+J!y)7SpUMq^mMtX>N7AU+ijy- ztE&_{+dO(;(ph^|zW(Ng;e~Z*pX8-(y|4$&OV)?BtVcM$B^2l@0| zj5Yg)rqvYrlPCIGYqqDRH1Yc&R>W|>*Mh)YAN6T2vD-~@P4u!tM|xP+=86RcX&!v2)mZ$d{z(r&?)e8A90q9r-k&s=PV`*ZXnmWRAL&oBA53m(0Px|K$aShCEaY~CbUyzqXpEH5L-JTu$NvaMrGXQ zg?52m`gg`-@6aP2uQ)K+gQ@q%tBho5lY2ZI&}`*S7z`et zO&FAy0fkZC?{jlt6=WVDi_(~i3CDUBYNXI^q zI7e@&%Ad+VxR)Dln2UMKzYOQ+j`p(B5ph@woTk9>v!2NmhLW12A+Z>joLfMarXi9Oc`o>*!_EaLuQy~Y(7tpa(uU?SjF_bLpv^e5Q_!#CdzdJpr*S? z?9UQg014~b{n>wSD$YTr`+@xU@VZ|1V=7CBrviAVEDpc}?;QCC1n1sXQsvzkF%!e{&2s-A7Qd=JzWB?~X30LQM!Ez}43n?q(KjtMcz=Re zV<%CF_ESIE#{}7(e%+ov=0*)bLLJzu+fA8%zP4!JeVo#BU!`QRtg&NlQE_I81W#q| zy<9fY5}5it_iigqYZmxz>8kRBV)_;Dde=$r-`yckeO72iZ=Uq{^rqM}jufohbM0CC z>3cZX564eSgnb8p#mss9!HJqa~c(_veTllfF8m}BK5eQyJIy zn&z?9)4K6H1+54W80+FJmzZzn-x#~-H-D8M&B0ilH}n=!h<-ljAorbo?y6UD$SwK$*^FH7ROzH; zQOKKD#^DljUZk=4n-zB_C5wAfw(HE_q@J7HudNFftbJ1PS&mxt35Lr|kk|s($A4Ee zNX?%(>eFs0l3xwQ9wcVBEunG?usFSi9&DrcB&Gbn>3CmD%d}&&<+|L|Y1p?IHD{fk ziJ|(N>=%4M58#EyFaK%%qCevnxG~V8E$XVCwzFSO=)HW;n19b`^xv&^bzHUohD_uB z-6A*od!`{k{=>TnB@f1f&dHnJux2;q*?9%SVo(LoR2>z} zE&9C-+78AhD@^x|0`iI>gCz&rkZ57wErQv~Zt--(5{{6)1pL&Xa6sLH^zP}`=6Q?} zSfUbvDvN_ge@T}rl(SMIIQ=oT5NX+d{)?ul%vw7)?4c#)QrO{Qph0g{N?ExV!`rnR zE;W;>*wk$HAj^y}vi!qx+K;oI-wlStIq2nV=Mw!K&HD+s-dE0)gNyVW>BRYetL2@R zy~|HpP#tt$N#W1o6E>GSGCcgQAGT0FZzVi#oBD0RFtUPPDi# zEor&GS6QuX1ry0V zgbCXy6z=Jk_}L^qr0!#`z|5}MF=3cW@7=rub?@6I-{n_iixFcW#IvYIW8NfVCe_61+KbOh93uAy+Aw9pKnUM3b*$maWi2lS!`*Z*vT_DIohzdkL0=6E}(Q) zL5Uy}aZ%!Hdd_Jcn%>*9w6mkJl_^LhPN&=8Pd(}IQ-Zg`yQx5{qk=GlL4Tmfa6 z2i2}O?OpP0VW+UKaK0Od>FG*6xz8M{#naq7{OzT}jrF*+M7U&0OU8XyOCa#Uvxqx~ z6p4$ejw(v4W7D~BIk7gQ@(S(l`6m&Wy`P@quo?ArnPY%mYDU@6e{sa-C*>rZ4!WEm zqFtQ%l#mw@9A5N0x4Za5KRRiXg*`}SO-m=G-m!Y4IKn0uTk?@bz}MOIAgy=(?FS24 zrC7;q%>{20diCN8LG0sEua_j}Tq zzBhKqTr_B7o+<1vpZcXp)U{Hfx2}AK_RO(dOTIUCugZgQZ7+_ApvN{9T=3aGFur~I z4>8BZtGne?+{9k@wG*;4LtJuv&-h%?cgvKFJ$8rFJ01CHX*_p+e4kv#lEZ_EXM)AHBQEeBfXM zM5eVx>*>_`Otn3a@;sX&@49}XC9M$`K}=k45I>5eQY}J?lLZx=#_}sN%$4FhGV01? z@|2#~M3sGF@o7y1>pWYB=WAW$yFNhI5TSxnYKwv#QoEq@^6xPRz}b|X_&gXs6C`S^ zrX4?Gs`na47lho8?``UI&OI-GpbxS~cCF7DrhK2|J&vdnSD?BPG(?HDidl!$9BesM zIGIUAK^I6WLFJs7IY>oFHMd7R>A;WlzKVp1p?3j~%b_$s?J?UaFje64p;kB& z8C2Tw0ilp```4Gp_h^+I%eUibz27${DK6?r?cbDN%Wsb z$ZeQLf0xazZ%gPQFD`2vZPZB6_RU0?#KVjOp;1z7@biq%1uHr0Re|ur@&<)$wTBmY zeIVZDE-7@ny=x%bn9xsxz_AtbiE4Wn>`btAaAqo7gO%jKS+30?#47%dov-LDJK{z7 zE2ZZAV8;z=R!44W$p*UFMyT1I=_x@a8k9pPDB+LMKeQnk^j?&oWPw|??iJ~CmFGCi z;*s6=uiYsyhJ&snX>f16Wzf=@sX&JG)TrWWPL6jwcsX!mj54V_G(y#M@6jl9^9ZU` zf2*5n<}<4aV|sd?oTMX-34xe!Vm3~@A4nQ>qE%%@JX+~}X`-S4mF`d*DR(Q$oTf@G;a1i4Jqffbh|V8`?L$Hui>eLqY<18>Y_MNQ_o& zPComjRZ*=xJJb*Pst5s!)e?*zI9IHWfZG|D}1AkY}6}{Ff>(3Xo%ok}SEtZn|jF>Lc=D5uHbe$~H zZN0ua)#zZ$#<(w{sU-2VH*$ZPAwGmwpiC`MJ zEP=n=YSK2Q^@`3(aJHo=9iBL3U{7*$@n4iP4`oUQOuwK5N2CsYN4+qkC|wGu*iW@o zSVgEgFJSLi$z5x=k;PVx!Iybl>-?s-u(Joqq`}Bj#U8UWC=rD6&MQ{;mRD-BXFv8A z!BDVx)20+RbdU!LzK5vZ6_?Sl$6d7oV|I7XAJF+aH(=P))BN5iMLFfkK6s~S=W8d4 zNY7aWN;OJstjdaloce!ACGKN{nED`@QStUk>EMco2FpP7f8T0f*i70uY2 zn|nx$mZ|F?Uly0uMP}>M$Z5v#a`suB!0WXyed-<#_Jh#ve5jQCy>z7O}Bjwx9|R)9bqAw`MLdsJ@e+i3b4W#7U$Ejr(8t>pz<%#yvTje7SW_R z87u;&S+!5v{`FTzqiIwZ6Cbm7~ z-@s)^8e3{0kG3{*M70a+X=6H`Ezj2P4Dej72{%vL85sK(k%L!rLpx(i zgQ0^YYrijmHyatL+B$C6)K}K0{et>|;+8K8M<;?ohqmZ-kq+=xLR;ON-$?8WuaQkv z4;OQq8W&9aZnhYurG8AS_mai=$6QS-49Bhcc^uDi!&a#$Fuueykhn@#70}L%+VMiB zWG2kVOF0Dg&+4L#V0-S%ri#lQ1YcJNxZ-@1ACHf4UR2%L|V>B7B!*!DAKo zt-4NfAsS>-(b$@Lc+Q1*g2V8Iv*o=;pEc~E1@Fn)rX{RVozi%33b4kfa-|fj=gasy z`3Q_X)@w;EVuAA;yd0B%z34@h&e_HV8-H-g>Y7LDJD!@XSyJ(x$8XrQdTD}%8P zNz*#D*y-UUn2T_7b8!!`ijYR@1jUVqe(|46?_D<|?p$xBQjPzmZdWiSHs*kpKfCIv z@FFL{FIXBgEoWp=V=x$=3E4G3^y+_n&0EYaVob7=HO=ioiZvjKu`TPF4*Cz-0^627Wck?OUceoGhXfbLBOYF(S)vt_91G5CYPZLzp`;PV+oc{F^~U`uIU=`+LPNuBI_ zk0^|NX_{Ne*y}%_4|eBO$#hmmIq&prS6kFK&&Jfc=km0U&BBia%b!3}yY~9vIG(qa zkGYT^O_~YU($Xq9^r2Pq?w6)dkxJuS0iqAmL#imVfIw>Fvka!D(}-(-h0vT0Ix1#rBMs?q+ zxh{zUOQ#Qy2|F_1<7jc$kgUqZ*zb*4-uHR4VGN)>MBPmX9vFjjBpq!$={Nao>BnT8 z^y8^

}%$@6!9%4@?xj`8!cw;cvYil3w|d_3xT+Ht%8%aLaf5M%rWd}u7Y<{_?@wb>+G@UQ7-INv?hbIf&@L&F29JXCctcs) zHomwz;Ipb_6cEJcI^BlNHWef4lLmc2OFQ@We?Z>(#LYxm)*PRVAc(kEBG&`wWm76z z-=4V=Xq)@?yTIP zzA<%QDMR(!T}$y>f6IsySN>GJ2FYj zcv9nzEqBT~0`lt+s7?H_hZ|^%ALO-OtS!)oH`(~~etD}a6K(-D+*eoGbjIR-$9&s= zJtbR-7FFWX5T%zCpwaNrQojP%=FVaIh0}POKFbZfB^hqz`c+qNX}xhrs`mR*siW(x zq5h0zs^tlXw58BKgp)BxFb3*cmGKH;?*hF*shTZ z8;pWgJjGC~$X+iC3u`Lm$mr$Pv$2)I5sO`Y)Z~{-NvPP8+bJg_I=URC0Uby~Qcc~3 zFtKWx1w+V|`J1yY?m1~E>V;*7O8A*@v`uOm)tC;Ky3(dcYGZghSFE0o2ljHk~fHcp$4v8I>KzE{;bd=cJsd&f-UDe}8@x z;h6*ZjGz6j_;%)I_H)um>c3Q>H53no9|#41ikGy}4iuUlGOHEr=$k=P60h##Q5Rg% zOTF_t&TroU;~MRnQm3v+EQNFF+C9RS#>WQjYQ#}v5JX-^&ma)sKnow)Lj1ngKIFTy>KcPXWQ7VXZi(AH)MK^(1!cy#FF zg?=#JtD8Ff?xE5^16tUQ31WZV*CVw%GN{|!o8FwT4>WeTPdYK|f9$_|)M{jWtQ%Ui zV#~i`RN`(&{E%bzIRhw)uz^MzhA&zCbmM7v^+vkXs6TF&P3i8@w&81cY>PK>1DohL zzvbPcyhbw4W7ekTAB1YXcX=c@tUJ*(E`PX9#^CFDnF=zTgE73@IJ zHS`qjP`E~b2SfV?jAqnr|qCV7WeZo@p;dktaa=i z1}TsV{K{wL_m!fw33U1gL;;hrEWeYgt^Op!37=kIh!KPpJ*a5`v3Ejk)eKE=H}cAg zohX$B(}bq7mD6*}l(1=vf75-YI}wBspt{O|cfXUlF=#DQVgOKo1l#3&=ipNjBfXa! zU8H2;Hjb_cq--bl&9J06Rv@!;CV>`RxZYD12A4*}VOmrSm>EH4rsrYnm}ERCZW?dsVCsDrt&C>X9e~TbFNI`+mc#5Rcf4RBTW+2P7D}y zyrN~=-2gOnD3vL+gK=&x!MuQX63*rK10QHgV-%gm#<3?%#qWQ7?>6O{_H=*};O15( z587oMWWA$tMW-g{Yf`5RFc0erB0s(;CHrz2ThhCe%Lz_mFe&Qitl7Nlt_^61--CN@ zE*c9RQp3Vr5V;MO5{sNK^rtrxMqNe53|q?`Oqr3ZVP{4nXQE7%lMF(~%GOQ{$XW&R z6aJ4R4`K_Y2m-WX;R0em#f85wu|5F2J_S8(MS_}|2{n+Km12vn|PFC1Etw9!BImGdChwQu|@7%c;7nqTg< z{M6fN^{2aIToC}CISZn_AYLO}-91516O_UEfVc_pC^s7+6RZmRt-mCd04M!kg?%y7NCa1-|eEvYGEN-5ydb! z#ur8}_=2S%Qnp%a1@Z5mH6(8k%2sS>fb#inyPv4c_Zh@ibO3|Pyu>5Ih0Z%s$OiIh zbk=t|7#*z5GIYAfLY*Vl5gm;23VAls9??Q}qV#DJPcz9p?I5mnlj!gaa5uymHLZR) z@KTczM0+ukl>2k*zGzgNs>%zt{LP=x&hj|_`+c-f3Wtki%gDCMsi+;z)%Ea>cmug) z1#i?NQ<-hcJ9TZ2Hj+;SSGP{~*s(OSg%&%z_26_LCGlDO^z~+cqs^8i|L?qcQos`0}+f?KimhGTh%HP_Z zFn<>Fz*0g#qM{`z(kt`$uv_u_dQg0}xUXXwhn8y67j#B%?HWY|&+3kbhHu{#eoZlKdv5SOXaDlKtUc0)or_B*@NpV5^XkM}# zGNpS6X16a3Kik0EXvSow)0W1Y;*?-7(l=h&MbS@lah8VIEenQ^eFY722yeQL2^Kr8 zOLA@FY=>&hT78nok7+9H{9C(qI;$6xHNIv>sPvt~A#YAP+&U$Q$MUQo zzO_z4UXuvH=4jeQz!jGApvKnFfH6}1TLYM-gPi62ivR{i+G6nG)c_{;M$F8wLAZ%#5QT~TyR*p6S=t%vXYPb%jqAR1$Fm_jqIRvPBxiX+nN3`y2(O)xxO`TpSS zi(=vKWh`$L7hG3lsA518mu@sz(5ZzvZ&^CQXwR^xqG71XeyXbC91|?a{U86h5+F%- zapZD5EPcxLuHlq3UjCH#wnF+E3$*5cdSp@yM5~TJ=|jg30sBhm-#b@%N}U|LT#%3jgIFoaurY z?$xalo|Pk&|Z_KM4VWLlIFV4b2#U$pru*6Ar zyir(%7D-BeUtb0z-M8rSa0B`(my~tO}N@FLe7^1fP)Kv=CX>$%ds8sa>4( zNjyVT{kob}JWS&k1;uN5wcs%%BYaNF-$t8JgbMq-i1m3Nn*4l9-{=9mi^0_F#Ug?? zttm#)Z|BF0a9!#G48f1qKZtYPzs050$)fF0M>OW{S&SkQx4B(d&_d9Qa|?^B)-oB- zi()qiMK^{*t253&FpeL%9GYzQ`EVV5xqVaAQ-|o6^c75B7k&DsF%@U=(sk|dye#z1 z)XGF$$Nh9@XavFt=eMI*rO-zK4{Bw~^}t&_l2DIF-zs%P7`<<%D|$aZXhUwN>Y{tD zztKKb%PS((a%c6|rkQ!_QK5MR*vM$7@zQ_q5b<{FW%WKECifJ@bam(|_X-@_T0A?P zImn>0fW62ej<+P~m^^TZZ+0|M%RHjoSNGd=!MT;&NN8Yqa922mz|RwyCa~iLzDg_qJ}+#lMx;t%DunTGEEwBQAz5 zR9_uUGeh$r84vj4_$WIw?uE`8DA z=_`}^OQO&(Bq!|Fz9DbCFegE7bFgy5fQxT5_Nmm(HS>><} zT4rYT(>61LYIW^$J`1h9W+58e<8_%KEYKbxEqmzP;zJhh9gQnQX7GEDXOAm97WbfwEGVkl~NGoat zqes;FZ-+_t|Lu*We%do}pIHb|HTj(+mfg*J7U9Jk4}FrjEEXnPf~LhIbIAU*pOOVh z@ZnsIPY)8=)~*KmliM zh02znH)(}n=4Wh_H76aAO2_^Kiq^nrqlXU6tbuPI0kq<X3#Y^O>?yFJ{E@H01& zbyA?x@lH0^r>K6`7j!7Q33OlXlPPdAbo2POa|n4yHZN$3-al|{uO4a1JTCuK;0m7f za#%RZmC8#Cxna56kGjgryOWx1K`U9doztzhe@^p-(Xe=EG3EUx_cxEIodSE<@y-Vn z<>GHjPvNGR4CnY2US7&>E;HLDS4NNr@auMiL;;4Cxrgq80GKI(;v>1^rA%~4w?Yk= zDGc5!^)L%{;1fx{1YR?^8#pU#>+XOO2zoGzK@$UXppY;2@x#H8&4&aenUtQAl0lS8 zParkd0wL5l0VVS%o3V#1lJ1H_QrW>`?XdPtk_&? z0NYi0o=o17Z3f32$H&8Os8>FHX3HxtrS?Ca_hTPl7H8lI{{fNJ467OS5Kz<7e*Qpd z_2h~3yVn#2lF2?V&pHKdHo%|aIfd1c_#xC}u*10L-`|ovNO{;99_Fe5H8a$Tk7y>g zW3P@y>P6m(uA5Qx!jG``lb(JtZR#ztt9ZrFg5V=K!v6!h_b_eEx|2RGYxHrk%X3K5 zd+HI@kFI>0!KWbUisunTHX{fnnjc}X6EplNvn|i(+8o9jmo~?+o{7gDb|yI-ju+}USHujW1Li0?pF{}2tT=l!kF>M0P{PRQGk?HIGZIwQSG5Gu)J0Odr zm@+8@2CzrSqVtD6QSX(%Jr6yqC7<(D=_h+lO5!n(X3X66!wW!OMbc2X7sPNr^ZGpU z(k%!AOw}P8*a7Tc{-@lD3N;CITO|*t!@X0Dy*_o5rjBVW+RI>?YylS-UTK@woz&t| zg*7z92!3XCXXhPo_rrj-LUaN}r7P+{t_&LBXS%ka>2QYG zvDuQn9c5YH(VM)4V8jRh-01TL`UhfZfEo3H58a(S9T9X>4}5-W{Rfn%siz{QfsG5t z)9yym*O0-=5=zycG{oUdIcsjXO(Z7Dv0h>oT?M(|{TAk5Hn3}Z7JuUP@WseX;?gN! zOk`^(y+1xXw0DKRaf(|2!L3vibE-dW(8D{l#Sl%1ej*A=&!K<&I9|Hw=3N%t-^qP; z*tDXmp{7Z1(Z9fD5tn6jfwT!@oJSJU3-S;)^a*u#W{RtcCF&(-?nJLXtgxb{>)L8n)nq5OY&D4M z;19fIp6`@VsAS5W7bUd`&D9!#v7*@)U&Wl7)MxlgO>L8JoOGq(6-aCci@9y!iI6?- zHhj>?Xs1UF^jIV&!)esJSDw|y&}gg7ZW4i0i1X9EsN;k$<7VVoZzvnHfz=YRJvlN+ z!Mt4I1nD`UdNSt`lD%APW1gXkC6OKaIr*P^80xoN`wl-*6Ji}wV?=@9t51c*So)c3 zL9T}OS9OUA}$H9kz{)Ga&u1@g~k9H%#}~fM&6k%KKCzGSmkmWe&xa+9>lJEB)9Se8 zU-7{YeYYlBWCM14r(SZpyp(n7jC#)_^oh}3`uto_wO`0js=8pWec{Htu0Azww$wA; zz~r;F{&x7cuP{CD0c4s@ox^zDnr4P{o0fFWbD3?}v_)(z+-0Ytj(^O&gf+%W9`o>xwe~1)Mxs*? zQ}50^!Phw4}-aZJ69A;Nuj60pHGA4=SFXf9S6Hu{}TAT0TJTLrriSWOX)ql!=Gyj>n(m*rr zkAQAk9EaV108TuU%KHUnmc8H;UWgp33n;MJWjq6!0b!BwNs-C!I>ttZcj& z9Na|*p;Rm6q>6?9;(2x&2hYIb8%$~tOU0=7tR^mL078Pu_@W*a>$PLVedsYzy5qt- z3<6oc5;JcJul{8d8Z*NeVXMgO7DOg_`jO37bKd}0awX6Gd5yx`*C31#lLvDj4xP_= zvEZC~R7SJSfS6SWv$B6e|4Xd)r5#Ii%r^ABfOalpDN9c>JsI|uTPCus3Cb1-RJC=p z&@o_4X2qkiCHlZVc<+HJ2QripKge`P4^zZ9nQ9(nrEF#LBni(y zV&w7SDrfr1y2UGDtW*iom^{Z4k+M0om9{lmQM7MP36V-Ev%k4NObKUX&eKkPml&zk zld!J?TPInMQLoh)R)w`*c8z2b%x{z@v3GFl8vUFk*W>7~4a8pG_pY5NeGY9Y$T{$9OAPpjhtg2MBJA)MCUV|UB|voXYMdY z94wXi$*Ed()#meJPsY6249-Og;@vT*Kl2Thf zDWA<`9ds5w&sRiM24$zPE-eC61o>a}|35y^`U3!#E8gs%4P5H^ZUu%LC|~9i(q92U zWb+XvQpS#B>7h)Yx3(1<-civSS_?j#WazB`};={xqdKrB&ZW1S?K zcrRUx`W0nx_Y&oqSO>B`%p55cDi{?{&V|7-JBNGuBmK3O%o+IG0MN5wlw|}-?w<2A zSXjg4fW6H+d&Id&W|me5WIv&t%&4(^*NRCMIshtKp-O8%byb{L{W!sZG33={E7j}DDJj0C-u#8dg&x>r z=Rr(W%A{L4cWD~GARUJd*2S#frArRpikUb2#+UC~tg|5n@L%<*I@@o4hoi+<^A0c) zSF+Ze(-T5QJk6fYjZD-Cj0!s>0o2{|2;@qCcB#i;3#Uim`Vp1tTiD0mq-6M$%L;)< zkMcSkm2$lYc1}oli3R~g%C%`$Q(tAY`t8zbm{b>(M&LM848__P8h#}AQ(wT9U%=G5 zZu?c*d0BFop+J*RCZjNM$;lx5tI^_MRWEqU@5!Hejd`MUx1WQA0*!kPaGTJ>-UczQB<6-xt&Q-A;815^o=?-WE4J4aQ%IM*C~%{x zO~Y+dwi0;+9<%?Q_h~a(a15Qq*LuVws`;vYt${N`e&$!qkHl!FyvI9LQH2zeY&+f2 zv`Dhs9AbBsiv7M-73pv zFKFABO_v>N=Fa^ku@xJqYgu0k)$O(}wvXd){%u1poZkv!H5>DAuv<4_Q&3Yk@j&z1 ze*jEGCfsX{7|S2M{hSip(hC+14Eh*=5V=J-w!98G#tCVEfaGL3SJ{z-9vml?Q%_r? zVzK_N5f-BbzKmUQ@j#f&fJpo9EP)e;3Eb@F)MeKs(XE{VyqOG1&b;<;*_EaN;_eW@ z?>Q%kk}W7_oGS^CTaTo{M}f)h>+QI5tQ{{-Mf;yYv#*|IW^juE6k(!y!f;&KL08-IqB<L3yDtuI z3>_HFyy8+vu?4e0hK5j6JvYMPp2MQty=mHp%Z6QwsT0aIhtjSOb!GZ|u^O*(nr8!i z^6V*JB!`zECb#mZ=`=F*orWCW5}Cj26pzSwO6^rvd@F9o{|&m%9yW^MrhagBX*oyL zq;ccj7SG`VZ+IE=P)97l*U_tES(=(FbDh&5h|a)l_u|L_9>{ih;Sbr;%C}vC%T5U2 za^(^og=*M(-z0ssKAt^1Qkb#6eUP~1qiM7#5HCAQ$YM<+MtSrE&-^fTM#9o|j(u2` z!(v4$$J=hsJ9RlN$a*Id@gp{W*O5WJOIDOIW&V)BZY?jf`=CgIQDK|;aK=-rc z*HQ1mM@7Dq0ZQO)TlG7#f7|A7!Lgn9O5Z6a0tbJyCzK|NIE!XUADL}hV*JN z*lp_)9NV6jw<#OCeU9|RU-U6|WMHOk_s;`)ebr-uK|eov6Md)mXyAIjKs}Z-rh%&Q zC$#Esut`o9y4DE3Dgx~mmf74g^t7J|Fr#o2{(vHK{_WZi{M3l;f4>R%|LXx7V*&kl zU~!%9<{EhMOxm&j`|$=^t7e=6Y}R$B&%N5C##~7s6!q2sNhFGj=>ufhZ1AdTxL zrrVc2>Ud?FO>+Xb13_Nb*^$F5%aFdQ{#q~gJd@Z6KKpI%tK@VVr-5i}w%KVYg)l)>k{nu|6yQZ86ka}e^$qIaA2n`OXp2!s_It17XWWbu$?l675+*&2dCKkV<|d^N7T%AbcM-~@bF+zrZkykV*%1)*u=2cSg1!NFQGV2(nqnpb7r6l^JY0hj$heY$} zq9q;oWsxX_<>RD1>xO$)q`;hN9pgzB&28!gkjv;&K=a=1XPln52|UhJuFrVd*sS5K z5hKYyQ-_S1*#<_3n%hEWu7Y&N2qDk|=ks0JR9&>z;@EHXi@7cr^Fg)q8J`-4XW3ml z)$eK?QC>=~@;n@9`oA%2=!tIXB?ib=Rn>mdjRGC0OS8{0zUVHb*eLbo?kRT4tzHWm z1&f6>(7Mp1T@L9!cA`=9_vuXGIH;+6|Jv%Z*H`_cerfB1cK2J$sZt-QM_`+fe3?_e z1dS~lOIwA#o$7wh4^xB`su?)&+{W5EA0o%K7@h~d@+I}j34?-ZBRe@X13V&Qt$y#) z+0{s74RF#Gf3Z8*%JH4?(9d#N?7YPUIrz%pV7sfC!&p(Uw4JM1soEyQDJw}h2_?Vg zd%vlWAJ{+hn7*+vZU@uJ3q_4%^r}A!QzX?-L(?r)8s(qOl;LOBnL9&>?Q#}q7~eip zoIB{Me5oaNKVfQ#Bpd{&i6<(d?uY2L93X$oQ>#R3KV3fgTpwN2=d~F-L(w1+<`9H> z-7FajVIGxe4Vd`OSfUJ5yeS44=sr5`y84P0@}5g1Tup9sC(9MZ%hKfZZ>W7nod;#- zCU5Il-S*`~tEaVGlIFfDednSnPby9F_XM<=`7|J~^!Ez$gakXlf=ubo;5^^na{r9$ z1S06zJdIU3Z$chLG|l8!!iUW;U?WnASu|t53xBkgycY-N-xywCC1T?+xs&Z z8j;%tYh#oza#Pbw6s#I+$#u!K--~&^Rm50n4L&PRZ6h`?hNx~_+~Nx|LQjZ;$00Se=7%e2Ym_Ky+f zK75}jR(=81Byzyw!ZK{lxd25t80)HO!*I^&kZe}TJZbe{g0Y&8=Rx4mk1s-TW2G39 zbedO{@MT4rba_{$J5+$QX7ARVYzD|b0KLWGE7b6}h4ya?MeO+I$NnNq*1 zM=I>0wyFHBno?4gso6qy21nIE&gvV#TYQe?dXt~XU8pHcPAYLB2G{9aF#T!$E;04A zvmOT+G-8JW`02hL743JtisRgRtk)-Lu}tB)U-gPH3Tt>^{tknC$Mh%n`;{mFtdF`5}?`vmGN&wMNao(?Dz-%{=?oUWe3{;vcc9dzl)VKc}P(v-rGjtB&VU~(tT;R zV9M~(yL7Tl3rLJ*crYW&_rx_YT8!4VW?UF%&iwNyTjbCY5f?88yil9P6I1hN2!k7! z7^By1k!r~0)Zeghafaqm$HTx$^>yak($Mo_S~_Hh!_}D!nGw{&p_NROipTg5sP=I> zIkd``baszSC^b4asebl&*=T?VsS+|1)*zvQS!4ka-FmtdnC>_^2girYTZ+wd=)lex zz0==^R>!ZK{j;NX4%`8{+%OQ+*}eUkj09N>xqWr*MSySD)J1UZlyB+_bTb8Q(LmSU zpp9IWcT&kH)Fu0l6}4kTM?Bgjk1v7UxYl`rnMP92CpKm{ z4RKW`BI8(qJ+b2NAv?ed1n?7ko&fpp#_h*p;cx4lI63O_iiUi`4F<2tqux$l?e{*V zrLUe`pSx^}u)H%jv_nndT3(pF_k$enXXiFxemZwG9VD<&t{tz$%~IDie?x0Un6F=Zu^uX0mK5sSl+~3JZ@4b-= zJ`9|audl*0#|?vP?%vOU4Zo`&kN$*zzw&aSd9LV0{E$;XWHCUKc8vEAC@B_kDew0r z7}U`VG*2Y~0!o=Muze{02$>J+qK__ zvKJ$<@?lOtc(}j&@cZf>?;JJmUg0ZC1`7_}X@Zfe5PwuRr3=t6{=LWbDq_*3VWxCZ zK}e&siouuR=Laufo2YWXRB#Y$Wv*=i=X80k3zp&XUiE!8GB4fqmDA{gi2}BHFnh7( zk*V1+*{w$7@wb@WlAzJ};aYRf?N5A-;-P549&rfMA6S0lA!%62Qdu@gJ?)B?=_97< zj9zYzwYa{XXAuL~6e2IcVxhXTf}9Krr;c3tP19bjFAaD1-F+C*>dk}6hdw9-`0Q#0 zsMIuv_Rv0^5ok!?InA5eWZ|z9{jwfgVXduwCq2dGR3gBss9fhCF2LT?cq{g}CeGJ| zswn1wbP%Hr79U9jv3XOSsoqqJ#LD9AaW6KB48x=8f%c$VKJt&UsBV$)BA})k(cRE& zsh+sIrmv{4Nf_oT?0r?%9j}TJGDW_hI$w`WN-Q!qGO~Lji0J7jpv=K|bjfEG@a;?@ zq1U7B;VdG6CVy;wjA;0RmE&E)!IV<2q6e<170WU%Xr6uUd9XMX*IURxI_EKOORx8-)&pQhKzB zQZ_t+?qY>dI0U61uYRz&0Wcvi7%b^7tpETUNV(Z!S-8AJM|G9mI}}YhzN`Br##@wr zNu_NqkpgH*nzJmsO74Qq#`@;3th{nM%(l&RD(MJT)bMsy)I54qqo&>o)BD)>AzAOK zkUY(cTlt!jsUyZlcS5VW*N`J=;uQ#SR~-zKJOU4V@R7;nHQ!IZsxLk%>c*GMW%eB(pUV@!EHin7OycnU1F|9gl4MF^`=hD_Db?O7B?&|=yKd?~w20eD=+<8*sJXGG!nffMGYmGtJ^gPFKx-gSDH+o2BketRsT{8Vn>PH-M5jWupYi|uPP`zXaQPfOr>x_S>SPh|$Du>7>Zt?ENk%Ng=-J)?Wd^Jt>5Z`gI}lhsb; zPNi$d1(5=`IA@EtjDb`dwV(0g6edYFKzKj--V@|dPRtzz2p8>D?s+$%iu3Go2^LoT z*?q9|tJ6zpnW&#Ai3|*G7Z?G?LFNx;-H~o?_i4v9oZo#T)0d6hUK=si1^WKLHKj2_ z`~iJVovqpM%8$=n5VAVsb3JXi9PJsqWom9(YvC+|=ym&Xwxw7@J(2PaDNJ>Vxp66x z{Jc1PJ@LxDKoqXaWBus*J{ee|0f^CXmVYeZx_TWT3Ah-m0PiHnDxe4-r(OA@pnxvZ zVbFdR@ehdn?~j(^9kUcIAe${fkI7t6&w;iO_7ouKw$r_O`ma}pCNQavg|+WonB6`2 z4%}w|NdIpfye@itd50PX7*b#r0BNF6+3wA1lmu6kRn@PzsXkw>pU}(mh+rmZh<#zf zX#Oj4|Bnwe0+sCm;9tjJ6A4t5tO6A!lTDVBjhT1=)?R?G-dxuMSR3&2X8dz^C%#Rm z8S)gaev1ph|Ll#j)W*yQ(EhP-@Bt?`Dl(?Y9>fkdnD88NUZ$Vr;B?7SJSF8~z$dLx zkd{1Fx^s|?0IfTD;xWiwg#&YvrCd-=;uHvva7p{C|C067c}UGND5zKQq$SLa!(H7&L*NILkQA5f3vn9#k>;Wt=l!ISL6-MB+@{1n z;?V=|KHSPRwsS$1eYNSDboUuSC5b|360yV1v3kSHSd1%tyyqf^wV28-6$Gz70IRHK z3u0QRT;Rn@T5?IeLU;LtT+X`kBeHL?)mBcykhSNFUU5r-*V=b zx>#~e9EWIkH9^CibU4kmrKEh7wqj?S2n&w$(b?EGFwk@JH(=?~c#``uP5s|lP~5z+ z`zGNu4p9Kf5__$@+gLdFV`9wV=0lQ@me_eY5t6pjqtImFh0F4LMV0e0iZ+2=omPjg zbMzJeGRQk{qSbg_#J~lxb zMWyL-4tt544Vnrv6k!t3M80aGlIltaOL)5(wX<*Z@_s^p7EPhj{i*2S*lcZ|_osdk zyc%nKkA*qos|I_F?oO(4ghnXI2as)rv7ruy#3Y7_2t{^xLm3|rn~@+p%iso9mT_o; zwye9dgtkBW(h0%m_!@w*4`tK$F20eA{^7%}BgH`UG~8T)dJ`(fnI^+V9bMXQI8vNy zCg$;>jhx}P8>dzwKbJ1eL|%()z|Kp<7tG7(|X*1C;xtJtWw@tPm3e!*HmB*0h3#| zThwE>V6oKmJr<1MrYr6B;Zg$m$sMDqIH~X}cQF?lUFG7?(EQ zd?8NKYg%t>peQ%X4whJin}+3I`K%Ru_C!!liW`ZPEIQhXZ(1cxXL{%fj9qi+{Q+H| z0jHUXG3@|H02=ARHJ3{t#r~v!lvQkWmGBC=vG2$AKDD;y#!5E|Z_ntfwYLi#TT&im z{I6oeYa1Xu0u&pXx*XTRFOGUwBr@MrPy(S=*mY8uxzH~odO8shbuht(Y21IQ-4soS z;raWs1UAJB-9C=@TJoa=g_1It)N`~MR7mxYgHj|jdfHQS`6jGBa$8n!@J8u(Nv2#FpE6ua_A3bB&W7hMidL$KT-7E-#O348y3b z5KJrD+FyGsXvY~PTSs@2`NORH^dZ2h{V1>V-WDT&ZKBr|$P*Mx@MR(qv<3E(^wWog zK{800E3+RWy*)R}9fvPw4wb+O$c|l+Q{2eCBh)9xo18|@$MB5qu(b> z`*W5k{aqe_%-z_ei`0pv`(%n&{Z%D{t}0dY&WFv$Wn{3d3^t3auS=zG`RTHfkyZ ziWXu*^WXZW9u=^$RM{sSxQ}hbW@lZx|$!-Vg3C2Bp zJ8$u0o)O=7MJwz`4$23)8N~in{{FW{ja;TER*=#>@kxxIUQtZiI0hMsS<{ce2NI99 zCZzGK9?M7;!dRy@FjX<_^@~78KQT(#WvI{^Nh=2iMwI2z+(>25@_@y!1OnekYBbO- zqJUdHjNnz32Qjhqa*}|)<@`T?ndg6)ztI2ro(DQBv+RbR2tn#mZB77WPiHTXWdC-& zdKaC+3A9Rlhu$-*fOdiMiGOVB+72KE)~o!@GyQ9gU&^uGUBFcWs%sK(Tr}+G|3HtL zFb<1xT$}b%$2C}amNe@mE5qpj`SF921O8Z7f^}&!z*e@IZM%9w&eJAUAA{GjV|In!oq0O8_Tx;51ak6gBjvv_8f*$&+M=!Z*OHE^k{ATqW#pW z{8iX5uXq<}PeyDs#7E;Q84ZGbyZ&mm)6lEk)X-2b4#zCdvt2eAEHG1Z5XPgRw`s^{ z#auhf?TO%pgd~UOblm7%c@-N-KR~Jnic#pjKpU|c%aVPq#}@eWG3?VJ_Ldc?XdHSH z7Y}=Uf?@_ojeDAZYVNxRt%-p<0u{R@1=P##bWWlJYYo<#e3EWbk(Re!)aRYx5+=cIU?R#rf0_-!596gJmID-$a3zXv0|Va!V(Y z8j7}uQObisF_2}T)@)(&GyGsZ`8o~*@Zy~@w2`yrIYS0>hjzi=YE*-#8WbcZc%jsr z?Oaul=4y<7|QY1;=-_2OJ&m+LZzlmW9U!AFhA?{F#CG;qn)ozcVSu9_o`! zu+4xIprA!FJ;_9vkbl1#7aVBVGndmj`MvH?Cc`vbHg0*3paR;ukS5qy2(jbPyyq`C zkotltOALa10YoWPmzAb0MNLc$k{|?Jz&3%c2h4X{PZr314gAda$maz@JN#1OtDY}x zzk?>#lR5i}^c8;iMZ=$SJNZKk^gdbWhEX4OD-!uYkjf*usB)cjqg z2&`gZfXaX$_&D^*PEQRl5c~2dT|eUT${Wq6^WmD#?_CK*GJN77;H`8tOZ%$|b?(3y zGka=m)XByV2ugR|X;;{fgT?v-*Pdq!e}Q?!59N@jGNcIZ{n4$1t`(ON{JWUul1~mg z&L7ei70z+JWgfTpnoG%?NWp?YRPrXWGjB0W2nhbk1?r#Op$NGj<$oPw5+XFV)}<)a zeu-|_rm?S%DX}8D)f5+KSoWq#Rctw<_~KP5d$Folp-ZsGiz!Z5&L0>j2a&f~T#RYHlQi8+;evDJcFx({&Z$9HXI4QFyCL_#f znFmXrd^O)(xLj6z)wT)o&UR}kKeuTTW^M6{;Qg#7)?PIE8W22cAQJ3pC`kYW#KiIGuhhe5%^5F}jU22o7b-LxR$`AI|u5!R@gh(&N36OJYO<{Z@-N``o#M zz1!k*EWMUl*2@Hwz1p(jz8VPV5Zy+ToY}+?+dNx125dxHOYcNXOvlp?aqq=i-a4#j zaBc(pTU8|1+qoFvE?=&BLDxgUR=*c=By2^`oqp<0A$}@*NKfV?GJ-NUB1C?{3!TyCl|tsG8gx?+|#-6wpq2 z&PSNb7$*-owwIeDpC%sC)K!4@OM#9Ky_^-2m(6~rBB07O{4X{w{n@w_r|G`e3*;uYo@T{68E#2y-AzV75E5siGFgrK^vB zVxK^3bpm;n&a#`_d|WEb&vbQGn-es3vhHEfZ44X>;JDD&5WG3gZo@tTc6(zNh~`C% z!@?f?PbX;KaV8u+6`{hODD{AUo_UE9fW_(OE66}B@UNa0Sk@_Gf&CJI|6H^#@M~Zt z0^}KF0L)DOKuHMES+u$S737TI8K&S_E}ZC~00t#^U%KE6)Zh!SrIbPs2OtcJz?r!xGa!dq-wk`myVCFy+fiLc#UPAe?UO~X~fs#_7 z!;xOWD`o(j04T(Gi{m8PtAE94{t9vj2_U|cczRC=dX?xAOBe)X!%B859K6$r3zhI2>f(W-++*7lCR znCRA8_(evDa1XGfrH1F(<~dvE z|Io8i>D)j$TX=?F5_|y#o^an3@CG$3v~yn%xHy9o1VSXUUU!BI;LosqKYc%E&A3WO zRJXJ$GTk8wZO#BX;I!}-^EAen)?K1+4XNRgUr{o+hVdHai90y6-7I|_8D}T ziyVU&+w|{BEk)=hdU_*G6^M8B$z0>iGkKM~jDauE4Rz_Dg|K>RG&3WlY(f09S1KnU zxqzrB05&c>zlYt<;gaIMK(09~;9aZ__3XcT?*1R$|9cFuy?=}2{J#_f4A_dDbC2Cd z0;FES@w-a!JUB``uaC7Vy|cXi-IR)=wbnXHZ?o#E#xX<8tN1Q@w4lTjB>l~%`X=mu zv#C~u)Xy8tb&wqRiZ$jzreX?iU(5Xn$Qylpo&^Br}<=sx5anj8&rQ2z%OPHpN5dQ|WyyJ6kd$KnRel zJXUT)uhpkc*s*hI(PE^acL@tV%OY@KrJ@4iJ1CIs`)(?}_98}!OTn=Cy=xHLsBcLi zwF8slJ8{SJTBx|=+~_Hc^s9Nf(W`*w(yyt4bwmAer{0sE6TX9_R`nZM6WgF^y1lp8 zlf5ylSj^N+&^4%xd+54i{^q2M&+g`%Na zbCtYd6m>~tW6Ax-q>;z-sygyfU|I^h4J7lz(F2GoJL>}wWKq!|+zJ35Us6iOmRV2;MDC+Cv0=$iHl zqAbWNaido9bWZdo7fFYic0SHQS8@6nT2f+9;>NV*>4GSWH((^TC+OCx--T!~rStNK zuXE>OWAk8=Adiq^Z5_xOCy|!>F5h0}f;8KRQqBsJ&q1$TBYZ5CuQTY#cJL3jGfD9D z1EeoDO@xY12Yz)`U!eh1klngdIXmMqz^h=faqW$$pV_kC1j+q0c$%JqmMimWi&F7Y z?7fK~kN)o?yZdcH9s?(d8ZF0lLgw`SSCG1@&b_#&{g;VGz%4=aG8X}ol3xsS{r=$P ztUBm7pqE_!P;6wPH4$eO!VHuCd-SXS?u>{Y7B57^e0SR~W$`E};mP(`rF0c(XlG|( z$ucc%OnJrfE|V~MLHK-NPj+@aXrKdiww~5v8P@(WoNyZhhp_pnVuAjOo?-1{s!EW2 zU93`)O~MX|5^1a}IAt#ID*L%Ad7`J?rDjgh2_*s3wt~vAiE#V;ZW7wh!U1EYULEs- zEN(b&YI2(;K2Eq?fO-~B`fRnrH*LkrimvytRPse_@MR*_5m@)?!QB$N$zZgryGbFR zneU8R>e`nGJmZ+weJ%Z1wb#Cl4a=QXb0r1N-zJLpPA1^pGgT5TwLbGK z_h|9y{L<6sm?KaC^~;!KU4xd^f)*A7`;!;*znk0toqEx(qTD@nP>58fJerGvN?uHm zg)DcdM;$-HyB}6kC87p(10*SQLCin%Bu7xA1Dsx_ZmP$D7itek1#gCY)$jfk`T2(GQMdEyHtgjQ16bE!6hb=p zH79{z6|g^fz5c=)5BoPPYJmm2nTa>7=zqmZ_Qu)>b}>0y{ajf>Took*xchidLMlKj zpu72!X_7}P+*F1kboq^N9Tong>r{^o1>`kdDJP#i3rrs&Rs;oy)x;RLhJZ6l7PEad1V!2Zwp7F{mg-n5!1={e;|w0#n?ef6@CXt(_Sk8Qz-w+^#t=|HO4Lo{`BE4 z$jQcLtKyhfZgMkDio79UL?(73$I<<&#;pK+$)DEZBj$r%mW{C~TECSYZa*5Mm?9IT zrU6`>1O#9-CWOmII`8cDLp@a2N3J?{(S>?`n*W+D6iTsIEP0R#hAeC1px#Elu8-~> zJD9QxWb(anCme(tkq-!hF#JIY5fC(?EHzl-#p7q@e?VH6Ru zbJqr1G7+yLKI2xNe0m$NZw$5)S4RK~-PN1PO@!K^EF*D0Q zZdi$#|EduXU=(wAmQZmvbT<8iBgRb3!uqEjQ1v$s{#0l%6SM!V9H@~swf*F5PRz>8 z#l2i4N|k=l%7wK|w!Vf;KmzyPR8SyI+GuG^>(k%VOPDNeWr zbRMn`rdv0|{Hj*z+O;1%?WkX|B=62&!Q5Y-Odp?iE<05xIvx*rwH!ps+x2OuQE&t^ z^nSNjmpTwB=Ox*GXt*7lNSu;jw}BxSkriLC6u{A~aVtrMKlxTWY|f3dQtIYmT(oi0 z+y9W-ewx~`qU~9GmEU5IJ+~V0dTq!-e0$XFd9$@q(fRCp^|*1_RI^G*)GqLJe7B#= zBjAX@ec^Ykt@XU+(@)17Jv6mesR@7cv3&%$f?h3g>R906nG@;7q#obAPI)BMet7!w z*;pv+$SD`8BJHlAjI5kpNZW~ORx-!LQ=ViU@5 zBZJOK!bQ2l(kocirnvVGoAbV{nyx11023Pd$K~<+B!h`lDfx4$|#Ou1+k#LsU4rS@& z-n|~`1x0Is)>vwy%@_YyhlBg6g3k_k=6bye*n2OoesKCJ${DM8L46{d^$#|8uwN!1 z-S}q$)XY2P?L``+Xu@yJ+GPT#X?UpvLfZ7i;k5Bw-*X&dw+k%w!O|8x{X%=pad=m? z%aX^ku3aF(ejWj<^ozHGB>0jnj4L=T&dHFt_*lmXnk4-fE^elDAa^*3l6`@wa*Dmr zQorC_QuHrb@f^9JutBbPY0to*k?)sbN!^VW0qM1J`ukNJ-*&KLnULX#slvkWUTcZccdU-+Cdg@Jlr^y1qGtNT}H> zqP(~58JjpgLEor&4+?r$NG+AJR{{DI|F$TVgys831fS0?(f+%|cCxP0pi7~b-c5B& z7%)FW6>*qEBD3Fdd*!lvfmuKf^D3`dCg-rrur1P5ei4SPmQzTJ?~TF5&Jjl>P%Wg0 zlv=j52rJakTeH_ny;n^N=dNRZJp@6fcBHIiOv^$-B7S(MeB<5}f*y`ZhSlv-=}-o5 z!xDpFfU5>K;-v+oU~Kz}D0mM?sVDaRuuLsmJ4zwR`s)HFJ8J#lCMZnxu1#4LcmpgB z>=zh9(uO=FsDluOAS${>6?ygEE9{EptIl5D;POxmkxV5-hs`?a+n*dDmpC)(yN)@U z$zqejUy*oVyh&qE zdZ9gR1jkB`9VP~BY_XX@C~adP5tf<8St5>Sfobikq>g{K*rUm~c?w$rTX%(_4G0++ zM!|a|0K>GDCZLQsw<|Cyd<=>KQ)T7ePkza~_-URUm+e+0*t~d{?A*~Oh2E`zvLj^& z>rPU++J$Q_B`s3LPC^L7Sj5I}@C>ye8`xC0Z^FHGX(S%&l6hr*oSth_c)|6vq47?I z0nP>eGC6OS)Pdnt0OkR<2d9u|$@v2lp4A*Iyvaw-DFMP2nG*w>#zj%ym1RqC^7_-A zQ=av$UmV2!KXjR(nHg>!l2cf(d713ao?y#s|xa)B@EMMHx@I>2xI+Wpm4=o@(< z-lubFb5m7ZG`Cr){jixYCC$U56W@7RR2+CQaiOLo%fxuqGbLH$V#I8!4)Kb*HGW|; zqu4CDj((CfluYdYNi7aaMooOE_kMg?ZWnF8VX7Hw>rM4G;I-RB_G}4lfCMVh=V^LAJ?7RiDtx`Y73-_ zL@ot{Q#*<97m@`nH)kN`m1i{=uy4(rh#oT$?ih#Fqc;lkLft4qLD{gz zUPwpgx2+@F=^1H_Gom=USH&`{#=3Tbw>hQn^};GQhM|$gEn>z?3R|qP>!Hlrd}>Y= z#a%jJSEj8d8!|vLymT}Dpl~3+%*eAg?#$b4UWpTK(+Y-m;owo1l7K-onu9csrzugS zBU>Hd0#zwi{bWRwbSH9R8jZioqbJV>{-aU)=qMhJB9(bn7RK%CIIg3pXkTjX_Ry28 zalPKKAC}AxX<{Bg~45OUEdmsSUADTQB=3>9qe@P;A6)tR-c-AR`seyI5^ zY4Y{az34>^rna9`rZXFT@Ve5paB<=}kqu_uqFS99FFf=4Frds|I`nC;Q#G^##nE(x z&q2ggUYoWc93LBet4tFXTPL6A7r0jQEZ< z4ZMV&jn=ZV)9l!&g=wW9F+7f*cc+6db&oF#eDk?_AdY+5YB5iS;V+;s3QtA3dYE9- z*STxwY$lVBFhpUuAFV8&uo@_1*1Wpo8T}AZTzdVC3714^cPAq6wu%OoSkTUV+hy=< z$!xH8heI7ssO$IF;p7pI`1QMBGc@0W{FZ~xU>``2BP$QJIO6ZZ1o0dr%CN~KTa3a! zJ9kMH!0k`=dPKe3{VdoWEnSvbYQ064io%mL^sfH|R5*a`ZuV4R8m1yS6>*NLq76+~ zCbj8_6Otr-i!v;d?magq2&^r-yI^d}0Xzt8(eLw)@@L`8;b8-K9ve_CP(x8iK50AB zqENK+uCWi%t%QyCpb`@-A{k)p5M}+p{oPpfuQ0lhL`;bukyOYl(L1_VJLMa)Rm}VX zAe7RZ@+gx$J56U~!?VN~Fb{@8n9#Eu%YHgaVvH(bYicz zW=IUA=}U(0r4w1GB8gAhEUZ1D{LAAgI8DC`N(g_CY@z%P(d;nWzz@DL*#~>uo(*zo z7Q`(G9+_SxBX}_yvC3mOA2;P`$QiixIV?a7=@{&XCRd^IdA7@S*6U`)-G1Csg3S3M z+Gd+A=xQ25cdR|KNT&x&n^GWty}H5pdeI7EqtfgP)ec7X>18J-bLg`O4q3R=mxu+4 zu1*o<>A>7%N!T)p8U*r?6asRb@CjmaR)SyLk*b*NglHa$vzaN0tn_@$92f;$&MP4UH5Q!3K1Mcb`iJcQgF9WeN2I>{vN< zA36o36^>Hw_f1I)qsR9MqSeZ!plcpH65Ygew{sJn&9BcZ&$kL;Usyj0SzqVn7e0_o zhUdhK|HdDpj1T0bqnzO$=Mn(N#hirMiaQf$+3L8- zF~!DLiw+HnyiGN>A6?`Jg<4qE59}7J*5a(U-vYvs?rmMP4rbUIoS!&Gjp77~58Ij_ zACu!`m@WR?Jl?_1kjec;RV*6mmTF>>4z3BDjZJ!#BFKCjn^H}zMc56@H%=0wMn!xF zT1S{!>>dMvir43#8g|xugg<#ZLsy=iUXYbhd$43^DhPIv^d1*{^XEA2DzIc;!H^M& zU;j((>P_AGPukU+mh=zR>P_EbViYlSGW}C5EF$_rOq^EL!p77|-qcOm&c@JIQH5T_ z&e}vyl~K$V&~YqmKQT&~nA$p9ID61bGJY_1G6r-yLtE!J0#4e*+<$Z>T}D+qHCu}} zorsw0Z`#v;Xnb!af9L6M3Kcsm^M9jH=_bk96)>TMp59{!xFTb?4+znUNwQfK;@^m7 zoFEgg7fK=$iy+qQ%V(Z2=G%JOx=7YpoX35)nAy^i_2kkl7tJZ* zcj8(1&1k9m3n_!&SP|hE4eWi`qsvXIk00)+XVw!^_Mp!vP%u9xpuY6Tb%ZoMP zP1Wh1ZAVyY*fN*zos2Bp{5oA7}un^4#?HmGf+9YzJ2&7w>g)}Pe~r+YY!P2ty~ z#4$T!lIgwcG~ha7J~f_=xzoU}(N*^>^y}R5z-is_=v5Xmwi+DBkjktz!>v6nh&y*cP76VUM1E>44KWR6*}xuL=5r>Z^Bo#WLG34wPAz0*}uyta44rZ`dur1tK*wkkHEgQWL( zyy=OLT=_1CoS`KBmIIEwp>indVW;>$SCkB)-wW7o1Q+aoDeiCL_jl3yV@&u@;w~!n z;qCsdg603O)B`vw9!}1tHd3}`cEl`ywq1Gr~@JD8Y=Ie>!&X!qv;$Z`P(D;GEL1>*kG4j>7@TMK4@ zBw&O9XaJTB09aW8GsYiyqr=Sh1{`nA+27z#JD@2CF(*J3Fn0j(rX0JsI({RJEx08gxM@J5A&?G65vu@kef|KX11jp@HIxQJP}|BQr{i5P%43|8hp z0Px2Ou*veL-P;Xdu6aALy#X^5z%(mB4QT#W$oU3uov^-*|1W~v#H@cY$i_tcHe~alOGC3v9sb{lykLz!Nad0A+Sy_E`R4umi+^@x4{C zy#ekSvU`HLPW6EOgQaOesM{KqQ!N2dNwmj6TO{@A?o7`aLW(o)z^S>?ge~SJ-fWNKt zEZm$NEdR|)AM6e5k3RS1)vKl@Mxf%MqqU`?rKO~`IQGi{w`cK}>WvPl0ldi+QDOAt zFG|A15US`(q@_xH36ioO90NLr(^p+Vi-q+XSncwsRz$VsG0?D%m8`i&!kQj?oG|)-^K2Ac26qB z2yxch)~u-)aoJ}|>ze0_*9W9si(IN)Y%Z9t8H-VO6)zMI73W#hva_eYX|uYS9tLhCt>wb?OVh`cT z21`dHP%`j=D%{nNn0V@##?&r#w2PW6z7A_Zx$MLq|@tuk4KjS}nyVsDZW0&gJ{8b9J7Q zUb~)y5k~WuYFg#1$%@123On!i7dw65q8FlDcYY=|MM-KndWx00qf_TwoyTjnTW^BK zo)s`oN);%OfcveIQrv5}vp_EE1x%)D?(FnW4WEkKJPfkJ)aWL17bb7BUSnReJV#zq zZW(98Tt_Gvlh~S?oT(cEwdkvgIHcSZ-MCllURSNg9k&F2d>NxzhhP|2(ut0F1j#(X z3y_2Eq2`o{?fU7PG54Dk&)f#_qmPy%&3!dwvmc6^hirE9_q9W+crAFYW#Lj{wW9v% z9I{08i8IAum_|4g6vCPX=2WGU&*aKh92o`9BK{&wQ!ai&{UxGn{x7vlWga5OQRPTw z&jsaLQx+R+0xTGOo9O5rg0sqf8H7#)`O;lPSELl1XvW0b=luqwI5EPVSXlnnOp98w zv<5Jz46=7m4|?}F;wfi0$vQSw0*%3L$|+|Zo@Glvn?0uLi;_)sr+>w}P<{lqS2jGp zKF5CBOqPityz7oXVoZ1ZavHJhy5f4pzwdKf#<)h)vV0`B;+1l-`T@3G+hjA2*Q z>x-Dx;%FDX$7nV8F*vv63|j%y?8MYWX-+5=j+v zM7MokBKke*75o@=QG$_33Z2vx$w+)5Hj(gap2v=}vZ^UJYV2iOFPe_93Izur7}lvE z==mbPpHeH^B+>$CE(_)q!kA{wLIL?hIiZa*?`eR;gah)0`HWc--%^rm?8lRV=o=&* zbZz^*UHf?3o{*$IG1BKeB}ded zT*zt+lB}+8&cea@eS5@B%y^7zR;=W2%9_IE4O-Tw(e|g0v2iC=CCkr&GKji7yeoPN zw-8wedWTraq=peI+hTcvC~S}`AO9)^Gt6Dsy9Ga>X668Ubr-tmq!5=9O-VF@;;b*Vw%BRVfUNmg zSU0Q>qz*Po)0pj9@ZW*aL6EgS&m;H^dqXBuOL`V=28pxE_z*8hD`VoYS1-duo-FN2 z5OdTJ>fv~h1_4bDLnbUT?Ps#5E3$1BZOy*=yLRbdreR8H1AW!uq9#xi=B4HvQl3a7 z?&&*BBGG6YQk|ZkG&RspAyACOzq(GlD|Oc>zsVh-FP5GzoE#mcPp8`D3~K$JQXIew ztE`$-`$SomNv>9yeLn;qS)x`lnr&(1t?Y6@Z|9mEVjm|>ad6@J0r~DdiB>m+U;mwc zss0wfB`Ah-$gU|wp7m$=osU&44bFCtqQBoAt1^1b7C9{;r)+#$T=~978|GNEvnt*8 z#jD~uEAx9+*1)*y#La}I{T#U!<28mh~(P zWNi|c9+o*8eX5U#8tv+RT94P3eCIfp%m}Q5Z1{iuxWikLufx&Z+(7%~c!D_|+)QZ8 z(3HJAz&~`ntGDMn{-sJM4R4z&HnIThCaHAuJ1Z?5KdaJ_ydo{SqR>bwxd1T~CiF+> zY=qyi_=sQapkGk9@N&UG$4HXEBu(Oz?JzUo$L7pbE@wy4^-wVi+o1iqguE3rI#w=FWD(#y9Du|EozOhy1%tmHmQR~+10 zs1~w&`s;~g?5`JA%Yc!7-I3R&K&57L_LO{7=#InrCXehy?YE5Y@v8za|extJlDm9 z>KY7PRxTph(mtD4kCz#CZj{dfL!kehCTv)1F4?cTG=yESq%~Dpv&L_~0%Lt^CFirK zNXA}K;yHAN$GLhRyk>a*Q*=Y)sJsk3lJ3ZJI>Mm(Iy-A&T>tv|v8Jt>^_>f3TmI54 zi;eB_C~q;c-1teQU?n*OIg{6LW|*PJ03<<{2RDNA_c9~<#KJu=Q2d$q)noF0E)no2 zF2dPR2q+)4x=Mm-Da6D-u`*Fl^JLVLiv@he$%*P5n#43%kFFdm-=KzRU0ztJc52rt zD_n5UtYKd~W;|}_IsOP!8Hl0D8D6bh-Of7!X9MKTc! zq6g&(Bj|_iL|)-4X3h$A%t8&JTw2&X(9U8Xec^6a7|}7{L4WewS>W)oJmdRt_dtPA zLbLlU$xuAssgSZ47XlYnN~IF>=lph^_z-Y96HX2V&nY1dQ4R1i!+37f01E%4IFT~n zbyPkv-W2Vie%J1U!U@#SgDn?4_nUG zv_A^*p*5qC{^dn~4W#<6XSol2_;G&h_p-SeH^%ldRl_K+dABjRp(^QT=u;Pda2|NB zvQL@D*_2;np{^0BmuHFg8z&ZJEDbw$;xhe0>u|5ft7i>@gOqBKA&<0m?&0f`{RQ`t z`Znqj=%VzpIdAIul9|TdPwcVs#kdMJ2nAwPkwD5^GZ(cSGGaveUahTn)nFZpG*4oP z;oX!yMpPfnk&qWr1M^Uur6VD8&Yt^s@>OV#J{ReHOcmZwAJGm>ZABy9qC`&PA0;o$ z(;OvqhLwbYO`0@pO=iU59}<38!48W=+J`wO*Pt3>q-UsNu%S7oW+unNqD+iK)9rD3 zAI62~w`eYvCp!aiXcs6KD!xa>4!PVCihv!;yCzImERxfokAX~h?*w-q7Te)>!2 zut(((C0iI?q>9=fi+iZ5zkHx|Bi0z3S`Bk3tmgKj|Gm~`v)``RwEC12p|NfEP`EcW)%ON0E z!XIkN=9u>EM}j+#qLx2kw^gqjgfgnwwD>N`*W>437-ZW~3GZLFu^q&8$Tm?TsWf=p z>hqW~naSOu1tvXiUTqc*4-Yyf621KTg=1JUf|c8$@;d%Z%bU1iua7BFIhIL1K z|L)3QSfRN+z+l~n()q=W*wpkLtK3JzcMWiO=@exRUvYZXVBEUf@9^^YhjW3&VH>9i9v$Jpqt6kXiN?JYI18l zB%da@7`{OwmYP@EKIcj5?RmTp?nS7hn$2^1Xvy?*{&hP=@V3-LRL{hLWM%=JHr!v|yaPKvt?ga2 z&bPqMdv*^VZ z{yvC^N)8i_WUen_uHR|P`{WFctEXVB7Li^-E7)=qwByD%haV1!!HywU zl)NiMfvCc)V|nn&%wK3hU{sV?sHu}>B_qEUIjVE7#6tz zf$63d5Poej$`d`>`8aK?(A(58wC{e0O!eDpd7-Y#N@X~Tli5jXY_oHCu3(h`6%&Si zMLWbYso@-r!m@p>p}&2-g`&QiyWWX>JrjiYB4qn&X8FXpk2-@56DYwx=z8ReHw|Bb z2qCd9Ovfurh`iGdE=*xduf1Mxd3{NLU}zGid_GM?W1t-r}?BLKv&v zo6s$HqZ=+=6!@6-7@PrQTmC@HE~|xYlQnOHf&qF;yo&z|a;{c|-3WTVN(-a3E}q25 z%ldM759Rx~;8GULyfE*^j~c(RlHiV$pODa4w)R@{em5aQ#kCI$)wsN#!X%!_r$5ya z3nkXqwnJ*FNxh9Yc6-B^+LwjF^eQ1cumUacoOK7dm0e*p{64cg&6k=@A2X!krPrf> z?3JBFPeA-UUx6d8HdpbyBL!%$Qlsp#LzE^ECUSc36Dj8Nvyz>B6iJj?cu0i>oHFzxstXEM5XO7yHrtc73xle;H6gze~ zvX&wdPC_K!AlS&gl{Pwu1vf~bjur~cnhi0FLjrezN&=yU{slXH@}-`E7qK*=n}Whd zc9CB0E5fI4%^n^+{vX!gY|x;BGQJ!)9{237AMLYGeTyKdN504aTV5Qy8GE_`35)p7 zQIBF&J`bD9j@>cB$lP)@M~w}x^4`wS=Q-9^qvK_aD97{GeVMMamxnbcvgkNFDG+Ds zlb_$j?TIHGNBf0O>G=NZG4jVbvr#1JM9kj41rD)xsCJ_iyQOHCAhWc|2hGIAX<4cH z#gFNEIPg#>s>4a&3M;9Xy)9czJ6u9cqjwl0jeX1{Uc_c1Qx;@vG*_A1jk!g0YBliB zOC=Ht&P&x3l(E=2V{<8vTb$YYo#I-%%pUW&he7F}8HM)4WRs%%3#6oWH8?`7FOSUy zWC_%A(Uvm8JV1`yM4gEx>}Hu(uLq2#492%?Y^C2vA=$WkO>c##3_(J&QA=&_INsX{ zL!cR;nMAA*Rg7We)2Yj=XZH07mb>Mi;G>FLJqVZW2V&i>p{`Crqk>#R6MHK@s>)rV zReBvjmtf>1a8L@148Xo09rzjJqy=%0?u0d)qQ}O!Mlo`-_p#_%!vXtf=$cFviB{>o zY?B#2=Zd1ouqx4`!{9F70K?;W=!0@T50zdRx`tsX>NVnWV{im&8;|estO!3i+10_IapXN>vhxLTQ@RGmmrRovE>4_Rp!W>pZxaI$_LTsw0~VhNd5pH0<%6 zGVK*(a(^)$04X5y3$wRiw)dQmp9qDK3R>i_UT3&NY@y2#an^-7?~IkW^Wu++qg6!@ z*_ta(=B1ic?AYYW*O+&zBX{k76nkxmyHR9Dak!wi9%(qL@n-zuVzN3oR_M+M4{-V2 zSkY>HGU@HU#MET;fc5pIBULYAh!-|;Ni@OVB>|%c*RZNhty$(J{W-2nz-Y}*f+^yj ze04Us-!NM1DMD!0j$M>>Gv6#{2JgDN5K^)u!$dE9By^gh;KMkzd!{_7(>#Qw;~8OC zkc1V}&xT63o?w-AIRWksX9I)ZsUFme!5&b-B@lA!)T0q}n5=`3xu-B~K;+e$AqSbS zX}GdB^2gFyl3B9MpW{B4)X#!+d?Rh(?6>U%wd;;k`x+lQwMQK#-PKRG5LbyN+tsP>j zbM4{SXXSavMVwVkb-3x7k3SSvQ=yRVvcq<#Z+mxgl{w4Hft^is%S`3Cf-K*)+$8&P z#4tbw{YXtAtwqrDMg}MTBYF;s)AzB&S1>=FiH&v|L*ECs-4c>Gh{1^V*Iu7SM{koS z(aM9=y^{6uE}YjdcWY!SDUk+j*saZczL6RCulgW-oV=(?H(Kbi~Ghjs@$Q>?35Nd&DlP;NqK)*|d!m-@2| z2bbtFbG!~yZ=B8I?j8+2sJqcU>-K#29ZBJx{QR)OnL;iW-)dV3C3Vz?)#xn)mn$)d zln+g~;*pc*SIE@#8($bK9Sfl8ux_R15dJw2`j=fKDhTwo0wN zoV$&Cm2=aR_+8jV1lVobDP>+KqyU^RdOm!n4aD=6f5)(A5FMMXx#_o`@k}uAhUnu% z2@)wqau@A)*#6dJsH=2Y3Gy;Y#E6oVWQV~JN{QAkBVbg_ljCYxc)SG)!TXT>bjSp%V z^`i*ZR}{ghxn!i8`F(GAgqwNuM+)dd9n!)4-L5^b(?-knW9K@`ZMDEMilW6t`YvY} zta3(m|1U!2AR-%1;!M4$x~xx1a%oIejo*eBhvWU1m*FFtj7UI(X7$1 z)8Btb4XGPOi1%o=oK=y1$CnoraK1KqI#a>a5!GE|WkVa1d?G<_*RqPxf#FGeVRq%l zz=~&J?2G>OUi3v$OR{w)yEu+q93kCT*&Qzr&by$I(=sNQK}XIA=LeM}+=BcajAxWb zlviKlOb6a^qfhKh_I<;C#+h6z!557fW>!qWzNL~eyIZj_+CbX(P19C3R)JRey!Sx? zK~tQ)r%v-S1Tn5c_e+{}Be>{xI=Lemz6fAJPYN}5s1>CH zen$QkezSMsX_68Z1)(liU`7O0U1_-=RI-ya^HjPOiGyK8)XsQ9F!p!sM<&)W#Y^Gp zB~Tcc#jmM}O?n_xtnc)}iw+_3@UWQp@z=4{zYyXGR%z;*qT46i++jsWz53?VMP2s^ zu79TB_rjsQ@^kOj6Zk}{1w*L?(MS=XVxaUsTb>(INQQ0;9l^M~Lboez8o8t=%&kMu zuk>a8NIy`atD011ucmXm)~c9Z!}a>zm67)Iz(efewffN~L2ryDLqY+zTm25jSJUT% z70xD0rn{WG^lR}z-N-GuZx>MaPBD)={1=_WTib!6TNiMJ9!6O=|9Zz)kp8S&x9 z6$1N-A(f|bD5t1FJBc>AZ5GJK0_&(dq=F;+9Z)0iBE_n>6!Ga!Xr{$tK8a!0wtpEF z!ZhQ=q^M%f4cm~MzrTn>Epf^&(GdKhoIfp6D8CHnpK32G z>ysR+9*V!4XNaH>wgOut%LW1Ng&oErCr|%f)<_x`_iK!jvNfuYO!nHl-%r02@N)JQ z#GJ5ta>R$7vThC!h)#7K2d_zvl|8Ey_B%YRce>dxs~>i?shsZlR%@v4y`S=HYXzT8 z^o6~ixv%!=O<}=Sil6ZwSiCtM;LA`!0!~Nu#rb@&0ivYlG+uUwRd9kLb>u&^>Gad7(*W{Q5fV>g0*L%tK*WXGic zF-?S!O(9{|IrY9<_=8FIG%|iD&O20Lf2xV?yM~0V)GIOUR{9-vE>w7H;t$NObNlZYXPD2H@%1%d-hQuDjWJk%=l_uWGw(nksd#<+yyd zP2KxzS55N9CXdaGxh;G5NRhGy5y*^sVR2rL3A@!k%iv<#4w9*$a-uw8jAZvmVuwpW zO0f(Uw2?)2!i9_v=e{bx3R$ zWs8t7z=W9OyrqPBhdz!{f-$RU?hpdbkKPSu^-XiPF8sjDe6c)t-GvVXDpVzam>O}a=*?skt~ zHa6X6fVxKFnj5pHl|S_}-(58-QW3v9+|<(=6$X|B-E%xc$%CO^+e5%8`mC7!DSh7=SM^PXojFb1gFFVgCfcT|>I6ty8FF^~ZJ!a29_v>Zb zt;at2wVL*8*{RZcAIDDxE-Q15!8C+I+QT+vrr#F)vFd9ZZ>-p9?STKi>^z@w; zG3BF>C@*m_ED`Q(q(gC_9P>zRfmj|s<%35BPnBy;YstvB>FSrhp* z42^YOKev7_)AiO>)c!vJTtK70v}kSHqaIaJL9MFu)y3*+b(6YXeNyGrI|SdLZwFQZ zLRuSa1BT_iHxUoEHe9Yc+HbI~+K^L^3n*0d-~j^qh0)PUL$4~=@Gt&9apoq@V54s6 z1{nUSLpA)7(ln z;|FaCrzYo2t5eZymvlXDs*k<()R)G(Z~lB+{r17u_}&Bc3nnZeS9oQ2M5(WSXmiYE z>@WHw-d_>iqvaA>XQXP0N0B6jh}I;9iLjQf@u*XhT~34`7vjSr23);Xiyk^SU9O(A zz@kO73|fv^Op9@WEWx<$!wiR`rIQIbm^hX|#e|kvoYV9f)J@6}XyJKzPr6AZ@( z6Px3k6AyqL{EqmJ!~t+1VK>E1iL5DYmE747BaDn+zR%r`tKFL0)oy}7@IXP>0e40A z3VS0QsOivCT}T|D0_O>nFwm2l*%hb)&f%_tk@5E%J>Eki>wdQ%kHPhS5O*w`2-I2# zAt7iMZ=?fxoifpabP+`Glo6m+M_-hSL-5$8JqhXTs#VkCb#ZV(RGr*Q&pmqx%~-E? zBSo>w^Otl{Bt@1vdp&FHcNF5raCWNV*sc}jo+ zCacft7l1$sCbr-y_zM22r|PTv=X>V+=KB}W3tX4RZlG6KuC}gpt@5q%UmaT^t`XL| zHu^r}zbUpZertMr{!#TU=J((o$9v5mf)6YoSkE{zNM0_`-#wERGRN!|=0 za=DytH?Rm+(GrLS#egXd(`u8LHvDuZPBi=7aeQZPpXg7;6)~lajIY`6q)>ch{3h*M z5j4lu=H}^Q+#`x{H!uT6MBqv>hPyEnWyZ?Pm4 zH4gtf{t?!9)U>F^MSRsMn4qOOoeKCZW~P}!z?vs9gw5@F-P+ex((8CaueCrhRLW~4 zNEkm8+LMQQzdw||C5}aL?x42BIp{>rmtYQ~ATu8AB_RgpP3CbkRWxhn0W)O|=JF%x z(*23=kbtb_J9YcXkQxf&;~ol(g@R*&dDE6peWzxa28q*vm;>=ogQH?*i(6RgZtXXW zRrJ<;Kpk@YV~aFYHFUo1O8uk>>l2o&tpZo!Dmp46@x{8r9veTo->SzrPt+f37H8SV zdCGqLYY_568d5Sv{KA-yb(!sU>%>dK%k`e_tv900A$4jTd{p{dS31}R-zp~K+qPI@ zxdMER(Aa>%SGPG3w}QU5Y5+}DK?o%#j!3Mhg}U@rWzc9|mJzuT;A zHQ$|kDD$G}uz6qRVE+B&2U9JUHgh{uW_sfDXifhR)l%tFFY!P0YTiJp*bB+W`(1%j z?+9hmydDopP4GlJciL>x0;04+ONknxA{xeXYRk?7J1rIEXX+^it}3ZD3%=+0OgM99 zpnGKegw8DN-LShgFw7!2EV8gbA~`5V%QR0WQAW7ntA)vspDa#BlV>Iba#7GP3Xm5> zOmrg@t$9+VBAyDBAYT$osH7!QYBTwJBEH(JHDyZ8Bt(>NUfsOCnHp$5(Ttkc_IDXE z$HZH4sxnGKKu0kjm?+0;LuV_(sZsqhVM`=|p)#iG4zE!XG}?#(^=M+Eq%Is)8zJk) z<-?#CKevL{k9bU?VhIxmA<2fRvE2A|mUS>VL;`>?w$Wj~Zbl$n#n;x^>No7ay6>aZ z(P&d;f+1&&4J1PUY_$@0EG}I;$3qc%R@20H;)SWL9!IRhKR&d zO2zz%#o^1ujM$~L9XB}k-UA$=@;B2A?m?zMEoy_Rmo_@|9T zi9{(K%zd0FVPs02mneZ*;HP|Q6G5%I(`U3w0-f~B=jTWyj2wx?M2>_*Ga~kk90>=> zk#LY42?xoMaC|yP!iba|CK?Q;S!ah4{qYYO5)A8{a0-2#BhkNk+JcRlrb+cwYtZFV z!%cIWd3R4er6)7qVgqFiE-`tbvoL0A99h5jdO=1;wu;I6Z zU@~Tmlj&6;2UYC~tpFLfm_#Hbhy(BJk;i%3{XTYI|nai|-KdozM}7_Z;k=7n#2;+Ky7 zyxQ)$`)>H-0~^-Po>ZE|m~4VSnnw3f)5g|b8BnNX5{8R&(Y9sN3OkNn(UqIrkuXP` zyw}1P+xM(pM)pd;I@dm3=ZYQ%GKpjjB}*q{*k;=4MS={w{IKl5*8iM;#BcKX{oX(@ z=m#{6f*@9$ypE{d##y7bL=dZ87$V=Nb@*8(&H+}ydOe%-V_EC>(hSq&58_tP%dz+- z>7YO8_i{ANChRtVSjJ3ZUk>GFm1MlF;0VkEKfGOY#cdkCw!{W)K}o(ovHgaRajFhe z8vu1;l?l2a+}i-8=Y!AMtDgj)pM)4&@ivfOLqHq~#5rfl;)htXB1WL0VK__z(7g=pmICCZ+3U~qIxb-FI-%nkM8j;i2H>+Oh8-F7Z=qT^jyU@rSe8cInqJ%ZjkBx^meGb#4qww9GA zC&lTq%u2bA1Pe{EO6v+!Y=y;QV!?E#($kdoxGgg^ES+^&a7j!mftkBz?wxsTrfDYL z8wbyeJmRyVAZFh|!mFPQ9t|E3Qd)3(5Cz{&NZEW2f0WZd${o!e&rw=#dk*E^1$?L2 zi8`~B`*bK1l}bzcPWGW)eS7}oam$af-`20po{h=2y<2EeQbWv;Xq|q`T03X1?w3jES7{q2Yx(yPrvdt)u!v?-#sP21C~UnBnU+76X_+ zy??HD+KubD*89BG=<|)wg~s=AP5LibKlV`R=Jz*r{{Mo3@SjFb%RlDReZT*X zd5hL31|Aw%dQ&dlTaT8*E{_^j7v!A&zIr5`<2{9NQ=-t0KZ`Yz>^59H<>E^h4J_RD zNd30!OITje>F`qc;Kr$mYPD`z9#Tj^soZiQd~lPN^on!pmaD4FCz9_VaEd}Mrsc5Q}Z;~|nBj5scrY4T(;O*SKA#+nQGJ%NDVY%z0|ra%z4 zgAB_yWwLls)^D>~OhQvAXvTarz+94r*-SQr`vJer%vh`~anUINWVLXtxs9;WJ`4Od zh=UBO01X6C6}N@)V+TpXX|T0z1YNWLe6Ff9)$>*VPwD)w@-X49u1_&o?h~B$Jf5op z9aWeA58iuz=YyKA_!Dy(KCTVCekdC^+e`7L20Ml|jH`z9JS;t9oN(lGOj+0Qp*x*N zdO|rnq4OKM9{a~s$-|WEm!<1_>-Q`5$x}NtGDBH_+H- z8m690-K!tb6`;|Ya7G7=4KKJ35_B=dI0Y9ut0_RX`@xkhQ&Yvu=9FrGuD<~1(W~-P z`xbp}PyMy}TlJCU)7x7wfPaBgp$13sy#?{TE!6MrLQU#0ZKdz#M|jgC=rQwd^qkp* z@6L_yjv@j)-nc90d~#PFjM{8Ad(nO2Hhg!djc5&Wljq%?n=lHJI)sR_?IU*=T-h=u zZQLB3Uw@&#B0sHf(S3ViH*A74^qZ^O>))z>r|xdV5d%Qm@i|w(cCBL>TnjhIgK5(a zc}H@0lKR;6Jy$Y}-bq=a!g3M-MNkHn&B`u?Rz~1qO^7F&kjsf6K4Qv`z!%{N+N1eC zHjW2V#k7{*m8K>UZ&Iz{NjQCa3{bhFFo~t8NL0wgP^06wzqvW z_dMJ8k9&06cR&s<8M=PQwFUUgdh(yxz;~@IJIr&xx=UY!VYHmOAMeo|@UCXJ1k96r z0zH#WaiaU6^#_ANF+Ky%_%>P(`Y0zx1x=LmMNks3emw%GAE@X_7jU)aaR705em8h| zQw6?Vffe?o1?o-#l5cG3bNtB!O&V^R`hZXCZugBjrs~<_-R)Dyfacf@c8f1pURFqO zG1i-QC=f5~WX;^E;og&-u=6OKjM+&ZWie*?32eitI4GP6jSj2iKmG}Xgi)3JKcxkcJJw?CdZ@Qc+5eGB{Q(+X27R(7fZo- zlqECBMxaJ!Vup-9Lq^Q($xw1Un!#hVE9JI$w2T$^MAH;N!i=lrbt~CS1`8e3T`qGR z%gJD}Fd0qmZROh_{=c{lwrvE{mP|w1w1H{6rtO_(nie;dt6nFTcliGR<`5gFP6T@l zor}U7Iy2EZ7f)*VO@<2?%&854rkCe6b!VJF{BiJq3pPG?@)G~l0d@X2Q1R1I$P*4J z__l9qxf#9MT#<2)w8tt&8{JpG;?rTtM!UWlZvK?-s?mmDC40XW>%LdfD}f)}@h#wh z-+=9agHIon786e=?6d1vSok*}10Q@E@bIs|i?8?)(i}VoXpXftg62jjEDjuw0e&^V zho=Or#0CS7e?b6n=rt_M{qSu)lI|2)@kV?NQG{IO4@S?z4~*BioWFh@=U$SKR?ri^ z>HXqZNAj}SAy-ShZI%oEg7O&gaxVt zvFErAVI+D)3M64FdQeKcKo}79@{7ONBsWL z-RPU>d5TKg9;T4RYOz8N+Fs&D zsN?|$4Q&*8<}jcI8qvRW{g{uyqY)Sh#drvhLI{J=BX9xSp$9abstplKasO#7PVfVd z>WQLmjyxVm9+V^YGETGW#trm9^PEk%<|H28 z@wOHxptU=73lr-U;@qXLEyBx0>LTCI#3giaAc zrwE}_WdEsigkCq?h$g(%c!a63<)#J+=rtZn>U~CTG5*e|o;j$+Br>T)CY8vf5}8yY z##xC-y?x5$^>s#$l-to%DKi$^b+@(&APt6LpTQA=G zOW4Rsdl8>QA5h_JEocriBBPiyel`&H#=S})V`e$HmWyI8yw63aakZbZyZnUnDnLu7 zN*o%S*K7P0Qoo45wZz}}R)e}RqhAKI@r0N_ z4DS#eh)K$dj2NrUVl&$~n~CxIJU+x=%{&kePz-_?tU-#&su>kAF{iW)q*z=;-2P>- ziN;035yis6mR<$|KHO3vZmH_|FH0crOTeIX2zpotYNQ$y8?3Q&v@nr+tVj47DKFHE zW$C5|FJ1PfNzDnhx9!BuYyP!3rT&J=k_vXIK_%qjv6dOkGHCY?_g;U`1y|Rm4n6jj zUmbqzD__3*rC-9Ud+u$C2jmyV{!~A?Y(_EObxVW4!qsRU>Loj&+0ZWl2+=TT`o`yU z?kBth3jO5#C)yKe-PmC?1Fk-9$n5#!r%coEd5(b`T(8NG+Ml%_wjc7DT&_+I!~~3| z#hjT7JSE1yD;qJpB%c{R#gH`J%MWqt7MqQ;VxhfQ3;Ne3QXUph1q_=-mLOLY$ij&X zM;^|HXfNi?p@Na&SZL(z7WB6Y*&L;T!ifSJEbJ{co6*xGa(GnGQBLhs*|$wn*6vO!vV!42%6MajLy_F6G|1X< zp*Rz1nC8`US-4q}<$goX17(=;3P!GQp^+yg~4Fp1yzO#VcQ2Ib~&!*;bsqb@nPHpcG0uf760_^xTi$xWSW%n-cw> zzqEJP?cd({r}d>i7+U3vMzUjf-R}{<@Z}f2mU7>t(@$djx70nDFQ^!~i7fBuSE}pO zJH2=Mo^U_xd(O4ncf@VYMXC|x;ot~-STh44VDbS8Yo8e|0USu6H$V#g8wddoKOxo^ zb$RXb;u`(iLCryj?7%aE+y~+i(v~9_^jTr(V9XHTm}wkx{upG049XA&;^h6%AIkAD z7$a;X7HmFGmSZ)M1pSl1P2u#Ivs?&M9#z7!pI}c~Y48 zV~HpR3F+M17#gveurRzpTes-GWy;Lo-1E6Zmt20!hWfwO>(9?GPgWCA;gw5fUv~^W zD<{ggR4!WkdHXZZK7Z4kd)v#;eD){xH_J`ce4m4R@|H#0euqyX*6nDA{!W=BXn~h# z4?OCJuI1r1Xif2%049*wk>)qpU*`T+ur$FgI1|naUxF5fm!rGTmg2MUd(A&-ekb;O z=}hcH>BFLPCYMqo$z<9QkD4Wk*Aq#LN{mW^e7x9_1xhR#!4TRL$t#N4lgxX)9+b^< zoS6eyqeSsv2fyeth1!xWd|FJSbdGlrs~$A%PY|<)I#0K1oFZ^=%7TL+FXT~Pe;Z`J zf0VpBqwxY7Vl>C-I!MZiXf{2p&0ofo7)~nVMM`QeA^QR}>sO?dUtv+vXke{?mrzxGJ2koT)e`;tereH({{B z^Sp5u+^|hgZcjK9oe5_f+OqyLpQ(@5y-VReaGbjK?9Su$6R>z3`HeWd`gCfPdIl8W zRWIr>wKftm_)WLBm%IjKHkfUn8Cn=x6kd?OE_7XZWqwZ~V(d2IObb+qWbTHk1 z4RZ~9jqTCGv*6j#n?XCC)n2gM3J%7`ii|ht6TJcrX=oB@kHal`vfgx4Qgsvx(?dZ| zC=|5W?E$;5YQK^!ncabP#Y7<#bl~M=z3Bo-k|w0-Q1U%>M~qLt7xQ|saz-;DU|rI3 zvgQ32N`D)K-P2TR@%ux(x8OzI5r{D#oyo-0rS#M^mHvK01@sBbg@VPFkHeGZ>BzZu z3M&JK_i*@Jcv8PWUPD5#o3o)7;g#m{su8B-XoM+&#_{++GBG*9h+iY&Ygn23Un9`a zCoES+s!%8VpZbrc_T}N9Tbf#T-O$t03(NWLsr3(*w@h1k(KRzlt&PVp% zzc|yOC`byV2I~*P@Xj8k87T@q>4mX5^|O_W7fhZtZ{SEjLbpX>TQJd&eq5~0tl`+Uy}e04FZM{R>$C%cg75;2Jg zNkO4`7w_ghn6)bA4(9wo=X`fGB{gimVs})LAYtBXI~oQzYz(M8cq> z1AtsK=7~mQ0Cq$pln6thqa8t%Qlb%;lLMf$D=dUysMm7b`nDBWLtT2zMI=_zZ>Wpp z>zKX1QddlDD(Dd-PV!m)WF8$OF*5mJXV(b47_-?mAG@S=Pi1AEpbo1fy&fN1oudZM zc_=YTF}Ny+k1vfWi6gEOT914)*rQG|!z5{jyyg=A?=~$c8^A)p=Ic1$GDe+%gQ%&w zk_>)q(jx7#4+CTG)Aq}1^`av;ugQw=hbqd#f1^IDcpZtr^0T*}XYm^SNHx82cG}hc z)~cdFu~e{LPA$5oEv@^g9ash)#(Q3c4`|@k8iv{Al7x{z&mi_5D7lNASdgMT&>!G+z*5*d!L1Vga_q#JHm+#>X91 z0duCRz&O|_6O4tT!5tX16`G>yaB z|M7(xCH%xSzQO2gnAPiQt|*W=AiZdExT`Sk3K+4D3n4uBW=)cyKux?I$#CL!LYPWV zL@*G3Ddxg4 zLuSZX-s}!mAMZHQf$}+6h{i;FA?gtA-8|Nmx_Q>liEJ<$5`)Q{A|^Y!JH?J84AkRq z1H4}h$f6|5?S+!qo{u?ziP1|r_+;Vv0=lAbU*TH?su11DKi~bWZt9Be`?~+iQ&qJd`Ns&G=0VXOZyo+buWAjz67bCW3}r_yXdjo#cs_dV_l55^98=!Btp?H z7Q}8-cOfT6c{}8obFE-Uds4;6&WD(b~B~p<6)w<)IrJev@#-%Ee*Ki?l; zHZyZ(=FFKhXU^Q7nP;95p!{3&$7Unpm)P(HN>psz`L>uTdh)4v%ZrPd+75N$YZ~H; zr_Bs#_m3NAB;`+hS(>6Ot;yHwqtbGM->=@hvHrm&iwmeWsYtJ32@6~ej$Be#y1tT@ z248*96Gxp+O>9*1yn8xo%u%SUR;RP>fj;8?ex-yB&91XqZMyuOv$}I)QZzVgxTI5U zcQo_?E!S+o)y*tl3f6ZyWzm!}V1&>Jwx2(8*iUn$r)&7euXrY}9_bE0@`KZ_)E`$eP1cj<;CTdkTUb~{Kk^?l!DDI_5Q17MKJo%#9UoUJVcD}!?=cg&etp*aG5C~{>SiQx$=P3ma zdsd70BgDcid`cOYSY)!7=gLNOleZXrbKfjHp&;t;plTbtl>A4>f|(@~WmTE0g0Fbv ztLr8jr)`%#RO3vvn#h-3p4iK1L4;m5XrtrOu^W$d8u?;(;)NGpD4%$teCm|gocxJ-NxpuZaOUCsGq1p* zv07~@Daom+7Sw&TWuqlRHpV8$$+6i^+383(P^21Tiq;b59y;q;P> zW2)Lz3r;*NHmmiDlw_QPCR@#^Dakggg%W*%jBUbb&|3PeW7w;vT%I~cvV8GQrz1AT zc)Z|r0UazL1=t72R2#{=hB4ARn$0)u$^L+)&&E4rD@pamCO4>8&~8gjmBZJ(yWQF9 zHEC%@`Pt*yN3&04OW6te1yAEzH4PS%li~YO!Z*6}lN%=|PM$rxdg8A(b?UNN?Y|T1 zZD-HMPn_iKTj-}g{F=rz-GoT~U|_mlPJ@MRs&DWRegQK-L%-TXB8z8D>)&*fU86V_ z&vY^0JZAQ*KUJmW_{c-^R^Hh8{>=&LMZpv$#eHyIR#`A5d@A|eO@FAEo0R0_@8J{< zwFjShK0aM?I^!*IWyD(Ym^iCE{a`YK`Jt0+Ow$H&Sl?b;uVgWk6cuTTj;b-Ob8UC& zGDvaU3hy@QMtW1iUh}=q$IXvB$MjD|o5D&KXJ9c__l)!;Q${?PiVC?S$$7H+Og#^s z5v^H$6?w9CC;4XLWD$x?Y`Li+qs`QBx)GN{xcv2Et%J!LpD`mHVvN{*=LxP{^M$@W=P3Jp6gv7n@I=olswWZWcNSbRID5nD=hSC5<@|m=u@{mBh|=7yVKr8m%*NvVRl7kBqb&%rX(_LhD)(H)1NyR2aEYdyc~ z+4lSEUkk1$*+zGKenOTvIkVEUATv4Hb@#j9RT7T<>O04;*%=J} z_-Bk4o+ykcqcE3kGMBA?%VE7{Uk?xKq1m%!^s+X)^ox`!DI7K z&(rje;H7^N$Me-M7EIlMYC9R+%pYiYR`dOXQ{h`saVGc8n@}fD6{(s#d0Bh-wyelv z(KEICnpc*WuWWubGT{eTU$dn&+m%D8&6W_CrZo~qJT;mP&E@>I)bi%wymPa6R$S57 zeo0P6Zm!7|qjy9`W@SW|_Qai{l||+GI`y72Pw1qSgkxeHWRj&mtG!k)E_@~N36V}Y zPQ4}7braL2x*{TQ=&;%%;&9T!w?%b{_H1>>MRfnOOG=9xu=A${*}m+C?B48s*?t@i zblDahITB>lXm_ODH@V|7ZaZqz*b=fXxFyx5ZfDrS+ZncR+>$ycU+l>U)NB#YVPslNTBIW~EsK0`Bl#dG-x!qXN906Mxhd!*@2h=b7I{|c zW;?|BO7|VDP%9&4m(`MWkjbj8!H1M2#Q3*wmpjWf!yE1?EZ07rn zUz{Rp?>~oUA0vKQQtj7=xM4eYys(46`xD$lj*vpswO_L0>YoSq zXK_+$DhKok!b_A?2|ma4ygn$@2hm2L}GEen?3Mq0?_>NjSz zDg50j_)lLpvzj8KtjUp*J~lq~kmZ;5S1d2u-^uwX=bxr8qK#?#_{5^eERWeK^7@g_ zlax}FHPglvNb$uQeG#T2qfsBHPXT6Ql(BL>(d$#GUWtukvUmj}I=;v%%W{J}BuldF ztT#lxnVjMST)^*zx0230&UL@1{-MmqA^bi%FI4u@Kt{dp8oh(qTMVGE|eCsUqcG6RJwf8cI=d`aS$)_XHBV)Vh?xBE=S6Xa{LSF zPM1lSbCVPiA7`=l*=&(It2I)VA;pH%bnJ7dHp=Zw?9^{3_QV3U+b#%ilow2GlouQ= zp!_Z^S|DfP;y_&XZ{$h!3q)7Let;xb4^MsnUI*iGH=;5t`S)s?}I-oC8FtM(l+xKP;0 z9tAsf`y%YXV%f*+YbKd_3o|$}ikOm^!Y}mtXf##f?ipWQLQ>IOx{$78gKUdwnBLBI zo4@AYh^u3@rfT!$Y=!AC`wi1k8z$dk`Uj=?`h0^Dsn}PT-ZuTs^rcB3K_ycXjW$W) zg+`W%Mn@TGBE60d(_c~zH4}{~#kX0iNGyWMY;QLYm~S%QZGPB%!2F{5r1=Z8!F+}Kl!p>#27@U&f?5cxjWCQc zm(Ltw1iQ^n?RH|++IdKpxfbY?K#bA+ZbPOP=zPN1+{0-@+-V;r_?tnJ)8NX z_IjLJf5uOMuC~qlOw6*+uS9Tl+9%Y?96G^2G-2jX5>3(EJ;Yyc7k*oe>RLtkiGU?) z>eSYC&vRO~S8T6)c${0Ue1|zwD8dU!!N9{&ih=SpcicY_`{jC+B6M9yrvcppG)pxr5T_s3qnnL1vN0?L zO(dp`LQ~K4LRytPz4mcfge9`0e;JwtOvCf&LwX)`Z$YyNO${0^#_tcq3la7roh1mT zVH%EGh$fN(tkm5h^0c5oA9<%pi}PEJ{`qLi>Eof7(C?A%ge-0_O(nY7+I3j|eprlU zjtR?XBM$Ocp~A#tCF}#x;Fh4)~QJ_1;P=@y;?sJj-$I}cz9(p|C$U}+=3vWEd0?*_PZ9l&Ks z<1)l;Mm~`RSc7F-a}=QUBtSdn+ljngxf5XBe*vt=JT_qZ4VceH4Bv!#ci#@sdlUao zu+XMU=$kMP?nVvfw{Yjf3Y5^j^m){C6e;S}C*fWTiB0ul1oeJ>7^a{n_Nm@&8Jq~i z61KB{gkipk>kY$N$RP{Eunx9P(a~d@`b8Kvz<0>jFl?rK$+;=LpDFF0Y8zJRw_%v# zOkWPe4E07Eh9!vBSi`UeOq%>Ktc6%jNf_2)1x?b?L%eh<3>%gM1{wl1>UxiiqtFS766;|c1!m9jLSd~AAjiT&t zg<&kaZdAY#7;e{-Ff4&rr^>{{dD9n$VdO_2C14BJG<`=HmY_giBVe1LQy7MkP8dIm z%TWJ=FpM(PZxL`b=SP1y3?o1Kdj%YW;nDh!!>|NJ`d0)T%kd2(rpuQHrS3!7$A0R1KE}{!ha&$G=s;PF|;mx56;isX>*So6E-# z3d1NL!*K!gJJyZH%rJ~}j5-eMr^#PGP5%06<*c37x_MCp zW}vO$xVa8G(5;6a=teUD+o4y4RH3gAVQ#l#c$dJ)!!a^ zAofbOw^s#)&rgYEB?NTIrpM` zR3f|)b62n()gx{jQtUx&&cOiA99D=@^@np%aJG<#vx9k6Awb3ak4v2c;{$5e`O&B|XzYZa#wIc06 zxMphLQp~R+oVIUTOs|k*JLb|RD0T_yZ4-I5q5bXos(y~!hUMKNO3^NGd(dtd@x2(M zmVis8J)Bopm_l2amRfqQvC6-cyhliLyQuRntYby!-rA|WzKyT$f1&;FOX)oI_NjXA z6S^}XIB%QM;BPOzn)|=FA zt?eC~TKhIAJ)Qq|dXs^3ze%IAXJfmPRo~Ut*VEtAIgq`gqpu$mD6{g43j{8r3e1M( zxg58?af*H;x2pSEw{>-2sVwR2>}u;!a+T!+t=$_twj&#TUHv`XIm(Kzwt=2LWl?Ki zdq?+xGGk7mx4CDFvZ-~uvZcR68CZwK=!T zKH=%@>uKN8HlTEME8Es}wXK_$7TvDywvAibG3h{$(%#kIyAd;J?QX|ZU5LUgo}z{((L$9m;E3_LzK%R*B#ss|#}( z=-9+-qpu5dZSUFEy|JftdZx&9tIBFepMvG>!K~2UGSItaKxyyT%7u$K>pC{}expPe z?e2Nlm7?+SPW58g1F(gd5&HO|n4O}jUJR>Nx-B2V{G&r;0w&}y!H!q$S}!h}>5{x?DJ5AdlS-s2X_how@=2vqoiyj7IW+#;`c%hDOJ*Q^aZIkw zy(sUC@=PM(&&+|oi)%@b7^+%9^IeenUDark+1>p6%b^d@;2JdaQE*}CoMr@`Wx!3~gpXi1JPWJgx0FE*$%7`4NIc+NoXmkbVgr8lm=Q-W2Nt9O*YXSD zFDRn@IBWTY)WQuU17}3{K{~P-i}Q@{!9H?H=wrA6-XdM_1g3tB_#hJ&k@=zHuoN0X zPZ9p<&l30{>?Ial&@UoJ;s_l>3jJ^k9D+YW2v)*9nte#60Snz7dJ31%+k`B&r^`G9q^zar&F;S@H?{~}qWi(F63=?;x|;)c*K!G$IC zA&mua88)$>5f|~1%Q5YJbQ`^%4zefN`%;#4Iy5`O`#6j#MZL?n%hI$ zLwE2mf}vzOFy(r<5pIU3;C1*j{2fk_L`<2FDa%O%xr5wEo~N&~%h)Qm8=Kv3=?UpM z>6|7~^IY(a;0csEr&EA)G z;vktM2TOMuX(HRmFu997OkN}(kdx#b1!|({_;b@)bRk_uchFDiIc8)IR>7`fKV)yR zucYl7FaDm;{8Dp9ds3I7fA!qG6UTxO>Wpp)4{65s2{Sbh|@G`uLvVR@kgZBl${P!`|#95etAfbf61*FHH%6kFUOciQN z3+ceRRe#rz9pqMWq}^q3980tyXrU!p%*?Ww87*d(#mtNrGq;!}iz`bSn*R#wJ|uITKl^C5m*eB&oU%=6kLvWssTa=0OW#v6rTO;(+f zt`UL1``L=)Yh{kx?!O_1xOM4z7jwt=V)lYwUn@agFQW>)-V!^cJOlp;^?ZOD{-z>v zTL?cRJ%g)$^PC2m#+JrDZj%PZhG>yB`zGe7ggUcP_+ING^BOl%l3r*v#dcyl|E}z+ z{@6iZFe&0)CVo+0%%|5Lp-sO(t3xINTvwi)0)5c(;BYs5Aypq|{5geSb%ezublK^G z`O5N&y$(vG5^r4)HU>J~=cW41)NF|K*c;)EDQQLi56RDVbPz`Ytb z8FJ)Mmj8;pd#R3VwWSmf@)@hzWs&%Knr4t_LB}VSmBTO-Trzyw)qBTy=I=cv7#{b> z<>{Lj3l^fE7@TbPLkh`>6lD1zs%hoH2S=ig%bu zcdqD1T4kC}+8`Sg>!i8v?1W!=&HP5h?0#gOplN<)MR&e+(r zj#bBlrz=s3;;c)KA0G-vA7uh%GKLw0(+!x*>guQvyhZha1&xMdVijwZaiqF=r;j%7 z4D3)qT`n&-wX0`Qs;<775z}+mIBtt?aWuu8q@})_pd6YU9lvg_uV>c5mJ|0#BPlK- zqqtr@H#Ew7zJF4USVlA0OWw2~6NA~Cn!hQXu-QZJp9?2j}ER`4B7DH;JsDd zjx<=^0L{8DBtvNE;sZWgZ1_^d7$%zvb^FsX5hrBFw{j&nGA>Cv9jPOKWxC=?9;R8h z>kovr7$+S(Dka>f6#Rg^4mN;mmr2Mkplyq>?0cZ$Pq?CunK|8Ucx3K6F9N&QaG{-G7Y_`ht4?v+T+q|TO#dr&skJ9{s`YDun;pPc4 zrNXzmYpe1%?zpI#erfO~DpIc9-lmvkeVd5ExeIeUo%ubp%5mNp3B&`f^O!#`#&7Nn zC{1c?>&$%YVepH6u2gG0yj~GIn@tTC9@aQb>FIG_iVCrR)AjyNxj(1uYu*2P092K1 zZxYg(q=S=#lBMYMLh=D24Har{U&ITI)~{YLD$@E4_-R+OFeGH|@ZmE3`fyB|Ylq_w z803GlHmOp?i1c37o#(@R|F+$evX+6YRY1Lj9Ki;cGB)cUUCIdx^M)hzFSK{_0C|j4q z$t0NZM)fB{qyg~kk2FZJx8g?p>4lnuo?8d8<>*16XY4@=y<1ytM3@FPR z&{`;z-NipeAC-+uC27u%?_>|Yp%)6fr@wekepOj3Spu|r>ME_;x{3!Tv%+D8KT68T ze~I)W$r_SiB>kL6V}4{!eN+1KVHK>@6mxu6l0*_j`P)Zp)N9_By{Pa#J#0Y8%#C_G zLmtC)mVePPw{s8d6Y?w-fXz(XVy?40vTd*n>F|3A93F9zJ8@h^YQ*xKkZQJX>qt{ozt1=DaJT#8f|OvBK_`z1)3T2eQOYnSmL{b zU8O!&r1nq`H~{T{rNRcUQT!e%vSc2$l^Er56NEc7@(>;>U|9DE9X;e}e@$fs0@Ftx zh6Syx3h>vcSJ9u#Jmu3hNCc6~&qb-aa*!fZiZb1IpX813=?T#P78FwFnBQnV79|_0 zncvp&759~bH79q=jFd4NAcXje_hni^WRq|xvzx{6RXhr#8Xj0+B)}9JKG547b^9@Q z`>9ww-;nFM6S5+XWc^w|Fz##Q%1%2x`t+Ii-XZz}Ij6vY#alY(b^NmQQQl51=JKdM zhSzsq%ir!HjGZA!@5GJxbxbc5XO>rauULNw4rTDpz=z*Nn3FGI`Sk2d24M8ep^+4cG=|Vr?`s1CC;Hgs zU(uJ8j^9GGdGTDYj1FYv;5 z0L6%TE50$+@IF|uUqa8&=`o_&3cFRi?wenT+|s6@nzNELK*IN5fsN%t zp6f&s=2+^#2>{@22-7fFh0PywvBe#~L?Hl6bfq9}qA&<}LPrsfI}C6Pc&a1Z#P!C& zGeVNzy=6=gt?#Bwe=&%Kn-OHEgtz=+6By^QITndmZO7Mi)0`Bn>JX8;$jEvhN1HCl zbd|pex~_d4AECq3bhZ8>f6`fI_OPR~qQm{-5e&R-)q%Bmn$Zm(Q)zHIgPeVn?(Dv9 z*Q9uK>GXc^RDQ93F$SOC+P+OEWt}j?iZ2;{Ha0UnH>2r zZFW*Vy#yU{JF+lx1yK*?G*Y)UvJ}KRdM!f_ArpZEtI+OZv{j6yy!oU#8~h4}O|^@} zAL&2x<@=9P$){eL$1;?6sgY?D+>X-!B;aUgX)rMGG6XeEf=wPlLtd-y& zHG%I9xDb25?uGBp{qmPIk;R`SyV=b7{SG+y&BSh|>BVPBy_g4c9`nxgJ;hlZ?YoLS z!RgOPrzO=gmFFcfCd`HMTxEmNDJT(hxS|1|kHh98Eex!3j}T>C7g++OCu4NHx=c{w ziW4IQOEG{RlmObh$d|KSxwgs8JViY$}!<3U$U%zYc*V=?#?T0(?=wKdQqw=QSs zG59F%eGBM)E9uoXD1hw_>oyB|udP{prIouCm%ELSyA_tZjmbRd-4m7=3}w|bY<*@p zKKPX2;7_vk;a&GqYjsy?b+-U6U2;<-L4HlK_}Vm#-Xr(WPj%oJ{tVv#rq%yeXXvBA z#6fe=@-;8PcPqt`{1N6gq$lfHIZlI~xfm@KQ85v|g;yW4*Fb7`U*B}V?~e?D(UiFtZly@tdQxBav*>cPJEPvETRU`8upK~iNk>v%ZkwN>6 z+kAw26b*SYiu_8&=K2$4MB>YrU6g1?K_3izQpdeoKOWdjad2A-?wFDZVbG??i9mn3 zkAagIuuRunN8g-VGHy?B4P35vzSwU^Yry*%9-vNur7RtDvVI)F&mHE^qLyTbO#lRz zt&W@^N91k_)Z&o?R%E%m={RlRom!vo{kH=u0JO;MUroP`Btx9ENLX^kePtxSV1 zyP!alL~t@4eak$Zz;TLqlQO2J&M3zpf%p*z$}w06xfcEg7v?7?I1No-_bBNOcI>0W zE~W~xSM6owffCy}CUhwIJp1zdX7hB=eu2g}VEieJVt+T(qXJG)FX*k~wct(Hto03- z!|*A7Pi@WprdnrpzqX?H#@xG%Hz4wvXL00}2QpZ%nNdT)ht?_*MW(&lG0rPymGpCp zZNUBMJIQ9xt$+~6pQceBZH^%=1@tc`c1f@6R6p$&&~IL7vlKJd_I1gtdjqt?EV#L= z6SdY|hv`@#2i2PO5|Ss;OiE7CzG*6ulN`9xCs@USM^YXl4++AY6k;YEjPs6}1l1>% zO)u_AM`Raqq9hQrdUx=yBd_1L{%#f1`4-KU+6t$WMz4Zd*>q~-5wTIgadPibsXOz7 zuj<~MwbLeYvk`n+)4@Z7#z70ECMq14s!v01Yw>Z{aFnqB#C-im%D1-IRt8h^Ap5-Z z_7s=i4u^Q}&v?NE|Hc<*EA&syQGBDmP#W8){P?GHXN5&|?3f{+R@B9+C|M)2z8`BD zeS}8%5JEp~J4nO+{N~u4UW2RE8r>Z|H;Hq!Ebr{Mgjr>qpY1q7zr5DolD@vI(%RVb zm{ckuJp?V@BUUd$RH+MOFmQ*taYyVGfN+UoW>r74Piyn*B6a%`!w z+8F((691zzI@;2=DPI$t%I4+Q5jla*NeO{}QQAOJ9J!vjbIzQa=B|0AErE_#@7Xx! zp7W`)ZoSogzO(Gxnj(%t z-@;6M;(|7|)hQjSY%v+#$4h}QO6(v}&+Hio7xlX-D=&2uNmGEhLBfF|#s?NR9=fl$ zq~;9qjXLKDpgS&F3M;hwxNP9l=M5KYDZrO<_rphJ;o7p#uUM>)v`Z~T4hx&+&U*U#OswBMLtRpx=Tp*GmJ z?S6Ui`evem$R2Y`~YI&=Et(z@^l@liFMnD`8!M>pUd4+81SdSqxnUc zwkqC1j-HnZHW%;b>dx`z48uk_Y9admL+B(>(dd%qqXPIKGZ8@q`dT(ioCpeKmF{KT z3UuPMh>|G^zfPVqirL?QYw>=*6az>c659uFH-k!udj6*{Fwo8YU&+A#M-aB`|Dk~m z^mzYj!2dmh4fJ~dHvJdh^>5Svz@xG;G5&9GRGq}p=p9BR(MPY~WWEShl;i{_Y3Q&6 z;@XrDKo%l&m;njAq;XF`Ct=fN<$}4oAA*a@b94L%d&s%Lky*k!w2|jsJ=dc!=9^7h z$U=HT=Ut!%+Jcie4^2__!-|Y)T>sPVF8k}L0cVV9%b3qV|0*8AfyH_h#b>|qrtqOK zzxLQ{?rrbdWX#jy^>_vkA%V{v-Qt6E5|pp(O09Jft?tz z5X-@d&7z|FTx|Lc+F$N(6=OdM`DyB}gdabn%S85$*FJ2)NW!t=4(UJ08em2dE(XyuFh8!)EkG}v=k4rD=gj8vY_><1e(@`RXt4yHfUGQ zZSb&qgRQ^b_ifZ4)Hr&Z4(2tUxEFrknGlbDRE3=}pwoJZa`dXPvUrJ}&LpduEpKv= zcv;aw=#7@wU!l7wY+0diTRjuOc)LsFR@)xjjtPP3js8J-NG?WsP-6l1cT0Qae24y0 zX9A{I?7XdIsq^$zi~I_m4q`Jt73g0V|2(@yuxsmBCS>r%QO({hY!5-AkWRC($m_et zIL!LY@SNo=sGH2S|IuYvjfKWNwgCh|Yb?|*}b|0>gelllME zuK(iEz}H{=7|1FA3nTw`y7gbx`mb*NSHJ$NTmNHP1I6Kg3FW`0H4tzHZu?iV{);L9 zhh+ViCH_aU{-^C9$@(Ah`X9;qpSFJ_>%aXm10`!9sr;8)X8Qj#tvUXV^B>dtAD%Tq!Yg%&x&jvU|VEpaxU(@=ZGY68^ z|DO9_+4^5|8|YaB*zeWoS&K{%$Ll{5-*04h0$RG3*$9nTGNG) z0#ZvB*$pvBf;SKZT)pXy)i*nqnzU1o-7K+Bo(VvVHnm*W_l41u zy}$ZZuR(zD{@!Lut`6ag!GqfBfH#*}{W9l3)g&VH>N(}()$5#Yt9jn~X4K#d5p`I# z-h6SRC36J*v@$u>ao##=hx78KoC##@6XQgg-}Tu!yXBdqQhfSD97Nsnl-*9r>XQHw zyv7V2(%v>*6Hy z1WaWPgI`con)oD=n@zbZ2gWOJ1~nVY^omJA7WHU?iYRY$-K^sagkVs7c5VsRs46x} zWKgL+G(@i;Qh1kr>`&k957#YW+z58q_e(%bet?1IR=@^QC|tLqgy0Yi8q|$SX?!(4 zp9^cYt5iogC)aaG0;Aq1YLhMhPZ1F;$1mQX4C1w*6T41Oyd1%h zis-%F`UyXrctSF!sJ2y4Zxb9r5F)PzzZc5ki`14k*6Ftla{PhQn<2Uuhv(`MyzhP70krn$|UE~Xa>BY;RQ3= zL7GIW5J$b+)vl+N`qOoE04&>ZF~fQpSp#Q`&@yBYrpzOOPIPZeMqV&+0r&-{Bd6E4 zev+5E{)LCey81+!KEr);8V^ic@`XeRkM@4myK&SobyV0+e3`pnMY@z#Q)~XlcE}%x zxt}Z#L3}WU?05CFmEhA(?|Vqi`tsg$5pd$*5PfKZ(roPvr5WOXDlphdaVrD zI$d&)2RGl`UM0j+W>~4`ezM%3RORfeg#XpY^5&4eQw$1g1M1-C2fdp8v zN)9DY9c;JO?MH?orG#*}!Eb#cG-xREfg~hWDKYFdXjK5gtPuuYP`7-t1`QEKp zwCQ{BF)E$~NO~k}=|F1NKfYUp(J@BS;SJu=TG)Of}DR%ZY;*7?}7(8}K0*dkNqFoZ)7&X-77 z0^ly22|OW4QcA+y(u*5w67IFHE@W3)EypAItj;eEisdLJw zla9u;A}==&_*3?ycal-vp1&y}gyFjS7Oeyx@ra`4?b}?WLLkbiFoLy8M@7yqcy!e{ zNjy|EDb~5TW$4H>#TV{^d=n6)kij%8_65LqHXk=Y;gY$PmKM+Z+=_g3?`ZDJ(c!|S zocYLU4DW+n?%{Rk1VByu&(99Rv||2+t>CL%3OnyNz;|RX$G%QRdqm8-ymFcu6IUAG%CRn`67$D(4!FYhObuH`Bt}oHub-4kxcRabu>2b#Vo2G;x7R6|J#;+Zpi4x4LL?nM z$m4glmt1dydVxIhhQtj2^HR#tvfa&=lbwa(QYWL3iD>eq{(HBggJ;KTprVK`kdQwhj-vl`;pgvNzTTX=E?>(K2_?Lm9P? zNwQGSm^r5pEErI6a8RY8r=Zzdd^k1?>BfGBScEra(}78NWNDklhM%dVrW|nH+-b$t z_Vy8327OH&AbdKRk@FF}0jnuf2moZz;-n~t1LmdZiivX7B`7hzg7POfY=0Olcaeq0 z>B0! zicw`aJ4e*y;v$jao1D}g{+e`<@3h2?V!>o)mJfi!j8scogVSWif__uCsOMYLkqWkY z$6kAHH`JL+w&AbW_#eYpMw%!=hUM9gTja@!_DM1rTBl@%gr$q(u^FZFSL6g-O6B3= zWN?f}MKwa%2P{&6_<{_BN@Jyk@H2v%N;zHd5y@dTAEgB@;!MyZ5hT@iRV~xvc#8_5oaxKJ+&TI zA%~XL&M4s5Sh;L2g!v-Hqc#ToxGdY|Nds5m6uQqB!y#koay^DH-pO|Q(1jUEAwsr)a>L+F>K{Kb*uKB(q0xWWW;7?;g zpn{1De5ZOityvNa^oB(_hfG{3w)KBfa-|i_8xu=@%MPJ74?bauI4$Alo#Em7iDPm5 zd%JWcL*;c+#K0yeKHHJXQfRKP?h}GL8VzKOjiRRPs5j@bd`!bceWzA^&DMGKT-6$m zGnR?bPsT5yn8J!wA%mAs!!E&8SZGgqLl^8tKxa^xtdwJ=qVt79SC=;%d5_6fa{ss&pWDAKSiUKLIq${rC zSOJ0Fps#c*)0a!vkaF1Z`0vs9QG)4bWVlnGcD_gJa72d3NG z2ecEEbJt`5h6;59lW?2}a@#hikFRWXz{kA3uK^~DVtn5{S#iAX!rf^YvOi?mUxyA? zJtE{3>!v-6_1Z7%`Ie)VxtsLY`K{N{8=tlVJTKatnJ?>VCq_oLL%afn#p+zAQzbp1 zdTLbdLDys30h(+zITa|hw-8Gi0|sj|q3?O1TFCs3D)C5D?l5an+0XF)^&|imL_j?W zwpK+=foXl8fRekDR26hjkyDXE9X4-k$vAYd{jo^MRES$I^sXR#xba$QOA za#bYE9eS45B;6x?xQw2w8l*n5GoY7NRVoE)BPX{0JhncD!~$X0=V5VxRwM%B z#f`JgG?`wo%ZF2jTQN^kPPZY~!bI`0!?v(0l-H%POFV9ClP%g~5D^K@Zk{r6Qaq{+ zsms@())Kpf&kvh59+x#Xa8Upk{WGpgos0PiGYuKe&{ZD$Eq7bbN!br$TI@H|xUqRL ztEF;aS}2qSgMuRnEN|!nDY~(Rwn-59vlOfqCpGozWOQA_P|pwAGc#fyvxkhyagd47 zjfgD)%wi|ns6RzcJ1PR7-z$i5kMHKf(m1&ygO~%z)-_6LaGtq6&Wdf2rg_=bt`AAD z>T-B#uvq@A@T_oC0uE{Mm<7`2$iu0$XsGQ0+JZzaL7{xm>4x=VNkm6mQq;7`6-o+l zLgJ{@+^JPWrtH=YLip%M{LJEMOm~q)NZ4ZBw1D(?6&OL1I^%BWo>*|YBS~aYf=!0_ zli2*3sywqnd;Q*d2eM9@x^Oty21usgua)SqFNx<=+FA-Wf^0FCAHIw{2R2a#r)lCm zO3`5pW6T7bk?aA>*w;MMlCga27sTwWU_ZWi{s^y}3t$*rrB{D_wtp$6P@}K3grUx! zx_Nck^qQ{7F}{Y$8=qx!GLfe6Vw~l06&;LUt#+i=dppbc)xuF3df8l8N;R8mMQ5TG zSxAa`;wsbWvP#Qgb$Q~Zf8kxTP-y&x26hoFud{jbjbh&91r)lg^vj_Yv9J&vJ~JbS zmJRcPL;4`(IQ@(h7vrBl1Cw4NXy%v;l}x^Wu4qW);FFU^p>>-q@V-2N7mTUK$l>#( zW%DX6Cf&P|_o>}vFUX>0!_O8ZCuZHQKu2s+u%2gIP?i`pJ6H{UtrPEF zOysdwAmhnbw^K|^zvj;u`+$byDve3`mAR3*!{YKZ%azafZYpcC8Jr({!NF~2sD#~o zbYJ*YNEuqdoH=#(!Uq%2S#S$Q=T#fE%BX6Wet-?j%TioeaG*oyty*kM;OrE9z7%yy znoKTFm?CFZ4b(gV=@Ybo%;?otWH6xQD2!`eqz?^SCNiv!Mb(z{dwUkgCT&q(edh4e z(b`(sL3M*(_I;r&f67PyTd6NjN~WAnZB?@i{%_)wb`OIc7U$MF*~w!0Sv~Md3h%MZ#0Cj)}g zB={4UTW;u<%(?N(8gvObeM64jO{BF85|BcYkD3W70_Dw8{8H68;G?rkBKW=!VG+rd z9>@?AUJ!{a#eSb-rDn8-P%pU9Sn}6-#+BT6ud|8eiMLgDf5yF_gsg)aSUl(=dHR=VtJv@5lb^)Q!WTf$Zim-E(+Hqn}?>U-;i-JyB=_>X)Ad_nPO z-hYK%^A?|`9+<-J#nv4wtr=hrH30mL9RP(8#ne8&xyw6Z;LNQAhg=*Ujz@{6{C0Kr z*vd2*S-XbxFc$HCl7`DW0J}_7sNc|=K%6DnPPK6%3~1+ z-li9xIV(KQLe)G8W{Jb++B)8A(K^E=1Gd4R^QmLu+L$5u!=X=3XIfZT_M&{w_(S_W zx<&=aN%;g{42WSwjB83ABpoySK5uthh<^}j zh_4I6?E6!kjhYvJL^Go|8=t$^cC7l;(?nsaRTm29{WQo&?C>|Pp5~Z+&*ISfd zUbqP}L6KqQP&Ach;g441pNt=erv|9K)HFZk92?*ntx;Zy-1M)^@%(5>H1I9BpNGA4 zGb+k2AcQsrSE8Oqkgp{=YVhuHjKP&XYH{GuhQ zO;&~M$|>t=x>X_Lys$HtHfENYJ+3`z<6t|yHo-UG)AgRzglSqyjgVMHml~sw*M?JDygX9}Sf_8mD` zWIS1er#K0vQACiYzLI)uRZ($oRe8y9yi|X{Gdc=4Agh;6q`N`&t^t{(pPs3rm%?lf zUeu1qffLC$5ZiM)tZ&dsjyG>ATfL_K@{+HXJ zv-IL*$CHwg3Nc|Z4XK@N%iGH8W{Q__y7-Wsue}CSg~gPWjfT7)lR_;1YEkDOriGT1 zJQ~E$?Cl6+nAhaw3@$;2!beaJ=xHt7hK!vyTf`celo`B&pGa)$+f-N}8mW*@1W1bh zd@A%DIKheo$4+?|HD6dMVAYYmvAnRdqi~_FITwQX?3;se_}vl8;fgTI(Mg-5ogYrf za`QARFL;F;GWfY25XOYF6l$SjUn^*B+@(=rD?;0eIc8f)mdL#beMN2%I(kTHL$H8tc(W%_$6XYTwivNbVq!#>x)-0gn-6i}Wo8J+SQG);6V)yU|>{G`Ovn94KB%9E={keimOUJnH zD)R*X-lK+(%sZ6xn#;=0L3Axr&7is^*5cE8RBm!3;`XU(0xnt`fmbcIW_aEqux(1tP9|GPC3j7O8^>dH3 z_k3Ar*a#gv$VuUO+%zspOP>m#i8_pa5cofm4ZqVal>C(J`R+uQMl>ODkZk|{a$bHp zB*x3{epI{pd6*lb2dt@<%@{G#+F016SntzlG25EH#`6K+)8vb|obA)(sM6NkjDD0r0rxEGH$h}zI-pp{YJjlO+7FG`zM>mu6$UIJmrX3@hyRxrOGzo~D9 zS63$Vha4Zg3E(77RZ1`1OA*Mr(JMi<*d&95h>9cMCc&=WR}r79AkG=N|R=Ut-@fcB8dKJp6_W(O{S zm=OSC4u%KCdy&)STK%|rQ2g?4-$eK_m^&UTXJFPoR3hWggvG*AY%yVQLUXS$He`Qk zY-8+Z+3aG76@hz)QJ#>{+voP3m`!1#ja7*lQ75__hbSOqJXy@Vqn0NxU;e*qg&iF8d&MzWxLmTyI?$@|?K z8qxZ*_}5rP%`8sDxUO$^|K^%Qzv%flx8rU?p@a3N93wM3lI$z%=pfIkr-LT`tb|n| zr|0h=N9p5_AelR+)_t_K2=!##@!zuY{18N;z`4Y6P*k9$LX&;XBvLZ)P>|{F^*I93 z2??mUG3cv;y+NQYK-0rf0$D`JDf+@r6pbt9N^QZ0p{_%LXr(JedgNguzJCUI4KcbD zi-XfQdU%*A;V-1TWGe_QQB80(f}S%m55CWM-}mf|S>VeNc_Qv$?rz0n7V-;0jUJNc zbh30{Jm~or8x>jtJ92vsc{Qm2jv(5b zB_%9rAFhWEfiO-;c#WZiyP@B}dfLya~<7|1owD#lK; zfq**AS>y@Ji7PqRDe%+g-`+KA@n*dCiG9nKlW|x~Lwr+$o@U@r zTL`2)Yj|q7RvS2SVO(NbTu{#R(Dg+2PMFtkSAuJd+Rd{ffK)FWGjz7D_+~f!l@r}Q zM=9u-m|X`Sub&{Z(VKhZ4f~B@DWTRR8?~vSm}nfbMK$B9YUHs3GopCTs4*w{fQ$pl ze1a$ZOXACzEvvVy+e03j|;!YE1sYxaO*yEy7W+#S{^XH>$Zd9NNDZ8 z@wF)99zt#LU%J^aIOJ|{$?rviv#}Oq4U>H-awO(;s_?6}5N2lk6c0-VVI>S3$(pTh zdY}E}*|ou#e5g!xt;TG1J~n3LelRw1ET~I2ne`{0UGhy}sP?m!grS5G6RV=yT=w+o zIq(3o@wl7@{hmA~nBpx}LFgBiqkTnbQ^ipeOcksg#qHV`<(w2n2X?!hToEHee3B`H z(EWz(Hvd-tmgS%IyQatFm+&|6f>rE(jL1Ci&pAgG5L1ALq#u*&Fdzt9NQA^(0Ujw3 z-YF+*C=QtvsA!xI!B*^@9q0Gy)hjRch5J_YYw!J|o%Z)&9+&y6k3Js>(}Nv%F?vs@ zQS0;KcJ9tE9G*UIFIj?-t}Y_>lf;_R-GMACKUr$uQbH$FSq-SAMv#xlI1kBQm(9S#sZubRYIT%q!B0XZCy!X|_`zsWfRNsL0h+vZ;QFFh<<0h=eJ2Cf< zu=n-M=RcqD{FZbG=cL2=&bm{S)^%+P5iLHE>L&)LNAEweK4iB`MW7>KI+cks)MA1z=c32jXSvG;>T+JF4(8}+xF9dd80(BY)b0)*Dof}daSi#EZVDZ zq)RSVKZ|-hc#MV%Rx+U}4#6URFCVLln#VgxiHJ4`u7X6dx-6S%j0yz$%h1ojrTeoG zgo!9~mPMDMDTq2}bNkPIspn4dd8=4#DaTy3BbCTwSIQD2x02K2^9Iepe8a~k7etp| zjU*#eimk-e;k=75-qz5fE$#lcXlJ3P9zLk*K_=h0Rj0@4shPc5pg#7H!XMutfY|V@v>yNb@Y#xDV$>1jh>Cm?g;vsj$Q5hcM0$`Y zKt&K0B_H1izsMUO6@QT1Jk$RjKM-HIdGyGSnxeRO!a02e6*T$DKgyNrH{fi)JgL$VFI;t(JcTVxQ6 z|292PWSp@xG$pk6d7>+L-IbnW8~8+wvp#|)UwPXC)eZE5-sEm_L4SLRYS?yc;T+_O2MBKl#ny^WKqX;O|| zr?b7`obiKV&2o`Tq>A_!V$?G}>h>5e1KaSmO-Ykcr}E$} zI`mAQD|goGa&Vo7TZn8Gz)ql{P>lQa&?${0CD{*+QZccXUoPH~HE-U=E?@>fjIiG) zu@JMrGof%Vl)=>`Lo7gC+8nlvx;btI>y!75*V%i|!`X3Gc4giE&x^tX)l=ew_KxQ= zivwc*2Et|7W|kM{2Nts=Va(-tN1}K6)!g zyl^eil~WBXLZ=~gWvK$C1PlVy&KEfni_`KGi__be5u#QOse9tvU*QC3t2xu`b1oH8 zwlu&6#}v{_e^M#N#l3r^mL1|u0umSAm|qP1a7nGQIoY!G7*?dkOzK-}J6?QEoSQ>* zwtErsBmuvuf5){&8%c-jW$>OJCR6)pEQV?6QE&y)L1LQ$jJ0}dLw`l>N7Q_a4A=fq z1Z{X7u%a0xK;ge~dk(V`F&%4Q*DH2GWt?AZFR`GLD+0+OkB>$JsVG2ASTVS5kEIqP zX256u;wvd*>cGxzZDXvY4|N7OhAJ8rohQHu4EtshpLYhcgSOA#=InIXlofEZwGO1O zyU_RaKBg~Lxfc~Vejv%&eK_U)ydTDGjHW2!_dTS>y=Qys!)!WtF*wt3JJ=lCWYYte zsxhUlPdPZG{B|s_m`6*UxMJD>-?-ifKP)^VJ8pE7QYXJrtX}^-zHqLpX*>G-I{nx- zco$Q&YeSCfUF@6}r8^W%3i{lcRv^)Z8I=S!w7|wM4N2rh@I68~x_=Gc z?3Fmnom>K#RYsdLylu7NI=r0qWlNt|;o)CZNF0+t}56I$&jkR-7! zq08gT>PdvM3BGb?O|!93y6^G;GApU4t|RmbBXopJs5a(DL( zY3D3CRYfvLd#BKQ4+_KIaZ#>|kr}CWDN#-wNsI(!b`P)1SGamZFK_CAVixbuZd0ZY zHmi^PrVnPMZI-Hxxnp^^Z0QPf`JKn?dnr7}iC!H$G~KpP>{oh@8_wV1#_H`Piu4RfN1OIX?$!gM&=Vv&7gJa1d#!w1d)u4)DR{~l=5lSm`L`N93H*! z!aYXgchGMFOmK!^QZ(dBKl7k(t)-C%dcsI_0_a4!u_Z7Kx-)3N=;o9mwpClTMP@)8 zgchHFIXd$Fw#AB$)c?h4r3!u~c*icY^NA4lJs=E6lGbV~_jauU9#Ht`>;AEyfBE5J z+0d+g8;nG)#J8#Y*p=TTxXy8gZkJ5)?@; zJQpcR_jW&6mwQR1-85=+GGS&?iPv#rFpCJoC@!ucdQ~HP%0>zoUa`Tv??R~m5fFHX zJfmTK#soG!jt<|y(c?NgFha;>y85K!5N&xAr<%F$iCb{UJ36tsuYukd7|I~j0=-Fspf49#3crKk^ zv^t+YJ+^8p#Xi3{zWn@u082o$zhe5g-+$wZAe;|CdyFU^HNSg7HyPJW zlY`_D^|ba8g(O8fu{tm4CT@4Ye6x1MUCPu;QUAH50W*dOZ&0 zdzF0ayoER{uP#`&+`wb<%VFR0jmx(!r#eO%yoZa9vjiMntoKAn!tZE zRy0=B|7Wvd9-tE02s)`>A@c{$rABq3%cHAXSu?wA`3hEUS>PmAf>R~l991VKvrhW- z_HMG*P4>E%sMxPV0{!Z$cIEQ{QK;Sm#umU%s=x!n1{AJ+6rghKj@bg8JYU^zkz@e zEV0K1{^+Ggky$W+ZFAiY#|DrIuBOBCIQ7YaruKBW1vLz7(~@*}K@agSAuIeqOizak z!v*QlgZ9ALEtD^}tUqh)LsE6bS7?1K47J@r`z zd_KSEj7JM998&kGlsXQ}hR~kQ>G6(2I~;1?*G{$LK5y^J?)Z`=k>37ZYP5H(mjbK$A)Fix$07AMP$!}WI%!Ex4CbsAgoC+byKBXI^N8DNh)`rkW=k)x4~ zeRf9KISaojZWHWUED^UwgU}%~IkfXFd}QG3khtNIhC@`GzhmY%TwTNu&$7OAw%6Hi z_B{LlQ*$DUuz$Uj-B4U{i|49)7cUx)dhM3B3#PkVv!XtWDUe)IzGewUc{=A$w=Ahx z&CyI>TX|_#X<0lytJ>-(^Cyx5bnDc~^+KX~{o0!rFJ9hx%k<_Isy7mk`-uPS?xCDf zUTD>)7ZV>rJnp;%^|ctGO#8HFbz2}F56oH)FMBW(Jv-OJh7_&8AS^3VXR)kIu&h9! zyQMy*k`uf!3<^2C7YoJH904VcAOc4af%6h~oR^@ymuSr18G!Q6Brdrd-|J_+9$qLPwG==sB zp_!4*{uxHDt8cYo9PuT-y8qQvht;(6I?uUIig58azq3HVh{Q7pUL zB5Taw;d}tK!(I||fp@togm3Bw>Mqk2nZ0hHb8l4hFV-0z%2ln9knmssV)|0`^ z+|%!dZk##k+Q`9e9BMexWw0Z!<4p|sh zxfouY$q2pQrwgC}!lW!hLs+fL94B_#1GsrPWY57I0~kPcp}p$d;jgKKkE z9l%qlsM>%&a(WUkNq~E_cQw>Af|;=~!}aVe*O?gyH!ohoB z44~nGSzt*^j|vy}l-mqyj*4W}Y*SYdUX;H$2$rTVR>3^qoFE`Z8kwMQt~fsyT6c92 z!;a(?;u%ml@@dK8T;#GS;h74jo{{RIPnc-r#nxfN%_4&@*2w=LlL&m|-`Rx3x<90y zoCWJMjv7Hy$eh22_RGswAKi1?;LEy$X3Vsp-`H{FvAGK}k!T?}^z#d98$bK?kAHB- zVym;vt}W>m=;>KMx75Gnih0H9kMf1i^)DWHs#tpX-{3{*`v&ehVwf4;=eL-dg+rr< z+=+_Ysj?>8%-e@98D96*m91^EtX;^ji?l>ymr>g{-~8yx3rB9=z52pWZY{3T3h~+7 z7M8qT6NBu>xo1w8%;tMR1Z1hj4GkcXCz&W|MkW>;S5I{!mK;z+UJH@%R+4_}B)UG4 z_Nv^_^FK0(y{D37b2W%lQ$l#s4%!PvBrL2&m%%%z)n#QLFheFT@4Th*t#ELFs^%AM-C2BmnLlO>9iSJ*f>PmY) zmnLJ#!!gSlCZB8M-Oi+~AfbvC1!Qp?tzypui34B{E@I0C3rcy+^%v^7(qR4;(=Fz^ zO{4j}`H4Jhb&l&A`;P^*B+|nngvL9J3&a4pi2C~-a zQ#qzv6A--}&Z(pffi78hmT+P^E9ft3UZyqSJnE z`YHGjcoq1-QX@G)4fu|DY2G)e98+i>0@h>_I0+n*44c)|DR?5DQ4j4Ihs}mHBCHiC zL6N`y?ZyfKQ#CT(Q>r_j>Y*1`eHj*_>(AuTV~F_NRl_{XS~bbt(z&?p!mD;pKb2|P z+3&LRZoaeFGXMIuS3OhrAIt{8w2!R4@}EZMYPgzO9aEo68g&e39*ZKa88r?sKBt6(Kd7FJrcH>LTVWlUlU&=$)mNppE(hW z{kI4Mewz#%bD6#=B&@E;Rm)xqB>M7GKPaq-d%v|dRqA0AqIt>m%gf@O?H`}s8fn(F zk~(6tIb7FX3TM~fD|O}sJw<;FXjGwrWZopPWw=Iw(yFE!zKKSrh#GES)(OK!(8^B+ z`X%BAk!F$#q?x$~sBxMsZ6L*GrW!b8jj^f@nF(baRu*Ti9>hq;SpdQ^ocV@?BoekK z8><@B-4qzx&I7fTY(1Tq<}6m#Dm&tu4_&o>Nh{L-}yCphP~kX$N> z;}R+{Csd{mbqNW_VDS)XW+emMARY)wN+!+0*-yrjoTi(y1Y&eex!8btfTS~A8ZVK*D*=BidL#ErvwhRcEwOHs)4 zI4o9+)fy#$QNJ9IP;zxF5ziZ4zjiO?oi^kKk)W*at#x38O&Y<8A0?`=|LqR|c1`J)Wrq6^?>&P@SQwyOM zTaGs7Z=tb zuI|Kysa31EVJe`j`AnXc%OZb*_NO(@N za$)H8!L9V?JvVv3OyBMO2K|8lY5E14x6uw06QrZwbSoaCud?(n6vRD4^M!xv6??&O?WjX zz-NKYg9=VDXQ9Ps&;GYb9wbJI!2r8bsF|3Pk1^H5Oj5i)ISDCOrhkq^X218%?Z4c* zdGev#f3hWV<@Bi+ruQDc`w*ygtX9XDzMT8@Phn+yb*4Yugh!RMoOkVbp6K&P)Fmz8p% z+S=E;0o)8W%UhI9g}cEH`HsRa@L=Id@OWWQ>%P{X`CgMJT7T_(TRzr0>HD4hJLP!m zhv09%zZA5EaFK6*el;BMt;m1Yca!pp{G-B~@|%U<%D*i*>d_!I67WZxa*;qP+C<@{ zwPN-7P&5)qMq@a537{Lu3V@0%<2BG`7xM0c>?`DDL~rQceSSspQ9Q>1P$(o*Twx6W zl#*IQ314uWcXzo-tY; zl%8Opv^*)AZiHLd?eH$vG>5aN0PRUJyxbL`^E8ExK9w%e25qJng-9|;wXB8=q|7?I zL|)}7fsgPMzbM#XMa+r4;;{ZJC`$ICy<{(kilI`d zjA1ovQp0K_oz50#!`W2TR8R`xLZlGApmafbp?zWVGV2QaN^wPcg&vB)2o)$s%7M0J z@-k(af1vf!;-#fa%a^vTZgP3;qdbz+*48>b~hun$>wm|Y!2Pl z>@X#GQZX?&g-|NhLdQ}Vt)UTaDnS^*L0uR##Juw*C-trqs?NR6cb&9zoZ9|eG1iZ7DVs*_C1$vyLfhS@zyJ^ z``X11zssAeNQnRQSHCiN`|8#!GvOa@x@Xx}e^FK<`9`b|>7=xO=2RNQ@ilXzZZsK%yyBKFPiC9StMS3X6sHCCd z>m1e3{ja-f^6wlKi5+Y3(o)MRd5uCVc;=3^+=P+ma!=Wj+1)&BBI{lamPoC))a!7%UL&iT)X8MMj>YP=(@AQ>P=Y0) z=@u{oN2nq5E$Xf2JDfY4_)e~5Dan<h!gPCQ)iZ!P2MIa?X<69~x1;Y)|a zCOFzue;a5Nt`7;WQWP&WpoltR#?xdaQ2&(to`Tp@KJA~x^@u!KPtqp3EnaV6dt-LE z83|R~_8 zfVM_%7_85<`@2)h@r?2q#FJB!PPjLiOnd(2O&@1Pu%(yWvEcLz>FLAXRK(dze@#oM zvBES%**iiGUa)GK$r+yi$scHQTVCWj!Y^rL8$XA1G!^VK0wNHRWKyBgw351tx{&u-^gwje$M4M~$5B^ayHXWxt+%cF`WH698`ma=)D|3@1+RajH*$-DtKe?k;0Sm(}fob$IILbWr!JKw{hF}QD&6g z$?fF%cq9;tHffQ79*uFtpT|WVjtIgMk}?^^J#3VsB20h{iUA779D)!kg2(h6PNb%? zDG)8wYQFw`Mi8;E~K=3TvRtSMT9&M3Afiq5+K zAxaEB5)NuFHon`r4~Oj%)PhgL=1s{GqiGI@bjkARH^o%Pd)IF$%w>Y=boc6{PtDKXI{mcf6ODEB@U7vLHvO;H^m_zA z)^(qvTxTZ+U?Q)zGD=OV~Y9>v~Q@y(+%t_po- z2}b=DBsmU!-47WB>X>?}?Pa^Z-(G7o^nQIzf5P!ZXphdQXlGQXMbvXlr~O;+iZ>X8Hu|C zKPf8Xk;oWcVguq8Jx{5$suH<3e&)}Hoj5-BH|zf##R=3T>#X4?z*KaRh;s&dPwSH- z;$)3DIqE`oA9FNtfUK-IIWmBK2=!F6iljHv20MoVb63~st9#b0YKi&-&dZ~6&U3i`bCC5+R!f&(Xi7&W$*`f06rmerj!<81O_Vx z)oMQPtB6y_(BH&)TE=0JIP<ZP3EO&~=sz9j=Q;06t9)*DhV zWvyR5lgUP-x$N1cVbEW#S|e-2kkw(Z|CGp~z%vqAwk%jJdy}fDI<=&lTbsL@AIg28 zeUSP{`zU3@1NOVhWZbKPNGY1jrPsHG6eSXfi8+%c5lUndmBe!2cYNQGzmwpsT6?@b z*$0-uUUm_;AU;3Yo9a#9#g2-j&M#|srS3|P<{lQmh6igeh=;Yqsh4uEYOki=*4|DX z%SC|MgiJ+~kJs2FpJLKw-yCs{v)_CPyF$Jsy~Dawyi4Ap?1blvVvi z_>2XP!)FdmfX}33I8N(9hIqdM_bBj1<#pvlmd!;)W{fJR~z=QzWY2l%bYJoh|Oj5ruDEGkIB(bTCFg@dgJ(W$9DELmctKMhCI z5a$D^X(PQaJ&`_^X48Y&vnwx6;_M&AKRI;*nH$3mZ3uPwQ3pCKpYTtLr|~wk6gys* z9q})YcFruKR&P(%4H~2`om*9hn^B;+^LzCFea-g2zvK?iIH)HaASD1#GC!EWP7J)T zAr6tdux9oDm-nTCQB`Nd=bU@*?8}{*WR^^lxigblvd(0KOo-$L61IRu)&xb8kU)Y+ zLKYB&5?3^+P^;DzuogG8OcKF>R?tuFqP}8l-QKohi&blFsin3SlKGx`Fw&@TX4nx6P$Qk$hSFmbB)NiXsT5R;iRMudtrY5& zv|~S~U|vQ%Q0}$7{7RWKWUs={%*jsEoP04Y6vN)-x#Rzl`|0@8wb5kT6m7{I!JpVbFJN7I`=9U^28m2YzDMxusy*OK* z`9mICo3cBzcL+OFJF;%$Z_iSflXCA;ZmG1~dsXHpVN2#tGMF)#`3U#8mhy(0ksq5y zi`ABHcWBa#gyTkWrfiYCX%fdvF!N<{Y9}v%>%I|5^4gB!uaT3p$$%nYAdP+p@cb?ks_d!NY2=npgOQC!Gx?Pl0JOVs6W&;~h;{`@l`hZ@jE`%hs+Ue?}-$ zzo36;@2;z#$GkBA$s?h?w~StOWFRzkaY<&r=q(%CaqX`v3)RF-_n8`@Erx^++ZqjWwU9WvV@L52m;_!BEE5Fr#lY_6)gjE9PcZFRlPFkhGno&ISh!lk0Vuta4 z`-l?>R3!R{*@&TXW!grvRiAtjz38qOt^x!GPA^ zianyc6?shn<>-Q~IMXV%BA)>?G_hlq$M))c z&C=KV_&2pUH@6~JE^97(v+PvaKXhtThHJDt{9B8j%sHBKtmtP&ANoHG{AbZ0eV+vk zb2Yi6__mR7SVW`b^vKC5j*fC=hdDvCW9ZiV4rj^vk+LjkyAjct8-5OVpfscgrMQF^rcoyua*387V`fc>1%l z?YNEDT&1)XKb;VGt_+Tq8_!bhw8$DWpPmh`WWz++>M4q32X$s%<@5TzIo^PmR|x@A zFsPGSafB~y#o1K>mIKSWff?IYL$NP`i1osS>c*&-IX^kT)X2@1s^|0O)cV6Dz}uJDU#Qmpqp*P?3Pnck{M8$ErOv7{C*aTePX&$tMIg&?!WyP zKiGA!;lMJpF(HhF$9`Q>`qSg_ zAG_0(y8*lb`p{JFc#@*a&tNpJi|G<0d@ah0B}@kwN|V{C9;6aE*~Ku*U7xR9TAvas z^5Eokb;dI5Wz4ePBE}|LWT)g_QHnLTD8;i@lwzL+U|hL#RwQDJhY#u;8G$fMh_bd( zAqhcL4n5R5h4s)WQ&G^Bl>0$tMrsX4MsRY!8`33ZMkX({kL8~@apFSfO|quGD!%+G zMWTS}&QG2*xaFhfm?uW8DiOo~_q2EG_UQK*9x&f;dBA$VXJ2eYr;E8_8LPxqmQ|kh zVwa`M^8nF)p8bV~3}~-2y~@36{)~KP{=)JXtESpg?X31xNY$}f<{n+YSrZ|7q7;yV zkyr(;5Y_48QoKl9BJqB4IbLr5gZOt*m}{Bic}e?{?)N&up|y*iERUy-%rL9;7PHNm zVaPIPn>?z;+){qA&?GLgEU~Fv=B%u2&tk$S$6;iOlTn#iXPrR1U(rR1U(6~!th$%CLEW#N5eWl|;tV>5v&TPm6{0j$X; zx;z=#uAlR4}KyTuz-nY8Wt(GX{LpaLPcs4JWAFCWm966FW0J zF&y*e6rr3*1Qm%zarXR<&`^X2iuM$dqUMU&D86RIONB}RvAwZpC4JdN{Nhrvhn@=3 z^M#cgt0vugrMS6^7MU^+LdOxSsu_4I6{ zI9Lze--JK35p87mT}3L$)~c`egsm|QDocir7dFQT<5`L6sbPtk6+^2S^d9z*CAHJ3 zblD~<)!4$g{gcSHO0Bjr-oMh+J4`tBLd`b?U@ZJ>oy54M(GSc%bSQPF!zwzx_tHF?#MI?z7O?!X*-D|xOlP)I*r`sH* zc{_fOXDYqVI|8)F>9ItAVSojsx{+2 zGhSuxG81!@=uq`o&&ridQ)Epu22i)GMPG?xO?oyJuA3v52*NYEY&~x>n{zlmjpKMu zPk1vnm>fo$3f~}LA!;4xQ zrQn_;))?{nsFk*Nng*Oc$4V~WSWL5aXqTXbHl6f<)JG|*P(T|ldKJ{ z3w4xUi?@X~6>KY|JnsYQ0nPQ6>#e)fcH8ge@6p_4xySl|{n6mlp{Gkn`6HUo^nWyb zX8J7jSxK(is0~%37_KN0YBk7^5#pI9I@pU(Dxr`XHe*(e7Mqb4C>^Sz0E>8yETVF` zoX}el`^!(2^X2~M!Gi;uCl6>IUDP2v_CVirl^w%>P7;#z5va3_i2UO8Sp`#JOQ1vH zgf7XCWcw_3UXvd13jS8)QD?Q{g0#F=RAdEA*vA8EoJ}8n%C{G`qKKtXA#EnkJ{8s& z${mwpMP@hd?Gw)!fLkDW&y-O*wxn!xh3WX#hgZJ-#1DVE{$M;d|AV0y*Du?Gi?5M4 ztywcrUOZ)S!|hkEzdksd9K3P=vKx;N_srjW)h(B<*|__)EiJt(hTd7fePQS3O$*C9 zBIBRTI?#OG16!BRiLHaawq$%B*#0SCji=zmhP>K8jo36>m(SD85jaTP0-3NO=%PBv zfFe4OQw^jy{_x*3fmhnfp&qU2B2vQ)ZiNbiCG6hz-4Bkx`HyS&E%3VPw+U@|^=s}N z-~6lbpO51Wfx6G}Rj>RiKKSVUY`%3TK2mQN=A#&wojS|K6}iDQDrTELYe7OJQyX>j zdMnXu=%P-!hpQ*=w#qK8%BVJIbXv7e7gfd7R+G~fGbCTg7}03U-1N3Kn3-V8p8_yN zTOOIGZQ>iXk7-pwRlcS`A2x(-;f!2&UN}@dMHR~^i_TWns_XT0+)GrA>PAhIuF=q# z(HLD)+^K3)uh(~Ebhxi7-NbKFZBlR2U8BF&aBapl?(Lb^Nd1wU_}estnYTo4iQZa# zr}{qq_iW#D-j{K&`>ycak-MUgYo63Tsedx#arYCMPi8$98BvdDp4E+JJQMv%^lQ!6 z`tw;|OY=G+?a_|nTeWrtniYDE zhbe;7>oe_$FODRGZ1`ElSP0ts1o+>ou{El~C@YYI@l73jIsn z^Mf_0K%1GV(Q0*>neMFYYzD`Aj^v8&VGVs4-J$=UFq=7R$Ryl2ytQ+ytMy zsgk>#bYBw?BxO7E0-)JG>4f-yy02-fqC1;vv8qWqKvqz!m5hH6Gbx&KS~zSsR%cU9 zih|~G0@9^0_%YNe-IqKTru&njlpG1Ve~E3OkRok-vLjaZ$Nc5$?d3DF)AGk}4v)V! z{zlID)rE$%x@q_;XL&^d)_)Y1(ld-Um&=w*#GHz4VC@s9r_*O4$BVv4#(Zc|FW ztZMd%ER*N2=(J4N1)WZp!)_0im6hxC21BV6<}bA?SXN$ES{|_Ownr@3QVt}e-H^52 z)sT&|BY`ge4nOhl_G7;*03=Uo;g`9&q0$DZ?d|2bToBX&ms*`u9!M)M4;bvBP_)#L zR$6L+Zs61!9HpUvOJ5NQI(3|(OkJKyhf;x7J7cLi6K8s$O(I3KL9&t6Vxc@6MTHXe zO(T*EyJ!>!v0Qe=U8h`UTs(E492>6Z$P`qH)OhW1 zc~NK-YeoR?DSeK-h+?RcEErLcO?v!HDc+U&8^1W4diZr^eu^c7-lGuNVnZMG1f-Kb z2$5u+Vx!TN9G2K=jct!O|0JHKXmsQ>t9ol}Wn?9|h|DT(`x8KFjaX#@;z+F8{_@Mz zcv3dOUWLtV{iY7f4j;6(OL0L3*O1VK~_E@fL0hJyrp$bxV(9}U$Cy{jj{`bv?W$;BF z?C2%+cod`mQd}-T0H)|Yz#T#tJ<&UrPI7L1DA()P+iU9Q_=0#!aZd5l?WdQ_iH$cD zx@_{z->WSw9RF30JGkP+kLF!=33pc@)8Q-;eH|UEGt#pH0nX{`d2D=iOEH&|lV)-_ zR=)i5u+_W%`)DF31p>&e7{my!ga3L-T#sL90@Dxs)m z%r--TX3)8lOPnUt0WagwBA^9@*F0;d06I3xg=8b)(g@)=1Z0~Yjbe|SM%i7Pxo&Ph zcZySS&*7)ZYy2qg8v2mtEIF>K*;=-p-MpsD2bn&e-he+B?)cX-;mHX;Zs9$^!)B7W zJbJC^1ru41@D9{Zt~G6nZYke7<+$#cQFArIR=%#t(l%uZSxVZ;0J&A(L++DDjL(>k zl^&bD_wqR}HNI!`cowAscy6u=QJedgmf^lI7r z=*u#*R`1eBaXBfKXUGk8kK#wj0r?0yqKnUX1N|DE#BY;#xX;n&_zeEL?u_9}qtjk$ zFDomFmMy{ep}UOtl-ymGLV3IqvoE_UyRJ4JrALV=inx%|?s7X-PE9VLEYm_1?E#dB zX>>~2$dI1MSe09j0WRrQQF&HgUxaakSJhnNt`d0OEtr{nD-YucpH^1so(ApjGa1uN zCSwgk6q8vJEdxxZ3}ItgjZhawHD!FI5fgezSgkg7n~s}^DX8YvYP;R_l(TBuv~W0d zNo8ejZt$s))8SC5gb)!lRo~-HfFAJ!0`3Odew0+m2HDtPBm+hqH)7)``C2ZBm>KVn znX&np+1{er=gDR}qDiq@jQ1ydZ*AT4xQdAla-na$*i6rZ>0J*t;R2;!EvDvP@xo8B zuo)HVimuAZS$NV*DoHdbDP!#-i!!sqq}oIIPa~y;HIW&GGb#TmLwj^noPg};q%>s; z6l1bzqSDnq9X7=fo##v`f#PVAK=CA6#OOMle92om5M;dT-{f9TvW~F}QR$W`Q;{+W zQ8yEzPs6t1v)BG;`Pj9UrMB|%0;5jKj~0!+5M2^<8g-t6Xq3z-iWGR9Y5h2Nx;wAL zgTE}8(=p$En4B3mU)uyoP{`>Fmf<(Y>(_c+R%akkEN$i1uk|@8>=5O)@fO>9EM6YY zcKGQt7}c}Vjh(z@csK^Lp)K&rkb>hAnq3}$CYsCIlFkGMl93+3atlh|ZNkahWB zx}C`kRAF^?@C?{>b#2lg`%7xdl%--$!|0_WHeNw7gaqk8tCW-zblPsATeU;ILo*-@sCKJ&1DUGR<`K2m0O)a0Hfr4JEV@egR9-@; zORnHnnb=JtSW|*te||w&r!nvX3|&9;U`HYH2T@q0d_3frasUW9Vs~gl`QfJ!<>HCv z%2din?jDuOqs9x>__!LYNAY2~2$|UYpM0!9c@xK%oyF$=EasTSx_{Q*qJI+(#xD9% zW}vat3Jdg~S5B~dUf7AwPx5-pA3{qZCJSh~;%b6%*f z_8&0X3!=qi$C9%E^cL=~Mt??K`#OP)eO})+UcPW%V0_s{b0~wy-&xfZrgMP=bmdW@ z1L=?vy(2vr&pgZ+=pKKJcN~=++Gy-oh@Xz77fPHE38dg}P`+k?~n7Qe+?j-iGvm^*spz}QQqf5Ud(l5ZnY zd36s#AFjijmD%yx*=z|xfziZQ?CF$18NHrsrC=HB6=kf4l-cOR-8S}6BKEO)(#Lv| zkItd2EBRPJUq%fT|C0r(08|825i%(6W1wnKRaC<8B`7DS=n_(tsRIOyZq>yiZzz(1 zsHV)LCzRW|zsWyA@9N4MyJzp8eRB4x*}QG`-b{H)1AxTpaqGQ4AC*w+EAzOEyuLb* zdpf(9r}O%49=F@;4|v>#UVpjAeTml(HQ>+5aZkVG61`qWiV6!eGu;}i%|~P({?Lad zU)0y_+wVK+JLOaPMu{Y6h_jn#pP0=_v+?Y@fUmrvthtPo?VUvzx&`7{y1Z41^xnWl zQgf82zd=1kf3M#e0aVm?TqG-QQxgVkxH9DYfR2F)%ij(D1jZW{kSP-2H)l|-=ve~>( z#j7$Rwrbl#8)vhWNFo;R(73F&E#9kEGW?aEOkFCj{KHCiKY0P;SceAtLsF!GGO*~O zr}7w_BR1)(e14DeVvT{KlT?B)5Yhh}Qtm)DVRm%nNFFD#tMm7tzljtK+*W;Cf-^P@`=_Dm74SM}&p^DxUB! zaPuJm$AzHEQDQ)zR#}eF03r>DJ|lJ#ok9Wv>>ri|5#y5>N3o3eV-8^vORzLwp;hxd zkbPHZ8re}m>M2_`kF)J~n3bVlenrJk6i2DJarz7JQ_ua3Dz10Y*+ndmmnBYfkHh@w zKp||9ORo*RFTCq}Kg4(NTZHYJt=i3oYm8fLo26Zv8*DnQW_K=`rV&C;Z^$Wd*#RD@ zg=2U%a$@m_vh1E$WIPt9VJa2NpnqeJz z?i`K95EKKML4VL=^tPfbb4Dx9(EudrvsP%zVo-QKKueHwa(bGpy}qOnpH*L$qbNwoO7uC zH_s05dSdY4x!;X%xo#afdi|v>A6>J5?=LqmD`4Z!n)pcg5ykt7^%QoDttD7{J>#j64r3c)jnt*yFFb{sVLoDZg)_|0gu^iG9hR+ z?KuZ_d=jcuJp5hiO&0Y`Cik$bSZdVRH6o=tT zg}aDs&uNSb8KuworbJrdjTdc^{U zqjCWJRx7NB#Ao%TS*tc&V5~HFlaw4TKsX#I@ z#VtkwCf--v%LMa@)#Y+7mU1zj^+>KQjU9@4y-^=N;REc8VOxx;%%_M`qj;Dr-137I z@(#!(W#~&u_5}xk|GDEhFYjs(30ey{G7ovIA zRcM8^3$3$mwLXBKz|Y~s*4OaAu=Otl)05>U1kI&vgOkMLbkCcz`;6F))mGYxVcL~5 z>6Y-P!%4%A0K}dp z=e&60#-Fl>;Qyvn0(PCujk~!^sDuIh9+f?ibN+gFaA9;jOa%{2yJT9{G-3XEHD^kR z7C3h+Uwi(?6M_Z~)fL#ZbTH5dJekU*3pAq7fpZiBlSh992N?R4B0#xl3Yvy4L30tz zf@nEffi|G6i3EfQqXJX`J~gNwEket{rv-I^j|N@zm-ro0FfWAc^adgz($mo#G#{Y( zXbsDO72L`pT@H|fJX8rW=b{LNDnl}SXGb1by#pwQ(jg~PAwM}l71aPGOHdl}p)6F5 zN+8vIppe|ifJ}hnS%At3LjG(}4i_@Rh_8V1sF4=J@%NHnVFfHnZ`I~=FhYD*~va9$;!RS-?UfvYDuM1 zm891yJ*t*&qdW@lsu)$@wD%Nh-vhQxJA@{dvcga@PQSECw<|besx=_s7LDfKM=$04 zB;;IrxTEW*N;pSfX( zN&`NI_e}$Rb>ROQr)I#?JlCwr&*HBEbDWi@kN>6>de{bF8{qWAmr`LXdR@;|5O+75 z{c#ceyntCcbPc*t=myx{^pXvq4|c{_uSsB;D=>?gM`m$|Wwd&C3OLFg%7PA-V8T4` z7M_rpmFT+*s8h5lg2NrZYb=38_JtR4s4)782)Qe-(uQ~-6rg1|9}y~VwA?W~V(PVG zAZ2MbRJU}nowMHLJBxs0BLVY+^^soexqlr53o~EiB2j%laL&&Mfto#tie|(ds_hrF z&^l+7oJPjLMZQi>EJTeDMx{f@z}9pKYmkp9gii2RHcW(7DE+Do;FE8GF1~93fnB)Q z-bAqqZ2azAfSstL?o|7>=$;;2o*v-eji+wPe~jaY$fG`U+zJD6GXm{_m5e@4QfW#B1Hzf2z_@<~5>!V)p1QVBwq>=WT$)UWF zLt4&EG?PJo8F_omLVjyW?{dtYN_)y8)(3j*;(~N6=EE0H&O#C41Irtn=Q%SI331ES7p;t>Q z7X(!~k@HLBRMIPeL;cOM=1{|NxkltB=tpPNa z{$OOcR(Nz$frqln+CMkgsx+=nD=6qsVWEgLMN04MePCjb>0zU9j_uhhs5Vh}XGxf4 zlyYFpAH4<{A0?3^8pxKcR&mO9p-0cARbyzC!&~)lO&OD_z5|fDL;Ku1lOTLKy%X0P zshl*!jK*^-#66_~XWRv%1M-#?q6Or&%A^vh6=i&aS_!?mflmtQQ6URW&Wk#V6+-aM zeOh^Y5wvXbL-M^7v-bKU89w!mDDv|3MMQud2=_HKU|IO)G?R$u=SWK zyl>1U$xvW}z~R5a!%CqF7}RtWDN8YR7O zr>CF3f1T;TL)KfF1ypeobX!1xj-ZPxP+nESo;Hc?Olp<9ov1u71U0kf z3;0+jsa>*a9Aw^acH)xbC@tjg@7#nn7uH}0auFnU-j|tc-Z}}CT5G4Vm0wt-q>~%9 z3XfQL#DM%f^UvqIxHvt(NLXEEn;8wF$2mJ~>yjr-_HA31v+|Q^0_5~U#|d4Az_G?z z<~2;P8|7uGXr!1Uaph&}e@o_qJ0W+C^srFSVs<(Vd?P}0eGxWq&_($Y5i@lV@#OB! z>H~Si0I>lS5CF$zOmNg~|4P4Y@5ET_cgam3%3941K+*<0?#>=7;VmyR$vz>Wsc zF0keVyv6h(c>qOhLo^3Hz>SbiQB^)P9xxax7^PiXz|&E`mX0rGt!leW)2A~uVR;A= z;AQ7A(+usnbYSUjq^W{Xc0Bp*T=qa_U3R-spMZ@Agjb{^k{};9PvAQMvkt@=-xlbJ zJom<{o>_|T+wRaEHTgI zN05$g!{Q^HBX=k0eK(8@eBpH|eco|0{%jj_(55`gnV03@2o4TJiE*)Y@XDARI&q$F ziHd?e*M6KjK{lfBg4XQ;GI?Ew+_XdRI{}v-&>!k<@YgEvur;W#e3N)VwpjwA_6)S7 zaJ#;%4&AS})Pb?T@K?bS$-DFbmoV=^ z`ko7<8;-uy;KtSo9<%#!^&%wPZbbA%gnp>A=RF~d7$`*2Zv_5p6}ZEQV|?@TM4s%% zi@L0x)W`vmA!eI1Vvu{YfNn3Mfs=~+~tpO%woA-h7mqCY5}^FHFfSBuiDPqiMdq074^J?PN2w79Vf0E)^I?6 zJtrg^;o4WEP0*Y;+XmZi`oY!PF-4Slh%5m4dH~TDIHXin(s#mfqB3!M*o86n#S-h~ zj2&wVqs#L(^9p^4SDaeX%I_5%AdSjD{X9@>6Wr?(;59Qa(+#8}Tu1bxw%t1n5-od( zs|L?q5U^f@B{S-<*A}o7##?fy3A@Ty7use3=W@*t{POM*%#)}SXKTXyL0|=Lo9q(t zkvk%$=R);S@D==3^p(UiVH>g=QCa0oqZx>i#) zGF=dsVjRi3`#Ib>dM}n|aKa_HTkPq6rNr0X3Br&4)=NeQ>=*VJxseL~rW_C+F&;wG z5i(Cs9RZ#`{r9#<_1S?5VoH&d_juVuTLd1^-bd~1?WR5CebjTX^Wgp%`GRV6AM#%$ zwgh3k!d6Fu7zQ*e8MRJ_5~PCmx(FpojLhHy$%X=~=I^Sv?Qk z-D zMZnaFE;sn%L$LcW>Z9CMoFj>bFm?TO(<CqJqf_!>kKOB?qPOq|NOsIQ?IsJOS-nB$k1*DW< z9&Bk}S_)G2+~c<@I;%aBXoTWGT+Ke}9R2JPQSD8=@etz$R_1?K$qsn}65$;e{<0GR zK6HqW>CKk6M9;rQFg2&2&UWdjchEsr11GTzX*utyhU^o}iM%DslyingVk7oTpbQC~ ztkQSD$$r;9*@ulJ*pFw8f;rAufJYb1@z>PlST*h&*(T`CJ@zf&xG3N+A)i3J4eb=# zuOVPhuKMTYGV4i?s1hO1m;A5|y_D`)XivO?9TPh;*|S%YBXS04OR!Y)AxE|ShLG@! zInn*@$=|X>!RLX-xq-m z>SNqRTOSZ>_84#;7{&E-kIHI8P7?t8d1W}nd?y4?+bE)s$Yzc#P<|$LM*D^F=2Q|Q=h@k8?OC~=u%0y$;rZ2cTeF*{ z2CW&4Lyc53tRzpdCf4(1_tkGbP;r{3tmo;!P7lCN(=(77;NZ@`@4nDke#R8IDp(-w#$TDzA?Sx1e~Vk=re-t zna!YG*og#tGTTEL&x;30V!ds;HLPvTE2MFJ4+cZG#>YiSB+$w=q|~ z^oy0uqV-#RgjKlDK%g#CZ#cM;TRJ?S^u(Rul4gBw9A|eILh{e5Q>#p1LD*}IrU|S4 z^?Y%^&@@AN{*Mj=LK6#f=IQ1~-|)MSM)?LXPI||l+4AP)uc9+)PC6GKmw7Y&pQ9BM zt!EOoVmsr9!ApI_FLdK6`eFXyC;SrKS_3;d*SIa}`@)29OUfQbN_yf8nNF5+kK z7>6jWJv~=#$I>TJ{9e8 zD_`@>$-!T-Vk96&#uMdKPsC`D8oFD-^9+*B)Z8BAF>?S9#+blObh*P39q01b={2|| zP}t;D;IMg zB=o>4l#~IU!@{dTzS@~!)uv$Mg*ri;$1v@CU>`%bI>R_VJ-UA%~~>Or7)0UzmY%L z8o}gC;0jm+(oc7S4jp$HuvQ2O11*;#KHk3dz3eOi=lGT_h-X~6my)P8n7d@?mexc~ zHtc&oGEFX|1j*7YU=TdPdP@;4b&@u^<$BIl#2#i6X(rgUNzXOWda#pKp7^+eWQ3Da zEaWCEZRqf9=4nU1M88td0mrfsN5V|Q^@1Y{*m&to93$|ifGvW{84WA$$;50hSbtjc zpHT9sAj5a%rM-%24I8U~G@@Hnp%%v00V+p88un~MGi-3BEkWy)GTD*3C!-3ZYK)r7 z>aC((x_C=j(^>P-_6v{;;tLcDn2Fh!imb({${VGC3%Cp3i7qh~F{#X_ZFj)#GV_wp z#X5^@N1!oS7ga4`mIY(5M});TavpqFazka^+Pjq?4Nw|}s7eEg+tGZYddU14Ade`? zK+EF@lQ28u+ZJG|OwEMTC9=T9CUP6%x(k7_CttAMVeSA?fpn3ZXA^1_v$Jv#iofYG z`AOX0so-K%v}XO)GlO& z_INndXenze629=&X;hTGmI0OZmcdT^`fHRt#iBAr z1diWc#jg{7J|vrcACMcU6iW%6JQEve0~pyJM2V$1C|ku3X*;6m_!g*@>{xW;fq@rF z-p3)17aY{bqPf;de*!4kLDg>+JUpN|3+SUr%?y;Y|91y6IEPNyd0=vdKPs_hzlY5@ zP$71PFSd{?<8ux`noe7HP$Nv5d*pxcJppruK$)uW-Ey$u_=e>z8&}i}w4#=N&hNsd zC!`V~QjQn~=P^#JMZmvcs zB)sZRcfA}_Fc}!t1z!dpEtW5lH9MspdastvCmxxHS4+t>R*z*n>2sN>PM&B}KTFXf!>yBq;`{Ma{QtLBNB3a`g)%SmXvO~JOz~!^hzBrwQri@ zk_W&fy*$onE5u`B5&X8qPhDyXRK)#J>>iDV{f3hk^5E|M@%6hcrkYR4AspUqyYqLI zrRu{8e|c>7-6!}D$4pfRk*iw$sW5+eST}4s43fucQwT~9IDJT-T@Iz{#hd!9C)~>b zI_A61Qh_Ppcht-mAU(W$E!E4f6JV`a674YUX;6h82dX%?<4}?N?ZS4 z${kK0z3$YOmbzxs+ED_zG$dOta#x}7S~egDhog7s?-Pu?!luksRxw9Yqu#KU%R+W1 zzBHPFuFdFbGH_9%yN=<*SVo~E;3uS_=;g^;5E2s~m0wnQPkvk|qPQpDo@iCBDYS95 zxLO#dH!RvpfKi$Y2FvR2$&7KmgNSCu0}Zw@ud2$me?#G?)9YxCf5byq!)KY;?0^p$ zTIPp^5NVW$jiw4WQrMHtOCFa(CThaBJWj~;1tEeu0SpTY1YeBb7Ys8M%NIbSN=Gxl286W-cCSCd&io$5wVyH=JF2JcC4PjkizI z_5nU3>k#4YsEa6za&#CO^J78}8Sy%k7g9H^F*`#EI)v<+uS{}04vWrYTepTCMai>8}ck%7dlG? zl?8l!kl8h3UN&#zlsjp+;I?eGc(w?#!rirI+=MDu4__i;PD9Q=ci^h3Oe$D`Cw*14 z)a9L!rt=4J=cgF?`EWR8ReqJTfNk8r&)4L49Y6`%fbre;9sQ^aq!?Ot7T1ci?vnJ)MBQqwycn`~reP!Xi|P z=GKbNHgv)^hPFoLHl}pqM#eTy=1$*e#OQ>K9SuK=(YJB>oxxEPpMm4E00n$)Iwf0W z8}rW$#`p~Ee?;-{{7sDCQGbacWo%>WWQNbk%uXj_Zsla`Kqq3Q?_?}w{5v-f&tEFI zrEAAd+4SMV3|@SL!fO)*6{oUJAk42=$j81H9lHkrNrx!`6dES5tR4-m{E_H|P;%ZX#I4r*L^ju-dP``_>3%nG}0g4;di4tnA`qsvD|J;{8TiDbQpNa90bO8ZdH%)vRHhdZe=0D58 zXJ_HirW1G4w=y^6w=uOc#;2#_|J~D^ey9AS*E#*Br>9f=R6zxwk@2(le5(A}gNzyR z85#Z%`=2EJ9rc%_|LAfI%pA=BO`khc(Y8`l#{S4!TZ^%_W^0^Dv_3mu2gk9|W>X{J zB#n-OPqeOtfI<3MdA2IRYt!mNk2@@IJMstsAKV`ZzQvapky;&^syA=uNf1W{o;yP> zzn2z=@wwOdWCs;0_{>q-W}4&V#fR7XhvOvo4-CW>Ge2tPII*dz*UP5`61z%722tib z3+?%R@OQBGI3uknP#zjuWO2ah9byb!Nj_p6k)_OWlxTPab$(vxR&Tg#y1Or-_!&R1 zIfl_;pSZL#CxsUPkndHezcHQITYpR&SNb5=$15{kDQ?lChEl^3t_2-w@pDi#;1HF% zL33VAn1zi`Fwyaf>{DM-*L!NUaW=kwF~j|U-QS;cZk4%+|Dj>dJ#*j}Ww{3CKR)p~ zi*VDdju4RLd<_S36Bsal@HD;?%Ox;TZZGJOQBYaeC2a#+&)R^Cqft1fB)ne(8r6K4 zY5e4eR@N24!p1V{Ab2R3HMLr;-wgbW^Gx?W55C!ZIk*J0pgL>izTC7Etz~40b2X){ zr?t_wc+66JiEVsr$Sf7?7&~LX0oK(9%EJoUyGs|#scEU)92t~U-n3LyIvK+lO5bVw zCzV^xLau7I3Yf!DYv*Ca+HIM+aew#ditfu}+p}n93GQo3Da_13T_3dP>*ee)>sw>z zc>gAWDFxw0)$vksMx3{1tg9zhOd_TQPyL)3j@ew{Mr}^z>9~@{KqY7e; z;4kFXk%w9apQOUh*>d+1Xub{ik@<%@D~jG{1!fUeIk2Q*8E(?X?^iz=t%T3hq^zj+ z)=JVEuTI8Lvj@qo4Zz--q^aZ5co=I6BI$nUQ{7)HBd?7k*N<%;oP+bo091Cxaqd^l>dMGc*lQu0}07L0nFsD&{VOdoDl)W zdOc0m@G3Huw{+j+d?#BIGrA}`zZQ~H7Rq`kY}PFE`g<-2NeRFv|ElnwT380Na2tP& zf%Gm)5HI)@Bsh+NS%zOM@1PzH8%pgm(bF_4|4 z2V*o!Vulzj^5}w07OVZz8mO7fahhNnpagp=bG_g(gL#DmRxJyo)G!f=-}c9xSmJbH zn4-fmDCDaQ4L0R(%MSIXwb;VVgympt)wxHb(B zR>`YF`&#iS$Lg3}1}rekg+6ZDacMP)GQ;8|`TOUDq?LEJhFQQS8REXl{AzkR1G$4Q z!$mJkVDccM0!HUI&sFWWIviKlx$0w?n{M91g$lnJeES+`YeRQ1yp>&Srt~XN-5$4@ zv}v5L$T!k07&0yo4E|=j(um=OF3TKU8F!TzH_URzs)lZs{{BR5N@_z*c2)q41B?!MvXI!9eW z>m|dOZQn=M$F*S9_Tb{Q(X0IifOX%5W?0&SG>cM%4;TFTiHHR$@8P;4TvQjrSj+?R zD+lvl;_byT#k!g1C7WhgSIR^^s;L>mhxw8@?^zG`7U3$Um`tcgun!T6&~+%srHXVt z#*w$@b@!2&qUEU)ycX~3@M1NV8&oyc!Qc<+Mx2^>Gj#{Szw z9sDf=UTj+0$HJs!Yv!;@Yoo9c>=A5rP%Lhke(@3^iS&9qh28w11f_T+GEB0tgmUXZ zZnLjKUBJBhVB-Jjh|0+LxA8&2*wp;f?)XN|Z)9s=OhG5>U}Ws@Y5I`=W%^KjnoM?f zR>s!9ha~!c8fTdP#z#P!PR7>3THlIJQCi>8@-tv#Oee^X&p>CW|M}n-SHw5bw{rZn za{k>mVr2dsga3^nzen`H2E6|bAJ)I|QIj>WFgA4hG#*9N)EMx8Cn@0nerLr0(}3XN z`C}OVgFxhy;CF5OK_SA3|J(Nb^PLHw;dgca8TcIX|4iZey#7`Xe>YYCvJu(XSpJ&< zyF)|Mc6|_aQ>?c9OKUC8+ROlyA=kw0wSwcG-2sa|GCm(v{k@=XD|}FHHmy}zVieXy z)kO*c#|`YjOl5i@J$39llj-D7t|kK6%#62}h2xd1u9sGJFIBsbi;d&8-F&b2je)Iq zMbDddy@4CscQGGcl-klD3UB;cRVQ1NDl7T5*7$+;ju!T{=ckdyCAvl#q{ZW3UXq@fru<aSTc=Kw?~VUAR}@;rB)O!dz;7jYhbN|Nfw_^)r702Np2J{7Zv9tm&VgUl%W zq>pr{j&w6_=o4;a4%>wa>7whCDXJQ4>NXrubvPZR&Lnx{U*zLEM)V+^4 zr$aX=FLARQ*^94R*_X_UUH5-MYz|f?txk_0l8>_X!P{B0JFAv#7+G~0;D^hPkz3O4 zE}P&ph64DWxJ^{!ME}H1&5i{ovz#~bNX!`>&hNCopuv{j5_@LWg(aynLOWA0Fvx8q zr4yevh5_Ro|LuMzGMixm*=WcxT--_Ywc7#&Nhw=|ZdfsV^o!CDGC9TqW}_(l0sG0f z7o+nDVr>9yMt%uyknamW-*|LbT;!UhAqb6g9!cg0OzU z>E04lS&li?j8sn?A$OO3xT%nC>IXtoB9Lu@^g{mxZxuolUSSDv=VIVgZMiBwuu8_4 zOd(B_H?h6|ISgUNKrbviN2{Omiv~oOy_Tc=x&Cza9w_~O8Kn8$R-V#&I88ZOzt8G? z;EvhRxR~}%Fzd)vpI3NqykpOmx$z#~HQ`1>9B~AMDcaph!iC$1i}-=$7uH!*{j``) z(?@VxAAj@?%QJObIqhvm$4tq)0U-;;l<>#;>uV^V@EoKbH?= zLl_4v!onEQGFdCMStK1g*L}(9ThU-bovoeGL2I;m{Ad{;pV&{;&>Aq{q@|;R+nF*w z65{=tv4ahdI)9{fC=S8ceVr8&%Skxc{~1rYS8mLXoAAczoVI0Qr07O`^<~0J=}Qz= zev$`4WHb`Z^rw3-5*w-Ih|U)^pIW!&G3ZVP+VYv@9Q-DtdQYSYT9vpEs#Zx{+3}zp zG)MN_@c7hc8xiYu;fP5)NaRwZD>s@DOJ>S+u>fcIS^D9-WSN-)i68URy59Z1>7Tnu z>INY~*fT7uisXP~-ot7HTkE!Tm!zp}*zrxKA4v|7i~ZmnmxiY&pF} zW{1{()gcmTB!E^V-kp)!!7mumTN@!9_|+|;DyEE9fPoC?AY7O#mHPzOn$nJ4-a;KEvWbW&HA8YES&MGt;n_kbZb%Luo=59O{46Dn)w;~NP?(YU2w$&P(=N}E1ZkKl ztV}y`=XsK&j=18Dja4xfK|z8+h8kt6<-qn6Rx6!R4zPh>b;IBxpf)6>$$CcYU=Y>$ zMi*CF4Wy%Iy98#h8RdK32(Vhqo{p6Rl%~Q3xsC4JD_YvGwQPv%*Y%8c=pMm5Amw1Ssmfwk2&1~o8hU#-}e05O~tO++j2 zu^KrPC}8*;x@jcKyjEjCfJr`R=?#t`l}45G?%_D(;&`W!b$K>dY)+^W<-qT}stW>l z9IA`;BB%X^!;pRsD8j)8Q<1h6xpFalq<)5GKM27oR5ocj27Omi8ZLj)AS~EsT#s2~> zTCiz8>&!V42}D@JumUK1{FxXZ(ASQ?%98NZz6au#3b-?dWRpp}L?Dhs^rmO8z|@PD zT$kmKcUav#s+E*OlCM2SEvzQyM~&)Y z1Njx<%rg*ne8YGvO=Jt_n*phvGGAwkCCX*TcYQZ(bh*F<7-xz>DDqLYHkykd*ahU{{b7$!xicclPLDJ(tZo85pj!-|`(Z=!%5K;O6{KHKokjl-Ap_@O|q zF|fjXd1+ za3HJlxiJ+{guOSqO_EJi{zSjfn3if|ITd|Fb)g(Qzb?i?%t_{0W&t8>E~-~|-MsDSEx zzb5M(9?wU)j+Y=ekHv-Kr!*d@cjz`%GsXXW#`1R%^$!eCPT$m+PEOz9_oU<>_O-OJ zk-7dK_}XtIkA;JUmI?oJMo9Y!uPGQi+B!QJ8h_$&f5K&Sf}a4|XWrky+#lBYKbhnI z0df4tT;SipW0B994&9$v-hW-z?}@=@DSyom{$crl&LIA6e(*Vi5O#ACRdo8qfd3#9 zX256q6NdafL7@92w zDV=Fk64NVW`zr+x;8&j){kPY?wJxJ{bCL|G8GxR>qKoyHv&`Xq^p$IK&$zUc^#i?c zu_IRBk|C#vk{w2C)jDs?I_;@_YG2J=Tshi!+p=){a)R`0S*SJ7(R?=V(}tmuU2%VL zzr5YOr**8+Wp=Qy^DcXMHFu@qoQwXrK)-NNHS*~kC4NYMLKt(#q;-)c^$ zL%HD;W7ujI_&!a!Lh?1m(xjcWw74lqPoVi}kfPhY)yt)h+x_+H^MJkn?kV2kj3uR8 z#o?E%{RlG+&xhT!ZSffRy&<(Budh7b{gZnuvMYIHS35C}FH5}4r>-w5on5vp=+P;@ z9=-B;xbj>jt@ZN*FK8I53ST%>=c^&s)0Saiq`#UAuD~K~f1WT+(lF?m4K$T6_@%`8 zk`Y%YSG*L~ruYYRB7@G#lgKdo4CK0lCbLGolL*LEt{q^vtfJuCS7qYj%^Tqx*vYlx zLG;ni(u_T@rq8_yJazSPHb`l|E%XcLSAM;5lSvS=ry7%xAy+S*Ot_3C&Wb(|Y_4Nw zSnY%mU+u)0OUrcyEin-G0&Lx`S!bxQPUMaQY0}Dlj!@S|T2vHDF-klpqx{~CdphFQ zH5z2#Z@G5WZ6Et)<0oS0Uuc@j5F0B%ul;mjnMy9M1_&LyI5oRNf80KXYH<*YiuSO?@Ew8^kfx^ZI(UPRsIQ!Q4a1p1AP#Qn; z%4V&I>D~4RZQlG9pJe0i?zG9rB+^DUZ{^$_D2QM4OWT}-oN#Xf)2ZS!`|HH=sr^7T z-F(hSv}UnFR@YF0V&4l!sKClB8NY#EccWZGfwUKX12ssu)A4)l2KOg(=tGm}!qizlJat`Kud_y_(vA zlBNzhk(g>MLvC1liGcdMcJfit%q00eCr+ZlSf&X`h)^}=UA}(NT)Oj;^jm;fu6JqT zrsmV0I3zxBVsJ24ddMN;9)V_ndtNTj=Cq3;s9TioPDC;l6E18m^)(z*LAa=Zv8B{E->3 zWi${gJi)Y5+inOCuya)c$W5>{v~>6Cvoe0>3gA`}2{&+27}XwL9e*bY5i%{-KEz%zG|oEBUYIF{+x zIMUxq?TFA&s&_6iB2&YHTS#??baJVshjA;pJaY`W%!7JalXp@_Wh5(DTvoR;AMVz* zF^9Bk)aQR~%?X(YLf61p`Qxv4eW?%#6;j8lz$&jCvGV7`P3|Ry0BN^jO1>k7=x0wf zLrO9J;g!M06Phisk{xtlJ~W0c$C#>TO&z2ss7i?d%V$j=6e*;>st2WBh$)o=PKqc+ zaM=o`m|ukqT^Q>H6p^DN*+ytQ*T$J6P7}G)xDkh<2z{39FgV@u5dt%7SegSpr|td@ zB)v#Fz}MWb2-6RwAW#qHTXfrVApZTP?`CZQG1tO6QN7}Xc#s~~+Fz^uqSW{*tQWk}aBDnBzlXd%FW&5YH^`X^YBPhQNW5_GXxe~&Ic+EQ1H65t*?Og#1Ht+y zLu^I;)q50zWi?&Nh-kfQscB}wBex@x@?sb4BcXKXwTTgA^kc^`{;-jaa8h=TkqvyL z#}0$q(1H-oc3v+jxaYF({I5io(qWbaQ=aw73gm)(2X6WB6R!Ybx=J!>xqjP~B)(AH zZg@)&>d;&G(`kqp;>MrtTBs7sW*4lf?F8-*Dwa{l5W^r%uB?ou)T-u^C`t;YY!W5k zQS9YACbQ8L`vtUQx$b6fgkgo)=DwIdLTx{X6+t!TVIrf+Vp@rPelP5%(7`4`k{K#F zD`eh}T!VlreE~c`@yx-I0+HtW9 zsMxq{Kh-_~XCh*U=A`J&>cI}S7v6$orlpJx=gf&@h;j&b|;Ng6|dFt7bmBm#8x22qU?ci`zK5M+l!hX9~y|b&@UD)iy zJKoxp__Ss2fO|6MmMwb(jQ#aXHR=QM`c~>Z>cC&>HUyY$-}#(8x5N%QoC*`2D5nYi z=b+kRe7vctj)faOBE9zu2kzlt=&01s@RWNUbs&F!H)i&}9rLvAcz6DI!KC~mTtZx$ zB77n_l~P5l*kidN^OmnNE@9f|c_z{!H-3sg;CW_3#4yPqA_9o$QMowoX04=UPmqjY z)7c1HH(At_c6@wHrO{)rfAkPzq0y!8+3Av_Gd-DpEsyP=i95r%XfHeUp9R@hF$2%$Em5T2Fvr0%g;3lcup<$)K zB|6Ok{)Z(JWjR%2om_8mFtg<3aEqC$C+Ae@P11tQ56UWcI6UNav7Yi_&q8j%;VSj@K4$%c3e66H7mtId6hA4kLV3@Qg+th*R0)Z zO?)GDDZDfk-^1F)49tGCWqHHuL@K&c@al+qWB}rFT#)%2I*FWk%Vj5&P*{8?kVfZt zrp(5JSQ+mJ8fv;}$60<<$2X!996bsNohn2kG%3rc@pmb_h9FHw`c45)2&O35`cpd{ z8h>Mmc$UVUgz1fB(C0wOObQ@DOBdL9&=Ud~|6oxkJ#Rfpg`aLH3=}oXRJoQ82BBCn zW>IO5UVy%Rv)4l}m5R2=j7&Q&T8n&|ce)?*3YC~O963v=Z!~~;eel`d079cr_(!kC zv9?tt7s%=rHd+Y$`z6nNH`y;Gh>cMZrpyolvsOsW6}B z1GlfXzE{sOWi`56O!H(#di8wzV{LkpXR`Ig_bkJE73Jq@QvMH6ZAu^AuNcH!$k z0*a)(#oZ^pgm9ZRbWn*9(oq1#vG6t+L5rcR6B{aSJ&+HAWGauUFLJ%;3@+-vmlRiA5ZqG?pl`lThv{pK2)#>y7f zn)X}NOGDHn%O6b<-&~VMyf4-pnx|3PYwdI995P3@AM?n$vYZf}^s0?v?s12rm9seO zT?6Duc&DQ&Or`7tkoH)!{EMRBX}h{owR@idQf1vi*;V6~#+YG0k8s`$l?4UC($E*6 zjZ4vGUL;LYh>)hRpGx;HV2$!l)O0aG-YqmTg7Vo9U9h*yN;8>>d+}9=CJ- zYf~FF(>Xm=CwaaF|B%2@5j61ODHVY=KtxYeHe>PtDIMvgo-=05AR&DnIQn72XN5B= zcm8Uqx!NhSQ9xgvIW9jUfUcZJQT>s002Ob~%EtWiRQoaSRGtZob5T1`J!`ZQLBX>> z>nXO5p|YGYGme@dCX-)OFkF63dD6Bc+V;vVr{dEO@5JNnWMKiEH(lrS{h`sFgUP=x z`{gQPWfO*vVH9ybS~B*^-yFbs4^RyJ0AW5F9KRF?oBymC#N2kz2Xa0N9A8izj{nRJ zAU+-xq+t(9sKHPCH+3i$a6q=pR{(KU`pqcGSeQn=_Al^!O6ajyHeUfK)ITG^@oAw3 z+w38cP^5<4*l>NnQ(;^T0@Oy$LHjcYKtLS3@*CMDAZ%3kixH%ZB-^le$L1i;!AVNZ zPm_pojfOUPz8#>50r75xKQ@1qBT++G8UshRl)MaXSVg`LDEjd4oLQTBr0(q*-XC0` zhjih!dtW_=;Uf8On+bX{G+sgg%`xF zf1?|!=0=~sVr&cybYjNlre;p~ENmQ~xP_IigQA_jq4B3{kc;`}7Vx41bOPp1j&jBh zg0|LnpR2B)h{(UWmk9|f{pnox7nZ=x{M!*o{1g0eu(SQ`dxr1i;QTk&vybJEZ2$xb z5pfX!KtKQhz|TK`k2L^605A|xFi;RMFi6f`s} z91=XNFGSdAX!z9lM5JUCloXhFH1ss&^d#gI zDH%BhB{K^v8#@Q*Z!bYnF>wh+C1n*=HFXU`BV!X&Gjj_^CubK|w{PwNfkD9`p<&_i z3EvZwek7-)=H}%W6c!bil-AbOH#9aix3u>5^$!dV4Ude@&do0@E-kOD?(FXE|2#N6 zIzG9&zPY`-e|UU){;d}v0Pvq`eLnwG>|gXk{L~8w1Oyla{I_0!K(3!RFd_&j0RtEk zzdX3UJu)Gq9|Vd(Tux07BoULsC8~kL3=|qM^A5??Z`JRiP9LHgr&q>+ zNa@K6zQTJ-c~$%X2ta;Eo!IoA>O^dBjXyZqQ;q)ycj(fBX?WtTW4TOTWJGH+#oPF( zZ*B= zUz+RXh2;r^QRD#s>l+JzyzrdkSTYqrgMvxK4A9cQ?gJoxrNn0$Y3l<(R8srE(!YWI zais+B*apg3daf*+qWj#`9o+Zfl-61^#at=`~m%X zjtn|aeF56jf?h7Jp*&emN5{y0sK<)Tf|jO)CaP#iGPD>KM?NKJMd}1Ob*pD```kED0vPX9U|?f$g7PgZHCK5mpw7(Zfvzo zFoS_dn5^%I1tXo|DZC31fm>(?`)cI0ii1s_-$(x<)m;tFM$MS6qMM717obo}wF^*5 zeH8lbZC8MhPovajz?Sy@-^}K}n(2S`-TPF+KP{e@bCgCzxRV&r74B^4x{>x2^RhfU zcaQCTCH5;_nOT=6{`Up&`=<_0WYLi~xK*UGfnHhBzA)YTwJt}yIc`Qg&BIpOm$p9d zIfCvNT)_!Q0>Qn679h%6$fpb;JS!vV)k%XhMP145#CWjwE7kq;x3RAK5SYD@eE&*ke5B=HG3?S3Og4QFaVQd-)w495Mr^AVyId zV(~a&1iEGX;R2*Y9^GwMXI3yVZE95$Ujef%F3g|RS4s4=ZBx~|65Z3}OeMP6m6t-k zrxt!>k8O=+J-Zs=Ox9-Nsn@)sAp8|p(-zO|H|Tei$3Xqvu-ykgl-{n@q#83PY^$#p zv2IM!jDgd90m8pIsLulxkS057r0LGLO1j}DG*o>;zvb9O%*4{lPpr4Lxtw`!7AfI7~E-8 z)&-d_?uNTE`Zl>XldU^?=^>yKj}lG`Tt&E~l-bmimOq>|=!jsZh>=!K7gh+O={Cp3 zS0eaE;d;)7c)?%lADpGw%qN&0KjdiLG=47U4i5BTkjkYyE??mMsMD(P%`Rzs`^D~3 z$Wsk6bxQW;CwcsU_0R~{w}u{4?6IN=vMxZ>WNrPMOyQw$=;dngWQvLrreC0o*cF#;?MZeFxMn&kr2TsF7(2pW`Rr5Rz^ z7K~mPfksdV#z0%QfD}`Ro#v(kf; z@G>yfgj=iXs7C*|*2g0m_v1HdF{qq)I_8L4>?;R;-7HDRSI2hE_JoR}Uxg)BCXIeQ z^^azX2u9HV5E?w%e&}(y5?^p{8nJEpupajKeOV)+&8cfBjmCjleb91WZq(QAn0FO3 z1@C*z#3b*HD-Z>ilOgLR;ZbOo8=drNof0(V@^6UFmUd44O#G8RhHZeK80HY{4DdD4 zQs5E_L{@d@w=Kj6JM^mNJjkzpOKgN6KfnZQ$cYO=_FjOP`e7HKr=ASwY)U%;m?oZk z4P|isi&{qj;{;Tw$o;oe#v+3N6l)zTGph>Je1bjL#}+LuQT|_IsS?Nf-HO4 zM+z5`cX6eOu2{(=od&sSZbNl=nKEg}mElwJ&(|SWKVjz&U=Jg5WTcf!=ON6+kG`EV zY@_-2G9&AyU{2XqNk)!%g0-@=<2R~6U7C40Mt&)(icJ|{jn0mGS{o_M#=21c@fw@3 zuFz7=a!P?>y7y#39Lizbyln<2!Oucx#ke3`5yv=~X1=AYgQ&i!V{DfbJ_RRR!JBMu znfD{JtC+EL2@v_Mr-?x*5l5kT0h4q}CN0L&%GWPK6hxOWW}so^`Zy`prEc;BoNX18 zZ{rBeZ7NIk+HJYAmLO)!1v08`sCrw|cJi%B)>hAADk7dAMT-HCT^*}U@5pl?!L!;d z9xK^j5FMqtZcYX`$~D7BwcgLX&6JW{-uQo=iodS#!j1~CYCOk^LoD_PKck82qgs+e zd!PF7?7+|DO4zXMUbFK@OgAW$Y)g|0(G1{EDCE1vwdABh37b~bN$feCSRRqxO{S&G z7R~R269}M57zElp-paPQ4!#GM7g>Ji(v0ug|G=1*%Zyt^Nqdw(`7!9z!iTAC@x|5? zarro?C5vmN>kaAsM`tb_d8J**sp=1`bf6+=`>B11vwg5-t4Ix|$tH1qjhs7e42^sI z?fa%&Q_=C7(Ym>df>$<9H4BowYj3w06(^+k>)t=|zp}4W%-$84%Px-|C3+ zXWWj#H@~|@aNi02Rts_o zPRz+&rgD8v_e>gE`a}#@{Kv49N#L(24^!I7 zy^^Ya(N)0>wmcTLv{2B`i@j=7HO-fqsR|mPz(BYKoOCIP45v~`J1nAD<8P4^hTrOh z;8m|a3|w^!b$=53q8me6JwWN}wXdm%%sIIh%z=9v-Oda4B>F11;~Rr^C1mhLT*HX1 z3t`=_KjDZz{wln)rDAQ0+-H)fd2`CQgkx4Lq^D{;^#u_>H&Ow)?!G(6MdAQx-sw5|uOk=0Fy#`f!? zZ}j=Nn|-5clad9$p?CzLm1EdhIB?m2I8&7~1Cwo6DROH$Q-(Osni9X;iO5-LuGCuFL0`HfVnSS%4kevE%+jtXJ!X{2LYH z6kzLGq-Ei>aXjuwucg(nu((Ct^dRFK>#;f7fT=L64A=uRc9qmF^vk8i+;)eAIg+D* zg-sg2oLZ~#n(EIFSNP=pY2)Gi7Wmit7k#}sOGGBE>(2C%+W9o;tVz$$2vW{}^l#7F zIi)zMLKM_&v#ad`ld7v+ZwO}fj5zU$;j4W}UajZj=f*c+XeT0uS|jrxKgx_#AdR3B z*yX``>S9T?xZJ7v*brH12odiz7Vom&5%PO-%A4@+fdJ+4tGPPfenrvwz0eKTJuFx1 zaFGL)$oNwI>t|w^Z4b$&R!LuHjtK~Nnm8&>Cw*3DW+FdYuq}9w14rj(NF+sM z96TC39#Z%$79a8~_QuOATeg!|`n2SD{D*)n3Mq?b^GUK$Z60y!9n>Q$y(7H^AXA z-Bu?0fiAxQHRhuQi-MmUc9v5f=fNfu7!J;DFF;UTlofik%SH1OzKqDmRbA9 zhK{gdE&93_^B~4Mi}5zaAt#fW=>BgI=9r{@7lt~v>$hmqEg4L0bm=0C7fIhpaN1D% zU4U*vz{Tg@fCdnczT5Ain}{@R5{HJo@`N~b3|@d<*wnm!wp7@f5IW7*lVP3AhKVUU z;1Ty|WWdkMJ2|6aT~Uzi^9Dp4`9L9Y0+YPN6T}4xjbnVt*%;?dZQgU zsbXfkS~9sfkuo(6rPYJZp58jcbKB4R#`uTZYq4Wm--=3BT27{yBZF<7;gnvos}7?Adkdh!!gD^x6rXmts%C!+Z1y^OMKOtI`%1U4s^CS0?9_ zs;eppMh1BD9-dsYo}+rqofyQ!$gsX=I|hBuoXdr@I3)vKNxJq{T@FP0FD|m??-!u% z7o&|^*X=Jr>6u0QmvwO2djOOL&|d!gm-P3ePyuN?xjR;UAZ{n<4e-+aJ&N;y)eF!d z!7KM7rv(Mn8+v9yX?p=7uqzoIm3$Jch=b-6ddlJFuvUWyvGo~`zK-Q6-w0qy)~Q5s z+5`B?U~xKho(URgjTAu3anqpd;6NSri~%RK~8sz1uX z`F*mHY>KM~{ob#8#@qZ=u+kaaxn3>E&VkX)?(%V}V#EWWMo~?9d*CrG`kqJaaX`s{&{f z|BYyd7(h2Qy4GC4Lg5sl89C$RcWJS$aby-wsW zcz*rJ`Rw_7rta(whshU7Qv`8Mak1`n(nmOC>t&B|m~)Y;tJ))Fi#ay1J&BRzUfbL$ z!#6#`e2#%{7$YeyaV9@P;2);?YwChIeg4s^+m)^jq#qfL;J5AnRS=(fia;d2h z6&1_2qc3l(Y@Oj4HyRNlpuU?`*l<|wkN;*T&79dbd_MZ2nAFj#)YXdDwhh9AW%ooI z%UDNODy%=c-u{?E!4k0HNOmvUlRcHltLrG8d8|~B0dL)a$5$}yp2gYCTl(Z`?|NhW z4J8$e?;&=B$~5;rwChAlif|jC&nR;@sbU5d|%Qw;0t)qQ-}w;@)z}A zXi9@jM3dA@5q&Wf+R1e1pVBvAh6pHm-uy0&6*qxtaN^ z688-A-F$M~&pWIxdi%Qc-?$TEHmLxi47iE3>7dz$m`1sW;BgL-ZY-2;w>5(zf}ar~ z$7NOc4N8E>PTOy%)BDVvOz$@jsoFuDv8fAnl`XuBu$xv68>Mb3EyH3yP;*!8ww)Fs-N)prQ7ekp;f$z=})D`;geSCg9q)F9Jv5(W_F^ZVzV79 zWT7P}cYyy85sbzKbbnYTx^-{$oKTnn$!3ZcGoDzNyNA1a{XBSa;FR_S81c9f<(RvH zCf%#Z&zxC5@!|a}e4;MMk3B4XTkpyI?n$viS{U}gy^*@Ri(xc0o)0CSKD*^F6mn%) z=ALvDTKvnUhH7irN{ry(OC5+*|MM;T12pnR8+*0WWDL-ip^vA z;NE$)UJm9~jscuh|7C^SbeTMx~i2V_GOj2rgTiFK*~^sA1RUNxUK%x2)#{ zh&~Ql1&wDdS*%{3#g~%U6A2QWF?UN>tDq>o{=oG6(+Ug^OQ=xwrLJTkIWIsg#jW6@ zwZ^aG_HzEeh$U(ZOb;Dg1YAA;zzfh~{LIk*rCUBb=b|j(`F%HRcZnzFCTBJtfA7Uz zZ(-8z?k1=@)y)~dzGr%_8`!udX^onUlYO+4n8}TqN%kP{|Ap5}3+`a}LBj7l@$aF?fAp#Q&Q>>UzBp81(2@EK1gWrk9<=EVG~RQ$$kZjB0i zs)6*mSu<@~TcUW0C@PNwQ@(S(_(qgm_3TO)jzmn%kMYdb z%n9jO7IK%NjPw_wF>HjAgg8Q!gNK=n5yEXGT}(5g=WLjv$SA%cFHImwKYI(p(W6+n z1EV5e(7s|J)f9enLdYf2pggeQ?dj{2m$zfH-&yhqLpq$2svYdf+UuhG!&p=^^(oEy zBxUF}ySOB-!^@DBuGtr$=kl+Rf?altWzU}n-nKR(zot6W5;>D+638uwPz&PqdLZ=C zXU=e}OqpBrvF8UvPc!PPVb2(&^Vu0IhJ9Y^0w&xwvJ22TA^HyB=|=-$`kL*92s7y% zka`p732s6lDdD5}3lJQm5wIxd+U8F78PFmMC`MfXfw+hKfJVz+fI2qo(qJ4E0-|7~ zG(Z(tbKMg;M+IMiB8L9l_%2&|JkFo*9CdUfdaMnABvWmj4BG&6pks58Ewd;1>p*PX zEd;o|COBf(OGc{D^V4McXl(mSLy9=|XO8#hc@&D5P4*BnUd2JD&jh~2yEi}Im}~G+ zaaEbE%Lh#4kgYn&c>+StcRQ>#byd~Mo}O0Wu@4^%-}GCi|NkJce$fWg#|&jH(Mk}Fy8l6s7Ot5!M0PK8?^JxKgVWPG`}u7qjjtri6*#H1aoch&?RnQRN>S{^`p85DV`bF?pRdE+ z#0O?<-y^=$aJbXvZfbeyE=++5=d6ztg%eK%<|v4{3+SIKMYK6@`l`X@S~^wa%#l!| zlCi_C;w@I)%EVjB1R2CHwF0^r{jUje)wI9%$ndb8xzC$&=l#=}gBMsB-qla#TD~qz z@tdJZJ`kPZOk1Fpe*)DJxCxl>&-m(6fz(u75`8~|MVP4ly% za{k0q{1du%*{cJPH5|)7A#3};B5NJHCL~$wH#ZuI>O`TX=HIyBDcc#sU))7>c2}jo zGeKIXNm3T-XKKf9B23YW+rfV&;Anl0D_y3B1=Lx16Q)e|Rb+cxMfc$o7nGDCZr)-W zCcovn-89QD&zUvoMI`lU5%0a1dgff6vV|3MwQWo9@te;6R(wJ=+HE?bM+e)onU0gY zVP?a13TEjIi8*Qx9z-!LN-6qN{?iFKzB22FedHy(PH+7?wMvjt1sgizfRpmQsBF(J zwpYCD`O7WWC)FIUA!}gVWhsia23EvLNP1oU5bxNSGP}+idx+~mwH~xro);x8;53lw z`Pt}!YSWr&ia_73x=P%y7EySkJGAOg8e_V=c+f-T!}i=l#&`uX&MARx%qD3JmigUi*3`I_ zpuWaF{yloF0~&8QvNK;_sb83>X!hh9htufus9UDF2bOQ@NEWGP-3cRCl>MAsF$~4Q zF%qB2CzZtmIWSNg*Skf9zHdm;)X_5!mbbzwk7#mV&D~kHxQj{Gzt&S1t|p2*9iDWY z#I{Umw#-Y8bZskfgQ6H#P-&y_ZWwv927GU z-g%Qs(bIGqT?))a^M$Kf*QuyZpJ!3?Mm4TMhQSC`D02ID>TE9EGFkHl=o#`19W=vy z0m9n80Cnb|+xJe+32GG3*QYK(QQAfy{)4;JUzSdP-K16Bk=CYSIVQVLb7ripY((S60ZgrQG|4V4bZ`@ZX8bcQKnppI(T+bpZj&|t z@;n0~%54!HYzRe>u>w>!IaEO6*Ilb3yk8%`0KN9?WH|g=Z+Vw!8!SELUYcHhUIE2C zD+(NkV*pVPTk!(at`22fKDal@34kz{8~A0oVLxzq-_H&iQT(ZaUj<;=CFa$C96H~4 zSzb}U2p|nD^WQH(Yj-EIk9Fe|gr%)F5?$zFonUkVHFS9#`4GUXB`nJJ?s$X$hW~8 z9xvgxeMc}76H_m_YQ)f3qO?Dr+j8J*D~tN){VsyOId~%=OR-`F2A8_Ic8|nh-lO~( zyamFK)?{h%u`T=S!tESnH2&Qv9vVhp1cGD3 z_F6UW#sRuAcBa~UCG)gApngTcE9n^<)Z^i--*yEHAFX>B7q%t1$lX#TXzh5pFE#I@ z(&T$p6enp4>0R8nK!B@gpZ3t1Q#pjIm|3mnPWXKMF*BC8?Rx}$zQuf>h&tl9(Sh!5 z1%+lG=2w@ncPud(y0R#R6-K|=78$V2OeNM-0KLB9BQPq(u#RR`(U?T9=n;HPdX*4H z-IWT9p;MK8@={ga^D$1|kWtL&${uR~q`Z;HMnkWlV*6!AORV`enDZ*#8^(oHq_K1U z#_Y}Eqe3%H5e-vAt}GG^`Ixu?N-7DDB+xeJuM}X`kn%GkLTin=jo5ed_ers_-%liC zKNe$20ggyntT zG_Yv+l-2)b(umD%NpqCpV7SaC#On7bJX>1&^Cm%1;cOKQP+gg*o`8RA%mY63dRr6qX@)Rd_$v4rxCx(js#lIOw2l*tj`P^@6sfe>k*m@83N(uT zXl`0vmfTAwx{A~>C0-)|Oh)UgGdF0?JoBq9j7-R9nN+G-{nbA=nbA5IXY)z2W3bV( zF$D6cAJEefy%zFzY;D^E$T-R(bwjBYl@vGH?`*zHkehXh3pn5*N@Y+wga%cN3)e8W zJEgXM%KQA|23pq3p3g0h(K&aN=t@cB9eUaWB%+!Ht9FhfswT(7K# zyaH$h292tb9nGyFiZ|@j#ccPlJ@FK_Euao6gfmcDO}b>~tK_XyH^;IqS3l5)tUTbk zss-w4A{p`LjEUu(YW#lEGM4oGqic>)bw!*-jlZ_W)dEoH^+s2w)BL9t>M07x7~;W|8GnE7$FZQ z9o_mJlFAprZ^U^5az5Y%xI25q{1|SwusI>P0d+qPDD$@MJS@iDkd^*|m{|=hUw6iH zqV;)^lnnd+U57F;c)i2LFL}?nu=<+>#iAO?tdW)OWO?N0AvLmg|LfGkdM%XS6|o5U ziBzqs^fq_A+*6xV^=vUV6d^(wqKHZ~6 zZflrju=^>`}E#ZLHN>7SA;HW}N8jnZukl5yf_N)O4WeIp8 zh3Rvo5Eh5LZ7x@0;pCL^VbtSL(RoC3q@c>_dAjho4pqarU1>Tbt;R6n+j8^&Q#!;| zoXk5o0IA^0sws6@FJ~ixL9eqd)-ZuaGp59oXzC3#J8Wo%zWPs!4%Gq$9$ zJqBMfa{U!hQg*0JE$e<3sXfKPS)7g^>P^}v&>i(eja!&7EE1c8DYb|Z2`lK-#}!R&J4VldWcaip9^d9**lGcLx8FwI;x`Q_ladkyG`N6^hYKQT zOIgSxnsk@At#Q`_EjY!1?e?t2D(edZorqsb+tK~P_tO!rgEZ}KtFic^0upRtpTjH* zo>+cAgk+=r;_KrVXo?hVLUAEFh)`7>V5N0u2!rZ3fL5-|# zv!j-JZ*5KqVY=pz@7zgxrPJ-8n}OJia1w?JcyJInb0RgejNX(&&P93bPT6&cQl1n5 z;b<#idZyvbk)wxRy{@-{DD#;yqk9w>1G<>%TDWlxYxX`4ye>kS7AU66W_LRKGAkwv z)V#I6ztcfcY+?gIO!97|$Ydk8w@7Y5%};bDX7 za8vIAAX>3jW1JP1WoUi+iW57t=5?gdT3VUaLpueWT7boTfG9pc0VCD_@VBnJ9Btal zzNqBf4d^OBE8ndKf|wD1EY$V@KAb&szzjf>cSq`?&tNDHF`5Obkp?jC_U-}jqidvr zG8+O4Y)W%G!HdK80PK>R_d6DoD;Z2T)PW*n*t3-Y&fgl|1EHyT58MSGTp%jVi5NKd_*pYT z{Epr~oJdAl4s-`6H63$MV@UeC-x}MbB+l~Bx}i1T{3=9ex6B>Wm!ToX%P(Po&-cyE z5ZyWk>&v}c*uV8E&2RX3yyP#v{|k2V@BbEshJ*Jx{>XSTiEsNyd?x^D$^!8C%Phx# zRxZN*3T@x|d)WQB>fRrzPrrrT-AK#+D!IS5f^i4yP-xXd&9_b_{d<@sXGf*Zn9a^! z`!S`d-3pid!|tV|!*E<9T%D?&mrz6~vKAe@*@z+sd{ZE$^HS0EU0N7pEr4grc^L!# z=FuD)5Aa2S$ol0nu=+mZeF*@(KHc`YLojgbe8n6Jh=5)ybJqPIG}dpA!YSGa8kRKc z(ABMoo^E%1BFxuW*8y+p36GR6eqgHP!q!e+BI4chyEt>~q5o{z{K2uWDq@~5M#VPb zUx3EbE;66{Fl%_EQ$QOqMHkir^R^C36GH<<#&XtX9(Yx5x)J`0RJC>fdaT{Qu)C@y#DUqvfk>Ol7mg?P^}41cD^+13}~4xp8vXI_AwC_~Tb zxbJ9reBAzhU;VQ+_G|q=6J+Nv2+98n^51OYoFc9Iy3nd#c8tu_&@1=o)#tlV-f$TH z`lO%_-LRcT4=>vz=Np+#k`BvzhR`W6F?dZIfX=L-$Qf6+bkc%*$*ji%ZA7PJucA=Y zKf?`|O2)Im2Ea>sx&W9vQ>~}BXY-+OW8STb+!e(y8Z3(w?#@{?<5Ay%So1KSJama| z4rK@1D=~xHziHeA!0@}qb&T$c=(WtJ)9}U$A4ce4egplkQNHLrorhqbNc+lD zOUsJlHU)H(rxt(!Y7bn1XwuQ-#X->eJm`GhDiBH5JCM4O05 zU*XKh)6&R?+XX8r{0hz%n&X4}oVLdSgJs*M@PtC1bX(ee{kG?RQw8?X`~)s3WQgPU z@Ivw_l_7{(`R5S%i9y#L#T~W6Thpt5@7m5k>Y93}%d`KnajJ)MwQQCGY4hI@11IHb zC^DnpcEly&{M?-Sf#YVcf0@Qt5qpNMxEYC~O_(end48KM_t5NuE9db5vB1dm0wg0? zPHkI~bSK-k&|0S^62|($+KDoObyC+crnz}Zdt(tz4DG%Pm@{tesQi+@=Lh^9q4-O_ zz^`RE`p=k!e;|X=XQb7Mj@1?uC8`AOyX_yQ+z%mAlq(UL z!3L!_m`ZL7aHJ^Mi4`puL)1`pvH;Z@+pGIb*NtEKY@4-B9N%w;HHOPhgLR_~z1=T~ z92ky^-4UXgIJN6J#%lrs!bxKH@5-|^mnhg58We%y9T8@5BAoRk30>ki`)dabZoOUZ zE?^B60N=1H2k6^xpu45_V|Q3_Ccen&oH-YxP{2=mhsr{2uKElSogcFw@W6X{+Vj|B zls>0gP4K4#s>(?L`Orc$>Ud(fsphdSv2`hqGoYPq;FF1rNyCUc$J~!SwCyVan{eP~ zUFw{{B3kO!1*oB&wzuks#RR-T$VA$Q)-7^Wg<%r=y%RaS)=ok6uxP9YJqe$^W*M>) zsMQtP^x51YW-8N-ciRH~t{ir)xoCYj-PF3ULWU;1j!T+3vPu;E(Zv*m;`tg+tt=n)($Z#2XxyQ-ywv1H_E;5erd&8(8Mo+IT=R zNj-bmTg3Wm_AEmg!_BuIJO`CvCKDpN@?n3IuZhK(4APE>(hFP{*$+4%Wm^i9g-W8+ z`Z7IHIbEjaG(rNI7piDSG6yk^Zo4R<#IJ;iAhRh?PEh{3W67>h5>hmQt$} zk~ovm6`33xOOg;;yGZP_^py8v;CRob`f`T-Jz&1rG>qGO&d77}=Kl8c3R8v9)t?Jc zbH%td!8lA%i_fTCjESA3YqQ)hH#g4qk}3EP?5_Vg7RVp>iS92!*4mzVz$aa*urWQx z0wO^}!^0K|xwAuZq}~728Cj4L!bfrLiPV1}S~jkC08=+ECAfV+*G>Hh1%(d4UH$3D z?%3m5<11k(R+=io&}5c=jlUTibhd>h0754Uoi<(8q-W<2=KkIe3v9T`aPae_CoBtw zyZQmA7*kOUK!x4Z_POmgtA;ycJ+^*K<6NsxIkR!2>?ON2;ny7S-R@4+;9{kv9W>WQ zLY#XgIW?qCRyE);`8WkvTn`IZmLRr|Wlu7`Lc6R z5SL=+uYnf}CpZvkfY`0_3~UfKBD zHL5NzK!?YgEH#^jpjmZI- zG3J9$mtbA$qvEuUD2_SIznEF#BMk5;KVpkI?sUHm$XIQ_a9#G|)9UJKME{3uIkLWQ zeWQ_FJLY$%rkOe%>HPZTWiWAAE2*xp2^QHg1o>e069pdnK-XuiSAppXM)d%pRLnxR z$PW;Q{M*UI28PocFRg1{Ybv;V3})O=knr@XG@kOezon``we+lZmuU0kWc&g|Xq;uu zWmm|eDHzA!%N0iS)%ePUyu1SUc&Jgk<*}iL?^I=Z~ca4?W!@A$`ITG7XZx*(FCONyu~K`FC)qU`$^Gok4y;%rgFEiyM}jsi1&WD#W`p( ze{kC=7uzg|nU2O+rAB$hQw9pWl!`JJpajD(eCJ6D#%FOZwAurDUl2I0 zADZUO5$FJi%HsGN!eEZFMtsz@-fA!I^!OuUZg+;)g;E2t4Dc!aBO$6H}s4R`*NtLhou}}^NZuc2-%cMbs|~X*xX~1reMH*0D-+mj#_r6L4Uj+`ZQvCu-)1hvh#3`J_*2HzXO_a&%m#sV9m z$$HnJ#JkS0n;U%AF30;;r(U&pxM^G9(ehLu<)>-M@xJ74$~RZ9ug8SyOD%eH#v+I! z3MEYOi*0Q^?aX2yOi9+JwuqV5NmDKEtTH2tD%7gxl!{&-i9&x>pD!B$cEi$r;5;kt zojd19sLdl5h(qogM!R4`GH zm8DfpF};Q~SnT4rGSEuVzzP%Yaw&51>wJ`ya?&D2TU(_ZR)13^2@cYZ*&h5HxOSz} z1_9wzPZ-e{*+KDh3)W8EGgjoeRr7HtmEJI&gq+GTBQBuhs>ed@x|gC8bfNgl%ygd_ zUExt(Y`kK!_`Um-{vr@&Nqz#AAsx$40~u=vHWjCV?SUUQ?h=&r@3F@yq@~nv>KoBt zwQVmb%axGbvp1$LJcc=rkxVH*p9lXWt3DAIvjH}OJfes8;p-ZZ9 z=oXR2)5nLi3x;&_>knrL*w$6-f^y8Z-zW&F~oL0}Eh0fV)renLskwwwCnV7Nf+h~D1S%nXyh9C7{hrl_Y* zkpA`vf7q-U&I;oa^&_jpdTY~o74yLuPZ zHjr@odPK|g^=VcumTsc%UboAe0epw@hHg3;=b3PxLD6o}b-BTOzcoJC462(%bRH3*%CtxS*{9qEi;;^FXw=0KJ^su5SZd zuSaHi_2Jk^+K9%nVLl_W;+gcmdAFET3TSn*mI$fWz(8#~E7acdHrbGpdQwK@U5~zQ z?^JC(R-`zlMoM6LE^C;VSguXt;Ec7$TRuo&YSP%w+(6}f#je|~_kV!8((%abdyz6)`44*SrcT z4Yz?Tf!T^W*+mFN>#4uKjqVM+ps)dpP>Q(TDACz7nMr;k*5L^3+_w$nfV~7~A1Sxr z_6A#I=6>04==g3m(b4bJSY);sDKJ}r6loQ&2+P|P(LIgG95r}*0kSp^o#;?M&O<1t z@TFH81Tnl3-beOBuwR`koH#It`Mp@C{6dD4J+rt=&oK%nM&0Ay8e2H(e0y8UIr z9lD7j%-8ue?q4wCUtK0D=VHhE9o5Yw}hK-{w(s zF)lPTg|P%No9piC@bsP>Sy{J)QsiSrMqL{9iI;?0cdi(1a=!cQB2J0?-Go@lF zqGaY`n+cdgTpp@0^7K2O=psIwJ_LCNzPV26LB3(z#f4_xtfUw8e0ltMeKS|cukyo_ z7){C>W@>SOzIFE0I!v#oyxq+)l%14W2wJl;c`G8TCDZ1` zAd2|=(c$Z4!VZtRX_G`BXJ(PPeq0I3*%Z%sGHvwkK3UcKGfoFz_oAuRIpRaxIRZVK zr1F`HgcH@hYuWF8PQ`R4p&^dqqgk59sy))Gw*7aIww%+Ql;d_CF=X(@p!Rh6{pD%B zr>9%^ic?5SveAtOLBwplTS84_lK@k40SzX|MmRI$Y56 zkn5{3>BikM+7z)*-~DUER50P+Rze)I)F8zIichU2Gvv0lmXxN#=sk%W{KEW@b}`S_!)H9hs3_;)}YmD`&P0MAO>a$@1V#O3S+14Q&uhQp^C=ze<`-vu8Am) zI;3-L?aR+Rd~BGuo7n%P;c+*nYnFptv~f@_{N6{+X2jaKM>5RD51w#uG(?uOo$(c< zo)9-Y^5_fvBabnIlCojg_ucI2f!l3Mb}r9Zk_kcG^MwBSjp22|NnR8Kg%jKv%L!rz z&bZ4mTsV^QoNB70FOHNc?FBdA6+tX$V)Z^amK+Nnl4sL>0${xcNgnZCcjk+pw7(fDw(j%uZ6w^8r`LH7iF@kXJl}>*4 z(nSR8#I>do@B;~OCTN=XDtU#G-y&!+f>&CXn^{rbaEfPI{9X8xq=CP4+-6QYlq%7u&Z9mk`^cRz>^_KWbLEIz?3Bhxc`oGUR;ht8 zZ!F!)!?~A}ZK#Z;fadw*B2Q)HgI6EM5?VF<%-(z*t1Zy#3X*OmJo~&MLVtDGGigo7 z^bK-hq@@u_0=@u|07)Ew932oQ7V^5Dss?W)us+r9`|4_M$q@U6%fw`vj-THhWs@CM z#|sV#dKco1B|Mf0wu%*lpoVyRbnRy;DW4y0YkSSTxd0uk!_`pNU>Be_S#>ayo|t1b zAih5jq;8|+M9N>6J%={Yh`o@&Bxp>Ec%JC0_#*VfqlaI*gyZ6BqgCBDk>i=;yokB` zb%@>9+po42=RM04tj+n%v&`d{n!0czCW_LMZUXpE*diDYr3U-)6U0dYIAXn->t|e$ zw+fWz7`J~Ys>h1XCrP-b_Z1`W=?z0nL|5x7+|)Dr2OlnqZW}G9A|vb}q{_DNXUva% zKZG1Kb)K+Gc2tN6nOQUf&gV;ni*fJ_y8+8gT$ z_87Do5ZrjpjjMbd?fvm)u1n`x{j;WwJ7Hc=D;SQQMi9l4d!}!^pzubb*^QF~da4Ax zhp5hnMXb{b6`Ev4U%V77ungREu`JP2KNk%fW_Fyy$n0KGQpdy$f4l%?(7vQV_(FC` zNqI>``sa$ES$;!A{tCo7e(WrcDzaPd9akRN^l zKM~xBxA~sc6T}PuYs0tBKe_RR;wVlN4QdBL-?W)+HIPwVfU0iJ8o=1k*Ex4g%WMaRz$foC6eA;*bm%A}JXCmwcL# z1Z`RJA6arwu6Hv=yB+4*dl4{sJSL&fY`eJ+iJ>R7f>`-rbEuF(_{!D1BFKTu zsH>RSy&!eTpUQhv&9%)QnAYE$b(%)WRaPcgmLW>@MoTdke0-tRa{eb`(BKr@@k7{+ z*4y9SSByNZND@)ByYcFV7?yYPqDMAXbmvI1K}G|?p7((7b{Tuwrkk!}3e_{Mnk&1Z zN_ueL7JQQM@YN#D97KQh9**fCm$R)h1HJ{gU_`&N!waco5?K6 z5|bcuw2iy02BN$7XfHs$*@4jCbY4ocClH_}!r!20MCLE*K@QDrPy9QSImfHC&ZuZc zAKe|$W(B#dQWd9eGrP@)SUlUuzVcyovCyFbfAHuGO1^2Z8bX-#d3MThN`Wk_7Mp~M zQ1OZQ4_Kxv>FrUK^(TKzYyB(L=vRFF{}w6d63N1~yjlO!%P}%~qBQG&r^No2xBO4u ztO4YS{iK9Im4MT-FA5!Lu(SCZ+2RC0=R~&(gHOe*@21z7_$MS~kThri$~nQw1ifse z0WT#W!_f6=2V_&N=v?p&S(}%w7}9Wo_w)$-RoDJHy@nHtDN+59;n%EGZAmk?adQP>r6`OqMPHu~Ket3rqVANJk?tg3Ep8(m1ZAl=ePH%NoDh=O#N zG)RZQLQs(|0R^Q~TDnU@5J~Co?pW0F4Bpu9>$~^2zrFu+zW@B^TpKTN%{Av7bB;O3 zxW_Y|aXI;u8ZTA zgzDENA8JO6Zsuh>f9{%!-a@^PtF&nwK#gs8VG9RH-jewS+FAiDrqojas(aQPbi z#srsN;)FCSp+{8v5^!vKpk!s2f?asy)>Xp-^pZ>w4z7cvF}1q}2vJL?#8imo&wAb! zW$T6J){jrsl_&=jI;$E&2q%Z6O=O*$2zGKl!Q2*DlY}z^7FdPJgy~F4AKVQb8=(wp z%hxHeUye1}7dQ>qDb4#b{Cro&OldVkjp|jk1nj39kofr_SR0&Ra;(1h8`1P$PI*N; z!l!(ZTk!gVBh4ZF?Tml~E8%*}qWJN+4?@YPR*RF(3|muO{dYIXvIP&O?oevl=lxD! z&M!22<>%#Mb0-vSee+?j7A785EF?UNjti>hzw5(WdJipeH(2_)KS~Zw=cck@+fdEy zb=VQBp2Rt>ConWnVxZwb=fn;)d$Hdh%gzNxPtD9gv_9gcKpBR@XuV3x1%@J=pQ&X$ zC#fa$r0(5LX2`(`9@~yVcjmYJcrni#p@H%qvb0Cp3Ov}4=-+5Xdor~)?ZvJq)Fx*X z!Jb>%E1j^H*x-`g$@=^{m08r7xhU}Grj)%~s-IsoGaB#LKSH*Yj9UF&lrz^r*XFKs zc|nQ9Eou2(f?}CYiJ%7Czr5)o2AG8znMfjhYTKqM%Wc%NpTKu%Ox}6^k z`wnpOH;33mR0DwLR8N0Es++nCG@jwY4uh`R;TonfW6smz?if6u5jL!oeYE@DzWo95 z{MeNh`2IjF0YAMIz?X!TLF4FGT;6<0$Ho zX!CGPAaC%}@dv~xq3K+M-9w}GI_@PJV;0`T2|fC#yOVdjEb_l{)Bke^e#M9UO7hv!RV5qPYR-LFg;w zCM#Uz#4ehiSV=^L^uVL)116<5uXe=i9}jqD$Ru{GoUrS*Z?nI?^%Z0^iyd<6BQ0iu z<;*?1uqu4gKe4j0AX`^m_qMo?Ed<{a_7Kw3nhKFn*QTN$aATEvhJ`mRmZr?SD{ybs zrG-FNz+ZT3n^6J2%7<`d&XYM%5M5$!OseflbDA^!wCmI782JxjY0rW0jR7Keh5dB) zPtNt@sU{hHiI$YJTbW>J58s8q5mTPZDxJPw${Y$(vUTOZ_g=MS_l#htX^TAyAd}xbQ9zNUW@HW9wg87^3 z1B8_ZwpEmpdhWEojKp_#yVTp@@J&!3NeW2I7vEV7N=nG zy};yD9^&ncFy3zk7~o=68&TPY&Nry-*(AEgbQ&i->XH_UFc@Q&rhrJzaZk5wj=w(PwMTGs z!IFKmgKFF*#(SI@fX4o?u%7#K=F*FYsVS}Xho8MyN>i-`?8R%4zb1wczUIJ3OMe_` z@`U`u2~H5wY6O>xS>FPAuS;tq`S+Dk-F9`&3;sz-Q@nXYS)8Q+RE!+b$wSw5NvbxL z*d4*a`X0@F-zDojWsCeyGVQT;mnX5Ubrk}}o!JO2cLL9{T%?Wmw+d z!`vs_mTc_tr+E2P$jVp+wgNEmqNk`+5r`Up1~7NBxZ}zsGydhHFUSR~jf(p754~f@ zw4yF~2YeoR%xfem@9U1~@-MISxERN5{eURON^lE{F2VUT77Z}l_dMmbV|F{JZTxi= z$dCs!81?hW<#3$6{rCkr-^{>r+>mEmi}6%!@2FsoDRiy1#J##3z^d0ab9;~@W=OSi zm_4%|Z~Gvwg+7|CS8Qt9WL1L(TdwosL3Z_E(=aF1m{kw>C#7 z8uFEEM@;tM!8ulUO#%)&f}b>dGG$4V-l z+=|%`wZFL;?+fFpR4pAmS4-WUL+WDv0!;MpvuVJm-FF#02pyCA#i3mc!|7gNr^4K9 zP}QhQ-WcJ$%eolJA&Ij%P?-EnF|Y9TpDVmvHP-*8`QL?#R-P_ z;-pK?v`gh0SYFYz=iRl*X2+Cm!RJ_-YFrY6U7vSW#dv(%i7}~G=fs|D49SMxA0nFF z_R!7F=k0Y?O+ezFZd|3gso2AeNLS?2?iTOIQ(ifgYVWN5aueNPq{VO<$!ghWs6=3x z4wZdKsE_cA4`;taaXpjmvy-m05xZkbC4slHwJb7I)zLbtwRs`3(e=ro9%Bl~0ZQjxQ zjo1o1I^cnDKITF%Wd-pkV$PL^M~ZZ_lu-=M9e0u z{Uu&wX(%_nrXsH(f?He&S3fT@)3#rV3$uCJ`%+C#B(STcPbowK)5ew|eA8BuG2glY zU%rA$_07gd^&TwTJyuJ1m*h@@Y4Vn#b=}4k52DOZvbI?JwW8E~g?s^uDwd5|~ApfuZ z+`#|+e=a4_k01Jl4M-q)RRQnBPn4WgoR1S|*=1FS>52j$Rs;EZ^Rsp5M`4dcj?ObjmHIeEQXI?AW|#6TDD_1eo~`nW%8tl^v1{d(?0JS zearTfmp-W89AwBS+Be>nR*5@9@54#6>2ia%v_^WCy@-kC_N%b!z1{P|%TXA^+DOAA zl(tEjrnJ!a%1ozVf!nyYM7gJ4)t@RKM&G;P9}yZG=ho)1qgvsO47qo*S4SuMsElcv zL5lV0DD!|eOktqo^;dDzj|dXFiAhXTj#-chY7hym7 zx+!A6_h?^6oDT{vkyyT_2~tI&o*4W}d-?ER+0)mwmn8Wt5`uDdym$GV-VV{r*wV|U zUM|b2YCFwXF@#b=hF;#Ew(ltK*l2UnhJ&CupJ^$d+r$*tEQ{^C@%qwm5`jFHE==*8%fr zvCnD@aj0OdHJ}A40yL2$cR2>Nm*N+z5F77mS^T1dPC@k&$Q{MD8=YVRR&4(&kh%TU zbZz>Z>B=yi^9f8{|7jvDRNTLE@2eyKE;{f{{Gut~^!_9q^-rCJfAKN`j_OY`=>LTT z@1Jbw8r2YQ`<2o?AY3qXGq%Vp_-_GwC~ zMGirj#vJS9OR*hHMW7ar^{Hy#;C*~;d-_$f@e)_8BSH%WMVOy(`n>5!Slwrg&&B@y zQL3cGM6;L9arG7pG`v7Jh6uR8o?m|hf>nghRK#S{R?a0p+v{mn#rR6JkbNDHXKnt< zR7K)yS|A)-ic-`R=5z0HBuuiR_!151@GQP*LY2Q%_l~@J#o2W9Ov@ugY0KfudyiIM z@eAcWjjYOin~{W9&)ylL7`G$#(fKRI+~ry~h@*Lk&CbW@VubIyS*X{nd;BT|FL7sK zU@?SCa_aL_9?t?yYC>4V%r`W>X74ZD>3I@Xm0R3DAY!ZoxO{KZQt!{>hSkdgsbM0p zV*1P7e~Mj*J4&XFl~zUF$kB{Ia2}bpu9~7_B5xY;9?hEmvNX}lEEkndG?u|oau?Sd z_5*T%(2q{f#|cWHoLOf?Vc2J0cH8O6SF`c3MH-wB&d7}XW^2W;`#wKi<@qA@&4^@# zk)}P`NuYnCWsj9?XEO$jG`xU2`szeMNwyMrZobs2F{h9AC#DbU3f z@@Mznd4|GYu-k(5NA@Pzvc#e!SXFb&1BtJoS5$FR2+di!M?#GIL9bs6?ftD{1((c2 zyGMO!1-gY9RO7u)84>;ygf58clZ;j-&_O@7~$JA1frZsnl-3 z+vA2CO6={|WRgDPDgQV~y$NfipTE7)9CH+|YwX8gE@}Z>K%%pPNuGB}e@@ z$8EV}kqf6jiL5{TA~aRn62KdQApDJYrm?X9No~!**2*w@;>iAjX5tO0gIS6qc~n%s z_|)$a>hN2fc+mcrh0r-!z@MS`cpen#)`Q8ivr~^*(CK)Cl-|Y3!9ACh>B_>Jh!t@G z^)i_3^vbgjcx}nN14_SXdmidHehS!`Y_9zprApRAjv4AyqtGR>DTe*P=jVHEm;>#H z;K6giX1xhrt0gdSRc5gx& zh+2PCQneaC`|b7n@~nA2QhP-SxhiJRV(k_;O#2w7*cF+GSNVY2(6^^`g_Q`sy8H;m z*)~qn-p^3Ma%j_wvWTKSv!HaJH7*BBmf8}#V|l|lW^sUGkTO?%PRa*#Le4;TL=a~N z?f;VV12Wsa(b!@Q77k0Gv!(K5>=LUZxECiZ8|*k*a3W|x)0+9v56JE^^b${8V9|Q*c7UPP>BwniUvmsG7VnxbZfTyXS|UGl?sT_w zUqzA?lTBFcQIWvtf_SW0*lCl+_l(l2xU}TuEhF>$IoNfiG>F5CH*#Yu(=#d)MPAb- zq7x}(K3v;yCROO7IVEKHr$X(&k06Bqw?~U>mPb_B;D96rb%$GLK+qKwEMTnPbDO^7 zukv)3Okzoi`WUd9fvJNx>UuX(!A{rqYQl{lzRgX++0u7Lbc}m}WdvP20b{5{V9Eje z2K5O^BZy|%HdtmTnVJ86L5eD_c_PfmwvqbyMi9vN+2HZLMGyML4jfrRV013F&Ye*| zsSJim;8X?I{9t2rJU9!E%kn@>Eh1l(zl<$ZhS!2HjkTkqIaeTf_zd&+lGO;2uJ4{W zB2r2U1447r93?BZ*u0^-nB@Q&|Z-J;hV5}e&?LAJV=?(n1ywxzCB=7k{)~kN zd6jn#Y(;MG=tDHtn%gP8#hyxFriM&hbZ|{}M9WZB(5BP+e-zDy-Kg8JH4$t5J=`Af zR1v%DwH7cXaD9wRYnqg^BUH%l-I$qP5k7o#KyYiK5;||SMt#@@s@9^>2{blTsv(!Q z6{v83XXx5;(V^+Z?5?wGM(QG09B}3$^ERLe1$t3EV){Y!m6gVa| zWzrgBEOg#(R7*}hxPK}CEBqDDQ{64VgM64C>HMO01d%UzO7AWk<$^BgAk>J2{*tO_He~vYW=aPWd6igsauPqW8uWR8a(t(n zYfA4OJz8|76i(_V|8XRo@BJ{3w)0+4eY1V0(ByH4Il#auDK{O^eRc6w|9L|f#1eu4 zGMcH^0g*H_sfvBT)DE~*v`pnlG)Yl!3a=kEVbP2WL|(KKVnSN-9xc1;7(8GQRYJqF zNH=8}vRK{(DN3+(vbA0?@MSMzjPX+UL3F9+W>cAS?8`R>;T<+LeplnSC&H`{slsS8$B?`xwQg7)AQzmE!(?lHB)? zl&^nb8t(!{@t-(eig8pqUZCgx5pI&bbFQBIcj>^=qY`x?^WuEat~U~!a4>!<(G{(^ z@wZ-D0kCF8t;L`=X`4M#OG_xb(TK$wR z5H`*nIbWGaAA}Z;$h`%5htAIo%byFwh2^>KUYr@ck7f(4@Tcn!8$!;cKJ1XVm{S`~ z({&TZ1;N*7*C+ru>P=wT_WPdv71o5u)n!rBRur_4qzNFvIN)d|bAxUipSJTp2!wSb zLHm1iptIfAVbFB&SN0-sxE9EvDzO@7blRNp`+0Kz?$OC23!JYT@lzJ$U+mr<-7*}R zU@c{P4wLS#?i)p^yS=Z~LVd(*4TO$AdZtg{?W?!g&_-fo@5l{YeJ&X#^cv+D zoa!k4^}AGk^epH0bu|3%gNin!ht#{$=EuNTI1B zZJamH*`v{nQRKZ^wC&wXGc3GJC;38!kFU81ydLc#dz6o~#{X@+1A(6iEvnaTY82l5 z3lg4G1DDTICBVrs9Mvm|ikachd4R?ESV7I6bQ9>-fLzEZqAa$3m+il4{ZioA2LD}j zfTDu{#H;}X>i%<0;m77CYr5XZ(qAD`TxLDdnBwL%+7mLb6FG;ilD~Pb8NU1jVrwH1@At$Vefcr_ z(U%o}e&&L4w`mS?l46TtyTNZp$1dgFc(6X~_f&7Td3A@})D~5m%In4u9BD0laDB)i z{eRIb@v!hGEfy(ltaJ>ojnY|QzY4#xjf38mct5i6{<}wd$>Fkm0?*Vx{wie;@`j(J z;&3jA=j7`sr|PU}X`UpI0nX`oqF3r+0B8Jm>kjoa<$9p>en0hruyqrR;Rj^8dtzi<1(J=!@BaeF#S1DlMmL@qgT_ zmy1p36t1(E_(02OL!zYK1DLH5NdSwD;L`@}1lkQj$^7XDufl1YPYLBIZSF&7m+P*O6^u-p71wxi7GF{ZuDmix(JC!av0{CV#g zl7r(;#PQ58?m|{LXgejo35W4@1JWo@oBqx$oA>((DwiKvNuE%Gg_fZ_v|es1k-fuC zkh$|DDR2G!TOM}Sf=yM?fC4!U-&d*iDJ>1qQH_9|ONoc?pc> zqC@brJjFK?Ld7s$IACd}UKKCR&I1(+D6szhMh_hKiblUg!o6~?4&`I1FAr5OR+7{E z7n(U+#yIwMAjvz3(7UaI@h!(NHqM1G)F4ku~?(*z0I zGeMXZIqGL9dU1PxgH%P5(~}9&hvi{MacQ(VFaxLy<`X_c!@3k&f2syB zNdCA9+uy66@mt;G($dYd=0+|4{2cC36MPO_C=Phl-ty`+ylIQULmI7l;rdC~ zXmq;^HT}ykHDAs3-)I6Lad60=-vCI)^QULG?eqI{^7`9*8Ri2~yM=S<5=*}}g9Jmr z0e3RuNXPPNIiX{0iC$OIQ3jZ4npP|=x^xRmA9r2agp_`N1b6(v_o!s%F7kRkzS0)irpP)ZzFk-^`{~88e(=^ zjMg$QJstuR=Nd8pUF}+rSNzJVw`Ocr3#I52qT8?re1oi5NbT%JU=kuOz1#$=`|&xB z1=DMTYyH@I+!bNUuI(J@rZQjZdd{THR*o*^Y;9BU(}pSxq&xVAuxg!IYKe7Y^vY@; zw%=CcS-K%CBKP1<@9FcRU5=qgzK#wRgcE((YZWWZ0ntmyv*AHZ;x)pnNZK`fzUMZl zTIb?T9PdXjbWivz6^RrHrO$%vcDNxpi_ay|IYT8X$tF4_KMA@RjIkfRQtYh`AmzEm zh9L3<+aHx&l{Ov5Gg#%%c6kz=bxWUxN5R~I(J7!a_Bh`KCavIDSX^$eyYTHq|L{tKpO@xq7^nf<5|zyJ&# zw*|q?!V*W$AQ0l9i7?#m(x?>B8tspcYTzBQP7U%FldGI6nG~yprDY&B+38-+6%6n7U5ccjy&6)SH0Mm_yHl7>kFnnoNY}xU4X(lLSxM;>uN#}TNUwp$XVsuZa?p$o2Xwjq!NpiZS<8}Y=xR>>-WB8>gdgp=v+5M-P3vi zD@n8;A28Ui7dMScx1o6>tRBgt`V}{uOT1j)U97HShNf}9us+c!kk5;s*{*ndIT5ZH zPwu?;^%H@#m&<{16qlH7(Oybeg7z@eAV0hKqQWS9zaqnjHk7F#|NWBrQBiZ(-H_0E zzIfHW&9=n{oiUZK3q*n;ZGpRW!UeOWZ&Ma51I#K~hShXtR2noVyKk4lznD=kDt~>p z=FRBY>+D?|?6ADg-7gBfy}d`=1*5UwmQ5<;E>h?4zfDVrIep>(9$9DTF&_l~1`Aqb z7uZmxJyiEBwp%ty;gMjzJ(*lb;w-k&b=MKo=KSU5`G@3U5G6Yu)-A1YM}(IMSLy{~CE zgH#{iy0T`|C?9Oupdzz9(D_CYPl?S0eTC~q5&D-eVrz(Gqk2P}$5;4m9aO|Md+-WJ zp=any`7QMRc%_Vn!qlyj+cV|j1miLVlN*LC!)AKns*mcExdw~w-;}4Ly;rja(SPs8 zlvw6952ntrkX_}_&(2nW#6$dOMsTdYGsYuqS^g_TD!D=BE+;lDu&p*e9e%5N$b86g z#WudIX{O0CuJKT=3+HjNd-P~pDo#M3RLOvkgqumuWPy9ow>S6XG?e3?K-@{0Zj{2Y zJA(JB+8FS(1Rljc9}Vdz^Q*H4Om;^%duX1Lejop2xfQ1QhvzaTQZ&d%ls33Qx&p=X zyV%-m6L}>uBFsc2E0rD$`x~TkLY*#>q$&7}^}}vrsI4c0JQEy+^x>{=qr9l%#Iw!`FX{F% z+N6UX_SYMHj30K*uPVRA_n=o)l9jHqUv9eq;S_091`LJjEwe|&fVOQ7g(X()U9=sp z>54W@@G^AdJ$rhZ5JS`Y&WIEf-UVSk*U~EY#_fLTBtoHj$*zhD?Kc9}J8xA%lwq7oZ*Cg7^CAhJAQF zkdHs}<|d9Pq-=v}@$e==j-g=4IK2w9Gi9{s9)^#&_SNT!C*tZefo7S;O z79rUM;ruuyaQ7MZ-4?ScYFp_}l0dA*YfuVhzgz#t)H86p~Cdxx4zS9#6|rbIVXfm$)P zV?W3>m@D19WQn+we5r}rcEzc`_dU4*ZST0AOMkjyxt!a8W1yOLYrS)tiC<4{P*l}y zuW`3+H$!rCP?irpw0+`U-M6j9%K1)jj@GUtW>G2Q35~d`;eA~hH!_7}pO7Zc8z_pK zz3Ri4)~V}>?b+|1<>|?KIcE)!$&x(ejbHVD<~{n>GryJU&Pe$#cI$+FZBdFfLs>@^ z_mYCfWAPO^as^Zxtnay()C=UkOf6M(N<*Cd26rZu_1@;ZwYN)sY&!Wsaso#TmcAwY zXp=pEjD1j#f4aKHDeiT5-I(|D_d}*5Y)my5qFV8NPhBpu;*_0Q7KIn;vv->(mJ&Ro z6)kdl`A2eaGOmi~C+Y7bp@%;X1NlcDedC%M>1?A$Y|kXGE7X!IDIeB^KTEPsH{QaR ze#!%(gDesZ*{RTLrnKLce+B1&g>4E#EpKV-y#4`k?|Vy^3>0`Z2abj)hvHK)l^BV>)M z>F8scUV2Ns6zBES((Y%gz|8L`OZIG2bm;F|5m=Lxo;$cZlJi;OWh43i;l!5(pkFl7 zT;SYtTW-QTPpsV)PT3FA8f8qZd+TFj#}$|gMY;Hk`KpLXUMt3Viok&c#Th%8gxvzu zl0Wxb+sf-cDmSnuafhEM2zY^i0)xMtYfyQ8oB-&UV4dZ!of=FIasr#yz%^n9fleI% zq&-#OL59^4bsX8Nf3INFg`B|G68nO`A<|x<#NrCz=8|RA$EvyxG6uVriXRwgnA0n1 zQGQt;rH=E}e0Wn1b=lC?dQZJ&WQGSAGVvm)7oJJm2%>GvZ129!HP);S@fEc2g2k3U z-ykKmb{T|l=;L6(|`_};u%zHkAm7+hF1Uxy(IA9{!1XsRp z45Rkfs2P4;I+*qR%gFg}7EGni6yln4%jfZ4l2rn`Yi0lRS?0YBV&@`ZV=-*+5@CYD z_e%(8>Tir6#yzk5DK*IOX}%M#|;i9Yoa&RQW42T+_t+ zF+>%eXkWM?Vn-o-S;9i_N9Lx=lJzo-%{1IRUk#au5^i#1vMXwiNj*i4d%Nfoh5D68 zXJU+*FVHba*@1tx^p}%aIqMOzyv{zg+?){mH@l1Ji%N@*!<99GOtJ&BY zYU<-|R|PAJMm#0JS*b4?v!SBd_Y1dvwty|i`Yaq`&`0?u8!1MLmc`;shFO@Y^pb~_ z&hW)%(~Z@}?;J%tMQq6$Ix1}e()!!Tdy=LEZo=dOwqir_}H(XpU z`WeGqs4SE}d3{HqG!BjYZSt59YEy%EyV4zLfqK~MezGNv;^|E$EMFxT(s8y1xXHIwB8I0nJehL zG;B}+PlZMV==L&zMKG|~_NRQyc|ZYf+HCy+2*;NGE&Lh>a{)MELjs#;PlzSXX_a3zja z$dV7kyvQ`Q?r?>Hx=yS{V?zYmO3Uc(`+?107llf8W^9LWYGxKw_btc$yc+hTZx232*?{$V^H!ZO) z8eQNh{(z)s6s^RJIlAy*Bg$84YV&;%XR!bLqEZ(1X`3tM#rg2LUxGk-XQ)ot}$cX+W@V^I03&c)y5st{D2`+gWMbs_J) zQODFD-ENrf&)k!_{%OXy$UwYz=j6>{A|YoLNynBYiBLOxQCyC$uA;52i8Xe?yIHKK zA2Tci;}B7b1|+GR>`kn*FU`uUh6?Ss**k(+@TAB(QqMNWu(W`W`sbOjSK`~a+g6#q zq#x;Z@38F9Rs}C7%SP~#TAKN1O!X(=)th{8yG@FPBa2`b3TuTf#8m)y<8OiecA2L2 zu^o43@)q+whAzo7Dz@XYj_@1PoP+{MsYv6x<)f4jUN|69>u&lmEh2ujaalWWOc97K zl1#R)co$d@y-L35)K=v8!h=w7IJ;Z~;>===N?YwnYzx0VbZM4uH6iM)-uaQOooe&z z*Cz~9;}c#;t{>@la*JeIlNp*CMvui%4JxXtzT25a_>O!c4zWt10p1EUngDFKqrZFO z|EPs%{YB8UZsIQjIsd)xBIv&VXde1+eTS>WEXIroxXC{tw~fev2iaNmijdNSzs*Mg z2x1k7L$`#P;2{xpiOMDoe_4LjOx(OQE~TRI72|jn&Uolk^rh;XVUmLIH&)ATSlkbY zgg9`pHmL*MV0tj#>w)y8s7xTtbk|0Wy)5vckpbG*2eJta1NM`A+&(MZ&xaKdFLHp{ zvCEue>fjaMm@IBU1ltf4C%Cxz@^Y(9K)_z4?p!P2lB5h)K4CCuwX^5xn;u0oTvmnr zKs43Q?%XWo0z>;n?iX6tjwwQuC@byOB0@=$c2+`{NSLaViC|kH;)R5j!nT^A)OY$v zL8!CtE;=(jG31fKei)VIw<}02lD*u0m#PjzKFs)+K4?%VC=NCxHb5a7sprqLA!Gl( zd{scqJzjgJc2<^Yw@j*y(ncjGyqUOl$uM_8jicz|gsDGcLwt{Px0wErF_scR2!b#M zL9O7V8L)BfX+1Qw+@~7fZKU^PoWNW!$hO)&=|e~8Kchp9lJ&M)*GXuk8Yr$n+bwp@ zXAK?O|Kw$&>DGClo0KGPj@GjTVzB+Asl~{6! z&E31N+;6`NF(IG`;G-yN!J#HAoMj-+Yq_ec824xY|IefoqE9*gDiBA zaa=TXVRj!_t1nZo;70oezB0|pgu}a8^}{~GnbQVeJrwyyUVmL3juFlUL8~xM_)8Vs z9}q|e@bJeumjf%klCSR^O$RVOmalm!KW#Mww}9<+I(eXzek1ad`7fKpVnfhBUpwQ^ z8wI#9&Fi?8G)NB2|ExG%v)5i!owr}8!Vv-1H3dpm??g?44<`WEeu>%qe=HP>^;^A{ zs3Fz)5 zI&e$*5V|(V2(wG)^2yn44pammedlQ3;^mx;SPzbSY#HzvyIM?oA)iw3^9;rCQIaaz z#Zm6 zUI@Sl+e&+0FH?k7dYZJm8zPJpR_tQx8BS<_5asQPnYfAYO2ZwWlY;uKpx7pVJyLwx zv_N2y&4gIPa5&q@$k%YH6X7QhHzv^~rrTY`DF)as#WJLFth+ZIi5B0HyVP;Bef@k) zAES7hiX+(JK43F@OOz$c2BMf#Kr9GXKKP_U)qX!Bh+hzpcYedT_?CmTGP2_@_5N&lHXT4XxXTV~sAhE98 z2U=Z*uM^?vB6V_Kdk$Ei79J!k3gvS4uQg(KEw-L&%E=|pJ{ab(D_k~<8K8f`DL(4~ zM>N3##-T#in0`>f> z_%;IG9vvT~ig)HJwFYP~oUx8_KEjsB$yt`7z4_L#2;sQk_F;qP#fqwR(kOTTL^S<-7U0jBcMx!uI=)}> zr`r;w1q({cFAgLkfLf;+)9U=n~-D;t-}upJ}we_0iLSxmmi3M zH3(c8fbW5{mvV)9mlF=@gcA;3(|i6?(0`uMf7;Q1y2w9+ktk2rR>0^JYYK9Gof96I zk@eoDyDbd~2iujItHFBMG!)~SF#3;h4}n+CD%~GzAdxv0k?#q>!Cq1z#Q*=pFF_`2 z)F0#67#0rZF0RfNCicI4bTqZW#^B|r;iCEFlc*@?W3Q(coZ70UHWp^Coa%0-uGiag zj?VU+($bEedf-Sd8h&AZ4qh4|K5hTf z{h5b``;QG?8g8Cn_K1sf%6huWtGk-GTKsa3EH@3W!0)?$yMteLXoDSmzwH2L-M4VC zbhV=4<`xj*l(V*TwQ%N?vjc_PvoLcsw-6Wqy?UPSr*#cf_2h55ksmw{M6#yRjZtII z!fmv!-`YC;h+B9=F-xKF&1W^*cCTE0CTBv)x6GGU-wXpiM#j3Vxz=fwpZJ`5&%YNI zv2^EbMrus;+ubgesce}1A+&a!@&o?*IN~WyV}$pGkk3~F!ND>Kt`jm(V@8r9?Po_O zV~&l!{i5%MVKLi+xJi+BdoKlFpA$k6YZ?z1md-;zNQ|GvYN`6O+}2)Yzoq|nV9dII z+-%P2W;xF*+mUhKX&XBY`!9jGjOApXV)U&_NM>>;K7L=r*+<3gV4b#M)AC$S*ic!u zK6J`CD>w^dr?|0sv@p5v$^{cJpX|HX{c+9(8=DpOgmO`A9ZO)LiV08{^Po6Yb~evY za<9Kwyy?7_ZbMhF5Ul3Ub6RxOv!7eTr`3ehF4m1Ox#@VIW5?ZBAAj?o0#P0UqA zbo8kg@9*fb6a^;av>r>{3G<~zVkoY@WlXa{S~HyL^R4&EyBAhjd|J0&_{1(rqwA!_ zq`%Rp6Sa%t^lA0}A>pLP$!L~EdFy~3K_S-fnVb)uQ%27cWE1)tlObdNS$si}v2LGBTD*E*Ea=K8Q$@gge#=9>o!}GCYzJ02z(;!pu z7!uZg#}=R669Wv#XT9b)XH+g`}RImS64HAq8VX|Sf_wk z#3{Ol-5+NPf18yXVV3; z@Naj!hOo8}Ix!T!rw(i+KgN*qTy$nts&Kcai0*Lu7A_afmWJeG$HJS=(;WQ17lQyb zZCiA!cP!`8sL{vfJ>xf_EBP-5wGtaUc>ua4VOqqa`|IHbO`Z1c<|!x z`{vua7ziq2TknQ#bfcB?{J%{-Bv5?qIwKd@EmXr7NEVw&I&4XH4^O0;w%s&Nlr9Qal7#%5v}CDz@$^e^&X1bWVyhig^p&q-Y<&Uv6eb-wvvx#9hX$duegzjp*A zf;xwdZ7cVmyGd5ePIKO(s?@m~M7#+@&qdk|%G^892p8J;A=zy_`)zs?=~UO|b56Fe z??%S>k{JO@z^N=3+9^Rj`Ok8ZihJyvCj1&zwr{^`m> zf4cJTpv)y8_}_r?cdcnh3jUiCR_9LEnMmglYcyvCY6r~CLzu1oKEy_b_4ae5h9IRE zsIHl6S=o5R!Dw*rjv2i^ws8%0T^4C3hMDPo$40+o(!R(CaGx5{v$J)&Qc>b4=xWcX z=}aTE_I#UjANKCy!uTTf{_abz!F3ux$NTB=lWJCPp$Em9Z%t2@96j9&|SgU^+){prU5EkA3r`?g1ODOmwi%0pzUS6 zz%tJ0o?UB~A{6n(<=OEF#kvYAF}AQO6v&Q7sF-h^H}y)K@+oQ`cn4!aWf3Vv3=;{D z8h0xG5JMHR$UOXZuKy!t`51Ci{On^Tlv{zah`aP=f#GD%Yz0T*7*QF4CoIL6cAt8e zsy@=#zn1G`q>h(-K0;#<{H8dpoO_Lf=(bJZrsNulEFC#Aj@s*fE=>dZbDvT;vqj$rI2Gm5W>cl!hL1d z(x_N`YOPp`^9tGQTLat?m*K-Mf-P3H`KJMnQkO38FQ$uoZx(c#Q*XRXW%AV@K#f@; z2{^DVz!k13oG_3(Bd7barQRrGbKe}2M2%_jSg)feHapiJmH7zC>1P|1# zk+`POQUTQIHQI0DMF?uj&X$)@mqYBZHv4d=S0?yJmbM$WXA){>nyzT71eq5zGz!a` z=ZXVAv^P_U%tllG2#wKSTPYx;e<~(zY~0}WzOCSh2Zg$i`P7K#@|=BbD^b4_|3fr} zW6#Rm9DdTSdd2R($BUOn78Z)nw+WUisoBl;h{K+!3si4E{mM=|Z7<;Pum>IT*l>pc z({rtoB|WHK_=rZSwVH)G8YX~vzoBoj;q$3wqG^X1O3N#fo#t}E$%611I*z-)wW_ADVH?rw}Nfi85@>C(q1Wa@mWcDJ(#A{ykO#3XHb zHf?B>wL2}7(OG`)xfTlruC5*j2glOcZ1Dp>qvVFWdS8~utvyKLsMOpjLH`&ph5rOP zzu2k|O)M=qADTE@IJnaMYFTosSeRRz{5RGr4Il5X$R`7AR&cHh4d3rJ<{#k7cWv$d z4zB;uGFG&|zPy&TIj~3txp_GiEUYc9TtVSnoHCAfj?U^&P0TDf?^(E8n^~yIOLIzF zyShBIaF%hje+s}I-16US*n2XMf3;wLH*k4{xdr(+6~Wy(KXtq|aA{nf-Tq|X{uui) z2f3*vrzi(OK!89Hz(2^3Nr(&t1qm4i83_di83h#;1r6gS1_nAh1_2H()=gpp5)xtp zA|f(M25K^LItn5p8a5g_Mkba!ETq)zTVPc1djFv`yohp$oRLprBH4?GC`$vBH(!*orXpyUD`sZHn2y}Yw8?;jzL6B zLQ2NK$i&QYhmT)CP)PW$%sp8-c?Cr!^~V~TTG~3gX66=8EUm0V*gDbpr_r5efCWUI;flz%L>m z67p?s6nv>is3uOgXnCHa5lBa;mA0VM@v7|+nmP|)5Yh9^Gwfei?U$PUYZVLlU#i*P zD)vXcCLmad2;k)*;z1-J=h=TsRB_{%&n%zt@m|j@_*2Syb+$SZz(S2GXBSwt?+bGK zW)JR+W270h>k1&f7owp!qPZYDa_Y)*sxbar>WU1$KjN9e>=fbu#9x)zD?MM9;c@8q zszHCz!+F2roV64*SG{e+9dZ$&h1)Cd0zR(DA<|zY0>bLcoH?U&vLomvhS7eJ)|^}u zRoQ%~JuRlHK+l7!NhjyUW!`rrXG*wg#7N2GHyRYIE1`#*j({SPaCt|gbHf=dRh8F+ z)oK~TNU%1ge7(LhxW{>g?JWUE=7fztx=OMH@*hB4=X4#~8o4zoadun<_$bP7f@5qP zBREmQbLg7w04Rk8+7aEz;za-S+?qZ>@Wm|PYK63ksSK(0#?${)o)PRgKZ;Vc5?wT+n(@@G?x-;)n5Ebl|~6z}$xe^e2_@`M=r1Ocy~>f^YulOCPX{*J_w!Y9BD zoB&Mbj}AE?$e~you)P>o;Sm9K^J+mZf$-V=|BJh~42x^o)<&D)9tiH1;O=h00zrd& zaB18njRXlU0TP_x!QEXN39iB2-F@A;_CEVtJL{ai*Zs~X&wcK%?q|;KnpN)@qpIep zG2U_fh>T}=BWU=wNPPFJS_h_UO)N=N zTy;+phBzn+hj*M9&H;7gzi(^L<*mEo^eStp1%HRBD91J;AuL~^n#RON z^F57w?T~AnP%J}I-~QRQkctsM=HW3JWHj0g;AhVTLYm&S%|8J|30YKwzkv{KfU@gF zWo=WoTvzI?&9!yrgwM3qIfOmAop`me^a(i1`zSWBr2JMz>mC4_5DBTICyW_D3pKP( zXCjn7kxP=H)ZBZ`g?F~iGu90s4l?b|iT(nEptJ2m_Us`w!KNN25J>HcR-{?weN zr?$>j14j6jXtnOb&{f0qoG7KF@%&L>9D9br=L705{~ndYQra_cx*>xcPa*hIBy`qv zo!cKVe5HQWWZHOwTV`S-d_)tA{{5HX!=^fy?OZ%q>U#*Z?Se)dvT_KynIiu)9zUM5p@!4;uJ zL@f~2dZ`!*bx8u4y5_dh`vo zB{?QYzX8w3h{3H~!A+-?=3BYibB!tRNXUNXqZ^Cikc16rb{?^2LL`4|`zN%b|BR;k zL{Hi7nO+y-R~oP&I4C!0T~0}lkqst_A4KM@QGy2>AVP7vLw@!Txtia&Lk-wxF0Ps< znsKd1na11Y_ZjkC>{Qw|lfEZkZ0+jNb{O7bZvw^)q{lxc$jKLU{d>h|&gMFx{tI(I zrW?Y8{j1;5^RgT^1s_-tZiT~iLhNs@pFW1OK9H6HaSPvxt|NskvUX+~0b>mSP9bI5eOQ?#=ayaX}I)*baY}@5fx0@&=h^Z#r+gCWRp*Ctq=g4REGtcPk z#>$=Rw|3=!ua6TI)7&XT^7?hbOFc<%>$TqDn_1Ppj&tomaaeKRovXPxd_R3Jqc25} z1FzNgwIiaCG9l!B&U4=$JlKSP-}0^xYcWrqF?ZK|O+(a{jcGK( z@^7F=XQfAinuV^Xk`JF<3A=ukWA-lqx_<@?hx{)LN7Ga4uT_WcTf4)bCxaqRU108T zF)?Coeqo(=x68CTUtIVjG8Oi|6%jG{;%=pt$rqUHA+sGe59#re64B2+uK%!;|1|$K z88$Ksoa7C;5 z1F}X4D?h~HNHfon_H`09l?}B!T&)-e?FEpjV-+Ro3E@8AXkpO)(yqY?YZz8f zV_Z|Ab$ly|o?1Rjp!TLKfPmr3Sl`t_1P;h13pz!8YGa06Bns$DyHBb}DF^%)+er+0 zXySFHeC|Tm`@6gpd+M6akG8OHP*$}NC`2aW%hR3HGc~Y)@G-mJKw_t_8)?8`wuwcS zi(N`fGzF}%CW&^i{gvM~sUluEMHmD?6HS`5PSApksGzcp@A?s7S7w@ZBjhASmAdChEq^<7j$K; z&CYtqxNKGQwJ@65Dbhm_Nr1{F zcWIFVGar=r1lOOLSrIv7sXjM*$B?Kjw-MWm;H=RslfUQQI2`0?0beAZzsTL4ZrY>m zA1r$UQX4iMF_?rIr}|Fu4n#))a?9nK9mIie*8m8m`7SasP=OHmxS1@-EcQ9T9Vf28 zv#W=Z2U)w^F}#9n1~}Vr6TSL|=#d`MP1g z$zLsDVV3W@OOF(M!&H1ppBX;rn?In5R+bb>zo@@pSCd_w>y$~YxPlP- zZNuy=S`OO+P0qWK}AfVt72xaMVCif*)ge>1w1@Wo-l6h0XAY%BvJashUh-6@6lgB z33DzQ>Owuu9Bt^0Nye(dHRTRnh=>(i4f~PLuFl_tylH9mzu0*wzc-@NR!5?g$EUEc z?1u`iZBW1#q}iYw)PG}!#`Pv4a*gYE{)W=I)iFXm}vJQ)e$-WI4w}-IeJ0Y>*R$YSCWRe zv^Sv?#Vmbdpday-7VFmd6U|A3px7`_)6$g-=0iOWAec?(-$3slAa}}B%cfp4&qAJ9 z6QGabrvTfaq^gOGuU=cUfF=1Ae(jTaqffNXj~Omqu*Ctu*1jq-$gv=^(Wi#df(+I3 za>=7QoGgg_Y!0))rmSSZ7VLV zA{@n@d)UMF8n9fP0~RWl-41S&or+)SKww98kYNaen?C2RXC2hWK zmrqo_mmWnxV)Ta+l)#T4ew)Bos$R6Q4W*W<6XapdyvRfw2_ZkD@Njmzba|uZ(l6@t1gAC9C%>3Iul^Hb!Go0&>VehD>>sJ@pbVKO);S3!^(2B1ci3 z8bEf8TY#N;kUKt#ng2Myfx%_bv5iaD-x3B~P#CQv;=X6a6e60= zF4WztQ|b4r$c9#Ogg$@Xna*zOHk74FQaEK{+o4phFT-f-eeRK{#5s`OA6z0&0=^RcLC@UCD)8kDCFR%_bq zebxX1v9a%JkazJMMTEVdjzh`dg+)%@{#8zs!_nUuxw9(fti%Xgm}qq7$K zzMod|p%t@L8z9gAaS>a78Z4_+X|QB8eHL1H8LigsF1Mu`&< zZb+z0+A|xz%<-X#5l0SLJH79>mCb?FgM__LUkRbM6Ik-^s>YMnAZfn&FS|%-k5Sdw zM^YkEVdo>J-P&??>I}##TC2yi0*DU{dgk!`XhkjG(UkJN^q@ZB9*3A)Up+Csm4>UT zuhkdQ-m(dnBMdxR5R(wqDJ$O9dCJfKCSX;_VXds6BjG+9`+DbQvFhcQ!VwVKHI(AT zvJ}~;HGM3u*>O}~MLBR@FnbLuQnFOMG*1T0CI4`}`#>UDSS)jYN z_a%lN_8||?8Ap@&Moi0ojxW`<%CvFYArE%MpvVLE;gIIqp>Yi=wE)Y{qR;|B>{Nj)F;6WHX(?Rx-d7UJ~R+U^nbAUg<+ zo1GEn4VdBEPt5KBh0p<^Be1(CQotIs6g`8R_zkqbB>D)afgfomHRVG^=trwb(BBIg z+bS7?i#~WOnsMt6c&Nm2nHPf+*6EvT>Qc)VIeJT?^Q745_U01m@6AGE2&bpE97O#c z#F%%}9rpuw_3}TC4^MPFVNTIby?Pfs3@WeA%qcwy?SxsZY=^ng1mk@*#!>CTO{9C2YjtJ0Bgx2wf+Ui;=Epw-t5Q`pBV;eU_=Bwn_u?&elizdS zL^7p(N$n*vqEs(iY>3I?g7(7V!k&muvt`$hv?Xq%gB)X7Td(yz3~$%*i;Md6>47PW z;bXSfSylU~7Tn-~$^;{n*Z@okI@pLCvx2wo;P2V9L(kCPoHqBy@eoHGym=^Al>U$& z+@w=vVPavb^>bbv;kac}8eddTy*OG2wDLg!j~vBELit(e%XV5qk@wR+UO>w7O16E# z#d~%DtmvoC>Q=s3AjYu@kVL7~Z=h_vrcl|lnH_ppa!J%q{3WWEs;>_!IdfJ?C^u1p z-ep1@>xF{S{3E!-djiMZmNlqf4uq(47Ic^*C=)LABX-jXN{5k2i$*Ky`mVlpHVk-3 zs-1!lC->JP5uoz^$^_|4_aiJnH?XQrqAu0C=b%wwS5K;w>tL5fNhOeNy01*XIp6l? ztYhG@eyvOmA=#uBiz6BFw2-T7kqlnjY8pppdtj@FD=YWs^$3=Zbl<(@2yP|3pZdCG zLDj_Qst6LM-r>g-*%q0^iXO-M=29fyD#i=vi@bU_;VfBTvaD6oS4%@VDqMx%k4+WM zBn~$G=c2<|+3HV}-$_T@1k8Kmbr>d``bXJ}Rx$?Ol*)WvIld=ghQyB>)mV76xoEPN z01Ws*o%rax>y{@KO~st|6|QeF5APO(^Xjhg&VJ^^LWvk~1yAc!10WyWzhfz_Im4XX zcgru#4niN`ruuRw^CmL`?s|VRp}kUKf5k!0t!EM@KQ-2l_(YeLUwqJX@WmHK|zVNpqT&JFL-Y6|LH@iN)Nw&rw^e zM_)PH-Fpwbo{5X9+DKAQ`|*ilLi&wPlO8>QY=(QCrh%1aA;{YC4S?=M6p#l|Q`FLT znd6^26V|q2bQ1FT(G(~{Ah&O#F%h_&lz85b_Th@AtcdOLv_2hU`Vs>v{z}4o9=47>kN+Z)rW|WY_JGhcWFMujWwq zwV97%;GSjW>>shrD1`r#W zx~Fej9IeInn^QoDwA5)+5jX;j0VgYG{MziGu(^i!ajrf*f!oVca0In*V0 z%W*bG#9FI;+x${xgs@JNp3q_jMo&n^s~x~bQZbPlk%9$%cn2f==XZ+ZN?{xS6WWK* ztf7XNw}5Ljr()bYlJpWsMM3=E9cqa0N&#g@9iJFaTm+sPObVbS)?OM}HgGZ2vwdMY zJZ4N7n|6CO@=v&z$B&o>On|HZUVA9u#&mB0yi)<&@W9M; zcl{Jnh6mJ67JaaezR{d%yAbxM0$AxO0iUG$(pJQnDLxB||fKQ&yFKey+0=Q^~(wzN>h z#n!i~BZf3Q>a|XtD}^VzO4}%Uv5o%}@{$=Lg+JpTy?FW!)CKsW3xLxXU%=BWITH?) z2Pc2x);Ei)GWIT5)SA=p;+`s97U7#^59e>Ub)iKJ8yga&)K3M(bY~0I7dbQQ0l*1&;L>{{>m!-rBhFCo0d?F0}0@&wKT(ldB6fOj3`k z{{XqD_5VZI7ZyA)34E&MZRI>wST_G^N4Y2@N)w*S_PDGPZxH({0{>w=|H*jM_xCu(&j>+LeO8Otm)ILPM2O3q2AcpgK>nr9eIu! zX+i8HupR4VIK?4u&XB*7{*mSX%mns}nf`gEyZ;UJ`H~@4ZZ-@#rn~sxp5~JN@aQUF`t}E*hN|H_75Y=m_G6= zX@=d?IvbS|GYEtxEp_w^dN9po`f_llBqXk-w=Apx<6CpGW3r%(O_TQY1>s8>m=qJ9 z^;9lXSN{3QsHVoG(8#od(2AV$ZetnCt_rjR7`ULhdbE7FjqN(m@(C_T6)?fGy8{RK zz?`^7_cYuJyhQaZKi&h=eP{vLmHExSN>c!%pfP&MsQ+TV;8z99d%yXaZITZ)#DC~Yfcfx&tt?%0%PCV+1YVbaZmDS(j3>-Av?SE=n46|!z6+t zmwrAJlY-@t;6X)rvmhN6`$yvF2o=kkTrBLNT7(0pT119oO$p4%r&3vHf5S5q#&(qM-1ainho#Sz#tM#v&M(16(mL_QL%Lv{aLq9&76;Jp^55+#O}y(bJV5*#J5J|F#esE)Wu=RNKm)5_)!u ztumS5GNQ-UL3qV&7X})Rmn!#`Wp4JXkl2#F!Y(d$PDI0Sa_3R>>>tqyUhir&>ws{~ zf_-yiwG-s@;048dSN#JcA~4^~eDhlarR$;K0N!9tp9Bf z-Kzq|0m}4~kCxcogm1AfVx4b2SO1$IMQe5N-MFr94z@VM=U8MuGhy)_j$@^|rW#%E z&m6CdOJUw)ybH+ZAAy6t#I+6s<@)C78Z;|86dN#obaj&kT@tEdU?9jy`l}y4h~~{x zG2sn_rMUSi;@c$YpC_Uznr!C4WweeI03vjS~^ak?De-e1p7vh%Zq+vO7;}z@%fWyU1wLVuvOYnmAA#_& z8(EjO+lS~&K*ip3xRSyRuJ*r=`nlDVOkm=XljH!8A&p9cW_nRrlSc7f7oDeyspEV^R4YABts@oGPRFcqRGTSB-5fMAWIPX zK-w1VB?sQ>5U!}5jib61ZI0P~sR)D7Tt;oVavGKKN2Bq;SH{10*bftf_Wf_)VTz>8Af1vxpu@-p1PEaR2skm40D7j_ zr?yoEbR9;3Nz7HtMev9j|&5a743twQ}x5#UoM^#R*%LYdt8_ z2o*e2Az4Szf>=KXrUA@a;^fY_Z-J^VPNOHqLsH|m0643UI>%ejxq?pM2a zHoAM|^WDU^K$SZ0GODd-J%+}rl8i2J zSPSl%Om;ppBXcf`et74U?X<9>;Oe6Em#tG=kFdIJJ)e26V&qD-6+VYKF1c&jw0xWVb@D`ATTZ zCrKXGHbq~yV^}6A2 zxGtS8+?_MaOSQNsF&7n)Wcum`9`;I?%fzsKTe`e=cbHb14YsX~le%)d=lKoPLI5sB zb>d8o4ICkWvU>#Tx6~wQC3ec0hbz`GjX^=PmEZA=sVfKx=sTS_oF_F^ta7#I4D}6k zy9XUz#J~QCaF1c*qb0#$$UP3eX!==d+EBZEmGx=jha*Wdc#uL+t{s-f!hctcWT~Qf zA6DJ9swn`~h>0g#vVLn#BeK$Y7}VFx4{ddB<}bD8)w$ftXp~MUwQKHQdyTOVo5SeW zO-D-+Kcbj=x`2tf+ibp&_>3~1G5$WgQN(#R&MNN4UOo;kbRLY%I2;w%*eDpM9T-?| z8lOt$EcU|kg~40Efjmcty~eN4#~`QtK~;?AnWh!>boF;)K`qKYJc8w|Mxj&)j15X~rq4aU7p*>W+GZU#FY~;x+p@5di?~#%phc`N zc|!+FhKar#vpBcNr?o$F;QAs8flPyeLf#{JTFCZVrz2fzH`}1P#%jh$f?bx z61FDvK$gaFSLdBFg1m>jg3T^Va|pI<$AM_V9D1ysqRasAOW216T2x(-yL(b>GKp?qs6%aWM;^ z)4NRiM}~d-)_ukqgnI2DM`Hp@uf8N9YW%9r{q}n&lln&TN6NhfY()vlcx+o)Z=kE%9 zh%B*6^m?0$=T`A^0X_@|DSJDlyfm>Yzb4^wUN;wZAZkf#c0097TIMOhMmmo89}I1O z6^@R9+CA5;e6xnm+l_y2<;7GRHeJay^h|F7&uC{n0Etq<*om|La;^3vO#sYrb+$$N z=ImH5?xp2nu3Noj>WjDtrwy#Si1bv~0&cM3TY=^|)=%KW>2Z&#!;L0oqk2Tjc;>TX zI1BqMIQL?E1f5x{bne!Qlj7BC*2>!?ywUzvRGYMQOGR1L!TJPJwtQGF7)Vzuhd_E& z>HJ8(HP7kn5D9zs9KZ8t^!gw~!UZ-=zU>wlfxJ4C+?q;*9~$0?jA(R`G;z;4+`a`s zVdMRKoOCoQ@^_YaPBW)iDQY}l)h2T#bXI@5a?vZ;Tjq%E^(4sFn?@gr05j#*SB|~b zpgIyP>Wdlfl^8hf);d(p0uD#7lcFwQW@oLZHAmkvy+D(28z{{sqK;Ti@MG*ij2#Ws z8uvKv3eT&#+jAT=FK!=15z2(8;Uvvm6^FCiJ=O!FSh*Sbh1}OZKTXPy8daMn46uIr zUTJ4G2Qni;Z+{OIz>QaE8LwHcY{^;EH@N7dHUkfs+ggx{tBRt4W=@mBk|a(_hD$80 z91GxUZ9-dr1dSHx1)J~Sw>nSc*(ga);XTcvL7ghO-W-hUHd`wqIB3Bv6?i-?)O8&{ zpVyI!a~TX>kK+Cfl&1g!sopqudzn3!rrGD~OLKgXci}#i4$r!!mv;nDXQ>z)=1fO>#-}AR~wX zHFVH*j}2L55~irY3W&f{)D>2|LoL{R@g;I2>eVM%jN;;MtaD5#E>k+&=(POogmRP> z(}E0_1LK>$o8i&#$nQb?uzbgAx*m`>A?hI-mDP2@t45Zvo&J*2J-Udbo+{CWr8b@T zI$&sb8IfO9t^x-OXOPe#wA9@(s$re5 zu$a4vaXz}L`wZ$>^#e-@wR6)sphNGO7>#MvSKQ&7yCY_Xi8tk8%mQ=McPG-_ZdG^1%Nw)sv98u!ZqCe7;WbEcWd(O7 zaO6F@=dkT?nkbubvr%pXNvy!SsNW;Kk65&;&AN(Bt7)KKRr(em$dd>sY_{*&c{)z2 z(Kg(BKVgiKZXMUfZZ;@Q!MR2Md1f3X3&ID90a(Wo!WD~Ln;974r=Zf>FU-wSEcp!j zQqFLAPGZLmx<&8`-~AL1)+3_wnodWR;YnS~l4M%Il+?bjfvgS-ZN=yPfpsE3dz=ht z!q#7p+p^jo+Z*b5r+73cHF#=lR29VXyr?6VPc0yJ+x9~$&F-$dCNlWs?rmW{Q`MQ$ z(^xycPA#-@K`1P!NxaT?p+06Y<%TZ^mI_%=A$`ND$@b&+Jwzy#~FSScv&{jnq8CLyQfVV zwk13EljwO)5Ot>8-HeR^WbJEp?@jjVEH^z<-_vmO<6?(F?g|vK750O)ANPZ(v%pQ! zyNS&D#Pc@+NNMJ=*I)9?9Y&9ZYH_HYC^k@@Ch3=+)QfniL@_ zTU8jEguN3p{D<0J)*UwM-da8!@2r@?>}|oYDITe~X9Me-n*2WglTI?vvYT4UmBN#i zbyJj=XRj&Nq=pqnV;`b7EMe8w@IPgAJ`qlGBQe>PZU--Jh~r2l$RcjJ?clfypN`jD zw8*iVh*c~U*`g`b9uqvEf*8ryTuP`;Hg!bI@b`Dm`6?yi^I9%Ze0ytnN5}n@vnJsb>F{^U$mMlM%snzj=Q~0 zn0yg`9lA{&cHo2McKw7i!IE34iP>4OT_oa zQKR8H#z0V*SHk8fNt>;3Oul{dLtJ5aL-&jj4Bq_uGLB&B-F?&mvSHStoTHV!6ONuF zNtU8mtfbN1x!Rc6^T$N=gC|vcvsqosenX{P0YQ&%{83zzPklXdQXs@w<@vb|TfO-% z65Zjg*-X>B)OVB0FbatTo6r=ZRslQ0{4p^k9EKoR5E_h1ZZ+L}9F4{$3QVD8Cel_g z|5%)E&?seiSc{4AVwK+?jb<{SU?J+_noon3^*;2wZXqmT zAzdcYx7uKQ_b?h6=R}4~yt~P26)~6s>dM+Ei)213xr}3hOtcSq`QQAgygl^mgk*>I z-n~$aqFetBq!AYQ7#rq`1A3>bD@kn^M`M1;fDRO;=O!jb4ANZH%KmmAxoIU?|b!w{4Td)`Wc#JSU|kD|G-WOtm3cYcRX%|Gh1%D>@7 z{=avRgg5#!(rd=xWC?iZDrVlETyi)!5IbiGT}N-%G-%U_J|M>y-4R}me!zYE8wk4V zV+Mr1^R|;8FGF_1jygb2Qk1FWvm!K*QTHzL8-^4hFK+A#^Aq_6=?U}+1t=o9ez~;a zH5R)^S007>x3&cT(bmpAVt4c%#tq<;g;#@|-W@v$OA(S)YhnC1f47h9N*y}&>9RixvT`YEs#@Dq2uTYmV^v=FT#SML?Zgoya-;4gdw*JK_Meb|8wQeoT8 zqkAS$2%r=wCg20e}O}0mv5VzaW)dV<{=NnMN-1u;v-JQ~jB`8&A$wE%!?7 zy#D^?fzkQbj_=$+pphRaEuI2mAiE^EfB;5=9Sn_bVgc{>G8$y%yZw`>%&$V5@diW$ z3|C7A{x3%GmF!y*^>jE)$Df8q{%iaCM=}N3)V~iDui_>K0aEurFmTSmbS(=_=Q!yW z_#u!VT%syx7Civwv7k1ov=DLkZ74Hz;z_pwXbUZX3>*#7%S+(UE*De2V2J!L5Npzu z9-dYU3=A9(a4D_IkAETWn!i0@ zWo5K9@^Ak^08+gEgoo)(>Gm3HNHgbm8*CdgqBJI;ya_wYSB@l8|dyj z8W1C{*Wbvf|LRref5&|c)Lz3gH6hri%j>7m{JnD}o{Y6frI7>{u3kvxw?2uo1njmS z*@5-(sh6sV)`h1icN2;?AuEDu5?iRcuHK{LLljbkA7T2}s!LrVhZyf* z!zq-e$1$Kw#1A&Y6oetxLm6Z3ydad@xoN^Za$#{F@8GVdwO$kj!kA z$R8&%c-Zxh3ql$2h0|5QbNBEV|+G{`%4$YuDf0%ri@OC*aLs zWNxt62_iush{XT(U0a}eL!8Db*%v%b?N>qeYd;xFd8*JZSG`R52#4vzdc1Fs$mM3Z zvxEj+O`K_eu4709eOxy-2>C$FPk0N>>|D~*lgrt=Th3r>tZs^tBdu7Ff3qI5?uilqjb-Mc;wa8{$Hd`A>g+$HsgTYdWhpH#3mKPd6 z=K@bc!Tx(ssi~U#9F?M&Z+v-Y4=pViCpqVf{s-(_$!&oKyPG1^6VHT-1b=`0oK!DK z#N`TEeIFuW)+Gd?WkTWir+aF+e>CjbtPYQ0C){rrc8tN`6w?Z}2@e@R=C0uCJR+4_ zkAzK2*K;RRN~+@%{QTn5$Qww=-z&tMoi!#x8+s6SpEy2JS8hPOGWyIJGycvY@At zTL~Ta2z-~Doym-g|Enzrk)K<` zo7LrwUJT2UY}_TkugEaUBW3JtET;aD@3l9>I+lM<>F#Q8BrSGWj>7bAepdu{iaGf4 z&3Q@^x!1aHQ(s3*ARYCD<+Ez$%QpD=h1*k`Mu}E!KcHdNWsmPio$<>cSCML6n-8e9 z_2;WIEmqE?R6>%Pr#JjXXgg=A?6 zoC)RYff%eH-&eZ8whTu+ZU)tr*yj%oO1BCqi`)e+>{{Sd3kyB&#R##Y0+x`LsnvD{gx8+LqSfUaJ>!ix%4g z{8XgFoyz$)(jA0+y^B5d!#3uucsHsvKgrs%sl1ps#2^Axr|v;iZ)5lGIP$Nhe!uX( zBDQddyJaa0{quNU3{5q5Ulhc-VO9Z1=RumI8WuTP^T=sS_V%9NVcLr2vga9nOU3^< zyanN+f5P&|aJ+Vphw98m_q5_fQIdE)MT814EWHLjTK??{D~m8h`W<;EFrZs=zlvyY zuIcM*2i><+mTi_bFJwz0&P|Xn-yuOBA$+?n(Kv?kOahTZEzOm$K3x^LCW>ue^J^}J zL4uqp5lMOL6z8#S>N;b4HlbxZgJkU02ERYX5bs8X3hPx{Hj|MNU2`a!FJSfhXq9`( z)oh$1>A{?Mx|B2w?f~y^C0ht-IF^CuiI?Z$2N*p)?YKHD-PNfH6|K zPgz2nsBfq&Q~vD)B)im~V7Ry*m;qdrBL!DO{1nTO-S}`BV~Dr4?}; zIL8Y#H~D&C}wE5ux@x&2mDAbBK2cPyYUi-7wAoIu!l2Hhm& zBSo;Rneo8~<5cvNTF$>zjv~9`9f*4Nk9(+u=`TaAlBzm#3u5_B2cibCjIvGY38XU(wWF1`d8%2UQdis)Gl4ta=X8Tdl3u8k zf1Sp8E+<>Gt$w=J7W0fswJh?n=Ntlog@M%+cLygSvg% zue|oar#;8hG&PoTop1x3C&W%F-?4RPSQ}DR&mmmsiTxf=h`0axE)`pFJ7S_dGUK4_ z8Du0nxb64{`KsJMw@&|r6C<4Wy^}Vb?nt(-)`xKIZTOkU8Pdx-HZ&*Pq+JamN+#Y` z{ID%ltDo0D{8`g<^amLWF$R$K>eu-Q_N6|mKMexG@i#X%PY)eHIrX0(fSBD3a&*Y@ z7tzj)mH8GK<+rc*65T*u5GL}5n3%A75L)KQhZFcyU%-w4*@Dhl2|9dOryeqPI``<>J${vn+Uza@y?GC3sq%?Kb1h@< zdP?nAeEous?YmJ*tNLwMhW?`4&@RL4EzFbe5DXDwD}!eOsREn>bG`>f0`tSLw@e5~ z=#zm^1CM>(bn2#PMPCsbT`lxYXzcdz*C@`KFQ%rZaZO@<8&B}Fgc<)V6S{wf6jxVk z>ga$=6l(;{?ZtwX@@=7Lj87rP_#52Cm@bmlwoqS2br!Rlnc|E6z7iIxNmW@gq&EZu z;7xz?Tg3wlYDC!76D=vxe^vmQf9`vUx7BKjeq)tNezkHJ@nMwMctDT;(wAEqSVW=o z-FNw?2nm>B}N2!GH}p!C%bvnh?t=kWut=mTv(KL9D%{}6cde$SK8jpr&k zk<_<+1F~_E9>-@xcv)Z@oPwMAkB3tKyeKgJ<#4jdl)tqP!a)?nM zYO;M#jyqdv>ZUFhm0nlcSlZ!Io@HZIQ};(7ajcZB>WX4K;9IS?GM zE9+xL9p*&U)0`hmzyM1APh*b$XJ~cjK3{oxy*91Gq#4=lA+bc2ou!36f!kOCVb}rWJ4JiggG8qkW$z?@?-W-oM!hC3qQqefXozRz)8OY_V9*LqN953zW;!Bh3Krue+j zBhl1w3B(~r5gWUk`lN3Bs?t~L;5GFjGSQFa20^ztvD=F3)WUWChn;c)@?W|ev-{7T zZ`8ae(vyK@qSA(8s3)W_p*`t)*i*Adz&rvL^j0Z6tX1&oi`ANtNY)4bHFQLo@!$YJtPj!uy=MHwh;*T`?)d!uWF2m!2$nTB~(6p}o8c^X{^mAfu?S z>os0z5JVqN=XFM)#H@|my85J@DMq0~cK=7vGru$p5J~D2Zc_0B$w^swiOy+>XjP5Y z(Gi+^tr7LJs#adsMI$zWh|}p<8GVeeE6&)2wn7}0xbS{q&tQ({ez6_Kys&8+f4JE0 zqwlah?Hs-~RKhk&uEF*dvbAcBIhtUiTRW{)$KeBPF*rXA86kkQLh@hEybQ-zo6&Qw zbipZ3(y42LAzJT(1`zu|o2?p%eqK}e;X?xG?5+r}y)}^3*FyX2`C(*Hkg2yw?wrTN zVM6}3ASdKWi?ndZWEC9BzPx);M?qpiu&ak3?Bjg`?M()>LB}8=-+F85ZEcc228RlPNAR34lp({(ki`A z_)*dIK(`|v^bp-Fn4g--RFC+6t z#2V@8i}^H!aT`^$p?v}(I1e`-SVowJI&d$7G$w^9S!)<*gKL#U^f@?_!D7AtdA}S9 zGTJK?PHf3qTpXyYXkIoC6K*2Hcj1q9ysn2MxqypZAUqhFG<f}* z#IVsGKUC_D#NPPyu}W(R(JZZ3%#sZO6E?oR@IjJ!>c^au6*g*-t4y5y>n1jNjuYH* z#T@!!X=!(~4E;wn*@??{pIjMb?KEC3q+27*d6{~tF6?UXvP+U&l{a>>P16+$^@^ae|G zbD98cUdvXDF(2imA!_!yoO2@K-< zb{E9u5|GnXZIXNANW@xYD_&3;m8wtq^4TtCJ@v`=yU2yg2ll#_@!XU*TGWx)jBct; z8d3@f3~~~Pa7HmwuFJDTD(P#^#&%})f_Wwcuhb@(_n{^7zB#jo^WoluTKRP#O!^sm z|GxEnYtX<=kO{Ce4d5;~QeV^B27%+(LqTf+e`SyF+kyclYoj0Rll^EWtgvySux) z2X}%5%U#*~oHILn=FWV-@7|gF2eQ`cuCA`Gmg=s4ioR#0W$EL>$L$l5AX>&k&c2M& zI-SrHR_i?g_h<+vD}0N*0Q!q%mSwGT@ocT47X<8@oDtxbZ@&K-bAnF6 zQP;D}h@{PL^>W^QcdU(umnE$ErTClQxT1W*u#8bBqE^Y#KI0wGRA-<$_bkpl#qdWa zFlx)z-4vMgnwc&%Xx5>)-nVex&3v(Q{me#yyNLYun8GQx5nKJEjS((Pw74LV#5)av zm%BAm&2O&7d(D%8gKV<_fMRFV=@S zC-m`~O_)M;M>9iG@<(#i<@4F*Ey-zvs7=+}`f1j_$X%Hff%&@85XM zM(`&VTPn*v`|!g{1yo&uNv%5A-!3s&D|t{9=|M_#Z0-@L%*`L;=tGy+$OQG#P-(b3 z8V>aAiub&Ipyas|4UhhGaI67+(xUdq?uzV>=l3;cNG4oQSnyQy_@JTbni3K5hN;n9 z)Z&D?#JAR;k>Ntug4Q3;(-60a9F1Q@49>NZD>wk#MDR zt1#Y&9A{Y~+uh%@ACsgx->Qzh)b>7>P?{=T*RZ!i@ikBAiJ9r39(3SC6WWhl5AaNt zx8+y(lqR=w*WM-Ci+$9&a5+Xo12dJPT83I)b-W9!Xf-XOGR zpliaDy$;LV_|ZC%X*q>JS^Gk}cU5-#3)x^)eATFgi2B}t6kX_l>K!!8Gtuqd4|g$w z0i*~R`SAIDT97c%r=xN382Lki7Q*Lm*~0ICxl6pxDq8`)e`}HDhkMq$OCQ#^tQ=OL zV;m)|^#+NYX^jlSRNla-|yd zXr2S&4+V91&1!W;=7h@2tGEwSDAb55qZ}_JVaT5|dv^4R2;vym2C$n&zL`jds0vaY zN*jEH$XZApNEVx(^QA-?#Rz#^=y?XPUbsaYb2@ZX1Ry+kn^k|mzxTd(OIit4H6|!} zt0oQ)E6InDfsBYRks5|cA|zjIeomQ|q}fKt5J&GjP);?r^P^R58Qij#$F(olbWT)_ z>RyT5^zj&to*1F-V6C-Rq{>u+2p=2%ZWzcS^7lGs1Nbh{P=L$&>2wQVC0` zzK=MLX)$L#1!dj6kejjyuEL-$p@6z`c#wBy(WzMF{0h#Gt_wx6blzA+ul}9CUXr=il^CX7%_t4Wrs z@$q%)3>*I-`vJ2X2w^WL_wV)0K&@!s3==aL#FH1=#@7P<-`aouKcd5`XaSYm&Y2Ca z@l21XuAX~(YvHB(Z8j4tX7Z#I#VyfEs9{z^kUSG_BWhQ@uX6>7Ha3j(=y%ee64N&2w+A3UkxO>Peb8ig|3{Tt{|tws56cYBFIs z?MmRzzPJ&PrFHOxd2$^&An+>))M<; zgY%P?U7Zd*UH+k0%P58?3Rfhlsx#zMuDVLA2dMJ9Qq%bJd@Y+;s`Vc$cpTP?{3GVc zNEylS3PUyoLNp+A`1@21GbiBNMiSsTM-HN6UECU?-arPxcYJlj$I6(|SDbEUy}3CP z;lJNY*g-9Lqtf-}ycIVV1V< z)i{+u=H8G_ZBNskKD!Sd<2indA^Ux=sSUp=zon*Sv;>@eNF_1-+UbTpkw**hGYBS* zcf(Kr3&NOBOqjri@~ubdlw;}fVbrlVpfE8{FK=DHXr8c54&6x2oOs;%eAf*QG-%S5 z=*F*-%LkhP4hb4feGM#lluoL3DCa2-Lc95!jb#Y(CTHLL*@1x#RyKEwOnEAx~$t zcx!OMEm+ms(kMY9UA$p&fVL^rL{)&BC1t&!gBEQnP4`t@Q^R=z#qB_B7RUsVs`>wdCk7dp=x2J{Nr(LK>W8%~FB_}mAX#}3+;!kr8 zoCY~jTUke}d@!PokVEl``lN*t(}ZX4{*B$z0VP@-eh&f#2RMu0 z6BFGSM_3~CzKQUVlCSf_vrT~i)KUOet7GI>lLM7-Z+gsMJJ!MZLr7r2hx;Ew`g>CG z%*@r(YhYLaFgJ==Nq*eV0r2_Gzm$Ih=)+aO5Rl#i1|V=b1RiwpgG0RA79*Z!0BDjD zpj%x2t3Zjr_5TPFk(RY%@t+{8b8zU>v#DwlxE1~Vy1Vb{Oe1z-eQFJM#4h6NZQ1w- zi5q4gB>t^E=sWvQ>+avRE!H#KswSa7JON-r|LA%EB#S0+_BFQJ_K#ud-wFXxUDPZD zE^o>JtpAlNg*N6FR z_(o}f^+H2*j67WP^D!aBE9uZG`agnP{lDQ$^e-BLVsL((L<&zbdO<%y1~OxhM6hlo zi?d`Zx~wZ=j~NqR#9BAn4BF|Ug1`O-$Y00Ci;i!R)@IiDXz(7HIe_vunJ*6C3yf_q_yE_wX=mW*ax$ni64grPbN6+ElZ=`9^H3Pn zVGdhn*b~2-Q2;+U+&uZVXEZ1qJpX6nqJ(Sw_|$}uJTann8G`soDWf>*A-lp3scq*Q zX6|i56IWe{@jbdL5%xC6yktvb#`f4Bk?PjLW_pZLTb74mt>&Lbm=K>2!fpBoA9-z- zN9%w)jU63qcCky$389Va?5<)4wz&4XQq**~b?2+AYS@SCCiFjrGC0+fN;Rx;jp{Og zK5im{Wg7EkVZh~HGipi#mavj^!LgtmD>8|XZ{7UQZPj)~P93qlv z8S1h|`Cv8Xe!WcEExwU6y6)sIE^YjnokmT^KBO%3h^@(b|2aoyfwEaeLy3HiuqiCs zy)x01i+s0~r5$uw!lqDT&9UfNmq1w)`Ie(WN0PAqnL+vqaXtS`-viQ5Pz?9YQx|^7 zQ(TLSoAA0vol=9h)a0VIwMTY*?J;e{TRWEYExnmhpWl+c%ATMh=#7=L-5@gCoK+9r zMCQtk6g?=;mlXJ1k~DHo@u9KQKY#yBGG@Tto*Zh~g#L;q#l6?^V`oKkg03D7f%Q;` ze{q`B!%tBB%#&jA7f@PBiU}IG`}i5})XFW3ctca_Zhd`Km?nku5DO{!`H3avB!gWk z>*1#_wq5gf9uPK8bEaxfT5t%8ua0XUuQDqUnevoN1eu(7vi65tE>&61^ z&jj?;@4+Yf73V z(pT(M7U72?k%=2hMi?X-%S9|IJ7$=?<(pdh{>otk$mf8J_&C-$AK@E;W6r z0r6X_0jKa%;O{U}N34bq!~ndc=7zvMoHh_xphAl~w`XMTDJ0b%1Tl3IbagdAlRRv2?)%yzQ)-EySN7E^A@ofj+9Q{26%sge^^t+A?(FJNm~2c+Cw9zX-qW=*P}c6z2JZ)Wac!YEsV|DJ5}jv1-7P2s}Hnx zKgR-%<(Argq=)8(4|{wz>w-CJM^C3`ODT17yiR42s`cCp+GBMqFfzU%|JF%YL~-U@ znuFbtyrtDIv&QbS8?$nG1{Tv|OAdO@z+V zmnvkeKQ65Fh_M$-4j1%)Oo4`BFqz;KJ;vbAkDRrWP`5Ai4wBwar&-Sr7FI?>xtORu z+X@elISs!lWVX1gTC-HQbvBz^s2(09GgTtARn_xYWI9Z@ni~J!SMI%VVvd934*4-9 ziDR_=Mir@n6G{c`Ys0A^ejKo2ilqQRzeUUKK01OQpUn(@Ee1SGC~uzX+nxd}8M(>= z!57jGz*(fh9{4+kZiM3+yufz+t@=~PIH2IKoA9x_yn#cn{~Y1(KSTff|MKlmgh0=- z2zq-~a40u@4R%DKm77%F@?zWks~43#QZG>gByg4Gl6$e;aVGSZJz%;o^h6ON-6qEy zVfL0TXeP^{c*7^di9aZnpLmwsgxj3%1X!2Q1Bt0b;pcqtw5GgnV1 zP)zf}YOnyKaYn!#-|O>!(|W~ScFMMxmtZrOeK9_F$uogE z+eE2|5!bh#5a~doTm5u>3*^z@YSL809G!rhkq`ukg)(E z%ASLtkbwo)r_vkDUj_V18TRi}w*9+={`MbBC5TbZeZfb>Z4bIm=_n5};Egi$^=ug-z01V5QE%XN*V6lWF@jwgA(3vc~ zB)1IuPxMX0YV-|-YzdjhW|cxVc6G;T4$^kI^@7aIedPXWVJjBXtXyli8o&}$*gRa| zOb;lUBAT<_>UU5P&VarTRaKYDtm${k|q0x!wVstG5m%d7x%kmN?hDYdWB|k zl6s$w?@^v)3;}7z;cirZED`T3{h}?mD%9S2&c> zk-vy!^_k>n!JNr@^{(-|eP$h}@Fd)89lmhu&jULf;Bl^_l;z#wxCD z#0uUy`dVG{#j798)NU(&&57l zc#`<>>bqxnPzg`k`w(q*xtf||HRtib(ln^R1h##}8V7gwg704P8Up7hfJL%-*!)Z2 ztegV>nY3o7uNGdlP1|lIO|BemYCcnk`XrAqs{YKt{98ZjsKLKN9(vxx?5{(J5}DHj_^=Zsm97%Fo=xg1Tb` z*N2R-9Ae4RM2H8i^AtW-qP3#@s~6m38hv7H=m4zl--NmM?>L(C4BI|#7Gdj32;w4=IL)}4;FyCZDx(B44>JBkDei@J6aW z_z7wZlA-?m1;$B%jK_l>O`JL*#NGMjJTFAIn8Di>C06cjyvgHs*heVxf_@!#_?14d z1CvSGoX>+3FJuFhnwAV518_?n&VGpPTayCmtOL#n&~lV4HEm zkVB-_p#W1@i4dumWstAcV$7Z)4(mAtYX0ggM3I-7VQSjl0kj>tqS1@zrYW(K4nM z<*bQvMldX;)38)_zfsLyJQR9V?<;F20k*dCj&L;YwTr45=`NqIvtn0{4A#tL?p865 zyy=@l4w^XR)YWu)!9IPxI4Enm>5XYDE!a33(ioXc!0m(dCrG|Et4xhDG@~zfb;J=P zly@3o;F&PF5hBlEBr+iY2CxSzweD^-);~~Nv#G#Oh<@#wFnmnfFMaH$_DTbU>-_?a zCo&-nba^wuV1zFVUbtM|KeLK(NT`&Aur-sIyCBJM2=HKoUivw3Luq@7eVA4E`VeY1 zzF$deL;y?eR!`T`K01RM|3l~?2S7XlP_WX@_-aNHgK2F}ZB6RZR_Th12gRMdu*g*z zkUI*rZhiTvx8#^=s?o83h_7_SsWL;_3lE zHscKm9I~kJe0E`W$Zf}b)Xo3wTu{AMjijwJ4BNA;1#G0cTD=yjswJ^T0r@g-BkF>A z5=$j`4TR8QJReJJ7FUy8P35S0EX`k2s>du z?W2Rmj9=hTJa58)VEU(|r79>_Z5C-qsCPC-09FrjfuTW611RB$B?aZt+A9pSbEVQS z>k^8+C7s54X>95@TEMy3QCCERg@OysZ|68d6--w{^IrIDwWT{Di0paGV$bfj{W4;> zLB`+~?{-~{OZ6Y&C_zpV@Nmsf0_ZxQGvbTMqq;3$e_?Zpk@#{64Y)ghK_CkntfO^?*s{NLNgm7yCN+$RSCOC>cN(V5z$;~0UEcdQ7Ipwk-gj6v7o z9{L9!j)1&)ah{Jh1gOM#nc`w7%@J+o%HbpPnJb6}91sjym|y^)&~GU;76^zue4>P5 z-N~Mlm=X8B2AI(-`w)-2nL{EGbvY`xIdMtm>v01NLNreqAJt{T*Z^5$bczrUjOlw}Ar_o8Gp#66BS9 zqbT`<1W3>+>@0JUDVOc=?qW`sn+3(@v$6U$!4s^9BricUp-K%tQiR+(Yjp2(Co_fpg)zsR!^)feg?LbK_HcEfBLZk zcU15r6oxFS#D(r_hr^y^{4zIS6K~Gt1^K6`H%>I+w~3gx;Po5tcTPjalrOCyU}vWGWx*qKy=n4%UT;Gh z?bdJ<=FW-1OyAXR=nO0hwDDqb`ICCwfef_uXr{F9>aR8lE=j_DnCu;M5~WIWBayqD z>xS%K%|{2Gn!F|aUUF>9hEo}B`Ep5nd3{snW0Dvl*oArbG6VIwvzTKW%1I5dqYJyI zo_N{Cl{P~jGgx~~Zq7BzJ)QVOC+Q>Mn8R9&tt9xd<8=2TqIV;GCETaog^HJ$Ns!1D zOG2V4?P6-Uu~yp?25Yf06w8m7xx811@H<@D|I^^P@B*7FFD^CTzgbx6u%j&_Yv#8R zyNMgc3C2B~ODdn$D!#d{)X~?@%^`28Iy)dt3<}G;2E*zjOI)5e4(tP?l}b zve$i zp;xqqV5$oHgAm0R+c6>$_TC>@y@5&jBsEJqNjCLrn$9QZqhKAg@EH9#BO+}1NYp0P zv&+aCB{1@;w)S3a65Y2HH+<9jAkvmP%RuHJgk9@0QkXg3l&jufYXx+fs<}j^%Nkwz zUTxLz!lgwmR!t*`;o(UxPP}&9!__(fd4g*VYtOy0XElN?ex;@D`u%)UcX@Skzl2;f zV;`F5PWt=I<%7B;pT^v2N`kjN!wwx7x|45S$uLp*r>sMbavf`b2N0b~ExaOgi!JIZ zRu)AYO^24LS!sLaCU`wYHzEnsLULW0XoEpj@yr;(H0=Ot1DbN@D)Eeq9Cw$rt^CUO zrROgg=$DHtUq8;c(MsewaP?U=xaa6g(e+;*ozm{u4%vId{RGjbWiaxV`Cz1ZZGr3o z7h&J$<2YT7Hnjz>ITPGdiXDGAifioJSjN^!%lHgD)Pm(L}_(xpTMb9qhfI#wHft%g7!$a48@iaxCxyFm7=+^dC^ufiMLVrQiYc zv%1FsV1wj4kJq7iL4+r3T}>+9@#+Q@k53&^e=!`I6GxdM_jG1T`l2rdu%W`x*>+1H z@DoIk{WoHB8jW8c6vRJYS7o!Ja?38cajGc_c8m%cP9urVumCVS*Hyi%QmJOPO@weD z|G<>+$PDFjop1b8=L}E&zQ7()`vPFcu8>&XCIUhC+K_;qB^%%@&#tk0hQk`T9b(PR z-L+dhQfCs#Noo)q{a!E&Re5-hYKnN)W0h0Qk;Zo6HAG@4K;fA?^e5O+<-c(^ah)Ct z{*HWB_5=s)>GU2J1*%-S4t(#cNu!FyN|9c2lgx>^SdZ->YR_6zxvXhQWh*fc-3X<8 z(YgYm!6u%<6ECyI1ILyMw(|{|SNt_LH_;V{A9UAJvJeTF0j(&-xpy>cV!! zIbv;SUO8kCrTU7bcecEkQ+|1fb|Ka{wyuu%U>rlPV#nWGoT3Q zwng}t=T$SxR?1;~sJBnz z!b2yElzytVXiRBLi@mP0F#Iew4F$I2ReV@X)7N*ix#Kl(GcKR)iDB$6>`*Zb zdXOrVm^pcY*crULg69I=L{__M!E=&lgSE+SHn|L*j3^sem2FhAoF!4e*W=v-YqVIw z?X~#m%v=r?D2?}{H`37&n~e4&$MJQkNi9$L5-v$xwD=@Tv=D9&0s$BL@A?0o99sKS zYFekn9*e3Ia+pvn{CT9{zmakE^BN@pA6h1-6lA>H-%Htvz!ovu%zkib}5xA|L`Qy=`m2!FPikg z=EtTi_J!=7dQfd|ZX|>G9vpZ#)#R3?C;d6E=0-A+v%HVTo3-KE`Woi$TQa9g6GCDy{H-?g0QinF6qBEzp(gNNZw9^Qf2F(t0E|UH zpydJoWkN)lf?RU6Jb-KD|3VTi>EgDRz>Xh!hzk%*?q2e2YH^|4 zEXJt$bH%$kJo%afa{t--vIv0->E?GoL3%I1)X=-ec01Z0Ib^VL?0n4B(!qUCyd?m{ zI)edR{z?}InfU|UyDgeN_REj>qi<%7 zLJUnk5%KOAOTDEid|5nF{Is7a*a^{ZPH4)X=Km#?In*30HL@*|P!o=XP4>xRL3<_# zNzcnsi+ase^&q8#C51+rp|>T=t2%>)M-dHr=B{Mc#KYVZ8zzl)Q>EEWl}W22p3clg z%tV8iJOu{^^4suhO<$~bThT_ ztnO~gLJW(grU=8~nm~aaYGm1HK(rwn%eBi#z)}ILKUn;w29x!gaXz$lYKx+w?xw6S z9`iQe5iq|1xG6=dAi{OeIV;rFwt!Hqq?F52r3t-cW_tC_T~GDnURR6$U#LtxR(u7I zU6nmP>YrzAn7spXY3&K~G;tZEn#4EfnHUMhd^RY;7?1pZiqPi_7fQ%cTd6HuDJ{)I zLx_hLLM^!`rt=%QvcHl8jI6NIqOD#h=m;-iDNdN1=d-5)CnekCW*$2cF?J`Po_B#f zFPCdDHb<-ICBz1Ieiss5pj!f@eju)5kq8@muL zV~fenZ(jmYpmw=-&vv4o>1q#bf_g0R|*x{12IFD^;1*%PnJHG!Vyu=4fff|>w zA5S#^?Elacs}Ak(8m#Wpz=pM_lExyNB?tS4|7FGQWNe4J+J%P_qLRk2RGTwEhsX7M ziNLWg2cXGqrx$=GGhEjuN8SE~&M%JIKG5dA6gQR}-C1UAPHT;8MQp6Khuw$#nDBhJ zl@>&m%GVWoB*wEzVBRh@9 zr-~TwqoH~=SBlVo&8vJxIpIpjyP#`@@fnT7wsF1>p0}JUTj@Q4($rIrYrePv=jc#L zxPueFf%ELApwD9jos|}*H~mqVXN{k~ne2|})&JsDYRf`oA495D)T8t`40!(&l(2{9 zATEx$ey(ICmvz7{VA42E#`sNY!L=e(31OS?3OZKV7DuT{Tf69XSSY?#rsPBXEB}73 z11LaAIeHz6_T4H^WAtc-lyWId4tWiB;bQ{C(J|AQg9zlSKl6r?tIr0>I#piPM@W(q zVgz`wQRdyTf3--M^ZH;$ZsJ&ZXh>U(Cq=p&00qqbYgd zydN+U#N3WzSrc$|^;e34Evs{ivv>YMPaMsfW=r7#l8y&iaiO5?E()Zt=M zG|r}RUWN=#Ns@Qn4~(?q%Vfolepc2!A>wIwVq*eAREh1I^{e7pBDuY&1APLn^;jndDE=-Wq3k)QpWc74josp!XKS+$t&j=pq&qo^y7CgK=xkYEa zyxt~U%ivBBeU8qA!Xx`;92>b>v)9Ueb`rbkeNV@uR}p+Ya=gp0Q1!B%?~hoRL8+*XaZ+| zyIkF09!o#n!%G1xSKX*5n6I}=fCs?7z<1;dz!(9b0FY>DQg$C`?4ba&rziukM+m_i zKyn2fYtKj?ks8sTp!%LLNrInX`X0g>;eS10 z0bYl{l_rw0&j~mw{K*QCd0UUyKIjQ{wt7ha`K^dVSf8TW#VJ`u#0;Kfzbg!>VF(%D zfmBKB^96zyt%I@7Yzg-9Y9)l%re>6~K_HVl82=IL2jN@3T#qfC$=qqAMN2j6?Iq3b zisqB<_q#-xu!K-)3a?W+I`v`oT6Tgc{VfuhwAv?rSl5agO@0fEN~VG#Pjg_AG&Aq| zL=r;GSJm1!>4TFame;{m7ioEvrS>HtpqrkPFx))dP&evtV$n=T@x|VMg0F0w3W?31 zA9ApxTijWZt$@zJq+51C;3SfBTbt)rc!WLzpAiXkrUp6xI*YYk7r4W6A$fp_dU*5u zBTe>G$0T?U$M`46Z~qAs{F~qa17z%0Z50MM-d7g_nhua65EJ8XFB%1oh^U_Y-2`sY z(l;dj+GxO%@o$CGj6MkE`%s9GXarvi74{9nyJ=7&g`%?`C1`N4?QU-E-v!#eOo}#9r+?a ztpdB;ydthE@@{R~r*;;|Y9iI+d8-9p`$^$Z0bwZDNzfhy&O?HZUHf@)vbC00aF`_M za2|r0AG%4bVh)N#7@&gZlXKBWYlbo1 zut(C`n~PqwLbVz#aHgmXaiNtyIqZig2|}=q`dz&Lk+cQ=H_~>)8g>4?6aBqUMg7(b zeiPDL4SMJFvyx_bz^|e-5NJ%mx(R~cX!AS&33~mn7cStx=tlG&x4gjiKU-dd-Xkxy z=z$cBeZUXIuVu@$^ygPVbR8)S8v9Z9bU*Peno2fL{(b-|L$FiKFnI=8Xmc+vkUJnR` zrFe_AvEnr+J6rt$U%ujH&W}(Ik+*82!~#C9xDOWPCtlDG9A~c-6@pw)w$i`<(!0dp zMN=BYN#@%$zkL!DiL4Q&tfryyNG9-u8iHU9n#_yd%?q~F2L1EUZ3 zuaHIm)@K0g<^O*>yeZe&-Hha&G=$7@r5 zwT*~&v@N!-Tk!gBP&52SiSXv=^r}^nEv0>_pE&xBa2^d0z-G!%@2Zng8^posPLfU#WHS z`A~l?`zO0lM+ZCgp(sJJUMa2C=(>WVn;f7@rL<;MQ>}4wFEK*31Yd z`WX7MiXYQfG1ZPFEfGob9}Z<*vN1tQEU+X@Fb9^_5$7(KrA?OCa$IZ)GVaV8394@p zHwCr74N$AY1AD>|f<&RFL>j&>4a8Z^vkX-_0~>1hJo?NEZY3x&upY_AvBZSo&+apb z+tp@+$PaU@&O~0kFMONw7+@)DZC9L82(Rctwllbm+KmfO zFLdc0AHGe2W0?s%s;ZcWGG@tG7pG1qxY>wyBQ60BB5r40HjtUcxfIc8{^ToF$f4KE z2azG6^kMv|91vPuH7F>K5F77qqQN<$OF-)q{9KM>$@MtNv!%)tI#nKPg~A^J29)ku zi&2j`Mk@pguo~%hyWyvuHLIPRQfA&)Q+fppvT5n#3Ed)PAfxi%17Llb`7BCbA3SM1 zU}X;nxpID(jUM5xY<_`r31Pk$958rP0nqUGwe)zCm1-wRlVzL3=8>B$zO5j-xq5C1 zLoKsEo_O$jxn`gxgmur>5R8z?NG)K0d+3p{$g-^{;B9!<@R4{eV*veZr?jzIx!HM9 zM}pw!#S*^|=d)ntL5#EzKHz%YoLz904+mznMM8ya5V>lH^0Zni$jA>#Zcv^B<H{9mlE3QOgQN*9;#vnMXgdJJ#{jt6cmPd~ z*K0j=ycIbo*;*eqbTouk;Y`~YM*XzO+F;ZNIw7-I6bF2L{B_+jovA&##Dk8rEKDQc z$9@W4&Y^~&7}%2DK^y_r_VP=$wPV#3a4pK0*0lRfndN0Emm!)e237WWglUWd_=(;4 z-y6kNidlX*bdcLj>J1Qlj`7!dyL96Waq33n8HbnBY^4a3iGWcI#$=$4<}QrPiD@sW z_U;1bb-2hjWTukry{QyOcO|iO^w}+~h)qw~Lq87Ge-Bf?3Dmv?#_rnsC8g8LWoV zyyXT15?q-_OHo_F1VQKAD#?mhszG0tw{`IvSd-wm*3;<=s; zKEOXv2d3?Dqf2_+joMvb$SHV>Cz$ck=mcUHFBhcy9cFAqa_h(5vd#ou6Eh1 zRVK5hs|#rDOg|T4Ilhi2($rus-@-%5_}+(=3Gf+#$X> z%Wm1*JSkaraAZ`N?5j8coH!Om@zW%goX%o2FEZdd?xBWDjR%r8&FFQlsd)TSk|6!io79AlmM&kIIkSbgK z!@Im9>#GinD2F~jAy4!CSJ@>vfvc%T2pk&w)r8;eg5lI>nfdnUa74t2ScXx&c9CV` znU~SLa6Y~I%1ih@V672XA!-d1qSedR$H9OVhCf7zPV(UNz5b`eGhnH z=~yegKK8+ecg6E@>3#*BC)r-VE%oT+xHL z3-e61BR_WaEtWwDgqy)63pXc#OPZHNfq@+f{ylrJ24Z?A?zpeNH&c&3hMLUaq%u+;#>7^>1})E_{(zyK~Ipa1+}J zK~KH>qenq)3N@N%i}HiQ&`gUG)__=?Zm;+hYbgQJ+VI1w&oA=v}80z zvOh|2Y^Ss^E~HQ-h8T5VUWPP(((x>)gTv#J+2KYpJ}^|cu}I@&+OG8s_PlV&`(S-< zl0XhkjawLK%Ic9~5j4sm9I;jM9junQDm0^v!|J zxcpCRMLzS;f@e6kb9~1InbhKpEJ1>ezFxgNa+RO4AoA#`4#j!*nc)(nYPH**kQpha z3ate4=c*oGFoQxC>yG`u>v%lGwKy%7il7-)$Gko) zEO3v>S>?RhiA!S7x{enz|CyK3dmz?2-}YmB9^1PUF5e_XW%v<@D5Gf+BUwjk46IVzpb}%$G;pSD8K7cuS zWT>>59!T98oY5-SRiwhGI5cTw*C|8KL*mg$mP7Xq4;p!HhfH%T8(>Flf;QGp^V$ct zM})N+%k?EG2iZ@~B|?K#<${hje*o00?rPc#iTo1QDp(P5qP`y|lw|-N7m?4ykwSsC zgAiF|nNy!hH&?viMxTY+Zn5;v-Eg#1YYLf_g~Hl{SLzvlugLBlt>@wL z_!*k_VFP_*FX`gAiB^>$AF647JGu53h=(p+os%W4Y6(A9RmTXYA8#U+xhyOK%VegP zrLCQohIATP%Jv;^E_At*LjwMT2DR)qhln;6@e#Ki_h9p4%7V7MDO_O~-7o?5%P&;2 zvR~;^WQ!mg7B!x;m)rhO&^+S}7v$%WPzcJ6zJ!O&g}i69Y-x)v2~;-2B?vfVrRz!; zMzLHFh>7Rud4W`ue2wb|U8C#p>n<*Znb9xb#T!SyPb^y23Q6C!N;sFeC!K|R^cqsr zYUkvA5RqKVYK{C#NKBOGNDN07HSj6%?9}~re>!Q6)81TMi0mEXDqFX-MEoo7_a!JZ zg?4Qx0*3((!ySGYi0U;>uTsv>TqWx*o5(ux#9^^Zt=vU9H}ZQsRgdiuzVeaVk}p@U zU~CX(VN!4S^72(NFgd&TXdLY>O!~i4U>E zT{kn8swJf#&AL7dgsIhXzfO9gVXaz3lO>ct9#JQkOoSoN67*fIrtvlYGZu2K1?~U~ z-9`4SMW;Z-=&_M`r!D4b2l07D>ar{%a_sT*l#$nG3Fuphi;kNge7Aj%?yVUm%|o;3 zS(~g`d5nwWat1${eEqbr6s04q^}w^z^Pa1GF^bWYZ<{g>XFS`ZpK_90DEP&*$t$Ab zR`&NiI+(hw!uutw4g|fy@GY*qg1&B)SmYlj458>IFjjZ+CeRm)On3M>QMd@)B#%B= zo4@O0{u zL>#!hRpPiA10Q<7{sW1y@8QM4lQ|Di1ewM!w`1@r1 zYgM2`3QXo2%>Ons|2D<5v;Q-RIP?E*zGot3=lW+VAkm*QJV2C%l;d9ou#$58b4lB;TuVufK$q{29Zs_C zMfWzegMg_T{C;eqZ+Ky(<=j8WH(Ic=>OPre_hHo7On#Pw>eQ__mJ0o};v6b=W~}3u zO}lJOAf@#-*_PKdfN$j%)t7{Mqy*_m-S4=a6L)~DYQkw`(2Fq@MZW~~)E0G)Crj)~ zM0D$3y3!)2OyG9Q--ueW_9_gK-sZ97{rsXaGeSpBXoq88Eqi(d97os-kRE)IL|_qT zbg#FeMBuk9R$c_EWebPOvt?YWM+=9>Hx!AtE(!9wvSoGpI7!~GN^;h7I~{h#qTk&9 zn9Y8b#89Kcn_-ndNZ~hcptyJwv@X7Ulv;F^FhV|W?vtjsBE5D^0JF__ zDTi|{EC)>u0kAC*IT5ThxYrO{%sT2sWnw#wHO;QsjSH({hu zBYfsoXt)oNogFC@<)Azos=^LeLJigX+~VOnTI1ung**mhlm%Y96_b#mpX>RXaPe(t`?l_a<54;DH2px8Uv?G(d27f?K1F-<2)refRm!XZOb)w|l_o zUcF>i&02HLs#VWZmpd(NN66kaax*N8uN>K%zMUfuE3PnzIrVMrJLla^8R3}ZCK~0T zYiGI`!TFJ-f@>%{mE)nQDEzFD+C^tH?ly*IezFSo$G^1MOiXl^d zlbz5Ao>mh6!tK};a-G?XNL7$&l6kyvjsR_w{{@bRrQYM)Wg(n{+YkEja6~e6n;fgr zfe9R{p83%sirD=?{!{cym+2g#DZeIfr%0j8bl!+4ln4nMe&Ojztm~QFX=SS- zQ(k{e(bm|MmM{Kp9)Aj5Ya;1!14nnm5P>m?uo`z`kPtu-(6T`16?~q9$L_-klLiL$Hf@@SOINORKV(*e zAWl+sLv{4}vViL8DL)GFHUGJxHOfp`LzkWRG0}zBTnO^ihljLWzN)eX`ttt5A}TA& z^q5KFSBHAfNqmC^Z+U%XHU;0NX1~q-Bva@}EM76sYR^~hVs z$qsR2u;F#-tH>NO==eNDs!A7gKJ{#h8aZ3Kip*sA+q8^cTK`asu>(10nt|j!0n$CY z->n z-1^_2gWne+I5ky>LgKn3SE{V=G;-GuINL6PPR|cM2a?%zHxQ`BwUNOo;~|s~+`dyV zy0B0ZJPlIYND4l5#xu+B%M=Job4G(+?kLXjS%KpH_?`CSp);CUD<7xntFo3Wh*DVi zb1uk&nw14;>oCF3VXvnVRO9YHAND4Y&6dEiM#`2-eSlA~s^c1~hO6yG=e;gLgJgp* zAjBAA?Z61?fsY46c6>f;4|+n+jfQrOyAc}~8R2afkdphk60b!z;hKh49|{pFYq1{=^%>-J#KqD!X-Y&6jRemY4q}I%H$Gaj;WEh2U+_(r z;6g1KnCzp_Pns-R2cGioV1@YCP$p&KQaxr+mMKJyRBl)w;%1)~Zqkk77V7rS_sk=E z8|UX^BpW#g48!ZTG5zHi@4pP6Mm6!DQ4>A1JxphI3@;yyr^tKKwZ2O;3m{=BeC_Z} zf7fc1u})G)pWW9bI0OoXfRMV}Fa^#UF?GJgKiiwN*m`kzu#lNDm?MUgkoTjw&Xo_! z?o7Yn%k%wva;N)R0i6>mdDmQ%mw@rlrC9BUapCauX-PSwFvnx43`aX>t8aR>6_938 z#HO0M6o@Jl^kgc3by#Mqctt(4=Uiq7sWL{-$9&CVsNQP$mf7amR|AJ zwM>whD#;2^MuxFO#4SjMUuL+LB?NNo)w0v_5aYvaqCHw`pG}5`hd*v)uMUs0stgW3 z_*MwLypr>}CHv3wH|Kww;QnyDm5eRTnU#!P%pKgw{+tq-<;~5kjsG{UHyP*eGXaye z7~p^d$GVbn{y8=OV>F?FTe{}O@?0;Wg)7lJhyg7Jzn5E6FEv?+hSlL;D6Wcku zs5%*&nlp==dsv&At4N75i(0$6Dw(^8Iodk`iw&U3Jb%0D;$mulI_m%O<5{>lSeRvi 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 Date: Mon, 1 Dec 2014 10:04:27 +0100 Subject: [PATCH 40/59] Added APA102 Led Device headers Former-commit-id: bd9a5d1216d37275bcb807e19d043e1ea1e3ead6 --- libsrc/leddevice/LedDeviceAPA102.h | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 libsrc/leddevice/LedDeviceAPA102.h diff --git a/libsrc/leddevice/LedDeviceAPA102.h b/libsrc/leddevice/LedDeviceAPA102.h new file mode 100644 index 00000000..d707ebdb --- /dev/null +++ b/libsrc/leddevice/LedDeviceAPA102.h @@ -0,0 +1,41 @@ +#pragma once + +// STL includes +#include + +// hyperion incluse +#include "LedSpiDevice.h" + +/// +/// Implementation of the LedDevice interface for writing to APA102 led device. +/// +/// APA102 is +/// +class LedDeviceAPA102 : public LedSpiDevice +{ +public: + /// + /// Constructs the LedDevice for a string containing leds of the type APA102 + /// + /// @param outputDevice The name of the output device (eg '/etc/SpiDev.0.0') + /// @param baudrate The used baudrate for writing to the output device + /// + LedDeviceAPA102(const std::string& outputDevice, + const unsigned baudrate); + + /// + /// Writes the led color values to the led-device + /// + /// @param ledValues The color-value per led + /// @return Zero on succes else negative + /// + virtual int write(const std::vector &ledValues); + + /// Switch the leds off + virtual int switchOff(); + +private: + + /// the number of leds (needed when switching off) + size_t mLedCount; +}; From 0fd25285a663ddaec2e72288d0c78ea33ef55f4f Mon Sep 17 00:00:00 2001 From: poljvd Date: Mon, 1 Dec 2014 20:24:53 +0100 Subject: [PATCH 41/59] add framerate parameter to X11 grabber application Former-commit-id: b8f2bdb5c3e376a4505eec46204efe46dce1a6d9 --- src/hyperion-x11/X11Wrapper.cpp | 7 +++++-- src/hyperion-x11/X11Wrapper.h | 2 +- src/hyperion-x11/hyperion-x11.cpp | 5 ++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/hyperion-x11/X11Wrapper.cpp b/src/hyperion-x11/X11Wrapper.cpp index 303a03b4..abaa4d3f 100644 --- a/src/hyperion-x11/X11Wrapper.cpp +++ b/src/hyperion-x11/X11Wrapper.cpp @@ -2,10 +2,13 @@ // Hyperion-X11 includes #include "X11Wrapper.h" -X11Wrapper::X11Wrapper(const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation) : +X11Wrapper::X11Wrapper(int grabInterval, const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation) : _timer(this), _grabber(cropHorizontal, cropVertical, pixelDecimation) { + _timer.setSingleShot(false); + _timer.setInterval(grabInterval); + // Connect capturing to the timeout signal of the timer connect(&_timer, SIGNAL(timeout()), this, SLOT(capture())); } @@ -18,7 +21,7 @@ const Image & X11Wrapper::getScreenshot() void X11Wrapper::start() { - _timer.start(100); + _timer.start(); } void X11Wrapper::stop() diff --git a/src/hyperion-x11/X11Wrapper.h b/src/hyperion-x11/X11Wrapper.h index 986dceaf..6f67d7b9 100644 --- a/src/hyperion-x11/X11Wrapper.h +++ b/src/hyperion-x11/X11Wrapper.h @@ -9,7 +9,7 @@ class X11Wrapper : public QObject { Q_OBJECT public: - X11Wrapper(const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation); + X11Wrapper(int grabInterval, const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation); const Image & getScreenshot(); diff --git a/src/hyperion-x11/hyperion-x11.cpp b/src/hyperion-x11/hyperion-x11.cpp index 0a494516..8d354378 100644 --- a/src/hyperion-x11/hyperion-x11.cpp +++ b/src/hyperion-x11/hyperion-x11.cpp @@ -29,6 +29,7 @@ int main(int argc, char ** argv) OptionsParser optionParser("X11 capture application for Hyperion"); ParameterSet & parameters = optionParser.getParameters(); + IntParameter & argFps = parameters.add ('f', "framerate", "Cpture frame rate [default=10]"); IntParameter & argCropWidth = parameters.add (0x0, "crop-width", "Number of pixels to crop from the left and right sides in the picture before decimation [default=0]"); IntParameter & argCropHeight = parameters.add (0x0, "crop-height", "Number of pixels to crop from the top and the bottom in the picture before decimation [default=0]"); IntParameter & argSizeDecimation = parameters.add ('s', "size-decimator", "Decimation factor for the output size [default=16]"); @@ -39,6 +40,7 @@ int main(int argc, char ** argv) SwitchParameter<> & argHelp = parameters.add> ('h', "help", "Show this help message and exit"); // set defaults + argFps.setDefault(10); argCropWidth.setDefault(0); argCropHeight.setDefault(0); argSizeDecimation.setDefault(16); @@ -56,7 +58,8 @@ int main(int argc, char ** argv) } // Create the X11 grabbing stuff - X11Wrapper x11Wrapper(argCropWidth.getValue(), argCropHeight.getValue(), argSizeDecimation.getValue()); + int grabInterval = 1000 / argFps.getValue(); + X11Wrapper x11Wrapper(grabInterval, argCropWidth.getValue(), argCropHeight.getValue(), argSizeDecimation.getValue()); if (argScreenshot.isSet()) { From bf7ddf5991a2b2ec7a41b7b1da0af3591b41ba37 Mon Sep 17 00:00:00 2001 From: poljvd Date: Mon, 1 Dec 2014 21:27:33 +0100 Subject: [PATCH 42/59] Fix output size of X11 image after deciamtion Former-commit-id: c6120c7c4116f855405f65c21b0c0f5e1e443b6b --- libsrc/grabber/v4l2/V4L2Grabber.cpp | 1201 ++++++++++++++------------- libsrc/grabber/x11/X11Grabber.cpp | 13 +- 2 files changed, 614 insertions(+), 600 deletions(-) diff --git a/libsrc/grabber/v4l2/V4L2Grabber.cpp b/libsrc/grabber/v4l2/V4L2Grabber.cpp index 2df80524..698d828f 100644 --- a/libsrc/grabber/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/v4l2/V4L2Grabber.cpp @@ -20,797 +20,802 @@ static inline uint8_t clamp(int x) { - return (x<0) ? 0 : ((x>255) ? 255 : uint8_t(x)); + return (x<0) ? 0 : ((x>255) ? 255 : uint8_t(x)); } static void yuv2rgb(uint8_t y, uint8_t u, uint8_t v, uint8_t & r, uint8_t & g, uint8_t & b) { - // see: http://en.wikipedia.org/wiki/YUV#Y.27UV444_to_RGB888_conversion - int c = y - 16; - int d = u - 128; - int e = v - 128; + // see: http://en.wikipedia.org/wiki/YUV#Y.27UV444_to_RGB888_conversion + int c = y - 16; + int d = u - 128; + int e = v - 128; - r = clamp((298 * c + 409 * e + 128) >> 8); - g = clamp((298 * c - 100 * d - 208 * e + 128) >> 8); - b = clamp((298 * c + 516 * d + 128) >> 8); + r = clamp((298 * c + 409 * e + 128) >> 8); + g = clamp((298 * c - 100 * d - 208 * e + 128) >> 8); + b = clamp((298 * c + 516 * d + 128) >> 8); } V4L2Grabber::V4L2Grabber(const std::string & device, - int input, - VideoStandard videoStandard, - PixelFormat pixelFormat, - int width, - int height, - int frameDecimation, - int horizontalPixelDecimation, - int verticalPixelDecimation) : - _deviceName(device), - _ioMethod(IO_METHOD_MMAP), - _fileDescriptor(-1), - _buffers(), - _pixelFormat(pixelFormat), - _width(width), - _height(height), - _frameByteSize(-1), - _cropLeft(0), - _cropRight(0), - _cropTop(0), - _cropBottom(0), - _frameDecimation(std::max(1, frameDecimation)), - _horizontalPixelDecimation(std::max(1, horizontalPixelDecimation)), - _verticalPixelDecimation(std::max(1, verticalPixelDecimation)), - _noSignalCounterThreshold(50), - _noSignalThresholdColor(ColorRgb{0,0,0}), - _mode3D(VIDEO_2D), - _currentFrame(0), - _noSignalCounter(0), - _streamNotifier(nullptr) + int input, + VideoStandard videoStandard, + PixelFormat pixelFormat, + int width, + int height, + int frameDecimation, + int horizontalPixelDecimation, + int verticalPixelDecimation) : + _deviceName(device), + _ioMethod(IO_METHOD_MMAP), + _fileDescriptor(-1), + _buffers(), + _pixelFormat(pixelFormat), + _width(width), + _height(height), + _frameByteSize(-1), + _cropLeft(0), + _cropRight(0), + _cropTop(0), + _cropBottom(0), + _frameDecimation(std::max(1, frameDecimation)), + _horizontalPixelDecimation(std::max(1, horizontalPixelDecimation)), + _verticalPixelDecimation(std::max(1, verticalPixelDecimation)), + _noSignalCounterThreshold(50), + _noSignalThresholdColor(ColorRgb{0,0,0}), + _mode3D(VIDEO_2D), + _currentFrame(0), + _noSignalCounter(0), + _streamNotifier(nullptr) { - open_device(); - init_device(videoStandard, input); + open_device(); + init_device(videoStandard, input); } V4L2Grabber::~V4L2Grabber() { - // stop if the grabber was not stopped - stop(); - uninit_device(); - close_device(); + // stop if the grabber was not stopped + stop(); + uninit_device(); + close_device(); } void V4L2Grabber::setCropping(int cropLeft, int cropRight, int cropTop, int cropBottom) { - _cropLeft = cropLeft; - _cropRight = cropRight; - _cropTop = cropTop; - _cropBottom = cropBottom; + _cropLeft = cropLeft; + _cropRight = cropRight; + _cropTop = cropTop; + _cropBottom = cropBottom; } void V4L2Grabber::set3D(VideoMode mode) { - _mode3D = mode; + _mode3D = mode; } void V4L2Grabber::setSignalThreshold(double redSignalThreshold, double greenSignalThreshold, double blueSignalThreshold, int noSignalCounterThreshold) { - _noSignalThresholdColor.red = uint8_t(255*redSignalThreshold); - _noSignalThresholdColor.green = uint8_t(255*greenSignalThreshold); - _noSignalThresholdColor.blue = uint8_t(255*blueSignalThreshold); - _noSignalCounterThreshold = std::max(1, noSignalCounterThreshold); + _noSignalThresholdColor.red = uint8_t(255*redSignalThreshold); + _noSignalThresholdColor.green = uint8_t(255*greenSignalThreshold); + _noSignalThresholdColor.blue = uint8_t(255*blueSignalThreshold); + _noSignalCounterThreshold = std::max(1, noSignalCounterThreshold); - std::cout << "V4L2 grabber signal threshold set to: " << _noSignalThresholdColor << std::endl; + std::cout << "V4L2 grabber signal threshold set to: " << _noSignalThresholdColor << std::endl; } void V4L2Grabber::start() { - if (_streamNotifier != nullptr && !_streamNotifier->isEnabled()) - { - _streamNotifier->setEnabled(true); - start_capturing(); - std::cout << "V4L2 grabber started" << std::endl; - } + if (_streamNotifier != nullptr && !_streamNotifier->isEnabled()) + { + _streamNotifier->setEnabled(true); + start_capturing(); + std::cout << "V4L2 grabber started" << std::endl; + } } void V4L2Grabber::stop() { - if (_streamNotifier != nullptr && _streamNotifier->isEnabled()) - { - stop_capturing(); - _streamNotifier->setEnabled(false); - std::cout << "V4L2 grabber stopped" << std::endl; - } + if (_streamNotifier != nullptr && _streamNotifier->isEnabled()) + { + stop_capturing(); + _streamNotifier->setEnabled(false); + std::cout << "V4L2 grabber stopped" << std::endl; + } } void V4L2Grabber::open_device() { - struct stat st; + struct stat st; - if (-1 == stat(_deviceName.c_str(), &st)) - { - std::ostringstream oss; - oss << "Cannot identify '" << _deviceName << "'"; - throw_errno_exception(oss.str()); - } + if (-1 == stat(_deviceName.c_str(), &st)) + { + std::ostringstream oss; + oss << "Cannot identify '" << _deviceName << "'"; + throw_errno_exception(oss.str()); + } - if (!S_ISCHR(st.st_mode)) - { - std::ostringstream oss; - oss << "'" << _deviceName << "' is no device"; - throw_exception(oss.str()); - } + if (!S_ISCHR(st.st_mode)) + { + std::ostringstream oss; + oss << "'" << _deviceName << "' is no device"; + throw_exception(oss.str()); + } - _fileDescriptor = open(_deviceName.c_str(), O_RDWR /* required */ | O_NONBLOCK, 0); + _fileDescriptor = open(_deviceName.c_str(), O_RDWR /* required */ | O_NONBLOCK, 0); - if (-1 == _fileDescriptor) - { - std::ostringstream oss; - oss << "Cannot open '" << _deviceName << "'"; - throw_errno_exception(oss.str()); - } + if (-1 == _fileDescriptor) + { + std::ostringstream oss; + oss << "Cannot open '" << _deviceName << "'"; + throw_errno_exception(oss.str()); + } - // create the notifier for when a new frame is available - _streamNotifier = new QSocketNotifier(_fileDescriptor, QSocketNotifier::Read); - _streamNotifier->setEnabled(false); - connect(_streamNotifier, SIGNAL(activated(int)), this, SLOT(read_frame())); + // create the notifier for when a new frame is available + _streamNotifier = new QSocketNotifier(_fileDescriptor, QSocketNotifier::Read); + _streamNotifier->setEnabled(false); + connect(_streamNotifier, SIGNAL(activated(int)), this, SLOT(read_frame())); } void V4L2Grabber::close_device() { - if (-1 == close(_fileDescriptor)) - throw_errno_exception("close"); + if (-1 == close(_fileDescriptor)) + throw_errno_exception("close"); - _fileDescriptor = -1; + _fileDescriptor = -1; - if (_streamNotifier != nullptr) - { - delete _streamNotifier; - _streamNotifier = nullptr; - } + if (_streamNotifier != nullptr) + { + delete _streamNotifier; + _streamNotifier = nullptr; + } } void V4L2Grabber::init_read(unsigned int buffer_size) { - _buffers.resize(1); + _buffers.resize(1); - _buffers[0].length = buffer_size; - _buffers[0].start = malloc(buffer_size); + _buffers[0].length = buffer_size; + _buffers[0].start = malloc(buffer_size); - if (!_buffers[0].start) { - throw_exception("Out of memory"); - } + if (!_buffers[0].start) { + throw_exception("Out of memory"); + } } void V4L2Grabber::init_mmap() { - struct v4l2_requestbuffers req; + struct v4l2_requestbuffers req; - CLEAR(req); + CLEAR(req); - req.count = 4; - req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - req.memory = V4L2_MEMORY_MMAP; + req.count = 4; + req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + req.memory = V4L2_MEMORY_MMAP; - if (-1 == xioctl(VIDIOC_REQBUFS, &req)) { - if (EINVAL == errno) { - std::ostringstream oss; - oss << "'" << _deviceName << "' does not support memory mapping"; - throw_exception(oss.str()); - } else { - throw_errno_exception("VIDIOC_REQBUFS"); - } - } + if (-1 == xioctl(VIDIOC_REQBUFS, &req)) { + if (EINVAL == errno) { + std::ostringstream oss; + oss << "'" << _deviceName << "' does not support memory mapping"; + throw_exception(oss.str()); + } else { + throw_errno_exception("VIDIOC_REQBUFS"); + } + } - if (req.count < 2) { - std::ostringstream oss; - oss << "Insufficient buffer memory on " << _deviceName; - throw_exception(oss.str()); - } + if (req.count < 2) { + std::ostringstream oss; + oss << "Insufficient buffer memory on " << _deviceName; + throw_exception(oss.str()); + } - _buffers.resize(req.count); + _buffers.resize(req.count); - for (size_t n_buffers = 0; n_buffers < req.count; ++n_buffers) { - struct v4l2_buffer buf; + for (size_t n_buffers = 0; n_buffers < req.count; ++n_buffers) { + struct v4l2_buffer buf; - CLEAR(buf); + CLEAR(buf); - buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - buf.memory = V4L2_MEMORY_MMAP; - buf.index = n_buffers; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = n_buffers; - if (-1 == xioctl(VIDIOC_QUERYBUF, &buf)) - throw_errno_exception("VIDIOC_QUERYBUF"); + if (-1 == xioctl(VIDIOC_QUERYBUF, &buf)) + throw_errno_exception("VIDIOC_QUERYBUF"); - _buffers[n_buffers].length = buf.length; - _buffers[n_buffers].start = - mmap(NULL /* start anywhere */, - buf.length, - PROT_READ | PROT_WRITE /* required */, - MAP_SHARED /* recommended */, - _fileDescriptor, buf.m.offset); + _buffers[n_buffers].length = buf.length; + _buffers[n_buffers].start = + mmap(NULL /* start anywhere */, + buf.length, + PROT_READ | PROT_WRITE /* required */, + MAP_SHARED /* recommended */, + _fileDescriptor, buf.m.offset); - if (MAP_FAILED == _buffers[n_buffers].start) - throw_errno_exception("mmap"); - } + if (MAP_FAILED == _buffers[n_buffers].start) + throw_errno_exception("mmap"); + } } void V4L2Grabber::init_userp(unsigned int buffer_size) { - struct v4l2_requestbuffers req; + struct v4l2_requestbuffers req; - CLEAR(req); + CLEAR(req); - req.count = 4; - req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - req.memory = V4L2_MEMORY_USERPTR; + req.count = 4; + req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + req.memory = V4L2_MEMORY_USERPTR; - if (-1 == xioctl(VIDIOC_REQBUFS, &req)) { - if (EINVAL == errno) - { - std::ostringstream oss; - oss << "'" << _deviceName << "' does not support user pointer"; - throw_exception(oss.str()); - } else { - throw_errno_exception("VIDIOC_REQBUFS"); - } - } + if (-1 == xioctl(VIDIOC_REQBUFS, &req)) { + if (EINVAL == errno) + { + std::ostringstream oss; + oss << "'" << _deviceName << "' does not support user pointer"; + throw_exception(oss.str()); + } else { + throw_errno_exception("VIDIOC_REQBUFS"); + } + } - _buffers.resize(4); + _buffers.resize(4); - for (size_t n_buffers = 0; n_buffers < 4; ++n_buffers) { - _buffers[n_buffers].length = buffer_size; - _buffers[n_buffers].start = malloc(buffer_size); + for (size_t n_buffers = 0; n_buffers < 4; ++n_buffers) { + _buffers[n_buffers].length = buffer_size; + _buffers[n_buffers].start = malloc(buffer_size); - if (!_buffers[n_buffers].start) { - throw_exception("Out of memory"); - } - } + if (!_buffers[n_buffers].start) { + throw_exception("Out of memory"); + } + } } void V4L2Grabber::init_device(VideoStandard videoStandard, int input) { - struct v4l2_capability cap; - if (-1 == xioctl(VIDIOC_QUERYCAP, &cap)) - { - if (EINVAL == errno) { - std::ostringstream oss; - oss << "'" << _deviceName << "' is no V4L2 device"; - throw_exception(oss.str()); - } else { - throw_errno_exception("VIDIOC_QUERYCAP"); - } - } + struct v4l2_capability cap; + if (-1 == xioctl(VIDIOC_QUERYCAP, &cap)) + { + if (EINVAL == errno) { + std::ostringstream oss; + oss << "'" << _deviceName << "' is no V4L2 device"; + throw_exception(oss.str()); + } else { + throw_errno_exception("VIDIOC_QUERYCAP"); + } + } - if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) - { - std::ostringstream oss; - oss << "'" << _deviceName << "' is no video capture device"; - throw_exception(oss.str()); - } + if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) + { + std::ostringstream oss; + oss << "'" << _deviceName << "' is no video capture device"; + throw_exception(oss.str()); + } - switch (_ioMethod) { - case IO_METHOD_READ: - if (!(cap.capabilities & V4L2_CAP_READWRITE)) - { - std::ostringstream oss; - oss << "'" << _deviceName << "' does not support read i/o"; - throw_exception(oss.str()); - } - break; + switch (_ioMethod) { + case IO_METHOD_READ: + if (!(cap.capabilities & V4L2_CAP_READWRITE)) + { + std::ostringstream oss; + oss << "'" << _deviceName << "' does not support read i/o"; + throw_exception(oss.str()); + } + break; - case IO_METHOD_MMAP: - case IO_METHOD_USERPTR: - if (!(cap.capabilities & V4L2_CAP_STREAMING)) - { - std::ostringstream oss; - oss << "'" << _deviceName << "' does not support streaming i/o"; - throw_exception(oss.str()); - } - break; - } + case IO_METHOD_MMAP: + case IO_METHOD_USERPTR: + if (!(cap.capabilities & V4L2_CAP_STREAMING)) + { + std::ostringstream oss; + oss << "'" << _deviceName << "' does not support streaming i/o"; + throw_exception(oss.str()); + } + break; + } - /* Select video input, video standard and tune here. */ + /* Select video input, video standard and tune here. */ - struct v4l2_cropcap cropcap; - CLEAR(cropcap); + struct v4l2_cropcap cropcap; + CLEAR(cropcap); - cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - if (0 == xioctl(VIDIOC_CROPCAP, &cropcap)) { - struct v4l2_crop crop; - crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - crop.c = cropcap.defrect; /* reset to default */ + if (0 == xioctl(VIDIOC_CROPCAP, &cropcap)) { + struct v4l2_crop crop; + crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + crop.c = cropcap.defrect; /* reset to default */ - if (-1 == xioctl(VIDIOC_S_CROP, &crop)) { - switch (errno) { - case EINVAL: - /* Cropping not supported. */ - break; - default: - /* Errors ignored. */ - break; - } - } - } else { - /* Errors ignored. */ - } + if (-1 == xioctl(VIDIOC_S_CROP, &crop)) { + switch (errno) { + case EINVAL: + /* Cropping not supported. */ + break; + default: + /* Errors ignored. */ + break; + } + } + } else { + /* Errors ignored. */ + } - // set input if needed - if (input >= 0) - { - if (-1 == xioctl(VIDIOC_S_INPUT, &input)) - { - throw_errno_exception("VIDIOC_S_INPUT"); - } - } + // set input if needed + if (input >= 0) + { + if (-1 == xioctl(VIDIOC_S_INPUT, &input)) + { + throw_errno_exception("VIDIOC_S_INPUT"); + } + } - // set the video standard if needed - switch (videoStandard) - { - case VIDEOSTANDARD_PAL: - { - v4l2_std_id std_id = V4L2_STD_PAL; - if (-1 == xioctl(VIDIOC_S_STD, &std_id)) - { - throw_errno_exception("VIDIOC_S_STD"); - } - } - break; - case VIDEOSTANDARD_NTSC: - { - v4l2_std_id std_id = V4L2_STD_NTSC; - if (-1 == xioctl(VIDIOC_S_STD, &std_id)) - { - throw_errno_exception("VIDIOC_S_STD"); - } - } - break; - case VIDEOSTANDARD_NO_CHANGE: - default: - // No change to device settings - break; - } + // set the video standard if needed + switch (videoStandard) + { + case VIDEOSTANDARD_PAL: + { + v4l2_std_id std_id = V4L2_STD_PAL; + if (-1 == xioctl(VIDIOC_S_STD, &std_id)) + { + throw_errno_exception("VIDIOC_S_STD"); + } + } + break; + case VIDEOSTANDARD_NTSC: + { + v4l2_std_id std_id = V4L2_STD_NTSC; + if (-1 == xioctl(VIDIOC_S_STD, &std_id)) + { + throw_errno_exception("VIDIOC_S_STD"); + } + } + break; + case VIDEOSTANDARD_NO_CHANGE: + default: + // No change to device settings + break; + } - // get the current settings - struct v4l2_format fmt; - CLEAR(fmt); - fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - if (-1 == xioctl(VIDIOC_G_FMT, &fmt)) - { - throw_errno_exception("VIDIOC_G_FMT"); - } + // get the current settings + struct v4l2_format fmt; + CLEAR(fmt); + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (-1 == xioctl(VIDIOC_G_FMT, &fmt)) + { + throw_errno_exception("VIDIOC_G_FMT"); + } - // set the requested pixel format - switch (_pixelFormat) - { - case PIXELFORMAT_UYVY: - fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_UYVY; - break; - case PIXELFORMAT_YUYV: - fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; - break; - case PIXELFORMAT_RGB32: - fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB32; - break; - case PIXELFORMAT_NO_CHANGE: - default: - // No change to device settings - break; - } + // set the requested pixel format + switch (_pixelFormat) + { + case PIXELFORMAT_UYVY: + fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_UYVY; + break; + case PIXELFORMAT_YUYV: + fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; + break; + case PIXELFORMAT_RGB32: + fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB32; + break; + case PIXELFORMAT_NO_CHANGE: + default: + // No change to device settings + break; + } - // set the requested withd and height - if (_width > 0 || _height > 0) - { - if (_width > 0) - { - fmt.fmt.pix.width = _width; - } + // set the requested withd and height + if (_width > 0 || _height > 0) + { + if (_width > 0) + { + fmt.fmt.pix.width = _width; + } - if (fmt.fmt.pix.height > 0) - { - fmt.fmt.pix.height = _height; - } - } + if (fmt.fmt.pix.height > 0) + { + fmt.fmt.pix.height = _height; + } + } - // set the settings - if (-1 == xioctl(VIDIOC_S_FMT, &fmt)) - { - throw_errno_exception("VIDIOC_S_FMT"); - } + // set the settings + if (-1 == xioctl(VIDIOC_S_FMT, &fmt)) + { + throw_errno_exception("VIDIOC_S_FMT"); + } - // get the format settings again - // (the size may not have been accepted without an error) - if (-1 == xioctl(VIDIOC_G_FMT, &fmt)) - { - throw_errno_exception("VIDIOC_G_FMT"); - } + // get the format settings again + // (the size may not have been accepted without an error) + if (-1 == xioctl(VIDIOC_G_FMT, &fmt)) + { + throw_errno_exception("VIDIOC_G_FMT"); + } - // store width & height - _width = fmt.fmt.pix.width; - _height = fmt.fmt.pix.height; + // store width & height + _width = fmt.fmt.pix.width; + _height = fmt.fmt.pix.height; - // print the eventually used width and height - std::cout << "V4L2 width=" << _width << " height=" << _height << std::endl; + // print the eventually used width and height + std::cout << "V4L2 width=" << _width << " height=" << _height << std::endl; - // check pixel format and frame size - switch (fmt.fmt.pix.pixelformat) - { - case V4L2_PIX_FMT_UYVY: - _pixelFormat = PIXELFORMAT_UYVY; - _frameByteSize = _width * _height * 2; - std::cout << "V4L2 pixel format=UYVY" << std::endl; - break; - case V4L2_PIX_FMT_YUYV: - _pixelFormat = PIXELFORMAT_YUYV; - _frameByteSize = _width * _height * 2; - std::cout << "V4L2 pixel format=YUYV" << std::endl; - break; - case V4L2_PIX_FMT_RGB32: - _pixelFormat = PIXELFORMAT_RGB32; - _frameByteSize = _width * _height * 4; - std::cout << "V4L2 pixel format=RGB32" << std::endl; - break; - default: - throw_exception("Only pixel formats UYVY, YUYV, and RGB32 are supported"); - } + // check pixel format and frame size + switch (fmt.fmt.pix.pixelformat) + { + case V4L2_PIX_FMT_UYVY: + _pixelFormat = PIXELFORMAT_UYVY; + _frameByteSize = _width * _height * 2; + std::cout << "V4L2 pixel format=UYVY" << std::endl; + break; + case V4L2_PIX_FMT_YUYV: + _pixelFormat = PIXELFORMAT_YUYV; + _frameByteSize = _width * _height * 2; + std::cout << "V4L2 pixel format=YUYV" << std::endl; + break; + case V4L2_PIX_FMT_RGB32: + _pixelFormat = PIXELFORMAT_RGB32; + _frameByteSize = _width * _height * 4; + std::cout << "V4L2 pixel format=RGB32" << std::endl; + break; + default: + throw_exception("Only pixel formats UYVY, YUYV, and RGB32 are supported"); + } - switch (_ioMethod) { - case IO_METHOD_READ: - init_read(fmt.fmt.pix.sizeimage); - break; + switch (_ioMethod) { + case IO_METHOD_READ: + init_read(fmt.fmt.pix.sizeimage); + break; - case IO_METHOD_MMAP: - init_mmap(); - break; + case IO_METHOD_MMAP: + init_mmap(); + break; - case IO_METHOD_USERPTR: - init_userp(fmt.fmt.pix.sizeimage); - break; - } + case IO_METHOD_USERPTR: + init_userp(fmt.fmt.pix.sizeimage); + break; + } } void V4L2Grabber::uninit_device() { - switch (_ioMethod) { - case IO_METHOD_READ: - free(_buffers[0].start); - break; + switch (_ioMethod) { + case IO_METHOD_READ: + free(_buffers[0].start); + break; - case IO_METHOD_MMAP: - for (size_t i = 0; i < _buffers.size(); ++i) - if (-1 == munmap(_buffers[i].start, _buffers[i].length)) - throw_errno_exception("munmap"); - break; + case IO_METHOD_MMAP: + for (size_t i = 0; i < _buffers.size(); ++i) + if (-1 == munmap(_buffers[i].start, _buffers[i].length)) + throw_errno_exception("munmap"); + break; - case IO_METHOD_USERPTR: - for (size_t i = 0; i < _buffers.size(); ++i) - free(_buffers[i].start); - break; - } + case IO_METHOD_USERPTR: + for (size_t i = 0; i < _buffers.size(); ++i) + free(_buffers[i].start); + break; + } - _buffers.resize(0); + _buffers.resize(0); } void V4L2Grabber::start_capturing() { - switch (_ioMethod) { - case IO_METHOD_READ: - /* Nothing to do. */ - break; + switch (_ioMethod) { + case IO_METHOD_READ: + /* Nothing to do. */ + break; - case IO_METHOD_MMAP: - { - for (size_t i = 0; i < _buffers.size(); ++i) { - struct v4l2_buffer buf; + case IO_METHOD_MMAP: + { + for (size_t i = 0; i < _buffers.size(); ++i) { + struct v4l2_buffer buf; - CLEAR(buf); - buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - buf.memory = V4L2_MEMORY_MMAP; - buf.index = i; + CLEAR(buf); + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = i; - if (-1 == xioctl(VIDIOC_QBUF, &buf)) - throw_errno_exception("VIDIOC_QBUF"); - } - v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - if (-1 == xioctl(VIDIOC_STREAMON, &type)) - throw_errno_exception("VIDIOC_STREAMON"); - break; - } - case IO_METHOD_USERPTR: - { - for (size_t i = 0; i < _buffers.size(); ++i) { - struct v4l2_buffer buf; + if (-1 == xioctl(VIDIOC_QBUF, &buf)) + throw_errno_exception("VIDIOC_QBUF"); + } + v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (-1 == xioctl(VIDIOC_STREAMON, &type)) + throw_errno_exception("VIDIOC_STREAMON"); + break; + } + case IO_METHOD_USERPTR: + { + for (size_t i = 0; i < _buffers.size(); ++i) { + struct v4l2_buffer buf; - CLEAR(buf); - buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - buf.memory = V4L2_MEMORY_USERPTR; - buf.index = i; - buf.m.userptr = (unsigned long)_buffers[i].start; - buf.length = _buffers[i].length; + CLEAR(buf); + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_USERPTR; + buf.index = i; + buf.m.userptr = (unsigned long)_buffers[i].start; + buf.length = _buffers[i].length; - if (-1 == xioctl(VIDIOC_QBUF, &buf)) - throw_errno_exception("VIDIOC_QBUF"); - } - v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - if (-1 == xioctl(VIDIOC_STREAMON, &type)) - throw_errno_exception("VIDIOC_STREAMON"); - break; - } - } + if (-1 == xioctl(VIDIOC_QBUF, &buf)) + throw_errno_exception("VIDIOC_QBUF"); + } + v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (-1 == xioctl(VIDIOC_STREAMON, &type)) + throw_errno_exception("VIDIOC_STREAMON"); + break; + } + } } void V4L2Grabber::stop_capturing() { - enum v4l2_buf_type type; + enum v4l2_buf_type type; - switch (_ioMethod) { - case IO_METHOD_READ: - /* Nothing to do. */ - break; + switch (_ioMethod) { + case IO_METHOD_READ: + /* Nothing to do. */ + break; - case IO_METHOD_MMAP: - case IO_METHOD_USERPTR: - type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - if (-1 == xioctl(VIDIOC_STREAMOFF, &type)) - throw_errno_exception("VIDIOC_STREAMOFF"); - break; - } + case IO_METHOD_MMAP: + case IO_METHOD_USERPTR: + type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (-1 == xioctl(VIDIOC_STREAMOFF, &type)) + throw_errno_exception("VIDIOC_STREAMOFF"); + break; + } } int V4L2Grabber::read_frame() { - bool rc = false; + bool rc = false; - struct v4l2_buffer buf; + struct v4l2_buffer buf; - switch (_ioMethod) { - case IO_METHOD_READ: - int size; - if ((size = read(_fileDescriptor, _buffers[0].start, _buffers[0].length)) == -1) - { - switch (errno) - { - case EAGAIN: - return 0; + switch (_ioMethod) { + case IO_METHOD_READ: + int size; + if ((size = read(_fileDescriptor, _buffers[0].start, _buffers[0].length)) == -1) + { + switch (errno) + { + case EAGAIN: + return 0; - case EIO: - /* Could ignore EIO, see spec. */ + case EIO: + /* Could ignore EIO, see spec. */ - /* fall through */ + /* fall through */ - default: - throw_errno_exception("read"); - } - } + default: + throw_errno_exception("read"); + } + } - rc = process_image(_buffers[0].start, size); - break; + rc = process_image(_buffers[0].start, size); + break; - case IO_METHOD_MMAP: - CLEAR(buf); + case IO_METHOD_MMAP: + CLEAR(buf); - buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - buf.memory = V4L2_MEMORY_MMAP; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; - if (-1 == xioctl(VIDIOC_DQBUF, &buf)) - { - switch (errno) - { - case EAGAIN: - return 0; + if (-1 == xioctl(VIDIOC_DQBUF, &buf)) + { + switch (errno) + { + case EAGAIN: + return 0; - case EIO: - /* Could ignore EIO, see spec. */ + case EIO: + /* Could ignore EIO, see spec. */ - /* fall through */ + /* fall through */ - default: - throw_errno_exception("VIDIOC_DQBUF"); - } - } + default: + throw_errno_exception("VIDIOC_DQBUF"); + } + } - assert(buf.index < _buffers.size()); + assert(buf.index < _buffers.size()); - rc = process_image(_buffers[buf.index].start, buf.bytesused); + rc = process_image(_buffers[buf.index].start, buf.bytesused); - if (-1 == xioctl(VIDIOC_QBUF, &buf)) - { - throw_errno_exception("VIDIOC_QBUF"); - } + if (-1 == xioctl(VIDIOC_QBUF, &buf)) + { + throw_errno_exception("VIDIOC_QBUF"); + } - break; + break; - case IO_METHOD_USERPTR: - CLEAR(buf); + case IO_METHOD_USERPTR: + CLEAR(buf); - buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - buf.memory = V4L2_MEMORY_USERPTR; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_USERPTR; - if (-1 == xioctl(VIDIOC_DQBUF, &buf)) - { - switch (errno) - { - case EAGAIN: - return 0; + if (-1 == xioctl(VIDIOC_DQBUF, &buf)) + { + switch (errno) + { + case EAGAIN: + return 0; - case EIO: - /* Could ignore EIO, see spec. */ + case EIO: + /* Could ignore EIO, see spec. */ - /* fall through */ + /* fall through */ - default: - throw_errno_exception("VIDIOC_DQBUF"); - } - } + default: + throw_errno_exception("VIDIOC_DQBUF"); + } + } - for (size_t i = 0; i < _buffers.size(); ++i) - { - if (buf.m.userptr == (unsigned long)_buffers[i].start && buf.length == _buffers[i].length) - { - break; - } - } + for (size_t i = 0; i < _buffers.size(); ++i) + { + if (buf.m.userptr == (unsigned long)_buffers[i].start && buf.length == _buffers[i].length) + { + break; + } + } - rc = process_image((void *)buf.m.userptr, buf.bytesused); + rc = process_image((void *)buf.m.userptr, buf.bytesused); - if (-1 == xioctl(VIDIOC_QBUF, &buf)) - { - throw_errno_exception("VIDIOC_QBUF"); - } - break; - } + if (-1 == xioctl(VIDIOC_QBUF, &buf)) + { + throw_errno_exception("VIDIOC_QBUF"); + } + break; + } - return rc ? 1 : 0; + return rc ? 1 : 0; } bool V4L2Grabber::process_image(const void *p, int size) { - if (++_currentFrame >= _frameDecimation) - { - // We do want a new frame... + if (++_currentFrame >= _frameDecimation) + { + // We do want a new frame... - if (size != _frameByteSize) - { - std::cout << "Frame too small: " << size << " != " << _frameByteSize << std::endl; - } - else - { - process_image(reinterpret_cast(p)); - _currentFrame = 0; // restart counting - return true; - } - } + if (size != _frameByteSize) + { + std::cout << "Frame too small: " << size << " != " << _frameByteSize << std::endl; + } + else + { + process_image(reinterpret_cast(p)); + _currentFrame = 0; // restart counting + return true; + } + } - return false; + return false; } void V4L2Grabber::process_image(const uint8_t * data) { - int width = _width; - int height = _height; + int width = _width; + int height = _height; - switch (_mode3D) - { - case VIDEO_3DSBS: - width = _width/2; - break; - case VIDEO_3DTAB: - height = _height/2; - break; - default: - break; - } + switch (_mode3D) + { + case VIDEO_3DSBS: + width = _width/2; + break; + case VIDEO_3DTAB: + height = _height/2; + break; + default: + break; + } - // create output structure - int outputWidth = (width - _cropLeft - _cropRight + _horizontalPixelDecimation/2) / _horizontalPixelDecimation; - int outputHeight = (height - _cropTop - _cropBottom + _verticalPixelDecimation/2) / _verticalPixelDecimation; - Image image(outputWidth, outputHeight); + // create output structure + int outputWidth = (width - _cropLeft - _cropRight + _horizontalPixelDecimation/2) / _horizontalPixelDecimation; + int outputHeight = (height - _cropTop - _cropBottom + _verticalPixelDecimation/2) / _verticalPixelDecimation; - for (int ySource = _cropTop + _verticalPixelDecimation/2, yDest = 0; ySource < height - _cropBottom; ySource += _verticalPixelDecimation, ++yDest) - { - for (int xSource = _cropLeft + _horizontalPixelDecimation/2, xDest = 0; xSource < width - _cropRight; xSource += _horizontalPixelDecimation, ++xDest) - { - ColorRgb & rgb = image(xDest, yDest); + // TODO: should this be the following (like X11): + //int outputWidth = (width - _cropLeft - _cropRight + _horizontalPixelDecimation/2 - 1) / _horizontalPixelDecimation + 1; + //int outputHeight = (height - _cropTop - _cropBottom + _verticalPixelDecimation/2 - 1) / _verticalPixelDecimation + 1; - switch (_pixelFormat) - { - case PIXELFORMAT_UYVY: - { - int index = (_width * ySource + xSource) * 2; - uint8_t y = data[index+1]; - uint8_t u = (xSource%2 == 0) ? data[index ] : data[index-2]; - uint8_t v = (xSource%2 == 0) ? data[index+2] : data[index ]; - yuv2rgb(y, u, v, rgb.red, rgb.green, rgb.blue); - } - break; - case PIXELFORMAT_YUYV: - { - int index = (_width * ySource + xSource) * 2; - uint8_t y = data[index]; - uint8_t u = (xSource%2 == 0) ? data[index+1] : data[index-1]; - uint8_t v = (xSource%2 == 0) ? data[index+3] : data[index+1]; - yuv2rgb(y, u, v, rgb.red, rgb.green, rgb.blue); - } - break; - case PIXELFORMAT_RGB32: - { - int index = (_width * ySource + xSource) * 4; - rgb.red = data[index ]; - rgb.green = data[index+1]; - rgb.blue = data[index+2]; - } - break; - default: - // this should not be possible - break; - } - } - } + Image image(outputWidth, outputHeight); - // check signal (only in center of the resulting image, because some grabbers have noise values along the borders) - bool noSignal = true; - for (unsigned x = 0; noSignal && x < (image.width()>>1); ++x) - { - int xImage = (image.width()>>2) + x; + for (int ySource = _cropTop + _verticalPixelDecimation/2, yDest = 0; ySource < height - _cropBottom; ySource += _verticalPixelDecimation, ++yDest) + { + for (int xSource = _cropLeft + _horizontalPixelDecimation/2, xDest = 0; xSource < width - _cropRight; xSource += _horizontalPixelDecimation, ++xDest) + { + ColorRgb & rgb = image(xDest, yDest); - for (unsigned y = 0; noSignal && y < (image.height()>>1); ++y) - { - int yImage = (image.height()>>2) + y; + switch (_pixelFormat) + { + case PIXELFORMAT_UYVY: + { + int index = (_width * ySource + xSource) * 2; + uint8_t y = data[index+1]; + uint8_t u = (xSource%2 == 0) ? data[index ] : data[index-2]; + uint8_t v = (xSource%2 == 0) ? data[index+2] : data[index ]; + yuv2rgb(y, u, v, rgb.red, rgb.green, rgb.blue); + } + break; + case PIXELFORMAT_YUYV: + { + int index = (_width * ySource + xSource) * 2; + uint8_t y = data[index]; + uint8_t u = (xSource%2 == 0) ? data[index+1] : data[index-1]; + uint8_t v = (xSource%2 == 0) ? data[index+3] : data[index+1]; + yuv2rgb(y, u, v, rgb.red, rgb.green, rgb.blue); + } + break; + case PIXELFORMAT_RGB32: + { + int index = (_width * ySource + xSource) * 4; + rgb.red = data[index ]; + rgb.green = data[index+1]; + rgb.blue = data[index+2]; + } + break; + default: + // this should not be possible + break; + } + } + } - ColorRgb & rgb = image(xImage, yImage); - noSignal &= rgb <= _noSignalThresholdColor; - } - } + // check signal (only in center of the resulting image, because some grabbers have noise values along the borders) + bool noSignal = true; + for (unsigned x = 0; noSignal && x < (image.width()>>1); ++x) + { + int xImage = (image.width()>>2) + x; - if (noSignal) - { - ++_noSignalCounter; - } - else - { - if (_noSignalCounter >= _noSignalCounterThreshold) - { - std::cout << "V4L2 Grabber: " << "Signal detected" << std::endl; - } + for (unsigned y = 0; noSignal && y < (image.height()>>1); ++y) + { + int yImage = (image.height()>>2) + y; - _noSignalCounter = 0; - } + ColorRgb & rgb = image(xImage, yImage); + noSignal &= rgb <= _noSignalThresholdColor; + } + } - if (_noSignalCounter < _noSignalCounterThreshold) - { - emit newFrame(image); - } - else if (_noSignalCounter == _noSignalCounterThreshold) - { - std::cout << "V4L2 Grabber: " << "Signal lost" << std::endl; - } + if (noSignal) + { + ++_noSignalCounter; + } + else + { + if (_noSignalCounter >= _noSignalCounterThreshold) + { + std::cout << "V4L2 Grabber: " << "Signal detected" << std::endl; + } + + _noSignalCounter = 0; + } + + if (_noSignalCounter < _noSignalCounterThreshold) + { + emit newFrame(image); + } + else if (_noSignalCounter == _noSignalCounterThreshold) + { + std::cout << "V4L2 Grabber: " << "Signal lost" << std::endl; + } } int V4L2Grabber::xioctl(int request, void *arg) { - int r; + int r; - do - { - r = ioctl(_fileDescriptor, request, arg); - } - while (-1 == r && EINTR == errno); + do + { + r = ioctl(_fileDescriptor, request, arg); + } + while (-1 == r && EINTR == errno); - return r; + return r; } void V4L2Grabber::throw_exception(const std::string & error) { - std::ostringstream oss; - oss << error << " error"; - throw std::runtime_error(oss.str()); + std::ostringstream oss; + oss << error << " error"; + throw std::runtime_error(oss.str()); } void V4L2Grabber::throw_errno_exception(const std::string & error) { - std::ostringstream oss; - oss << error << " error " << errno << ", " << strerror(errno); - throw std::runtime_error(oss.str()); + std::ostringstream oss; + oss << error << " error " << errno << ", " << strerror(errno); + throw std::runtime_error(oss.str()); } diff --git a/libsrc/grabber/x11/X11Grabber.cpp b/libsrc/grabber/x11/X11Grabber.cpp index 93f6e434..dd991aa6 100644 --- a/libsrc/grabber/x11/X11Grabber.cpp +++ b/libsrc/grabber/x11/X11Grabber.cpp @@ -64,8 +64,11 @@ Image & X11Grabber::grab() // Copy the capture XImage to the local image (and apply required decimation) ColorRgb * outputPtr = _image.memptr(); + int width = 0; + int height = 0; for (int iY=(_pixelDecimation/2); iY & X11Grabber::grab() // Move to the next output pixel ++outputPtr; + ++width; } + ++height; } + + std::cout << "decimated X11 message: " << width << " x " << height << std::endl; + // Cleanup allocated resources of the X11 grab XDestroyImage(xImage); @@ -107,8 +115,9 @@ int X11Grabber::updateScreenDimensions() std::cout << "[" << _screenWidth << "x" << _screenHeight <<"]" << std::endl; // Update the size of the buffer used to transfer the screenshot - int width = (_screenWidth - 2 * _cropWidth + _pixelDecimation/2) / _pixelDecimation; - int height = (_screenHeight - 2 * _cropHeight + _pixelDecimation/2) / _pixelDecimation; + int width = (_screenWidth - 2 * _cropWidth - _pixelDecimation/2 - 1) / _pixelDecimation + 1; + int height = (_screenHeight - 2 * _cropHeight - _pixelDecimation/2 - 1) / _pixelDecimation + 1; + _image.resize(width, height); return 0; From 93de6569855179523f5608b4eaae84393ada6db9 Mon Sep 17 00:00:00 2001 From: poljvd Date: Mon, 1 Dec 2014 21:35:04 +0100 Subject: [PATCH 43/59] fix slot name Former-commit-id: fc2faf3b70be3b0a01cf3c72f7092c57e7c88783 --- src/hyperion-x11/hyperion-x11.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hyperion-x11/hyperion-x11.cpp b/src/hyperion-x11/hyperion-x11.cpp index 8d354378..c473b7ed 100644 --- a/src/hyperion-x11/hyperion-x11.cpp +++ b/src/hyperion-x11/hyperion-x11.cpp @@ -73,7 +73,7 @@ int main(int argc, char ** argv) ProtoConnectionWrapper protoWrapper(argAddress.getValue(), argPriority.getValue(), 1000, argSkipReply.isSet()); // Connect the screen capturing to the proto processing - QObject::connect(&x11Wrapper, SIGNAL(sig_screenshot(const Image &)), &protoWrapper, SLOT(process(Image))); + QObject::connect(&x11Wrapper, SIGNAL(sig_screenshot(const Image &)), &protoWrapper, SLOT(receiveImage(Image))); // Start the capturing x11Wrapper.start(); From 205fe8ab667e6f34a975e61453d7d5d8a0f20cbf Mon Sep 17 00:00:00 2001 From: wayland Date: Wed, 3 Dec 2014 09:34:48 +0100 Subject: [PATCH 44/59] Added APA102 device to factory Former-commit-id: ecea592ddf97bdcc2c54d12af194c15d6c09966a --- libsrc/leddevice/LedDeviceFactory.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/libsrc/leddevice/LedDeviceFactory.cpp b/libsrc/leddevice/LedDeviceFactory.cpp index fb602b02..96746551 100755 --- a/libsrc/leddevice/LedDeviceFactory.cpp +++ b/libsrc/leddevice/LedDeviceFactory.cpp @@ -14,6 +14,7 @@ #include "LedDeviceLpd8806.h" #include "LedDeviceP9813.h" #include "LedDeviceWs2801.h" + #include "LedDeviceAPA102.h" #endif #ifdef ENABLE_TINKERFORGE @@ -85,6 +86,15 @@ LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) deviceP9813->open(); device = deviceP9813; + else if (type == "apa102") + { + const std::string output = deviceConfig["output"].asString(); + const unsigned rate = deviceConfig["rate"].asInt(); + + LedDeviceAPA102* deviceAPA102 = new LedDeviceAPA102(output, rate); + deviceAPA102->open(); + + device = deviceAPA102; } else if (type == "ws2801" || type == "lightberry") { From 4b716a9e2f17663d775bd94c922362c97a7984eb Mon Sep 17 00:00:00 2001 From: wayland Date: Wed, 3 Dec 2014 09:35:10 +0100 Subject: [PATCH 45/59] Added the apa102 engine class Former-commit-id: 03e11a19aed2ae782c5d134e1f4fcf9faea1969e --- libsrc/leddevice/LedDeviceAPA102.cpp | 47 ++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 libsrc/leddevice/LedDeviceAPA102.cpp diff --git a/libsrc/leddevice/LedDeviceAPA102.cpp b/libsrc/leddevice/LedDeviceAPA102.cpp new file mode 100644 index 00000000..c6b3278b --- /dev/null +++ b/libsrc/leddevice/LedDeviceAPA102.cpp @@ -0,0 +1,47 @@ + +// STL includes +#include +#include +#include + +// Linux includes +#include +#include + +// hyperion local includes +#include "LedDeviceAPA102.h" + +LedDeviceAPA102::LedDeviceAPA102(const std::string& outputDevice, const unsigned baudrate) : + LedSpiDevice(outputDevice, baudrate, 500000), + mLedCount(0) +{ + // empty +} + +int LedDeviceAPA102::write(const std::vector &ledValues) +{ + mLedCount = ledValues.size(); + + const unsigned dataLen = 8 + (ledValues.size() * (sizeof(ColorRgb) + 1)); + const uint8_t data[dataLen] = { 0x00, 0x00, 0x00, 0x00 }; + int position = 4; + for (ColorRgb* rgb : ledValues ){ + data[i++] = 0xFF; + data[i++] = rgb.red; + data[i++] = rgb.green; + data[i++] = rgb.blue; + } + + // Write end frame + data[i++] = 0xFF; + data[i++] = 0xFF; + data[i++] = 0xFF; + data[i++] = 0xFF; + + return writeBytes(dataLen, data); +} + +int LedDeviceAPA102::switchOff() +{ + return write(std::vector(mLedCount, ColorRgb{0,0,0})); +} From bdbe6a8c85a5c37826a2135847bb13bcea0fec74 Mon Sep 17 00:00:00 2001 From: wayland Date: Wed, 3 Dec 2014 18:36:18 +0100 Subject: [PATCH 46/59] Added APA102 to cmakelist Former-commit-id: d495d538a46dc19e535a3ae0e2c1069954fdf963 --- libsrc/leddevice/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt index 4304c874..19f1d96e 100755 --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -57,6 +57,7 @@ if(ENABLE_SPIDEV) ${CURRENT_SOURCE_DIR}/LedDeviceLpd8806.h ${CURRENT_SOURCE_DIR}/LedDeviceP9813.h ${CURRENT_SOURCE_DIR}/LedDeviceWs2801.h + ${CURRENT_SOURCE_DIR}/LedDeviceAPA102.h ) SET(Leddevice_SOURCES ${Leddevice_SOURCES} @@ -65,6 +66,7 @@ if(ENABLE_SPIDEV) ${CURRENT_SOURCE_DIR}/LedDeviceLpd8806.cpp ${CURRENT_SOURCE_DIR}/LedDeviceP9813.cpp ${CURRENT_SOURCE_DIR}/LedDeviceWs2801.cpp + ${CURRENT_SOURCE_DIR}/LedDeviceAPA102.cpp ) endif(ENABLE_SPIDEV) From a4bcdcc10a3d2ba33d3f057116781a10432b41b8 Mon Sep 17 00:00:00 2001 From: wayland Date: Wed, 3 Dec 2014 18:41:44 +0100 Subject: [PATCH 47/59] Added APA102 device to factory Former-commit-id: 4d7c8889aee2ca7b75c0e0ec90e9b5833257324d --- libsrc/leddevice/LedDeviceFactory.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libsrc/leddevice/LedDeviceFactory.cpp b/libsrc/leddevice/LedDeviceFactory.cpp index 96746551..3208b393 100755 --- a/libsrc/leddevice/LedDeviceFactory.cpp +++ b/libsrc/leddevice/LedDeviceFactory.cpp @@ -86,6 +86,7 @@ LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) deviceP9813->open(); device = deviceP9813; + } else if (type == "apa102") { const std::string output = deviceConfig["output"].asString(); From ac42b7225384df788d06dea6bc95e3ebd509f78f Mon Sep 17 00:00:00 2001 From: thigg Date: Thu, 11 Dec 2014 14:30:36 +0200 Subject: [PATCH 48/59] fixed assembler instructions to work without optimization Former-commit-id: 788583de5e7614a5450a533702e75baa5bf2f034 --- libsrc/leddevice/LedDeviceWS2812b.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libsrc/leddevice/LedDeviceWS2812b.cpp b/libsrc/leddevice/LedDeviceWS2812b.cpp index dc6bbe70..67a10947 100644 --- a/libsrc/leddevice/LedDeviceWS2812b.cpp +++ b/libsrc/leddevice/LedDeviceWS2812b.cpp @@ -268,7 +268,7 @@ LedDeviceWS2812b::LedDeviceWS2812b() : static inline __attribute__((always_inline)) uint32_t arm_ror_imm(uint32_t v, uint32_t sh) { uint32_t d; - asm ("ROR %[Rd], %[Rm], %[Is]" : [Rd] "=r" (d) : [Rm] "r" (v), [Is] "i" (sh)); + asm ("ROR %[Rd], %[Rm], %[Is]" : [Rd] "=r" (d) : [Rm] "r" (v), [Is] "r" (sh)); return d; } @@ -278,7 +278,7 @@ static inline __attribute__((always_inline)) uint32_t arm_ror_imm_add_on_carry(u uint32_t d; asm ("RORS %[Rd], %[Rm], %[Is]\n\t" "ADDCS %[Rd1], %[Rd1], #1" - : [Rd] "=r" (d), [Rd1] "+r" (inc): [Rm] "r" (v), [Is] "i" (sh)); + : [Rd] "=r" (d), [Rd1] "+r" (inc): [Rm] "r" (v), [Is] "r" (sh)); return d; } From 63d0f6652e2eadcac122143491cebb268e3d6082 Mon Sep 17 00:00:00 2001 From: wayland Date: Thu, 11 Dec 2014 13:48:14 +0100 Subject: [PATCH 49/59] Rewrote the class to make it work with apa102 led strips Former-commit-id: 9f63b95fb635b15e550d9368c7c8dbc632572bc5 --- libsrc/leddevice/LedDeviceAPA102.cpp | 31 ++++++++++++---------------- libsrc/leddevice/LedDeviceAPA102.h | 5 +++-- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/libsrc/leddevice/LedDeviceAPA102.cpp b/libsrc/leddevice/LedDeviceAPA102.cpp index c6b3278b..2a2f5e8c 100644 --- a/libsrc/leddevice/LedDeviceAPA102.cpp +++ b/libsrc/leddevice/LedDeviceAPA102.cpp @@ -13,35 +13,30 @@ LedDeviceAPA102::LedDeviceAPA102(const std::string& outputDevice, const unsigned baudrate) : LedSpiDevice(outputDevice, baudrate, 500000), - mLedCount(0) + _ledBuffer(0) { // empty } int LedDeviceAPA102::write(const std::vector &ledValues) { - mLedCount = ledValues.size(); - - const unsigned dataLen = 8 + (ledValues.size() * (sizeof(ColorRgb) + 1)); - const uint8_t data[dataLen] = { 0x00, 0x00, 0x00, 0x00 }; - int position = 4; - for (ColorRgb* rgb : ledValues ){ - data[i++] = 0xFF; - data[i++] = rgb.red; - data[i++] = rgb.green; - data[i++] = rgb.blue; + const unsigned int mLedCount = (ledValues.size() * 4) + 8; + if(_ledBuffer.size() != mLedCount){ + _ledBuffer.resize(mLedCount, 0x00); } - // Write end frame - data[i++] = 0xFF; - data[i++] = 0xFF; - data[i++] = 0xFF; - data[i++] = 0xFF; + for (unsigned iLed=1; iLed<=ledValues.size(); ++iLed) { + const ColorRgb& rgb = ledValues[iLed]; + _ledBuffer[iLed*4] = 0xFF; + _ledBuffer[iLed*4+1] = rgb.red; + _ledBuffer[iLed*4+2] = rgb.green; + _ledBuffer[iLed*4+3] = rgb.blue; + } - return writeBytes(dataLen, data); + return writeBytes(_ledBuffer.size(), _ledBuffer.data()); } int LedDeviceAPA102::switchOff() { - return write(std::vector(mLedCount, ColorRgb{0,0,0})); + return write(std::vector(_ledBuffer.size(), ColorRgb{0,0,0})); } diff --git a/libsrc/leddevice/LedDeviceAPA102.h b/libsrc/leddevice/LedDeviceAPA102.h index d707ebdb..3cc4e518 100644 --- a/libsrc/leddevice/LedDeviceAPA102.h +++ b/libsrc/leddevice/LedDeviceAPA102.h @@ -36,6 +36,7 @@ public: private: - /// the number of leds (needed when switching off) - size_t mLedCount; + /// The buffer containing the packed RGB values + std::vector _ledBuffer; + }; From e608b11caaa7f8b559c60e1c4c5421f7e579a2e3 Mon Sep 17 00:00:00 2001 From: poljvd Date: Sun, 14 Dec 2014 14:26:59 +0100 Subject: [PATCH 50/59] Extract ImageResampler from X11Grabber Former-commit-id: 63e95fa85fc0ef5a66f6eb8b396cf4b6370cf5dd --- CMakeLists.txt | 4 +- include/grabber/V4L2Grabber.h | 136 ++++----- include/grabber/X11Grabber.h | 12 +- include/utils/Image.h | 353 +++++++++++------------ include/utils/ImageResampler.h | 40 +++ include/{grabber => utils}/PixelFormat.h | 17 +- libsrc/grabber/v4l2/CMakeLists.txt | 1 - libsrc/grabber/v4l2/V4L2Grabber.cpp | 16 +- libsrc/grabber/x11/X11Grabber.cpp | 52 +--- libsrc/utils/CMakeLists.txt | 6 + libsrc/utils/ImageResampler.cpp | 133 +++++++++ src/hyperion-v4l2/PixelFormatParameter.h | 58 ++-- src/hyperion-v4l2/hyperion-v4l2.cpp | 1 + src/hyperion-x11/X11Wrapper.cpp | 4 +- src/hyperion-x11/X11Wrapper.h | 2 +- src/hyperion-x11/hyperion-x11.cpp | 27 +- 16 files changed, 517 insertions(+), 345 deletions(-) create mode 100644 include/utils/ImageResampler.h rename include/{grabber => utils}/PixelFormat.h (70%) create mode 100644 libsrc/utils/ImageResampler.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 251bf2da..d8a25196 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,8 +57,8 @@ include_directories(${CMAKE_SOURCE_DIR}/include) # Prefer static linking over dynamic #set(CMAKE_FIND_LIBRARY_SUFFIXES ".a;.so") -#set(CMAKE_BUILD_TYPE "Debug") -set(CMAKE_BUILD_TYPE "Release") +set(CMAKE_BUILD_TYPE "Debug") +#set(CMAKE_BUILD_TYPE "Release") # enable C++11 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -Wall") diff --git a/include/grabber/V4L2Grabber.h b/include/grabber/V4L2Grabber.h index fe867112..d130a5e5 100644 --- a/include/grabber/V4L2Grabber.h +++ b/include/grabber/V4L2Grabber.h @@ -11,119 +11,119 @@ // util includes #include #include +#include #include // grabber includes #include -#include /// Capture class for V4L2 devices /// /// @see http://linuxtv.org/downloads/v4l-dvb-apis/capture-example.html class V4L2Grabber : public QObject { - Q_OBJECT + Q_OBJECT public: - V4L2Grabber(const std::string & device, - int input, - VideoStandard videoStandard, PixelFormat pixelFormat, - int width, - int height, - int frameDecimation, - int horizontalPixelDecimation, - int verticalPixelDecimation); - virtual ~V4L2Grabber(); + V4L2Grabber(const std::string & device, + int input, + VideoStandard videoStandard, PixelFormat pixelFormat, + int width, + int height, + int frameDecimation, + int horizontalPixelDecimation, + int verticalPixelDecimation); + virtual ~V4L2Grabber(); public slots: - void setCropping(int cropLeft, - int cropRight, - int cropTop, - int cropBottom); + void setCropping(int cropLeft, + int cropRight, + int cropTop, + int cropBottom); - void set3D(VideoMode mode); + void set3D(VideoMode mode); - void setSignalThreshold(double redSignalThreshold, - double greenSignalThreshold, - double blueSignalThreshold, - int noSignalCounterThreshold); + void setSignalThreshold(double redSignalThreshold, + double greenSignalThreshold, + double blueSignalThreshold, + int noSignalCounterThreshold); - void start(); + void start(); - void stop(); + void stop(); signals: - void newFrame(const Image & image); + void newFrame(const Image & image); private slots: - int read_frame(); + int read_frame(); private: - void open_device(); + void open_device(); - void close_device(); + void close_device(); - void init_read(unsigned int buffer_size); + void init_read(unsigned int buffer_size); - void init_mmap(); + void init_mmap(); - void init_userp(unsigned int buffer_size); + void init_userp(unsigned int buffer_size); - void init_device(VideoStandard videoStandard, int input); + void init_device(VideoStandard videoStandard, int input); - void uninit_device(); + void uninit_device(); - void start_capturing(); + void start_capturing(); - void stop_capturing(); + void stop_capturing(); - bool process_image(const void *p, int size); + bool process_image(const void *p, int size); - void process_image(const uint8_t *p); + void process_image(const uint8_t *p); - int xioctl(int request, void *arg); + int xioctl(int request, void *arg); - void throw_exception(const std::string &error); + void throw_exception(const std::string &error); - void throw_errno_exception(const std::string &error); + void throw_errno_exception(const std::string &error); private: - enum io_method { - IO_METHOD_READ, - IO_METHOD_MMAP, - IO_METHOD_USERPTR - }; + enum io_method { + IO_METHOD_READ, + IO_METHOD_MMAP, + IO_METHOD_USERPTR + }; - struct buffer { - void *start; - size_t length; - }; + struct buffer { + void *start; + size_t length; + }; private: - const std::string _deviceName; - const io_method _ioMethod; - int _fileDescriptor; - std::vector _buffers; + const std::string _deviceName; + const io_method _ioMethod; + int _fileDescriptor; + std::vector _buffers; - PixelFormat _pixelFormat; - int _width; - int _height; - int _frameByteSize; - int _cropLeft; - int _cropRight; - int _cropTop; - int _cropBottom; - int _frameDecimation; - int _horizontalPixelDecimation; - int _verticalPixelDecimation; - int _noSignalCounterThreshold; + PixelFormat _pixelFormat; + int _width; + int _height; + int _frameByteSize; + int _cropLeft; + int _cropRight; + int _cropTop; + int _cropBottom; + int _frameDecimation; + int _horizontalPixelDecimation; + int _verticalPixelDecimation; + int _noSignalCounterThreshold; - ColorRgb _noSignalThresholdColor; + ColorRgb _noSignalThresholdColor; - VideoMode _mode3D; + VideoMode _mode3D; - int _currentFrame; - int _noSignalCounter; + int _currentFrame; + int _noSignalCounter; - QSocketNotifier * _streamNotifier; + QSocketNotifier * _streamNotifier; }; diff --git a/include/grabber/X11Grabber.h b/include/grabber/X11Grabber.h index 21d36c47..ee857b01 100644 --- a/include/grabber/X11Grabber.h +++ b/include/grabber/X11Grabber.h @@ -2,6 +2,7 @@ // Hyperion-utils includes #include #include +#include // X11 includes #include @@ -10,7 +11,7 @@ class X11Grabber { public: - X11Grabber(const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation); + X11Grabber(int cropLeft, int cropRight, int cropTop, int cropBottom, int horizontalPixelDecimation, int verticalPixelDecimation); virtual ~X11Grabber(); @@ -19,11 +20,12 @@ public: Image & grab(); private: + ImageResampler _imageResampler; - const unsigned _pixelDecimation; - - const unsigned _cropWidth; - const unsigned _cropHeight; + int _cropLeft; + int _cropRight; + int _cropTop; + int _cropBottom; /// Reference to the X11 display (nullptr if not opened) Display * _x11Display; diff --git a/include/utils/Image.h b/include/utils/Image.h index 5e8b45ad..19b5ae35 100644 --- a/include/utils/Image.h +++ b/include/utils/Image.h @@ -11,207 +11,202 @@ class Image { public: - typedef Pixel_T pixel_type; + typedef Pixel_T pixel_type; - /// - /// Default constructor for an image - /// - Image() : - _width(1), - _height(1), - _pixels(new Pixel_T[2]), - _endOfPixels(_pixels + 1) - { - memset(_pixels, 0, 2*sizeof(Pixel_T)); - } + /// + /// Default constructor for an image + /// + Image() : + _width(1), + _height(1), + _pixels(new Pixel_T[2]), + _endOfPixels(_pixels + 1) + { + memset(_pixels, 0, 2*sizeof(Pixel_T)); + } - /// - /// Constructor for an image with specified width and height - /// - /// @param width The width of the image - /// @param height The height of the image - /// - Image(const unsigned width, const unsigned height) : - _width(width), - _height(height), - _pixels(new Pixel_T[width * height + 1]), - _endOfPixels(_pixels + width * height) - { - memset(_pixels, 0, (_width*_height+1)*sizeof(Pixel_T)); - } + /// + /// Constructor for an image with specified width and height + /// + /// @param width The width of the image + /// @param height The height of the image + /// + Image(const unsigned width, const unsigned height) : + _width(width), + _height(height), + _pixels(new Pixel_T[width * height + 1]), + _endOfPixels(_pixels + width * height) + { + memset(_pixels, 0, (_width*_height+1)*sizeof(Pixel_T)); + } - /// - /// Constructor for an image with specified width and height - /// - /// @param width The width of the image - /// @param height The height of the image - /// @param background The color of the image - /// - Image(const unsigned width, const unsigned height, const Pixel_T background) : - _width(width), - _height(height), - _pixels(new Pixel_T[width * height + 1]), - _endOfPixels(_pixels + width * height) - { - std::fill(_pixels, _endOfPixels, background); - } + /// + /// Constructor for an image with specified width and height + /// + /// @param width The width of the image + /// @param height The height of the image + /// @param background The color of the image + /// + Image(const unsigned width, const unsigned height, const Pixel_T background) : + _width(width), + _height(height), + _pixels(new Pixel_T[width * height + 1]), + _endOfPixels(_pixels + width * height) + { + std::fill(_pixels, _endOfPixels, background); + } - /// - /// Copy constructor for an image - /// - Image(const Image & other) : - _width(other._width), - _height(other._height), - _pixels(new Pixel_T[other._width * other._height + 1]), - _endOfPixels(_pixels + other._width * other._height) - { - memcpy(_pixels, other._pixels, other._width * other._height * sizeof(Pixel_T)); - } + /// + /// Copy constructor for an image + /// + Image(const Image & other) : + _width(other._width), + _height(other._height), + _pixels(new Pixel_T[other._width * other._height + 1]), + _endOfPixels(_pixels + other._width * other._height) + { + memcpy(_pixels, other._pixels, other._width * other._height * sizeof(Pixel_T)); + } - /// - /// Destructor - /// - ~Image() - { - delete[] _pixels; - } + /// + /// Destructor + /// + ~Image() + { + delete[] _pixels; + } - /// - /// Returns the width of the image - /// - /// @return The width of the image - /// - inline unsigned width() const - { - return _width; - } + /// + /// Returns the width of the image + /// + /// @return The width of the image + /// + inline unsigned width() const + { + return _width; + } - /// - /// Returns the height of the image - /// - /// @return The height of the image - /// - inline unsigned height() const - { - return _height; - } + /// + /// Returns the height of the image + /// + /// @return The height of the image + /// + inline unsigned height() const + { + return _height; + } - uint8_t alpha(const unsigned pixel) const - { - return (_pixels + pixel)->red; - } + uint8_t red(const unsigned pixel) const + { + return (_pixels + pixel)->red; + } - uint8_t red(const unsigned pixel) const - { - return (_pixels + pixel)->red; - } + uint8_t green(const unsigned pixel) const + { + return (_pixels + pixel)->green; + } - uint8_t green(const unsigned pixel) const - { - return (_pixels + pixel)->green; - } + uint8_t blue(const unsigned pixel) const + { + return (_pixels + pixel)->blue; + } - uint8_t blue(const unsigned pixel) const - { - return (_pixels + pixel)->blue; - } + /// + /// Returns a const reference to a specified pixel in the image + /// + /// @param x The x index + /// @param y The y index + /// + /// @return const reference to specified pixel + /// + const Pixel_T& operator()(const unsigned x, const unsigned y) const + { + return _pixels[toIndex(x,y)]; + } - /// - /// Returns a const reference to a specified pixel in the image - /// - /// @param x The x index - /// @param y The y index - /// - /// @return const reference to specified pixel - /// - const Pixel_T& operator()(const unsigned x, const unsigned y) const - { - return _pixels[toIndex(x,y)]; - } + /// + /// Returns a reference to a specified pixel in the image + /// + /// @param x The x index + /// @param y The y index + /// + /// @return reference to specified pixel + /// + Pixel_T& operator()(const unsigned x, const unsigned y) + { + return _pixels[toIndex(x,y)]; + } - /// - /// Returns a reference to a specified pixel in the image - /// - /// @param x The x index - /// @param y The y index - /// - /// @return reference to specified pixel - /// - Pixel_T& operator()(const unsigned x, const unsigned y) - { - return _pixels[toIndex(x,y)]; - } + /// Resize the image + /// @param width The width of the image + /// @param height The height of the image + void resize(const unsigned width, const unsigned height) + { + if ((width*height) > (_endOfPixels-_pixels)) + { + delete[] _pixels; + _pixels = new Pixel_T[width*height + 1]; + _endOfPixels = _pixels + width*height; + } - /// Resize the image - /// @param width The width of the image - /// @param height The height of the image - void resize(const unsigned width, const unsigned height) - { - if ((width*height) > (_endOfPixels-_pixels)) - { - delete[] _pixels; - _pixels = new Pixel_T[width*height + 1]; - _endOfPixels = _pixels + width*height; - } + _width = width; + _height = height; + } - _width = width; - _height = height; - } + /// + /// Copies another image into this image. The images should have exactly the same size. + /// + /// @param other The image to copy into this + /// + void copy(const Image& other) + { + assert(other._width == _width); + assert(other._height == _height); - /// - /// Copies another image into this image. The images should have exactly the same size. - /// - /// @param other The image to copy into this - /// - void copy(const Image& other) - { - assert(other._width == _width); - assert(other._height == _height); + memcpy(_pixels, other._pixels, _width*_height*sizeof(Pixel_T)); + } - memcpy(_pixels, other._pixels, _width*_height*sizeof(Pixel_T)); - } + /// + /// Returns a memory pointer to the first pixel in the image + /// @return The memory pointer to the first pixel + /// + Pixel_T* memptr() + { + return _pixels; + } - /// - /// Returns a memory pointer to the first pixel in the image - /// @return The memory pointer to the first pixel - /// - Pixel_T* memptr() - { - return _pixels; - } - - /// - /// Returns a const memory pointer to the first pixel in the image - /// @return The const memory pointer to the first pixel - /// - const Pixel_T* memptr() const - { - return _pixels; - } + /// + /// Returns a const memory pointer to the first pixel in the image + /// @return The const memory pointer to the first pixel + /// + const Pixel_T* memptr() const + { + return _pixels; + } private: - /// - /// Translate x and y coordinate to index of the underlying vector - /// - /// @param x The x index - /// @param y The y index - /// - /// @return The index into the underlying data-vector - /// - inline unsigned toIndex(const unsigned x, const unsigned y) const - { - return y*_width + x; - } + /// + /// Translate x and y coordinate to index of the underlying vector + /// + /// @param x The x index + /// @param y The y index + /// + /// @return The index into the underlying data-vector + /// + inline unsigned toIndex(const unsigned x, const unsigned y) const + { + return y*_width + x; + } private: - /// The width of the image - unsigned _width; - /// The height of the image - unsigned _height; + /// The width of the image + unsigned _width; + /// The height of the image + unsigned _height; - /// The pixels of the image - Pixel_T* _pixels; + /// The pixels of the image + Pixel_T* _pixels; - /// Pointer to the last(extra) pixel - Pixel_T* _endOfPixels; + /// Pointer to the last(extra) pixel + Pixel_T* _endOfPixels; }; diff --git a/include/utils/ImageResampler.h b/include/utils/ImageResampler.h new file mode 100644 index 00000000..b81eb5b7 --- /dev/null +++ b/include/utils/ImageResampler.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include + +class ImageResampler +{ +public: + ImageResampler(); + ~ImageResampler(); + + void setHorizontalPixelDecimation(int decimator); + + void setVerticalPixelDecimation(int decimator); + + void setCropping(int cropLeft, + int cropRight, + int cropTop, + int cropBottom); + + void set3D(VideoMode mode); + + void processImage(const uint8_t * data, int width, int height, int lineLength, PixelFormat pixelFormat, + Image & outputImage) const; + +private: + static inline uint8_t clamp(int x); + static void yuv2rgb(uint8_t y, uint8_t u, uint8_t v, uint8_t & r, uint8_t & g, uint8_t & b); + +private: + int _horizontalDecimation; + int _verticalDecimation; + int _cropLeft; + int _cropRight; + int _cropTop; + int _cropBottom; + VideoMode _videoMode; +}; diff --git a/include/grabber/PixelFormat.h b/include/utils/PixelFormat.h similarity index 70% rename from include/grabber/PixelFormat.h rename to include/utils/PixelFormat.h index 6afc7510..b269dfcb 100644 --- a/include/grabber/PixelFormat.h +++ b/include/utils/PixelFormat.h @@ -9,8 +9,9 @@ enum PixelFormat { PIXELFORMAT_YUYV, PIXELFORMAT_UYVY, - PIXELFORMAT_RGB32, - PIXELFORMAT_NO_CHANGE + PIXELFORMAT_RGB32, + PIXELFORMAT_BGR32, + PIXELFORMAT_NO_CHANGE }; inline PixelFormat parsePixelFormat(std::string pixelFormat) @@ -26,10 +27,14 @@ inline PixelFormat parsePixelFormat(std::string pixelFormat) { return PIXELFORMAT_UYVY; } - else if (pixelFormat == "rgb32") - { - return PIXELFORMAT_RGB32; - } + else if (pixelFormat == "rgb32") + { + return PIXELFORMAT_RGB32; + } + else if (pixelFormat == "bgr32") + { + return PIXELFORMAT_BGR32; + } // return the default NO_CHANGE return PIXELFORMAT_NO_CHANGE; diff --git a/libsrc/grabber/v4l2/CMakeLists.txt b/libsrc/grabber/v4l2/CMakeLists.txt index 540a1217..5c7844e0 100644 --- a/libsrc/grabber/v4l2/CMakeLists.txt +++ b/libsrc/grabber/v4l2/CMakeLists.txt @@ -9,7 +9,6 @@ SET(V4L2_QT_HEADERS SET(V4L2_HEADERS ${CURRENT_HEADER_DIR}/VideoStandard.h - ${CURRENT_HEADER_DIR}/PixelFormat.h ) SET(V4L2_SOURCES diff --git a/libsrc/grabber/v4l2/V4L2Grabber.cpp b/libsrc/grabber/v4l2/V4L2Grabber.cpp index 698d828f..27eefd43 100644 --- a/libsrc/grabber/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/v4l2/V4L2Grabber.cpp @@ -20,19 +20,19 @@ static inline uint8_t clamp(int x) { - return (x<0) ? 0 : ((x>255) ? 255 : uint8_t(x)); + return (x<0) ? 0 : ((x>255) ? 255 : uint8_t(x)); } static void yuv2rgb(uint8_t y, uint8_t u, uint8_t v, uint8_t & r, uint8_t & g, uint8_t & b) { - // see: http://en.wikipedia.org/wiki/YUV#Y.27UV444_to_RGB888_conversion - int c = y - 16; - int d = u - 128; - int e = v - 128; + // see: http://en.wikipedia.org/wiki/YUV#Y.27UV444_to_RGB888_conversion + int c = y - 16; + int d = u - 128; + int e = v - 128; - r = clamp((298 * c + 409 * e + 128) >> 8); - g = clamp((298 * c - 100 * d - 208 * e + 128) >> 8); - b = clamp((298 * c + 516 * d + 128) >> 8); + r = clamp((298 * c + 409 * e + 128) >> 8); + g = clamp((298 * c - 100 * d - 208 * e + 128) >> 8); + b = clamp((298 * c + 516 * d + 128) >> 8); } diff --git a/libsrc/grabber/x11/X11Grabber.cpp b/libsrc/grabber/x11/X11Grabber.cpp index dd991aa6..0787e056 100644 --- a/libsrc/grabber/x11/X11Grabber.cpp +++ b/libsrc/grabber/x11/X11Grabber.cpp @@ -8,16 +8,20 @@ // X11Grabber includes #include -X11Grabber::X11Grabber(const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation) : - _pixelDecimation(pixelDecimation), - _cropWidth(cropHorizontal), - _cropHeight(cropVertical), +X11Grabber::X11Grabber(int cropLeft, int cropRight, int cropTop, int cropBottom, int horizontalPixelDecimation, int verticalPixelDecimation) : + _imageResampler(), + _cropLeft(cropLeft), + _cropRight(cropRight), + _cropTop(cropTop), + _cropBottom(cropBottom), _x11Display(nullptr), _screenWidth(0), _screenHeight(0), _image(0,0) { - // empty + _imageResampler.setHorizontalPixelDecimation(horizontalPixelDecimation); + _imageResampler.setVerticalPixelDecimation(verticalPixelDecimation); + _imageResampler.setCropping(0, 0, 0, 0); // cropping is performed by XGetImage } X11Grabber::~X11Grabber() @@ -51,42 +55,18 @@ Image & X11Grabber::grab() updateScreenDimensions(); - const int croppedWidth = _screenWidth - 2*_cropWidth; - const int croppedHeight = _screenHeight - 2*_cropHeight; + const unsigned croppedWidth = _screenWidth - _cropLeft - _cropRight; + const unsigned croppedHeight = _screenHeight - _cropTop - _cropBottom; // Capture the current screen - XImage * xImage = XGetImage(_x11Display, DefaultRootWindow(_x11Display), _cropWidth, _cropHeight, croppedWidth, croppedHeight, AllPlanes, ZPixmap); + XImage * xImage = XGetImage(_x11Display, DefaultRootWindow(_x11Display), _cropLeft, _cropTop, croppedWidth, croppedHeight, AllPlanes, ZPixmap); if (xImage == nullptr) { std::cerr << "Grab failed" << std::endl; return _image; } - // Copy the capture XImage to the local image (and apply required decimation) - ColorRgb * outputPtr = _image.memptr(); - int width = 0; - int height = 0; - for (int iY=(_pixelDecimation/2); iYred = uint8_t((pixel >> 16) & 0xff); - outputPtr->green = uint8_t((pixel >> 8) & 0xff); - outputPtr->blue = uint8_t((pixel >> 0) & 0xff); - - // Move to the next output pixel - ++outputPtr; - ++width; - } - ++height; - } - - std::cout << "decimated X11 message: " << width << " x " << height << std::endl; + _imageResampler.processImage(reinterpret_cast(xImage->data), xImage->width, xImage->height, xImage->bytes_per_line, PIXELFORMAT_BGR32, _image); // Cleanup allocated resources of the X11 grab XDestroyImage(xImage); @@ -114,11 +94,5 @@ int X11Grabber::updateScreenDimensions() _screenHeight = window_attributes_return.height; std::cout << "[" << _screenWidth << "x" << _screenHeight <<"]" << std::endl; - // Update the size of the buffer used to transfer the screenshot - int width = (_screenWidth - 2 * _cropWidth - _pixelDecimation/2 - 1) / _pixelDecimation + 1; - int height = (_screenHeight - 2 * _cropHeight - _pixelDecimation/2 - 1) / _pixelDecimation + 1; - - _image.resize(width, height); - return 0; } diff --git a/libsrc/utils/CMakeLists.txt b/libsrc/utils/CMakeLists.txt index b1f0709e..5549ce74 100644 --- a/libsrc/utils/CMakeLists.txt +++ b/libsrc/utils/CMakeLists.txt @@ -13,6 +13,12 @@ add_library(hyperion-utils ${CURRENT_HEADER_DIR}/Image.h ${CURRENT_HEADER_DIR}/Sleep.h + ${CURRENT_HEADER_DIR}/PixelFormat.h + ${CURRENT_HEADER_DIR}/VideoMode.h + + ${CURRENT_HEADER_DIR}/ImageResampler.h + ${CURRENT_SOURCE_DIR}/ImageResampler.cpp + ${CURRENT_HEADER_DIR}/HsvTransform.h ${CURRENT_SOURCE_DIR}/HsvTransform.cpp ${CURRENT_HEADER_DIR}/RgbChannelTransform.h diff --git a/libsrc/utils/ImageResampler.cpp b/libsrc/utils/ImageResampler.cpp new file mode 100644 index 00000000..d66ca742 --- /dev/null +++ b/libsrc/utils/ImageResampler.cpp @@ -0,0 +1,133 @@ +#include "utils/ImageResampler.h" + +ImageResampler::ImageResampler() : + _horizontalDecimation(1), + _verticalDecimation(1), + _cropLeft(0), + _cropRight(0), + _cropTop(0), + _cropBottom(0), + _videoMode(VIDEO_2D) +{ + +} + +ImageResampler::~ImageResampler() +{ + +} + +void ImageResampler::setHorizontalPixelDecimation(int decimator) +{ + _horizontalDecimation = decimator; +} + +void ImageResampler::setVerticalPixelDecimation(int decimator) +{ + _verticalDecimation = decimator; +} + +void ImageResampler::setCropping(int cropLeft, int cropRight, int cropTop, int cropBottom) +{ + _cropLeft = cropLeft; + _cropRight = cropRight; + _cropTop = cropTop; + _cropBottom = cropBottom; +} + +void ImageResampler::set3D(VideoMode mode) +{ + _videoMode = mode; +} + +void ImageResampler::processImage(const uint8_t * data, int width, int height, int lineLength, PixelFormat pixelFormat, Image &outputImage) const +{ + int cropLeft = _cropLeft; + int cropRight = _cropRight; + int cropTop = _cropTop; + int cropBottom = _cropBottom; + + // handle 3D mode + switch (_videoMode) + { + case VIDEO_3DSBS: + cropRight = width/2; + break; + case VIDEO_3DTAB: + cropBottom = height/2; + break; + default: + break; + } + + // calculate the output size + int outputWidth = (width - cropLeft - cropRight - _horizontalDecimation/2 + _horizontalDecimation - 1) / _horizontalDecimation; + int outputHeight = (height - cropTop - cropBottom - _verticalDecimation/2 + _verticalDecimation - 1) / _verticalDecimation; + outputImage.resize(outputWidth, outputHeight); + + for (int yDest = 0, ySource = cropTop + _verticalDecimation/2; yDest < outputHeight; ySource += _verticalDecimation, ++yDest) + { + for (int xDest = 0, xSource = cropLeft + _horizontalDecimation/2; xDest < outputWidth; xSource += _horizontalDecimation, ++xDest) + { + ColorRgb & rgb = outputImage(xDest, yDest); + + switch (pixelFormat) + { + case PIXELFORMAT_UYVY: + { + int index = lineLength * ySource + xSource * 2; + uint8_t y = data[index+1]; + uint8_t u = ((xSource&1) == 0) ? data[index ] : data[index-2]; + uint8_t v = ((xSource&1) == 0) ? data[index+2] : data[index ]; + yuv2rgb(y, u, v, rgb.red, rgb.green, rgb.blue); + } + break; + case PIXELFORMAT_YUYV: + { + int index = lineLength * ySource + xSource * 2; + uint8_t y = data[index]; + uint8_t u = ((xSource&1) == 0) ? data[index+1] : data[index-1]; + uint8_t v = ((xSource&1) == 0) ? data[index+3] : data[index+1]; + yuv2rgb(y, u, v, rgb.red, rgb.green, rgb.blue); + } + break; + case PIXELFORMAT_RGB32: + { + int index = lineLength * ySource + xSource * 4; + rgb.red = data[index ]; + rgb.green = data[index+1]; + rgb.blue = data[index+2]; + } + break; + case PIXELFORMAT_BGR32: + { + int index = lineLength * ySource + xSource * 4; + rgb.blue = data[index ]; + rgb.green = data[index+1]; + rgb.red = data[index+2]; + } + break; + case PIXELFORMAT_NO_CHANGE: + std::cerr << "Invalid pixel format given" << std::endl; + break; + } + } + } +} + +uint8_t ImageResampler::clamp(int x) +{ + return (x<0) ? 0 : ((x>255) ? 255 : uint8_t(x)); +} + +void ImageResampler::yuv2rgb(uint8_t y, uint8_t u, uint8_t v, uint8_t &r, uint8_t &g, uint8_t &b) +{ + // see: http://en.wikipedia.org/wiki/YUV#Y.27UV444_to_RGB888_conversion + int c = y - 16; + int d = u - 128; + int e = v - 128; + + r = clamp((298 * c + 409 * e + 128) >> 8); + g = clamp((298 * c - 100 * d - 208 * e + 128) >> 8); + b = clamp((298 * c + 516 * d + 128) >> 8); +} diff --git a/src/hyperion-v4l2/PixelFormatParameter.h b/src/hyperion-v4l2/PixelFormatParameter.h index f9dff93e..3ebccfc7 100644 --- a/src/hyperion-v4l2/PixelFormatParameter.h +++ b/src/hyperion-v4l2/PixelFormatParameter.h @@ -2,7 +2,7 @@ #include // grabber includes -#include +#include using namespace vlofgren; @@ -10,34 +10,34 @@ using namespace vlofgren; typedef vlofgren::PODParameter PixelFormatParameter; namespace vlofgren { - /// Translates a string (as passed on the commandline) to a pixel format - /// - /// @param[in] s The string (as passed on the commandline) - /// @return The pixel format - /// @throws Parameter::ParameterRejected If the string did not result in a pixel format - template<> - PixelFormat PixelFormatParameter::validate(const std::string& s) throw (Parameter::ParameterRejected) - { - QString input = QString::fromStdString(s).toLower(); + /// Translates a string (as passed on the commandline) to a pixel format + /// + /// @param[in] s The string (as passed on the commandline) + /// @return The pixel format + /// @throws Parameter::ParameterRejected If the string did not result in a pixel format + template<> + PixelFormat PixelFormatParameter::validate(const std::string& s) throw (Parameter::ParameterRejected) + { + QString input = QString::fromStdString(s).toLower(); - if (input == "yuyv") - { - return PIXELFORMAT_YUYV; - } - else if (input == "uyvy") - { - return PIXELFORMAT_UYVY; - } - else if (input == "rgb32") - { - return PIXELFORMAT_RGB32; - } - else if (input == "no-change") - { - return PIXELFORMAT_NO_CHANGE; - } + if (input == "yuyv") + { + return PIXELFORMAT_YUYV; + } + else if (input == "uyvy") + { + return PIXELFORMAT_UYVY; + } + else if (input == "rgb32") + { + return PIXELFORMAT_RGB32; + } + else if (input == "no-change") + { + return PIXELFORMAT_NO_CHANGE; + } - throw Parameter::ParameterRejected("Invalid value for pixel format. Valid values are: YUYV, UYVY, RGB32, and NO-CHANGE"); - return PIXELFORMAT_NO_CHANGE; - } + throw Parameter::ParameterRejected("Invalid value for pixel format. Valid values are: YUYV, UYVY, RGB32, and NO-CHANGE"); + return PIXELFORMAT_NO_CHANGE; + } } diff --git a/src/hyperion-v4l2/hyperion-v4l2.cpp b/src/hyperion-v4l2/hyperion-v4l2.cpp index aacd1c19..fe011f6d 100644 --- a/src/hyperion-v4l2/hyperion-v4l2.cpp +++ b/src/hyperion-v4l2/hyperion-v4l2.cpp @@ -102,6 +102,7 @@ int main(int argc, char** argv) return 0; } + // cropping values if not defined if (!argCropLeft.isSet()) argCropLeft.setDefault(argCropWidth.getValue()); if (!argCropRight.isSet()) argCropRight.setDefault(argCropWidth.getValue()); if (!argCropTop.isSet()) argCropTop.setDefault(argCropHeight.getValue()); diff --git a/src/hyperion-x11/X11Wrapper.cpp b/src/hyperion-x11/X11Wrapper.cpp index abaa4d3f..6f33cc61 100644 --- a/src/hyperion-x11/X11Wrapper.cpp +++ b/src/hyperion-x11/X11Wrapper.cpp @@ -2,9 +2,9 @@ // Hyperion-X11 includes #include "X11Wrapper.h" -X11Wrapper::X11Wrapper(int grabInterval, const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation) : +X11Wrapper::X11Wrapper(int grabInterval, int cropLeft, int cropRight, int cropTop, int cropBottom, int horizontalPixelDecimation, int verticalPixelDecimation) : _timer(this), - _grabber(cropHorizontal, cropVertical, pixelDecimation) + _grabber(cropLeft, cropRight, cropTop, cropBottom, horizontalPixelDecimation, verticalPixelDecimation) { _timer.setSingleShot(false); _timer.setInterval(grabInterval); diff --git a/src/hyperion-x11/X11Wrapper.h b/src/hyperion-x11/X11Wrapper.h index 6f67d7b9..211dfe18 100644 --- a/src/hyperion-x11/X11Wrapper.h +++ b/src/hyperion-x11/X11Wrapper.h @@ -9,7 +9,7 @@ class X11Wrapper : public QObject { Q_OBJECT public: - X11Wrapper(int grabInterval, const unsigned cropHorizontal, const unsigned cropVertical, const unsigned pixelDecimation); + X11Wrapper(int grabInterval, int cropLeft, int cropRight, int cropTop, int cropBottom, int horizontalPixelDecimation, int verticalPixelDecimation); const Image & getScreenshot(); diff --git a/src/hyperion-x11/hyperion-x11.cpp b/src/hyperion-x11/hyperion-x11.cpp index c473b7ed..609a3d9a 100644 --- a/src/hyperion-x11/hyperion-x11.cpp +++ b/src/hyperion-x11/hyperion-x11.cpp @@ -30,9 +30,13 @@ int main(int argc, char ** argv) ParameterSet & parameters = optionParser.getParameters(); IntParameter & argFps = parameters.add ('f', "framerate", "Cpture frame rate [default=10]"); - IntParameter & argCropWidth = parameters.add (0x0, "crop-width", "Number of pixels to crop from the left and right sides in the picture before decimation [default=0]"); - IntParameter & argCropHeight = parameters.add (0x0, "crop-height", "Number of pixels to crop from the top and the bottom in the picture before decimation [default=0]"); - IntParameter & argSizeDecimation = parameters.add ('s', "size-decimator", "Decimation factor for the output size [default=16]"); + IntParameter & argCropWidth = parameters.add (0x0, "crop-width", "Number of pixels to crop from the left and right sides of the picture before decimation [default=0]"); + IntParameter & argCropHeight = parameters.add (0x0, "crop-height", "Number of pixels to crop from the top and the bottom of the picture before decimation [default=0]"); + IntParameter & argCropLeft = parameters.add (0x0, "crop-left", "Number of pixels to crop from the left of the picture before decimation (overrides --crop-width)"); + IntParameter & argCropRight = parameters.add (0x0, "crop-right", "Number of pixels to crop from the right of the picture before decimation (overrides --crop-width)"); + IntParameter & argCropTop = parameters.add (0x0, "crop-top", "Number of pixels to crop from the top of the picture before decimation (overrides --crop-height)"); + IntParameter & argCropBottom = parameters.add (0x0, "crop-bottom", "Number of pixels to crop from the bottom of the picture before decimation (overrides --crop-height)"); + IntParameter & argSizeDecimation = parameters.add ('s', "size-decimator", "Decimation factor for the output size [default=8]"); SwitchParameter<> & argScreenshot = parameters.add> (0x0, "screenshot", "Take a single screenshot, save it to file and quit"); StringParameter & argAddress = parameters.add ('a', "address", "Set the address of the hyperion server [default: 127.0.0.1:19445]"); IntParameter & argPriority = parameters.add ('p', "priority", "Use the provided priority channel (the lower the number, the higher the priority) [default: 800]"); @@ -43,7 +47,7 @@ int main(int argc, char ** argv) argFps.setDefault(10); argCropWidth.setDefault(0); argCropHeight.setDefault(0); - argSizeDecimation.setDefault(16); + argSizeDecimation.setDefault(8); argAddress.setDefault("127.0.0.1:19445"); argPriority.setDefault(800); @@ -57,9 +61,22 @@ int main(int argc, char ** argv) return 0; } + // cropping values if not defined + if (!argCropLeft.isSet()) argCropLeft.setDefault(argCropWidth.getValue()); + if (!argCropRight.isSet()) argCropRight.setDefault(argCropWidth.getValue()); + if (!argCropTop.isSet()) argCropTop.setDefault(argCropHeight.getValue()); + if (!argCropBottom.isSet()) argCropBottom.setDefault(argCropHeight.getValue()); + // Create the X11 grabbing stuff int grabInterval = 1000 / argFps.getValue(); - X11Wrapper x11Wrapper(grabInterval, argCropWidth.getValue(), argCropHeight.getValue(), argSizeDecimation.getValue()); + X11Wrapper x11Wrapper( + grabInterval, + argCropLeft.getValue(), + argCropRight.getValue(), + argCropTop.getValue(), + argCropBottom.getValue(), + argSizeDecimation.getValue(), // horizontal decimation + argSizeDecimation.getValue()); // vertical decimation if (argScreenshot.isSet()) { From 2b6d485ea7192f26dc56524755846e76ac3c845d Mon Sep 17 00:00:00 2001 From: poljvd Date: Tue, 16 Dec 2014 21:30:08 +0100 Subject: [PATCH 51/59] Use ImageResampler in V4L2 grabber Former-commit-id: c6ec92c17b07444a49a303cdced6f59347f98f76 --- include/grabber/V4L2Grabber.h | 12 +-- libsrc/grabber/v4l2/V4L2Grabber.cpp | 109 ++++------------------------ 2 files changed, 17 insertions(+), 104 deletions(-) diff --git a/include/grabber/V4L2Grabber.h b/include/grabber/V4L2Grabber.h index d130a5e5..c4fc5a27 100644 --- a/include/grabber/V4L2Grabber.h +++ b/include/grabber/V4L2Grabber.h @@ -13,6 +13,7 @@ #include #include #include +#include // grabber includes #include @@ -108,22 +109,17 @@ private: PixelFormat _pixelFormat; int _width; int _height; + int _lineLength; int _frameByteSize; - int _cropLeft; - int _cropRight; - int _cropTop; - int _cropBottom; int _frameDecimation; - int _horizontalPixelDecimation; - int _verticalPixelDecimation; int _noSignalCounterThreshold; ColorRgb _noSignalThresholdColor; - VideoMode _mode3D; - int _currentFrame; int _noSignalCounter; QSocketNotifier * _streamNotifier; + + ImageResampler _imageResampler; }; diff --git a/libsrc/grabber/v4l2/V4L2Grabber.cpp b/libsrc/grabber/v4l2/V4L2Grabber.cpp index 27eefd43..a3202f88 100644 --- a/libsrc/grabber/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/v4l2/V4L2Grabber.cpp @@ -18,24 +18,6 @@ #define CLEAR(x) memset(&(x), 0, sizeof(x)) -static inline uint8_t clamp(int x) -{ - return (x<0) ? 0 : ((x>255) ? 255 : uint8_t(x)); -} - -static void yuv2rgb(uint8_t y, uint8_t u, uint8_t v, uint8_t & r, uint8_t & g, uint8_t & b) -{ - // see: http://en.wikipedia.org/wiki/YUV#Y.27UV444_to_RGB888_conversion - int c = y - 16; - int d = u - 128; - int e = v - 128; - - r = clamp((298 * c + 409 * e + 128) >> 8); - g = clamp((298 * c - 100 * d - 208 * e + 128) >> 8); - b = clamp((298 * c + 516 * d + 128) >> 8); -} - - V4L2Grabber::V4L2Grabber(const std::string & device, int input, VideoStandard videoStandard, @@ -52,21 +34,19 @@ V4L2Grabber::V4L2Grabber(const std::string & device, _pixelFormat(pixelFormat), _width(width), _height(height), + _lineLength(-1), _frameByteSize(-1), - _cropLeft(0), - _cropRight(0), - _cropTop(0), - _cropBottom(0), _frameDecimation(std::max(1, frameDecimation)), - _horizontalPixelDecimation(std::max(1, horizontalPixelDecimation)), - _verticalPixelDecimation(std::max(1, verticalPixelDecimation)), _noSignalCounterThreshold(50), _noSignalThresholdColor(ColorRgb{0,0,0}), - _mode3D(VIDEO_2D), _currentFrame(0), _noSignalCounter(0), - _streamNotifier(nullptr) + _streamNotifier(nullptr), + _imageResampler() { + _imageResampler.setHorizontalPixelDecimation(std::max(1, horizontalPixelDecimation)); + _imageResampler.setVerticalPixelDecimation(std::max(1, verticalPixelDecimation)); + open_device(); init_device(videoStandard, input); } @@ -81,15 +61,12 @@ V4L2Grabber::~V4L2Grabber() void V4L2Grabber::setCropping(int cropLeft, int cropRight, int cropTop, int cropBottom) { - _cropLeft = cropLeft; - _cropRight = cropRight; - _cropTop = cropTop; - _cropBottom = cropBottom; + _imageResampler.setCropping(cropLeft, cropRight, cropTop, cropBottom); } void V4L2Grabber::set3D(VideoMode mode) { - _mode3D = mode; + _imageResampler.set3D(mode); } void V4L2Grabber::setSignalThreshold(double redSignalThreshold, double greenSignalThreshold, double blueSignalThreshold, int noSignalCounterThreshold) @@ -414,6 +391,9 @@ void V4L2Grabber::init_device(VideoStandard videoStandard, int input) } } + // set the line length + _lineLength = fmt.fmt.pix.bytesperline; + // set the settings if (-1 == xioctl(VIDIOC_S_FMT, &fmt)) { @@ -688,71 +668,8 @@ bool V4L2Grabber::process_image(const void *p, int size) void V4L2Grabber::process_image(const uint8_t * data) { - int width = _width; - int height = _height; - - switch (_mode3D) - { - case VIDEO_3DSBS: - width = _width/2; - break; - case VIDEO_3DTAB: - height = _height/2; - break; - default: - break; - } - - // create output structure - int outputWidth = (width - _cropLeft - _cropRight + _horizontalPixelDecimation/2) / _horizontalPixelDecimation; - int outputHeight = (height - _cropTop - _cropBottom + _verticalPixelDecimation/2) / _verticalPixelDecimation; - - // TODO: should this be the following (like X11): - //int outputWidth = (width - _cropLeft - _cropRight + _horizontalPixelDecimation/2 - 1) / _horizontalPixelDecimation + 1; - //int outputHeight = (height - _cropTop - _cropBottom + _verticalPixelDecimation/2 - 1) / _verticalPixelDecimation + 1; - - Image image(outputWidth, outputHeight); - - for (int ySource = _cropTop + _verticalPixelDecimation/2, yDest = 0; ySource < height - _cropBottom; ySource += _verticalPixelDecimation, ++yDest) - { - for (int xSource = _cropLeft + _horizontalPixelDecimation/2, xDest = 0; xSource < width - _cropRight; xSource += _horizontalPixelDecimation, ++xDest) - { - ColorRgb & rgb = image(xDest, yDest); - - switch (_pixelFormat) - { - case PIXELFORMAT_UYVY: - { - int index = (_width * ySource + xSource) * 2; - uint8_t y = data[index+1]; - uint8_t u = (xSource%2 == 0) ? data[index ] : data[index-2]; - uint8_t v = (xSource%2 == 0) ? data[index+2] : data[index ]; - yuv2rgb(y, u, v, rgb.red, rgb.green, rgb.blue); - } - break; - case PIXELFORMAT_YUYV: - { - int index = (_width * ySource + xSource) * 2; - uint8_t y = data[index]; - uint8_t u = (xSource%2 == 0) ? data[index+1] : data[index-1]; - uint8_t v = (xSource%2 == 0) ? data[index+3] : data[index+1]; - yuv2rgb(y, u, v, rgb.red, rgb.green, rgb.blue); - } - break; - case PIXELFORMAT_RGB32: - { - int index = (_width * ySource + xSource) * 4; - rgb.red = data[index ]; - rgb.green = data[index+1]; - rgb.blue = data[index+2]; - } - break; - default: - // this should not be possible - break; - } - } - } + Image image(0, 0); + _imageResampler.processImage(data, _width, _height, _lineLength, _pixelFormat, image); // check signal (only in center of the resulting image, because some grabbers have noise values along the borders) bool noSignal = true; From 12da40dc58de95a805995c44160016446615421b Mon Sep 17 00:00:00 2001 From: poljvd Date: Wed, 17 Dec 2014 16:18:23 +0100 Subject: [PATCH 52/59] Fix typo Former-commit-id: 134df5769b2bd608d08357c132d2a5cb1763dc4f --- src/hyperion-x11/hyperion-x11.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hyperion-x11/hyperion-x11.cpp b/src/hyperion-x11/hyperion-x11.cpp index 609a3d9a..449f3990 100644 --- a/src/hyperion-x11/hyperion-x11.cpp +++ b/src/hyperion-x11/hyperion-x11.cpp @@ -29,7 +29,7 @@ int main(int argc, char ** argv) OptionsParser optionParser("X11 capture application for Hyperion"); ParameterSet & parameters = optionParser.getParameters(); - IntParameter & argFps = parameters.add ('f', "framerate", "Cpture frame rate [default=10]"); + IntParameter & argFps = parameters.add ('f', "framerate", "Capture frame rate [default=10]"); IntParameter & argCropWidth = parameters.add (0x0, "crop-width", "Number of pixels to crop from the left and right sides of the picture before decimation [default=0]"); IntParameter & argCropHeight = parameters.add (0x0, "crop-height", "Number of pixels to crop from the top and the bottom of the picture before decimation [default=0]"); IntParameter & argCropLeft = parameters.add (0x0, "crop-left", "Number of pixels to crop from the left of the picture before decimation (overrides --crop-width)"); From 6248489aede46d4ed6f2f6d4884fffd9a5dc9e4b Mon Sep 17 00:00:00 2001 From: poljvd Date: Wed, 17 Dec 2014 16:43:14 +0100 Subject: [PATCH 53/59] Improve readability of usage message Former-commit-id: 7919b8e95e57518ee5dd60c6de8b6524a1749998 --- dependencies/build/getoptPlusPlus/getoptpp.cc | 268 ++++++++++-------- 1 file changed, 144 insertions(+), 124 deletions(-) diff --git a/dependencies/build/getoptPlusPlus/getoptpp.cc b/dependencies/build/getoptPlusPlus/getoptpp.cc index 5444a325..37212901 100644 --- a/dependencies/build/getoptPlusPlus/getoptpp.cc +++ b/dependencies/build/getoptPlusPlus/getoptpp.cc @@ -17,6 +17,9 @@ #include "getoptpp.h" #include #include +#include +#include +#include using namespace std; @@ -34,107 +37,124 @@ OptionsParser::OptionsParser(const char* programDesc) : fprogramDesc(programDesc OptionsParser::~OptionsParser() {} ParameterSet& OptionsParser::getParameters() { - return parameters; + return parameters; } void OptionsParser::parse(int argc, const char* argv[]) throw(runtime_error) { - argv0 = argv[0]; + argv0 = argv[0]; - if(argc == 1) return; + if(argc == 1) return; - vector v(&argv[1], &argv[argc]); + vector v(&argv[1], &argv[argc]); - ParserState state(/* *this,*/ v); + ParserState state(/* *this,*/ v); - for(; !state.end(); state.advance()) { + for(; !state.end(); state.advance()) { - std::list::iterator i; + std::list::iterator i; - for(i = parameters.parameters.begin(); - i != parameters.parameters.end(); i++) - { - int n = 0; - try - { - n = (*i)->receive(state); - } - catch(Parameter::ExpectedArgument &) - { - throw Parameter::ExpectedArgument(state.get() + ": expected an argument"); - } - catch(Parameter::UnexpectedArgument &) - { - throw Parameter::UnexpectedArgument(state.get() + ": did not expect an argument"); - } - catch(Switchable::SwitchingError &) - { - throw Parameter::ParameterRejected(state.get() + ": parameter already set"); - } - catch(Parameter::ParameterRejected & pr) { - std::string what = pr.what(); - if(what.length()) - { - throw Parameter::ParameterRejected(state.get() + ": " + what); - } - throw Parameter::ParameterRejected(state.get() + " (unspecified error)"); - } + for(i = parameters.parameters.begin(); + i != parameters.parameters.end(); i++) + { + int n = 0; + try + { + n = (*i)->receive(state); + } + catch(Parameter::ExpectedArgument &) + { + throw Parameter::ExpectedArgument(state.get() + ": expected an argument"); + } + catch(Parameter::UnexpectedArgument &) + { + throw Parameter::UnexpectedArgument(state.get() + ": did not expect an argument"); + } + catch(Switchable::SwitchingError &) + { + throw Parameter::ParameterRejected(state.get() + ": parameter already set"); + } + catch(Parameter::ParameterRejected & pr) { + std::string what = pr.what(); + if(what.length()) + { + throw Parameter::ParameterRejected(state.get() + ": " + what); + } + throw Parameter::ParameterRejected(state.get() + " (unspecified error)"); + } - for (int j = 1; j < n; ++j) - { - state.advance(); - } + for (int j = 1; j < n; ++j) + { + state.advance(); + } - if(n != 0) - { - break; - } - } + if(n != 0) + { + break; + } + } - if(i == parameters.parameters.end()) { - std::string file = state.get(); - if(file == "--") { - state.advance(); - break; - } - else if(file.at(0) == '-') - throw Parameter::ParameterRejected(string("Bad parameter: ") + file); - else files.push_back(state.get()); - } - } + if(i == parameters.parameters.end()) { + std::string file = state.get(); + if(file == "--") { + state.advance(); + break; + } + else if(file.at(0) == '-') + throw Parameter::ParameterRejected(string("Bad parameter: ") + file); + else files.push_back(state.get()); + } + } - if(!state.end()) for(; !state.end(); state.advance()) { - files.push_back(state.get()); - } + if(!state.end()) for(; !state.end(); state.advance()) { + files.push_back(state.get()); + } } void OptionsParser::usage() const { - cerr << fprogramDesc << endl; - cerr << "Build time: " << __DATE__ << " " << __TIME__ << endl << endl; - cerr << "Usage: " << programName() << " [OPTIONS]" << endl << endl; + cerr << fprogramDesc << endl; + cerr << "Build time: " << __DATE__ << " " << __TIME__ << endl << endl; + cerr << "Usage: " << programName() << " [OPTIONS]" << endl << endl; - cerr << "Parameters: " << endl; + cerr << "Parameters: " << endl; - std::list::const_iterator i; - for(i = parameters.parameters.begin(); - i != parameters.parameters.end(); i++) - { - cerr.width(33); - cerr << std::left << " " + (*i)->usageLine(); + int totalWidth = 80; + int usageWidth = 33; - cerr.width(40); - cerr << std::left << (*i)->description() << endl; + // read total width from the terminal + struct winsize w; + if (ioctl(0, TIOCGWINSZ, &w) == 0) + { + if (w.ws_col > totalWidth) + totalWidth = w.ws_col; + } - } + std::list::const_iterator i; + for(i = parameters.parameters.begin(); + i != parameters.parameters.end(); i++) + { + cerr.width(usageWidth); + cerr << std::left << " " + (*i)->usageLine(); + + std::string description = (*i)->description(); + while (int(description.length()) > (totalWidth - usageWidth)) + { + size_t pos = description.find_last_of(' ', totalWidth - usageWidth); + cerr << description.substr(0, pos) << std::endl << std::string(usageWidth - 1, ' '); + description = description.substr(pos); + } + cerr << description << endl; + + } } const vector& OptionsParser::getFiles() const { - return files; + return files; } const string& OptionsParser::programName() const { - return argv0; + return argv0; } /* @@ -144,15 +164,15 @@ const string& OptionsParser::programName() const { */ ParameterSet::ParameterSet(const ParameterSet& ps) { - throw new runtime_error("ParameterSet not copyable"); + throw new runtime_error("ParameterSet not copyable"); } ParameterSet::~ParameterSet() { - for(std::list::iterator i = parameters.begin(); - i != parameters.end(); i++) - { - delete *i; - } + for(std::list::iterator i = parameters.begin(); + i != parameters.end(); i++) + { + delete *i; + } } @@ -161,18 +181,18 @@ ParameterSet::~ParameterSet() { */ Parameter& ParameterSet::operator[](char c) const { - for(std::list::const_iterator i = parameters.begin(); i!= parameters.end(); i++) { - if((*i)->shortOption() == c) return *(*i); - } - throw out_of_range("ParameterSet["+string(&c)+string("]")); + for(std::list::const_iterator i = parameters.begin(); i!= parameters.end(); i++) { + if((*i)->shortOption() == c) return *(*i); + } + throw out_of_range("ParameterSet["+string(&c)+string("]")); } Parameter& ParameterSet::operator[](const string& param) const { - for(std::list::const_iterator i = parameters.begin(); i!= parameters.end(); i++) { - if((*i)->longOption() == param) return *(*i); - } - throw out_of_range("ParameterSet["+param+"]"); + for(std::list::const_iterator i = parameters.begin(); i!= parameters.end(); i++) { + if((*i)->longOption() == param) return *(*i); + } + throw out_of_range("ParameterSet["+param+"]"); } @@ -186,29 +206,29 @@ Parameter& ParameterSet::operator[](const string& param) const { ParserState::ParserState(/*OptionsParser &opts, */vector& args) : - /*opts(opts),*/ arguments(args), iterator(args.begin()) + /*opts(opts),*/ arguments(args), iterator(args.begin()) { } const string ParserState::peek() const { - vector::const_iterator next = iterator+1; - if(next != arguments.end()) return *next; - else return ""; + vector::const_iterator next = iterator+1; + if(next != arguments.end()) return *next; + else return ""; } const string ParserState::get() const { - if(!end()) return *iterator; - else return ""; + if(!end()) return *iterator; + else return ""; } void ParserState::advance() { - iterator++; + iterator++; } bool ParserState::end() const { - return iterator == arguments.end(); + return iterator == arguments.end(); } @@ -222,7 +242,7 @@ bool ParserState::end() const { Parameter::Parameter(char shortOption, const std::string & longOption, const std::string & description) : - fshortOption(shortOption), flongOption(longOption), fdescription(description) + fshortOption(shortOption), flongOption(longOption), fdescription(description) { } @@ -250,22 +270,22 @@ MultiSwitchable::~MultiSwitchable() {} void UniquelySwitchable::set() throw (Switchable::SwitchingError) { - if(UniquelySwitchable::isSet()) throw Switchable::SwitchingError(); - fset = true; + if(UniquelySwitchable::isSet()) throw Switchable::SwitchingError(); + fset = true; } UniquelySwitchable::~UniquelySwitchable() {} PresettableUniquelySwitchable::~PresettableUniquelySwitchable() {} bool PresettableUniquelySwitchable::isSet() const { - return UniquelySwitchable::isSet() || fpreset.isSet(); + return UniquelySwitchable::isSet() || fpreset.isSet(); } void PresettableUniquelySwitchable::set() throw (Switchable::SwitchingError) { - UniquelySwitchable::set(); + UniquelySwitchable::set(); } void PresettableUniquelySwitchable::preset() { - fpreset.set(); + fpreset.set(); } /* @@ -279,58 +299,58 @@ void PresettableUniquelySwitchable::preset() { template<> PODParameter::PODParameter(char shortOption, const char *longOption, - const char* description) : CommonParameter(shortOption, longOption, description) { + const char* description) : CommonParameter(shortOption, longOption, description) { } template<> int PODParameter::validate(const string &s) throw(Parameter::ParameterRejected) { - // This is sadly necessary for strto*-functions to operate on - // const char*. The function doesn't write to the memory, though, - // so it's quite safe. + // This is sadly necessary for strto*-functions to operate on + // const char*. The function doesn't write to the memory, though, + // so it's quite safe. - char* cstr = const_cast(s.c_str()); - if(*cstr == '\0') throw ParameterRejected("No argument given"); + char* cstr = const_cast(s.c_str()); + if(*cstr == '\0') throw ParameterRejected("No argument given"); - long l = strtol(cstr, &cstr, 10); - if(*cstr != '\0') throw ParameterRejected("Expected int"); + long l = strtol(cstr, &cstr, 10); + if(*cstr != '\0') throw ParameterRejected("Expected int"); - if(l > INT_MAX || l < INT_MIN) { - throw ParameterRejected("Expected int"); - } + if(l > INT_MAX || l < INT_MIN) { + throw ParameterRejected("Expected int"); + } - return l; + return l; } template<> long PODParameter::validate(const string &s) throw(Parameter::ParameterRejected) { - char* cstr = const_cast(s.c_str()); - if(*cstr == '\0') throw ParameterRejected("No argument given"); + char* cstr = const_cast(s.c_str()); + if(*cstr == '\0') throw ParameterRejected("No argument given"); - long l = strtol(cstr, &cstr, 10); - if(*cstr != '\0') throw ParameterRejected("Expected long"); + long l = strtol(cstr, &cstr, 10); + if(*cstr != '\0') throw ParameterRejected("Expected long"); - return l; + return l; } template<> double PODParameter::validate(const string &s) throw(Parameter::ParameterRejected) { - char* cstr = const_cast(s.c_str()); - if(*cstr == '\0') throw ParameterRejected("No argument given"); + char* cstr = const_cast(s.c_str()); + if(*cstr == '\0') throw ParameterRejected("No argument given"); - double d = strtod(cstr, &cstr); - if(*cstr != '\0') throw ParameterRejected("Expected double"); + double d = strtod(cstr, &cstr); + if(*cstr != '\0') throw ParameterRejected("Expected double"); - return d; + return d; } template<> string PODParameter::validate(const string &s) throw(Parameter::ParameterRejected) { - return s; + return s; } From b50611fef78849fd9536012b77c6d3f0f91f9e20 Mon Sep 17 00:00:00 2001 From: poljvd Date: Fri, 19 Dec 2014 21:21:00 +0100 Subject: [PATCH 54/59] add protobuf as submodule Former-commit-id: 56497c61fa8403b489c1fc7ca45a9d2e964a4e10 --- .gitignore | 3 +-- .gitmodules | 3 +++ dependencies/external/protobuf | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 .gitmodules create mode 160000 dependencies/external/protobuf diff --git a/.gitignore b/.gitignore index 550dcb07..91a498c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /*.user -/build -/build-x86 +/build* .DS_Store diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..aa392190 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "dependencies/external/protobuf"] + path = dependencies/external/protobuf + url = https://github.com/tvdzwan/protobuf.git diff --git a/dependencies/external/protobuf b/dependencies/external/protobuf new file mode 160000 index 00000000..f644b9c7 --- /dev/null +++ b/dependencies/external/protobuf @@ -0,0 +1 @@ +Subproject commit f644b9c73281e8c0ee3a400b20d3b8b6f63bafdd From a041941e7aa3e56c89e1c1116d2d42368774af5b Mon Sep 17 00:00:00 2001 From: poljvd Date: Sat, 20 Dec 2014 13:15:20 +0100 Subject: [PATCH 55/59] Fix build with embedded protobuf library Former-commit-id: 5c60566f2d4a9c65fc0fe677841a34d7ef73549a --- CMakeLists.txt | 9 --- dependencies/CMakeLists.txt | 103 +++++++++++++++++++++++++++++- dependencies/build/CMakeLists.txt | 6 -- dependencies/external/protobuf | 2 +- libsrc/protoserver/CMakeLists.txt | 48 +++++++------- src/CMakeLists.txt | 19 +++--- src/hyperion-x11/CMakeLists.txt | 1 + 7 files changed, 140 insertions(+), 48 deletions(-) delete mode 100644 dependencies/build/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index d8a25196..7e11cf60 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,15 +67,6 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -Wall") # Configure the use of QT4 find_package(Qt4 COMPONENTS QtCore QtGui QtNetwork REQUIRED QUIET) -if (ENABLE_PROTOBUF) - # add protocol buffers (make sure to find the static version) - set(CMAKE_FIND_LIBRARY_SUFFIXES_OLD ${CMAKE_FIND_LIBRARY_SUFFIXES}) - set(CMAKE_FIND_LIBRARY_SUFFIXES ".a") - find_package(Protobuf REQUIRED) - set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES_OLD}) - set(CMAKE_FIND_LIBRARY_SUFFIXES_OLD) -endif (ENABLE_PROTOBUF) - #add libusb and pthreads find_package(libusb-1.0 REQUIRED) find_package(Threads REQUIRED) diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index 059e258e..17747bd5 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -1,2 +1,103 @@ +add_subdirectory(build/getoptPlusPlus) +add_subdirectory(build/hidapi) +add_subdirectory(build/jsoncpp) +add_subdirectory(build/serial) +add_subdirectory(build/tinkerforge) -add_subdirectory(build) +if(ENABLE_PROTOBUF) + set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared protobuf library") + add_subdirectory(external/protobuf) + + if(CMAKE_CROSSCOMPILING) + # when crosscompiling import the protoc executable targets from a file generated by a native build + option(IMPORT_PROTOC "Protoc export file (protoc_export.cmake) from a native build" "IMPORT_PROTOC-FILE_NOT_FOUND") + include(${IMPORT_PROTOC}) + else() + # export the protoc compiler so it can be used when cross compiling + export(TARGETS protoc_compiler FILE "${CMAKE_BINARY_DIR}/protoc_export.cmake") + endif() + + # define the include for the protobuf library at the parent scope + set(PROTOBUF_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/external/protobuf/src") + set(PROTOBUF_INCLUDE_DIRS ${PROTOBUF_INCLUDE_DIRS} PARENT_SCOPE) + + # define the protoc executable at the parent scope + get_property(PROTOBUF_PROTOC_EXECUTABLE TARGET protoc_compiler PROPERTY LOCATION) + set(PROTOBUF_PROTOC_EXECUTABLE ${PROTOBUF_PROTOC_EXECUTABLE} PARENT_SCOPE) + message(STATUS "Using protobuf compiler: " ${PROTOBUF_PROTOC_EXECUTABLE}) + + #============================================================================= + # Copyright 2009 Kitware, Inc. + # Copyright 2009-2011 Philip Lowman + # Copyright 2008 Esben Mose Hansen, Ange Optimization ApS + # + # Distributed under the OSI-approved BSD License (the "License"); + # see accompanying file Copyright.txt for details. + # + # This software is distributed WITHOUT ANY WARRANTY; without even the + # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + # See the License for more information. + #============================================================================= + # (To distribute this file outside of CMake, substitute the full + # License text for the above reference.) + function(PROTOBUF_GENERATE_CPP SRCS HDRS) + if(NOT ARGN) + message(SEND_ERROR "Error: PROTOBUF_GENERATE_CPP() called without any proto files") + return() + endif() + + if(PROTOBUF_GENERATE_CPP_APPEND_PATH) + # Create an include path for each file specified + foreach(FIL ${ARGN}) + get_filename_component(ABS_FIL ${FIL} ABSOLUTE) + get_filename_component(ABS_PATH ${ABS_FIL} PATH) + list(FIND _protobuf_include_path ${ABS_PATH} _contains_already) + if(${_contains_already} EQUAL -1) + list(APPEND _protobuf_include_path -I ${ABS_PATH}) + endif() + endforeach() + else() + set(_protobuf_include_path -I ${CMAKE_CURRENT_SOURCE_DIR}) + endif() + + if(DEFINED PROTOBUF_IMPORT_DIRS) + foreach(DIR ${PROTOBUF_IMPORT_DIRS}) + get_filename_component(ABS_PATH ${DIR} ABSOLUTE) + list(FIND _protobuf_include_path ${ABS_PATH} _contains_already) + if(${_contains_already} EQUAL -1) + list(APPEND _protobuf_include_path -I ${ABS_PATH}) + endif() + endforeach() + endif() + + if(CMAKE_CROSSCOMPILING) + set(PROTOC_DEPENDENCY ${PROTOBUF_PROTOC_EXECUTABLE}) + else() + set(PROTOC_DEPENDENCY protoc_compiler) + endif() + + set(${SRCS}) + set(${HDRS}) + foreach(FIL ${ARGN}) + get_filename_component(ABS_FIL ${FIL} ABSOLUTE) + get_filename_component(FIL_WE ${FIL} NAME_WE) + + list(APPEND ${SRCS} "${CMAKE_CURRENT_BINARY_DIR}/${FIL_WE}.pb.cc") + list(APPEND ${HDRS} "${CMAKE_CURRENT_BINARY_DIR}/${FIL_WE}.pb.h") + + add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${FIL_WE}.pb.cc" + "${CMAKE_CURRENT_BINARY_DIR}/${FIL_WE}.pb.h" + COMMAND ${PROTOBUF_PROTOC_EXECUTABLE} + ARGS --cpp_out ${CMAKE_CURRENT_BINARY_DIR} ${_protobuf_include_path} ${ABS_FIL} + DEPENDS ${ABS_FIL} ${PROTOC_DEPENDENCY} + COMMENT "Running C++ protocol buffer compiler on ${FIL}" + VERBATIM + ) + endforeach() + + set_source_files_properties(${${SRCS}} ${${HDRS}} PROPERTIES GENERATED TRUE) + set(${SRCS} ${${SRCS}} PARENT_SCOPE) + set(${HDRS} ${${HDRS}} PARENT_SCOPE) + endfunction() +endif() diff --git a/dependencies/build/CMakeLists.txt b/dependencies/build/CMakeLists.txt deleted file mode 100644 index e931ea79..00000000 --- a/dependencies/build/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ - -add_subdirectory(getoptPlusPlus) -add_subdirectory(hidapi) -add_subdirectory(jsoncpp) -add_subdirectory(serial) -add_subdirectory(tinkerforge) diff --git a/dependencies/external/protobuf b/dependencies/external/protobuf index f644b9c7..efb59b79 160000 --- a/dependencies/external/protobuf +++ b/dependencies/external/protobuf @@ -1 +1 @@ -Subproject commit f644b9c73281e8c0ee3a400b20d3b8b6f63bafdd +Subproject commit efb59b79e5a8f26eae4d15f38bbfb5667e23df60 diff --git a/libsrc/protoserver/CMakeLists.txt b/libsrc/protoserver/CMakeLists.txt index f13c6a83..6a9b6d1e 100644 --- a/libsrc/protoserver/CMakeLists.txt +++ b/libsrc/protoserver/CMakeLists.txt @@ -4,49 +4,51 @@ set(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/protoserver) set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/protoserver) include_directories( - ${CMAKE_CURRENT_BINARY_DIR} - ${PROTOBUF_INCLUDE_DIRS}) + ${CMAKE_CURRENT_BINARY_DIR} + ${PROTOBUF_INCLUDE_DIRS} +) # Group the headers that go through the MOC compiler set(ProtoServer_QT_HEADERS - ${CURRENT_HEADER_DIR}/ProtoServer.h - ${CURRENT_HEADER_DIR}/ProtoConnection.h - ${CURRENT_SOURCE_DIR}/ProtoClientConnection.h - ${CURRENT_HEADER_DIR}/ProtoConnectionWrapper.h + ${CURRENT_HEADER_DIR}/ProtoServer.h + ${CURRENT_HEADER_DIR}/ProtoConnection.h + ${CURRENT_SOURCE_DIR}/ProtoClientConnection.h + ${CURRENT_HEADER_DIR}/ProtoConnectionWrapper.h ) set(ProtoServer_HEADERS ) set(ProtoServer_SOURCES - ${CURRENT_SOURCE_DIR}/ProtoServer.cpp - ${CURRENT_SOURCE_DIR}/ProtoClientConnection.cpp - ${CURRENT_SOURCE_DIR}/ProtoConnection.cpp - ${CURRENT_SOURCE_DIR}/ProtoConnectionWrapper.cpp + ${CURRENT_SOURCE_DIR}/ProtoServer.cpp + ${CURRENT_SOURCE_DIR}/ProtoClientConnection.cpp + ${CURRENT_SOURCE_DIR}/ProtoConnection.cpp + ${CURRENT_SOURCE_DIR}/ProtoConnectionWrapper.cpp ) set(ProtoServer_PROTOS - ${CURRENT_SOURCE_DIR}/message.proto + ${CURRENT_SOURCE_DIR}/message.proto ) protobuf_generate_cpp(ProtoServer_PROTO_SRCS ProtoServer_PROTO_HDRS - ${ProtoServer_PROTOS} + ${ProtoServer_PROTOS} ) qt4_wrap_cpp(ProtoServer_HEADERS_MOC ${ProtoServer_QT_HEADERS}) add_library(protoserver - ${ProtoServer_HEADERS} - ${ProtoServer_QT_HEADERS} - ${ProtoServer_SOURCES} - ${ProtoServer_HEADERS_MOC} - ${ProtoServer_PROTOS} - ${ProtoServer_PROTO_SRCS} - ${ProtoServer_PROTO_HDRS} + ${ProtoServer_HEADERS} + ${ProtoServer_QT_HEADERS} + ${ProtoServer_SOURCES} + ${ProtoServer_HEADERS_MOC} + ${ProtoServer_PROTOS} + ${ProtoServer_PROTO_SRCS} + ${ProtoServer_PROTO_HDRS} ) target_link_libraries(protoserver - hyperion - hyperion-utils - ${PROTOBUF_LIBRARIES} - ${QT_LIBRARIES}) + hyperion + hyperion-utils + protobuf + ${QT_LIBRARIES} +) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 16c88d77..098a08d3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,12 +1,15 @@ add_subdirectory(hyperiond) add_subdirectory(hyperion-remote) -# Add the 'Video 4 Linux' grabber if it is enabled -if(ENABLE_V4L2) - add_subdirectory(hyperion-v4l2) -endif(ENABLE_V4L2) +# The following clients depend on the protobuf library +if(ENABLE_PROTOBUF) + # Add the 'Video 4 Linux' grabber if it is enabled + if(ENABLE_V4L2) + add_subdirectory(hyperion-v4l2) + endif() -# Add the X11 grabber if it is enabled -if(ENABLE_X11) - add_subdirectory(hyperion-x11) -endif(ENABLE_X11) + # Add the X11 grabber if it is enabled + if(ENABLE_X11) + add_subdirectory(hyperion-x11) + endif() +endif() diff --git a/src/hyperion-x11/CMakeLists.txt b/src/hyperion-x11/CMakeLists.txt index 7841232c..89912f66 100644 --- a/src/hyperion-x11/CMakeLists.txt +++ b/src/hyperion-x11/CMakeLists.txt @@ -14,6 +14,7 @@ include_directories( ${CMAKE_CURRENT_BINARY_DIR}/../../libsrc/protoserver ${QT_INCLUDES} ${X11_INCLUDES} + ${PROTOBUF_INCLUDE_DIRS} ) set(Hyperion_X11_QT_HEADERS From 05df260d2cbd5887ca56b8f94e3f7f195edfb985 Mon Sep 17 00:00:00 2001 From: poljvd Date: Sat, 20 Dec 2014 20:50:54 +0100 Subject: [PATCH 56/59] Change X11 default to OFF Former-commit-id: 33239ee9411c4413a717c21c66be01f0dfda2c7e --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e11cf60..c058526e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ message(STATUS "ENABLE_WS2812BPWM = " ${ENABLE_WS2812BPWM}) option(ENABLE_V4L2 "Enable the V4L2 grabber" ON) message(STATUS "ENABLE_V4L2 = " ${ENABLE_V4L2}) -option(ENABLE_X11 "Enable the X11 grabber" ON) +option(ENABLE_X11 "Enable the X11 grabber" OFF) message(STATUS "ENABLE_X11 = " ${ENABLE_X11}) option(ENABLE_TINKERFORGE "Enable the TINKERFORGE device" ON) From ae4462f098b56483bbc5c861a02bc05bd904a402 Mon Sep 17 00:00:00 2001 From: poljvd Date: Sat, 20 Dec 2014 20:51:17 +0100 Subject: [PATCH 57/59] Update compile instructions Former-commit-id: b5e0ad129c174de32fbb814595aaefe1fc0f6c13 --- CompileHowto.txt | 6 +++--- CrossCompileHowto.txt | 30 ++++++++++++++++++++---------- Toolchain-RaspberryPi.cmake | 4 ++-- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/CompileHowto.txt b/CompileHowto.txt index 17a274e3..7ad606aa 100644 --- a/CompileHowto.txt +++ b/CompileHowto.txt @@ -1,6 +1,6 @@ # Install the required tools and dependencies sudo apt-get update -sudo apt-get install git cmake build-essential libprotobuf-dev libQt4-dev libusb-1.0-0-dev protobuf-compiler python-dev +sudo apt-get install git cmake build-essential libQt4-dev libusb-1.0-0-dev python-dev # RPI ONLY: when you build on the rapberry pi and inlcude the dispmanx grabber (which is the default) # you also need the firmware including headers installed. This downloads the firmware from the raspberrypi github @@ -11,7 +11,7 @@ sudo cp -R "$FIRMWARE_DIR/hardfp/opt/*" /opt # create hyperion directory and checkout the code from github export HYPERION_DIR="hyperion" -git clone https://github.com/tvdzwan/hyperion.git "$HYPERION_DIR" +git clone --recursive https://github.com/tvdzwan/hyperion.git "$HYPERION_DIR" # create and enter the build directory mkdir "$HYPERION_DIR/build" @@ -20,7 +20,7 @@ cd "$HYPERION_DIR/build" # run cmake to generate make files on the rsapberry pi cmake .. # or if you are not compiling on the raspberry pi and need to disable the Dispmanx grabber and support for spi devices -cmake -DENABLE_DISPMANX=OFF -DENABLE_SPIDEV=OFF .. +cmake -DENABLE_DISPMANX=OFF -DENABLE_SPIDEV=OFF -DENABLE_X11=ON .. # run make to build Hyperion make diff --git a/CrossCompileHowto.txt b/CrossCompileHowto.txt index ac959f10..c5a2523e 100644 --- a/CrossCompileHowto.txt +++ b/CrossCompileHowto.txt @@ -1,6 +1,6 @@ ON RASPBERRY -------------- -sudo apt-get install libprotobuf-dev libQt4-dev libusb-1.0-0-dev python-dev rsync +sudo apt-get install libQt4-dev libusb-1.0-0-dev python-dev rsync ON HOST --------- @@ -8,21 +8,31 @@ export RASPI=192.168.1.17 export RASCROSS_DIR="$HOME/raspberrypi" export HYPERION_DIR="$HOME/hyperion" -sudo apt-get install git rsync cmake ia32-libs protobuf-compiler +# install required packages +sudo apt-get install git rsync cmake build-essential libQt4-dev libusb-1.0-0-dev python-dev +# create the rootfs by copying it from an rpi running for example raspbmc mkdir -p "$RASCROSS_DIR/rootfs" -git clone git://github.com/raspberrypi/tools.git "$RASCROSS_DIR/tools" - -git clone https://github.com/raspberrypi/firmware.git "$RASCROSS_DIR/firmware" -ln -s "$RASCROSS_DIR/firmware/opt" "$RASCROSS_DIR/rootfs/opt" - rsync -rl --delete-after --safe-links pi@$RASPI:/{lib,usr} "$RASCROSS_DIR/rootfs" -git clone https://github.com/tvdzwan/hyperion.git "$HYPERION_DIR" +# get the raspberry pi firmware and add it to the rootfs +git clone https://github.com/raspberrypi/firmware.git "$RASCROSS_DIR/firmware" +ln -s "$RASCROSS_DIR/firmware/hardfp/opt" "$RASCROSS_DIR/rootfs/opt" + +# get the compile tools +git clone git://github.com/raspberrypi/tools.git "$RASCROSS_DIR/tools" + +# get the Hyperion sources +git clone --recursive https://github.com/tvdzwan/hyperion.git "$HYPERION_DIR" + +# do a native build (to build the protobuf compiler for the native platform) mkdir "$HYPERION_DIR/build" -cmake -DCMAKE_TOOLCHAIN_FILE="$HYPERION_DIR/Toolchain-RaspberryPi.cmake" --build "$HYPERION_DIR/build" "$HYPERION_DIR" - +cmake -DENABLE_DISPMANX=OFF --build "$HYPERION_DIR/build" "$HYPERION_DIR" +# do the rpi build +# specify the protoc export file to import the protobuf compiler from the native build +mkdir "$HYPERION_DIR/build-rpi" +cmake -DCMAKE_TOOLCHAIN_FILE="$HYPERION_DIR/Toolchain-RaspberryPi.cmake" -DIMPORT_PROTOC=$HYPERION_DIR/build/protoc_export.cmake --build "$HYPERION_DIR/build-rpi" "$HYPERION_DIR" ------------------------------------------------------------------------------ These instructions are based on the guide given by: diff --git a/Toolchain-RaspberryPi.cmake b/Toolchain-RaspberryPi.cmake index 77ea25b0..1be297d4 100644 --- a/Toolchain-RaspberryPi.cmake +++ b/Toolchain-RaspberryPi.cmake @@ -4,8 +4,8 @@ SET(CMAKE_SYSTEM_NAME Linux) SET(CMAKE_SYSTEM_VERSION 1) # specify the cross compiler -SET(CMAKE_C_COMPILER ${RASPCROSS_DIR}/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian/bin/arm-linux-gnueabihf-gcc) -SET(CMAKE_CXX_COMPILER ${RASPCROSS_DIR}/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian/bin/arm-linux-gnueabihf-g++) +SET(CMAKE_C_COMPILER ${RASPCROSS_DIR}/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin/arm-linux-gnueabihf-gcc) +SET(CMAKE_CXX_COMPILER ${RASPCROSS_DIR}/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin/arm-linux-gnueabihf-g++) # where is the target environment SET(CMAKE_FIND_ROOT_PATH ${RASPCROSS_DIR}/rootfs) From 1891a0a9c0d6df4e264069232c21b763642f2c52 Mon Sep 17 00:00:00 2001 From: poljvd Date: Sat, 20 Dec 2014 21:33:47 +0100 Subject: [PATCH 58/59] Update RPi deploy binaries Former-commit-id: a8721c604613e4b079b64c753a4954373b468aad --- deploy/hyperion.tar.gz.REMOVED.git-id | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/hyperion.tar.gz.REMOVED.git-id b/deploy/hyperion.tar.gz.REMOVED.git-id index 874b3ca0..5e0f5d63 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -c2332ae026dcd9b5ededbbe2db493ae13e5208c5 \ No newline at end of file +05b947e9badc75841cd5d881a050062add6affc4 \ No newline at end of file From c5151e497dda6c8cd6f9971ed94c13343a48c110 Mon Sep 17 00:00:00 2001 From: poljvd Date: Sun, 21 Dec 2014 15:26:51 +0100 Subject: [PATCH 59/59] Update Rpi binaries with WS2812 DMA code enabled Former-commit-id: 987624f1061937fdbc7102aaab26a6864401c50f --- deploy/hyperion.tar.gz.REMOVED.git-id | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/hyperion.tar.gz.REMOVED.git-id b/deploy/hyperion.tar.gz.REMOVED.git-id index 5e0f5d63..05e6311d 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -05b947e9badc75841cd5d881a050062add6affc4 \ No newline at end of file +0598f021ab2b4e585b03034d9451b0559a85f038 \ No newline at end of file