From c5b18d592576f9200915b70503ded4bc31d472d0 Mon Sep 17 00:00:00 2001 From: Johan Date: Fri, 7 Feb 2014 09:20:00 +0100 Subject: [PATCH 01/78] Removed use of qt4_use_modules from build files Former-commit-id: 49860cd93e1492a2c67bd2a84bd082713a0c8e9b --- CMakeLists.txt | 12 ++++++------ libsrc/boblightserver/CMakeLists.txt | 8 ++------ libsrc/jsonserver/CMakeLists.txt | 8 ++------ libsrc/protoserver/CMakeLists.txt | 8 ++------ src/hyperion-remote/CMakeLists.txt | 8 ++------ src/hyperion-v4l2/CMakeLists.txt | 6 +----- 6 files changed, 15 insertions(+), 35 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b6b9ed3c..34aec49e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,19 +7,19 @@ 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}) -message(STATUS "ENABLE_SPIDEV = " ${ENABLE_SPIDEV}) -option (ENABLE_V4L2 "Enable the V4L2 grabber" ON) +option(ENABLE_SPIDEV "Enable the SPIDEV device" ON) +message(STATUS "ENABLE_SPIDEV = " ${ENABLE_SPIDEV}) + +option(ENABLE_V4L2 "Enable the V4L2 grabber" ON) message(STATUS "ENABLE_V4L2 = " ${ENABLE_V4L2}) # Createt the configuration file # configure a header file to pass some of the CMake settings # to the source code -configure_file ("${PROJECT_SOURCE_DIR}/HyperionConfig.h.in" "${PROJECT_BINARY_DIR}/HyperionConfig.h") +configure_file("${PROJECT_SOURCE_DIR}/HyperionConfig.h.in" "${PROJECT_BINARY_DIR}/HyperionConfig.h") include_directories("${PROJECT_BINARY_DIR}") # Add project specific cmake modules (find, etc) diff --git a/libsrc/boblightserver/CMakeLists.txt b/libsrc/boblightserver/CMakeLists.txt index af825e74..1bb30cdb 100644 --- a/libsrc/boblightserver/CMakeLists.txt +++ b/libsrc/boblightserver/CMakeLists.txt @@ -28,9 +28,5 @@ add_library(boblightserver target_link_libraries(boblightserver hyperion - hyperion-utils) - -qt4_use_modules(boblightserver - Core - Gui - Network) + hyperion-utils + ${QT_LIBRARIES}) diff --git a/libsrc/jsonserver/CMakeLists.txt b/libsrc/jsonserver/CMakeLists.txt index f0ff1378..208c1ed9 100644 --- a/libsrc/jsonserver/CMakeLists.txt +++ b/libsrc/jsonserver/CMakeLists.txt @@ -37,9 +37,5 @@ add_library(jsonserver target_link_libraries(jsonserver hyperion hyperion-utils - jsoncpp) - -qt4_use_modules(jsonserver - Core - Gui - Network) + jsoncpp + ${QT_LIBRARIES}) diff --git a/libsrc/protoserver/CMakeLists.txt b/libsrc/protoserver/CMakeLists.txt index a66ef336..9e1992a3 100644 --- a/libsrc/protoserver/CMakeLists.txt +++ b/libsrc/protoserver/CMakeLists.txt @@ -44,9 +44,5 @@ add_library(protoserver target_link_libraries(protoserver hyperion hyperion-utils - ${PROTOBUF_LIBRARIES}) - -qt4_use_modules(protoserver - Core - Gui - Network) + ${PROTOBUF_LIBRARIES} + ${QT_LIBRARIES}) diff --git a/src/hyperion-remote/CMakeLists.txt b/src/hyperion-remote/CMakeLists.txt index aafbf472..676dc7d3 100644 --- a/src/hyperion-remote/CMakeLists.txt +++ b/src/hyperion-remote/CMakeLists.txt @@ -28,11 +28,7 @@ add_executable(hyperion-remote ${hyperion-remote_HEADERS} ${hyperion-remote_SOURCES}) -qt4_use_modules(hyperion-remote - Core - Gui - Network) - target_link_libraries(hyperion-remote jsoncpp - getoptPlusPlus) + getoptPlusPlus + ${QT_LIBRARIES}) diff --git a/src/hyperion-v4l2/CMakeLists.txt b/src/hyperion-v4l2/CMakeLists.txt index 5e5de616..635e470e 100644 --- a/src/hyperion-v4l2/CMakeLists.txt +++ b/src/hyperion-v4l2/CMakeLists.txt @@ -49,9 +49,5 @@ target_link_libraries(hyperion-v4l2 hyperion-utils ${PROTOBUF_LIBRARIES} pthread + ${QT_LIBRARIES} ) - -qt4_use_modules(hyperion-v4l2 - Core - Gui - Network) From 99b3719ceea4551d326ff2aec58bd1629de3639b Mon Sep 17 00:00:00 2001 From: Johan Date: Fri, 7 Feb 2014 14:55:54 +0100 Subject: [PATCH 02/78] STL include added Former-commit-id: b99ccce4ac849914e0528ddf6255d52ba47b09c7 --- libsrc/leddevice/LedDeviceFactory.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libsrc/leddevice/LedDeviceFactory.cpp b/libsrc/leddevice/LedDeviceFactory.cpp index e091da9e..2a9aeb4e 100644 --- a/libsrc/leddevice/LedDeviceFactory.cpp +++ b/libsrc/leddevice/LedDeviceFactory.cpp @@ -1,3 +1,6 @@ +// Stl includes +#include +#include // Build configuration #include From fe6bfb0ad27fcfb06d10e05e9ce52a0a22b160a6 Mon Sep 17 00:00:00 2001 From: johan Date: Fri, 7 Feb 2014 15:28:14 +0100 Subject: [PATCH 03/78] Missing include added Former-commit-id: e8bde1967c71b2171de270b25d7474a1a528c56a --- src/hyperion-remote/CustomParameter.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/hyperion-remote/CustomParameter.h b/src/hyperion-remote/CustomParameter.h index aa3aed0d..4f336dd7 100644 --- a/src/hyperion-remote/CustomParameter.h +++ b/src/hyperion-remote/CustomParameter.h @@ -1,5 +1,8 @@ #pragma once +// STL includes +#include + // Qt includes #include #include From f0c35071da1287179df0f621e8fc069f6a94f79f Mon Sep 17 00:00:00 2001 From: johan Date: Fri, 7 Feb 2014 21:11:50 +0100 Subject: [PATCH 04/78] Move V4L2 grabber code to separate library Former-commit-id: f3003eb0142af3d085ccf93fff1b297ebc2321fc --- .../DispmanxWrapper.h | 0 .../grabber}/V4L2Grabber.h | 0 libsrc/CMakeLists.txt | 5 +---- libsrc/grabber/CMakeLists.txt | 8 ++++++++ .../dispmanx}/CMakeLists.txt | 4 ++-- .../dispmanx}/DispmanxFrameGrabber.cpp | 0 .../dispmanx}/DispmanxFrameGrabber.h | 0 .../dispmanx}/DispmanxWrapper.cpp | 4 ++-- libsrc/grabber/v4l2/CMakeLists.txt | 19 +++++++++++++++++++ .../grabber/v4l2}/V4L2Grabber.cpp | 2 +- src/hyperion-v4l2/CMakeLists.txt | 3 +-- src/hyperion-v4l2/hyperion-v4l2.cpp | 4 +++- src/hyperiond/hyperiond.cpp | 2 +- test/dispmanx2png/dispmanx2png.cpp | 2 +- 14 files changed, 39 insertions(+), 14 deletions(-) rename include/{dispmanx-grabber => grabber}/DispmanxWrapper.h (100%) rename {src/hyperion-v4l2 => include/grabber}/V4L2Grabber.h (100%) create mode 100644 libsrc/grabber/CMakeLists.txt rename libsrc/{dispmanx-grabber => grabber/dispmanx}/CMakeLists.txt (81%) rename libsrc/{dispmanx-grabber => grabber/dispmanx}/DispmanxFrameGrabber.cpp (100%) rename libsrc/{dispmanx-grabber => grabber/dispmanx}/DispmanxFrameGrabber.h (100%) rename libsrc/{dispmanx-grabber => grabber/dispmanx}/DispmanxWrapper.cpp (96%) create mode 100644 libsrc/grabber/v4l2/CMakeLists.txt rename {src/hyperion-v4l2 => libsrc/grabber/v4l2}/V4L2Grabber.cpp (99%) diff --git a/include/dispmanx-grabber/DispmanxWrapper.h b/include/grabber/DispmanxWrapper.h similarity index 100% rename from include/dispmanx-grabber/DispmanxWrapper.h rename to include/grabber/DispmanxWrapper.h diff --git a/src/hyperion-v4l2/V4L2Grabber.h b/include/grabber/V4L2Grabber.h similarity index 100% rename from src/hyperion-v4l2/V4L2Grabber.h rename to include/grabber/V4L2Grabber.h diff --git a/libsrc/CMakeLists.txt b/libsrc/CMakeLists.txt index 8db13505..62911dd7 100644 --- a/libsrc/CMakeLists.txt +++ b/libsrc/CMakeLists.txt @@ -12,7 +12,4 @@ add_subdirectory(leddevice) add_subdirectory(utils) add_subdirectory(xbmcvideochecker) add_subdirectory(effectengine) - -if (ENABLE_DISPMANX) - add_subdirectory(dispmanx-grabber) -endif (ENABLE_DISPMANX) +add_subdirectory(grabber) diff --git a/libsrc/grabber/CMakeLists.txt b/libsrc/grabber/CMakeLists.txt new file mode 100644 index 00000000..322a5a98 --- /dev/null +++ b/libsrc/grabber/CMakeLists.txt @@ -0,0 +1,8 @@ + +if (ENABLE_DISPMANX) + add_subdirectory(dispmanx) +endif (ENABLE_DISPMANX) + +if (ENABLE_V4L2) + add_subdirectory(v4l2) +endif (ENABLE_V4L2) diff --git a/libsrc/dispmanx-grabber/CMakeLists.txt b/libsrc/grabber/dispmanx/CMakeLists.txt similarity index 81% rename from libsrc/dispmanx-grabber/CMakeLists.txt rename to libsrc/grabber/dispmanx/CMakeLists.txt index f8bf6ad8..20714dba 100644 --- a/libsrc/dispmanx-grabber/CMakeLists.txt +++ b/libsrc/grabber/dispmanx/CMakeLists.txt @@ -4,8 +4,8 @@ find_package(BCM REQUIRED) include_directories(${BCM_INCLUDE_DIRS}) # Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/dispmanx-grabber) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/dispmanx-grabber) +SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber) +SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/dispmanx) # Group the headers that go through the MOC compiler SET(DispmanxGrabberQT_HEADERS diff --git a/libsrc/dispmanx-grabber/DispmanxFrameGrabber.cpp b/libsrc/grabber/dispmanx/DispmanxFrameGrabber.cpp similarity index 100% rename from libsrc/dispmanx-grabber/DispmanxFrameGrabber.cpp rename to libsrc/grabber/dispmanx/DispmanxFrameGrabber.cpp diff --git a/libsrc/dispmanx-grabber/DispmanxFrameGrabber.h b/libsrc/grabber/dispmanx/DispmanxFrameGrabber.h similarity index 100% rename from libsrc/dispmanx-grabber/DispmanxFrameGrabber.h rename to libsrc/grabber/dispmanx/DispmanxFrameGrabber.h diff --git a/libsrc/dispmanx-grabber/DispmanxWrapper.cpp b/libsrc/grabber/dispmanx/DispmanxWrapper.cpp similarity index 96% rename from libsrc/dispmanx-grabber/DispmanxWrapper.cpp rename to libsrc/grabber/dispmanx/DispmanxWrapper.cpp index 5ffb2a48..c9d2b22c 100644 --- a/libsrc/dispmanx-grabber/DispmanxWrapper.cpp +++ b/libsrc/grabber/dispmanx/DispmanxWrapper.cpp @@ -7,8 +7,8 @@ #include #include -// Local-dispmanx includes -#include +// Dispmanx grabber includes +#include #include "DispmanxFrameGrabber.h" diff --git a/libsrc/grabber/v4l2/CMakeLists.txt b/libsrc/grabber/v4l2/CMakeLists.txt new file mode 100644 index 00000000..ec7830e0 --- /dev/null +++ b/libsrc/grabber/v4l2/CMakeLists.txt @@ -0,0 +1,19 @@ +# Define the current source locations +SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber) +SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/v4l2) + +SET(V4L2_HEADERS + ${CURRENT_HEADER_DIR}/V4L2Grabber.h +) + +SET(V4L2_SOURCES + ${CURRENT_SOURCE_DIR}/V4L2Grabber.cpp +) + +add_library(v4l2-grabber + ${V4L2_HEADERS} + ${V4L2_SOURCES} +) + +target_link_libraries(v4l2-grabber + hyperion-utils) diff --git a/src/hyperion-v4l2/V4L2Grabber.cpp b/libsrc/grabber/v4l2/V4L2Grabber.cpp similarity index 99% rename from src/hyperion-v4l2/V4L2Grabber.cpp rename to libsrc/grabber/v4l2/V4L2Grabber.cpp index 9c0e63ff..09a36140 100644 --- a/src/hyperion-v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/v4l2/V4L2Grabber.cpp @@ -14,7 +14,7 @@ #include #include -#include "V4L2Grabber.h" +#include "grabber/V4L2Grabber.h" #define CLEAR(x) memset(&(x), 0, sizeof(x)) diff --git a/src/hyperion-v4l2/CMakeLists.txt b/src/hyperion-v4l2/CMakeLists.txt index 635e470e..db49e438 100644 --- a/src/hyperion-v4l2/CMakeLists.txt +++ b/src/hyperion-v4l2/CMakeLists.txt @@ -15,7 +15,6 @@ include_directories( ) set(Hyperion_V4L2_HEADERS - V4L2Grabber.h ProtoConnection.h ImageHandler.h VideoStandardParameter.h @@ -23,7 +22,6 @@ set(Hyperion_V4L2_HEADERS set(Hyperion_V4L2_SOURCES hyperion-v4l2.cpp - V4L2Grabber.cpp ProtoConnection.cpp ImageHandler.cpp ) @@ -44,6 +42,7 @@ add_executable(hyperion-v4l2 ) target_link_libraries(hyperion-v4l2 + v4l2-grabber getoptPlusPlus blackborder hyperion-utils diff --git a/src/hyperion-v4l2/hyperion-v4l2.cpp b/src/hyperion-v4l2/hyperion-v4l2.cpp index 5bc22652..7f654c35 100644 --- a/src/hyperion-v4l2/hyperion-v4l2.cpp +++ b/src/hyperion-v4l2/hyperion-v4l2.cpp @@ -12,8 +12,10 @@ // blackborder includes #include +// grabber includes +#include "grabber/V4L2Grabber.h" + // hyperion-v4l2 includes -#include "V4L2Grabber.h" #include "ProtoConnection.h" #include "VideoStandardParameter.h" #include "ImageHandler.h" diff --git a/src/hyperiond/hyperiond.cpp b/src/hyperiond/hyperiond.cpp index db29c89e..08244158 100644 --- a/src/hyperiond/hyperiond.cpp +++ b/src/hyperiond/hyperiond.cpp @@ -19,7 +19,7 @@ #ifdef ENABLE_DISPMANX // Dispmanx grabber includes -#include +#include #endif // XBMC Video checker includes diff --git a/test/dispmanx2png/dispmanx2png.cpp b/test/dispmanx2png/dispmanx2png.cpp index 3439a0e9..1004b8ed 100644 --- a/test/dispmanx2png/dispmanx2png.cpp +++ b/test/dispmanx2png/dispmanx2png.cpp @@ -10,7 +10,7 @@ #include // Dispmanx grabber includes -#include +#include using namespace vlofgren; From 69d6e473283e5f57085a3b6837236580f80c0c7f Mon Sep 17 00:00:00 2001 From: johan Date: Wed, 19 Feb 2014 21:52:37 +0100 Subject: [PATCH 05/78] Create image callback using Qt signal Former-commit-id: cf469ba01ffd26d286e6fb8d9f081cf126042e50 --- include/grabber/V4L2Grabber.h | 35 ++++++------ libsrc/grabber/v4l2/CMakeLists.txt | 13 ++++- libsrc/grabber/v4l2/V4L2Grabber.cpp | 75 ++++++------------------- src/hyperion-v4l2/CMakeLists.txt | 15 ++++- src/hyperion-v4l2/ImageHandler.cpp | 11 ++-- src/hyperion-v4l2/ImageHandler.h | 20 ++++--- src/hyperion-v4l2/ScreenshotHandler.cpp | 25 +++++++++ src/hyperion-v4l2/ScreenshotHandler.h | 24 ++++++++ src/hyperion-v4l2/hyperion-v4l2.cpp | 29 +++++----- 9 files changed, 141 insertions(+), 106 deletions(-) create mode 100644 src/hyperion-v4l2/ScreenshotHandler.cpp create mode 100644 src/hyperion-v4l2/ScreenshotHandler.h diff --git a/include/grabber/V4L2Grabber.h b/include/grabber/V4L2Grabber.h index ab002c3d..40de0870 100644 --- a/include/grabber/V4L2Grabber.h +++ b/include/grabber/V4L2Grabber.h @@ -4,26 +4,27 @@ #include #include +// Qt includes +#include +#include + // util includes #include #include +#include /// Capture class for V4L2 devices /// /// @see http://linuxtv.org/downloads/v4l-dvb-apis/capture-example.html -class V4L2Grabber +class V4L2Grabber : public QObject { -public: - typedef void (*ImageCallback)(void * arg, const Image & image); + Q_OBJECT +public: enum VideoStandard { PAL, NTSC, NO_CHANGE }; - enum Mode3D { - MODE_NONE, MODE_3DSBS, MODE_3DTAB - }; - public: V4L2Grabber( const std::string & device, @@ -36,21 +37,24 @@ public: int verticalPixelDecimation); virtual ~V4L2Grabber(); +public slots: void setCropping(int cropLeft, int cropRight, int cropTop, int cropBottom); - void set3D(Mode3D mode); - - void setCallback(ImageCallback callback, void * arg); + void set3D(VideoMode mode); void start(); - void capture(int frameCount = -1); - void stop(); +signals: + void newFrame(const Image & image); + +private slots: + int read_frame(); + private: void open_device(); @@ -70,8 +74,6 @@ private: void stop_capturing(); - int read_frame(); - bool process_image(const void *p, int size); void process_image(const uint8_t *p); @@ -111,10 +113,9 @@ private: int _horizontalPixelDecimation; int _verticalPixelDecimation; - Mode3D _mode3D; + VideoMode _mode3D; int _currentFrame; - ImageCallback _callback; - void * _callbackArg; + QSocketNotifier * _streamNotifier; }; diff --git a/libsrc/grabber/v4l2/CMakeLists.txt b/libsrc/grabber/v4l2/CMakeLists.txt index ec7830e0..bb8e15b9 100644 --- a/libsrc/grabber/v4l2/CMakeLists.txt +++ b/libsrc/grabber/v4l2/CMakeLists.txt @@ -2,18 +2,27 @@ SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber) SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/v4l2) -SET(V4L2_HEADERS +SET(V4L2_QT_HEADERS ${CURRENT_HEADER_DIR}/V4L2Grabber.h ) +SET(V4L2_HEADERS +) + SET(V4L2_SOURCES ${CURRENT_SOURCE_DIR}/V4L2Grabber.cpp ) +QT4_WRAP_CPP(V4L2_HEADERS_MOC ${V4L2_QT_HEADERS}) + add_library(v4l2-grabber ${V4L2_HEADERS} ${V4L2_SOURCES} + ${V4L2_QT_HEADERS} + ${V4L2_HEADERS_MOC} ) target_link_libraries(v4l2-grabber - hyperion-utils) + hyperion + ${QT_LIBRARIES} +) diff --git a/libsrc/grabber/v4l2/V4L2Grabber.cpp b/libsrc/grabber/v4l2/V4L2Grabber.cpp index 09a36140..59bd0635 100644 --- a/libsrc/grabber/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/v4l2/V4L2Grabber.cpp @@ -59,10 +59,9 @@ V4L2Grabber::V4L2Grabber( _frameDecimation(std::max(1, frameDecimation)), _horizontalPixelDecimation(std::max(1, horizontalPixelDecimation)), _verticalPixelDecimation(std::max(1, verticalPixelDecimation)), - _mode3D(MODE_NONE), + _mode3D(VIDEO_2D), _currentFrame(0), - _callback(nullptr), - _callbackArg(nullptr) + _streamNotifier(nullptr) { open_device(); init_device(videoStandard, input); @@ -82,66 +81,21 @@ void V4L2Grabber::setCropping(int cropLeft, int cropRight, int cropTop, int crop _cropBottom = cropBottom; } -void V4L2Grabber::set3D(Mode3D mode) +void V4L2Grabber::set3D(VideoMode mode) { _mode3D = mode; } -void V4L2Grabber::setCallback(V4L2Grabber::ImageCallback callback, void *arg) -{ - _callback = callback; - _callbackArg = arg; -} - void V4L2Grabber::start() { + _streamNotifier->setEnabled(true); start_capturing(); } -void V4L2Grabber::capture(int frameCount) -{ - for (int count = 0; count < frameCount || frameCount < 0; ++count) - { - for (;;) - { - // the set of file descriptors for select - fd_set fds; - FD_ZERO(&fds); - FD_SET(_fileDescriptor, &fds); - - // timeout - struct timeval tv; - tv.tv_sec = 2; - tv.tv_usec = 0; - - // block until data is available - int r = select(_fileDescriptor + 1, &fds, NULL, NULL, &tv); - - if (-1 == r) - { - if (EINTR == errno) - continue; - throw_errno_exception("select"); - } - - if (0 == r) - { - throw_exception("select timeout"); - } - - if (read_frame()) - { - break; - } - - /* EAGAIN - continue select loop. */ - } - } -} - void V4L2Grabber::stop() { stop_capturing(); + _streamNotifier->setEnabled(false); } void V4L2Grabber::open_device() @@ -170,6 +124,10 @@ void V4L2Grabber::open_device() 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); + connect(_streamNotifier, SIGNAL(activated(int)), this, SLOT(read_frame())); } void V4L2Grabber::close_device() @@ -178,6 +136,12 @@ void V4L2Grabber::close_device() throw_errno_exception("close"); _fileDescriptor = -1; + + if (_streamNotifier != nullptr) + { + delete _streamNotifier; + _streamNotifier = nullptr; + } } void V4L2Grabber::init_read(unsigned int buffer_size) @@ -674,10 +638,10 @@ void V4L2Grabber::process_image(const uint8_t * data) switch (_mode3D) { - case MODE_3DSBS: + case VIDEO_3DSBS: width = _width/2; break; - case MODE_3DTAB: + case VIDEO_3DTAB: height = _height/2; break; default: @@ -717,10 +681,7 @@ void V4L2Grabber::process_image(const uint8_t * data) } } - if (_callback != nullptr) - { - (*_callback)(_callbackArg, image); - } + emit newFrame(image); } int V4L2Grabber::xioctl(int request, void *arg) diff --git a/src/hyperion-v4l2/CMakeLists.txt b/src/hyperion-v4l2/CMakeLists.txt index db49e438..0cf387af 100644 --- a/src/hyperion-v4l2/CMakeLists.txt +++ b/src/hyperion-v4l2/CMakeLists.txt @@ -14,16 +14,21 @@ include_directories( ${QT_INCLUDES} ) -set(Hyperion_V4L2_HEADERS - ProtoConnection.h +set(Hyperion_V4L2_QT_HEADERS ImageHandler.h + ScreenshotHandler.h +) + +set(Hyperion_V4L2_HEADERS VideoStandardParameter.h + ProtoConnection.h ) set(Hyperion_V4L2_SOURCES hyperion-v4l2.cpp ProtoConnection.cpp ImageHandler.cpp + ScreenshotHandler.cpp ) set(Hyperion_V4L2_PROTOS @@ -34,9 +39,13 @@ 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 ${Hyperion_V4L2_HEADERS} ${Hyperion_V4L2_SOURCES} + ${Hyperion_V4L2_QT_HEADERS} + ${Hyperion_V4L2_MOC_SOURCES} ${Hyperion_V4L2_PROTO_SRCS} ${Hyperion_V4L2_PROTO_HDRS} ) @@ -48,5 +57,5 @@ target_link_libraries(hyperion-v4l2 hyperion-utils ${PROTOBUF_LIBRARIES} pthread - ${QT_LIBRARIES} + ${QT_LIBRARIES} ) diff --git a/src/hyperion-v4l2/ImageHandler.cpp b/src/hyperion-v4l2/ImageHandler.cpp index 49198953..afe7419c 100644 --- a/src/hyperion-v4l2/ImageHandler.cpp +++ b/src/hyperion-v4l2/ImageHandler.cpp @@ -10,6 +10,10 @@ ImageHandler::ImageHandler(const std::string & address, int priority, double sig _connection.setSkipReply(skipProtoReply); } +ImageHandler::~ImageHandler() +{ +} + void ImageHandler::receiveImage(const Image & image) { // check if we should do signal detection @@ -32,10 +36,3 @@ void ImageHandler::receiveImage(const Image & image) } } } - -void ImageHandler::imageCallback(void *arg, const Image &image) -{ - ImageHandler * handler = static_cast(arg); - handler->receiveImage(image); -} - diff --git a/src/hyperion-v4l2/ImageHandler.h b/src/hyperion-v4l2/ImageHandler.h index e0e0adbf..5658c750 100644 --- a/src/hyperion-v4l2/ImageHandler.h +++ b/src/hyperion-v4l2/ImageHandler.h @@ -1,24 +1,30 @@ +// Qt includes +#include + +// hyperion includes +#include +#include + // blackborder includes #include -// hyperion-v4l includes +// hyperion v4l2 includes #include "ProtoConnection.h" /// This class handles callbacks from the V4L2 grabber -class ImageHandler +class ImageHandler : public QObject { + Q_OBJECT + public: ImageHandler(const std::string & address, int priority, double signalThreshold, bool skipProtoReply); + virtual ~ImageHandler(); +public slots: /// Handle a single image /// @param image The image to process void receiveImage(const Image & image); - /// static function used to direct callbacks to a ImageHandler object - /// @param arg This should be an ImageHandler instance - /// @param image The image to process - static void imageCallback(void * arg, const Image & image); - private: /// Priority for calls to Hyperion const int _priority; diff --git a/src/hyperion-v4l2/ScreenshotHandler.cpp b/src/hyperion-v4l2/ScreenshotHandler.cpp new file mode 100644 index 00000000..0f9daaef --- /dev/null +++ b/src/hyperion-v4l2/ScreenshotHandler.cpp @@ -0,0 +1,25 @@ +// Qt includes +#include +#include + +// hyperion-v4l2 includes +#include "ScreenshotHandler.h" + +ScreenshotHandler::ScreenshotHandler(const std::string & filename) : + _filename(filename) +{ +} + +ScreenshotHandler::~ScreenshotHandler() +{ +} + +void ScreenshotHandler::receiveImage(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.c_str()); + + // Quit the application after the first image + QCoreApplication::quit(); +} diff --git a/src/hyperion-v4l2/ScreenshotHandler.h b/src/hyperion-v4l2/ScreenshotHandler.h new file mode 100644 index 00000000..3118c57d --- /dev/null +++ b/src/hyperion-v4l2/ScreenshotHandler.h @@ -0,0 +1,24 @@ +// Qt includes +#include + +// hyperionincludes +#include +#include + +/// This class handles callbacks from the V4L2 grabber +class ScreenshotHandler : public QObject +{ + Q_OBJECT + +public: + ScreenshotHandler(const std::string & filename); + virtual ~ScreenshotHandler(); + +public slots: + /// Handle a single image + /// @param image The image to process + void receiveImage(const Image & image); + +private: + const std::string _filename; +}; diff --git a/src/hyperion-v4l2/hyperion-v4l2.cpp b/src/hyperion-v4l2/hyperion-v4l2.cpp index 7f654c35..2268a033 100644 --- a/src/hyperion-v4l2/hyperion-v4l2.cpp +++ b/src/hyperion-v4l2/hyperion-v4l2.cpp @@ -4,7 +4,7 @@ #include // QT includes -#include +#include // getoptPlusPLus includes #include @@ -19,6 +19,7 @@ #include "ProtoConnection.h" #include "VideoStandardParameter.h" #include "ImageHandler.h" +#include "ScreenshotHandler.h" using namespace vlofgren; @@ -32,8 +33,11 @@ void saveScreenshot(void *, const Image & image) int main(int argc, char** argv) { + QCoreApplication app(argc, argv); + // force the locale setlocale(LC_ALL, "C"); + QLocale::setDefault(QLocale::c()); try { @@ -113,31 +117,30 @@ int main(int argc, char** argv) // set 3D mode if applicable if (arg3DSBS.isSet()) { - grabber.set3D(V4L2Grabber::MODE_3DSBS); + grabber.set3D(VIDEO_3DSBS); } else if (arg3DTAB.isSet()) { - grabber.set3D(V4L2Grabber::MODE_3DTAB); + grabber.set3D(VIDEO_3DTAB); } - // start the grabber - grabber.start(); - // run the grabber if (argScreenshot.isSet()) { - grabber.setCallback(&saveScreenshot, nullptr); - grabber.capture(1); + 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(), argSignalThreshold.getValue(), argSkipReply.isSet()); - grabber.setCallback(&ImageHandler::imageCallback, &handler); - grabber.capture(); + QObject::connect(&grabber, SIGNAL(newFrame(Image)), &handler, SLOT(receiveImage(Image))); + grabber.start(); + QCoreApplication::exec(); + grabber.stop(); } - - // stop the grabber - grabber.stop(); } catch (const std::runtime_error & e) { From 22472d8f9597affaa667770a416172574a634ccd Mon Sep 17 00:00:00 2001 From: johan Date: Fri, 21 Feb 2014 22:30:34 +0100 Subject: [PATCH 06/78] Added v4l2 to hyperiond (partial) Former-commit-id: 06684261ec3ac7bb4e8ff0479e5b1472823648ae --- libsrc/grabber/v4l2/V4L2Grabber.cpp | 5 ++++ src/hyperiond/CMakeLists.txt | 4 ++- src/hyperiond/hyperiond.cpp | 41 +++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/libsrc/grabber/v4l2/V4L2Grabber.cpp b/libsrc/grabber/v4l2/V4L2Grabber.cpp index 59bd0635..341485a7 100644 --- a/libsrc/grabber/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/v4l2/V4L2Grabber.cpp @@ -69,6 +69,11 @@ V4L2Grabber::V4L2Grabber( V4L2Grabber::~V4L2Grabber() { + // stop if the grabber was not stopped + if (_streamNotifier != nullptr && _streamNotifier->isEnabled()) { + stop(); + } + uninit_device(); close_device(); } diff --git a/src/hyperiond/CMakeLists.txt b/src/hyperiond/CMakeLists.txt index 1a255b17..17b7464f 100644 --- a/src/hyperiond/CMakeLists.txt +++ b/src/hyperiond/CMakeLists.txt @@ -8,7 +8,9 @@ target_link_libraries(hyperiond effectengine jsonserver protoserver - boblightserver) + boblightserver + v4l2-grabber +) if (ENABLE_DISPMANX) target_link_libraries(hyperiond dispmanx-grabber) diff --git a/src/hyperiond/hyperiond.cpp b/src/hyperiond/hyperiond.cpp index 08244158..536bf27e 100644 --- a/src/hyperiond/hyperiond.cpp +++ b/src/hyperiond/hyperiond.cpp @@ -22,6 +22,11 @@ #include #endif +#ifdef ENABLE_V4L2 +// v4l2 grabber +#include +#endif + // XBMC Video checker includes #include @@ -165,6 +170,39 @@ int main(int argc, char** argv) } #endif +#ifdef ENABLE_V4L2 + // construct and start the v4l2 grabber if the configuration is present + V4L2Grabber * v4l2Grabber = nullptr; + if (config.isMember("grabber-v4l2")) + { + const Json::Value & grabberConfig = config["grabber-v4l2"]; + v4l2Grabber = new V4L2Grabber( + grabberConfig.get("device", "/dev/video0").asString(), + grabberConfig.get("input", 0).asInt(), + grabberConfig.get("standard", V4L2Grabber::NONE), + grabberConfig.get("width", -1).asInt(), + grabberConfig.get("height", -1).asInt(), + grabberConfig.get("frameDecimation", 2).asInt(), + grabberConfig.get("sizeDecimation", 8).asInt(), + grabberConfig.get("sizeDecimation", 8).asInt()); + v4l2Grabber->set3D(grabberConfig.get("mode", VIDEO_2D)); + v4l2Grabber->setCropping( + grabberConfig.get("cropLeft", 0).asInt(), + grabberConfig.get("cropRight", 0).asInt(), + grabberConfig.get("cropTop", 0).asInt(), + grabberConfig.get("cropBottom", 0).asInt()); + + // TODO: create handler + v4l2Grabber->start(); + std::cout << "V4l2 grabber created and started" << std::endl; + } +#else + if (config.isMember("grabber-v4l2")) + { + std::cerr << "The v4l2 grabber can not be instantiated, becuse it has been left out from the build" << std::endl; + } +#endif + // Create Json server if configuration is present JsonServer * jsonServer = nullptr; if (config.isMember("jsonServer")) @@ -199,6 +237,9 @@ int main(int argc, char** argv) // Delete all component #ifdef ENABLE_DISPMANX delete dispmanx; +#endif +#ifdef ENABLE_V4L2 + delete v4l2Grabber; #endif delete xbmcVideoChecker; delete jsonServer; From 1a92e6ccd2248ce4c49c7891bb74ec5b54e0a3f1 Mon Sep 17 00:00:00 2001 From: johan Date: Fri, 21 Feb 2014 22:35:13 +0100 Subject: [PATCH 07/78] Added additional zeroes after LPD6803 device message Former-commit-id: ba15d222339736e5281e667270ddb37b50d1319f --- deploy/hyperion.tar.gz.REMOVED.git-id | 2 +- libsrc/leddevice/LedDeviceLpd6803.cpp | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/deploy/hyperion.tar.gz.REMOVED.git-id b/deploy/hyperion.tar.gz.REMOVED.git-id index e3115d3b..440b9e4d 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -f3afa39f64294a9ce71af8e424e4bab72d2b29cb \ No newline at end of file +8f880f994b0855ace23e27818896fc34ba1677a8 \ No newline at end of file diff --git a/libsrc/leddevice/LedDeviceLpd6803.cpp b/libsrc/leddevice/LedDeviceLpd6803.cpp index bfaa010b..09d5f838 100644 --- a/libsrc/leddevice/LedDeviceLpd6803.cpp +++ b/libsrc/leddevice/LedDeviceLpd6803.cpp @@ -19,11 +19,12 @@ LedDeviceLpd6803::LedDeviceLpd6803(const std::string& outputDevice, const unsign int LedDeviceLpd6803::write(const std::vector &ledValues) { + unsigned messageLength = 4 + 2*ledValues.size() + ledValues.size()/8 + 1; // Reconfigure if the current connfiguration does not match the required configuration - if (4 + 2*ledValues.size() != _ledBuffer.size()) + if (messageLength != _ledBuffer.size()) { // Initialise the buffer - _ledBuffer.resize(4 + 2*ledValues.size(), 0x00); + _ledBuffer.resize(messageLength, 0x00); } // Copy the colors from the ColorRgb vector to the Ldp6803 data vector From 99fd50805cc7b326aae421f5c2bebdd6d48deae8 Mon Sep 17 00:00:00 2001 From: johan Date: Sun, 23 Feb 2014 22:39:23 +0100 Subject: [PATCH 08/78] Added image handler to v4l2 grabber to send colors to Hyperion Former-commit-id: be6fb4dd8080b3325ba6161f48c093f8a145786d --- include/grabber/DispmanxWrapper.h | 2 +- include/grabber/V4L2Wrapper.h | 59 +++++++++++++++++++++++++ libsrc/grabber/v4l2/CMakeLists.txt | 2 + libsrc/grabber/v4l2/V4L2Wrapper.cpp | 67 +++++++++++++++++++++++++++++ src/hyperiond/hyperiond.cpp | 12 +++--- 5 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 include/grabber/V4L2Wrapper.h create mode 100644 libsrc/grabber/v4l2/V4L2Wrapper.cpp diff --git a/include/grabber/DispmanxWrapper.h b/include/grabber/DispmanxWrapper.h index f0156740..4868c1a0 100644 --- a/include/grabber/DispmanxWrapper.h +++ b/include/grabber/DispmanxWrapper.h @@ -73,7 +73,7 @@ private: const int _updateInterval_ms; /// The timeout of the led colors [ms] const int _timeout_ms; - /// The priority of the led colors [ms] + /// The priority of the led colors const int _priority; /// The timer for generating events with the specified update rate diff --git a/include/grabber/V4L2Wrapper.h b/include/grabber/V4L2Wrapper.h new file mode 100644 index 00000000..39a38d7c --- /dev/null +++ b/include/grabber/V4L2Wrapper.h @@ -0,0 +1,59 @@ +#pragma once + +// Hyperion includes +#include +#include + +// Grabber includes +#include + +class V4L2Wrapper : public QObject +{ + Q_OBJECT + +public: + V4L2Wrapper(const std::string & device, + int input, + VideoStandard videoStandard, + int width, + int height, + int frameDecimation, + int pixelDecimation, + Hyperion * hyperion, + int hyperionPriority); + virtual ~V4L2Wrapper(); + +public slots: + void start(); + + void stop(); + + void setCropping(int cropLeft, + int cropRight, + int cropTop, + int cropBottom); + + void set3D(VideoMode mode); + +private slots: + void newFrame(const Image & image); + +private: + /// The timeout of the led colors [ms] + const int _timeout_ms; + + /// The priority of the led colors + const int _priority; + + /// The V4L2 grabber + V4L2Grabber _grabber; + + /// The processor for transforming images to led colors + ImageProcessor * _processor; + + /// The Hyperion instance + Hyperion * _hyperion; + + /// The list with computed led colors + std::vector _ledColors; +}; diff --git a/libsrc/grabber/v4l2/CMakeLists.txt b/libsrc/grabber/v4l2/CMakeLists.txt index aaafd384..5c7844e0 100644 --- a/libsrc/grabber/v4l2/CMakeLists.txt +++ b/libsrc/grabber/v4l2/CMakeLists.txt @@ -4,6 +4,7 @@ SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/v4l2) SET(V4L2_QT_HEADERS ${CURRENT_HEADER_DIR}/V4L2Grabber.h + ${CURRENT_HEADER_DIR}/V4L2Wrapper.h ) SET(V4L2_HEADERS @@ -12,6 +13,7 @@ SET(V4L2_HEADERS SET(V4L2_SOURCES ${CURRENT_SOURCE_DIR}/V4L2Grabber.cpp + ${CURRENT_SOURCE_DIR}/V4L2Wrapper.cpp ) QT4_WRAP_CPP(V4L2_HEADERS_MOC ${V4L2_QT_HEADERS}) diff --git a/libsrc/grabber/v4l2/V4L2Wrapper.cpp b/libsrc/grabber/v4l2/V4L2Wrapper.cpp new file mode 100644 index 00000000..e9745ad3 --- /dev/null +++ b/libsrc/grabber/v4l2/V4L2Wrapper.cpp @@ -0,0 +1,67 @@ +#include + +#include + +V4L2Wrapper::V4L2Wrapper(const std::string &device, + int input, + VideoStandard videoStandard, + int width, + int height, + int frameDecimation, + int pixelDecimation, + Hyperion *hyperion, + int hyperionPriority) : + _timeout_ms(1000), + _priority(hyperionPriority), + _grabber(device, + input, + videoStandard, + width, + height, + frameDecimation, + pixelDecimation, + pixelDecimation), + _processor(ImageProcessorFactory::getInstance().newImageProcessor()), + _hyperion(hyperion), + _ledColors(hyperion->getLedCount(), ColorRgb{0,0,0}) +{ + // connect the new frame signal using a queued connection, because it will be called from a different thread + QObject::connect(&_grabber, SIGNAL(newFrame(Image)), this, SLOT(newFrame(Image)), Qt::QueuedConnection); +} + +V4L2Wrapper::~V4L2Wrapper() +{ + delete _processor; +} + +void V4L2Wrapper::start() +{ + _grabber.start(); +} + +void V4L2Wrapper::stop() +{ + _grabber.stop(); +} + +void V4L2Wrapper::setCropping(int cropLeft, int cropRight, int cropTop, int cropBottom) +{ + _grabber.setCropping(cropLeft, cropRight, cropTop, cropBottom); +} + +void V4L2Wrapper::set3D(VideoMode mode) +{ + _grabber.set3D(mode); +} + +void V4L2Wrapper::newFrame(const Image &image) +{ + // TODO: add a signal detector + + // process the new image + _processor->process(image, _ledColors); + + // send colors to Hyperion + _hyperion->setColors(_priority, _ledColors, _timeout_ms); +} + diff --git a/src/hyperiond/hyperiond.cpp b/src/hyperiond/hyperiond.cpp index 4541ca20..71b01c0b 100644 --- a/src/hyperiond/hyperiond.cpp +++ b/src/hyperiond/hyperiond.cpp @@ -24,7 +24,7 @@ #ifdef ENABLE_V4L2 // v4l2 grabber -#include +#include #endif // XBMC Video checker includes @@ -172,19 +172,20 @@ int main(int argc, char** argv) #ifdef ENABLE_V4L2 // construct and start the v4l2 grabber if the configuration is present - V4L2Grabber * v4l2Grabber = nullptr; + V4L2Wrapper * v4l2Grabber = nullptr; if (config.isMember("grabber-v4l2")) { const Json::Value & grabberConfig = config["grabber-v4l2"]; - v4l2Grabber = new V4L2Grabber( + v4l2Grabber = new V4L2Wrapper( grabberConfig.get("device", "/dev/video0").asString(), grabberConfig.get("input", 0).asInt(), - parseVideoStandard(grabberConfig.get("standard", "NONE").asString()), + parseVideoStandard(grabberConfig.get("standard", "no-change").asString()), grabberConfig.get("width", -1).asInt(), grabberConfig.get("height", -1).asInt(), grabberConfig.get("frameDecimation", 2).asInt(), grabberConfig.get("sizeDecimation", 8).asInt(), - grabberConfig.get("sizeDecimation", 8).asInt()); + &hyperion, + grabberConfig.get("priority", 800).asInt()); v4l2Grabber->set3D(parse3DMode(grabberConfig.get("mode", "2D").asString())); v4l2Grabber->setCropping( grabberConfig.get("cropLeft", 0).asInt(), @@ -192,7 +193,6 @@ int main(int argc, char** argv) grabberConfig.get("cropTop", 0).asInt(), grabberConfig.get("cropBottom", 0).asInt()); - // TODO: create handler v4l2Grabber->start(); std::cout << "V4l2 grabber created and started" << std::endl; } From 4b9dfe3e036e2522c97f40bba35cdbf2f1b5b266 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Tue, 25 Feb 2014 20:49:43 +0100 Subject: [PATCH 09/78] Added missing include Former-commit-id: b35b00821740d48ba073344ac734a5a24c23637e --- 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 e0d405034f799821a116e6553e628e25d657adb1 Mon Sep 17 00:00:00 2001 From: Johan Date: Wed, 26 Feb 2014 18:10:17 +0100 Subject: [PATCH 10/78] Add support for Python 3 Former-commit-id: b6aec954ba0e79ba5697ea8cc305eb9f7d29f332 --- CMakeLists.txt | 4 +- config/hyperion_x86.config.json | 4 +- dependencies/build/getoptPlusPlus/getoptpp.cc | 1 - include/effectengine/EffectEngine.h | 2 +- libsrc/effectengine/Effect.cpp | 86 +++++++++++++++---- libsrc/effectengine/Effect.h | 20 ++++- libsrc/effectengine/EffectEngine.cpp | 3 +- src/hyperion-remote/hyperion-remote.cpp | 18 ++-- 8 files changed, 101 insertions(+), 37 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 34aec49e..6547c450 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,8 +41,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/config/hyperion_x86.config.json b/config/hyperion_x86.config.json index 071713aa..4abd64db 100644 --- a/config/hyperion_x86.config.json +++ b/config/hyperion_x86.config.json @@ -14,7 +14,7 @@ { "name" : "MyPi", "type" : "adalight", - "output" : "/dev/ttyUSB0", + "output" : "/dev/ttyUSB0", "rate" : 115200, "colorOrder" : "rgb" }, @@ -363,7 +363,7 @@ { "paths" : [ - "/opt/hyperion/effects" + "/home/dincs/projects/hyperion/effects" ] }, diff --git a/dependencies/build/getoptPlusPlus/getoptpp.cc b/dependencies/build/getoptPlusPlus/getoptpp.cc index e7b8b420..5444a325 100644 --- a/dependencies/build/getoptPlusPlus/getoptpp.cc +++ b/dependencies/build/getoptPlusPlus/getoptpp.cc @@ -280,7 +280,6 @@ void PresettableUniquelySwitchable::preset() { template<> PODParameter::PODParameter(char shortOption, const char *longOption, const char* description) : CommonParameter(shortOption, longOption, description) { - preset(); } diff --git a/include/effectengine/EffectEngine.h b/include/effectengine/EffectEngine.h index bea45576..c40732a1 100644 --- a/include/effectengine/EffectEngine.h +++ b/include/effectengine/EffectEngine.h @@ -55,5 +55,5 @@ private: std::list _activeEffects; - PyThreadState * _mainThreadState; + PyThreadState * _mainThreadState; }; diff --git a/libsrc/effectengine/Effect.cpp b/libsrc/effectengine/Effect.cpp index e09f8979..b19d40cc 100644 --- a/libsrc/effectengine/Effect.cpp +++ b/libsrc/effectengine/Effect.cpp @@ -19,9 +19,39 @@ PyMethodDef Effect::effectMethods[] = { {NULL, NULL, 0, NULL} }; +#if PY_MAJOR_VERSION >= 3 +// create the hyperion module +struct PyModuleDef Effect::moduleDef = { + PyModuleDef_HEAD_INIT, + "hyperion", /* m_name */ + "Hyperion module", /* m_doc */ + -1, /* m_size */ + Effect::effectMethods, /* m_methods */ + NULL, /* m_reload */ + NULL, /* m_traverse */ + NULL, /* m_clear */ + NULL, /* m_free */ +}; -Effect::Effect(int priority, int timeout, const std::string & script, const Json::Value & args) : +PyObject* Effect::PyInit_hyperion() +{ + return PyModule_Create(&moduleDef); +} +#else +void Effect::PyInit_hyperion() +{ + Py_InitModule("hyperion", effectMethods); +} +#endif + +void Effect::registerHyperionExtensionModule() +{ + PyImport_AppendInittab("hyperion", &PyInit_hyperion); +} + +Effect::Effect(PyThreadState * mainThreadState, int priority, int timeout, const std::string & script, const Json::Value & args) : QThread(), + _mainThreadState(mainThreadState), _priority(priority), _timeout(timeout), _script(script), @@ -44,20 +74,26 @@ Effect::~Effect() void Effect::run() { + // switch to the main thread state and acquire the GIL + PyEval_RestoreThread(_mainThreadState); + // Initialize a new thread state - PyEval_AcquireLock(); // Get the GIL - _interpreterThreadState = Py_NewInterpreter(); + _interpreterThreadState = Py_NewInterpreter(); - // add methods extra builtin methods to the interpreter - PyObject * thisCapsule = PyCapsule_New(this, nullptr, nullptr); - PyObject * module = Py_InitModule4("hyperion", effectMethods, nullptr, thisCapsule, PYTHON_API_VERSION); + // import the buildtin Hyperion module + PyObject * module = PyImport_ImportModule("hyperion"); - // add ledCount variable to the interpreter - PyObject_SetAttrString(module, "ledCount", Py_BuildValue("i", _imageProcessor->getLedCount())); + // add a capsule containing 'this' to the module to be able to retrieve the effect from the callback function + PyObject_SetAttrString(module, "__effectObj", PyCapsule_New(this, nullptr, nullptr)); - // add a args variable to the interpreter - PyObject_SetAttrString(module, "args", json2python(_args)); - //PyObject_SetAttrString(module, "args", Py_BuildValue("s", _args.c_str())); + // add ledCount variable to the interpreter + PyObject_SetAttrString(module, "ledCount", Py_BuildValue("i", _imageProcessor->getLedCount())); + + // add a args variable to the interpreter + PyObject_SetAttrString(module, "args", json2python(_args)); + + // decref the module + Py_XDECREF(module); // Set the end time if applicable if (_timeout > 0) @@ -144,7 +180,7 @@ PyObject *Effect::json2python(const Json::Value &json) const PyObject* Effect::wrapSetColor(PyObject *self, PyObject *args) { // get the effect - Effect * effect = getEffect(self); + Effect * effect = getEffect(); // check if we have aborted already if (effect->_abortRequested) @@ -229,7 +265,7 @@ PyObject* Effect::wrapSetColor(PyObject *self, PyObject *args) PyObject* Effect::wrapSetImage(PyObject *self, PyObject *args) { // get the effect - Effect * effect = getEffect(self); + Effect * effect = getEffect(); // check if we have aborted already if (effect->_abortRequested) @@ -292,7 +328,7 @@ PyObject* Effect::wrapSetImage(PyObject *self, PyObject *args) PyObject* Effect::wrapAbort(PyObject *self, PyObject *) { - Effect * effect = getEffect(self); + Effect * effect = getEffect(); // Test if the effect has reached it end time if (effect->_timeout > 0 && QDateTime::currentMSecsSinceEpoch() > effect->_endTime) @@ -303,8 +339,24 @@ PyObject* Effect::wrapAbort(PyObject *self, PyObject *) return Py_BuildValue("i", effect->_abortRequested ? 1 : 0); } -Effect * Effect::getEffect(PyObject *self) +Effect * Effect::getEffect() { - // Get the effect from the capsule in the self pointer - return reinterpret_cast(PyCapsule_GetPointer(self, nullptr)); + // extract the module from the runtime + PyObject * module = PyObject_GetAttrString(PyImport_AddModule("__main__"), "hyperion"); + + if (PyModule_Check(module)) + { + // retrieve the capsule with the effect + PyObject * effectCapsule = PyObject_GetAttrString(module, "__effectObj"); + + if (PyCapsule_CheckExact(effectCapsule)) + { + // Get the effect from the capsule + return reinterpret_cast(PyCapsule_GetPointer(effectCapsule, nullptr)); + } + } + + // something is wrong + std::cerr << "Unable to retrieve the effect object from the Python runtime" << std::endl; + return nullptr; } diff --git a/libsrc/effectengine/Effect.h b/libsrc/effectengine/Effect.h index 62c2ed75..8540d507 100644 --- a/libsrc/effectengine/Effect.h +++ b/libsrc/effectengine/Effect.h @@ -14,7 +14,7 @@ class Effect : public QThread Q_OBJECT public: - Effect(int priority, int timeout, const std::string & script, const Json::Value & args = Json::Value()); + Effect(PyThreadState * mainThreadState, int priority, int timeout, const std::string & script, const Json::Value & args = Json::Value()); virtual ~Effect(); virtual void run(); @@ -23,6 +23,9 @@ public: bool isAbortRequested() const; + /// This function registers the extension module in Python + static void registerHyperionExtensionModule(); + public slots: void abort(); @@ -38,13 +41,22 @@ private: PyObject * json2python(const Json::Value & json) const; // Wrapper methods for Python interpreter extra buildin methods - static PyMethodDef effectMethods[]; - static PyObject* wrapSetColor(PyObject *self, PyObject *args); + static PyMethodDef effectMethods[]; + static PyObject* wrapSetColor(PyObject *self, PyObject *args); static PyObject* wrapSetImage(PyObject *self, PyObject *args); static PyObject* wrapAbort(PyObject *self, PyObject *args); - static Effect * getEffect(PyObject *self); + static Effect * getEffect(); + +#if PY_MAJOR_VERSION >= 3 + static struct PyModuleDef moduleDef; + static PyObject* PyInit_hyperion(); +#else + static void PyInit_hyperion(); +#endif private: + PyThreadState * _mainThreadState; + const int _priority; const int _timeout; diff --git a/libsrc/effectengine/EffectEngine.cpp b/libsrc/effectengine/EffectEngine.cpp index a343ae4e..f7057e21 100644 --- a/libsrc/effectengine/EffectEngine.cpp +++ b/libsrc/effectengine/EffectEngine.cpp @@ -54,6 +54,7 @@ EffectEngine::EffectEngine(Hyperion * hyperion, const Json::Value & jsonEffectCo // initialize the python interpreter std::cout << "Initializing Python interpreter" << std::endl; + Effect::registerHyperionExtensionModule(); Py_InitializeEx(0); PyEval_InitThreads(); // Create the GIL _mainThreadState = PyEval_SaveThread(); @@ -151,7 +152,7 @@ int EffectEngine::runEffectScript(const std::string &script, const Json::Value & channelCleared(priority); // create the effect - Effect * effect = new Effect(priority, timeout, script, args); + Effect * effect = new Effect(_mainThreadState, priority, timeout, script, args); connect(effect, SIGNAL(setColors(int,std::vector,int,bool)), _hyperion, SLOT(setColors(int,std::vector,int,bool)), Qt::QueuedConnection); connect(effect, SIGNAL(effectFinished(Effect*)), this, SLOT(effectFinished(Effect*))); _activeEffects.push_back(effect); diff --git a/src/hyperion-remote/hyperion-remote.cpp b/src/hyperion-remote/hyperion-remote.cpp index 4fdee922..da9d4b00 100644 --- a/src/hyperion-remote/hyperion-remote.cpp +++ b/src/hyperion-remote/hyperion-remote.cpp @@ -48,8 +48,8 @@ int main(int argc, char * argv[]) IntParameter & argDuration = parameters.add ('d', "duration" , "Specify how long the leds should be switched on in millseconds [default: infinity]"); ColorParameter & argColor = parameters.add ('c', "color" , "Set all leds to a constant color (either RRGGBB hex value or a color name. The color may be repeated multiple time like: RRGGBBRRGGBB)"); ImageParameter & argImage = parameters.add ('i', "image" , "Set the leds to the colors according to the given image file"); - StringParameter & argEffect = parameters.add ('e', "effect" , "Enable the effect with the given name"); - StringParameter & argEffectArgs = parameters.add (0x0, "effectArgs", "Arguments to use in combination with the specified effect. Should be a Json object string."); + StringParameter & argEffect = parameters.add ('e', "effect" , "Enable the effect with the given name"); + StringParameter & argEffectArgs = parameters.add (0x0, "effectArgs", "Arguments to use in combination with the specified effect. Should be a Json object string."); SwitchParameter<> & argServerInfo = parameters.add >('l', "list" , "List server info"); SwitchParameter<> & argClear = parameters.add >('x', "clear" , "Clear data for the priority channel provided by the -p option"); SwitchParameter<> & argClearAll = parameters.add >(0x0, "clearall" , "Clear data for all active priority channels"); @@ -83,13 +83,13 @@ int main(int argc, char * argv[]) bool colorTransform = argSaturation.isSet() || argValue.isSet() || argThreshold.isSet() || argGamma.isSet() || argBlacklevel.isSet() || argWhitelevel.isSet(); // check that exactly one command was given - int commandCount = count({argColor.isSet(), argImage.isSet(), argEffect.isSet(), argServerInfo.isSet(), argClear.isSet(), argClearAll.isSet(), colorTransform}); + int commandCount = count({argColor.isSet(), argImage.isSet(), argEffect.isSet(), argServerInfo.isSet(), argClear.isSet(), argClearAll.isSet(), colorTransform}); if (commandCount != 1) { std::cerr << (commandCount == 0 ? "No command found." : "Multiple commands found.") << " Provide exactly one of the following options:" << std::endl; std::cerr << " " << argColor.usageLine() << std::endl; std::cerr << " " << argImage.usageLine() << std::endl; - std::cerr << " " << argEffect.usageLine() << std::endl; + std::cerr << " " << argEffect.usageLine() << std::endl; std::cerr << " " << argServerInfo.usageLine() << std::endl; std::cerr << " " << argClear.usageLine() << std::endl; std::cerr << " " << argClearAll.usageLine() << std::endl; @@ -116,11 +116,11 @@ int main(int argc, char * argv[]) { connection.setImage(argImage.getValue(), argPriority.getValue(), argDuration.getValue()); } - else if (argEffect.isSet()) - { - connection.setEffect(argEffect.getValue(), argEffectArgs.getValue(), argPriority.getValue(), argDuration.getValue()); - } - else if (argServerInfo.isSet()) + else if (argEffect.isSet()) + { + connection.setEffect(argEffect.getValue(), argEffectArgs.getValue(), argPriority.getValue(), argDuration.getValue()); + } + else if (argServerInfo.isSet()) { QString info = connection.getServerInfo(); std::cout << "Server info:\n" << info.toStdString() << std::endl; From e761a30b586946393fa386729184bf2802f86504 Mon Sep 17 00:00:00 2001 From: johan Date: Sat, 1 Mar 2014 19:28:57 +0100 Subject: [PATCH 11/78] Support for Python3 tested Former-commit-id: 987571bec31e72c4c5dad9b4ceebbca62de6c794 --- CMakeLists.txt | 4 +- deploy/hyperion.tar.gz.REMOVED.git-id | 2 +- libsrc/effectengine/Effect.cpp | 109 ++++++++++++++------------ 3 files changed, 64 insertions(+), 51 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6547c450..34aec49e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,8 +41,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/deploy/hyperion.tar.gz.REMOVED.git-id b/deploy/hyperion.tar.gz.REMOVED.git-id index 440b9e4d..93746779 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -8f880f994b0855ace23e27818896fc34ba1677a8 \ No newline at end of file +0c20a678370e38a9a8da650e04445088d892a49f \ No newline at end of file diff --git a/libsrc/effectengine/Effect.cpp b/libsrc/effectengine/Effect.cpp index b19d40cc..09411c87 100644 --- a/libsrc/effectengine/Effect.cpp +++ b/libsrc/effectengine/Effect.cpp @@ -22,36 +22,36 @@ PyMethodDef Effect::effectMethods[] = { #if PY_MAJOR_VERSION >= 3 // create the hyperion module struct PyModuleDef Effect::moduleDef = { - PyModuleDef_HEAD_INIT, - "hyperion", /* m_name */ - "Hyperion module", /* m_doc */ - -1, /* m_size */ - Effect::effectMethods, /* m_methods */ - NULL, /* m_reload */ - NULL, /* m_traverse */ - NULL, /* m_clear */ - NULL, /* m_free */ + PyModuleDef_HEAD_INIT, + "hyperion", /* m_name */ + "Hyperion module", /* m_doc */ + -1, /* m_size */ + Effect::effectMethods, /* m_methods */ + NULL, /* m_reload */ + NULL, /* m_traverse */ + NULL, /* m_clear */ + NULL, /* m_free */ }; PyObject* Effect::PyInit_hyperion() { - return PyModule_Create(&moduleDef); + return PyModule_Create(&moduleDef); } #else void Effect::PyInit_hyperion() { - Py_InitModule("hyperion", effectMethods); + Py_InitModule("hyperion", effectMethods); } #endif void Effect::registerHyperionExtensionModule() { - PyImport_AppendInittab("hyperion", &PyInit_hyperion); + PyImport_AppendInittab("hyperion", &PyInit_hyperion); } Effect::Effect(PyThreadState * mainThreadState, int priority, int timeout, const std::string & script, const Json::Value & args) : QThread(), - _mainThreadState(mainThreadState), + _mainThreadState(mainThreadState), _priority(priority), _timeout(timeout), _script(script), @@ -74,26 +74,26 @@ Effect::~Effect() void Effect::run() { - // switch to the main thread state and acquire the GIL - PyEval_RestoreThread(_mainThreadState); + // switch to the main thread state and acquire the GIL + PyEval_RestoreThread(_mainThreadState); // Initialize a new thread state - _interpreterThreadState = Py_NewInterpreter(); + _interpreterThreadState = Py_NewInterpreter(); - // import the buildtin Hyperion module - PyObject * module = PyImport_ImportModule("hyperion"); + // import the buildtin Hyperion module + PyObject * module = PyImport_ImportModule("hyperion"); - // add a capsule containing 'this' to the module to be able to retrieve the effect from the callback function - PyObject_SetAttrString(module, "__effectObj", PyCapsule_New(this, nullptr, nullptr)); + // add a capsule containing 'this' to the module to be able to retrieve the effect from the callback function + PyObject_SetAttrString(module, "__effectObj", PyCapsule_New(this, nullptr, nullptr)); - // add ledCount variable to the interpreter - PyObject_SetAttrString(module, "ledCount", Py_BuildValue("i", _imageProcessor->getLedCount())); + // add ledCount variable to the interpreter + PyObject_SetAttrString(module, "ledCount", Py_BuildValue("i", _imageProcessor->getLedCount())); - // add a args variable to the interpreter - PyObject_SetAttrString(module, "args", json2python(_args)); + // add a args variable to the interpreter + PyObject_SetAttrString(module, "args", json2python(_args)); - // decref the module - Py_XDECREF(module); + // decref the module + Py_XDECREF(module); // Set the end time if applicable if (_timeout > 0) @@ -155,19 +155,23 @@ PyObject *Effect::json2python(const Json::Value &json) const return Py_BuildValue("s", json.asCString()); case Json::objectValue: { - PyObject * obj = PyDict_New(); + PyObject * dict= PyDict_New(); for (Json::Value::iterator i = json.begin(); i != json.end(); ++i) { - PyDict_SetItemString(obj, i.memberName(), json2python(*i)); + PyObject * obj = json2python(*i); + PyDict_SetItemString(dict, i.memberName(), obj); + Py_XDECREF(obj); } - return obj; + return dict; } case Json::arrayValue: { PyObject * list = PyList_New(json.size()); for (Json::Value::iterator i = json.begin(); i != json.end(); ++i) { - PyList_SetItem(list, i.index(), json2python(*i)); + PyObject * obj = json2python(*i); + PyList_SetItem(list, i.index(), obj); + Py_XDECREF(obj); } return list; } @@ -180,7 +184,7 @@ PyObject *Effect::json2python(const Json::Value &json) const PyObject* Effect::wrapSetColor(PyObject *self, PyObject *args) { // get the effect - Effect * effect = getEffect(); + Effect * effect = getEffect(); // check if we have aborted already if (effect->_abortRequested) @@ -265,7 +269,7 @@ PyObject* Effect::wrapSetColor(PyObject *self, PyObject *args) PyObject* Effect::wrapSetImage(PyObject *self, PyObject *args) { // get the effect - Effect * effect = getEffect(); + Effect * effect = getEffect(); // check if we have aborted already if (effect->_abortRequested) @@ -328,7 +332,7 @@ PyObject* Effect::wrapSetImage(PyObject *self, PyObject *args) PyObject* Effect::wrapAbort(PyObject *self, PyObject *) { - Effect * effect = getEffect(); + Effect * effect = getEffect(); // Test if the effect has reached it end time if (effect->_timeout > 0 && QDateTime::currentMSecsSinceEpoch() > effect->_endTime) @@ -341,22 +345,31 @@ PyObject* Effect::wrapAbort(PyObject *self, PyObject *) Effect * Effect::getEffect() { - // extract the module from the runtime - PyObject * module = PyObject_GetAttrString(PyImport_AddModule("__main__"), "hyperion"); + // extract the module from the runtime + PyObject * module = PyObject_GetAttrString(PyImport_AddModule("__main__"), "hyperion"); - if (PyModule_Check(module)) - { - // retrieve the capsule with the effect - PyObject * effectCapsule = PyObject_GetAttrString(module, "__effectObj"); + if (!PyModule_Check(module)) + { + // something is wrong + Py_XDECREF(module); + std::cerr << "Unable to retrieve the effect object from the Python runtime" << std::endl; + return nullptr; + } - if (PyCapsule_CheckExact(effectCapsule)) - { - // Get the effect from the capsule - return reinterpret_cast(PyCapsule_GetPointer(effectCapsule, nullptr)); - } - } + // retrieve the capsule with the effect + PyObject * effectCapsule = PyObject_GetAttrString(module, "__effectObj"); + Py_XDECREF(module); - // something is wrong - std::cerr << "Unable to retrieve the effect object from the Python runtime" << std::endl; - return nullptr; + if (!PyCapsule_CheckExact(effectCapsule)) + { + // something is wrong + Py_XDECREF(effectCapsule); + std::cerr << "Unable to retrieve the effect object from the Python runtime" << std::endl; + return nullptr; + } + + // Get the effect from the capsule + Effect * effect = reinterpret_cast(PyCapsule_GetPointer(effectCapsule, nullptr)); + Py_XDECREF(effectCapsule); + return effect; } From e790cb87ca00279c3ecb5d8c27b7bfc2e64c9b10 Mon Sep 17 00:00:00 2001 From: johan Date: Tue, 4 Mar 2014 20:17:38 +0100 Subject: [PATCH 12/78] Fix embedded V4L2 grabber Former-commit-id: f9dc759a8fcac8ac95288b12a007e9c78aed82c3 --- include/utils/Image.h | 28 ++++++++++++++++++++++++---- libsrc/grabber/v4l2/V4L2Wrapper.cpp | 5 +++++ src/hyperion-v4l2/hyperion-v4l2.cpp | 3 +++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/include/utils/Image.h b/include/utils/Image.h index 48625bbb..ac06f087 100644 --- a/include/utils/Image.h +++ b/include/utils/Image.h @@ -13,6 +13,14 @@ public: typedef Pixel_T pixel_type; + /// + /// Default constructor for an image + /// + Image() : + Image(1, 1) + { + } + /// /// Constructor for an image with specified width and height /// @@ -22,8 +30,8 @@ public: Image(const unsigned width, const unsigned height) : _width(width), _height(height), - _pixels(new Pixel_T[width*height + 1]), - _endOfPixels(_pixels + width*height) + _pixels(new Pixel_T[width * height + 1]), + _endOfPixels(_pixels + width * height) { memset(_pixels, 0, (_width*_height+1)*sizeof(Pixel_T)); } @@ -38,12 +46,24 @@ public: 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) + _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)); + } + /// /// Destructor /// diff --git a/libsrc/grabber/v4l2/V4L2Wrapper.cpp b/libsrc/grabber/v4l2/V4L2Wrapper.cpp index e9745ad3..0423e64a 100644 --- a/libsrc/grabber/v4l2/V4L2Wrapper.cpp +++ b/libsrc/grabber/v4l2/V4L2Wrapper.cpp @@ -1,3 +1,5 @@ +#include + #include #include @@ -25,6 +27,9 @@ V4L2Wrapper::V4L2Wrapper(const std::string &device, _hyperion(hyperion), _ledColors(hyperion->getLedCount(), ColorRgb{0,0,0}) { + // register the image type + qRegisterMetaType>("Image"); + // connect the new frame signal using a queued connection, because it will be called from a different thread QObject::connect(&_grabber, SIGNAL(newFrame(Image)), this, SLOT(newFrame(Image)), Qt::QueuedConnection); } diff --git a/src/hyperion-v4l2/hyperion-v4l2.cpp b/src/hyperion-v4l2/hyperion-v4l2.cpp index 4d7ff9d3..04d5d5b4 100644 --- a/src/hyperion-v4l2/hyperion-v4l2.cpp +++ b/src/hyperion-v4l2/hyperion-v4l2.cpp @@ -39,6 +39,9 @@ int main(int argc, char** argv) setlocale(LC_ALL, "C"); QLocale::setDefault(QLocale::c()); + // register the image type to use in signals + qRegisterMetaType>("Image"); + try { // create the option parser and initialize all parameters From 4888294e032a9ff2bc5ecc619d60dfdc5ee864eb Mon Sep 17 00:00:00 2001 From: johan Date: Tue, 4 Mar 2014 20:32:54 +0100 Subject: [PATCH 13/78] Reduce copying of data Former-commit-id: 858ca2331d68458acf87359df87cb25fd051fa30 --- include/grabber/V4L2Wrapper.h | 3 +++ libsrc/grabber/v4l2/V4L2Wrapper.cpp | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/include/grabber/V4L2Wrapper.h b/include/grabber/V4L2Wrapper.h index 39a38d7c..ad6ca247 100644 --- a/include/grabber/V4L2Wrapper.h +++ b/include/grabber/V4L2Wrapper.h @@ -35,6 +35,9 @@ public slots: void set3D(VideoMode mode); +signals: + void emitColors(int priority, const std::vector &ledColors, const int timeout_ms); + private slots: void newFrame(const Image & image); diff --git a/libsrc/grabber/v4l2/V4L2Wrapper.cpp b/libsrc/grabber/v4l2/V4L2Wrapper.cpp index 0423e64a..f859c05e 100644 --- a/libsrc/grabber/v4l2/V4L2Wrapper.cpp +++ b/libsrc/grabber/v4l2/V4L2Wrapper.cpp @@ -29,9 +29,19 @@ V4L2Wrapper::V4L2Wrapper(const std::string &device, { // register the image type qRegisterMetaType>("Image"); + qRegisterMetaType>("std::vector"); - // connect the new frame signal using a queued connection, because it will be called from a different thread - QObject::connect(&_grabber, SIGNAL(newFrame(Image)), this, SLOT(newFrame(Image)), Qt::QueuedConnection); + // Handle the image in the captured thread using a direct connection + QObject::connect( + &_grabber, SIGNAL(newFrame(Image)), + this, SLOT(newFrame(Image)), + Qt::DirectConnection); + + // send color data to Hyperion using a queued connection to handle the data over to the main event loop + QObject::connect( + this, SIGNAL(emitColors(int,std::vector,int)), + _hyperion, SLOT(setColors(int,std::vector,int)), + Qt::QueuedConnection); } V4L2Wrapper::~V4L2Wrapper() @@ -67,6 +77,6 @@ void V4L2Wrapper::newFrame(const Image &image) _processor->process(image, _ledColors); // send colors to Hyperion - _hyperion->setColors(_priority, _ledColors, _timeout_ms); + emit emitColors(_priority, _ledColors, _timeout_ms); } From 2b683fdfaa8935c5ce05fbc8ece24ed8a2d4be1e Mon Sep 17 00:00:00 2001 From: Bjoern Bilger Date: Tue, 4 Mar 2014 20:38:54 +0100 Subject: [PATCH 14/78] add support for tinkerforge Former-commit-id: 8dbd3d915f70ace8d1b27602f0d76a2a546ac043 --- CMakeLists.txt | 7 ++ HyperionConfig.h.in | 3 + libsrc/leddevice/CMakeLists.txt | 42 +++++-- libsrc/leddevice/LedDeviceFactory.cpp | 20 +++ libsrc/leddevice/LedDeviceTinkerforge.cpp | 141 ++++++++++++++++++++++ libsrc/leddevice/LedDeviceTinkerforge.h | 82 +++++++++++++ 6 files changed, 288 insertions(+), 7 deletions(-) create mode 100644 libsrc/leddevice/LedDeviceTinkerforge.cpp create mode 100644 libsrc/leddevice/LedDeviceTinkerforge.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 34aec49e..9903cbaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,9 @@ message(STATUS "ENABLE_SPIDEV = " ${ENABLE_SPIDEV}) option(ENABLE_V4L2 "Enable the V4L2 grabber" ON) message(STATUS "ENABLE_V4L2 = " ${ENABLE_V4L2}) +option(ENABLE_TINKERFORGE "Enable the TINKERFORGE device" OFF) +message(STATUS "ENABLE_TINKERFORGE = " ${ENABLE_TINKERFORGE}) + # Createt the configuration file # configure a header file to pass some of the CMake settings # to the source code @@ -62,6 +65,10 @@ set(CMAKE_FIND_LIBRARY_SUFFIXES_OLD) find_package(libusb-1.0 REQUIRED) find_package(Threads REQUIRED) +if (ENABLE_TINKERFORGE) + find_package(libtinkerforge-1.0 REQUIRED) +endif (ENABLE_TINKERFORGE) + include(${QT_USE_FILE}) add_definitions(${QT_DEFINITIONS}) # TODO[TvdZ]: This linking directory should only be added if we are cross compiling diff --git a/HyperionConfig.h.in b/HyperionConfig.h.in index 14500c71..17f040c0 100644 --- a/HyperionConfig.h.in +++ b/HyperionConfig.h.in @@ -8,3 +8,6 @@ // Define to enable the spi-device #cmakedefine ENABLE_SPIDEV + +// Define to enable the spi-device +#cmakedefine ENABLE_TINKERFORGE diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt index 6bfc2fe8..633e8dcd 100644 --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -6,10 +6,20 @@ SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/leddevice) #add libusb and pthreads (required for the Lighpack usb device) find_package(libusb-1.0 REQUIRED) find_package(Threads REQUIRED) +if(ENABLE_TINKERFORGE) + find_package(libtinkerforge-1.0 REQUIRED) +endif(ENABLE_TINKERFORGE) + include_directories( - ../../include/hidapi - ${LIBUSB_1_INCLUDE_DIRS}) # for Lightpack device + ../../include/hidapi + ${LIBUSB_1_INCLUDE_DIRS}) # for Lightpack device + + +if(ENABLE_TINKERFORGE) + include_directories( + ${LIBTINKERFORGE_1_INCLUDE_DIRS}) # for Tinkerforge device +endif(ENABLE_TINKERFORGE) # Group the headers that go through the MOC compiler SET(Leddevice_QT_HEADERS @@ -67,6 +77,17 @@ if(ENABLE_SPIDEV) ) endif(ENABLE_SPIDEV) +if(ENABLE_TINKERFORGE) + SET(Leddevice_HEADERS + ${Leddevice_HEADERS} + ${CURRENT_SOURCE_DIR}/LedDeviceTinkerforge.h + ) + SET(Leddevice_SOURCES + ${Leddevice_SOURCES} + ${CURRENT_SOURCE_DIR}/LedDeviceTinkerforge.cpp + ) +endif(ENABLE_TINKERFORGE) + QT4_WRAP_CPP(Leddevice_HEADERS_MOC ${Leddevice_QT_HEADERS}) @@ -78,12 +99,19 @@ add_library(leddevice ) target_link_libraries(leddevice - hyperion-utils - serialport - ${LIBUSB_1_LIBRARIES} #apt-get install libusb-1.0-0-dev - ${CMAKE_THREAD_LIBS_INIT} - ${QT_LIBRARIES} + hyperion-utils + serialport + ${LIBUSB_1_LIBRARIES} #apt-get install libusb-1.0-0-dev + ${CMAKE_THREAD_LIBS_INIT} + ${QT_LIBRARIES} ) + +if(ENABLE_TINKERFORGE) + target_link_libraries(leddevice + ${LIBTINKERFORGE_1_LIBRARIES} + ) +endif() + if(APPLE) target_link_libraries(leddevice hidapi-mac) else() diff --git a/libsrc/leddevice/LedDeviceFactory.cpp b/libsrc/leddevice/LedDeviceFactory.cpp index 2a9aeb4e..4c596e88 100644 --- a/libsrc/leddevice/LedDeviceFactory.cpp +++ b/libsrc/leddevice/LedDeviceFactory.cpp @@ -16,6 +16,10 @@ #include "LedDeviceWs2801.h" #endif +#ifdef ENABLE_TINKERFORGE + #include "LedDeviceTinkerforge.h" +#endif + #include "LedDeviceAdalight.h" #include "LedDeviceLightpack.h" #include "LedDeviceMultiLightpack.h" @@ -87,6 +91,22 @@ LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) device = deviceWs2801; } #endif + +#ifdef ENABLE_TINKERFORGE + else if (type=="tinkerforge") + { + const std::string host = deviceConfig.get("output", "127.0.0.1").asString(); + const uint16_t port = deviceConfig.get("port", 4223).asInt(); + const std::string uid = deviceConfig["uid"].asString(); + const unsigned rate = deviceConfig["rate"].asInt(); + + LedDeviceTinkerforge* deviceTinkerforge = new LedDeviceTinkerforge(host, port, uid, rate); + deviceTinkerforge->open(); + + device = deviceTinkerforge; + } +#endif + // else if (type == "ws2811") // { // const std::string output = deviceConfig["output"].asString(); diff --git a/libsrc/leddevice/LedDeviceTinkerforge.cpp b/libsrc/leddevice/LedDeviceTinkerforge.cpp new file mode 100644 index 00000000..04cfb151 --- /dev/null +++ b/libsrc/leddevice/LedDeviceTinkerforge.cpp @@ -0,0 +1,141 @@ + +// STL includes +#include +#include + +// Local LedDevice includes +#include "LedDeviceTinkerforge.h" + +static const unsigned MAX_NUM_LEDS = 320; +static const unsigned MAX_NUM_LEDS_SETTABLE = 16; + +LedDeviceTinkerforge::LedDeviceTinkerforge(const std::string &host, uint16_t port, const std::string &uid, const unsigned interval) : + LedDevice(), + _host(host), + _port(port), + _uid(uid), + _interval(interval), + _ipConnection(nullptr), + _ledStrip(nullptr), + _colorChannelSize(0) +{ + // empty +} + +LedDeviceTinkerforge::~LedDeviceTinkerforge() +{ + // Close the device (if it is opened) + if (_ipConnection != nullptr && _ledStrip != nullptr) + { + switchOff(); + } + if (_ipConnection != nullptr) + delete _ipConnection; + if (_ledStrip != nullptr) + delete _ledStrip; +} + +int LedDeviceTinkerforge::open() +{ + _ipConnection = new IPConnection; + ipcon_create(_ipConnection); + + int connectionStatus = ipcon_connect(_ipConnection, _host.c_str(), _port); + if (connectionStatus < 0) + { + std::cerr << "Attempt to connect to master brick (" << _host << ":" << _port << ") failed with status " << connectionStatus << std::endl; + return -1; + } + + _ledStrip = new LEDStrip; + led_strip_create(_ledStrip, _uid.c_str(), _ipConnection); + + int frameStatus = led_strip_set_frame_duration(_ledStrip, _interval); + if (frameStatus < 0) + { + std::cerr << "Attempt to connect to led strip bricklet (led_strip_set_frame_duration()) failed with status " << frameStatus << std::endl; + return -1; + } + + return 0; +} + +int LedDeviceTinkerforge::write(const std::vector &ledValues) +{ + std::cerr << "Write" << std::endl; + + unsigned nrLedValues = ledValues.size(); + + if (nrLedValues > MAX_NUM_LEDS) + { + std::cerr << "Invalid attempt to write led values. Not more than " << MAX_NUM_LEDS << " leds are allowed." << std::endl; + return -1; + } + + if (_colorChannelSize < nrLedValues) + { + _redChannel.resize(nrLedValues, uint8_t(0)); + _greenChannel.resize(nrLedValues, uint8_t(0)); + _blueChannel.resize(nrLedValues, uint8_t(0)); + } + _colorChannelSize = nrLedValues; + + auto redIt = _redChannel.begin(); + auto greenIt = _greenChannel.begin(); + auto blueIt = _blueChannel.begin(); + + for (const ColorRgb &ledValue : ledValues) + { + *redIt = ledValue.red; + redIt++; + *greenIt = ledValue.green; + greenIt++; + *blueIt = ledValue.blue; + blueIt++; + } + + return transferLedData(_ledStrip, 0, _colorChannelSize, &_redChannel[0], &_greenChannel[0], &_blueChannel[0]); +} + +int LedDeviceTinkerforge::switchOff() +{ + std::cerr << "Switchoff" << std::endl; + std::fill(_redChannel.begin(), _redChannel.end(), 0); + std::fill(_greenChannel.begin(), _greenChannel.end(), 0); + std::fill(_blueChannel.begin(), _blueChannel.end(), 0); + + return transferLedData(_ledStrip, 0, _colorChannelSize, &_redChannel[0], &_greenChannel[0], &_blueChannel[0]); +} + +int LedDeviceTinkerforge::transferLedData(LEDStrip *ledStrip, unsigned index, unsigned length, uint8_t *redChannel, uint8_t *greenChannel, uint8_t *blueChannel) +{ + // we need that array size no matter how many leds will really be set + uint8_t _reds[MAX_NUM_LEDS_SETTABLE]; + uint8_t _greens[MAX_NUM_LEDS_SETTABLE]; + uint8_t _blues[MAX_NUM_LEDS_SETTABLE]; + + int status = E_INVALID_PARAMETER; + unsigned i; + unsigned int copyLength; + + if (index >= 0 && length > 0 && index < length && length <= MAX_NUM_LEDS) + { + for (i = index; i < length; i += MAX_NUM_LEDS_SETTABLE) + { + copyLength = (i + MAX_NUM_LEDS_SETTABLE > length) ? length - i : MAX_NUM_LEDS_SETTABLE; + + memcpy(_reds, redChannel + i, copyLength * sizeof(uint8_t)); + memcpy(_greens, greenChannel + i, copyLength * sizeof(uint8_t)); + memcpy(_blues, blueChannel + i, copyLength * sizeof(uint8_t)); + + status = led_strip_set_rgb_values(ledStrip, i, copyLength, _reds, _greens, _blues); + + if (status != E_OK) + { + std::cerr << "Setting led values failed with status " << status << std::endl; + break; + } + } + } + return status; +} diff --git a/libsrc/leddevice/LedDeviceTinkerforge.h b/libsrc/leddevice/LedDeviceTinkerforge.h new file mode 100644 index 00000000..95f43332 --- /dev/null +++ b/libsrc/leddevice/LedDeviceTinkerforge.h @@ -0,0 +1,82 @@ + +#pragma once + +// STL includes +#include + +// Hyperion-Leddevice includes +#include + + +extern "C" { + #include + #include +} + +class LedDeviceTinkerforge : public LedDevice +{ +public: + + LedDeviceTinkerforge(const std::string &host, uint16_t port, const std::string &uid, const unsigned interval); + + virtual ~LedDeviceTinkerforge(); + + /// + /// Attempts to open a connection to the master bricklet and the led strip bricklet. + /// + /// @return Zero on succes else negative + /// + int open(); + + /// + /// Writes the colors to the led strip bricklet + /// + /// @param ledValues The color value for each led + /// + /// @return Zero on success else negative + /// + virtual int write(const std::vector &ledValues); + + /// + /// Switches off the leds + /// + /// @return Zero on success else negative + /// + virtual int switchOff(); + +private: + /// + /// Writes the data to the led strip blicklet + int transferLedData(LEDStrip *ledstrip, unsigned int index, unsigned int length, uint8_t *redChannel, uint8_t *greenChannel, uint8_t *blueChannel); + + /// The host of the master brick + const std::string _host; + + /// The port of the master brick + const uint16_t _port; + + /// The uid of the led strip bricklet + const std::string _uid; + + /// The interval/rate + const unsigned _interval; + + /// ip connection handle + IPConnection *_ipConnection; + + /// led strip handle + LEDStrip *_ledStrip; + + /// buffer for red channel led data + std::vector _redChannel; + + /// buffer for red channel led data + std::vector _greenChannel; + + /// buffer for red channel led data + std::vector _blueChannel; + + /// buffer size of the color channels + unsigned int _colorChannelSize; + +}; From cf21ba76c9e4208599ce77de7eb434231100fa5c Mon Sep 17 00:00:00 2001 From: Bjoern Bilger Date: Tue, 4 Mar 2014 20:38:54 +0100 Subject: [PATCH 15/78] add support for tinkerforge Former-commit-id: f624e4ea226365d6cc832ebbe51b471559341f33 --- CMakeLists.txt | 7 ++ HyperionConfig.h.in | 3 + cmake/Findlibtinkerforge-1.0.cmake | 96 +++++++++++++++ libsrc/leddevice/CMakeLists.txt | 42 +++++-- libsrc/leddevice/LedDeviceFactory.cpp | 20 +++ libsrc/leddevice/LedDeviceTinkerforge.cpp | 141 ++++++++++++++++++++++ libsrc/leddevice/LedDeviceTinkerforge.h | 82 +++++++++++++ 7 files changed, 384 insertions(+), 7 deletions(-) create mode 100644 cmake/Findlibtinkerforge-1.0.cmake create mode 100644 libsrc/leddevice/LedDeviceTinkerforge.cpp create mode 100644 libsrc/leddevice/LedDeviceTinkerforge.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 34aec49e..9903cbaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,9 @@ message(STATUS "ENABLE_SPIDEV = " ${ENABLE_SPIDEV}) option(ENABLE_V4L2 "Enable the V4L2 grabber" ON) message(STATUS "ENABLE_V4L2 = " ${ENABLE_V4L2}) +option(ENABLE_TINKERFORGE "Enable the TINKERFORGE device" OFF) +message(STATUS "ENABLE_TINKERFORGE = " ${ENABLE_TINKERFORGE}) + # Createt the configuration file # configure a header file to pass some of the CMake settings # to the source code @@ -62,6 +65,10 @@ set(CMAKE_FIND_LIBRARY_SUFFIXES_OLD) find_package(libusb-1.0 REQUIRED) find_package(Threads REQUIRED) +if (ENABLE_TINKERFORGE) + find_package(libtinkerforge-1.0 REQUIRED) +endif (ENABLE_TINKERFORGE) + include(${QT_USE_FILE}) add_definitions(${QT_DEFINITIONS}) # TODO[TvdZ]: This linking directory should only be added if we are cross compiling diff --git a/HyperionConfig.h.in b/HyperionConfig.h.in index 14500c71..17f040c0 100644 --- a/HyperionConfig.h.in +++ b/HyperionConfig.h.in @@ -8,3 +8,6 @@ // Define to enable the spi-device #cmakedefine ENABLE_SPIDEV + +// Define to enable the spi-device +#cmakedefine ENABLE_TINKERFORGE diff --git a/cmake/Findlibtinkerforge-1.0.cmake b/cmake/Findlibtinkerforge-1.0.cmake new file mode 100644 index 00000000..e9b6b3ba --- /dev/null +++ b/cmake/Findlibtinkerforge-1.0.cmake @@ -0,0 +1,96 @@ +# - Try to find libtinkerforge-1.0 +# Once done this will define +# +# LIBTINKERFORGE_1_FOUND - system has libtinkerforge +# LIBTINKERFORGE_1_INCLUDE_DIRS - the libtinkerforge include directory +# LIBTINKERFORGE_1_LIBRARIES - Link these to use libtinkerforge +# LIBTINKERFORGE_1_DEFINITIONS - Compiler switches required for using libtinkerforge +# +# Adapted from cmake-modules Google Code project +# +# Copyright (c) 2006 Andreas Schneider +# +# (Changes for libtinkerforge) Copyright (c) 2014 Björn Bilger +# +# Redistribution and use is allowed according to the terms of the New BSD license. +# +# CMake-Modules Project New BSD License +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the CMake-Modules Project nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + + +if (LIBTINKERFORGE_1_LIBRARIES AND LIBTINKERFORGE_1_INCLUDE_DIRS) + # in cache already + set(LIBTINKERFORGE_FOUND TRUE) +else (LIBTINKERFORGE_1_LIBRARIES AND LIBTINKERFORGE_1_INCLUDE_DIRS) + find_path(LIBTINKERFORGE_1_INCLUDE_DIR + NAMES + tinkerforge/tinkerforge.h + PATHS + /usr/include + /usr/local/include + /opt/local/include + /sw/include + PATH_SUFFIXES + tinkerforge-1.0 + ) + + find_library(LIBTINKERFORGE_1_LIBRARY + NAMES + tinkerforge-1.0 + PATHS + /usr/lib + /usr/local/lib + /opt/local/lib + /sw/lib + ) + + set(LIBTINKERFORGE_1_INCLUDE_DIRS + ${LIBTINKERFORGE_1_INCLUDE_DIR} + ) + set(LIBTINKERFORGE_1_LIBRARIES + ${LIBTINKERFORGE_1_LIBRARY} +) + + if (LIBTINKERFORGE_1_INCLUDE_DIRS AND LIBTINKERFORGE_1_LIBRARIES) + set(LIBTINKERFORGE_1_FOUND TRUE) + endif (LIBTINKERFORGE_1_INCLUDE_DIRS AND LIBTINKERFORGE_1_LIBRARIES) + + if (LIBTINKERFORGE_1_FOUND) + if (NOT libtinkerforge_1_FIND_QUIETLY) + message(STATUS "Found libtinkerforge-1.0:") + message(STATUS " - Includes: ${LIBTINKERFORGE_1_INCLUDE_DIRS}") + message(STATUS " - Libraries: ${LIBTINKERFORGE_1_LIBRARIES}") + endif (NOT libtinkerforge_1_FIND_QUIETLY) + else (LIBTINKERFORGE_1_FOUND) + message(FATAL_ERROR "Could not find libtinkerforge") + endif (LIBTINKERFORGE_1_FOUND) + + # show the LIBTINKERFORGE_1_INCLUDE_DIRS and LIBTINKERFORGE_1_LIBRARIES variables only in the advanced view + mark_as_advanced(LIBTINKERFORGE_1_INCLUDE_DIRS LIBTINKERFORGE_1_LIBRARIES) + +endif (LIBTINKERFORGE_1_LIBRARIES AND LIBTINKERFORGE_1_INCLUDE_DIRS) diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt index 6bfc2fe8..633e8dcd 100644 --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -6,10 +6,20 @@ SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/leddevice) #add libusb and pthreads (required for the Lighpack usb device) find_package(libusb-1.0 REQUIRED) find_package(Threads REQUIRED) +if(ENABLE_TINKERFORGE) + find_package(libtinkerforge-1.0 REQUIRED) +endif(ENABLE_TINKERFORGE) + include_directories( - ../../include/hidapi - ${LIBUSB_1_INCLUDE_DIRS}) # for Lightpack device + ../../include/hidapi + ${LIBUSB_1_INCLUDE_DIRS}) # for Lightpack device + + +if(ENABLE_TINKERFORGE) + include_directories( + ${LIBTINKERFORGE_1_INCLUDE_DIRS}) # for Tinkerforge device +endif(ENABLE_TINKERFORGE) # Group the headers that go through the MOC compiler SET(Leddevice_QT_HEADERS @@ -67,6 +77,17 @@ if(ENABLE_SPIDEV) ) endif(ENABLE_SPIDEV) +if(ENABLE_TINKERFORGE) + SET(Leddevice_HEADERS + ${Leddevice_HEADERS} + ${CURRENT_SOURCE_DIR}/LedDeviceTinkerforge.h + ) + SET(Leddevice_SOURCES + ${Leddevice_SOURCES} + ${CURRENT_SOURCE_DIR}/LedDeviceTinkerforge.cpp + ) +endif(ENABLE_TINKERFORGE) + QT4_WRAP_CPP(Leddevice_HEADERS_MOC ${Leddevice_QT_HEADERS}) @@ -78,12 +99,19 @@ add_library(leddevice ) target_link_libraries(leddevice - hyperion-utils - serialport - ${LIBUSB_1_LIBRARIES} #apt-get install libusb-1.0-0-dev - ${CMAKE_THREAD_LIBS_INIT} - ${QT_LIBRARIES} + hyperion-utils + serialport + ${LIBUSB_1_LIBRARIES} #apt-get install libusb-1.0-0-dev + ${CMAKE_THREAD_LIBS_INIT} + ${QT_LIBRARIES} ) + +if(ENABLE_TINKERFORGE) + target_link_libraries(leddevice + ${LIBTINKERFORGE_1_LIBRARIES} + ) +endif() + if(APPLE) target_link_libraries(leddevice hidapi-mac) else() diff --git a/libsrc/leddevice/LedDeviceFactory.cpp b/libsrc/leddevice/LedDeviceFactory.cpp index 2a9aeb4e..4c596e88 100644 --- a/libsrc/leddevice/LedDeviceFactory.cpp +++ b/libsrc/leddevice/LedDeviceFactory.cpp @@ -16,6 +16,10 @@ #include "LedDeviceWs2801.h" #endif +#ifdef ENABLE_TINKERFORGE + #include "LedDeviceTinkerforge.h" +#endif + #include "LedDeviceAdalight.h" #include "LedDeviceLightpack.h" #include "LedDeviceMultiLightpack.h" @@ -87,6 +91,22 @@ LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) device = deviceWs2801; } #endif + +#ifdef ENABLE_TINKERFORGE + else if (type=="tinkerforge") + { + const std::string host = deviceConfig.get("output", "127.0.0.1").asString(); + const uint16_t port = deviceConfig.get("port", 4223).asInt(); + const std::string uid = deviceConfig["uid"].asString(); + const unsigned rate = deviceConfig["rate"].asInt(); + + LedDeviceTinkerforge* deviceTinkerforge = new LedDeviceTinkerforge(host, port, uid, rate); + deviceTinkerforge->open(); + + device = deviceTinkerforge; + } +#endif + // else if (type == "ws2811") // { // const std::string output = deviceConfig["output"].asString(); diff --git a/libsrc/leddevice/LedDeviceTinkerforge.cpp b/libsrc/leddevice/LedDeviceTinkerforge.cpp new file mode 100644 index 00000000..04cfb151 --- /dev/null +++ b/libsrc/leddevice/LedDeviceTinkerforge.cpp @@ -0,0 +1,141 @@ + +// STL includes +#include +#include + +// Local LedDevice includes +#include "LedDeviceTinkerforge.h" + +static const unsigned MAX_NUM_LEDS = 320; +static const unsigned MAX_NUM_LEDS_SETTABLE = 16; + +LedDeviceTinkerforge::LedDeviceTinkerforge(const std::string &host, uint16_t port, const std::string &uid, const unsigned interval) : + LedDevice(), + _host(host), + _port(port), + _uid(uid), + _interval(interval), + _ipConnection(nullptr), + _ledStrip(nullptr), + _colorChannelSize(0) +{ + // empty +} + +LedDeviceTinkerforge::~LedDeviceTinkerforge() +{ + // Close the device (if it is opened) + if (_ipConnection != nullptr && _ledStrip != nullptr) + { + switchOff(); + } + if (_ipConnection != nullptr) + delete _ipConnection; + if (_ledStrip != nullptr) + delete _ledStrip; +} + +int LedDeviceTinkerforge::open() +{ + _ipConnection = new IPConnection; + ipcon_create(_ipConnection); + + int connectionStatus = ipcon_connect(_ipConnection, _host.c_str(), _port); + if (connectionStatus < 0) + { + std::cerr << "Attempt to connect to master brick (" << _host << ":" << _port << ") failed with status " << connectionStatus << std::endl; + return -1; + } + + _ledStrip = new LEDStrip; + led_strip_create(_ledStrip, _uid.c_str(), _ipConnection); + + int frameStatus = led_strip_set_frame_duration(_ledStrip, _interval); + if (frameStatus < 0) + { + std::cerr << "Attempt to connect to led strip bricklet (led_strip_set_frame_duration()) failed with status " << frameStatus << std::endl; + return -1; + } + + return 0; +} + +int LedDeviceTinkerforge::write(const std::vector &ledValues) +{ + std::cerr << "Write" << std::endl; + + unsigned nrLedValues = ledValues.size(); + + if (nrLedValues > MAX_NUM_LEDS) + { + std::cerr << "Invalid attempt to write led values. Not more than " << MAX_NUM_LEDS << " leds are allowed." << std::endl; + return -1; + } + + if (_colorChannelSize < nrLedValues) + { + _redChannel.resize(nrLedValues, uint8_t(0)); + _greenChannel.resize(nrLedValues, uint8_t(0)); + _blueChannel.resize(nrLedValues, uint8_t(0)); + } + _colorChannelSize = nrLedValues; + + auto redIt = _redChannel.begin(); + auto greenIt = _greenChannel.begin(); + auto blueIt = _blueChannel.begin(); + + for (const ColorRgb &ledValue : ledValues) + { + *redIt = ledValue.red; + redIt++; + *greenIt = ledValue.green; + greenIt++; + *blueIt = ledValue.blue; + blueIt++; + } + + return transferLedData(_ledStrip, 0, _colorChannelSize, &_redChannel[0], &_greenChannel[0], &_blueChannel[0]); +} + +int LedDeviceTinkerforge::switchOff() +{ + std::cerr << "Switchoff" << std::endl; + std::fill(_redChannel.begin(), _redChannel.end(), 0); + std::fill(_greenChannel.begin(), _greenChannel.end(), 0); + std::fill(_blueChannel.begin(), _blueChannel.end(), 0); + + return transferLedData(_ledStrip, 0, _colorChannelSize, &_redChannel[0], &_greenChannel[0], &_blueChannel[0]); +} + +int LedDeviceTinkerforge::transferLedData(LEDStrip *ledStrip, unsigned index, unsigned length, uint8_t *redChannel, uint8_t *greenChannel, uint8_t *blueChannel) +{ + // we need that array size no matter how many leds will really be set + uint8_t _reds[MAX_NUM_LEDS_SETTABLE]; + uint8_t _greens[MAX_NUM_LEDS_SETTABLE]; + uint8_t _blues[MAX_NUM_LEDS_SETTABLE]; + + int status = E_INVALID_PARAMETER; + unsigned i; + unsigned int copyLength; + + if (index >= 0 && length > 0 && index < length && length <= MAX_NUM_LEDS) + { + for (i = index; i < length; i += MAX_NUM_LEDS_SETTABLE) + { + copyLength = (i + MAX_NUM_LEDS_SETTABLE > length) ? length - i : MAX_NUM_LEDS_SETTABLE; + + memcpy(_reds, redChannel + i, copyLength * sizeof(uint8_t)); + memcpy(_greens, greenChannel + i, copyLength * sizeof(uint8_t)); + memcpy(_blues, blueChannel + i, copyLength * sizeof(uint8_t)); + + status = led_strip_set_rgb_values(ledStrip, i, copyLength, _reds, _greens, _blues); + + if (status != E_OK) + { + std::cerr << "Setting led values failed with status " << status << std::endl; + break; + } + } + } + return status; +} diff --git a/libsrc/leddevice/LedDeviceTinkerforge.h b/libsrc/leddevice/LedDeviceTinkerforge.h new file mode 100644 index 00000000..95f43332 --- /dev/null +++ b/libsrc/leddevice/LedDeviceTinkerforge.h @@ -0,0 +1,82 @@ + +#pragma once + +// STL includes +#include + +// Hyperion-Leddevice includes +#include + + +extern "C" { + #include + #include +} + +class LedDeviceTinkerforge : public LedDevice +{ +public: + + LedDeviceTinkerforge(const std::string &host, uint16_t port, const std::string &uid, const unsigned interval); + + virtual ~LedDeviceTinkerforge(); + + /// + /// Attempts to open a connection to the master bricklet and the led strip bricklet. + /// + /// @return Zero on succes else negative + /// + int open(); + + /// + /// Writes the colors to the led strip bricklet + /// + /// @param ledValues The color value for each led + /// + /// @return Zero on success else negative + /// + virtual int write(const std::vector &ledValues); + + /// + /// Switches off the leds + /// + /// @return Zero on success else negative + /// + virtual int switchOff(); + +private: + /// + /// Writes the data to the led strip blicklet + int transferLedData(LEDStrip *ledstrip, unsigned int index, unsigned int length, uint8_t *redChannel, uint8_t *greenChannel, uint8_t *blueChannel); + + /// The host of the master brick + const std::string _host; + + /// The port of the master brick + const uint16_t _port; + + /// The uid of the led strip bricklet + const std::string _uid; + + /// The interval/rate + const unsigned _interval; + + /// ip connection handle + IPConnection *_ipConnection; + + /// led strip handle + LEDStrip *_ledStrip; + + /// buffer for red channel led data + std::vector _redChannel; + + /// buffer for red channel led data + std::vector _greenChannel; + + /// buffer for red channel led data + std::vector _blueChannel; + + /// buffer size of the color channels + unsigned int _colorChannelSize; + +}; From d990afb89be140a557e832648abf259e630d61e6 Mon Sep 17 00:00:00 2001 From: Bjoern Bilger Date: Tue, 4 Mar 2014 20:52:23 +0100 Subject: [PATCH 16/78] added cmake finder Former-commit-id: b71cf3d7d70c9a79fbfb7c62ec737abdadcc06fd --- cmake/Findlibtinkerforge-1.0.cmake | 96 ++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 cmake/Findlibtinkerforge-1.0.cmake diff --git a/cmake/Findlibtinkerforge-1.0.cmake b/cmake/Findlibtinkerforge-1.0.cmake new file mode 100644 index 00000000..451441d7 --- /dev/null +++ b/cmake/Findlibtinkerforge-1.0.cmake @@ -0,0 +1,96 @@ +# - Try to find libtinkerforge-1.0 +# Once done this will define +# +# LIBTINKERFORGE_1_FOUND - system has libtinkerforge +# LIBTINKERFORGE_1_INCLUDE_DIRS - the libtinkerforge include directory +# LIBTINKERFORGE_1_LIBRARIES - Link these to use libtinkerforge +# LIBTINKERFORGE_1_DEFINITIONS - Compiler switches required for using libtinkerforge +# +# Adapted from cmake-modules Google Code project +# +# Copyright (c) 2006 Andreas Schneider +# +# (Changes for libtinkerforge) Copyright (c) 2014 Björn Bilger +# +# Redistribution and use is allowed according to the terms of the New BSD license. +# +# CMake-Modules Project New BSD License +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the CMake-Modules Project nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + + +if (LIBTINKERFORGE_1_LIBRARIES AND LIBTINKERFORGE_1_INCLUDE_DIRS) + # in cache already + set(LIBTINKERFORGE_FOUND TRUE) +else (LIBTINKERFORGE_1_LIBRARIES AND LIBTINKERFORGE_1_INCLUDE_DIRS) + find_path(LIBTINKERFORGE_1_INCLUDE_DIR + NAMES + tinkerforge/tinkerforge.h + PATHS + /usr/include + /usr/local/include + /opt/local/include + /sw/include + PATH_SUFFIXES + tinkerforge-1.0 + ) + + find_library(LIBTINKERFORGE_1_LIBRARY + NAMES + tinkerforge-1.0 + PATHS + /usr/lib + /usr/local/lib + /opt/local/lib + /sw/lib + ) + + set(LIBTINKERFORGE_1_INCLUDE_DIRS + ${LIBTINKERFORGE_1_INCLUDE_DIR} + ) + set(LIBTINKERFORGE_1_LIBRARIES + ${LIBTINKERFORGE_1_LIBRARY} +) + + if (LIBTINKERFORGE_1_INCLUDE_DIRS AND LIBTINKERFORGE_1_LIBRARIES) + set(LIBTINKERFORGE_1_FOUND TRUE) + endif (LIBTINKERFORGE_1_INCLUDE_DIRS AND LIBTINKERFORGE_1_LIBRARIES) + + if (LIBTINKERFORGE_1_FOUND) + if (NOT libtinkerforge_1_FIND_QUIETLY) + message(STATUS "Found libtinkerforge-1.0:") + message(STATUS " - Includes: ${LIBTINKERFORGE_1_INCLUDE_DIRS}") + message(STATUS " - Libraries: ${LIBTINKERFORGE_1_LIBRARIES}") + endif (NOT libtinkerforge_1_FIND_QUIETLY) + else (LIBTINKERFORGE_1_FOUND) + message(FATAL_ERROR "Could not find libtinkerforge") + endif (LIBTINKERFORGE_1_FOUND) + + # show the LIBTINKERFORGE_1_INCLUDE_DIRS and LIBTINKERFORGE_1_LIBRARIES variables only in the advanced view + mark_as_advanced(LIBTINKERFORGE_1_INCLUDE_DIRS LIBTINKERFORGE_1_LIBRARIES) + +endif (LIBTINKERFORGE_1_LIBRARIES AND LIBTINKERFORGE_1_INCLUDE_DIRS) From 64978201fba5bcf96c5988f7dce50eb8ffbdbdc2 Mon Sep 17 00:00:00 2001 From: Bjoern Bilger Date: Tue, 4 Mar 2014 20:57:25 +0100 Subject: [PATCH 17/78] add cmake finder Former-commit-id: 517a1ffc548d2de1eab5766071eeddb061f3c63a --- cmake/Findlibtinkerforge-1.0.cmake | 96 ++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 cmake/Findlibtinkerforge-1.0.cmake diff --git a/cmake/Findlibtinkerforge-1.0.cmake b/cmake/Findlibtinkerforge-1.0.cmake new file mode 100644 index 00000000..451441d7 --- /dev/null +++ b/cmake/Findlibtinkerforge-1.0.cmake @@ -0,0 +1,96 @@ +# - Try to find libtinkerforge-1.0 +# Once done this will define +# +# LIBTINKERFORGE_1_FOUND - system has libtinkerforge +# LIBTINKERFORGE_1_INCLUDE_DIRS - the libtinkerforge include directory +# LIBTINKERFORGE_1_LIBRARIES - Link these to use libtinkerforge +# LIBTINKERFORGE_1_DEFINITIONS - Compiler switches required for using libtinkerforge +# +# Adapted from cmake-modules Google Code project +# +# Copyright (c) 2006 Andreas Schneider +# +# (Changes for libtinkerforge) Copyright (c) 2014 Björn Bilger +# +# Redistribution and use is allowed according to the terms of the New BSD license. +# +# CMake-Modules Project New BSD License +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the CMake-Modules Project nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + + +if (LIBTINKERFORGE_1_LIBRARIES AND LIBTINKERFORGE_1_INCLUDE_DIRS) + # in cache already + set(LIBTINKERFORGE_FOUND TRUE) +else (LIBTINKERFORGE_1_LIBRARIES AND LIBTINKERFORGE_1_INCLUDE_DIRS) + find_path(LIBTINKERFORGE_1_INCLUDE_DIR + NAMES + tinkerforge/tinkerforge.h + PATHS + /usr/include + /usr/local/include + /opt/local/include + /sw/include + PATH_SUFFIXES + tinkerforge-1.0 + ) + + find_library(LIBTINKERFORGE_1_LIBRARY + NAMES + tinkerforge-1.0 + PATHS + /usr/lib + /usr/local/lib + /opt/local/lib + /sw/lib + ) + + set(LIBTINKERFORGE_1_INCLUDE_DIRS + ${LIBTINKERFORGE_1_INCLUDE_DIR} + ) + set(LIBTINKERFORGE_1_LIBRARIES + ${LIBTINKERFORGE_1_LIBRARY} +) + + if (LIBTINKERFORGE_1_INCLUDE_DIRS AND LIBTINKERFORGE_1_LIBRARIES) + set(LIBTINKERFORGE_1_FOUND TRUE) + endif (LIBTINKERFORGE_1_INCLUDE_DIRS AND LIBTINKERFORGE_1_LIBRARIES) + + if (LIBTINKERFORGE_1_FOUND) + if (NOT libtinkerforge_1_FIND_QUIETLY) + message(STATUS "Found libtinkerforge-1.0:") + message(STATUS " - Includes: ${LIBTINKERFORGE_1_INCLUDE_DIRS}") + message(STATUS " - Libraries: ${LIBTINKERFORGE_1_LIBRARIES}") + endif (NOT libtinkerforge_1_FIND_QUIETLY) + else (LIBTINKERFORGE_1_FOUND) + message(FATAL_ERROR "Could not find libtinkerforge") + endif (LIBTINKERFORGE_1_FOUND) + + # show the LIBTINKERFORGE_1_INCLUDE_DIRS and LIBTINKERFORGE_1_LIBRARIES variables only in the advanced view + mark_as_advanced(LIBTINKERFORGE_1_INCLUDE_DIRS LIBTINKERFORGE_1_LIBRARIES) + +endif (LIBTINKERFORGE_1_LIBRARIES AND LIBTINKERFORGE_1_INCLUDE_DIRS) From 5e3cb497fa350da20aa6dde035cade5f67d323c1 Mon Sep 17 00:00:00 2001 From: johan Date: Tue, 4 Mar 2014 22:04:15 +0100 Subject: [PATCH 18/78] Add support to set the threshold for each RGB channel separately Former-commit-id: 5edb206bb2657e78f711f67625fd5f6164d8296c --- include/grabber/V4L2Grabber.h | 12 +++++++-- include/grabber/V4L2Wrapper.h | 3 +++ include/utils/ColorRgb.h | 7 +++++ libsrc/grabber/v4l2/V4L2Grabber.cpp | 40 ++++++++++++++++++++++++++--- libsrc/grabber/v4l2/V4L2Wrapper.cpp | 26 ++++++++++++------- src/hyperion-v4l2/ImageHandler.cpp | 26 +++---------------- src/hyperion-v4l2/ImageHandler.h | 11 +------- src/hyperion-v4l2/hyperion-v4l2.cpp | 12 ++++++++- src/hyperiond/hyperiond.cpp | 3 +++ 9 files changed, 92 insertions(+), 48 deletions(-) diff --git a/include/grabber/V4L2Grabber.h b/include/grabber/V4L2Grabber.h index c2f61f16..ff600108 100644 --- a/include/grabber/V4L2Grabber.h +++ b/include/grabber/V4L2Grabber.h @@ -24,8 +24,7 @@ class V4L2Grabber : public QObject Q_OBJECT public: - V4L2Grabber( - const std::string & device, + V4L2Grabber(const std::string & device, int input, VideoStandard videoStandard, int width, @@ -43,6 +42,11 @@ public slots: void set3D(VideoMode mode); + void setSignalThreshold(double redSignalThreshold, + double greenSignalThreshold, + double blueSignalThreshold, + int noSignalCounterThreshold); + void start(); void stop(); @@ -110,10 +114,14 @@ private: int _frameDecimation; int _horizontalPixelDecimation; int _verticalPixelDecimation; + int _noSignalCounterThreshold; + + ColorRgb _noSignalThresholdColor; VideoMode _mode3D; int _currentFrame; + int _noSignalCounter; QSocketNotifier * _streamNotifier; }; diff --git a/include/grabber/V4L2Wrapper.h b/include/grabber/V4L2Wrapper.h index ad6ca247..99159186 100644 --- a/include/grabber/V4L2Wrapper.h +++ b/include/grabber/V4L2Wrapper.h @@ -19,6 +19,9 @@ public: int height, int frameDecimation, int pixelDecimation, + double redSignalThreshold, + double greenSignalThreshold, + double blueSignalThreshold, Hyperion * hyperion, int hyperionPriority); virtual ~V4L2Wrapper(); diff --git a/include/utils/ColorRgb.h b/include/utils/ColorRgb.h index c578d69f..ea544fe0 100644 --- a/include/utils/ColorRgb.h +++ b/include/utils/ColorRgb.h @@ -48,3 +48,10 @@ inline std::ostream& operator<<(std::ostream& os, const ColorRgb& color) os << "{" << unsigned(color.red) << "," << unsigned(color.green) << "," << unsigned(color.blue) << "}"; return os; } + + +/// Compare operator to check if a color is 'smaller' than another color +inline bool operator<(const ColorRgb & lhs, const ColorRgb & rhs) +{ + return (lhs.red < rhs.red) && (lhs.green < rhs.green) && (lhs.blue < rhs.blue); +} diff --git a/libsrc/grabber/v4l2/V4L2Grabber.cpp b/libsrc/grabber/v4l2/V4L2Grabber.cpp index dc0e47c6..2e4bf67d 100644 --- a/libsrc/grabber/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/v4l2/V4L2Grabber.cpp @@ -36,8 +36,7 @@ static void yuv2rgb(uint8_t y, uint8_t u, uint8_t v, uint8_t & r, uint8_t & g, u } -V4L2Grabber::V4L2Grabber( - const std::string & device, +V4L2Grabber::V4L2Grabber(const std::string & device, int input, VideoStandard videoStandard, int width, @@ -59,8 +58,11 @@ V4L2Grabber::V4L2Grabber( _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(); @@ -91,6 +93,14 @@ void V4L2Grabber::set3D(VideoMode 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); +} + void V4L2Grabber::start() { _streamNotifier->setEnabled(true); @@ -658,6 +668,8 @@ void V4L2Grabber::process_image(const uint8_t * data) int outputHeight = (height - _cropTop - _cropBottom + _verticalPixelDecimation/2) / _verticalPixelDecimation; Image image(outputWidth, outputHeight); + bool noSignal = true; + 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) @@ -683,10 +695,32 @@ void V4L2Grabber::process_image(const uint8_t * data) ColorRgb & rgb = image(xDest, yDest); yuv2rgb(y, u, v, rgb.red, rgb.green, rgb.blue); + noSignal &= rgb < _noSignalThresholdColor; } } - emit newFrame(image); + 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) diff --git a/libsrc/grabber/v4l2/V4L2Wrapper.cpp b/libsrc/grabber/v4l2/V4L2Wrapper.cpp index f859c05e..9a8f450d 100644 --- a/libsrc/grabber/v4l2/V4L2Wrapper.cpp +++ b/libsrc/grabber/v4l2/V4L2Wrapper.cpp @@ -11,22 +11,32 @@ V4L2Wrapper::V4L2Wrapper(const std::string &device, int height, int frameDecimation, int pixelDecimation, + double redSignalThreshold, + double greenSignalThreshold, + double blueSignalThreshold, Hyperion *hyperion, int hyperionPriority) : _timeout_ms(1000), _priority(hyperionPriority), _grabber(device, - input, - videoStandard, - width, - height, - frameDecimation, - pixelDecimation, - pixelDecimation), + input, + videoStandard, + width, + height, + frameDecimation, + pixelDecimation, + pixelDecimation), _processor(ImageProcessorFactory::getInstance().newImageProcessor()), _hyperion(hyperion), _ledColors(hyperion->getLedCount(), ColorRgb{0,0,0}) { + // set the signal detection threshold of the grabber + _grabber.setSignalThreshold( + redSignalThreshold, + greenSignalThreshold, + blueSignalThreshold, + 50); + // register the image type qRegisterMetaType>("Image"); qRegisterMetaType>("std::vector"); @@ -71,8 +81,6 @@ void V4L2Wrapper::set3D(VideoMode mode) void V4L2Wrapper::newFrame(const Image &image) { - // TODO: add a signal detector - // process the new image _processor->process(image, _ledColors); diff --git a/src/hyperion-v4l2/ImageHandler.cpp b/src/hyperion-v4l2/ImageHandler.cpp index afe7419c..19a1da80 100644 --- a/src/hyperion-v4l2/ImageHandler.cpp +++ b/src/hyperion-v4l2/ImageHandler.cpp @@ -1,11 +1,9 @@ // hyperion-v4l2 includes #include "ImageHandler.h" -ImageHandler::ImageHandler(const std::string & address, int priority, double signalThreshold, bool skipProtoReply) : +ImageHandler::ImageHandler(const std::string & address, int priority, bool skipProtoReply) : _priority(priority), - _connection(address), - _signalThreshold(signalThreshold), - _signalProcessor(100, 50, 0, uint8_t(std::min(255, std::max(0, int(255*signalThreshold))))) + _connection(address) { _connection.setSkipReply(skipProtoReply); } @@ -16,23 +14,5 @@ ImageHandler::~ImageHandler() void ImageHandler::receiveImage(const Image & image) { - // check if we should do signal detection - if (_signalThreshold < 0) - { - _connection.setImage(image, _priority, 1000); - } - else - { - if (_signalProcessor.process(image)) - { - std::cout << "Signal state = " << (_signalProcessor.getCurrentBorder().unknown ? "off" : "on") << std::endl; - } - - // consider an unknown border as no signal - // send the image to Hyperion if we have a signal - if (!_signalProcessor.getCurrentBorder().unknown) - { - _connection.setImage(image, _priority, 1000); - } - } + _connection.setImage(image, _priority, 1000); } diff --git a/src/hyperion-v4l2/ImageHandler.h b/src/hyperion-v4l2/ImageHandler.h index 5658c750..8730989d 100644 --- a/src/hyperion-v4l2/ImageHandler.h +++ b/src/hyperion-v4l2/ImageHandler.h @@ -5,9 +5,6 @@ #include #include -// blackborder includes -#include - // hyperion v4l2 includes #include "ProtoConnection.h" @@ -17,7 +14,7 @@ class ImageHandler : public QObject Q_OBJECT public: - ImageHandler(const std::string & address, int priority, double signalThreshold, bool skipProtoReply); + ImageHandler(const std::string & address, int priority, bool skipProtoReply); virtual ~ImageHandler(); public slots: @@ -31,10 +28,4 @@ private: /// Hyperion proto connection object ProtoConnection _connection; - - /// Threshold used for signal detection - double _signalThreshold; - - /// Blackborder detector which is used as a signal detector (unknown border = no signal) - hyperion::BlackBorderProcessor _signalProcessor; }; diff --git a/src/hyperion-v4l2/hyperion-v4l2.cpp b/src/hyperion-v4l2/hyperion-v4l2.cpp index 04d5d5b4..611abe3f 100644 --- a/src/hyperion-v4l2/hyperion-v4l2.cpp +++ b/src/hyperion-v4l2/hyperion-v4l2.cpp @@ -63,6 +63,9 @@ int main(int argc, char** argv) 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]"); @@ -110,6 +113,13 @@ int main(int argc, char** argv) 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 cropping values grabber.setCropping( std::max(0, argCropLeft.getValue()), @@ -138,7 +148,7 @@ int main(int argc, char** argv) } else { - ImageHandler handler(argAddress.getValue(), argPriority.getValue(), argSignalThreshold.getValue(), argSkipReply.isSet()); + ImageHandler handler(argAddress.getValue(), argPriority.getValue(), argSkipReply.isSet()); QObject::connect(&grabber, SIGNAL(newFrame(Image)), &handler, SLOT(receiveImage(Image))); grabber.start(); QCoreApplication::exec(); diff --git a/src/hyperiond/hyperiond.cpp b/src/hyperiond/hyperiond.cpp index 71b01c0b..d31e9f07 100644 --- a/src/hyperiond/hyperiond.cpp +++ b/src/hyperiond/hyperiond.cpp @@ -184,6 +184,9 @@ int main(int argc, char** argv) grabberConfig.get("height", -1).asInt(), grabberConfig.get("frameDecimation", 2).asInt(), grabberConfig.get("sizeDecimation", 8).asInt(), + grabberConfig.get("redSignalThreshold", 0.0).asDouble(), + grabberConfig.get("greenSignalThreshold", 0.0).asDouble(), + grabberConfig.get("blueSignalThreshold", 0.0).asDouble(), &hyperion, grabberConfig.get("priority", 800).asInt()); v4l2Grabber->set3D(parse3DMode(grabberConfig.get("mode", "2D").asString())); From 0107aa7af60f1802db276137eb495940acad11a0 Mon Sep 17 00:00:00 2001 From: johan Date: Tue, 4 Mar 2014 22:05:34 +0100 Subject: [PATCH 19/78] Create new deploy Former-commit-id: d97adcdc347f3fdfb07e11761f3851d0bb8c1ed4 --- 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 913779f6..c4d18dcb 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -7a6fa32d667a0ad6e2bfd80c32fded0ecd0cdc04 \ No newline at end of file +720aa07ca87c27c2581fc6dcb4ac7f086aa163c4 \ No newline at end of file From dd0a18642becc998737a050498c0396fdbd16006 Mon Sep 17 00:00:00 2001 From: johan Date: Tue, 4 Mar 2014 23:22:46 +0100 Subject: [PATCH 20/78] Fix build Former-commit-id: 1b98924c9d9292b6661a5c9e3e17b06813348b54 --- include/utils/Image.h | 6 +++++- src/hyperiond/CMakeLists.txt | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/include/utils/Image.h b/include/utils/Image.h index ac06f087..5e8b45ad 100644 --- a/include/utils/Image.h +++ b/include/utils/Image.h @@ -17,8 +17,12 @@ public: /// Default constructor for an image /// Image() : - Image(1, 1) + _width(1), + _height(1), + _pixels(new Pixel_T[2]), + _endOfPixels(_pixels + 1) { + memset(_pixels, 0, 2*sizeof(Pixel_T)); } /// diff --git a/src/hyperiond/CMakeLists.txt b/src/hyperiond/CMakeLists.txt index 17b7464f..f843b15f 100644 --- a/src/hyperiond/CMakeLists.txt +++ b/src/hyperiond/CMakeLists.txt @@ -9,9 +9,12 @@ target_link_libraries(hyperiond jsonserver protoserver boblightserver - v4l2-grabber ) if (ENABLE_DISPMANX) target_link_libraries(hyperiond dispmanx-grabber) endif (ENABLE_DISPMANX) + +if (ENABLE_V4L2) + target_link_libraries(hyperiond v4l2-grabber) +endif (ENABLE_V4L2) From fdaa5c0068fa5ac899b7ad2b3b09c29c6bc5559b Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Thu, 6 Mar 2014 21:48:11 +0100 Subject: [PATCH 21/78] Added tinkerforge as local dependency Former-commit-id: b739eba0676d9c105416d9040ffbe78b2dc4bfbd --- .gitignore | 2 + CMakeLists.txt | 2 +- cmake/Findlibtinkerforge-1.0.cmake | 96 - dependencies/build/CMakeLists.txt | 5 +- dependencies/build/tinkerforge/CMakeLists.txt | 14 + .../build/tinkerforge/bricklet_led_strip.c | 373 +++ .../build/tinkerforge/ip_connection.c | 2013 +++++++++++++++++ .../include/tinkerforge/bricklet_led_strip.h | 301 +++ .../include/tinkerforge/ip_connection.h | 630 ++++++ .../tinkerforge_c_bindings_2_0_13.zip | Bin 0 -> 381221 bytes libsrc/leddevice/CMakeLists.txt | 14 +- libsrc/leddevice/LedDeviceTinkerforge.cpp | 100 +- 12 files changed, 3389 insertions(+), 161 deletions(-) delete mode 100644 cmake/Findlibtinkerforge-1.0.cmake create mode 100644 dependencies/build/tinkerforge/CMakeLists.txt create mode 100644 dependencies/build/tinkerforge/bricklet_led_strip.c create mode 100644 dependencies/build/tinkerforge/ip_connection.c create mode 100644 dependencies/include/tinkerforge/bricklet_led_strip.h create mode 100644 dependencies/include/tinkerforge/ip_connection.h create mode 100644 dependencies/tinkerforge_c_bindings_2_0_13.zip diff --git a/.gitignore b/.gitignore index 6a5b347a..550dcb07 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /*.user /build /build-x86 +.DS_Store + diff --git a/CMakeLists.txt b/CMakeLists.txt index 9903cbaf..e8a7f674 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,7 +66,7 @@ find_package(libusb-1.0 REQUIRED) find_package(Threads REQUIRED) if (ENABLE_TINKERFORGE) - find_package(libtinkerforge-1.0 REQUIRED) + #find_package(libtinkerforge-1.0 REQUIRED) endif (ENABLE_TINKERFORGE) include(${QT_USE_FILE}) diff --git a/cmake/Findlibtinkerforge-1.0.cmake b/cmake/Findlibtinkerforge-1.0.cmake deleted file mode 100644 index 451441d7..00000000 --- a/cmake/Findlibtinkerforge-1.0.cmake +++ /dev/null @@ -1,96 +0,0 @@ -# - Try to find libtinkerforge-1.0 -# Once done this will define -# -# LIBTINKERFORGE_1_FOUND - system has libtinkerforge -# LIBTINKERFORGE_1_INCLUDE_DIRS - the libtinkerforge include directory -# LIBTINKERFORGE_1_LIBRARIES - Link these to use libtinkerforge -# LIBTINKERFORGE_1_DEFINITIONS - Compiler switches required for using libtinkerforge -# -# Adapted from cmake-modules Google Code project -# -# Copyright (c) 2006 Andreas Schneider -# -# (Changes for libtinkerforge) Copyright (c) 2014 Björn Bilger -# -# Redistribution and use is allowed according to the terms of the New BSD license. -# -# CMake-Modules Project New BSD License -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# * Neither the name of the CMake-Modules Project nor the names of its -# contributors may be used to endorse or promote products derived from this -# software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# - - -if (LIBTINKERFORGE_1_LIBRARIES AND LIBTINKERFORGE_1_INCLUDE_DIRS) - # in cache already - set(LIBTINKERFORGE_FOUND TRUE) -else (LIBTINKERFORGE_1_LIBRARIES AND LIBTINKERFORGE_1_INCLUDE_DIRS) - find_path(LIBTINKERFORGE_1_INCLUDE_DIR - NAMES - tinkerforge/tinkerforge.h - PATHS - /usr/include - /usr/local/include - /opt/local/include - /sw/include - PATH_SUFFIXES - tinkerforge-1.0 - ) - - find_library(LIBTINKERFORGE_1_LIBRARY - NAMES - tinkerforge-1.0 - PATHS - /usr/lib - /usr/local/lib - /opt/local/lib - /sw/lib - ) - - set(LIBTINKERFORGE_1_INCLUDE_DIRS - ${LIBTINKERFORGE_1_INCLUDE_DIR} - ) - set(LIBTINKERFORGE_1_LIBRARIES - ${LIBTINKERFORGE_1_LIBRARY} -) - - if (LIBTINKERFORGE_1_INCLUDE_DIRS AND LIBTINKERFORGE_1_LIBRARIES) - set(LIBTINKERFORGE_1_FOUND TRUE) - endif (LIBTINKERFORGE_1_INCLUDE_DIRS AND LIBTINKERFORGE_1_LIBRARIES) - - if (LIBTINKERFORGE_1_FOUND) - if (NOT libtinkerforge_1_FIND_QUIETLY) - message(STATUS "Found libtinkerforge-1.0:") - message(STATUS " - Includes: ${LIBTINKERFORGE_1_INCLUDE_DIRS}") - message(STATUS " - Libraries: ${LIBTINKERFORGE_1_LIBRARIES}") - endif (NOT libtinkerforge_1_FIND_QUIETLY) - else (LIBTINKERFORGE_1_FOUND) - message(FATAL_ERROR "Could not find libtinkerforge") - endif (LIBTINKERFORGE_1_FOUND) - - # show the LIBTINKERFORGE_1_INCLUDE_DIRS and LIBTINKERFORGE_1_LIBRARIES variables only in the advanced view - mark_as_advanced(LIBTINKERFORGE_1_INCLUDE_DIRS LIBTINKERFORGE_1_LIBRARIES) - -endif (LIBTINKERFORGE_1_LIBRARIES AND LIBTINKERFORGE_1_INCLUDE_DIRS) diff --git a/dependencies/build/CMakeLists.txt b/dependencies/build/CMakeLists.txt index 88f6ca5f..e931ea79 100644 --- a/dependencies/build/CMakeLists.txt +++ b/dependencies/build/CMakeLists.txt @@ -1,5 +1,6 @@ -add_subdirectory(jsoncpp) add_subdirectory(getoptPlusPlus) -add_subdirectory(serial) add_subdirectory(hidapi) +add_subdirectory(jsoncpp) +add_subdirectory(serial) +add_subdirectory(tinkerforge) diff --git a/dependencies/build/tinkerforge/CMakeLists.txt b/dependencies/build/tinkerforge/CMakeLists.txt new file mode 100644 index 00000000..f1f82f5e --- /dev/null +++ b/dependencies/build/tinkerforge/CMakeLists.txt @@ -0,0 +1,14 @@ +project(tinkerforge) + +# define the current source/header path +set(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/dependencies/include/tinkerforge) +set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/dependencies/build/tinkerforge) + +include_directories(${CURRENT_HEADER_DIR}) + +add_library(tinkerforge + ${CURRENT_HEADER_DIR}/bricklet_led_strip.h + ${CURRENT_HEADER_DIR}/ip_connection.h + + ${CURRENT_SOURCE_DIR}/bricklet_led_strip.c + ${CURRENT_SOURCE_DIR}/ip_connection.c) diff --git a/dependencies/build/tinkerforge/bricklet_led_strip.c b/dependencies/build/tinkerforge/bricklet_led_strip.c new file mode 100644 index 00000000..a67486fb --- /dev/null +++ b/dependencies/build/tinkerforge/bricklet_led_strip.c @@ -0,0 +1,373 @@ +/* *********************************************************** + * This file was automatically generated on 2013-12-19. * + * * + * Bindings Version 2.0.13 * + * * + * If you have a bugfix for this file and want to commit it, * + * please fix the bug in the generator. You can find a link * + * to the generator git on tinkerforge.com * + *************************************************************/ + + +#define IPCON_EXPOSE_INTERNALS + +#include "bricklet_led_strip.h" + +#include + + + +typedef void (*FrameRenderedCallbackFunction)(uint16_t, void *); + +#if defined _MSC_VER || defined __BORLANDC__ + #pragma pack(push) + #pragma pack(1) + #define ATTRIBUTE_PACKED +#elif defined __GNUC__ + #ifdef _WIN32 + // workaround struct packing bug in GCC 4.7 on Windows + // http://gcc.gnu.org/bugzilla/show_bug.cgi?id=52991 + #define ATTRIBUTE_PACKED __attribute__((gcc_struct, packed)) + #else + #define ATTRIBUTE_PACKED __attribute__((packed)) + #endif +#else + #error unknown compiler, do not know how to enable struct packing +#endif + +typedef struct { + PacketHeader header; + uint16_t index; + uint8_t length; + uint8_t r[16]; + uint8_t g[16]; + uint8_t b[16]; +} ATTRIBUTE_PACKED SetRGBValues_; + +typedef struct { + PacketHeader header; + uint16_t index; + uint8_t length; +} ATTRIBUTE_PACKED GetRGBValues_; + +typedef struct { + PacketHeader header; + uint8_t r[16]; + uint8_t g[16]; + uint8_t b[16]; +} ATTRIBUTE_PACKED GetRGBValuesResponse_; + +typedef struct { + PacketHeader header; + uint16_t duration; +} ATTRIBUTE_PACKED SetFrameDuration_; + +typedef struct { + PacketHeader header; +} ATTRIBUTE_PACKED GetFrameDuration_; + +typedef struct { + PacketHeader header; + uint16_t duration; +} ATTRIBUTE_PACKED GetFrameDurationResponse_; + +typedef struct { + PacketHeader header; +} ATTRIBUTE_PACKED GetSupplyVoltage_; + +typedef struct { + PacketHeader header; + uint16_t voltage; +} ATTRIBUTE_PACKED GetSupplyVoltageResponse_; + +typedef struct { + PacketHeader header; + uint16_t length; +} ATTRIBUTE_PACKED FrameRenderedCallback_; + +typedef struct { + PacketHeader header; + uint32_t frequency; +} ATTRIBUTE_PACKED SetClockFrequency_; + +typedef struct { + PacketHeader header; +} ATTRIBUTE_PACKED GetClockFrequency_; + +typedef struct { + PacketHeader header; + uint32_t frequency; +} ATTRIBUTE_PACKED GetClockFrequencyResponse_; + +typedef struct { + PacketHeader header; +} ATTRIBUTE_PACKED GetIdentity_; + +typedef struct { + PacketHeader header; + char uid[8]; + char connected_uid[8]; + char position; + uint8_t hardware_version[3]; + uint8_t firmware_version[3]; + uint16_t device_identifier; +} ATTRIBUTE_PACKED GetIdentityResponse_; + +#if defined _MSC_VER || defined __BORLANDC__ + #pragma pack(pop) +#endif +#undef ATTRIBUTE_PACKED + +static void led_strip_callback_wrapper_frame_rendered(DevicePrivate *device_p, Packet *packet) { + FrameRenderedCallbackFunction callback_function; + void *user_data = device_p->registered_callback_user_data[LED_STRIP_CALLBACK_FRAME_RENDERED]; + FrameRenderedCallback_ *callback = (FrameRenderedCallback_ *)packet; + *(void **)(&callback_function) = device_p->registered_callbacks[LED_STRIP_CALLBACK_FRAME_RENDERED]; + + if (callback_function == NULL) { + return; + } + + callback->length = leconvert_uint16_from(callback->length); + + callback_function(callback->length, user_data); +} + +void led_strip_create(LEDStrip *led_strip, const char *uid, IPConnection *ipcon) { + DevicePrivate *device_p; + + device_create(led_strip, uid, ipcon->p, 2, 0, 1); + + device_p = led_strip->p; + + device_p->response_expected[LED_STRIP_FUNCTION_SET_RGB_VALUES] = DEVICE_RESPONSE_EXPECTED_FALSE; + device_p->response_expected[LED_STRIP_FUNCTION_GET_RGB_VALUES] = DEVICE_RESPONSE_EXPECTED_ALWAYS_TRUE; + device_p->response_expected[LED_STRIP_FUNCTION_SET_FRAME_DURATION] = DEVICE_RESPONSE_EXPECTED_FALSE; + device_p->response_expected[LED_STRIP_FUNCTION_GET_FRAME_DURATION] = DEVICE_RESPONSE_EXPECTED_ALWAYS_TRUE; + device_p->response_expected[LED_STRIP_FUNCTION_GET_SUPPLY_VOLTAGE] = DEVICE_RESPONSE_EXPECTED_ALWAYS_TRUE; + device_p->response_expected[LED_STRIP_CALLBACK_FRAME_RENDERED] = DEVICE_RESPONSE_EXPECTED_ALWAYS_FALSE; + device_p->response_expected[LED_STRIP_FUNCTION_SET_CLOCK_FREQUENCY] = DEVICE_RESPONSE_EXPECTED_FALSE; + device_p->response_expected[LED_STRIP_FUNCTION_GET_CLOCK_FREQUENCY] = DEVICE_RESPONSE_EXPECTED_ALWAYS_TRUE; + device_p->response_expected[LED_STRIP_FUNCTION_GET_IDENTITY] = DEVICE_RESPONSE_EXPECTED_ALWAYS_TRUE; + + device_p->callback_wrappers[LED_STRIP_CALLBACK_FRAME_RENDERED] = led_strip_callback_wrapper_frame_rendered; +} + +void led_strip_destroy(LEDStrip *led_strip) { + device_destroy(led_strip); +} + +int led_strip_get_response_expected(LEDStrip *led_strip, uint8_t function_id, bool *ret_response_expected) { + return device_get_response_expected(led_strip->p, function_id, ret_response_expected); +} + +int led_strip_set_response_expected(LEDStrip *led_strip, uint8_t function_id, bool response_expected) { + return device_set_response_expected(led_strip->p, function_id, response_expected); +} + +int led_strip_set_response_expected_all(LEDStrip *led_strip, bool response_expected) { + return device_set_response_expected_all(led_strip->p, response_expected); +} + +void led_strip_register_callback(LEDStrip *led_strip, uint8_t id, void *callback, void *user_data) { + device_register_callback(led_strip->p, id, callback, user_data); +} + +int led_strip_get_api_version(LEDStrip *led_strip, uint8_t ret_api_version[3]) { + return device_get_api_version(led_strip->p, ret_api_version); +} + +int led_strip_set_rgb_values(LEDStrip *led_strip, uint16_t index, uint8_t length, uint8_t r[16], uint8_t g[16], uint8_t b[16]) { + DevicePrivate *device_p = led_strip->p; + SetRGBValues_ request; + int ret; + + ret = packet_header_create(&request.header, sizeof(request), LED_STRIP_FUNCTION_SET_RGB_VALUES, device_p->ipcon_p, device_p); + + if (ret < 0) { + return ret; + } + + request.index = leconvert_uint16_to(index); + request.length = length; + memcpy(request.r, r, 16 * sizeof(uint8_t)); + memcpy(request.g, g, 16 * sizeof(uint8_t)); + memcpy(request.b, b, 16 * sizeof(uint8_t)); + + ret = device_send_request(device_p, (Packet *)&request, NULL); + + + return ret; +} + +int led_strip_get_rgb_values(LEDStrip *led_strip, uint16_t index, uint8_t length, uint8_t ret_r[16], uint8_t ret_g[16], uint8_t ret_b[16]) { + DevicePrivate *device_p = led_strip->p; + GetRGBValues_ request; + GetRGBValuesResponse_ response; + int ret; + + ret = packet_header_create(&request.header, sizeof(request), LED_STRIP_FUNCTION_GET_RGB_VALUES, device_p->ipcon_p, device_p); + + if (ret < 0) { + return ret; + } + + request.index = leconvert_uint16_to(index); + request.length = length; + + ret = device_send_request(device_p, (Packet *)&request, (Packet *)&response); + + if (ret < 0) { + return ret; + } + memcpy(ret_r, response.r, 16 * sizeof(uint8_t)); + memcpy(ret_g, response.g, 16 * sizeof(uint8_t)); + memcpy(ret_b, response.b, 16 * sizeof(uint8_t)); + + + + return ret; +} + +int led_strip_set_frame_duration(LEDStrip *led_strip, uint16_t duration) { + DevicePrivate *device_p = led_strip->p; + SetFrameDuration_ request; + int ret; + + ret = packet_header_create(&request.header, sizeof(request), LED_STRIP_FUNCTION_SET_FRAME_DURATION, device_p->ipcon_p, device_p); + + if (ret < 0) { + return ret; + } + + request.duration = leconvert_uint16_to(duration); + + ret = device_send_request(device_p, (Packet *)&request, NULL); + + + return ret; +} + +int led_strip_get_frame_duration(LEDStrip *led_strip, uint16_t *ret_duration) { + DevicePrivate *device_p = led_strip->p; + GetFrameDuration_ request; + GetFrameDurationResponse_ response; + int ret; + + ret = packet_header_create(&request.header, sizeof(request), LED_STRIP_FUNCTION_GET_FRAME_DURATION, device_p->ipcon_p, device_p); + + if (ret < 0) { + return ret; + } + + + ret = device_send_request(device_p, (Packet *)&request, (Packet *)&response); + + if (ret < 0) { + return ret; + } + *ret_duration = leconvert_uint16_from(response.duration); + + + + return ret; +} + +int led_strip_get_supply_voltage(LEDStrip *led_strip, uint16_t *ret_voltage) { + DevicePrivate *device_p = led_strip->p; + GetSupplyVoltage_ request; + GetSupplyVoltageResponse_ response; + int ret; + + ret = packet_header_create(&request.header, sizeof(request), LED_STRIP_FUNCTION_GET_SUPPLY_VOLTAGE, device_p->ipcon_p, device_p); + + if (ret < 0) { + return ret; + } + + + ret = device_send_request(device_p, (Packet *)&request, (Packet *)&response); + + if (ret < 0) { + return ret; + } + *ret_voltage = leconvert_uint16_from(response.voltage); + + + + return ret; +} + +int led_strip_set_clock_frequency(LEDStrip *led_strip, uint32_t frequency) { + DevicePrivate *device_p = led_strip->p; + SetClockFrequency_ request; + int ret; + + ret = packet_header_create(&request.header, sizeof(request), LED_STRIP_FUNCTION_SET_CLOCK_FREQUENCY, device_p->ipcon_p, device_p); + + if (ret < 0) { + return ret; + } + + request.frequency = leconvert_uint32_to(frequency); + + ret = device_send_request(device_p, (Packet *)&request, NULL); + + + return ret; +} + +int led_strip_get_clock_frequency(LEDStrip *led_strip, uint32_t *ret_frequency) { + DevicePrivate *device_p = led_strip->p; + GetClockFrequency_ request; + GetClockFrequencyResponse_ response; + int ret; + + ret = packet_header_create(&request.header, sizeof(request), LED_STRIP_FUNCTION_GET_CLOCK_FREQUENCY, device_p->ipcon_p, device_p); + + if (ret < 0) { + return ret; + } + + + ret = device_send_request(device_p, (Packet *)&request, (Packet *)&response); + + if (ret < 0) { + return ret; + } + *ret_frequency = leconvert_uint32_from(response.frequency); + + + + return ret; +} + +int led_strip_get_identity(LEDStrip *led_strip, char ret_uid[8], char ret_connected_uid[8], char *ret_position, uint8_t ret_hardware_version[3], uint8_t ret_firmware_version[3], uint16_t *ret_device_identifier) { + DevicePrivate *device_p = led_strip->p; + GetIdentity_ request; + GetIdentityResponse_ response; + int ret; + + ret = packet_header_create(&request.header, sizeof(request), LED_STRIP_FUNCTION_GET_IDENTITY, device_p->ipcon_p, device_p); + + if (ret < 0) { + return ret; + } + + + ret = device_send_request(device_p, (Packet *)&request, (Packet *)&response); + + if (ret < 0) { + return ret; + } + strncpy(ret_uid, response.uid, 8); + strncpy(ret_connected_uid, response.connected_uid, 8); + *ret_position = response.position; + memcpy(ret_hardware_version, response.hardware_version, 3 * sizeof(uint8_t)); + memcpy(ret_firmware_version, response.firmware_version, 3 * sizeof(uint8_t)); + *ret_device_identifier = leconvert_uint16_from(response.device_identifier); + + + + return ret; +} diff --git a/dependencies/build/tinkerforge/ip_connection.c b/dependencies/build/tinkerforge/ip_connection.c new file mode 100644 index 00000000..31cf4aee --- /dev/null +++ b/dependencies/build/tinkerforge/ip_connection.c @@ -0,0 +1,2013 @@ +/* + * Copyright (C) 2012-2013 Matthias Bolte + * Copyright (C) 2011 Olaf Lüke + * + * Redistribution and use in source and binary forms of this file, + * with or without modification, are permitted. + */ + +#ifndef _WIN32 + #define _BSD_SOURCE // for usleep from unistd.h +#endif + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 + #include +#else + #include + #include + #include // gettimeofday + #include // connect + #include + #include // TCP_NO_DELAY + #include // gethostbyname + #include // struct sockaddr_in +#endif + +#define IPCON_EXPOSE_INTERNALS + +#include "ip_connection.h" + +#if defined _MSC_VER || defined __BORLANDC__ + #pragma pack(push) + #pragma pack(1) + #define ATTRIBUTE_PACKED +#elif defined __GNUC__ + #ifdef _WIN32 + // workaround struct packing bug in GCC 4.7 on Windows + // http://gcc.gnu.org/bugzilla/show_bug.cgi?id=52991 + #define ATTRIBUTE_PACKED __attribute__((gcc_struct, packed)) + #else + #define ATTRIBUTE_PACKED __attribute__((packed)) + #endif +#else + #error unknown compiler, do not know how to enable struct packing +#endif + +typedef struct { + PacketHeader header; +} ATTRIBUTE_PACKED Enumerate; + +typedef struct { + PacketHeader header; + char uid[8]; + char connected_uid[8]; + char position; + uint8_t hardware_version[3]; + uint8_t firmware_version[3]; + uint16_t device_identifier; + uint8_t enumeration_type; +} ATTRIBUTE_PACKED EnumerateCallback; + +#if defined _MSC_VER || defined __BORLANDC__ + #pragma pack(pop) +#endif +#undef ATTRIBUTE_PACKED + +#ifndef __cplusplus + #ifdef __GNUC__ + #ifndef __GNUC_PREREQ + #define __GNUC_PREREQ(major, minor) \ + ((((__GNUC__) << 16) + (__GNUC_MINOR__)) >= (((major) << 16) + (minor))) + #endif + #if __GNUC_PREREQ(4, 6) + #define STATIC_ASSERT(condition, message) \ + _Static_assert(condition, message) + #else + #define STATIC_ASSERT(condition, message) // FIXME + #endif + #else + #define STATIC_ASSERT(condition, message) // FIXME + #endif + + STATIC_ASSERT(sizeof(PacketHeader) == 8, "PacketHeader has invalid size"); + STATIC_ASSERT(sizeof(Packet) == 80, "Packet has invalid size"); + STATIC_ASSERT(sizeof(EnumerateCallback) == 34, "EnumerateCallback has invalid size"); +#endif + +/***************************************************************************** + * + * BASE58 + * + *****************************************************************************/ + +#define BASE58_MAX_STR_SIZE 13 + +static const char BASE58_ALPHABET[] = \ + "123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"; + +#if 0 +static void base58_encode(uint64_t value, char *str) { + uint32_t mod; + char reverse_str[BASE58_MAX_STR_SIZE] = {'\0'}; + int i = 0; + int k = 0; + + while (value >= 58) { + mod = value % 58; + reverse_str[i] = BASE58_ALPHABET[mod]; + value = value / 58; + ++i; + } + + reverse_str[i] = BASE58_ALPHABET[value]; + + for (k = 0; k <= i; k++) { + str[k] = reverse_str[i - k]; + } + + for (; k < BASE58_MAX_STR_SIZE; k++) { + str[k] = '\0'; + } +} +#endif + +static uint64_t base58_decode(const char *str) { + int i; + int k; + uint64_t value = 0; + uint64_t base = 1; + + for (i = 0; i < BASE58_MAX_STR_SIZE; i++) { + if (str[i] == '\0') { + break; + } + } + + --i; + + for (; i >= 0; i--) { + if (str[i] == '\0') { + continue; + } + + for (k = 0; k < 58; k++) { + if (BASE58_ALPHABET[k] == str[i]) { + break; + } + } + + value += k * base; + base *= 58; + } + + return value; +} + +/***************************************************************************** + * + * Socket + * + *****************************************************************************/ + +struct _Socket { +#ifdef _WIN32 + SOCKET handle; +#else + int handle; +#endif + Mutex send_mutex; // used to serialize socket_send calls +}; + +#ifdef _WIN32 + +static int socket_create(Socket *socket_, int domain, int type, int protocol) { + BOOL flag = 1; + + socket_->handle = socket(domain, type, protocol); + + if (socket_->handle == INVALID_SOCKET) { + return -1; + } + + if (setsockopt(socket_->handle, IPPROTO_TCP, TCP_NODELAY, + (const char *)&flag, sizeof(flag)) == SOCKET_ERROR) { + closesocket(socket_->handle); + + return -1; + } + + mutex_create(&socket_->send_mutex); + + return 0; +} + +static void socket_destroy(Socket *socket) { + mutex_destroy(&socket->send_mutex); + + closesocket(socket->handle); +} + +static int socket_connect(Socket *socket, struct sockaddr_in *address, int length) { + return connect(socket->handle, (struct sockaddr *)address, length) == SOCKET_ERROR ? -1 : 0; +} + +static void socket_shutdown(Socket *socket) { + shutdown(socket->handle, SD_BOTH); +} + +static int socket_receive(Socket *socket, void *buffer, int length) { + length = recv(socket->handle, (char *)buffer, length, 0); + + if (length == SOCKET_ERROR) { + length = -1; + + if (WSAGetLastError() == WSAEINTR) { + errno = EINTR; + } else { + errno = EFAULT; + } + } + + return length; +} + +static int socket_send(Socket *socket, void *buffer, int length) { + mutex_lock(&socket->send_mutex); + + length = send(socket->handle, (const char *)buffer, length, 0); + + mutex_unlock(&socket->send_mutex); + + if (length == SOCKET_ERROR) { + length = -1; + } + + return length; +} + +#else + +static int socket_create(Socket *socket_, int domain, int type, int protocol) { + int flag = 1; + + socket_->handle = socket(domain, type, protocol); + + if (socket_->handle < 0) { + return -1; + } + + if (setsockopt(socket_->handle, IPPROTO_TCP, TCP_NODELAY, (void *)&flag, + sizeof(flag)) < 0) { + close(socket_->handle); + + return -1; + } + + mutex_create(&socket_->send_mutex); + + return 0; +} + +static void socket_destroy(Socket *socket) { + mutex_destroy(&socket->send_mutex); + + close(socket->handle); +} + +static int socket_connect(Socket *socket, struct sockaddr_in *address, int length) { + return connect(socket->handle, (struct sockaddr *)address, length); +} + +static void socket_shutdown(Socket *socket) { + shutdown(socket->handle, SHUT_RDWR); +} + +static int socket_receive(Socket *socket, void *buffer, int length) { + return recv(socket->handle, buffer, length, 0); +} + +static int socket_send(Socket *socket, void *buffer, int length) { + int rc; + + mutex_lock(&socket->send_mutex); + + rc = send(socket->handle, buffer, length, 0); + + mutex_unlock(&socket->send_mutex); + + return rc; +} + +#endif + +/***************************************************************************** + * + * Mutex + * + *****************************************************************************/ + +#ifdef _WIN32 + +void mutex_create(Mutex *mutex) { + InitializeCriticalSection(&mutex->handle); +} + +void mutex_destroy(Mutex *mutex) { + DeleteCriticalSection(&mutex->handle); +} + +void mutex_lock(Mutex *mutex) { + EnterCriticalSection(&mutex->handle); +} + +void mutex_unlock(Mutex *mutex) { + LeaveCriticalSection(&mutex->handle); +} + +#else + +void mutex_create(Mutex *mutex) { + pthread_mutex_init(&mutex->handle, NULL); +} + +void mutex_destroy(Mutex *mutex) { + pthread_mutex_destroy(&mutex->handle); +} + +void mutex_lock(Mutex *mutex) { + pthread_mutex_lock(&mutex->handle); +} + +void mutex_unlock(Mutex *mutex) { + pthread_mutex_unlock(&mutex->handle); +} +#endif + +/***************************************************************************** + * + * Event + * + *****************************************************************************/ + +#ifdef _WIN32 + +static void event_create(Event *event) { + event->handle = CreateEvent(NULL, TRUE, FALSE, NULL); +} + +static void event_destroy(Event *event) { + CloseHandle(event->handle); +} + +static void event_set(Event *event) { + SetEvent(event->handle); +} + +static void event_reset(Event *event) { + ResetEvent(event->handle); +} + +static int event_wait(Event *event, uint32_t timeout) { // in msec + return WaitForSingleObject(event->handle, timeout) == WAIT_OBJECT_0 ? 0 : -1; +} + +#else + +static void event_create(Event *event) { + pthread_mutex_init(&event->mutex, NULL); + pthread_cond_init(&event->condition, NULL); + + event->flag = false; +} + +static void event_destroy(Event *event) { + pthread_mutex_destroy(&event->mutex); + pthread_cond_destroy(&event->condition); +} + +static void event_set(Event *event) { + pthread_mutex_lock(&event->mutex); + + event->flag = true; + + pthread_cond_signal(&event->condition); + pthread_mutex_unlock(&event->mutex); +} + +static void event_reset(Event *event) { + pthread_mutex_lock(&event->mutex); + + event->flag = false; + + pthread_mutex_unlock(&event->mutex); +} + +static int event_wait(Event *event, uint32_t timeout) { // in msec + struct timeval tp; + struct timespec ts; + int ret = 0; + + gettimeofday(&tp, NULL); + + ts.tv_sec = tp.tv_sec + timeout / 1000; + ts.tv_nsec = (tp.tv_usec + (timeout % 1000) * 1000) * 1000; + + while (ts.tv_nsec >= 1000000000L) { + ts.tv_sec += 1; + ts.tv_nsec -= 1000000000L; + } + + pthread_mutex_lock(&event->mutex); + + while (!event->flag) { + ret = pthread_cond_timedwait(&event->condition, &event->mutex, &ts); + + if (ret != 0) { + ret = -1; + break; + } + } + + pthread_mutex_unlock(&event->mutex); + + return ret; +} + +#endif + +/***************************************************************************** + * + * Semaphore + * + *****************************************************************************/ + +#ifdef _WIN32 + +static int semaphore_create(Semaphore *semaphore) { + semaphore->handle = CreateSemaphore(NULL, 0, INT32_MAX, NULL); + + return semaphore->handle == NULL ? -1 : 0; +} + +static void semaphore_destroy(Semaphore *semaphore) { + CloseHandle(semaphore->handle); +} + +static int semaphore_acquire(Semaphore *semaphore) { + return WaitForSingleObject(semaphore->handle, INFINITE) != WAIT_OBJECT_0 ? -1 : 0; +} + +static void semaphore_release(Semaphore *semaphore) { + ReleaseSemaphore(semaphore->handle, 1, NULL); +} + +#else + +static int semaphore_create(Semaphore *semaphore) { +#ifdef __APPLE__ + // Mac OS X does not support unnamed semaphores, so we fake them. Unlink + // first to ensure that there is no existing semaphore with that name. + // Then open the semaphore to create a new one. Finally unlink it again to + // avoid leaking the name. The semaphore will work fine without a name. + char name[100]; + + snprintf(name, sizeof(name), "tf-ipcon-%p", semaphore); + + sem_unlink(name); + semaphore->pointer = sem_open(name, O_CREAT | O_EXCL, S_IRWXU, 0); + sem_unlink(name); + + if (semaphore->pointer == SEM_FAILED) { + return -1; + } +#else + semaphore->pointer = &semaphore->object; + + if (sem_init(semaphore->pointer, 0, 0) < 0) { + return -1; + } +#endif + + return 0; +} + +static void semaphore_destroy(Semaphore *semaphore) { +#ifdef __APPLE__ + sem_close(semaphore->pointer); +#else + sem_destroy(semaphore->pointer); +#endif +} + +static int semaphore_acquire(Semaphore *semaphore) { + return sem_wait(semaphore->pointer) < 0 ? -1 : 0; +} + +static void semaphore_release(Semaphore *semaphore) { + sem_post(semaphore->pointer); +} + +#endif + +/***************************************************************************** + * + * Thread + * + *****************************************************************************/ + +#ifdef _WIN32 + +static DWORD WINAPI thread_wrapper(void *opaque) { + Thread *thread = (Thread *)opaque; + + thread->function(thread->opaque); + + return 0; +} + +static int thread_create(Thread *thread, ThreadFunction function, void *opaque) { + thread->function = function; + thread->opaque = opaque; + + thread->handle = CreateThread(NULL, 0, thread_wrapper, thread, 0, &thread->id); + + return thread->handle == NULL ? -1 : 0; +} + +static void thread_destroy(Thread *thread) { + CloseHandle(thread->handle); +} + +static bool thread_is_current(Thread *thread) { + return thread->id == GetCurrentThreadId(); +} + +static void thread_join(Thread *thread) { + WaitForSingleObject(thread->handle, INFINITE); +} + +static void thread_sleep(int msec) { + Sleep(msec); +} + +#else + +static void *thread_wrapper(void *opaque) { + Thread *thread = (Thread *)opaque; + + thread->function(thread->opaque); + + return NULL; +} + +static int thread_create(Thread *thread, ThreadFunction function, void *opaque) { + thread->function = function; + thread->opaque = opaque; + + return pthread_create(&thread->handle, NULL, thread_wrapper, thread); +} + +static void thread_destroy(Thread *thread) { + (void)thread; +} + +static bool thread_is_current(Thread *thread) { + return pthread_equal(thread->handle, pthread_self()) ? true : false; +} + +static void thread_join(Thread *thread) { + pthread_join(thread->handle, NULL); +} + +static void thread_sleep(int msec) { + usleep(msec * 1000); +} + +#endif + +/***************************************************************************** + * + * Table + * + *****************************************************************************/ + +static void table_create(Table *table) { + mutex_create(&table->mutex); + + table->used = 0; + table->allocated = 16; + table->keys = (uint32_t *)malloc(sizeof(uint32_t) * table->allocated); + table->values = (void **)malloc(sizeof(void *) * table->allocated); +} + +static void table_destroy(Table *table) { + free(table->keys); + free(table->values); + + mutex_destroy(&table->mutex); +} + +static void table_insert(Table *table, uint32_t key, void *value) { + int i; + + mutex_lock(&table->mutex); + + for (i = 0; i < table->used; ++i) { + if (table->keys[i] == key) { + table->values[i] = value; + + mutex_unlock(&table->mutex); + + return; + } + } + + if (table->allocated <= table->used) { + table->allocated += 16; + table->keys = (uint32_t *)realloc(table->keys, sizeof(uint32_t) * table->allocated); + table->values = (void **)realloc(table->values, sizeof(void *) * table->allocated); + } + + table->keys[table->used] = key; + table->values[table->used] = value; + + ++table->used; + + mutex_unlock(&table->mutex); +} + +static void table_remove(Table *table, uint32_t key) { + int i; + int tail; + + mutex_lock(&table->mutex); + + for (i = 0; i < table->used; ++i) { + if (table->keys[i] == key) { + tail = table->used - i - 1; + + if (tail > 0) { + memmove(table->keys + i, table->keys + i + 1, sizeof(uint32_t) * tail); + memmove(table->values + i, table->values + i + 1, sizeof(void *) * tail); + } + + --table->used; + + break; + } + } + + mutex_unlock(&table->mutex); +} + +static void *table_get(Table *table, uint32_t key) { + int i; + void *value = NULL; + + mutex_lock(&table->mutex); + + for (i = 0; i < table->used; ++i) { + if (table->keys[i] == key) { + value = table->values[i]; + + break; + } + } + + mutex_unlock(&table->mutex); + + return value; +} + +/***************************************************************************** + * + * Queue + * + *****************************************************************************/ + +enum { + QUEUE_KIND_EXIT = 0, + QUEUE_KIND_META, + QUEUE_KIND_PACKET +}; + +typedef struct { + uint8_t function_id; + uint8_t parameter; + uint64_t socket_id; +} Meta; + +static void queue_create(Queue *queue) { + queue->head = NULL; + queue->tail = NULL; + + mutex_create(&queue->mutex); + semaphore_create(&queue->semaphore); +} + +static void queue_destroy(Queue *queue) { + QueueItem *item = queue->head; + QueueItem *next; + + while (item != NULL) { + next = item->next; + + free(item->data); + free(item); + + item = next; + } + + mutex_destroy(&queue->mutex); + semaphore_destroy(&queue->semaphore); +} + +static void queue_put(Queue *queue, int kind, void *data, int length) { + QueueItem *item = (QueueItem *)malloc(sizeof(QueueItem)); + + item->next = NULL; + item->kind = kind; + item->data = NULL; + item->length = length; + + if (data != NULL) { + item->data = malloc(length); + memcpy(item->data, data, length); + } + + mutex_lock(&queue->mutex); + + if (queue->tail == NULL) { + queue->head = item; + queue->tail = item; + } else { + queue->tail->next = item; + queue->tail = item; + } + + mutex_unlock(&queue->mutex); + semaphore_release(&queue->semaphore); +} + +static int queue_get(Queue *queue, int *kind, void **data, int *length) { + QueueItem *item; + + if (semaphore_acquire(&queue->semaphore) < 0) { + return -1; + } + + mutex_lock(&queue->mutex); + + if (queue->head == NULL) { + mutex_unlock(&queue->mutex); + + return -1; + } + + item = queue->head; + queue->head = item->next; + item->next = NULL; + + if (queue->tail == item) { + queue->head = NULL; + queue->tail = NULL; + } + + mutex_unlock(&queue->mutex); + + *kind = item->kind; + *data = item->data; + *length = item->length; + + free(item); + + return 0; +} + +/***************************************************************************** + * + * Device + * + *****************************************************************************/ + +enum { + IPCON_FUNCTION_ENUMERATE = 254 +}; + +static int ipcon_send_request(IPConnectionPrivate *ipcon_p, Packet *request); + +void device_create(Device *device, const char *uid_str, + IPConnectionPrivate *ipcon_p, uint8_t api_version_major, + uint8_t api_version_minor, uint8_t api_version_release) { + DevicePrivate *device_p; + uint64_t uid; + uint32_t value1; + uint32_t value2; + int i; + + device_p = (DevicePrivate *)malloc(sizeof(DevicePrivate)); + device->p = device_p; + + uid = base58_decode(uid_str); + + if (uid > 0xFFFFFFFF) { + // convert from 64bit to 32bit + value1 = uid & 0xFFFFFFFF; + value2 = (uid >> 32) & 0xFFFFFFFF; + + uid = (value1 & 0x00000FFF); + uid |= (value1 & 0x0F000000) >> 12; + uid |= (value2 & 0x0000003F) << 16; + uid |= (value2 & 0x000F0000) << 6; + uid |= (value2 & 0x3F000000) << 2; + } + + device_p->uid = uid & 0xFFFFFFFF; + + device_p->ipcon_p = ipcon_p; + + device_p->api_version[0] = api_version_major; + device_p->api_version[1] = api_version_minor; + device_p->api_version[2] = api_version_release; + + // request + mutex_create(&device_p->request_mutex); + + // response + device_p->expected_response_function_id = 0; + device_p->expected_response_sequence_number = 0; + + mutex_create(&device_p->response_mutex); + + memset(&device_p->response_packet, 0, sizeof(Packet)); + + event_create(&device_p->response_event); + + for (i = 0; i < DEVICE_NUM_FUNCTION_IDS; i++) { + device_p->response_expected[i] = DEVICE_RESPONSE_EXPECTED_INVALID_FUNCTION_ID; + } + + device_p->response_expected[IPCON_FUNCTION_ENUMERATE] = DEVICE_RESPONSE_EXPECTED_ALWAYS_FALSE; + device_p->response_expected[IPCON_CALLBACK_ENUMERATE] = DEVICE_RESPONSE_EXPECTED_ALWAYS_FALSE; + + // callbacks + for (i = 0; i < DEVICE_NUM_FUNCTION_IDS; i++) { + device_p->registered_callbacks[i] = NULL; + device_p->registered_callback_user_data[i] = NULL; + device_p->callback_wrappers[i] = NULL; + } + + // add to IPConnection + table_insert(&ipcon_p->devices, device_p->uid, device_p); +} + +void device_destroy(Device *device) { + DevicePrivate *device_p = device->p; + + table_remove(&device_p->ipcon_p->devices, device_p->uid); + + event_destroy(&device_p->response_event); + + mutex_destroy(&device_p->response_mutex); + + mutex_destroy(&device_p->request_mutex); + + free(device_p); +} + +int device_get_response_expected(DevicePrivate *device_p, uint8_t function_id, + bool *ret_response_expected) { + int flag = device_p->response_expected[function_id]; + + if (flag == DEVICE_RESPONSE_EXPECTED_INVALID_FUNCTION_ID) { + return E_INVALID_PARAMETER; + } + + if (flag == DEVICE_RESPONSE_EXPECTED_ALWAYS_TRUE || + flag == DEVICE_RESPONSE_EXPECTED_TRUE) { + *ret_response_expected = true; + } else { + *ret_response_expected = false; + } + + return E_OK; +} + +int device_set_response_expected(DevicePrivate *device_p, uint8_t function_id, + bool response_expected) { + int current_flag = device_p->response_expected[function_id]; + + if (current_flag != DEVICE_RESPONSE_EXPECTED_TRUE && + current_flag != DEVICE_RESPONSE_EXPECTED_FALSE) { + return E_INVALID_PARAMETER; + } + + device_p->response_expected[function_id] = + response_expected ? DEVICE_RESPONSE_EXPECTED_TRUE + : DEVICE_RESPONSE_EXPECTED_FALSE; + + return E_OK; +} + +int device_set_response_expected_all(DevicePrivate *device_p, bool response_expected) { + int flag = response_expected ? DEVICE_RESPONSE_EXPECTED_TRUE + : DEVICE_RESPONSE_EXPECTED_FALSE; + int i; + + for (i = 0; i < DEVICE_NUM_FUNCTION_IDS; ++i) { + if (device_p->response_expected[i] == DEVICE_RESPONSE_EXPECTED_TRUE || + device_p->response_expected[i] == DEVICE_RESPONSE_EXPECTED_FALSE) { + device_p->response_expected[i] = flag; + } + } + + return E_OK; +} + +void device_register_callback(DevicePrivate *device_p, uint8_t id, void *callback, + void *user_data) { + device_p->registered_callbacks[id] = callback; + device_p->registered_callback_user_data[id] = user_data; +} + +int device_get_api_version(DevicePrivate *device_p, uint8_t ret_api_version[3]) { + ret_api_version[0] = device_p->api_version[0]; + ret_api_version[1] = device_p->api_version[1]; + ret_api_version[2] = device_p->api_version[2]; + + return E_OK; +} + +int device_send_request(DevicePrivate *device_p, Packet *request, Packet *response) { + int ret = E_OK; + uint8_t sequence_number = packet_header_get_sequence_number(&request->header); + uint8_t response_expected = packet_header_get_response_expected(&request->header); + uint8_t error_code; + + if (response_expected) { + mutex_lock(&device_p->request_mutex); + + event_reset(&device_p->response_event); + + device_p->expected_response_function_id = request->header.function_id; + device_p->expected_response_sequence_number = sequence_number; + } + + ret = ipcon_send_request(device_p->ipcon_p, request); + + if (ret != E_OK) { + if (response_expected) { + mutex_unlock(&device_p->request_mutex); + } + + return ret; + } + + if (response_expected) { + if (event_wait(&device_p->response_event, device_p->ipcon_p->timeout) < 0) { + ret = E_TIMEOUT; + } + + device_p->expected_response_function_id = 0; + device_p->expected_response_sequence_number = 0; + + event_reset(&device_p->response_event); + + if (ret == E_OK) { + mutex_lock(&device_p->response_mutex); + + error_code = packet_header_get_error_code(&device_p->response_packet.header); + + if (device_p->response_packet.header.function_id != request->header.function_id || + packet_header_get_sequence_number(&device_p->response_packet.header) != sequence_number) { + ret = E_TIMEOUT; + } else if (error_code == 0) { + // no error + if (response != NULL) { + memcpy(response, &device_p->response_packet, + device_p->response_packet.header.length); + } + } else if (error_code == 1) { + ret = E_INVALID_PARAMETER; + } else if (error_code == 2) { + ret = E_NOT_SUPPORTED; + } else { + ret = E_UNKNOWN_ERROR_CODE; + } + + mutex_unlock(&device_p->response_mutex); + } + + mutex_unlock(&device_p->request_mutex); + } + + return ret; +} + +/***************************************************************************** + * + * IPConnection + * + *****************************************************************************/ + +struct _CallbackContext { + IPConnectionPrivate *ipcon_p; + Queue queue; + Thread thread; + Mutex mutex; + bool packet_dispatch_allowed; +}; + +static int ipcon_connect_unlocked(IPConnectionPrivate *ipcon_p, bool is_auto_reconnect); +static void ipcon_disconnect_unlocked(IPConnectionPrivate *ipcon_p); + +static void ipcon_dispatch_meta(IPConnectionPrivate *ipcon_p, Meta *meta) { + ConnectedCallbackFunction connected_callback_function; + DisconnectedCallbackFunction disconnected_callback_function; + void *user_data; + bool retry; + + if (meta->function_id == IPCON_CALLBACK_CONNECTED) { + if (ipcon_p->registered_callbacks[IPCON_CALLBACK_CONNECTED] != NULL) { + *(void **)(&connected_callback_function) = ipcon_p->registered_callbacks[IPCON_CALLBACK_CONNECTED]; + user_data = ipcon_p->registered_callback_user_data[IPCON_CALLBACK_CONNECTED]; + + connected_callback_function(meta->parameter, user_data); + } + } else if (meta->function_id == IPCON_CALLBACK_DISCONNECTED) { + // need to do this here, the receive loop is not allowed to + // hold the socket mutex because this could cause a deadlock + // with a concurrent call to the (dis-)connect function + if (meta->parameter != IPCON_DISCONNECT_REASON_REQUEST) { + mutex_lock(&ipcon_p->socket_mutex); + + // don't close the socket if it got disconnected or + // reconnected in the meantime + if (ipcon_p->socket != NULL && ipcon_p->socket_id == meta->socket_id) { + // destroy disconnect probe thread + event_set(&ipcon_p->disconnect_probe_event); + thread_join(&ipcon_p->disconnect_probe_thread); + thread_destroy(&ipcon_p->disconnect_probe_thread); + + // destroy socket + socket_destroy(ipcon_p->socket); + free(ipcon_p->socket); + ipcon_p->socket = NULL; + } + + mutex_unlock(&ipcon_p->socket_mutex); + } + + // FIXME: wait a moment here, otherwise the next connect + // attempt will succeed, even if there is no open server + // socket. the first receive will then fail directly + thread_sleep(100); + + if (ipcon_p->registered_callbacks[IPCON_CALLBACK_DISCONNECTED] != NULL) { + *(void **)(&disconnected_callback_function) = ipcon_p->registered_callbacks[IPCON_CALLBACK_DISCONNECTED]; + user_data = ipcon_p->registered_callback_user_data[IPCON_CALLBACK_DISCONNECTED]; + + disconnected_callback_function(meta->parameter, user_data); + } + + if (meta->parameter != IPCON_DISCONNECT_REASON_REQUEST && + ipcon_p->auto_reconnect && ipcon_p->auto_reconnect_allowed) { + ipcon_p->auto_reconnect_pending = true; + retry = true; + + // block here until reconnect. this is okay, there is no + // callback to deliver when there is no connection + while (retry) { + retry = false; + + mutex_lock(&ipcon_p->socket_mutex); + + if (ipcon_p->auto_reconnect_allowed && ipcon_p->socket == NULL) { + if (ipcon_connect_unlocked(ipcon_p, true) < 0) { + retry = true; + } + } else { + ipcon_p->auto_reconnect_pending = false; + } + + mutex_unlock(&ipcon_p->socket_mutex); + + if (retry) { + // wait a moment to give another thread a chance to + // interrupt the auto-reconnect + thread_sleep(100); + } + } + } + } +} + +static void ipcon_dispatch_packet(IPConnectionPrivate *ipcon_p, Packet *packet) { + EnumerateCallbackFunction enumerate_callback_function; + void *user_data; + EnumerateCallback *enumerate_callback; + DevicePrivate *device_p; + CallbackWrapperFunction callback_wrapper_function; + + if (packet->header.function_id == IPCON_CALLBACK_ENUMERATE) { + if (ipcon_p->registered_callbacks[IPCON_CALLBACK_ENUMERATE] != NULL) { + *(void **)(&enumerate_callback_function) = ipcon_p->registered_callbacks[IPCON_CALLBACK_ENUMERATE]; + user_data = ipcon_p->registered_callback_user_data[IPCON_CALLBACK_ENUMERATE]; + enumerate_callback = (EnumerateCallback *)packet; + + enumerate_callback_function(enumerate_callback->uid, + enumerate_callback->connected_uid, + enumerate_callback->position, + enumerate_callback->hardware_version, + enumerate_callback->firmware_version, + leconvert_uint16_from(enumerate_callback->device_identifier), + enumerate_callback->enumeration_type, + user_data); + } + } else { + device_p = (DevicePrivate *)table_get(&ipcon_p->devices, packet->header.uid); + + if (device_p == NULL) { + return; + } + + callback_wrapper_function = device_p->callback_wrappers[packet->header.function_id]; + + if (callback_wrapper_function == NULL) { + return; + } + + callback_wrapper_function(device_p, packet); + } +} + +static void ipcon_callback_loop(void *opaque) { + CallbackContext *callback = (CallbackContext *)opaque; + int kind; + void *data; + int length; + + while (true) { + if (queue_get(&callback->queue, &kind, &data, &length) < 0) { + // FIXME: what to do here? try again? exit? + break; + } + + // FIXME: cannot lock callback mutex here because this can + // deadlock due to an ordering problem with the socket mutex + //mutex_lock(&callback->mutex); + + if (kind == QUEUE_KIND_EXIT) { + //mutex_unlock(&callback->mutex); + break; + } else if (kind == QUEUE_KIND_META) { + ipcon_dispatch_meta(callback->ipcon_p, (Meta *)data); + } else if (kind == QUEUE_KIND_PACKET) { + // don't dispatch callbacks when the receive thread isn't running + if (callback->packet_dispatch_allowed) { + ipcon_dispatch_packet(callback->ipcon_p, (Packet *)data); + } + } + + //mutex_unlock(&callback->mutex); + + free(data); + } + + // cleanup + mutex_destroy(&callback->mutex); + queue_destroy(&callback->queue); + thread_destroy(&callback->thread); + + free(callback); +} + +// NOTE: assumes that socket_mutex is locked if disconnect_immediately is true +static void ipcon_handle_disconnect_by_peer(IPConnectionPrivate *ipcon_p, + uint8_t disconnect_reason, + uint64_t socket_id, + bool disconnect_immediately) { + Meta meta; + + ipcon_p->auto_reconnect_allowed = true; + + if (disconnect_immediately) { + ipcon_disconnect_unlocked(ipcon_p); + } + + meta.function_id = IPCON_CALLBACK_DISCONNECTED; + meta.parameter = disconnect_reason; + meta.socket_id = socket_id; + + queue_put(&ipcon_p->callback->queue, QUEUE_KIND_META, &meta, sizeof(meta)); +} + +enum { + IPCON_DISCONNECT_PROBE_INTERVAL = 5000 +}; + +enum { + IPCON_FUNCTION_DISCONNECT_PROBE = 128 +}; + +// NOTE: the disconnect probe loop is not allowed to hold the socket_mutex at any +// time because it is created and joined while the socket_mutex is locked +static void ipcon_disconnect_probe_loop(void *opaque) { + IPConnectionPrivate *ipcon_p = (IPConnectionPrivate *)opaque; + PacketHeader disconnect_probe; + + packet_header_create(&disconnect_probe, sizeof(PacketHeader), + IPCON_FUNCTION_DISCONNECT_PROBE, ipcon_p, NULL); + + while (event_wait(&ipcon_p->disconnect_probe_event, + IPCON_DISCONNECT_PROBE_INTERVAL) < 0) { + if (ipcon_p->disconnect_probe_flag) { + // FIXME: this might block + if (socket_send(ipcon_p->socket, &disconnect_probe, + disconnect_probe.length) < 0) { + ipcon_handle_disconnect_by_peer(ipcon_p, IPCON_DISCONNECT_REASON_ERROR, + ipcon_p->socket_id, false); + break; + } + } else { + ipcon_p->disconnect_probe_flag = true; + } + } +} + +static void ipcon_handle_response(IPConnectionPrivate *ipcon_p, Packet *response) { + DevicePrivate *device_p; + uint8_t sequence_number = packet_header_get_sequence_number(&response->header); + + ipcon_p->disconnect_probe_flag = false; + + response->header.uid = leconvert_uint32_from(response->header.uid); + + if (sequence_number == 0 && + response->header.function_id == IPCON_CALLBACK_ENUMERATE) { + if (ipcon_p->registered_callbacks[IPCON_CALLBACK_ENUMERATE] != NULL) { + queue_put(&ipcon_p->callback->queue, QUEUE_KIND_PACKET, response, + response->header.length); + } + + return; + } + + device_p = (DevicePrivate *)table_get(&ipcon_p->devices, response->header.uid); + + if (device_p == NULL) { + // ignoring response for an unknown device + return; + } + + if (sequence_number == 0) { + if (device_p->registered_callbacks[response->header.function_id] != NULL) { + queue_put(&ipcon_p->callback->queue, QUEUE_KIND_PACKET, response, + response->header.length); + } + + return; + } + + if (device_p->expected_response_function_id == response->header.function_id && + device_p->expected_response_sequence_number == sequence_number) { + mutex_lock(&device_p->response_mutex); + memcpy(&device_p->response_packet, response, response->header.length); + mutex_unlock(&device_p->response_mutex); + + event_set(&device_p->response_event); + return; + } + + // response seems to be OK, but can't be handled +} + +// NOTE: the receive loop is now allowed to hold the socket_mutex at any time +// because it is created and joined while the socket_mutex is locked +static void ipcon_receive_loop(void *opaque) { + IPConnectionPrivate *ipcon_p = (IPConnectionPrivate *)opaque; + uint64_t socket_id = ipcon_p->socket_id; + Packet pending_data[10]; + int pending_length = 0; + int length; + uint8_t disconnect_reason; + + while (ipcon_p->receive_flag) { + length = socket_receive(ipcon_p->socket, (uint8_t *)pending_data + pending_length, + sizeof(pending_data) - pending_length); + + if (!ipcon_p->receive_flag) { + return; + } + + if (length <= 0) { + if (length < 0 && errno == EINTR) { + continue; + } + + if (length == 0) { + disconnect_reason = IPCON_DISCONNECT_REASON_SHUTDOWN; + } else { + disconnect_reason = IPCON_DISCONNECT_REASON_ERROR; + } + + ipcon_handle_disconnect_by_peer(ipcon_p, disconnect_reason, socket_id, false); + return; + } + + pending_length += length; + + while (ipcon_p->receive_flag) { + if (pending_length < 8) { + // wait for complete header + break; + } + + length = pending_data[0].header.length; + + if (pending_length < length) { + // wait for complete packet + break; + } + + ipcon_handle_response(ipcon_p, pending_data); + + memmove(pending_data, (uint8_t *)pending_data + length, + pending_length - length); + pending_length -= length; + } + } +} + +// NOTE: assumes that socket_mutex is locked +static int ipcon_connect_unlocked(IPConnectionPrivate *ipcon_p, bool is_auto_reconnect) { + struct hostent *entity; + struct sockaddr_in address; + uint8_t connect_reason; + Meta meta; + + // create callback queue and thread + if (ipcon_p->callback == NULL) { + ipcon_p->callback = (CallbackContext *)malloc(sizeof(CallbackContext)); + + ipcon_p->callback->ipcon_p = ipcon_p; + ipcon_p->callback->packet_dispatch_allowed = false; + + queue_create(&ipcon_p->callback->queue); + mutex_create(&ipcon_p->callback->mutex); + + if (thread_create(&ipcon_p->callback->thread, ipcon_callback_loop, + ipcon_p->callback) < 0) { + mutex_destroy(&ipcon_p->callback->mutex); + queue_destroy(&ipcon_p->callback->queue); + + free(ipcon_p->callback); + ipcon_p->callback = NULL; + + return E_NO_THREAD; + } + } + + // create and connect socket + entity = gethostbyname(ipcon_p->host); + + if (entity == NULL) { + // destroy callback thread + if (!is_auto_reconnect) { + queue_put(&ipcon_p->callback->queue, QUEUE_KIND_EXIT, NULL, 0); + + if (!thread_is_current(&ipcon_p->callback->thread)) { + thread_join(&ipcon_p->callback->thread); + } + + ipcon_p->callback = NULL; + } + + return E_HOSTNAME_INVALID; + } + + memset(&address, 0, sizeof(struct sockaddr_in)); + memcpy(&address.sin_addr, entity->h_addr_list[0], entity->h_length); + + address.sin_family = AF_INET; + address.sin_port = htons(ipcon_p->port); + + ipcon_p->socket = (Socket *)malloc(sizeof(Socket)); + + if (socket_create(ipcon_p->socket, AF_INET, SOCK_STREAM, 0) < 0) { + // destroy callback thread + if (!is_auto_reconnect) { + queue_put(&ipcon_p->callback->queue, QUEUE_KIND_EXIT, NULL, 0); + + if (!thread_is_current(&ipcon_p->callback->thread)) { + thread_join(&ipcon_p->callback->thread); + } + + ipcon_p->callback = NULL; + } + + // destroy socket + free(ipcon_p->socket); + ipcon_p->socket = NULL; + + return E_NO_STREAM_SOCKET; + } + + if (socket_connect(ipcon_p->socket, &address, sizeof(address)) < 0) { + // destroy callback thread + if (!is_auto_reconnect) { + queue_put(&ipcon_p->callback->queue, QUEUE_KIND_EXIT, NULL, 0); + + if (!thread_is_current(&ipcon_p->callback->thread)) { + thread_join(&ipcon_p->callback->thread); + } + + ipcon_p->callback = NULL; + } + + // destroy socket + socket_destroy(ipcon_p->socket); + free(ipcon_p->socket); + ipcon_p->socket = NULL; + + return E_NO_CONNECT; + } + + ++ipcon_p->socket_id; + + // create disconnect probe thread + ipcon_p->disconnect_probe_flag = true; + + event_reset(&ipcon_p->disconnect_probe_event); + + if (thread_create(&ipcon_p->disconnect_probe_thread, + ipcon_disconnect_probe_loop, ipcon_p) < 0) { + // destroy callback thread + if (!is_auto_reconnect) { + queue_put(&ipcon_p->callback->queue, QUEUE_KIND_EXIT, NULL, 0); + + if (!thread_is_current(&ipcon_p->callback->thread)) { + thread_join(&ipcon_p->callback->thread); + } + + ipcon_p->callback = NULL; + } + + // destroy socket + socket_destroy(ipcon_p->socket); + free(ipcon_p->socket); + ipcon_p->socket = NULL; + + return E_NO_THREAD; + } + + // create receive thread + ipcon_p->receive_flag = true; + ipcon_p->callback->packet_dispatch_allowed = true; + + if (thread_create(&ipcon_p->receive_thread, ipcon_receive_loop, ipcon_p) < 0) { + ipcon_disconnect_unlocked(ipcon_p); + + // destroy callback thread + if (!is_auto_reconnect) { + queue_put(&ipcon_p->callback->queue, QUEUE_KIND_EXIT, NULL, 0); + + if (!thread_is_current(&ipcon_p->callback->thread)) { + thread_join(&ipcon_p->callback->thread); + } + + ipcon_p->callback = NULL; + } + + return E_NO_THREAD; + } + + ipcon_p->auto_reconnect_allowed = false; + ipcon_p->auto_reconnect_pending = false; + + // trigger connected callback + if (is_auto_reconnect) { + connect_reason = IPCON_CONNECT_REASON_AUTO_RECONNECT; + } else { + connect_reason = IPCON_CONNECT_REASON_REQUEST; + } + + meta.function_id = IPCON_CALLBACK_CONNECTED; + meta.parameter = connect_reason; + meta.socket_id = 0; + + queue_put(&ipcon_p->callback->queue, QUEUE_KIND_META, &meta, sizeof(meta)); + + return E_OK; +} + +// NOTE: assumes that socket_mutex is locked +static void ipcon_disconnect_unlocked(IPConnectionPrivate *ipcon_p) { + // destroy disconnect probe thread + event_set(&ipcon_p->disconnect_probe_event); + thread_join(&ipcon_p->disconnect_probe_thread); + thread_destroy(&ipcon_p->disconnect_probe_thread); + + // stop dispatching packet callbacks before ending the receive + // thread to avoid timeout exceptions due to callback functions + // trying to call getters + if (!thread_is_current(&ipcon_p->callback->thread)) { + // FIXME: cannot lock callback mutex here because this can + // deadlock due to an ordering problem with the socket mutex + //mutex_lock(&ipcon->callback->mutex); + + ipcon_p->callback->packet_dispatch_allowed = false; + + //mutex_unlock(&ipcon->callback->mutex); + } else { + ipcon_p->callback->packet_dispatch_allowed = false; + } + + // destroy receive thread + if (ipcon_p->receive_flag) { + ipcon_p->receive_flag = false; + + socket_shutdown(ipcon_p->socket); + + thread_join(&ipcon_p->receive_thread); + thread_destroy(&ipcon_p->receive_thread); + } + + // destroy socket + socket_destroy(ipcon_p->socket); + free(ipcon_p->socket); + ipcon_p->socket = NULL; +} + +static int ipcon_send_request(IPConnectionPrivate *ipcon_p, Packet *request) { + int ret = E_OK; + + mutex_lock(&ipcon_p->socket_mutex); + + if (ipcon_p->socket == NULL) { + ret = E_NOT_CONNECTED; + } + + if (ret == E_OK) { + if (socket_send(ipcon_p->socket, request, request->header.length) < 0) { + ipcon_handle_disconnect_by_peer(ipcon_p, IPCON_DISCONNECT_REASON_ERROR, + 0, true); + + ret = E_NOT_CONNECTED; + } else { + ipcon_p->disconnect_probe_flag = false; + } + } + + mutex_unlock(&ipcon_p->socket_mutex); + + return ret; +} + +void ipcon_create(IPConnection *ipcon) { + IPConnectionPrivate *ipcon_p; + int i; + + ipcon_p = (IPConnectionPrivate *)malloc(sizeof(IPConnectionPrivate)); + ipcon->p = ipcon_p; + +#ifdef _WIN32 + ipcon_p->wsa_startup_done = false; +#endif + + ipcon_p->host = NULL; + ipcon_p->port = 0; + + ipcon_p->timeout = 2500; + + ipcon_p->auto_reconnect = true; + ipcon_p->auto_reconnect_allowed = false; + ipcon_p->auto_reconnect_pending = false; + + mutex_create(&ipcon_p->sequence_number_mutex); + ipcon_p->next_sequence_number = 0; + + table_create(&ipcon_p->devices); + + for (i = 0; i < IPCON_NUM_CALLBACK_IDS; ++i) { + ipcon_p->registered_callbacks[i] = NULL; + ipcon_p->registered_callback_user_data[i] = NULL; + } + + mutex_create(&ipcon_p->socket_mutex); + ipcon_p->socket = NULL; + ipcon_p->socket_id = 0; + + ipcon_p->receive_flag = false; + + ipcon_p->callback = NULL; + + ipcon_p->disconnect_probe_flag = false; + event_create(&ipcon_p->disconnect_probe_event); + + semaphore_create(&ipcon_p->wait); +} + +void ipcon_destroy(IPConnection *ipcon) { + IPConnectionPrivate *ipcon_p = ipcon->p; + + ipcon_disconnect(ipcon); // FIXME: disable disconnected callback before? + + mutex_destroy(&ipcon_p->sequence_number_mutex); + + table_destroy(&ipcon_p->devices); + + mutex_destroy(&ipcon_p->socket_mutex); + + event_destroy(&ipcon_p->disconnect_probe_event); + + semaphore_destroy(&ipcon_p->wait); + + free(ipcon_p->host); + + free(ipcon_p); +} + +int ipcon_connect(IPConnection *ipcon, const char *host, uint16_t port) { + IPConnectionPrivate *ipcon_p = ipcon->p; + int ret; +#ifdef _WIN32 + WSADATA wsa_data; +#endif + + mutex_lock(&ipcon_p->socket_mutex); + +#ifdef _WIN32 + if (!ipcon_p->wsa_startup_done) { + if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0) { + mutex_unlock(&ipcon_p->socket_mutex); + + return E_NO_STREAM_SOCKET; + } + + ipcon_p->wsa_startup_done = true; + } +#endif + + if (ipcon_p->socket != NULL) { + mutex_unlock(&ipcon_p->socket_mutex); + + return E_ALREADY_CONNECTED; + } + + free(ipcon_p->host); + + ipcon_p->host = strdup(host); + ipcon_p->port = port; + + ret = ipcon_connect_unlocked(ipcon_p, false); + + mutex_unlock(&ipcon_p->socket_mutex); + + return ret; +} + +int ipcon_disconnect(IPConnection *ipcon) { + IPConnectionPrivate *ipcon_p = ipcon->p; + CallbackContext *callback; + Meta meta; + + mutex_lock(&ipcon_p->socket_mutex); + + ipcon_p->auto_reconnect_allowed = false; + + if (ipcon_p->auto_reconnect_pending) { + // abort pending auto-reconnect + ipcon_p->auto_reconnect_pending = false; + } else { + if (ipcon_p->socket == NULL) { + mutex_unlock(&ipcon_p->socket_mutex); + + return E_NOT_CONNECTED; + } + + ipcon_disconnect_unlocked(ipcon_p); + } + + // destroy callback thread + callback = ipcon_p->callback; + ipcon_p->callback = NULL; + + mutex_unlock(&ipcon_p->socket_mutex); + + // do this outside of socket_mutex to allow calling (dis-)connect from + // the callbacks while blocking on the join call here + meta.function_id = IPCON_CALLBACK_DISCONNECTED; + meta.parameter = IPCON_DISCONNECT_REASON_REQUEST; + meta.socket_id = 0; + + queue_put(&callback->queue, QUEUE_KIND_META, &meta, sizeof(meta)); + queue_put(&callback->queue, QUEUE_KIND_EXIT, NULL, 0); + + if (!thread_is_current(&callback->thread)) { + thread_join(&callback->thread); + } + + // NOTE: no further cleanup of the callback queue and thread here, the + // callback thread is doing this on exit + + return E_OK; +} + +int ipcon_get_connection_state(IPConnection *ipcon) { + IPConnectionPrivate *ipcon_p = ipcon->p; + + if (ipcon_p->socket != NULL) { + return IPCON_CONNECTION_STATE_CONNECTED; + } else if (ipcon_p->auto_reconnect_pending) { + return IPCON_CONNECTION_STATE_PENDING; + } else { + return IPCON_CONNECTION_STATE_DISCONNECTED; + } +} + +void ipcon_set_auto_reconnect(IPConnection *ipcon, bool auto_reconnect) { + IPConnectionPrivate *ipcon_p = ipcon->p; + + ipcon_p->auto_reconnect = auto_reconnect; + + if (!ipcon_p->auto_reconnect) { + // abort potentially pending auto reconnect + ipcon_p->auto_reconnect_allowed = false; + } +} + +bool ipcon_get_auto_reconnect(IPConnection *ipcon) { + return ipcon->p->auto_reconnect; +} + +void ipcon_set_timeout(IPConnection *ipcon, uint32_t timeout) { // in msec + ipcon->p->timeout = timeout; +} + +uint32_t ipcon_get_timeout(IPConnection *ipcon) { // in msec + return ipcon->p->timeout; +} + +int ipcon_enumerate(IPConnection *ipcon) { + IPConnectionPrivate *ipcon_p = ipcon->p; + Enumerate enumerate; + int ret; + + ret = packet_header_create(&enumerate.header, sizeof(Enumerate), + IPCON_FUNCTION_ENUMERATE, ipcon_p, NULL); + + if (ret < 0) { + return ret; + } + + return ipcon_send_request(ipcon_p, (Packet *)&enumerate); +} + +void ipcon_wait(IPConnection *ipcon) { + semaphore_acquire(&ipcon->p->wait); +} + +void ipcon_unwait(IPConnection *ipcon) { + semaphore_release(&ipcon->p->wait); +} + +void ipcon_register_callback(IPConnection *ipcon, uint8_t id, void *callback, + void *user_data) { + IPConnectionPrivate *ipcon_p = ipcon->p; + + ipcon_p->registered_callbacks[id] = callback; + ipcon_p->registered_callback_user_data[id] = user_data; +} + +int packet_header_create(PacketHeader *header, uint8_t length, + uint8_t function_id, IPConnectionPrivate *ipcon_p, + DevicePrivate *device_p) { + uint8_t sequence_number; + bool response_expected = false; + int ret = E_OK; + + mutex_lock(&ipcon_p->sequence_number_mutex); + + sequence_number = ipcon_p->next_sequence_number + 1; + ipcon_p->next_sequence_number = sequence_number % 15; + + mutex_unlock(&ipcon_p->sequence_number_mutex); + + memset(header, 0, sizeof(PacketHeader)); + + if (device_p != NULL) { + header->uid = leconvert_uint32_to(device_p->uid); + } + + header->length = length; + header->function_id = function_id; + packet_header_set_sequence_number(header, sequence_number); + + if (device_p != NULL) { + ret = device_get_response_expected(device_p, function_id, &response_expected); + packet_header_set_response_expected(header, response_expected ? 1 : 0); + } + + return ret; +} + +uint8_t packet_header_get_sequence_number(PacketHeader *header) { + return (header->sequence_number_and_options >> 4) & 0x0F; +} + +void packet_header_set_sequence_number(PacketHeader *header, + uint8_t sequence_number) { + header->sequence_number_and_options |= (sequence_number << 4) & 0xF0; +} + +uint8_t packet_header_get_response_expected(PacketHeader *header) { + return (header->sequence_number_and_options >> 3) & 0x01; +} + +void packet_header_set_response_expected(PacketHeader *header, + uint8_t response_expected) { + header->sequence_number_and_options |= (response_expected << 3) & 0x08; +} + +uint8_t packet_header_get_error_code(PacketHeader *header) { + return (header->error_code_and_future_use >> 6) & 0x03; +} + +// undefine potential defines from /usr/include/endian.h +#undef LITTLE_ENDIAN +#undef BIG_ENDIAN + +#define LITTLE_ENDIAN 0x03020100ul +#define BIG_ENDIAN 0x00010203ul + +static const union { + uint8_t bytes[4]; + uint32_t value; +} native_endian = { + { 0, 1, 2, 3 } +}; + +static void *leconvert_swap16(void *data) { + uint8_t *s = (uint8_t *)data; + uint8_t d[2]; + + d[0] = s[1]; + d[1] = s[0]; + + s[0] = d[0]; + s[1] = d[1]; + + return data; +} + +static void *leconvert_swap32(void *data) { + uint8_t *s = (uint8_t *)data; + uint8_t d[4]; + + d[0] = s[3]; + d[1] = s[2]; + d[2] = s[1]; + d[3] = s[0]; + + s[0] = d[0]; + s[1] = d[1]; + s[2] = d[2]; + s[3] = d[3]; + + return data; +} + +static void *leconvert_swap64(void *data) { + uint8_t *s = (uint8_t *)data; + uint8_t d[8]; + + d[0] = s[7]; + d[1] = s[6]; + d[2] = s[5]; + d[3] = s[4]; + d[4] = s[3]; + d[5] = s[2]; + d[6] = s[1]; + d[7] = s[0]; + + s[0] = d[0]; + s[1] = d[1]; + s[2] = d[2]; + s[3] = d[3]; + s[4] = d[4]; + s[5] = d[5]; + s[6] = d[6]; + s[7] = d[7]; + + return data; +} + +int16_t leconvert_int16_to(int16_t native) { + if (native_endian.value == LITTLE_ENDIAN) { + return native; + } else { + return *(int16_t *)leconvert_swap16(&native); + } +} + +uint16_t leconvert_uint16_to(uint16_t native) { + if (native_endian.value == LITTLE_ENDIAN) { + return native; + } else { + return *(uint16_t *)leconvert_swap16(&native); + } +} + +int32_t leconvert_int32_to(int32_t native) { + if (native_endian.value == LITTLE_ENDIAN) { + return native; + } else { + return *(int32_t *)leconvert_swap32(&native); + } +} + +uint32_t leconvert_uint32_to(uint32_t native) { + if (native_endian.value == LITTLE_ENDIAN) { + return native; + } else { + return *(uint32_t *)leconvert_swap32(&native); + } +} + +int64_t leconvert_int64_to(int64_t native) { + if (native_endian.value == LITTLE_ENDIAN) { + return native; + } else { + return *(int64_t *)leconvert_swap64(&native); + } +} + +uint64_t leconvert_uint64_to(uint64_t native) { + if (native_endian.value == LITTLE_ENDIAN) { + return native; + } else { + return *(uint64_t *)leconvert_swap64(&native); + } +} + +float leconvert_float_to(float native) { + if (native_endian.value == LITTLE_ENDIAN) { + return native; + } else { + return *(float *)leconvert_swap32(&native); + } +} + +int16_t leconvert_int16_from(int16_t little) { + if (native_endian.value == LITTLE_ENDIAN) { + return little; + } else { + return *(int16_t *)leconvert_swap16(&little); + } +} + +uint16_t leconvert_uint16_from(uint16_t little) { + if (native_endian.value == LITTLE_ENDIAN) { + return little; + } else { + return *(uint16_t *)leconvert_swap16(&little); + } +} + +int32_t leconvert_int32_from(int32_t little) { + if (native_endian.value == LITTLE_ENDIAN) { + return little; + } else { + return *(int32_t *)leconvert_swap32(&little); + } +} + +uint32_t leconvert_uint32_from(uint32_t little) { + if (native_endian.value == LITTLE_ENDIAN) { + return little; + } else { + return *(uint32_t *)leconvert_swap32(&little); + } +} + +int64_t leconvert_int64_from(int64_t little) { + if (native_endian.value == LITTLE_ENDIAN) { + return little; + } else { + return *(int64_t *)leconvert_swap64(&little); + } +} + +uint64_t leconvert_uint64_from(uint64_t little) { + if (native_endian.value == LITTLE_ENDIAN) { + return little; + } else { + return *(uint64_t *)leconvert_swap64(&little); + } +} + +float leconvert_float_from(float little) { + if (native_endian.value == LITTLE_ENDIAN) { + return little; + } else { + return *(float *)leconvert_swap32(&little); + } +} diff --git a/dependencies/include/tinkerforge/bricklet_led_strip.h b/dependencies/include/tinkerforge/bricklet_led_strip.h new file mode 100644 index 00000000..11d608c7 --- /dev/null +++ b/dependencies/include/tinkerforge/bricklet_led_strip.h @@ -0,0 +1,301 @@ +/* *********************************************************** + * This file was automatically generated on 2013-12-19. * + * * + * Bindings Version 2.0.13 * + * * + * If you have a bugfix for this file and want to commit it, * + * please fix the bug in the generator. You can find a link * + * to the generator git on tinkerforge.com * + *************************************************************/ + +#ifndef BRICKLET_LED_STRIP_H +#define BRICKLET_LED_STRIP_H + +#include "ip_connection.h" + +/** + * \defgroup BrickletLEDStrip LEDStrip Bricklet + */ + +/** + * \ingroup BrickletLEDStrip + * + * Device to control up to 320 RGB LEDs + */ +typedef Device LEDStrip; + +/** + * \ingroup BrickletLEDStrip + */ +#define LED_STRIP_FUNCTION_SET_RGB_VALUES 1 + +/** + * \ingroup BrickletLEDStrip + */ +#define LED_STRIP_FUNCTION_GET_RGB_VALUES 2 + +/** + * \ingroup BrickletLEDStrip + */ +#define LED_STRIP_FUNCTION_SET_FRAME_DURATION 3 + +/** + * \ingroup BrickletLEDStrip + */ +#define LED_STRIP_FUNCTION_GET_FRAME_DURATION 4 + +/** + * \ingroup BrickletLEDStrip + */ +#define LED_STRIP_FUNCTION_GET_SUPPLY_VOLTAGE 5 + +/** + * \ingroup BrickletLEDStrip + */ +#define LED_STRIP_FUNCTION_SET_CLOCK_FREQUENCY 7 + +/** + * \ingroup BrickletLEDStrip + */ +#define LED_STRIP_FUNCTION_GET_CLOCK_FREQUENCY 8 + +/** + * \ingroup BrickletLEDStrip + */ +#define LED_STRIP_FUNCTION_GET_IDENTITY 255 + +/** + * \ingroup BrickletLEDStrip + * + * Signature: \code void callback(uint16_t length, void *user_data) \endcode + * + * This callback is triggered directly after a new frame is rendered. + * + * You should send the data for the next frame directly after this callback + * was triggered. + * + * For an explanation of the general approach see {@link led_strip_set_rgb_values}. + */ +#define LED_STRIP_CALLBACK_FRAME_RENDERED 6 + + +/** + * \ingroup BrickletLEDStrip + * + * This constant is used to identify a LEDStrip Bricklet. + * + * The {@link led_strip_get_identity} function and the + * {@link IPCON_CALLBACK_ENUMERATE} callback of the IP Connection have a + * \c device_identifier parameter to specify the Brick's or Bricklet's type. + */ +#define LED_STRIP_DEVICE_IDENTIFIER 231 + +/** + * \ingroup BrickletLEDStrip + * + * Creates the device object \c led_strip with the unique device ID \c uid and adds + * it to the IPConnection \c ipcon. + */ +void led_strip_create(LEDStrip *led_strip, const char *uid, IPConnection *ipcon); + +/** + * \ingroup BrickletLEDStrip + * + * Removes the device object \c led_strip from its IPConnection and destroys it. + * The device object cannot be used anymore afterwards. + */ +void led_strip_destroy(LEDStrip *led_strip); + +/** + * \ingroup BrickletLEDStrip + * + * Returns the response expected flag for the function specified by the + * \c function_id parameter. It is *true* if the function is expected to + * send a response, *false* otherwise. + * + * For getter functions this is enabled by default and cannot be disabled, + * because those functions will always send a response. For callback + * configuration functions it is enabled by default too, but can be disabled + * via the led_strip_set_response_expected function. For setter functions it is + * disabled by default and can be enabled. + * + * Enabling the response expected flag for a setter function allows to + * detect timeouts and other error conditions calls of this setter as well. + * The device will then send a response for this purpose. If this flag is + * disabled for a setter function then no response is send and errors are + * silently ignored, because they cannot be detected. + */ +int led_strip_get_response_expected(LEDStrip *led_strip, uint8_t function_id, bool *ret_response_expected); + +/** + * \ingroup BrickletLEDStrip + * + * Changes the response expected flag of the function specified by the + * \c function_id parameter. This flag can only be changed for setter + * (default value: *false*) and callback configuration functions + * (default value: *true*). For getter functions it is always enabled and + * callbacks it is always disabled. + * + * Enabling the response expected flag for a setter function allows to detect + * timeouts and other error conditions calls of this setter as well. The device + * will then send a response for this purpose. If this flag is disabled for a + * setter function then no response is send and errors are silently ignored, + * because they cannot be detected. + */ +int led_strip_set_response_expected(LEDStrip *led_strip, uint8_t function_id, bool response_expected); + +/** + * \ingroup BrickletLEDStrip + * + * Changes the response expected flag for all setter and callback configuration + * functions of this device at once. + */ +int led_strip_set_response_expected_all(LEDStrip *led_strip, bool response_expected); + +/** + * \ingroup BrickletLEDStrip + * + * Registers a callback with ID \c id to the function \c callback. The + * \c user_data will be given as a parameter of the callback. + */ +void led_strip_register_callback(LEDStrip *led_strip, uint8_t id, void *callback, void *user_data); + +/** + * \ingroup BrickletLEDStrip + * + * Returns the API version (major, minor, release) of the bindings for this + * device. + */ +int led_strip_get_api_version(LEDStrip *led_strip, uint8_t ret_api_version[3]); + +/** + * \ingroup BrickletLEDStrip + * + * Sets the *rgb* values for the LEDs with the given *length* starting + * from *index*. + * + * The maximum length is 16, the index goes from 0 to 319 and the rgb values + * have 8 bits each. + * + * Example: If you set + * + * * index to 5, + * * length to 3, + * * r to [255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + * * g to [0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] and + * * b to [0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + * + * the LED with index 5 will be red, 6 will be green and 7 will be blue. + * + * The colors will be transfered to actual LEDs when the next + * frame duration ends, see {@link led_strip_set_frame_duration}. + * + * Generic approach: + * + * * Set the frame duration to a value that represents + * the number of frames per second you want to achieve. + * * Set all of the LED colors for one frame. + * * Wait for the {@link LED_STRIP_CALLBACK_FRAME_RENDERED} callback. + * * Set all of the LED colors for next frame. + * * Wait for the {@link LED_STRIP_CALLBACK_FRAME_RENDERED} callback. + * * and so on. + * + * This approach ensures that you can change the LED colors with + * a fixed frame rate. + * + * The actual number of controllable LEDs depends on the number of free + * Bricklet ports. See :ref:`here ` for more + * information. A call of {@link led_strip_set_rgb_values} with index + length above the + * bounds is ignored completely. + */ +int led_strip_set_rgb_values(LEDStrip *led_strip, uint16_t index, uint8_t length, uint8_t r[16], uint8_t g[16], uint8_t b[16]); + +/** + * \ingroup BrickletLEDStrip + * + * Returns the rgb with the given *length* starting from the + * given *index*. + * + * The values are the last values that were set by {@link led_strip_set_rgb_values}. + */ +int led_strip_get_rgb_values(LEDStrip *led_strip, uint16_t index, uint8_t length, uint8_t ret_r[16], uint8_t ret_g[16], uint8_t ret_b[16]); + +/** + * \ingroup BrickletLEDStrip + * + * Sets the frame duration in ms. + * + * Example: If you want to achieve 20 frames per second, you should + * set the frame duration to 50ms (50ms * 20 = 1 second). + * + * For an explanation of the general approach see {@link led_strip_set_rgb_values}. + * + * Default value: 100ms (10 frames per second). + */ +int led_strip_set_frame_duration(LEDStrip *led_strip, uint16_t duration); + +/** + * \ingroup BrickletLEDStrip + * + * Returns the frame duration as set by {@link led_strip_set_frame_duration}. + */ +int led_strip_get_frame_duration(LEDStrip *led_strip, uint16_t *ret_duration); + +/** + * \ingroup BrickletLEDStrip + * + * Returns the current supply voltage of the LEDs. The voltage is given in mV. + */ +int led_strip_get_supply_voltage(LEDStrip *led_strip, uint16_t *ret_voltage); + +/** + * \ingroup BrickletLEDStrip + * + * Sets the frequency of the clock in Hz. The range is 10000Hz (10kHz) up to + * 2000000Hz (2MHz). + * + * The Bricklet will choose the nearest achievable frequency, which may + * be off by a few Hz. You can get the exact frequency that is used by + * calling {@link led_strip_get_clock_frequency}. + * + * If you have problems with flickering LEDs, they may be bits flipping. You + * can fix this by either making the connection between the LEDs and the + * Bricklet shorter or by reducing the frequency. + * + * With a decreasing frequency your maximum frames per second will decrease + * too. + * + * The default value is 1.66MHz. + * + * \note + * The frequency in firmware version 2.0.0 is fixed at 2MHz. + * + * .. versionadded:: 2.0.1~(Plugin) + */ +int led_strip_set_clock_frequency(LEDStrip *led_strip, uint32_t frequency); + +/** + * \ingroup BrickletLEDStrip + * + * Returns the currently used clock frequency. + * + * .. versionadded:: 2.0.1~(Plugin) + */ +int led_strip_get_clock_frequency(LEDStrip *led_strip, uint32_t *ret_frequency); + +/** + * \ingroup BrickletLEDStrip + * + * Returns the UID, the UID where the Bricklet is connected to, + * the position, the hardware and firmware version as well as the + * device identifier. + * + * The position can be 'a', 'b', 'c' or 'd'. + * + * The device identifiers can be found :ref:`here `. + * + * .. versionadded:: 2.0.0~(Plugin) + */ +int led_strip_get_identity(LEDStrip *led_strip, char ret_uid[8], char ret_connected_uid[8], char *ret_position, uint8_t ret_hardware_version[3], uint8_t ret_firmware_version[3], uint16_t *ret_device_identifier); + +#endif diff --git a/dependencies/include/tinkerforge/ip_connection.h b/dependencies/include/tinkerforge/ip_connection.h new file mode 100644 index 00000000..5369bf76 --- /dev/null +++ b/dependencies/include/tinkerforge/ip_connection.h @@ -0,0 +1,630 @@ +/* + * Copyright (C) 2012-2013 Matthias Bolte + * Copyright (C) 2011 Olaf Lüke + * + * Redistribution and use in source and binary forms of this file, + * with or without modification, are permitted. + */ + +#ifndef IP_CONNECTION_H +#define IP_CONNECTION_H + +/** + * \defgroup IPConnection IP Connection + */ + +#ifndef __STDC_LIMIT_MACROS + #define __STDC_LIMIT_MACROS +#endif +#include +#include +#include + +#if !defined __cplusplus && defined __GNUC__ + #include +#endif + +#ifdef _WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #include +#else + #include + #include +#endif + +enum { + E_OK = 0, + E_TIMEOUT = -1, + E_NO_STREAM_SOCKET = -2, + E_HOSTNAME_INVALID = -3, + E_NO_CONNECT = -4, + E_NO_THREAD = -5, + E_NOT_ADDED = -6, // unused since v2.0 + E_ALREADY_CONNECTED = -7, + E_NOT_CONNECTED = -8, + E_INVALID_PARAMETER = -9, // error response from device + E_NOT_SUPPORTED = -10, // error response from device + E_UNKNOWN_ERROR_CODE = -11 // error response from device +}; + +#ifdef IPCON_EXPOSE_INTERNALS + +typedef struct _Socket Socket; + +typedef struct { +#ifdef _WIN32 + CRITICAL_SECTION handle; +#else + pthread_mutex_t handle; +#endif +} Mutex; + +void mutex_create(Mutex *mutex); + +void mutex_destroy(Mutex *mutex); + +void mutex_lock(Mutex *mutex); + +void mutex_unlock(Mutex *mutex); + +typedef struct { +#ifdef _WIN32 + HANDLE handle; +#else + pthread_cond_t condition; + pthread_mutex_t mutex; + bool flag; +#endif +} Event; + +typedef struct { +#ifdef _WIN32 + HANDLE handle; +#else + sem_t object; + sem_t *pointer; +#endif +} Semaphore; + +typedef void (*ThreadFunction)(void *opaque); + +typedef struct { +#ifdef _WIN32 + HANDLE handle; + DWORD id; +#else + pthread_t handle; +#endif + ThreadFunction function; + void *opaque; +} Thread; + +typedef struct { + Mutex mutex; + int used; + int allocated; + uint32_t *keys; + void **values; +} Table; + +typedef struct _QueueItem { + struct _QueueItem *next; + int kind; + void *data; + int length; +} QueueItem; + +typedef struct { + Mutex mutex; + Semaphore semaphore; + QueueItem *head; + QueueItem *tail; +} Queue; + +#if defined _MSC_VER || defined __BORLANDC__ + #pragma pack(push) + #pragma pack(1) + #define ATTRIBUTE_PACKED +#elif defined __GNUC__ + #ifdef _WIN32 + // workaround struct packing bug in GCC 4.7 on Windows + // http://gcc.gnu.org/bugzilla/show_bug.cgi?id=52991 + #define ATTRIBUTE_PACKED __attribute__((gcc_struct, packed)) + #else + #define ATTRIBUTE_PACKED __attribute__((packed)) + #endif +#else + #error unknown compiler, do not know how to enable struct packing +#endif + +typedef struct { + uint32_t uid; + uint8_t length; + uint8_t function_id; + uint8_t sequence_number_and_options; + uint8_t error_code_and_future_use; +} ATTRIBUTE_PACKED PacketHeader; + +typedef struct { + PacketHeader header; + uint8_t payload[64]; + uint8_t optional_data[8]; +} ATTRIBUTE_PACKED Packet; + +#if defined _MSC_VER || defined __BORLANDC__ + #pragma pack(pop) +#endif +#undef ATTRIBUTE_PACKED + +#endif // IPCON_EXPOSE_INTERNALS + +typedef struct _IPConnection IPConnection; +typedef struct _IPConnectionPrivate IPConnectionPrivate; +typedef struct _Device Device; +typedef struct _DevicePrivate DevicePrivate; + +#ifdef IPCON_EXPOSE_INTERNALS + +typedef struct _CallbackContext CallbackContext; + +#endif + +typedef void (*EnumerateCallbackFunction)(const char *uid, + const char *connected_uid, + char position, + uint8_t hardware_version[3], + uint8_t firmware_version[3], + uint16_t device_identifier, + uint8_t enumeration_type, + void *user_data); +typedef void (*ConnectedCallbackFunction)(uint8_t connect_reason, + void *user_data); +typedef void (*DisconnectedCallbackFunction)(uint8_t disconnect_reason, + void *user_data); + +#ifdef IPCON_EXPOSE_INTERNALS + +typedef void (*CallbackWrapperFunction)(DevicePrivate *device_p, Packet *packet); + +#endif + +/** + * \internal + */ +struct _Device { + DevicePrivate *p; +}; + +#ifdef IPCON_EXPOSE_INTERNALS + +#define DEVICE_NUM_FUNCTION_IDS 256 + +/** + * \internal + */ +struct _DevicePrivate { + uint32_t uid; + + IPConnectionPrivate *ipcon_p; + + uint8_t api_version[3]; + + Mutex request_mutex; + + uint8_t expected_response_function_id; // protected by request_mutex + uint8_t expected_response_sequence_number; // protected by request_mutex + Mutex response_mutex; + Packet response_packet; // protected by response_mutex + Event response_event; + int response_expected[DEVICE_NUM_FUNCTION_IDS]; + + void *registered_callbacks[DEVICE_NUM_FUNCTION_IDS]; + void *registered_callback_user_data[DEVICE_NUM_FUNCTION_IDS]; + CallbackWrapperFunction callback_wrappers[DEVICE_NUM_FUNCTION_IDS]; +}; + +/** + * \internal + */ +enum { + DEVICE_RESPONSE_EXPECTED_INVALID_FUNCTION_ID = 0, + DEVICE_RESPONSE_EXPECTED_ALWAYS_TRUE, // getter + DEVICE_RESPONSE_EXPECTED_ALWAYS_FALSE, // callback + DEVICE_RESPONSE_EXPECTED_TRUE, // setter + DEVICE_RESPONSE_EXPECTED_FALSE // setter, default +}; + +/** + * \internal + */ +void device_create(Device *device, const char *uid, + IPConnectionPrivate *ipcon_p, uint8_t api_version_major, + uint8_t api_version_minor, uint8_t api_version_release); + +/** + * \internal + */ +void device_destroy(Device *device); + +/** + * \internal + */ +int device_get_response_expected(DevicePrivate *device_p, uint8_t function_id, + bool *ret_response_expected); + +/** + * \internal + */ +int device_set_response_expected(DevicePrivate *device_p, uint8_t function_id, + bool response_expected); + +/** + * \internal + */ +int device_set_response_expected_all(DevicePrivate *device_p, bool response_expected); + +/** + * \internal + */ +void device_register_callback(DevicePrivate *device_p, uint8_t id, void *callback, + void *user_data); + +/** + * \internal + */ +int device_get_api_version(DevicePrivate *device_p, uint8_t ret_api_version[3]); + +/** + * \internal + */ +int device_send_request(DevicePrivate *device_p, Packet *request, Packet *response); + +#endif // IPCON_EXPOSE_INTERNALS + +/** + * \ingroup IPConnection + * + * Possible IDs for ipcon_register_callback. + */ +enum { + IPCON_CALLBACK_ENUMERATE = 253, + IPCON_CALLBACK_CONNECTED = 0, + IPCON_CALLBACK_DISCONNECTED = 1 +}; + +/** + * \ingroup IPConnection + * + * Possible values for enumeration_type parameter of EnumerateCallback. + */ +enum { + IPCON_ENUMERATION_TYPE_AVAILABLE = 0, + IPCON_ENUMERATION_TYPE_CONNECTED = 1, + IPCON_ENUMERATION_TYPE_DISCONNECTED = 2 +}; + +/** + * \ingroup IPConnection + * + * Possible values for connect_reason parameter of ConnectedCallback. + */ +enum { + IPCON_CONNECT_REASON_REQUEST = 0, + IPCON_CONNECT_REASON_AUTO_RECONNECT = 1 +}; + +/** + * \ingroup IPConnection + * + * Possible values for disconnect_reason parameter of DisconnectedCallback. + */ +enum { + IPCON_DISCONNECT_REASON_REQUEST = 0, + IPCON_DISCONNECT_REASON_ERROR = 1, + IPCON_DISCONNECT_REASON_SHUTDOWN = 2 +}; + +/** + * \ingroup IPConnection + * + * Possible return values of ipcon_get_connection_state. + */ +enum { + IPCON_CONNECTION_STATE_DISCONNECTED = 0, + IPCON_CONNECTION_STATE_CONNECTED = 1, + IPCON_CONNECTION_STATE_PENDING = 2 // auto-reconnect in progress +}; + +/** + * \internal + */ +struct _IPConnection { + IPConnectionPrivate *p; +}; + +#ifdef IPCON_EXPOSE_INTERNALS + +#define IPCON_NUM_CALLBACK_IDS 256 + +/** + * \internal + */ +struct _IPConnectionPrivate { +#ifdef _WIN32 + bool wsa_startup_done; // protected by socket_mutex +#endif + + char *host; + uint16_t port; + + uint32_t timeout; // in msec + + bool auto_reconnect; + bool auto_reconnect_allowed; + bool auto_reconnect_pending; + + Mutex sequence_number_mutex; + uint8_t next_sequence_number; // protected by sequence_number_mutex + + Table devices; + + void *registered_callbacks[IPCON_NUM_CALLBACK_IDS]; + void *registered_callback_user_data[IPCON_NUM_CALLBACK_IDS]; + + Mutex socket_mutex; + Socket *socket; // protected by socket_mutex + uint64_t socket_id; // protected by socket_mutex + + bool receive_flag; + Thread receive_thread; // protected by socket_mutex + + CallbackContext *callback; + + bool disconnect_probe_flag; + Thread disconnect_probe_thread; // protected by socket_mutex + Event disconnect_probe_event; + + Semaphore wait; +}; + +#endif // IPCON_EXPOSE_INTERNALS + +/** + * \ingroup IPConnection + * + * Creates an IP Connection object that can be used to enumerate the available + * devices. It is also required for the constructor of Bricks and Bricklets. + */ +void ipcon_create(IPConnection *ipcon); + +/** + * \ingroup IPConnection + * + * Destroys the IP Connection object. Calls ipcon_disconnect internally. + * The connection to the Brick Daemon gets closed and the threads of the + * IP Connection are terminated. + */ +void ipcon_destroy(IPConnection *ipcon); + +/** + * \ingroup IPConnection + * + * Creates a TCP/IP connection to the given \c host and c\ port. The host and + * port can point to a Brick Daemon or to a WIFI/Ethernet Extension. + * + * Devices can only be controlled when the connection was established + * successfully. + * + * Blocks until the connection is established and returns an error code if + * there is no Brick Daemon or WIFI/Ethernet Extension listening at the given + * host and port. + */ +int ipcon_connect(IPConnection *ipcon, const char *host, uint16_t port); + +/** + * \ingroup IPConnection + * + * Disconnects the TCP/IP connection from the Brick Daemon or the WIFI/Ethernet + * Extension. + */ +int ipcon_disconnect(IPConnection *ipcon); + +/** + * \ingroup IPConnection + * + * Can return the following states: + * + * - IPCON_CONNECTION_STATE_DISCONNECTED: No connection is established. + * - IPCON_CONNECTION_STATE_CONNECTED: A connection to the Brick Daemon or + * the WIFI/Ethernet Extension is established. + * - IPCON_CONNECTION_STATE_PENDING: IP Connection is currently trying to + * connect. + */ +int ipcon_get_connection_state(IPConnection *ipcon); + +/** + * \ingroup IPConnection + * + * Enables or disables auto-reconnect. If auto-reconnect is enabled, + * the IP Connection will try to reconnect to the previously given + * host and port, if the connection is lost. + * + * Default value is *true*. + */ +void ipcon_set_auto_reconnect(IPConnection *ipcon, bool auto_reconnect); + +/** + * \ingroup IPConnection + * + * Returns *true* if auto-reconnect is enabled, *false* otherwise. + */ +bool ipcon_get_auto_reconnect(IPConnection *ipcon); + +/** + * \ingroup IPConnection + * + * Sets the timeout in milliseconds for getters and for setters for which the + * response expected flag is activated. + * + * Default timeout is 2500. + */ +void ipcon_set_timeout(IPConnection *ipcon, uint32_t timeout); + +/** + * \ingroup IPConnection + * + * Returns the timeout as set by ipcon_set_timeout. + */ +uint32_t ipcon_get_timeout(IPConnection *ipcon); + +/** + * \ingroup IPConnection + * + * Broadcasts an enumerate request. All devices will respond with an enumerate + * callback. + */ +int ipcon_enumerate(IPConnection *ipcon); + +/** + * \ingroup IPConnection + * + * Stops the current thread until ipcon_unwait is called. + * + * This is useful if you rely solely on callbacks for events, if you want + * to wait for a specific callback or if the IP Connection was created in + * a thread. + * + * ipcon_wait and ipcon_unwait act in the same way as "acquire" and "release" + * of a semaphore. + */ +void ipcon_wait(IPConnection *ipcon); + +/** + * \ingroup IPConnection + * + * Unwaits the thread previously stopped by ipcon_wait. + * + * ipcon_wait and ipcon_unwait act in the same way as "acquire" and "release" + * of a semaphore. + */ +void ipcon_unwait(IPConnection *ipcon); + +/** + * \ingroup IPConnection + * + * Registers a callback for a given ID. + */ +void ipcon_register_callback(IPConnection *ipcon, uint8_t id, + void *callback, void *user_data); + +#ifdef IPCON_EXPOSE_INTERNALS + +/** + * \internal + */ +int packet_header_create(PacketHeader *header, uint8_t length, + uint8_t function_id, IPConnectionPrivate *ipcon_p, + DevicePrivate *device_p); + +/** + * \internal + */ +uint8_t packet_header_get_sequence_number(PacketHeader *header); + +/** + * \internal + */ +void packet_header_set_sequence_number(PacketHeader *header, + uint8_t sequence_number); + +/** + * \internal + */ +uint8_t packet_header_get_response_expected(PacketHeader *header); + +/** + * \internal + */ +void packet_header_set_response_expected(PacketHeader *header, + uint8_t response_expected); + +/** + * \internal + */ +uint8_t packet_header_get_error_code(PacketHeader *header); + +/** + * \internal + */ +int16_t leconvert_int16_to(int16_t native); + +/** + * \internal + */ +uint16_t leconvert_uint16_to(uint16_t native); + +/** + * \internal + */ +int32_t leconvert_int32_to(int32_t native); + +/** + * \internal + */ +uint32_t leconvert_uint32_to(uint32_t native); + +/** + * \internal + */ +int64_t leconvert_int64_to(int64_t native); + +/** + * \internal + */ +uint64_t leconvert_uint64_to(uint64_t native); + +/** + * \internal + */ +float leconvert_float_to(float native); + +/** + * \internal + */ +int16_t leconvert_int16_from(int16_t little); + +/** + * \internal + */ +uint16_t leconvert_uint16_from(uint16_t little); + +/** + * \internal + */ +int32_t leconvert_int32_from(int32_t little); + +/** + * \internal + */ +uint32_t leconvert_uint32_from(uint32_t little); + +/** + * \internal + */ +int64_t leconvert_int64_from(int64_t little); + +/** + * \internal + */ +uint64_t leconvert_uint64_from(uint64_t little); + +/** + * \internal + */ +float leconvert_float_from(float little); + +#endif // IPCON_EXPOSE_INTERNALS + +#endif diff --git a/dependencies/tinkerforge_c_bindings_2_0_13.zip b/dependencies/tinkerforge_c_bindings_2_0_13.zip new file mode 100644 index 0000000000000000000000000000000000000000..198d9688a7fcee9297adf563a5a891ba9bd6e6c7 GIT binary patch literal 381221 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

5&ID3zKp(_7T$v>-B(J{WS; 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

H#7V!UE^D{D_(fV<$`t~AN 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

kn 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

pFRt1qV#?@6k?la!O$t}z44$r1=Ka&Q~34!V77 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 literal 0 HcmV?d00001 diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt index 633e8dcd..6e2acfdc 100644 --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -6,21 +6,11 @@ SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/leddevice) #add libusb and pthreads (required for the Lighpack usb device) find_package(libusb-1.0 REQUIRED) find_package(Threads REQUIRED) -if(ENABLE_TINKERFORGE) - find_package(libtinkerforge-1.0 REQUIRED) -endif(ENABLE_TINKERFORGE) - include_directories( ../../include/hidapi ${LIBUSB_1_INCLUDE_DIRS}) # for Lightpack device - -if(ENABLE_TINKERFORGE) - include_directories( - ${LIBTINKERFORGE_1_INCLUDE_DIRS}) # for Tinkerforge device -endif(ENABLE_TINKERFORGE) - # Group the headers that go through the MOC compiler SET(Leddevice_QT_HEADERS ${CURRENT_SOURCE_DIR}/LedDeviceAdalight.h @@ -107,9 +97,7 @@ target_link_libraries(leddevice ) if(ENABLE_TINKERFORGE) - target_link_libraries(leddevice - ${LIBTINKERFORGE_1_LIBRARIES} - ) + target_link_libraries(leddevice tinkerforge) endif() if(APPLE) diff --git a/libsrc/leddevice/LedDeviceTinkerforge.cpp b/libsrc/leddevice/LedDeviceTinkerforge.cpp index 04cfb151..e99f482f 100644 --- a/libsrc/leddevice/LedDeviceTinkerforge.cpp +++ b/libsrc/leddevice/LedDeviceTinkerforge.cpp @@ -9,15 +9,15 @@ static const unsigned MAX_NUM_LEDS = 320; static const unsigned MAX_NUM_LEDS_SETTABLE = 16; -LedDeviceTinkerforge::LedDeviceTinkerforge(const std::string &host, uint16_t port, const std::string &uid, const unsigned interval) : - LedDevice(), - _host(host), - _port(port), - _uid(uid), - _interval(interval), - _ipConnection(nullptr), - _ledStrip(nullptr), - _colorChannelSize(0) +LedDeviceTinkerforge::LedDeviceTinkerforge(const std::string & host, uint16_t port, const std::string & uid, const unsigned interval) : + LedDevice(), + _host(host), + _port(port), + _uid(uid), + _interval(interval), + _ipConnection(nullptr), + _ledStrip(nullptr), + _colorChannelSize(0) { // empty } @@ -29,14 +29,22 @@ LedDeviceTinkerforge::~LedDeviceTinkerforge() { switchOff(); } - if (_ipConnection != nullptr) - delete _ipConnection; - if (_ledStrip != nullptr) - delete _ledStrip; + + // Clean up claimed resources + delete _ipConnection; + delete _ledStrip; } int LedDeviceTinkerforge::open() { + // Check if connection is already createds + if (_ipConnection != nullptr) + { + std::cout << "Attempt to open existing connection; close before opening" << std::endl; + return -1; + } + + // Initialise a new connection _ipConnection = new IPConnection; ipcon_create(_ipConnection); @@ -47,6 +55,7 @@ int LedDeviceTinkerforge::open() return -1; } + // Create the 'LedStrip' _ledStrip = new LEDStrip; led_strip_create(_ledStrip, _uid.c_str(), _ipConnection); @@ -62,8 +71,6 @@ int LedDeviceTinkerforge::open() int LedDeviceTinkerforge::write(const std::vector &ledValues) { - std::cerr << "Write" << std::endl; - unsigned nrLedValues = ledValues.size(); if (nrLedValues > MAX_NUM_LEDS) @@ -80,62 +87,57 @@ int LedDeviceTinkerforge::write(const std::vector &ledValues) } _colorChannelSize = nrLedValues; - auto redIt = _redChannel.begin(); + auto redIt = _redChannel.begin(); auto greenIt = _greenChannel.begin(); - auto blueIt = _blueChannel.begin(); + auto blueIt = _blueChannel.begin(); for (const ColorRgb &ledValue : ledValues) { *redIt = ledValue.red; - redIt++; + ++redIt; *greenIt = ledValue.green; - greenIt++; + ++greenIt; *blueIt = ledValue.blue; - blueIt++; + ++blueIt; } - return transferLedData(_ledStrip, 0, _colorChannelSize, &_redChannel[0], &_greenChannel[0], &_blueChannel[0]); + return transferLedData(_ledStrip, 0, _colorChannelSize, _redChannel.data(), _greenChannel.data(), _blueChannel.data()); } int LedDeviceTinkerforge::switchOff() { std::cerr << "Switchoff" << std::endl; - std::fill(_redChannel.begin(), _redChannel.end(), 0); + std::fill(_redChannel.begin(), _redChannel.end(), 0); std::fill(_greenChannel.begin(), _greenChannel.end(), 0); - std::fill(_blueChannel.begin(), _blueChannel.end(), 0); + std::fill(_blueChannel.begin(), _blueChannel.end(), 0); - return transferLedData(_ledStrip, 0, _colorChannelSize, &_redChannel[0], &_greenChannel[0], &_blueChannel[0]); + return transferLedData(_ledStrip, 0, _colorChannelSize, _redChannel.data(), _greenChannel.data(), _blueChannel.data()); } int LedDeviceTinkerforge::transferLedData(LEDStrip *ledStrip, unsigned index, unsigned length, uint8_t *redChannel, uint8_t *greenChannel, uint8_t *blueChannel) { - // we need that array size no matter how many leds will really be set - uint8_t _reds[MAX_NUM_LEDS_SETTABLE]; - uint8_t _greens[MAX_NUM_LEDS_SETTABLE]; - uint8_t _blues[MAX_NUM_LEDS_SETTABLE]; - - int status = E_INVALID_PARAMETER; - unsigned i; - unsigned int copyLength; - - if (index >= 0 && length > 0 && index < length && length <= MAX_NUM_LEDS) + if (length == 0 || index >= length || length > MAX_NUM_LEDS) { - for (i = index; i < length; i += MAX_NUM_LEDS_SETTABLE) + return E_INVALID_PARAMETER; + } + + uint8_t * redPtr = redChannel; + uint8_t * greenPtr = greenChannel; + uint8_t * bluePtr = blueChannel; + for (unsigned i=index; i length) ? length - i : MAX_NUM_LEDS_SETTABLE; + const int status = led_strip_set_rgb_values(ledStrip, i, copyLength, redPtr, greenPtr, bluePtr); + redPtr += copyLength; + greenPtr += copyLength; + bluePtr += copyLength; + + if (status != E_OK) { - copyLength = (i + MAX_NUM_LEDS_SETTABLE > length) ? length - i : MAX_NUM_LEDS_SETTABLE; - - memcpy(_reds, redChannel + i, copyLength * sizeof(uint8_t)); - memcpy(_greens, greenChannel + i, copyLength * sizeof(uint8_t)); - memcpy(_blues, blueChannel + i, copyLength * sizeof(uint8_t)); - - status = led_strip_set_rgb_values(ledStrip, i, copyLength, _reds, _greens, _blues); - - if (status != E_OK) - { - std::cerr << "Setting led values failed with status " << status << std::endl; - break; - } + std::cerr << "Setting led values failed with status " << status << std::endl; + return status; } } - return status; + + return E_OK; } From e549e15c3ff7f4c5afc500150453e30776d882e6 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Sat, 8 Mar 2014 19:55:23 +0100 Subject: [PATCH 22/78] Fixed call to thinkerforge library Former-commit-id: e2764d3ecbf7d462ad5d967572b3ea37548fec73 --- libsrc/leddevice/LedDeviceTinkerforge.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/libsrc/leddevice/LedDeviceTinkerforge.cpp b/libsrc/leddevice/LedDeviceTinkerforge.cpp index e99f482f..83367042 100644 --- a/libsrc/leddevice/LedDeviceTinkerforge.cpp +++ b/libsrc/leddevice/LedDeviceTinkerforge.cpp @@ -106,7 +106,6 @@ int LedDeviceTinkerforge::write(const std::vector &ledValues) int LedDeviceTinkerforge::switchOff() { - std::cerr << "Switchoff" << std::endl; std::fill(_redChannel.begin(), _redChannel.end(), 0); std::fill(_greenChannel.begin(), _greenChannel.end(), 0); std::fill(_blueChannel.begin(), _blueChannel.end(), 0); @@ -121,17 +120,18 @@ int LedDeviceTinkerforge::transferLedData(LEDStrip *ledStrip, unsigned index, un return E_INVALID_PARAMETER; } - uint8_t * redPtr = redChannel; - uint8_t * greenPtr = greenChannel; - uint8_t * bluePtr = blueChannel; + uint8_t reds[MAX_NUM_LEDS_SETTABLE]; + uint8_t greens[MAX_NUM_LEDS_SETTABLE]; + uint8_t blues[MAX_NUM_LEDS_SETTABLE]; + for (unsigned i=index; i length) ? length - i : MAX_NUM_LEDS_SETTABLE; - const int status = led_strip_set_rgb_values(ledStrip, i, copyLength, redPtr, greenPtr, bluePtr); - redPtr += copyLength; - greenPtr += copyLength; - bluePtr += copyLength; + memcpy(reds, redChannel + i, copyLength); + memcpy(greens, greenChannel + i, copyLength); + memcpy(blues, blueChannel + i, copyLength); + const int status = led_strip_set_rgb_values(ledStrip, i, copyLength, reds, greens, blues); if (status != E_OK) { std::cerr << "Setting led values failed with status " << status << std::endl; From e22c720e68ccbd3bb287107ad8286620fddf1344 Mon Sep 17 00:00:00 2001 From: johan Date: Sun, 9 Mar 2014 11:36:46 +0100 Subject: [PATCH 23/78] Add hyperion-usbasp led devices Remove all WS281x direct UART code (does not work reliable) Former-commit-id: cd8103058d4ce0cd3280c7a2c5370397a14acf5c --- libsrc/leddevice/CMakeLists.txt | 6 +- libsrc/leddevice/LedDeviceFactory.cpp | 39 +- libsrc/leddevice/LedDeviceHyperionUsbasp.cpp | 205 ++++++++++ libsrc/leddevice/LedDeviceHyperionUsbasp.h | 88 +++++ libsrc/leddevice/LedDeviceWs2811.cpp | 182 --------- libsrc/leddevice/LedDeviceWs2811.h | 147 ------- libsrc/leddevice/LedDeviceWs2812b.cpp | 96 ----- libsrc/leddevice/LedDeviceWs2812b.h | 60 --- test/CMakeLists.txt | 17 - test/DetermineWs2811Timing.cpp | 62 --- test/TestNonInvWs2812b.cpp | 209 ---------- test/TestNonUniformWs2812b.cpp | 188 --------- test/TestRs232HighSpeed.cpp | 260 ------------- test/TestUartHighSpeed.cpp | 387 ------------------- 14 files changed, 308 insertions(+), 1638 deletions(-) create mode 100644 libsrc/leddevice/LedDeviceHyperionUsbasp.cpp create mode 100644 libsrc/leddevice/LedDeviceHyperionUsbasp.h delete mode 100644 libsrc/leddevice/LedDeviceWs2811.cpp delete mode 100644 libsrc/leddevice/LedDeviceWs2811.h delete mode 100644 libsrc/leddevice/LedDeviceWs2812b.cpp delete mode 100644 libsrc/leddevice/LedDeviceWs2812b.h delete mode 100644 test/DetermineWs2811Timing.cpp delete mode 100644 test/TestNonInvWs2812b.cpp delete mode 100644 test/TestNonUniformWs2812b.cpp delete mode 100644 test/TestRs232HighSpeed.cpp delete mode 100644 test/TestUartHighSpeed.cpp diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt index 6bfc2fe8..608b1cb9 100644 --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -28,8 +28,7 @@ SET(Leddevice_HEADERS ${CURRENT_SOURCE_DIR}/LedDevicePiBlaster.h ${CURRENT_SOURCE_DIR}/LedDeviceSedu.h ${CURRENT_SOURCE_DIR}/LedDeviceTest.h - ${CURRENT_SOURCE_DIR}/LedDeviceWs2812b.h - ${CURRENT_SOURCE_DIR}/LedDeviceWs2811.h + ${CURRENT_SOURCE_DIR}/LedDeviceHyperionUsbasp.h ) SET(Leddevice_SOURCES @@ -44,8 +43,7 @@ SET(Leddevice_SOURCES ${CURRENT_SOURCE_DIR}/LedDevicePiBlaster.cpp ${CURRENT_SOURCE_DIR}/LedDeviceSedu.cpp ${CURRENT_SOURCE_DIR}/LedDeviceTest.cpp - ${CURRENT_SOURCE_DIR}/LedDeviceWs2811.cpp - ${CURRENT_SOURCE_DIR}/LedDeviceWs2812b.cpp + ${CURRENT_SOURCE_DIR}/LedDeviceHyperionUsbasp.cpp ) if(ENABLE_SPIDEV) diff --git a/libsrc/leddevice/LedDeviceFactory.cpp b/libsrc/leddevice/LedDeviceFactory.cpp index 2a9aeb4e..855fe818 100644 --- a/libsrc/leddevice/LedDeviceFactory.cpp +++ b/libsrc/leddevice/LedDeviceFactory.cpp @@ -23,8 +23,7 @@ #include "LedDevicePiBlaster.h" #include "LedDeviceSedu.h" #include "LedDeviceTest.h" -#include "LedDeviceWs2811.h" -#include "LedDeviceWs2812b.h" +#include "LedDeviceHyperionUsbasp.h" LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) { @@ -87,23 +86,6 @@ LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) device = deviceWs2801; } #endif -// else if (type == "ws2811") -// { -// const std::string output = deviceConfig["output"].asString(); -// const std::string outputSpeed = deviceConfig["output"].asString(); -// const std::string timingOption = deviceConfig["timingOption"].asString(); - -// ws2811::SpeedMode speedMode = (outputSpeed == "high")? ws2811::highspeed : ws2811::lowspeed; -// if (outputSpeed != "high" && outputSpeed != "low") -// { -// std::cerr << "Incorrect speed-mode selected for WS2811: " << outputSpeed << " != {'high', 'low'}" << std::endl; -// } - -// LedDeviceWs2811 * deviceWs2811 = new LedDeviceWs2811(output, ws2811::fromString(timingOption, ws2811::option_2855), speedMode); -// deviceWs2811->open(); - -// device = deviceWs2811; -// } else if (type == "lightpack") { const std::string output = deviceConfig.get("output", "").asString(); @@ -147,18 +129,23 @@ LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) device = deviceSedu; } + else if (type == "hyperion-usbasp-ws2801") + { + LedDeviceHyperionUsbasp * deviceHyperionUsbasp = new LedDeviceHyperionUsbasp(LedDeviceHyperionUsbasp::CMD_WRITE_WS2801); + deviceHyperionUsbasp->open(); + device = deviceHyperionUsbasp; + } + else if (type == "hyperion-usbasp-ws2812") + { + LedDeviceHyperionUsbasp * deviceHyperionUsbasp = new LedDeviceHyperionUsbasp(LedDeviceHyperionUsbasp::CMD_WRITE_WS2812); + deviceHyperionUsbasp->open(); + device = deviceHyperionUsbasp; + } else if (type == "test") { const std::string output = deviceConfig["output"].asString(); device = new LedDeviceTest(output); } - else if (type == "ws2812b") - { - LedDeviceWs2812b * deviceWs2812b = new LedDeviceWs2812b(); - deviceWs2812b->open(); - - device = deviceWs2812b; - } else { std::cout << "Unable to create device " << type << std::endl; diff --git a/libsrc/leddevice/LedDeviceHyperionUsbasp.cpp b/libsrc/leddevice/LedDeviceHyperionUsbasp.cpp new file mode 100644 index 00000000..9eddde90 --- /dev/null +++ b/libsrc/leddevice/LedDeviceHyperionUsbasp.cpp @@ -0,0 +1,205 @@ +// stl includes +#include +#include + +// Local Hyperion includes +#include "LedDeviceHyperionUsbasp.h" + +// Static constants which define the Hyperion Usbasp device +uint16_t LedDeviceHyperionUsbasp::_usbVendorId = 0x16c0; +uint16_t LedDeviceHyperionUsbasp::_usbProductId = 0x05dc; +std::string LedDeviceHyperionUsbasp::_usbProductDescription = "Hyperion led controller"; + + +LedDeviceHyperionUsbasp::LedDeviceHyperionUsbasp(uint8_t writeLedsCommand) : + LedDevice(), + _writeLedsCommand(writeLedsCommand), + _libusbContext(nullptr), + _deviceHandle(nullptr), + _ledCount(256) +{ +} + +LedDeviceHyperionUsbasp::~LedDeviceHyperionUsbasp() +{ + if (_deviceHandle != nullptr) + { + libusb_release_interface(_deviceHandle, 0); + libusb_attach_kernel_driver(_deviceHandle, 0); + libusb_close(_deviceHandle); + + _deviceHandle = nullptr; + } + + if (_libusbContext != nullptr) + { + libusb_exit(_libusbContext); + _libusbContext = nullptr; + } +} + +int LedDeviceHyperionUsbasp::open() +{ + int error; + + // initialize the usb context + if ((error = libusb_init(&_libusbContext)) != LIBUSB_SUCCESS) + { + std::cerr << "Error while initializing USB context(" << error << "): " << libusb_error_name(error) << std::endl; + _libusbContext = nullptr; + return -1; + } + //libusb_set_debug(_libusbContext, 3); + std::cout << "USB context initialized" << std::endl; + + // retrieve the list of usb devices + libusb_device ** deviceList; + ssize_t deviceCount = libusb_get_device_list(_libusbContext, &deviceList); + + // iterate the list of devices + for (ssize_t i = 0 ; i < deviceCount; ++i) + { + // try to open and initialize the device + error = testAndOpen(deviceList[i]); + + if (error == 0) + { + // a device was sucessfully opened. break from list + break; + } + } + + // free the device list + libusb_free_device_list(deviceList, 1); + + if (_deviceHandle == nullptr) + { + std::cerr << "No " << _usbProductDescription << " has been found" << std::endl; + } + + return _deviceHandle == nullptr ? -1 : 0; +} + +int LedDeviceHyperionUsbasp::testAndOpen(libusb_device * device) +{ + libusb_device_descriptor deviceDescriptor; + int error = libusb_get_device_descriptor(device, &deviceDescriptor); + if (error != LIBUSB_SUCCESS) + { + std::cerr << "Error while retrieving device descriptor(" << error << "): " << libusb_error_name(error) << std::endl; + return -1; + } + + if (deviceDescriptor.idVendor == _usbVendorId && + deviceDescriptor.idProduct == _usbProductId && + deviceDescriptor.iProduct != 0 && + getString(device, deviceDescriptor.iProduct) == _usbProductDescription) + { + // get the hardware address + int busNumber = libusb_get_bus_number(device); + int addressNumber = libusb_get_device_address(device); + + std::cout << _usbProductDescription << " found: bus=" << busNumber << " address=" << addressNumber << std::endl; + + try + { + _deviceHandle = openDevice(device); + std::cout << _usbProductDescription << " successfully opened" << std::endl; + return 0; + } + catch(int e) + { + _deviceHandle = nullptr; + std::cerr << "Unable to open " << _usbProductDescription << ". Searching for other device(" << e << "): " << libusb_error_name(e) << std::endl; + } + } + + return -1; +} + +int LedDeviceHyperionUsbasp::write(const std::vector &ledValues) +{ + _ledCount = ledValues.size(); + + int nbytes = libusb_control_transfer( + _deviceHandle, // device handle + LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE | LIBUSB_ENDPOINT_OUT, // request type + _writeLedsCommand, // request + 0, // value + 0, // index + (uint8_t *) ledValues.data(), // data + (3*_ledCount) & 0xffff, // length + 5000); // timeout + + // Disabling interupts for a little while on the device results in a PIPE error. All seems to keep functioning though... + if(nbytes < 0 && nbytes != LIBUSB_ERROR_PIPE) + { + std::cerr << "Error while writing data to " << _usbProductDescription << " (" << libusb_error_name(nbytes) << ")" << std::endl; + return -1; + } + + return 0; +} + +int LedDeviceHyperionUsbasp::switchOff() +{ + std::vector ledValues(_ledCount, ColorRgb::BLACK); + return write(ledValues); +} + +libusb_device_handle * LedDeviceHyperionUsbasp::openDevice(libusb_device *device) +{ + libusb_device_handle * handle = nullptr; + + int error = libusb_open(device, &handle); + if (error != LIBUSB_SUCCESS) + { + std::cerr << "unable to open device(" << error << "): " << libusb_error_name(error) << std::endl; + throw error; + } + + // detach kernel driver if it is active + if (libusb_kernel_driver_active(handle, 0) == 1) + { + error = libusb_detach_kernel_driver(handle, 0); + if (error != LIBUSB_SUCCESS) + { + std::cerr << "unable to detach kernel driver(" << error << "): " << libusb_error_name(error) << std::endl; + libusb_close(handle); + throw error; + } + } + + error = libusb_claim_interface(handle, 0); + if (error != LIBUSB_SUCCESS) + { + std::cerr << "unable to claim interface(" << error << "): " << libusb_error_name(error) << std::endl; + libusb_attach_kernel_driver(handle, 0); + libusb_close(handle); + throw error; + } + + return handle; +} + +std::string LedDeviceHyperionUsbasp::getString(libusb_device * device, int stringDescriptorIndex) +{ + libusb_device_handle * handle = nullptr; + + int error = libusb_open(device, &handle); + if (error != LIBUSB_SUCCESS) + { + throw error; + } + + char buffer[256]; + error = libusb_get_string_descriptor_ascii(handle, stringDescriptorIndex, reinterpret_cast(buffer), sizeof(buffer)); + if (error <= 0) + { + libusb_close(handle); + throw error; + } + + libusb_close(handle); + return std::string(buffer, error); +} diff --git a/libsrc/leddevice/LedDeviceHyperionUsbasp.h b/libsrc/leddevice/LedDeviceHyperionUsbasp.h new file mode 100644 index 00000000..a8f91cc7 --- /dev/null +++ b/libsrc/leddevice/LedDeviceHyperionUsbasp.h @@ -0,0 +1,88 @@ +#pragma once + +// stl includes +#include +#include +#include + +// libusb include +#include + +// Hyperion includes +#include + +/// +/// LedDevice implementation for a lightpack device (http://code.google.com/p/light-pack/) +/// +class LedDeviceHyperionUsbasp : public LedDevice +{ +public: + // Commands to the Device + enum Commands { + CMD_WRITE_WS2801 = 10, + CMD_WRITE_WS2812 = 11 + }; + + /// + /// Constructs the LedDeviceLightpack + /// + LedDeviceHyperionUsbasp(uint8_t writeLedsCommand); + + /// + /// Destructor of the LedDevice; closes the output device if it is open + /// + virtual ~LedDeviceHyperionUsbasp(); + + /// + /// Opens and configures the output device + /// + /// @return Zero on succes else negative + /// + int open(); + + /// + /// Writes the RGB-Color values to the leds. + /// + /// @param[in] ledValues The RGB-color per led + /// + /// @return Zero on success else negative + /// + virtual int write(const std::vector& ledValues); + + /// + /// Switch the leds off + /// + /// @return Zero on success else negative + /// + virtual int switchOff(); + +private: + /// + /// Test if the device is a Hyperion Usbasp device + /// + /// @return Zero on succes else negative + /// + int testAndOpen(libusb_device * device); + + static libusb_device_handle * openDevice(libusb_device * device); + + static std::string getString(libusb_device * device, int stringDescriptorIndex); + +private: + /// command to write the leds + const uint8_t _writeLedsCommand; + + /// libusb context + libusb_context * _libusbContext; + + /// libusb device handle + libusb_device_handle * _deviceHandle; + + /// Number of leds + int _ledCount; + + /// Usb device identifiers + static uint16_t _usbVendorId; + static uint16_t _usbProductId; + static std::string _usbProductDescription; +}; diff --git a/libsrc/leddevice/LedDeviceWs2811.cpp b/libsrc/leddevice/LedDeviceWs2811.cpp deleted file mode 100644 index 9fe0f8a3..00000000 --- a/libsrc/leddevice/LedDeviceWs2811.cpp +++ /dev/null @@ -1,182 +0,0 @@ - -// Local hyperion includes -#include "LedDeviceWs2811.h" - - -ws2811::SignalTiming ws2811::fromString(const std::string& signalTiming, const SignalTiming defaultValue) -{ - SignalTiming result = defaultValue; - if (signalTiming == "3755" || signalTiming == "option_3755") - { - result = option_3755; - } - else if (signalTiming == "3773" || signalTiming == "option_3773") - { - result = option_3773; - } - else if (signalTiming == "2855" || signalTiming == "option_2855") - { - result = option_2855; - } - else if (signalTiming == "2882" || signalTiming == "option_2882") - { - result = option_2882; - } - - return result; -} - -unsigned ws2811::getBaudrate(const SpeedMode speedMode) -{ - switch (speedMode) - { - case highspeed: - // Bit length: 125ns - return 8000000; - case lowspeed: - // Bit length: 250ns - return 4000000; - } - - return 0; -} -inline unsigned ws2811::getLength(const SignalTiming timing, const TimeOption option) -{ - switch (timing) - { - case option_3755: - // Reference: http://www.mikrocontroller.net/attachment/180459/WS2812B_preliminary.pdf - // Unit length: 125ns - switch (option) - { - case T0H: - return 3; // 400ns +-150ns - case T0L: - return 7; // 850ns +-150ns - case T1H: - return 7; // 800ns +-150ns - case T1L: - return 3; // 450ns +-150ns - } - case option_3773: - // Reference: www.adafruit.com/datasheets/WS2812.pdf‎ - // Unit length: 125ns - switch (option) - { - case T0H: - return 3; // 350ns +-150ns - case T0L: - return 7; // 800ns +-150ns - case T1H: - return 7; // 700ns +-150ns - case T1L: - return 3; // 600ns +-150ns - } - case option_2855: - // Reference: www.adafruit.com/datasheets/WS2811.pdf‎ - // Unit length: 250ns - switch (option) - { - case T0H: - return 2; // 500ns +-150ns - case T0L: - return 8; // 2000ns +-150ns - case T1H: - return 5; // 1200ns +-150ns - case T1L: - return 5; // 1300ns +-150ns - } - case option_2882: - // Reference: www.szparkson.net/download/WS2811.pdf‎ - // Unit length: 250ns - switch (option) - { - case T0H: - return 2; // 500ns +-150ns - case T0L: - return 8; // 2000ns +-150ns - case T1H: - return 8; // 2000ns +-150ns - case T1L: - return 2; // 500ns +-150ns - } - default: - std::cerr << "Unknown signal timing for ws2811: " << timing << std::endl; - } - return 0; -} - -uint8_t ws2811::bitToSignal(unsigned lenHigh) -{ - // Sanity check on the length of the 'high' signal - assert(0 < lenHigh && lenHigh < 10); - - uint8_t result = 0x00; - for (unsigned i=1; i & ledValues) -{ - if (_ledBuffer.size() != ledValues.size() * 3) - { - _ledBuffer.resize(ledValues.size() * 3); - } - - auto bufIt = _ledBuffer.begin(); - for (const ColorRgb & color : ledValues) - { - *bufIt = _byteToSignalTable[color.red ]; - ++bufIt; - *bufIt = _byteToSignalTable[color.green]; - ++bufIt; - *bufIt = _byteToSignalTable[color.blue ]; - ++bufIt; - } - - writeBytes(_ledBuffer.size() * 3, reinterpret_cast(_ledBuffer.data())); - - return 0; -} - -int LedDeviceWs2811::switchOff() -{ - write(std::vector(_ledBuffer.size()/3, ColorRgb::BLACK)); - return 0; -} - -void LedDeviceWs2811::fillEncodeTable(const ws2811::SignalTiming ledOption) -{ - _byteToSignalTable.resize(256); - for (unsigned byteValue=0; byteValue<256; ++byteValue) - { - const uint8_t byteVal = uint8_t(byteValue); - _byteToSignalTable[byteValue] = ws2811::translate(ledOption, byteVal); - } -} diff --git a/libsrc/leddevice/LedDeviceWs2811.h b/libsrc/leddevice/LedDeviceWs2811.h deleted file mode 100644 index 88edfceb..00000000 --- a/libsrc/leddevice/LedDeviceWs2811.h +++ /dev/null @@ -1,147 +0,0 @@ -#pragma once - -// STL includes -#include - -// Local hyperion includes -#include "LedRs232Device.h" - -namespace ws2811 -{ - /// - /// Enumaration of known signal timings - /// - enum SignalTiming - { - option_3755, - option_3773, - option_2855, - option_2882, - not_a_signaltiming - }; - - /// - /// Enumaration of the possible speeds on which the ws2811 can operate. - /// - enum SpeedMode - { - lowspeed, - highspeed - }; - - /// - /// Enumeration of the signal 'parts' (T 0 high, T 1 high, T 0 low, T 1 low). - /// - enum TimeOption - { - T0H, - T1H, - T0L, - T1L - }; - - /// - /// Structure holding the signal for a signle byte - /// - struct ByteSignal - { - uint8_t bit_1; - uint8_t bit_2; - uint8_t bit_3; - uint8_t bit_4; - uint8_t bit_5; - uint8_t bit_6; - uint8_t bit_7; - uint8_t bit_8; - }; - // Make sure the structure is exatly the length we require - static_assert(sizeof(ByteSignal) == 8, "Incorrect sizeof ByteSignal (expected 8)"); - - /// - /// Translates a string to a signal timing - /// - /// @param signalTiming The string specifying the signal timing - /// @param defaultValue The default value (used if the string does not match any known timing) - /// - /// @return The SignalTiming (or not_a_signaltiming if it did not match) - /// - SignalTiming fromString(const std::string& signalTiming, const SignalTiming defaultValue); - - /// - /// Returns the required baudrate for a specific signal-timing - /// - /// @param SpeedMode The WS2811/WS2812 speed mode (WS2812b only has highspeed) - /// - /// @return The required baudrate for the signal timing - /// - unsigned getBaudrate(const SpeedMode speedMode); - - /// - /// The number of 'signal units' (bits) For the subpart of a specific timing scheme - /// - /// @param timing The controller option - /// @param option The signal part - /// - unsigned getLength(const SignalTiming timing, const TimeOption option); - - /// - /// Constructs a 'bit' based signal with defined 'high' length (and implicite defined 'low' - /// length. The signal is based on a 10bits bytes (incl. high startbit and low stopbit). The - /// total length of the high is given as parameter:
- /// lenHigh=7 => |-------|___| => 1 1111 1100 0 => 252 (start and stop bit are implicite) - /// - /// @param lenHigh The total length of the 'high' length (incl start-bit) - /// @return The byte representing the high-low signal - /// - uint8_t bitToSignal(unsigned lenHigh); - - /// - /// Translate a byte into signal levels for a specific WS2811 option - /// - /// @param ledOption The WS2811 configuration - /// @param byte The byte to translate - /// - /// @return The signal for the given byte (one byte per bit) - /// - ByteSignal translate(SignalTiming ledOption, uint8_t byte); -} - -class LedDeviceWs2811 : public LedRs232Device -{ -public: - /// - /// Constructs the LedDevice with Ws2811 attached via a serial port - /// - /// @param outputDevice The name of the output device (eg '/dev/ttyS0') - /// @param signalTiming The timing scheme used by the Ws2811 chip - /// @param speedMode The speed modus of the Ws2811 chip - /// - LedDeviceWs2811(const std::string& outputDevice, const ws2811::SignalTiming signalTiming, const ws2811::SpeedMode speedMode); - - /// - /// 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: - - /// - /// Fill the byte encoding table (_byteToSignalTable) for the specific timing option - /// - /// @param ledOption The timing option - /// - void fillEncodeTable(const ws2811::SignalTiming ledOption); - - /// Translation table of byte to signal/// - std::vector _byteToSignalTable; - - /// The buffer containing the packed RGB values - std::vector _ledBuffer; -}; diff --git a/libsrc/leddevice/LedDeviceWs2812b.cpp b/libsrc/leddevice/LedDeviceWs2812b.cpp deleted file mode 100644 index dd0bbb94..00000000 --- a/libsrc/leddevice/LedDeviceWs2812b.cpp +++ /dev/null @@ -1,96 +0,0 @@ - -// Linux includes -#include - -// Local Hyperion-Leddevice includes -#include "LedDeviceWs2812b.h" - -LedDeviceWs2812b::LedDeviceWs2812b() : - LedRs232Device("/dev/ttyUSB0", 2000000) -{ - // empty -} - -int LedDeviceWs2812b::write(const std::vector & ledValues) -{ - // Ensure the size of the led-buffer - if (_ledBuffer.size() != ledValues.size()*8) - { - _ledBuffer.resize(ledValues.size()*8, ~0x24); - } - - // Translate the channel of each color to a signal - uint8_t * signal_ptr = _ledBuffer.data(); - for (const ColorRgb & color : ledValues) - { - signal_ptr = color2signal(color, signal_ptr); - } - - const int result = writeBytes(_ledBuffer.size(), _ledBuffer.data()); - // Official latch time is 50us (lets give it 50us more) - usleep(100); - return result; -} - -uint8_t * LedDeviceWs2812b::color2signal(const ColorRgb & color, uint8_t * signal) -{ - *signal = bits2Signal(color.red & 0x80, color.red & 0x40, color.red & 0x20); - ++signal; - *signal = bits2Signal(color.red & 0x10, color.red & 0x08, color.red & 0x04); - ++signal; - *signal = bits2Signal(color.red & 0x02, color.green & 0x01, color.green & 0x80); - ++signal; - *signal = bits2Signal(color.green & 0x40, color.green & 0x20, color.green & 0x10); - ++signal; - *signal = bits2Signal(color.green & 0x08, color.green & 0x04, color.green & 0x02); - ++signal; - *signal = bits2Signal(color.green & 0x01, color.blue & 0x80, color.blue & 0x40); - ++signal; - *signal = bits2Signal(color.blue & 0x20, color.blue & 0x10, color.blue & 0x08); - ++signal; - *signal = bits2Signal(color.blue & 0x04, color.blue & 0x02, color.blue & 0x01); - ++signal; - - return signal; -} - -int LedDeviceWs2812b::switchOff() -{ - // Set all bytes in the signal buffer to zero - for (uint8_t & signal : _ledBuffer) - { - signal = ~0x24; - } - - return writeBytes(_ledBuffer.size(), _ledBuffer.data()); -} - -uint8_t LedDeviceWs2812b::bits2Signal(const bool bit_1, const bool bit_2, const bool bit_3) const -{ - // See https://github.com/tvdzwan/hyperion/wiki/Ws2812b for the explanation of the given - // translations - - // Bit index(default):1 2 3 - // | | | - // default value (1) 00 100 10 (0) - // - // Reversed value (1) 01 001 00 (0) - // | | | - // Bit index (rev): 3 2 1 - uint8_t result = 0x24; - - if(bit_1) - { - result |= 0x01; - } - if (bit_2) - { - result |= 0x08; - } - if (bit_3) - { - result |= 0x40; - } - - return ~result; -} diff --git a/libsrc/leddevice/LedDeviceWs2812b.h b/libsrc/leddevice/LedDeviceWs2812b.h deleted file mode 100644 index 6345a060..00000000 --- a/libsrc/leddevice/LedDeviceWs2812b.h +++ /dev/null @@ -1,60 +0,0 @@ - -#pragma once - -// Hyperion leddevice includes -#include "LedRs232Device.h" - -/// -/// The LedDevice for controlling a string of WS2812B leds. These are controlled over the mini-UART -/// of the RPi (/dev/ttyAMA0). -/// -class LedDeviceWs2812b : public LedRs232Device -{ -public: - /// - /// Constructs the device (all required parameters are hardcoded) - /// - LedDeviceWs2812b(); - - /// - /// Write the color data the the WS2812B led string - /// - /// @param ledValues The color data - /// @return Zero on succes else negative - /// - virtual int write(const std::vector & ledValues); - - /// - /// Write zero to all leds(that have been written by a previous write operation) - /// - /// @return Zero on succes else negative - /// - virtual int switchOff(); - -private: - - /// - /// Translate a color to the signal bits. The resulting bits are written to the given memory. - /// - /// @param color The color to translate - /// @param signal The pointer at the beginning of the signal to write - /// @return The pointer at the end of the written signal - /// - uint8_t * color2signal(const ColorRgb & color, uint8_t * signal); - - /// - /// Translates three bits to a single byte - /// - /// @param bit1 The value of the first bit (1=true, zero=false) - /// @param bit2 The value of the second bit (1=true, zero=false) - /// @param bit3 The value of the third bit (1=true, zero=false) - /// - /// @return The output-byte for the given two bit - /// - uint8_t bits2Signal(const bool bit1, const bool bit2, const bool bit3) const; - - /// - /// The output buffer for writing bytes to the output - /// - std::vector _ledBuffer; -}; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bd46d059..fa4b142b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -52,20 +52,3 @@ target_link_libraries(test_qregexp add_executable(test_qtscreenshot TestQtScreenshot.cpp) target_link_libraries(test_qtscreenshot ${QT_LIBRARIES}) - -add_executable(determineWs2811Timing DetermineWs2811Timing.cpp) - -add_executable(test_rs232highspeed - TestRs232HighSpeed.cpp - ../libsrc/leddevice/LedRs232Device.cpp - ../libsrc/leddevice/LedDeviceWs2812b.cpp) -target_link_libraries(test_rs232highspeed - serialport) - -if(NOT APPLE AND UNIX) - include_directories(/usr/include) - add_executable(test_uartHighSpeed TestUartHighSpeed.cpp) - - add_executable(test_nonUniformWs2812b TestNonUniformWs2812b.cpp) - add_executable(test_nonInvWs2812b TestNonInvWs2812b.cpp) -endif() diff --git a/test/DetermineWs2811Timing.cpp b/test/DetermineWs2811Timing.cpp deleted file mode 100644 index 1a47191a..00000000 --- a/test/DetermineWs2811Timing.cpp +++ /dev/null @@ -1,62 +0,0 @@ - -// STl includes -#include -#include - -bool requiredTiming(const int tHigh_ns, const int tLow_ns, const int error_ns, const int nrBits) -{ - std::cout << "=== " << nrBits << " bits case ===== " << std::endl; - double bitLength_ns = (tHigh_ns + tLow_ns)/double(nrBits); - double baudrate_Hz = 1.0 / bitLength_ns * 1e9; - std::cout << "Required bit length: " << bitLength_ns << "ns => baudrate = " << baudrate_Hz << std::endl; - - double highBitsExact = tHigh_ns/bitLength_ns; - int highBits = std::round(highBitsExact); - double lowBitsExact = tLow_ns/bitLength_ns; - int lowBits = std::round(lowBitsExact); - std::cout << "Bit division: high=" << highBits << "(" << highBitsExact << "); low=" << lowBits << "(" << lowBitsExact << ")" << std::endl; - - double highBitsError = std::fabs(highBitsExact - highBits); - double lowBitsError = std::fabs(highBitsExact - highBits); - double highError_ns = highBitsError * bitLength_ns; - double lowError_ns = lowBitsError * bitLength_ns; - - if (highError_ns > error_ns || lowError_ns > error_ns) - { - std::cerr << "Timing error outside specs: " << highError_ns << "; " << lowError_ns << " > " << error_ns << std::endl; - } - else - { - std::cout << "Timing within margins: " << highError_ns << "; " << lowError_ns << " < " << error_ns << std::endl; - } - - - - return true; -} - -int main() -{ - // 10bits - requiredTiming(400, 850, 150, 10); // Zero - requiredTiming(800, 450, 150, 10); // One - - // 6bits - requiredTiming(400, 850, 150, 6); // Zero - requiredTiming(800, 450, 150, 6); // One - - // 5bits - requiredTiming(400, 850, 150, 5); // Zero - requiredTiming(800, 450, 150, 5); // One - - requiredTiming(650, 600, 150, 5); // One - - // 4bits - requiredTiming(400, 850, 150, 4); // Zero - requiredTiming(800, 450, 150, 4); // One - - // 3bits - requiredTiming(400, 850, 150, 3); // Zero - requiredTiming(800, 450, 150, 3); // One - return 0; -} diff --git a/test/TestNonInvWs2812b.cpp b/test/TestNonInvWs2812b.cpp deleted file mode 100644 index 5db10dfb..00000000 --- a/test/TestNonInvWs2812b.cpp +++ /dev/null @@ -1,209 +0,0 @@ - -// STL includes -#include -#include -#include - -#include //Used for UART -#include //Used for UART -#include //Used for UART -#include - -std::vector encode(const std::vector & data); -void split(const uint8_t byte, uint8_t & out1, uint8_t & out2); -uint8_t encode(const bool bit1, const bool bit2, const bool bit3); - -void print(uint8_t byte) -{ - for (int i=0; i<8; ++i) - { - if (byte & (1 << i)) - { - std::cout << '1'; - } - else - { - std::cout << '0'; - } - } -} - -void printClockSignal(const std::vector & signal) -{ - bool prevBit = true; - bool nextBit = true; - - for (uint8_t byte : signal) - { - - for (int i=-1; i<9; ++i) - { - if (i == -1) // Start bit - nextBit = false; - else if (i == 8) // Stop bit - nextBit = true; - else - nextBit = byte & (1 << i); - - if (!prevBit && nextBit) - { - std::cout << ' '; - } - - if (nextBit) - std::cout << '1'; - else - std::cout << '0'; - - prevBit = nextBit; - } - } -} - -int main() -{ - const std::vector data(9, 0x00); - std::vector encData = encode(data); - - for (uint8_t encByte : encData) - { - std::cout << "0 "; - print(encByte); - std::cout << " 1"; - } - std::cout << std::endl; - printClockSignal(encData); - std::cout << std::endl; - - //OPEN THE UART -// int uart0_filestream = open("/dev/ttyAMA0", O_WRONLY | O_NOCTTY | O_NDELAY); - int uart0_filestream = open("/dev/ttyUSB0", O_WRONLY | O_NOCTTY | O_NDELAY); - if (uart0_filestream == -1) - { - //ERROR - CAN'T OPEN SERIAL PORT - printf("Error - Unable to open UART. Ensure it is not in use by another application\n"); - return -1; - } - - // Configure the port - struct termios options; - tcgetattr(uart0_filestream, &options); - options.c_cflag = B2500000 | CS8 | CLOCAL; - options.c_iflag = IGNPAR; - options.c_oflag = 0; - options.c_lflag = 0; - - tcflush(uart0_filestream, TCIFLUSH); - tcsetattr(uart0_filestream, TCSANOW, &options); - - getchar(); - - const int breakLength_ms = 1; - - encData = std::vector(128, 0x00); - - write(uart0_filestream, encData.data(), encData.size()); - - tcsendbreak(uart0_filestream, breakLength_ms); - - //tcdrain(uart0_filestream); -// res = write(uart0_filestream, encData.data(), encData.size()); -// (void)res; - - close(uart0_filestream); - - return 0; -} - -std::vector encode(const std::vector & data) -{ - std::vector result; - - uint8_t previousByte = 0x00; - uint8_t nextByte = 0x00; - for (unsigned iData=0; iData> 4; -} - -uint8_t encode(const bool bit1, const bool bit2, const bool bit3) -{ - if (bit2) - { - uint8_t result = 0x19; // 0--1 01 10-1 - if (bit1) result |= 0x02; -// else result &= ~0x02; - - if (bit3) result |= 0x60; -// else result &= ~0x60; - - return result; - } - else - { - uint8_t result = 0x21;// 0x21 (0-10 01 0--1) - if (bit1) result |= 0x06; -// else result &= ~0x06; - - if (bit3) result |= 0x40; -// else result &= ~0x40; - - return result; - } -} diff --git a/test/TestNonUniformWs2812b.cpp b/test/TestNonUniformWs2812b.cpp deleted file mode 100644 index 97f14695..00000000 --- a/test/TestNonUniformWs2812b.cpp +++ /dev/null @@ -1,188 +0,0 @@ - -// STL includes -#include -#include -#include - -#include //Used for UART -#include //Used for UART -#include //Used for UART -#include - -std::vector encode(const std::vector & data); -uint8_t encode(const bool bit1, const bool bit2, const bool bit3); - -void printClockSignal(const std::vector & signal) -{ - bool prevBit = true; - bool nextBit = true; - - for (uint8_t byte : signal) - { - - for (int i=-1; i<9; ++i) - { - if (i == -1) // Start bit - nextBit = true; - else if (i == 8) // Stop bit - nextBit = false; - else - nextBit = ~byte & (1 << i); - - if (!prevBit && nextBit) - { - std::cout << ' '; - } - - if (nextBit) - std::cout << '1'; - else - std::cout << '0'; - - prevBit = nextBit; - } - } -} - -int main() -{ - const std::vector white{0xff,0xff,0xff, 0xff,0xff,0xff, 0xff,0xff,0xff}; - const std::vector green{0xff, 0x00, 0x00}; - const std::vector red {0x00, 0xff, 0x00}; - const std::vector blue {0x00, 0x00, 0xff}; - const std::vector cyan {0xff, 0x00, 0xff}; - const std::vector mix {0x55, 0x55, 0x55}; - const std::vector black{0x00, 0x00, 0x00}; - const std::vector gray{0x01, 0x01, 0x01}; - -// printClockSignal(encode(mix));std::cout << std::endl; - - //OPEN THE UART -// int uart0_filestream = open("/dev/ttyAMA0", O_WRONLY | O_NOCTTY | O_NDELAY); - int uart0_filestream = open("/dev/ttyUSB0", O_WRONLY | O_NOCTTY | O_NDELAY); - if (uart0_filestream == -1) - { - //ERROR - CAN'T OPEN SERIAL PORT - printf("Error - Unable to open UART. Ensure it is not in use by another application\n"); - return -1; - } - - // Configure the port - struct termios options; - tcgetattr(uart0_filestream, &options); - options.c_cflag = B2500000 | CS8 | CLOCAL; - options.c_iflag = IGNPAR; - options.c_oflag = 0; - options.c_lflag = 0; - - tcflush(uart0_filestream, TCIFLUSH); - tcsetattr(uart0_filestream, TCSANOW, &options); - - { - getchar(); - const std::vector encGreenData = encode(green); - const std::vector encBlueData = encode(blue); - const std::vector encRedData = encode(red); - const std::vector encGrayData = encode(gray); - const std::vector encBlackData = encode(black); - - //std::cout << "Writing GREEN ("; printClockSignal(encode(green)); std::cout << ")" << std::endl; -// const std::vector garbage {0x0f}; -// write(uart0_filestream, garbage.data(), garbage.size()); -// write(uart0_filestream, encGreenData.data(), encGreenData.size()); -// write(uart0_filestream, encRedData.data(), encRedData.size()); -// write(uart0_filestream, encBlueData.data(), encBlueData.size()); -// write(uart0_filestream, encGrayData.data(), encGrayData.size()); -// write(uart0_filestream, encBlackData.data(), encBlackData.size()); -// } -// { -// getchar(); - const std::vector encData = encode(white); - std::cout << "Writing WHITE ("; printClockSignal(encode(white)); std::cout << ")" << std::endl; -// const std::vector garbage {0x0f}; -// write(uart0_filestream, garbage.data(), garbage.size()); - write(uart0_filestream, encData.data(), encData.size()); - write(uart0_filestream, encData.data(), encData.size()); - write(uart0_filestream, encData.data(), encData.size()); - } - { - getchar(); - const std::vector encData = encode(green); - std::cout << "Writing GREEN ("; printClockSignal(encode(green)); std::cout << ")" << std::endl; - write(uart0_filestream, encData.data(), encData.size()); - } - { - getchar(); - const std::vector encData = encode(red); - std::cout << "Writing RED ("; printClockSignal(encode(red)); std::cout << ")" << std::endl; - write(uart0_filestream, encData.data(), encData.size()); - } - { - getchar(); - const std::vector encData = encode(blue); - std::cout << "Writing BLUE ("; printClockSignal(encode(blue)); std::cout << ")" << std::endl; - write(uart0_filestream, encData.data(), encData.size()); - } - { - getchar(); - const std::vector encData = encode(cyan); - std::cout << "Writing CYAN? ("; printClockSignal(encode(cyan)); std::cout << ")" << std::endl; - write(uart0_filestream, encData.data(), encData.size()); - } - { - getchar(); - const std::vector encData = encode(mix); - std::cout << "Writing MIX ("; printClockSignal(encode(mix)); std::cout << ")" << std::endl; - write(uart0_filestream, encData.data(), encData.size()); - } - { - getchar(); - const std::vector encData = encode(black); - std::cout << "Writing BLACK ("; printClockSignal(encode(black)); std::cout << ")" << std::endl; - write(uart0_filestream, encData.data(), encData.size()); - write(uart0_filestream, encData.data(), encData.size()); - write(uart0_filestream, encData.data(), encData.size()); - write(uart0_filestream, encData.data(), encData.size()); - } - - close(uart0_filestream); - - return 0; -} - -std::vector encode(const std::vector & data) -{ - std::vector result; - for (size_t iByte=0; iByte -#include - -// Serialport includes -#include - -int testSerialPortLib(); -int testHyperionDevice(int argc, char** argv); -int testWs2812bDevice(); - -int main(int argc, char** argv) -{ -// if (argc == 1) -// { -// return testSerialPortLib(); -// } -// else -// { -// return testHyperionDevice(argc, argv); -// } - return testWs2812bDevice(); -} - -int testSerialPortLib() -{ - serial::Serial rs232Port("/dev/ttyAMA0", 4000000); - - std::default_random_engine generator; - std::uniform_int_distribution distribution(1,2); - - std::vector data; - for (int i=0; i<9; ++i) - { - int coinFlip = distribution(generator); - if (coinFlip == 1) - { - data.push_back(0xCE); - data.push_back(0xCE); - data.push_back(0xCE); - data.push_back(0xCE); - } - else - { - data.push_back(0x8C); - data.push_back(0x8C); - data.push_back(0x8C); - data.push_back(0x8C); - } - } - std::cout << "Type 'c' to continue, 'q' or 'x' to quit: "; - while (true) - { - char c = getchar(); - if (c == 'q' || c == 'x') - { - break; - } - if (c != 'c') - { - continue; - } - - rs232Port.flushOutput(); - rs232Port.write(data); - rs232Port.flush(); - - data.clear(); - for (int i=0; i<9; ++i) - { - int coinFlip = distribution(generator); - if (coinFlip == 1) - { - data.push_back(0xCE); - data.push_back(0xCE); - data.push_back(0xCE); - data.push_back(0xCE); - } - else - { - data.push_back(0x8C); - data.push_back(0x8C); - data.push_back(0x8C); - data.push_back(0x8C); - } - } - } - - try - { - - rs232Port.close(); - } - catch (const std::runtime_error& excp) - { - std::cout << "Caught exception: " << excp.what() << std::endl; - return -1; - } - - return 0; -} - -#include "../libsrc/leddevice/LedRs232Device.h" - -class TestDevice : public LedRs232Device -{ -public: - TestDevice() : - LedRs232Device("/dev/ttyAMA0", 4000000) - { - open(); - } - - int write(const std::vector &ledValues) - { - std::vector bytes(ledValues.size() * 3 * 4); - - uint8_t * bytePtr = bytes.data(); - for (ColorRgb color : ledValues) - { - byte2Signal(color.green, bytePtr); - bytePtr += 4; - byte2Signal(color.red, bytePtr); - bytePtr += 4; - byte2Signal(color.blue, bytePtr); - bytePtr += 4; - } - - writeBytes(bytes.size(), bytes.data()); - - return 0; - } - - int switchOff() { return 0; }; - - void writeTestSequence(const std::vector & data) - { - writeBytes(data.size(), data.data()); - } - - void byte2Signal(const uint8_t byte, uint8_t * output) - { - output[0] = bits2Signal(byte & 0x80, byte & 0x40); - output[1] = bits2Signal(byte & 0x20, byte & 0x10); - output[2] = bits2Signal(byte & 0x08, byte & 0x04); - output[3] = bits2Signal(byte & 0x02, byte & 0x01); - } - - uint8_t bits2Signal(const bool bit1, const bool bit2) - { - if (bit1) - { - if (bit2) - { - return 0x8C; - } - else - { - return 0xCC; - } - } - else - { - if (bit2) - { - return 0x8E; - } - else - { - return 0xCE; - } - } - - return 0x00; - } -}; - -int testHyperionDevice(int argc, char** argv) -{ - TestDevice rs232Device; - - if (argc > 1 && strncmp(argv[1], "off", 3) == 0) - { - rs232Device.write(std::vector(150, {0, 0, 0})); - return 0; - } - - - int loopCnt = 0; - - std::cout << "Type 'c' to continue, 'q' or 'x' to quit: "; - while (true) - { - char c = getchar(); - if (c == 'q' || c == 'x') - { - break; - } - if (c != 'c') - { - continue; - } - - rs232Device.write(std::vector(loopCnt, {255, 255, 255})); - - ++loopCnt; - } - - rs232Device.write(std::vector(150, {0, 0, 0})); - - return 0; -} - -#include "../libsrc/leddevice/LedDeviceWs2812b.h" - -#include - -int testWs2812bDevice() -{ - LedDeviceWs2812b device; - device.open(); - - std::cout << "Type 'c' to continue, 'q' or 'x' to quit: "; - int loopCnt = 0; - while (true) - { -// char c = getchar(); -// if (c == 'q' || c == 'x') -// { -// break; -// } -// if (c != 'c') -// { -// continue; -// } - - if (loopCnt%4 == 0) - device.write(std::vector(25, {255, 0, 0})); - else if (loopCnt%4 == 1) - device.write(std::vector(25, {0, 255, 0})); - else if (loopCnt%4 == 2) - device.write(std::vector(25, {0, 0, 255})); - else if (loopCnt%4 == 3) - device.write(std::vector(25, {17, 188, 66})); - - ++loopCnt; - - usleep(200000); - if (loopCnt > 200) - { - break; - } - } - - device.write(std::vector(150, {0, 0, 0})); - device.switchOff(); - - return 0; -} diff --git a/test/TestUartHighSpeed.cpp b/test/TestUartHighSpeed.cpp deleted file mode 100644 index 1a7b9440..00000000 --- a/test/TestUartHighSpeed.cpp +++ /dev/null @@ -1,387 +0,0 @@ - -#include -#include -#include -#include //Used for UART -#include //Used for UART -#include //Used for UART -#include - -#include - -#include -#include -#include -#include - -#include -#include - -void set_realtime_priority() { - int ret; - - // We'll operate on the currently running thread. - pthread_t this_thread = pthread_self(); - // struct sched_param is used to store the scheduling priority - struct sched_param params; - // We'll set the priority to the maximum. - params.sched_priority = sched_get_priority_max(SCHED_FIFO); - std::cout << "Trying to set thread realtime prio = " << params.sched_priority << std::endl; - - // Attempt to set thread real-time priority to the SCHED_FIFO policy - ret = pthread_setschedparam(this_thread, SCHED_FIFO, ¶ms); - if (ret != 0) { - // Print the error - std::cout << "Unsuccessful in setting thread realtime prio (erno=" << ret << ")" << std::endl; - return; - } - - // Now verify the change in thread priority - int policy = 0; - ret = pthread_getschedparam(this_thread, &policy, ¶ms); - if (ret != 0) { - std::cout << "Couldn't retrieve real-time scheduling paramers" << std::endl; - return; - } - - // Check the correct policy was applied - if(policy != SCHED_FIFO) { - std::cout << "Scheduling is NOT SCHED_FIFO!" << std::endl; - } else { - std::cout << "SCHED_FIFO OK" << std::endl; - } - - // Print thread scheduling priority - std::cout << "Thread priority is " << params.sched_priority << std::endl; -} - - -struct ColorSignal -{ - uint8_t green_1; - uint8_t green_2; - uint8_t green_3; - uint8_t green_4; - - uint8_t red_1; - uint8_t red_2; - uint8_t red_3; - uint8_t red_4; - - uint8_t blue_1; - uint8_t blue_2; - uint8_t blue_3; - uint8_t blue_4; -}; - -static ColorSignal RED_Signal = { 0xCE, 0xCE, 0xCE, 0xCE, - 0xCE, 0x8C, 0x8C, 0x8C, - 0xCE, 0xCE, 0xCE, 0xCE }; -static ColorSignal GREEN_Signal = { 0xCE, 0x8C, 0x8C, 0x8C, - 0xCE, 0xCE, 0xCE, 0xCE, - 0xCE, 0xCE, 0xCE, 0xCE }; -static ColorSignal BLUE_Signal = { 0xCE, 0xCE, 0xCE, 0xCE, - 0xCE, 0xCE, 0xCE, 0xCE, - 0xCE, 0x8C, 0x8C, 0x8C}; -static ColorSignal BLACK_Signal = { 0xCE, 0xCE, 0xCE, 0xCE, - 0xCE, 0xCE, 0xCE, 0xCE, - 0xCE, 0xCE, 0xCE, 0xCE}; - -static volatile bool _running; - -void signal_handler(int signum) -{ - _running = false; - -} - -void test3bitsEncoding(); - -int main() -{ - if (true) - { - test3bitsEncoding(); - return 0; - } - - _running = true; - signal(SIGTERM, &signal_handler); - - //------------------------- - //----- SETUP USART 0 ----- - //------------------------- - //At bootup, pins 8 and 10 are already set to UART0_TXD, UART0_RXD (ie the alt0 function) respectively - int uart0_filestream = -1; - - //OPEN THE UART - //The flags (defined in fcntl.h): - // Access modes (use 1 of these): - // O_RDONLY - Open for reading only. - // O_RDWR - Open for reading and writing. - // O_WRONLY - Open for writing only. - // - // O_NDELAY / O_NONBLOCK (same function) - Enables nonblocking mode. When set read requests on the file can return immediately with a failure status - // if there is no input immediately available (instead of blocking). Likewise, write requests can also return - // immediately with a failure status if the output can't be written immediately. - // - // O_NOCTTY - When set and path identifies a terminal device, open() shall not cause the terminal device to become the controlling terminal for the process. - uart0_filestream = open("/dev/ttyAMA0", O_WRONLY | O_NOCTTY | O_NDELAY); //Open in non blocking read/write mode - if (uart0_filestream == -1) - { - //ERROR - CAN'T OPEN SERIAL PORT - printf("Error - Unable to open UART. Ensure it is not in use by another application\n"); - } - -// if (0) - { - //CONFIGURE THE UART - //The flags (defined in /usr/include/termios.h - see http://pubs.opengroup.org/onlinepubs/007908799/xsh/termios.h.html): - // Baud rate:- B1200, B2400, B4800, B9600, B19200, B38400, B57600, B115200, B230400, B460800, B500000, B576000, B921600, B1000000, B1152000, B1500000, B2000000, B2500000, B3000000, B3500000, B4000000 - // CSIZE:- CS5, CS6, CS7, CS8 - // CLOCAL - Ignore modem status lines - // CREAD - Enable receiver - // IGNPAR = Ignore characters with parity errors - // ICRNL - Map CR to NL on input (Use for ASCII comms where you want to auto correct end of line characters - don't use for bianry comms!) - // PARENB - Parity enable - // PARODD - Odd parity (else even) - struct termios options; - tcgetattr(uart0_filestream, &options); - options.c_cflag = B4000000 | CS8 | CLOCAL; // signalData(10, RED_Signal); - - int loopCnt = 0; - std::cout << "Type 'c' to continue, 'q' or 'x' to quit: "; - while (_running) - { - char c = getchar(); - if (c == 'q' || c == 'x') - { - break; - } - if (c != 'c') - { - continue; - } - - set_realtime_priority(); - for (int iRun=0; iRun<10; ++iRun) - { -// tcflush(uart0_filestream, TCOFLUSH); - write(uart0_filestream, signalData.data(), signalData.size()*sizeof(ColorSignal)); - tcdrain(uart0_filestream); - - usleep(100000); - ++loopCnt; - - if (loopCnt%3 == 2) - signalData = std::vector(10, GREEN_Signal); - else if(loopCnt%3 == 1) - signalData = std::vector(10, BLUE_Signal); - else if(loopCnt%3 == 0) - signalData = std::vector(10, RED_Signal); - - } - } - - signalData = std::vector(50, BLACK_Signal); - write(uart0_filestream, signalData.data(), signalData.size()*sizeof(ColorSignal)); - //----- CLOSE THE UART ----- - close(uart0_filestream); - - std::cout << "Program finished" << std::endl; - - return 0; -} - -std::vector bit3Encode(const std::vector & bytes); -uint8_t bit3Encode(const bool bit_1, const bool bit_2, const bool bit_3); - -void test3bitsEncoding() -{ - //OPEN THE UART -// int uart0_filestream = open("/dev/ttyAMA0", O_WRONLY | O_NOCTTY | O_NDELAY); - int uart0_filestream = open("/dev/ttyUSB0", O_WRONLY | O_NOCTTY | O_NDELAY); - if (uart0_filestream == -1) - { - //ERROR - CAN'T OPEN SERIAL PORT - printf("Error - Unable to open UART. Ensure it is not in use by another application\n"); - return; - } - - // Configure the port - struct termios options; - tcgetattr(uart0_filestream, &options); - options.c_cflag = B2500000 | CS7 | CLOCAL; - options.c_iflag = IGNPAR; - options.c_oflag = 0; - options.c_lflag = 0; - - tcflush(uart0_filestream, TCIFLUSH); - tcsetattr(uart0_filestream, TCSANOW, &options); - - std::vector colorRed; - for (unsigned i=0; i<10; ++i) - { - colorRed.push_back(0x00); - colorRed.push_back(0xFF); - colorRed.push_back(0x00); - } - std::vector colorGreen; - for (unsigned i=0; i<10; ++i) - { - colorGreen.push_back(0xFF); - colorGreen.push_back(0x00); - colorGreen.push_back(0x00); - } - std::vector colorBlue; - for (unsigned i=0; i<10; ++i) - { - colorBlue.push_back(0x00); - colorBlue.push_back(0x00); - colorBlue.push_back(0xFF); - } - std::vector colorBlack; - for (unsigned i=0; i<10; ++i) - { - colorBlack.push_back(0x00); - colorBlack.push_back(0x00); - colorBlack.push_back(0x00); - } - const std::vector colorRedSignal = bit3Encode(colorRed); - const std::vector colorGreenSignal = bit3Encode(colorGreen); - const std::vector colorBlueSignal = bit3Encode(colorBlue); - const std::vector colorBlackSignal = bit3Encode(colorBlack); - - for (unsigned i=0; i<100; ++i) - { - size_t res; - res = write(uart0_filestream, colorRedSignal.data(), colorRedSignal.size()); - (void)res; - usleep(100000); - res = write(uart0_filestream, colorGreenSignal.data(), colorGreenSignal.size()); - (void)res; - usleep(100000); - res = write(uart0_filestream, colorBlueSignal.data(), colorBlueSignal.size()); - (void)res; - usleep(100000); - } - size_t res = write(uart0_filestream, colorBlackSignal.data(), colorBlackSignal.size()); - (void)res; - //----- CLOSE THE UART ----- - res = close(uart0_filestream); - (void)res; - - std::cout << "Program finished" << std::endl; -} - -std::vector bit3Encode(const std::vector & bytes) -{ - std::vector result; - - for (unsigned iByte=0; iByte Date: Sun, 9 Mar 2014 12:09:17 +0100 Subject: [PATCH 24/78] Change tinkerforge support default to ON, because no external libraries are needed Former-commit-id: 5901c9405e3ea69bde5ab273806e6d9f13060b65 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e8a7f674..4f4ff4f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,7 +16,7 @@ message(STATUS "ENABLE_SPIDEV = " ${ENABLE_SPIDEV}) option(ENABLE_V4L2 "Enable the V4L2 grabber" ON) message(STATUS "ENABLE_V4L2 = " ${ENABLE_V4L2}) -option(ENABLE_TINKERFORGE "Enable the TINKERFORGE device" OFF) +option(ENABLE_TINKERFORGE "Enable the TINKERFORGE device" ON) message(STATUS "ENABLE_TINKERFORGE = " ${ENABLE_TINKERFORGE}) # Createt the configuration file From 70581ffc31b3c0d5ff6f96f8a687a30eecefaa3f Mon Sep 17 00:00:00 2001 From: johan Date: Sun, 9 Mar 2014 12:18:18 +0100 Subject: [PATCH 25/78] Update binaries Former-commit-id: 3741949237cbbb19f2396e08e148d7c990207c94 --- 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 c4d18dcb..24eb3b53 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -720aa07ca87c27c2581fc6dcb4ac7f086aa163c4 \ No newline at end of file +b51b4914e6e3325a3571b834a0e5f7001d294656 \ No newline at end of file From fb73aa786c5bbf46b9ea03815182db6b64b23d14 Mon Sep 17 00:00:00 2001 From: poljvd Date: Mon, 17 Mar 2014 11:06:30 +0100 Subject: [PATCH 26/78] Bugfix in Borderdetector green was checked twice in isBlack(). Should have been green and blue. Former-commit-id: c21a5feffa2245414bbf2b224e8a4dc8955f5e00 --- include/blackborder/BlackBorderDetector.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/include/blackborder/BlackBorderDetector.h b/include/blackborder/BlackBorderDetector.h index ce0bf25d..c5ee91e7 100644 --- a/include/blackborder/BlackBorderDetector.h +++ b/include/blackborder/BlackBorderDetector.h @@ -1,4 +1,3 @@ - #pragma once // Utils includes @@ -126,7 +125,7 @@ namespace hyperion inline bool isBlack(const Pixel_T & color) { // Return the simple compare of the color against black - return color.red < _blackborderThreshold && color.green < _blackborderThreshold && color.green < _blackborderThreshold; + return color.red < _blackborderThreshold && color.green < _blackborderThreshold && color.blue < _blackborderThreshold; } private: From 932b3d7f5a8a77a5d86046e632778b626e3922d4 Mon Sep 17 00:00:00 2001 From: johan Date: Sat, 22 Mar 2014 13:53:43 +0100 Subject: [PATCH 27/78] Updated the binaries Former-commit-id: ed6fbd9660be658de017858bb0a9272588e90ddc --- 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 24eb3b53..7870ca65 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -b51b4914e6e3325a3571b834a0e5f7001d294656 \ No newline at end of file +0172259694aa374d6ae32dcbd122a62ce5bd3b05 \ No newline at end of file From c3ba03e0eebaa11b6018634414c15f7124054900 Mon Sep 17 00:00:00 2001 From: johan Date: Sat, 22 Mar 2014 14:49:24 +0100 Subject: [PATCH 28/78] Fix v4l2 signal threshold comparison Former-commit-id: 698a22af752fc42e778e97587938ed6542653bd9 --- deploy/hyperion.tar.gz.REMOVED.git-id | 2 +- include/utils/ColorRgb.h | 6 ++++++ libsrc/grabber/v4l2/V4L2Grabber.cpp | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/deploy/hyperion.tar.gz.REMOVED.git-id b/deploy/hyperion.tar.gz.REMOVED.git-id index 7870ca65..3c26a2f2 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -0172259694aa374d6ae32dcbd122a62ce5bd3b05 \ No newline at end of file +90bef144811aa3c3db8620622b57e76f699f0121 \ No newline at end of file diff --git a/include/utils/ColorRgb.h b/include/utils/ColorRgb.h index ea544fe0..bbf9f615 100644 --- a/include/utils/ColorRgb.h +++ b/include/utils/ColorRgb.h @@ -55,3 +55,9 @@ inline bool operator<(const ColorRgb & lhs, const ColorRgb & rhs) { return (lhs.red < rhs.red) && (lhs.green < rhs.green) && (lhs.blue < rhs.blue); } + +/// Compare operator to check if a color is 'smaller' than or 'equal' to another color +inline bool operator<=(const ColorRgb & lhs, const ColorRgb & rhs) +{ + return (lhs.red <= rhs.red) && (lhs.green <= rhs.green) && (lhs.blue <= rhs.blue); +} diff --git a/libsrc/grabber/v4l2/V4L2Grabber.cpp b/libsrc/grabber/v4l2/V4L2Grabber.cpp index 2e4bf67d..526f5754 100644 --- a/libsrc/grabber/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/v4l2/V4L2Grabber.cpp @@ -695,7 +695,7 @@ void V4L2Grabber::process_image(const uint8_t * data) ColorRgb & rgb = image(xDest, yDest); yuv2rgb(y, u, v, rgb.red, rgb.green, rgb.blue); - noSignal &= rgb < _noSignalThresholdColor; + noSignal &= rgb <= _noSignalThresholdColor; } } From 6fafb23a3fd9db1aace41c95fdd89885e17e80b2 Mon Sep 17 00:00:00 2001 From: johan Date: Sat, 22 Mar 2014 15:35:25 +0100 Subject: [PATCH 29/78] Added functionality to disable the embedded v4l2 grabber when a higher priority source is active Former-commit-id: 4c0403584093a1fac29d16b502773c3ed11a13a9 --- deploy/hyperion.tar.gz.REMOVED.git-id | 2 +- include/grabber/V4L2Wrapper.h | 8 ++++++++ libsrc/grabber/v4l2/V4L2Grabber.cpp | 22 ++++++++++++++-------- libsrc/grabber/v4l2/V4L2Wrapper.cpp | 27 ++++++++++++++++++++++++++- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/deploy/hyperion.tar.gz.REMOVED.git-id b/deploy/hyperion.tar.gz.REMOVED.git-id index 3c26a2f2..006b5960 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -90bef144811aa3c3db8620622b57e76f699f0121 \ No newline at end of file +3581784ce307344c0a79870314e269117fd139b3 \ No newline at end of file diff --git a/include/grabber/V4L2Wrapper.h b/include/grabber/V4L2Wrapper.h index 99159186..49958a15 100644 --- a/include/grabber/V4L2Wrapper.h +++ b/include/grabber/V4L2Wrapper.h @@ -1,5 +1,8 @@ #pragma once +// Qt includes +#include + // Hyperion includes #include #include @@ -44,6 +47,8 @@ signals: private slots: void newFrame(const Image & image); + void checkSources(); + private: /// The timeout of the led colors [ms] const int _timeout_ms; @@ -62,4 +67,7 @@ private: /// The list with computed led colors std::vector _ledColors; + + /// Timer which tests if a higher priority source is active + QTimer _timer; }; diff --git a/libsrc/grabber/v4l2/V4L2Grabber.cpp b/libsrc/grabber/v4l2/V4L2Grabber.cpp index 526f5754..42b527db 100644 --- a/libsrc/grabber/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/v4l2/V4L2Grabber.cpp @@ -72,10 +72,7 @@ V4L2Grabber::V4L2Grabber(const std::string & device, V4L2Grabber::~V4L2Grabber() { // stop if the grabber was not stopped - if (_streamNotifier != nullptr && _streamNotifier->isEnabled()) { - stop(); - } - + stop(); uninit_device(); close_device(); } @@ -103,14 +100,22 @@ void V4L2Grabber::setSignalThreshold(double redSignalThreshold, double greenSign void V4L2Grabber::start() { - _streamNotifier->setEnabled(true); - start_capturing(); + if (_streamNotifier != nullptr && !_streamNotifier->isEnabled()) + { + _streamNotifier->setEnabled(true); + start_capturing(); + std::cout << "V4L2 grabber started" << std::endl; + } } void V4L2Grabber::stop() { - stop_capturing(); - _streamNotifier->setEnabled(false); + if (_streamNotifier != nullptr && _streamNotifier->isEnabled()) + { + stop_capturing(); + _streamNotifier->setEnabled(false); + std::cout << "V4L2 grabber stopped" << std::endl; + } } void V4L2Grabber::open_device() @@ -142,6 +147,7 @@ void V4L2Grabber::open_device() // 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())); } diff --git a/libsrc/grabber/v4l2/V4L2Wrapper.cpp b/libsrc/grabber/v4l2/V4L2Wrapper.cpp index 9a8f450d..860cc555 100644 --- a/libsrc/grabber/v4l2/V4L2Wrapper.cpp +++ b/libsrc/grabber/v4l2/V4L2Wrapper.cpp @@ -28,7 +28,8 @@ V4L2Wrapper::V4L2Wrapper(const std::string &device, pixelDecimation), _processor(ImageProcessorFactory::getInstance().newImageProcessor()), _hyperion(hyperion), - _ledColors(hyperion->getLedCount(), ColorRgb{0,0,0}) + _ledColors(hyperion->getLedCount(), ColorRgb{0,0,0}), + _timer() { // set the signal detection threshold of the grabber _grabber.setSignalThreshold( @@ -52,6 +53,13 @@ V4L2Wrapper::V4L2Wrapper(const std::string &device, this, SIGNAL(emitColors(int,std::vector,int)), _hyperion, SLOT(setColors(int,std::vector,int)), Qt::QueuedConnection); + + // setup the higher prio source checker + // this will disable the v4l2 grabber when a source with hisher priority is active + _timer.setInterval(500); + _timer.setSingleShot(false); + QObject::connect(&_timer, SIGNAL(timeout()), this, SLOT(checkSources())); + _timer.start(); } V4L2Wrapper::~V4L2Wrapper() @@ -88,3 +96,20 @@ void V4L2Wrapper::newFrame(const Image &image) emit emitColors(_priority, _ledColors, _timeout_ms); } +void V4L2Wrapper::checkSources() +{ + QList activePriorities = _hyperion->getActivePriorities(); + + for (int x : activePriorities) + { + if (x < _priority) + { + // found a higher priority source: grabber should be disabled + _grabber.stop(); + return; + } + } + + // no higher priority source was found: grabber should be enabled + _grabber.start(); +} From 40185e3c7c24dc13ea89d6fb9b2a6207a810b93f Mon Sep 17 00:00:00 2001 From: johan Date: Sat, 22 Mar 2014 15:41:52 +0100 Subject: [PATCH 30/78] Only use center of V4L2 grabbed image to determine if there is a signal to allow some noise at the borders Former-commit-id: f0c1920666297e06c9d29ef0128e0d3340851251 --- libsrc/grabber/v4l2/V4L2Grabber.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/libsrc/grabber/v4l2/V4L2Grabber.cpp b/libsrc/grabber/v4l2/V4L2Grabber.cpp index 42b527db..aa9fa911 100644 --- a/libsrc/grabber/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/v4l2/V4L2Grabber.cpp @@ -96,6 +96,8 @@ void V4L2Grabber::setSignalThreshold(double redSignalThreshold, double greenSign _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; } void V4L2Grabber::start() @@ -674,8 +676,6 @@ void V4L2Grabber::process_image(const uint8_t * data) int outputHeight = (height - _cropTop - _cropBottom + _verticalPixelDecimation/2) / _verticalPixelDecimation; Image image(outputWidth, outputHeight); - bool noSignal = true; - 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) @@ -701,6 +701,20 @@ void V4L2Grabber::process_image(const uint8_t * data) ColorRgb & rgb = image(xDest, yDest); yuv2rgb(y, u, v, rgb.red, rgb.green, rgb.blue); + } + } + + // 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 (unsigned y = 0; noSignal && y < (image.height()>>1); ++y) + { + int yImage = (image.height()>>2) + y; + + ColorRgb & rgb = image(xImage, yImage); noSignal &= rgb <= _noSignalThresholdColor; } } From b62bcc5331943b69b3ff214d7773e49fd92d96a8 Mon Sep 17 00:00:00 2001 From: johan Date: Sat, 22 Mar 2014 16:21:23 +0100 Subject: [PATCH 31/78] Update binaries Former-commit-id: 161058f7e8f2b82413d9df5e8a1370aaa7eee1ac --- 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 006b5960..86dae830 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -3581784ce307344c0a79870314e269117fd139b3 \ No newline at end of file +c15e72c24d6a1c0cce621df38bfa18a6eeb08cc1 \ No newline at end of file From 8eb43f0c90e4bb3f0c50df13cc9bedf0d26d0f61 Mon Sep 17 00:00:00 2001 From: poljvd Date: Wed, 26 Mar 2014 16:26:11 +0100 Subject: [PATCH 32/78] Fix led mapping boundaries Former-commit-id: 3f41d6b41ceff5c0b81c0041ea185233e37d363c --- libsrc/hyperion/ImageToLedsMap.cpp | 31 +++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/libsrc/hyperion/ImageToLedsMap.cpp b/libsrc/hyperion/ImageToLedsMap.cpp index 3e9a7fd3..202f22ff 100644 --- a/libsrc/hyperion/ImageToLedsMap.cpp +++ b/libsrc/hyperion/ImageToLedsMap.cpp @@ -1,4 +1,3 @@ - // STL includes #include #include @@ -33,17 +32,35 @@ ImageToLedsMap::ImageToLedsMap( for (const Led& led : leds) { + // skip leds without area + if ((led.maxX_frac-led.minX_frac) < 1e-6 || (led.maxY_frac-led.minY_frac) < 1e-6) + { + continue; + } + // Compute the index boundaries for this led - const unsigned minX_idx = xOffset + unsigned(std::round((actualWidth-1) * led.minX_frac)); - const unsigned maxX_idx = xOffset + unsigned(std::round((actualWidth-1) * led.maxX_frac)); - const unsigned minY_idx = yOffset + unsigned(std::round((actualHeight-1) * led.minY_frac)); - const unsigned maxY_idx = yOffset + unsigned(std::round((actualHeight-1) * led.maxY_frac)); + unsigned minX_idx = xOffset + unsigned(std::round(actualWidth * led.minX_frac)); + unsigned maxX_idx = xOffset + unsigned(std::round(actualWidth * led.maxX_frac)); + unsigned minY_idx = yOffset + unsigned(std::round(actualHeight * led.minY_frac)); + unsigned maxY_idx = yOffset + unsigned(std::round(actualHeight * led.maxY_frac)); + + // make sure that the area is at least a single led large + minX_idx = std::min(minX_idx, xOffset + actualWidth - 1); + if (minX_idx == maxX_idx) + { + maxX_idx = minX_idx + 1; + } + minY_idx = std::min(minY_idx, yOffset + actualHeight - 1); + if (minY_idx == maxY_idx) + { + maxY_idx = minY_idx + 1; + } // Add all the indices in the above defined rectangle to the indices for this led std::vector ledColors; - for (unsigned y = minY_idx; y<=maxY_idx && y Date: Sun, 30 Mar 2014 03:16:42 +0200 Subject: [PATCH 33/78] Fix error in start script Former-commit-id: 8eb0b39d953b2ba7c8371ca7c002de73029ee32e --- bin/hyperion.init.sh | 52 +++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/bin/hyperion.init.sh b/bin/hyperion.init.sh index 50e7d7f8..d83bdb28 100644 --- a/bin/hyperion.init.sh +++ b/bin/hyperion.init.sh @@ -7,22 +7,27 @@ DAEMON=hyperiond DAEMONOPTS="/etc/hyperion.config.json" DAEMON_PATH="/usr/bin" -NAME=$DEAMON +NAME=$DAEMON DESC="Hyperion ambilight server" PIDFILE=/var/run/$NAME.pid SCRIPTNAME=/etc/init.d/$NAME - case "$1" in start) - printf "%-50s" "Starting $NAME..." - cd $DAEMON_PATH - PID=`$DAEMON $DAEMONOPTS > /dev/null 2>&1 & echo $!` - #echo "Saving PID" $PID " to " $PIDFILE - if [ -z $PID ]; then - printf "%s\n" "Fail" + if [ $(pgrep -l $NAME |wc -l) = 1 ] + then + printf "%-50s\n" "Already running..." + exit 1 else - echo $PID > $PIDFILE - printf "%s\n" "Ok" + printf "%-50s" "Starting $NAME..." + cd $DAEMON_PATH + PID=`$DAEMON $DAEMONOPTS > /dev/null 2>&1 & echo $!` + #echo "Saving PID" $PID " to " $PIDFILE + if [ -z $PID ]; then + printf "%s\n" "Fail" + else + echo $PID > $PIDFILE + printf "%s\n" "Ok" + fi fi ;; status) @@ -39,24 +44,31 @@ status) fi ;; stop) - printf "%-50s" "Stopping $NAME" - PID=`cat $PIDFILE` - cd $DAEMON_PATH - if [ -f $PIDFILE ]; then - kill -HUP $PID - printf "%s\n" "Ok" - rm -f $PIDFILE + if [ -f $PIDFILE ] + then + printf "%-50s" "Stopping $NAME" + PID=`cat $PIDFILE` + cd $DAEMON_PATH + if [ -f $PIDFILE ]; then + kill -HUP $PID + printf "%s\n" "Ok" + rm -f $PIDFILE + else + printf "%s\n" "pidfile not found" + fi else - printf "%s\n" "pidfile not found" + printf "%-50s\n" "No PID file $NAME not running?" fi ;; restart) - $0 stop - $0 start + $0 stop + $0 start ;; *) echo "Usage: $0 {status|start|stop|restart}" exit 1 esac + +exit 0 From 6be5652f8f5b4da5ce46508d16b2ff8fa8b525ab Mon Sep 17 00:00:00 2001 From: johan Date: Fri, 11 Apr 2014 19:56:11 +0200 Subject: [PATCH 34/78] Updated binaries Former-commit-id: d6ec0159f068a0fab600ec8c0c7aedd42654657a --- 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 86dae830..4a4f84c0 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -c15e72c24d6a1c0cce621df38bfa18a6eeb08cc1 \ No newline at end of file +0dd229e56d483fd0a75ae8a9d92fa142a6afa983 \ No newline at end of file From ef6aa76409dc620692bda863504bf6ff7d49cd81 Mon Sep 17 00:00:00 2001 From: johan Date: Mon, 14 Apr 2014 19:19:47 +0200 Subject: [PATCH 35/78] V4L2: Fix error with frame size of RGB32 Former-commit-id: ab98c7b87acb7654c40a715ed21c0857c11a30ff --- deploy/hyperion.tar.gz.REMOVED.git-id | 2 +- include/grabber/V4L2Grabber.h | 1 + libsrc/grabber/v4l2/V4L2Grabber.cpp | 46 +++++++++++++++------------ 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/deploy/hyperion.tar.gz.REMOVED.git-id b/deploy/hyperion.tar.gz.REMOVED.git-id index 4a4f84c0..91855036 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -0dd229e56d483fd0a75ae8a9d92fa142a6afa983 \ No newline at end of file +62843d7b04fbbe0a2ae994531100d2f3f3d73b11 \ No newline at end of file diff --git a/include/grabber/V4L2Grabber.h b/include/grabber/V4L2Grabber.h index 05941fb7..fe867112 100644 --- a/include/grabber/V4L2Grabber.h +++ b/include/grabber/V4L2Grabber.h @@ -108,6 +108,7 @@ private: PixelFormat _pixelFormat; int _width; int _height; + int _frameByteSize; int _cropLeft; int _cropRight; int _cropTop; diff --git a/libsrc/grabber/v4l2/V4L2Grabber.cpp b/libsrc/grabber/v4l2/V4L2Grabber.cpp index fc76c06e..b3517dac 100644 --- a/libsrc/grabber/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/v4l2/V4L2Grabber.cpp @@ -52,6 +52,7 @@ V4L2Grabber::V4L2Grabber(const std::string & device, _pixelFormat(pixelFormat), _width(width), _height(height), + _frameByteSize(-1), _cropLeft(0), _cropRight(0), _cropTop(0), @@ -426,25 +427,6 @@ void V4L2Grabber::init_device(VideoStandard videoStandard, int input) throw_errno_exception("VIDIOC_G_FMT"); } - // check pixel format - switch (fmt.fmt.pix.pixelformat) - { - case V4L2_PIX_FMT_UYVY: - _pixelFormat = PIXELFORMAT_UYVY; - std::cout << "V4L2 pixel format=UYVY" << std::endl; - break; - case V4L2_PIX_FMT_YUYV: - _pixelFormat = PIXELFORMAT_YUYV; - std::cout << "V4L2 pixel format=YUYV" << std::endl; - break; - case V4L2_PIX_FMT_RGB32: - _pixelFormat = PIXELFORMAT_RGB32; - std::cout << "V4L2 pixel format=RGB32" << std::endl; - break; - default: - throw_exception("Only pixel formats UYVY, YUYV, and RGB32 are supported"); - } - // store width & height _width = fmt.fmt.pix.width; _height = fmt.fmt.pix.height; @@ -452,6 +434,28 @@ void V4L2Grabber::init_device(VideoStandard videoStandard, int input) // 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"); + } + switch (_ioMethod) { case IO_METHOD_READ: init_read(fmt.fmt.pix.sizeimage); @@ -667,9 +671,9 @@ bool V4L2Grabber::process_image(const void *p, int size) { // We do want a new frame... - if (size != 2*_width*_height) + if (size != _frameByteSize) { - std::cout << "Frame too small: " << size << " != " << (2*_width*_height) << std::endl; + std::cout << "Frame too small: " << size << " != " << _frameByteSize << std::endl; } else { From d102ffd0432897a9545a346b0d161e47f012c8a9 Mon Sep 17 00:00:00 2001 From: johan Date: Tue, 15 Apr 2014 20:44:16 +0200 Subject: [PATCH 36/78] V4L2: Fix error with byte order of RGB32 Former-commit-id: ad354369b22d4e85b0b2fbf2d1c33611632d7014 --- deploy/hyperion.tar.gz.REMOVED.git-id | 2 +- libsrc/grabber/v4l2/V4L2Grabber.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/hyperion.tar.gz.REMOVED.git-id b/deploy/hyperion.tar.gz.REMOVED.git-id index 91855036..a3a391be 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -62843d7b04fbbe0a2ae994531100d2f3f3d73b11 \ No newline at end of file +d61b685eca164580cb39eb5bc3cf65b89afad410 \ No newline at end of file diff --git a/libsrc/grabber/v4l2/V4L2Grabber.cpp b/libsrc/grabber/v4l2/V4L2Grabber.cpp index b3517dac..2df80524 100644 --- a/libsrc/grabber/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/v4l2/V4L2Grabber.cpp @@ -737,9 +737,9 @@ void V4L2Grabber::process_image(const uint8_t * data) case PIXELFORMAT_RGB32: { int index = (_width * ySource + xSource) * 4; - rgb.red = data[index+1]; - rgb.green = data[index+2]; - rgb.blue = data[index+3]; + rgb.red = data[index ]; + rgb.green = data[index+1]; + rgb.blue = data[index+2]; } break; default: From 6d6f4bf6298e0387f4c0f94a4dc2b07a40471c7a Mon Sep 17 00:00:00 2001 From: spudwebb Date: Tue, 22 Apr 2014 14:15:09 -0400 Subject: [PATCH 37/78] add color parameter to the knight rider effect Former-commit-id: 9d4cd0e18b7871acd76a9cafb04efc39ce6fac74 --- effects/knight-rider.json | 3 ++- effects/knight-rider.py | 13 ++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/effects/knight-rider.json b/effects/knight-rider.json index bf83f897..b4644387 100644 --- a/effects/knight-rider.json +++ b/effects/knight-rider.json @@ -4,6 +4,7 @@ "args" : { "speed" : 1.0, - "fadeFactor" : 0.7 + "fadeFactor" : 0.7, + "color" : [255,0,0] } } diff --git a/effects/knight-rider.py b/effects/knight-rider.py index d2ff3ecc..38c8aabd 100644 --- a/effects/knight-rider.py +++ b/effects/knight-rider.py @@ -2,9 +2,10 @@ import hyperion import time import colorsys -# Get the rotation time +# Get the parameters speed = float(hyperion.args.get('speed', 1.0)) fadeFactor = float(hyperion.args.get('fadeFactor', 0.7)) +color = hyperion.args.get('color', (255,0,0)) # Check parameters speed = max(0.0001, speed) @@ -13,7 +14,9 @@ fadeFactor = max(0.0, min(fadeFactor, 1.0)) # Initialize the led data width = 25 imageData = bytearray(width * (0,0,0)) -imageData[0] = 255 +imageData[0] = color[0] +imageData[1] = color[1] +imageData[2] = color[2] # Calculate the sleep time and rotation increment increment = 1 @@ -41,9 +44,13 @@ while not hyperion.abort(): # Fade the old data for j in range(width): imageData[3*j] = int(fadeFactor * imageData[3*j]) + imageData[3*j+1] = int(fadeFactor * imageData[3*j+1]) + imageData[3*j+2] = int(fadeFactor * imageData[3*j+2]) # Insert new data - imageData[3*position] = 255 + imageData[3*position] = color[0] + imageData[3*position+1] = color[1] + imageData[3*position+2] = color[2] # Sleep for a while time.sleep(sleepTime) From b619fcda5528b55457bdd58553a58ddf2925efc4 Mon Sep 17 00:00:00 2001 From: bimsarck Date: Sat, 26 Apr 2014 00:58:47 +0200 Subject: [PATCH 38/78] add 3D autodetection for xbmc with versions check Former-commit-id: 6fd0195fdae82ebe5a26a6ce1138164e09e8b929 --- include/xbmcvideochecker/XBMCVideoChecker.h | 10 ++- libsrc/xbmcvideochecker/XBMCVideoChecker.cpp | 83 ++++++++++++++++---- 2 files changed, 75 insertions(+), 18 deletions(-) diff --git a/include/xbmcvideochecker/XBMCVideoChecker.h b/include/xbmcvideochecker/XBMCVideoChecker.h index 08fc4236..d016b0d4 100644 --- a/include/xbmcvideochecker/XBMCVideoChecker.h +++ b/include/xbmcvideochecker/XBMCVideoChecker.h @@ -38,7 +38,7 @@ public: /// @param grabPhoto Whether or not to grab when the XBMC photo player is playing /// @param grabAudio Whether or not to grab when the XBMC audio player is playing /// @param grabMenu Whether or not to grab when nothing is playing (in XBMC menu) - /// @param grabScreensaver Whether or not to grab when the XBMC screensaver is activated + /// @param grabScreensaver Whether or not to grab when the XBMC screensaver is activated /// @param enable3DDetection Wheter or not to enable the detection of 3D movies playing /// XBMCVideoChecker(const std::string & address, uint16_t port, bool grabVideo, bool grabPhoto, bool grabAudio, bool grabMenu, bool grabScreensaver, bool enable3DDetection); @@ -96,6 +96,12 @@ private: /// The JSON-RPC message to check the screensaver const QString _checkScreensaverRequest; + /// The JSON-RPC message to check the active stereoscopicmode + const QString _getStereoscopicMode; + + /// The JSON-RPC message to check the xbmc version + const QString _getXbmcVersion; + /// The QT TCP Socket with connection to XBMC QTcpSocket _socket; @@ -111,7 +117,7 @@ private: /// Flag indicating whether or not to grab when XBMC is playing nothing (in menu) const bool _grabMenu; - /// Flag inidcating whether or not to grab when the XBMC screensaver is activated + /// Flag indicating whether or not to grab when the XBMC screensaver is activated const bool _grabScreensaver; /// Flag indicating wheter or not to enable the detection of 3D movies playing diff --git a/libsrc/xbmcvideochecker/XBMCVideoChecker.cpp b/libsrc/xbmcvideochecker/XBMCVideoChecker.cpp index 0c396693..ad7843b2 100644 --- a/libsrc/xbmcvideochecker/XBMCVideoChecker.cpp +++ b/libsrc/xbmcvideochecker/XBMCVideoChecker.cpp @@ -17,6 +17,12 @@ // {"id":668,"jsonrpc":"2.0","method":"XBMC.GetInfoBooleans","params":{"booleans":["System.ScreenSaverActive"]}} // {"id":668,"jsonrpc":"2.0","result":{"System.ScreenSaverActive":false}} +// Request stereoscopicmode example: +// {"jsonrpc":"2.0","method":"GUI.GetProperties","params":{"properties":["stereoscopicmode"]},"id":1} +// {"id":1,"jsonrpc":"2.0","result":{"stereoscopicmode":{"label":"Nebeneinander","mode":"split_vertical"}}} + +int xbmcVersion = 0; + XBMCVideoChecker::XBMCVideoChecker(const std::string & address, uint16_t port, bool grabVideo, bool grabPhoto, bool grabAudio, bool grabMenu, bool grabScreensaver, bool enable3DDetection) : QObject(), _address(QString::fromStdString(address)), @@ -24,6 +30,8 @@ XBMCVideoChecker::XBMCVideoChecker(const std::string & address, uint16_t port, b _activePlayerRequest(R"({"id":666,"jsonrpc":"2.0","method":"Player.GetActivePlayers"})"), _currentPlayingItemRequest(R"({"id":667,"jsonrpc":"2.0","method":"Player.GetItem","params":{"playerid":%1,"properties":["file"]}})"), _checkScreensaverRequest(R"({"id":668,"jsonrpc":"2.0","method":"XBMC.GetInfoBooleans","params":{"booleans":["System.ScreenSaverActive"]}})"), + _getStereoscopicMode(R"({"jsonrpc":"2.0","method":"GUI.GetProperties","params":{"properties":["stereoscopicmode"]},"id":1})"), + _getXbmcVersion(R"({"jsonrpc":"2.0","method":"Application.GetProperties","params":{"properties":["version"]},"id":1})"), _socket(), _grabVideo(grabVideo), _grabPhoto(grabPhoto), @@ -116,24 +124,32 @@ void XBMCVideoChecker::receiveReply() } else if (reply.contains("\"id\":667")) { - // result of Player.GetItem - // TODO: what if the filename contains a '"'. In Json this should have been escaped - QRegExp regex("\"file\":\"((?!\").)*\""); - int pos = regex.indexIn(reply); - if (pos > 0) + if (xbmcVersion >= 13) { - QStringRef filename = QStringRef(&reply, pos+8, regex.matchedLength()-9); - if (filename.contains("3DSBS", Qt::CaseInsensitive) || filename.contains("HSBS", Qt::CaseInsensitive)) + // check of active stereoscopicmode + _socket.write(_getStereoscopicMode.toUtf8()); + } + else + { + // result of Player.GetItem + // TODO: what if the filename contains a '"'. In Json this should have been escaped + QRegExp regex("\"file\":\"((?!\").)*\""); + int pos = regex.indexIn(reply); + if (pos > 0) { - setVideoMode(VIDEO_3DSBS); - } - else if (filename.contains("3DTAB", Qt::CaseInsensitive) || filename.contains("HTAB", Qt::CaseInsensitive)) - { - setVideoMode(VIDEO_3DTAB); - } - else - { - setVideoMode(VIDEO_2D); + QStringRef filename = QStringRef(&reply, pos+8, regex.matchedLength()-9); + if (filename.contains("3DSBS", Qt::CaseInsensitive) || filename.contains("HSBS", Qt::CaseInsensitive)) + { + setVideoMode(VIDEO_3DSBS); + } + else if (filename.contains("3DTAB", Qt::CaseInsensitive) || filename.contains("HTAB", Qt::CaseInsensitive)) + { + setVideoMode(VIDEO_3DTAB); + } + else + { + setVideoMode(VIDEO_2D); + } } } } @@ -142,6 +158,41 @@ void XBMCVideoChecker::receiveReply() // result of System.ScreenSaverActive bool active = reply.contains("\"System.ScreenSaverActive\":true"); setScreensaverMode(!_grabScreensaver && active); + + // check here xbmc version + if (_socket.state() == QTcpSocket::ConnectedState) + { + if (xbmcVersion == 0) + { + _socket.write(_getXbmcVersion.toUtf8()); + } + } + } + else if (reply.contains("\"stereoscopicmode\"")) + { + QRegExp regex("\"mode\":\"(split_vertical|split_horizontal)\""); + int pos = regex.indexIn(reply); + if (pos > 0) + { + QString sMode = regex.cap(1); + if (sMode == "split_vertical") + { + setVideoMode(VIDEO_3DSBS); + } + else if (sMode == "split_horizontal") + { + setVideoMode(VIDEO_3DTAB); + } + } + } + else if (reply.contains("\"version\":")) + { + QRegExp regex("\"major\":(\\d+)"); + int pos = regex.indexIn(reply); + if (pos > 0) + { + xbmcVersion = regex.cap(1).toInt(); + } } } From 88c523518a92d61a4562135fffacf55f28045fa0 Mon Sep 17 00:00:00 2001 From: ntim Date: Sun, 27 Apr 2014 12:59:44 +0200 Subject: [PATCH 39/78] Initial commit of support for the Philips Hue system. Former-commit-id: 5b7d802c326151ee96a5b950badb01e94adfe7f3 --- libsrc/leddevice/CMakeLists.txt | 2 + libsrc/leddevice/LedDeviceFactory.cpp | 6 ++ libsrc/leddevice/LedDevicePhilipsHue.cpp | 81 +++++++++++++++++++++ libsrc/leddevice/LedDevicePhilipsHue.h | 90 ++++++++++++++++++++++++ 4 files changed, 179 insertions(+) mode change 100644 => 100755 libsrc/leddevice/CMakeLists.txt mode change 100644 => 100755 libsrc/leddevice/LedDeviceFactory.cpp create mode 100755 libsrc/leddevice/LedDevicePhilipsHue.cpp create mode 100755 libsrc/leddevice/LedDevicePhilipsHue.h diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt old mode 100644 new mode 100755 index 443e845d..098ac38c --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -29,6 +29,7 @@ SET(Leddevice_HEADERS ${CURRENT_SOURCE_DIR}/LedDeviceSedu.h ${CURRENT_SOURCE_DIR}/LedDeviceTest.h ${CURRENT_SOURCE_DIR}/LedDeviceHyperionUsbasp.h + ${CURRENT_SOURCE_DIR}/LedDevicePhilipsHue.h ) SET(Leddevice_SOURCES @@ -44,6 +45,7 @@ SET(Leddevice_SOURCES ${CURRENT_SOURCE_DIR}/LedDeviceSedu.cpp ${CURRENT_SOURCE_DIR}/LedDeviceTest.cpp ${CURRENT_SOURCE_DIR}/LedDeviceHyperionUsbasp.cpp + ${CURRENT_SOURCE_DIR}/LedDevicePhilipsHue.cpp ) if(ENABLE_SPIDEV) diff --git a/libsrc/leddevice/LedDeviceFactory.cpp b/libsrc/leddevice/LedDeviceFactory.cpp old mode 100644 new mode 100755 index b5bc6f5e..71aeda06 --- a/libsrc/leddevice/LedDeviceFactory.cpp +++ b/libsrc/leddevice/LedDeviceFactory.cpp @@ -28,6 +28,7 @@ #include "LedDeviceSedu.h" #include "LedDeviceTest.h" #include "LedDeviceHyperionUsbasp.h" +#include "LedDevicePhilipsHue.h" LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) { @@ -159,6 +160,11 @@ LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) deviceHyperionUsbasp->open(); device = deviceHyperionUsbasp; } + else if (type == "philipshue") + { + const std::string output = deviceConfig["output"].asString(); + device = new LedDevicePhilipsHue(output); + } else if (type == "test") { const std::string output = deviceConfig["output"].asString(); diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp new file mode 100755 index 00000000..01334403 --- /dev/null +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -0,0 +1,81 @@ +// Local-Hyperion includes +#include "LedDevicePhilipsHue.h" + +#include + +#include +#include +#include +#include + +LedDevicePhilipsHue::LedDevicePhilipsHue(const std::string& output) : + host(output.c_str()), username("newdeveloper") { + http = new QHttp(host, 80); +} + +LedDevicePhilipsHue::~LedDevicePhilipsHue() { + delete http; +} + +int LedDevicePhilipsHue::write(const std::vector & ledValues) { + // Due to rate limiting (max. 30 request per seconds), discard new values if + // the previous request have not been completed yet. + if (http->hasPendingRequests()) { + return -1; + } + unsigned int lightId = 1; + for (const ColorRgb& color : ledValues) { + float x, y, b; + // Scale colors from [0, 255] to [0, 1] and convert to xy space. + rgbToXYBrightness(color.red / 255.0f, color.green / 255.0f, color.blue / 255.0f, x, y, b); + // Send adjust color command in JSON format. + put(getRoute(lightId), QString("{\"xy\": [%1, %2]}").arg(x).arg(y)); + // Send brightness color command in JSON format. + put(getRoute(lightId), QString("{\"bri\": %1}").arg(qRound(b * 255.0f))); + // Next light id. + lightId++; + } + return 0; +} + +int LedDevicePhilipsHue::switchOff() { + return 0; +} + +void LedDevicePhilipsHue::put(QString route, QString content) { + QString url = QString("/api/%1/%2").arg(username).arg(route); + QHttpRequestHeader header("PUT", url); + header.setValue("Host", host); + header.setValue("Accept-Encoding", "identity"); + header.setValue("Content-Length", QString("%1").arg(content.size())); + http->setHost(host); + http->request(header, content.toAscii()); + // std::cout << "LedDevicePhilipsHue::put(): " << header.toString().toUtf8().constData() << std::endl; + // std::cout << "LedDevicePhilipsHue::put(): " << content.toUtf8().constData() << std::endl; +} + +QString LedDevicePhilipsHue::getRoute(unsigned int lightId) { + return QString("lights/%1/state").arg(lightId); +} + +void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, float& x, float& y, float& brightness) { + // Apply gamma correction. + red = (red > 0.04045f) ? qPow((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f); + green = (green > 0.04045f) ? qPow ((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f); + blue = (blue > 0.04045f) ? qPow ((blue + 0.055f) / (1.0f + 0.055f), 2.4f) : (blue / 12.92f); + // Convert to XYZ space. + float X = red * 0.649926f + green * 0.103455f + blue * 0.197109f; + float Y = red * 0.234327f + green * 0.743075f + blue * 0.022598f; + float Z = red * 0.0000000f + green * 0.053077f + blue * 1.035763f; + // Convert to x,y space. + x = X / (X + Y + Z); + y = Y / (X + Y + Z); + if (isnan(x)) { + x = 0.0f; + } + if (isnan(y)) { + y = 0.0f; + } + // Brightness is simply Y in the XYZ space. + brightness = Y; +} diff --git a/libsrc/leddevice/LedDevicePhilipsHue.h b/libsrc/leddevice/LedDevicePhilipsHue.h new file mode 100755 index 00000000..e5710f40 --- /dev/null +++ b/libsrc/leddevice/LedDevicePhilipsHue.h @@ -0,0 +1,90 @@ +#pragma once + +// STL includes +#include + +// Qt includes +#include +#include + +// Leddevice includes +#include + +/** + * Implementation for the Philips Hue system. + * + * To use set the device to "philipshue". + * Uses the official Philips Hue API (http://developers.meethue.com). + * Framegrabber should be limited to 30 Hz / numer of lights to avoid rate limitation by the hue bridge. + * Create a new API user name "newdeveloper" on the bridge (http://developers.meethue.com/gettingstarted.html) + */ +class LedDevicePhilipsHue : public LedDevice +{ +public: + /// + /// Constructs the device. + /// + /// @param output the ip address of the bridge + /// + LedDevicePhilipsHue(const std::string& output); + + /// + /// Destructor of this device + /// + virtual ~LedDevicePhilipsHue(); + + /// + /// Sends the given led-color values via put request to the hue system + /// + /// @param ledValues The color-value per led + /// + /// @return Zero on success else negative + /// + virtual int write(const std::vector & ledValues); + + /// Switch the leds off + virtual int switchOff(); + +private: + /// Ip address of the bridge + QString host; + /// User name for the API ("newdeveloper") + QString username; + QHttp* http; + + /// + /// Sends a HTTP PUT request + /// + /// @param route the URI of the request + /// + /// @param content content of the request + /// + void put(QString route, QString content); + + /// + /// @param lightId the id of the hue light (starting from 1) + /// + /// @return the URI of the light + /// + QString getRoute(unsigned int lightId); + + /// + /// Converts an RGB color to the Hue xy color space and brightness + /// https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/blob/master/ApplicationDesignNotes/RGB%20to%20xy%20Color%20conversion.md + /// + /// @param red the red component in [0, 1] + /// + /// @param green the green component in [0, 1] + /// + /// @param blue the blue component in [0, 1] + /// + /// @param x converted x component + /// + /// @param y converted y component + /// + /// @param brightness converted brightness component + /// + void rgbToXYBrightness(float red, float green, float blue, + float& x, float& y, float& brightness); + +}; From ebb22cdc87799612bea0bdd2bf111250f0d03ce0 Mon Sep 17 00:00:00 2001 From: ntim Date: Sun, 27 Apr 2014 18:42:26 +0200 Subject: [PATCH 40/78] Removed rate limiting from code. Setting framegrabbing frequency of 10 Hz / number of lights is sufficient. Former-commit-id: 0686a8d18c780038eb017fdf26b5faf4b8053f3a --- libsrc/leddevice/LedDevicePhilipsHue.cpp | 19 ++++++------------- libsrc/leddevice/LedDevicePhilipsHue.h | 2 +- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index 01334403..11e99a21 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -18,11 +18,6 @@ LedDevicePhilipsHue::~LedDevicePhilipsHue() { } int LedDevicePhilipsHue::write(const std::vector & ledValues) { - // Due to rate limiting (max. 30 request per seconds), discard new values if - // the previous request have not been completed yet. - if (http->hasPendingRequests()) { - return -1; - } unsigned int lightId = 1; for (const ColorRgb& color : ledValues) { float x, y, b; @@ -50,8 +45,6 @@ void LedDevicePhilipsHue::put(QString route, QString content) { header.setValue("Content-Length", QString("%1").arg(content.size())); http->setHost(host); http->request(header, content.toAscii()); - // std::cout << "LedDevicePhilipsHue::put(): " << header.toString().toUtf8().constData() << std::endl; - // std::cout << "LedDevicePhilipsHue::put(): " << content.toUtf8().constData() << std::endl; } QString LedDevicePhilipsHue::getRoute(unsigned int lightId) { @@ -71,11 +64,11 @@ void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, x = X / (X + Y + Z); y = Y / (X + Y + Z); if (isnan(x)) { - x = 0.0f; - } - if (isnan(y)) { - y = 0.0f; - } - // Brightness is simply Y in the XYZ space. + x = 0.0f; + } + if (isnan(y)) { + y = 0.0f; + } + // Brightness is simply Y in the XYZ space. brightness = Y; } diff --git a/libsrc/leddevice/LedDevicePhilipsHue.h b/libsrc/leddevice/LedDevicePhilipsHue.h index e5710f40..a9155899 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/LedDevicePhilipsHue.h @@ -15,7 +15,7 @@ * * To use set the device to "philipshue". * Uses the official Philips Hue API (http://developers.meethue.com). - * Framegrabber should be limited to 30 Hz / numer of lights to avoid rate limitation by the hue bridge. + * Framegrabber must be limited to 10 Hz / numer of lights to avoid rate limitation by the hue bridge. * Create a new API user name "newdeveloper" on the bridge (http://developers.meethue.com/gettingstarted.html) */ class LedDevicePhilipsHue : public LedDevice From ab5e17e1057324955260060a01e13a86246ba68d Mon Sep 17 00:00:00 2001 From: Tim Niggemann Date: Mon, 28 Apr 2014 14:32:37 +0200 Subject: [PATCH 41/78] State of all lights is saved and restored on switchOff(). Former-commit-id: 1ee26e8c01d90456424c1b5ea3f113dfd0ff6525 --- libsrc/leddevice/LedDevicePhilipsHue.cpp | 83 +++++++++++++++++++++--- libsrc/leddevice/LedDevicePhilipsHue.h | 46 +++++++++++-- 2 files changed, 113 insertions(+), 16 deletions(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index 11e99a21..690a7b94 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -1,16 +1,18 @@ // Local-Hyperion includes #include "LedDevicePhilipsHue.h" -#include +// jsoncpp includes +#include +// qt includes #include #include #include -#include +#include LedDevicePhilipsHue::LedDevicePhilipsHue(const std::string& output) : - host(output.c_str()), username("newdeveloper") { - http = new QHttp(host, 80); + host(output.c_str()), username("newdeveloper") { + http = new QHttp(host); } LedDevicePhilipsHue::~LedDevicePhilipsHue() { @@ -18,15 +20,20 @@ LedDevicePhilipsHue::~LedDevicePhilipsHue() { } int LedDevicePhilipsHue::write(const std::vector & ledValues) { + // Save light states if not done before. + if (!statesSaved()) { + saveStates(ledValues.size()); + } + // Iterate through colors and set light states. unsigned int lightId = 1; for (const ColorRgb& color : ledValues) { float x, y, b; // Scale colors from [0, 255] to [0, 1] and convert to xy space. rgbToXYBrightness(color.red / 255.0f, color.green / 255.0f, color.blue / 255.0f, x, y, b); // Send adjust color command in JSON format. - put(getRoute(lightId), QString("{\"xy\": [%1, %2]}").arg(x).arg(y)); + put(getStateRoute(lightId), QString("{\"xy\": [%1, %2]}").arg(x).arg(y)); // Send brightness color command in JSON format. - put(getRoute(lightId), QString("{\"bri\": %1}").arg(qRound(b * 255.0f))); + put(getStateRoute(lightId), QString("{\"bri\": %1}").arg(qRound(b * 255.0f))); // Next light id. lightId++; } @@ -34,6 +41,11 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { } int LedDevicePhilipsHue::switchOff() { + // If light states have been saved before, ... + if (statesSaved()) { + // ... restore them. + restoreStates(); + } return 0; } @@ -47,15 +59,68 @@ void LedDevicePhilipsHue::put(QString route, QString content) { http->request(header, content.toAscii()); } -QString LedDevicePhilipsHue::getRoute(unsigned int lightId) { +QByteArray LedDevicePhilipsHue::get(QString route) { + QString url = QString("/api/%1/%2").arg(username).arg(route); + // Event loop to block until request finished. + QEventLoop loop; + // Connect requestFinished signal to quit slot of the loop. + loop.connect(http, SIGNAL(requestFinished(int, bool)), SLOT(quit())); + // Perfrom request + http->get(url); + // Go into the loop until the request is finished. + loop.exec(); + // Read all data of the response. + return http->readAll(); +} + +QString LedDevicePhilipsHue::getStateRoute(unsigned int lightId) { return QString("lights/%1/state").arg(lightId); } +QString LedDevicePhilipsHue::getRoute(unsigned int lightId) { + return QString("lights/%1").arg(lightId); +} + +void LedDevicePhilipsHue::saveStates(unsigned int nLights) { + // Clear saved light states. + states.clear(); + // Use json parser to parse reponse. + Json::Reader reader; + Json::FastWriter writer; + // Iterate lights. + for (unsigned int i = 0; i < nLights; i++) { + // Read the response. + QByteArray response = get(getRoute(i + 1)); + // Parse JSON. + Json::Value state; + if (!reader.parse(QString(response).toStdString(), state)) { + // Error occured, break loop. + break; + } + // Save state object. + states.push_back(QString(writer.write(state["state"]).c_str())); + } +} + +void LedDevicePhilipsHue::restoreStates() { + unsigned int lightId = 1; + for (QString state : states) { + put(getStateRoute(lightId), state); + lightId++; + } + // Clear saved light states. + states.clear(); +} + +bool LedDevicePhilipsHue::statesSaved() { + return !states.empty(); +} + void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, float& x, float& y, float& brightness) { // Apply gamma correction. red = (red > 0.04045f) ? qPow((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f); - green = (green > 0.04045f) ? qPow ((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f); - blue = (blue > 0.04045f) ? qPow ((blue + 0.055f) / (1.0f + 0.055f), 2.4f) : (blue / 12.92f); + green = (green > 0.04045f) ? qPow((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f); + blue = (blue > 0.04045f) ? qPow((blue + 0.055f) / (1.0f + 0.055f), 2.4f) : (blue / 12.92f); // Convert to XYZ space. float X = red * 0.649926f + green * 0.103455f + blue * 0.197109f; float Y = red * 0.234327f + green * 0.743075f + blue * 0.022598f; diff --git a/libsrc/leddevice/LedDevicePhilipsHue.h b/libsrc/leddevice/LedDevicePhilipsHue.h index a9155899..0bb71182 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/LedDevicePhilipsHue.h @@ -9,7 +9,7 @@ // Leddevice includes #include - + /** * Implementation for the Philips Hue system. * @@ -18,8 +18,7 @@ * Framegrabber must be limited to 10 Hz / numer of lights to avoid rate limitation by the hue bridge. * Create a new API user name "newdeveloper" on the bridge (http://developers.meethue.com/gettingstarted.html) */ -class LedDevicePhilipsHue : public LedDevice -{ +class LedDevicePhilipsHue: public LedDevice { public: /// /// Constructs the device. @@ -46,14 +45,26 @@ public: virtual int switchOff(); private: + /// Array to save the light states. + std::vector states; /// Ip address of the bridge QString host; /// User name for the API ("newdeveloper") QString username; + /// Qhttp object for sending requests. QHttp* http; /// - /// Sends a HTTP PUT request + /// Sends a HTTP GET request (blocking). + /// + /// @param route the URI of the request + /// + /// @return response of the request + /// + QByteArray get(QString route); + + /// + /// Sends a HTTP PUT request (non-blocking). /// /// @param route the URI of the request /// @@ -64,10 +75,32 @@ private: /// /// @param lightId the id of the hue light (starting from 1) /// - /// @return the URI of the light + /// @return the URI of the light state for PUT requests. + /// + QString getStateRoute(unsigned int lightId); + + /// + /// @param lightId the id of the hue light (starting from 1) + /// + /// @return the URI of the light for GET requests. /// QString getRoute(unsigned int lightId); + /// + /// Queries the status of all lights and saves it. + /// + /// @param nLights the number of lights + /// + void saveStates(unsigned int nLights); + + /// Restores the status of all lights. + void restoreStates(); + + /// + /// @return true if light states have been saved. + /// + bool statesSaved(); + /// /// Converts an RGB color to the Hue xy color space and brightness /// https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/blob/master/ApplicationDesignNotes/RGB%20to%20xy%20Color%20conversion.md @@ -84,7 +117,6 @@ private: /// /// @param brightness converted brightness component /// - void rgbToXYBrightness(float red, float green, float blue, - float& x, float& y, float& brightness); + void rgbToXYBrightness(float red, float green, float blue, float& x, float& y, float& brightness); }; From 1e66045d3ed27fd5480de04e4391bd259d0ee264 Mon Sep 17 00:00:00 2001 From: johan Date: Wed, 30 Apr 2014 22:38:59 +0200 Subject: [PATCH 42/78] Added author tag for Philps Hue device Former-commit-id: 6fc5dc0ac7177ddd23e601c8ca1f65981ff0e778 --- libsrc/leddevice/LedDevicePhilipsHue.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.h b/libsrc/leddevice/LedDevicePhilipsHue.h index 0bb71182..2152f625 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/LedDevicePhilipsHue.h @@ -12,11 +12,13 @@ /** * Implementation for the Philips Hue system. - * + * * To use set the device to "philipshue". * Uses the official Philips Hue API (http://developers.meethue.com). * Framegrabber must be limited to 10 Hz / numer of lights to avoid rate limitation by the hue bridge. * Create a new API user name "newdeveloper" on the bridge (http://developers.meethue.com/gettingstarted.html) + * + * @author ntim (github) */ class LedDevicePhilipsHue: public LedDevice { public: From 8d75a57f18a1ffae1781c11b134e96c59fe910ea Mon Sep 17 00:00:00 2001 From: johan Date: Wed, 30 Apr 2014 22:46:26 +0200 Subject: [PATCH 43/78] Cleanup of XBMC 3D checker code Former-commit-id: 26acf7dceb3e26a2e59af82736dec9fdf09c26d5 --- include/xbmcvideochecker/XBMCVideoChecker.h | 3 ++ libsrc/xbmcvideochecker/XBMCVideoChecker.cpp | 29 ++++++++++---------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/include/xbmcvideochecker/XBMCVideoChecker.h b/include/xbmcvideochecker/XBMCVideoChecker.h index d016b0d4..01401093 100644 --- a/include/xbmcvideochecker/XBMCVideoChecker.h +++ b/include/xbmcvideochecker/XBMCVideoChecker.h @@ -131,4 +131,7 @@ private: /// Previous emitted video mode VideoMode _previousVideoMode; + + /// XBMC version number + int _xbmcVersion; }; diff --git a/libsrc/xbmcvideochecker/XBMCVideoChecker.cpp b/libsrc/xbmcvideochecker/XBMCVideoChecker.cpp index ad7843b2..a540c08b 100644 --- a/libsrc/xbmcvideochecker/XBMCVideoChecker.cpp +++ b/libsrc/xbmcvideochecker/XBMCVideoChecker.cpp @@ -18,10 +18,8 @@ // {"id":668,"jsonrpc":"2.0","result":{"System.ScreenSaverActive":false}} // Request stereoscopicmode example: -// {"jsonrpc":"2.0","method":"GUI.GetProperties","params":{"properties":["stereoscopicmode"]},"id":1} -// {"id":1,"jsonrpc":"2.0","result":{"stereoscopicmode":{"label":"Nebeneinander","mode":"split_vertical"}}} - -int xbmcVersion = 0; +// {"jsonrpc":"2.0","method":"GUI.GetProperties","params":{"properties":["stereoscopicmode"]},"id":669} +// {"id":669,"jsonrpc":"2.0","result":{"stereoscopicmode":{"label":"Nebeneinander","mode":"split_vertical"}}} XBMCVideoChecker::XBMCVideoChecker(const std::string & address, uint16_t port, bool grabVideo, bool grabPhoto, bool grabAudio, bool grabMenu, bool grabScreensaver, bool enable3DDetection) : QObject(), @@ -30,8 +28,8 @@ XBMCVideoChecker::XBMCVideoChecker(const std::string & address, uint16_t port, b _activePlayerRequest(R"({"id":666,"jsonrpc":"2.0","method":"Player.GetActivePlayers"})"), _currentPlayingItemRequest(R"({"id":667,"jsonrpc":"2.0","method":"Player.GetItem","params":{"playerid":%1,"properties":["file"]}})"), _checkScreensaverRequest(R"({"id":668,"jsonrpc":"2.0","method":"XBMC.GetInfoBooleans","params":{"booleans":["System.ScreenSaverActive"]}})"), - _getStereoscopicMode(R"({"jsonrpc":"2.0","method":"GUI.GetProperties","params":{"properties":["stereoscopicmode"]},"id":1})"), - _getXbmcVersion(R"({"jsonrpc":"2.0","method":"Application.GetProperties","params":{"properties":["version"]},"id":1})"), + _getStereoscopicMode(R"({"jsonrpc":"2.0","method":"GUI.GetProperties","params":{"properties":["stereoscopicmode"]},"id":669})"), + _getXbmcVersion(R"({"jsonrpc":"2.0","method":"Application.GetProperties","params":{"properties":["version"]},"id":670})"), _socket(), _grabVideo(grabVideo), _grabPhoto(grabPhoto), @@ -41,7 +39,8 @@ XBMCVideoChecker::XBMCVideoChecker(const std::string & address, uint16_t port, b _enable3DDetection(enable3DDetection), _previousScreensaverMode(false), _previousGrabbingMode(GRABBINGMODE_INVALID), - _previousVideoMode(VIDEO_2D) + _previousVideoMode(VIDEO_2D), + _xbmcVersion(0) { // setup socket connect(&_socket, SIGNAL(readyRead()), this, SLOT(receiveReply())); @@ -124,10 +123,10 @@ void XBMCVideoChecker::receiveReply() } else if (reply.contains("\"id\":667")) { - if (xbmcVersion >= 13) + if (_xbmcVersion >= 13) { - // check of active stereoscopicmode - _socket.write(_getStereoscopicMode.toUtf8()); + // check of active stereoscopicmode + _socket.write(_getStereoscopicMode.toUtf8()); } else { @@ -162,13 +161,13 @@ void XBMCVideoChecker::receiveReply() // check here xbmc version if (_socket.state() == QTcpSocket::ConnectedState) { - if (xbmcVersion == 0) - { + if (_xbmcVersion == 0) + { _socket.write(_getXbmcVersion.toUtf8()); } } } - else if (reply.contains("\"stereoscopicmode\"")) + else if (reply.contains("\"id\":669")) { QRegExp regex("\"mode\":\"(split_vertical|split_horizontal)\""); int pos = regex.indexIn(reply); @@ -185,13 +184,13 @@ void XBMCVideoChecker::receiveReply() } } } - else if (reply.contains("\"version\":")) + else if (reply.contains("\"id\":670")) { QRegExp regex("\"major\":(\\d+)"); int pos = regex.indexIn(reply); if (pos > 0) { - xbmcVersion = regex.cap(1).toInt(); + _xbmcVersion = regex.cap(1).toInt(); } } } From d5597d55a778d21e6d57f1193219dc4c3a99864c Mon Sep 17 00:00:00 2001 From: johan Date: Wed, 30 Apr 2014 22:53:05 +0200 Subject: [PATCH 44/78] Disable the blackborder detector for effects Former-commit-id: 2d4660f48c17977aabff52b7cbbc8d832b216f00 --- include/hyperion/ImageProcessor.h | 5 ++++- libsrc/effectengine/Effect.cpp | 3 +++ libsrc/hyperion/ImageProcessor.cpp | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/include/hyperion/ImageProcessor.h b/include/hyperion/ImageProcessor.h index deb40df1..5049ac03 100644 --- a/include/hyperion/ImageProcessor.h +++ b/include/hyperion/ImageProcessor.h @@ -37,6 +37,9 @@ public: /// void setSize(const unsigned width, const unsigned height); + /// Enable or disable the black border detector + void enableBalckBorderDetector(bool enable); + /// /// Processes the image to a list of led colors. This will update the size of the buffer-image /// if required and call the image-to-leds mapping to determine the mean color per led. @@ -142,7 +145,7 @@ private: const LedString _ledString; /// Flag the enables(true)/disabled(false) blackborder detector - const bool _enableBlackBorderRemoval; + bool _enableBlackBorderRemoval; /// The processor for black border detection hyperion::BlackBorderProcessor * _borderProcessor; diff --git a/libsrc/effectengine/Effect.cpp b/libsrc/effectengine/Effect.cpp index 09411c87..72abbf3c 100644 --- a/libsrc/effectengine/Effect.cpp +++ b/libsrc/effectengine/Effect.cpp @@ -64,6 +64,9 @@ Effect::Effect(PyThreadState * mainThreadState, int priority, int timeout, const { _colors.resize(_imageProcessor->getLedCount(), ColorRgb::BLACK); + // disable the black border detector for effects + _imageProcessor->enableBalckBorderDetector(false); + // connect the finished signal connect(this, SIGNAL(finished()), this, SLOT(effectFinished())); } diff --git a/libsrc/hyperion/ImageProcessor.cpp b/libsrc/hyperion/ImageProcessor.cpp index 66b02e74..25784d0a 100644 --- a/libsrc/hyperion/ImageProcessor.cpp +++ b/libsrc/hyperion/ImageProcessor.cpp @@ -43,6 +43,11 @@ void ImageProcessor::setSize(const unsigned width, const unsigned height) _imageToLeds = new ImageToLedsMap(width, height, 0, 0, _ledString.leds()); } +void ImageProcessor::enableBalckBorderDetector(bool enable) +{ + _enableBlackBorderRemoval = enable; +} + bool ImageProcessor::getScanParameters(size_t led, double &hscanBegin, double &hscanEnd, double &vscanBegin, double &vscanEnd) const { if (led < _ledString.leds().size()) From 8c1f14f8dc3c0d12b0773f06fa133690b3ea5a10 Mon Sep 17 00:00:00 2001 From: johan Date: Wed, 30 Apr 2014 22:55:39 +0200 Subject: [PATCH 45/78] Update binaries for RPi Former-commit-id: dadd309b43dd982d8ddb1452b9e3c5d656391246 --- 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 a3a391be..a48b195a 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -d61b685eca164580cb39eb5bc3cf65b89afad410 \ No newline at end of file +e80ba1bef0cc5983a767b293f0c5915c2535a47f \ No newline at end of file From 9220c346bbd8a0a6c95626edc89771e85420e21b Mon Sep 17 00:00:00 2001 From: ntim Date: Thu, 1 May 2014 00:02:10 +0200 Subject: [PATCH 46/78] Blocking PUT requests. Only save the brightness and [x,y] state of the lights. Former-commit-id: 5887fdf1b350172dcdd52c46bd8da74dab521444 --- libsrc/leddevice/LedDevicePhilipsHue.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index 690a7b94..2cedc8a5 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -54,9 +54,15 @@ void LedDevicePhilipsHue::put(QString route, QString content) { QHttpRequestHeader header("PUT", url); header.setValue("Host", host); header.setValue("Accept-Encoding", "identity"); + header.setValue("Connection", "keep-alive"); header.setValue("Content-Length", QString("%1").arg(content.size())); - http->setHost(host); + QEventLoop loop; + // Connect requestFinished signal to quit slot of the loop. + loop.connect(http, SIGNAL(requestFinished(int, bool)), SLOT(quit())); + // Perfrom request http->request(header, content.toAscii()); + // Go into the loop until the request is finished. + loop.exec(); } QByteArray LedDevicePhilipsHue::get(QString route) { @@ -92,13 +98,17 @@ void LedDevicePhilipsHue::saveStates(unsigned int nLights) { // Read the response. QByteArray response = get(getRoute(i + 1)); // Parse JSON. - Json::Value state; - if (!reader.parse(QString(response).toStdString(), state)) { + Json::Value json; + if (!reader.parse(QString(response).toStdString(), json)) { // Error occured, break loop. break; } + // Save state object values which are subject to change. + Json::Value state(Json::objectValue); + state["xy"] = json["state"]["xy"]; + state["bri"] = json["state"]["bri"]; // Save state object. - states.push_back(QString(writer.write(state["state"]).c_str())); + states.push_back(QString(writer.write(state).c_str()).trimmed()); } } From 8cca280fdcc1384567a7162a611c46127e9fa953 Mon Sep 17 00:00:00 2001 From: ntim Date: Sat, 3 May 2014 10:12:41 +0200 Subject: [PATCH 47/78] Implemented timeout for restoring original state. Former-commit-id: 925b063aba1a859b592ead9b75ce67d90fb81e28 --- libsrc/leddevice/CMakeLists.txt | 2 +- libsrc/leddevice/LedDevicePhilipsHue.cpp | 5 +++++ libsrc/leddevice/LedDevicePhilipsHue.h | 14 ++++++++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt index 098ac38c..5a5d2c12 100755 --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -14,6 +14,7 @@ include_directories( # Group the headers that go through the MOC compiler SET(Leddevice_QT_HEADERS ${CURRENT_SOURCE_DIR}/LedDeviceAdalight.h + ${CURRENT_SOURCE_DIR}/LedDevicePhilipsHue.h ) SET(Leddevice_HEADERS @@ -29,7 +30,6 @@ SET(Leddevice_HEADERS ${CURRENT_SOURCE_DIR}/LedDeviceSedu.h ${CURRENT_SOURCE_DIR}/LedDeviceTest.h ${CURRENT_SOURCE_DIR}/LedDeviceHyperionUsbasp.h - ${CURRENT_SOURCE_DIR}/LedDevicePhilipsHue.h ) SET(Leddevice_SOURCES diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index 2cedc8a5..35f045f2 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -13,6 +13,9 @@ LedDevicePhilipsHue::LedDevicePhilipsHue(const std::string& output) : host(output.c_str()), username("newdeveloper") { http = new QHttp(host); + timer.setInterval(1000); + timer.setSingleShot(true); + connect(&timer, SIGNAL(timeout()), this, SLOT(restoreStates()())); } LedDevicePhilipsHue::~LedDevicePhilipsHue() { @@ -37,10 +40,12 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { // Next light id. lightId++; } + timer.start(); return 0; } int LedDevicePhilipsHue::switchOff() { + timer.stop(); // If light states have been saved before, ... if (statesSaved()) { // ... restore them. diff --git a/libsrc/leddevice/LedDevicePhilipsHue.h b/libsrc/leddevice/LedDevicePhilipsHue.h index 0bb71182..7a0e86cd 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/LedDevicePhilipsHue.h @@ -4,8 +4,10 @@ #include // Qt includes +#include #include #include +#include // Leddevice includes #include @@ -18,7 +20,8 @@ * Framegrabber must be limited to 10 Hz / numer of lights to avoid rate limitation by the hue bridge. * Create a new API user name "newdeveloper" on the bridge (http://developers.meethue.com/gettingstarted.html) */ -class LedDevicePhilipsHue: public LedDevice { +class LedDevicePhilipsHue: public QObject, public LedDevice { +Q_OBJECT public: /// /// Constructs the device. @@ -44,6 +47,10 @@ public: /// Switch the leds off virtual int switchOff(); +private slots: + /// Restores the status of all lights. + void restoreStates(); + private: /// Array to save the light states. std::vector states; @@ -53,6 +60,8 @@ private: QString username; /// Qhttp object for sending requests. QHttp* http; + /// Use timer to reset lights when we got into "GRABBINGMODE_OFF". + QTimer timer; /// /// Sends a HTTP GET request (blocking). @@ -93,9 +102,6 @@ private: /// void saveStates(unsigned int nLights); - /// Restores the status of all lights. - void restoreStates(); - /// /// @return true if light states have been saved. /// From 3eb29146dd26568de7661d361e21c9a41e165a35 Mon Sep 17 00:00:00 2001 From: johan Date: Sun, 4 May 2014 11:31:13 +0200 Subject: [PATCH 48/78] Added a possible delay after connecting an Adalight device Former-commit-id: 756fae2ebc57455bf6360dc96bf2ae5469460172 --- deploy/hyperion.tar.gz.REMOVED.git-id | 2 +- include/utils/Sleep.h | 10 ++++++++++ libsrc/leddevice/LedDeviceAdalight.cpp | 4 ++-- libsrc/leddevice/LedDeviceAdalight.h | 2 +- libsrc/leddevice/LedDeviceFactory.cpp | 3 ++- libsrc/leddevice/LedRs232Device.cpp | 19 +++++++++++++------ libsrc/leddevice/LedRs232Device.h | 10 +++++++--- libsrc/utils/CMakeLists.txt | 1 + 8 files changed, 37 insertions(+), 14 deletions(-) create mode 100644 include/utils/Sleep.h diff --git a/deploy/hyperion.tar.gz.REMOVED.git-id b/deploy/hyperion.tar.gz.REMOVED.git-id index a48b195a..288cb926 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -e80ba1bef0cc5983a767b293f0c5915c2535a47f \ No newline at end of file +ab3338d1164469e008bd9635c817659b6e528a1f \ No newline at end of file diff --git a/include/utils/Sleep.h b/include/utils/Sleep.h new file mode 100644 index 00000000..a2c55815 --- /dev/null +++ b/include/utils/Sleep.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +class Sleep : protected QThread { +public: + static inline void msleep(unsigned long msecs) { + QThread::msleep(msecs); + } +}; diff --git a/libsrc/leddevice/LedDeviceAdalight.cpp b/libsrc/leddevice/LedDeviceAdalight.cpp index f19bb4be..47c09278 100644 --- a/libsrc/leddevice/LedDeviceAdalight.cpp +++ b/libsrc/leddevice/LedDeviceAdalight.cpp @@ -11,8 +11,8 @@ // hyperion local includes #include "LedDeviceAdalight.h" -LedDeviceAdalight::LedDeviceAdalight(const std::string& outputDevice, const unsigned baudrate) : - LedRs232Device(outputDevice, baudrate), +LedDeviceAdalight::LedDeviceAdalight(const std::string& outputDevice, const unsigned baudrate, int delayAfterConnect_ms) : + LedRs232Device(outputDevice, baudrate, delayAfterConnect_ms), _ledBuffer(0), _timer() { diff --git a/libsrc/leddevice/LedDeviceAdalight.h b/libsrc/leddevice/LedDeviceAdalight.h index 30e7bfb7..3621c5ca 100644 --- a/libsrc/leddevice/LedDeviceAdalight.h +++ b/libsrc/leddevice/LedDeviceAdalight.h @@ -23,7 +23,7 @@ public: /// @param outputDevice The name of the output device (eg '/dev/ttyS0') /// @param baudrate The used baudrate for writing to the output device /// - LedDeviceAdalight(const std::string& outputDevice, const unsigned baudrate); + LedDeviceAdalight(const std::string& outputDevice, const unsigned baudrate, int delayAfterConnect_ms); /// /// Writes the led color values to the led-device diff --git a/libsrc/leddevice/LedDeviceFactory.cpp b/libsrc/leddevice/LedDeviceFactory.cpp index 71aeda06..76eb0137 100755 --- a/libsrc/leddevice/LedDeviceFactory.cpp +++ b/libsrc/leddevice/LedDeviceFactory.cpp @@ -43,8 +43,9 @@ LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) { const std::string output = deviceConfig["output"].asString(); const unsigned rate = deviceConfig["rate"].asInt(); + const int delay_ms = deviceConfig["delayAfterConnect"].asInt(); - LedDeviceAdalight* deviceAdalight = new LedDeviceAdalight(output, rate); + LedDeviceAdalight* deviceAdalight = new LedDeviceAdalight(output, rate, delay_ms); deviceAdalight->open(); device = deviceAdalight; diff --git a/libsrc/leddevice/LedRs232Device.cpp b/libsrc/leddevice/LedRs232Device.cpp index 6bc3560b..c8c185a4 100644 --- a/libsrc/leddevice/LedRs232Device.cpp +++ b/libsrc/leddevice/LedRs232Device.cpp @@ -9,11 +9,13 @@ // Local Hyperion includes #include "LedRs232Device.h" +#include "utils/Sleep.h" -LedRs232Device::LedRs232Device(const std::string& outputDevice, const unsigned baudrate) : - mDeviceName(outputDevice), - mBaudRate_Hz(baudrate), +LedRs232Device::LedRs232Device(const std::string& outputDevice, const unsigned baudrate, int delayAfterConnect_ms) : + _deviceName(outputDevice), + _baudRate_Hz(baudrate), + _delayAfterConnect_ms(delayAfterConnect_ms), _rs232Port() { // empty @@ -31,10 +33,15 @@ int LedRs232Device::open() { try { - std::cout << "Opening UART: " << mDeviceName << std::endl; - _rs232Port.setPort(mDeviceName); - _rs232Port.setBaudrate(mBaudRate_Hz); + std::cout << "Opening UART: " << _deviceName << std::endl; + _rs232Port.setPort(_deviceName); + _rs232Port.setBaudrate(_baudRate_Hz); _rs232Port.open(); + + if (_delayAfterConnect_ms > 0) + { + Sleep::msleep(_delayAfterConnect_ms); + } } catch (const std::exception& e) { diff --git a/libsrc/leddevice/LedRs232Device.h b/libsrc/leddevice/LedRs232Device.h index 856a80fe..f8154581 100644 --- a/libsrc/leddevice/LedRs232Device.h +++ b/libsrc/leddevice/LedRs232Device.h @@ -18,7 +18,7 @@ public: /// @param[in] outputDevice The name of the output device (eg '/etc/ttyS0') /// @param[in] baudrate The used baudrate for writing to the output device /// - LedRs232Device(const std::string& outputDevice, const unsigned baudrate); + LedRs232Device(const std::string& outputDevice, const unsigned baudrate, int delayAfterConnect_ms = 0); /// /// Destructor of the LedDevice; closes the output device if it is open @@ -45,9 +45,13 @@ protected: private: /// The name of the output device - const std::string mDeviceName; + const std::string _deviceName; + /// The used baudrate of the output device - const int mBaudRate_Hz; + const int _baudRate_Hz; + + /// Sleep after the connect before continuing + const int _delayAfterConnect_ms; /// The RS232 serial-device serial::Serial _rs232Port; diff --git a/libsrc/utils/CMakeLists.txt b/libsrc/utils/CMakeLists.txt index 52094a61..b1f0709e 100644 --- a/libsrc/utils/CMakeLists.txt +++ b/libsrc/utils/CMakeLists.txt @@ -11,6 +11,7 @@ add_library(hyperion-utils ${CURRENT_HEADER_DIR}/ColorRgba.h ${CURRENT_SOURCE_DIR}/ColorRgba.cpp ${CURRENT_HEADER_DIR}/Image.h + ${CURRENT_HEADER_DIR}/Sleep.h ${CURRENT_HEADER_DIR}/HsvTransform.h ${CURRENT_SOURCE_DIR}/HsvTransform.cpp From 1e2160279876ee001dcdfe0c674dbbcf3e53ce1f Mon Sep 17 00:00:00 2001 From: johan Date: Sun, 4 May 2014 11:41:55 +0200 Subject: [PATCH 49/78] Changed connection delay to something non-blocking Former-commit-id: b313005a29cb42eb6839060bc4c48e4cbd927d3b --- deploy/hyperion.tar.gz.REMOVED.git-id | 2 +- libsrc/leddevice/CMakeLists.txt | 3 +-- libsrc/leddevice/LedDeviceAdalight.h | 2 +- libsrc/leddevice/LedRs232Device.cpp | 23 +++++++++++++++++++---- libsrc/leddevice/LedRs232Device.h | 12 +++++++++++- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/deploy/hyperion.tar.gz.REMOVED.git-id b/deploy/hyperion.tar.gz.REMOVED.git-id index 288cb926..a76f2a2a 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -ab3338d1164469e008bd9635c817659b6e528a1f \ No newline at end of file +e7867d56a269136c5c795a55b831dca58e962139 \ No newline at end of file diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt index 098ac38c..ec1ee381 100755 --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -13,6 +13,7 @@ include_directories( # Group the headers that go through the MOC compiler SET(Leddevice_QT_HEADERS + ${CURRENT_SOURCE_DIR}/LedRs232Device.h ${CURRENT_SOURCE_DIR}/LedDeviceAdalight.h ) @@ -20,8 +21,6 @@ SET(Leddevice_HEADERS ${CURRENT_HEADER_DIR}/LedDevice.h ${CURRENT_HEADER_DIR}/LedDeviceFactory.h - ${CURRENT_SOURCE_DIR}/LedRs232Device.h - ${CURRENT_SOURCE_DIR}/LedDeviceLightpack.h ${CURRENT_SOURCE_DIR}/LedDeviceMultiLightpack.h ${CURRENT_SOURCE_DIR}/LedDevicePaintpack.h diff --git a/libsrc/leddevice/LedDeviceAdalight.h b/libsrc/leddevice/LedDeviceAdalight.h index 3621c5ca..bc08200b 100644 --- a/libsrc/leddevice/LedDeviceAdalight.h +++ b/libsrc/leddevice/LedDeviceAdalight.h @@ -12,7 +12,7 @@ /// /// Implementation of the LedDevice interface for writing to an Adalight led device. /// -class LedDeviceAdalight : public QObject, public LedRs232Device +class LedDeviceAdalight : public LedRs232Device { Q_OBJECT diff --git a/libsrc/leddevice/LedRs232Device.cpp b/libsrc/leddevice/LedRs232Device.cpp index c8c185a4..a4b058d2 100644 --- a/libsrc/leddevice/LedRs232Device.cpp +++ b/libsrc/leddevice/LedRs232Device.cpp @@ -4,19 +4,21 @@ #include #include +// Qt includes +#include + // Serial includes #include // Local Hyperion includes #include "LedRs232Device.h" -#include "utils/Sleep.h" - LedRs232Device::LedRs232Device(const std::string& outputDevice, const unsigned baudrate, int delayAfterConnect_ms) : _deviceName(outputDevice), _baudRate_Hz(baudrate), _delayAfterConnect_ms(delayAfterConnect_ms), - _rs232Port() + _rs232Port(), + _blockedForDelay(false) { // empty } @@ -40,7 +42,9 @@ int LedRs232Device::open() if (_delayAfterConnect_ms > 0) { - Sleep::msleep(_delayAfterConnect_ms); + _blockedForDelay = true; + QTimer::singleShot(_delayAfterConnect_ms, this, SLOT(unblockAfterDelay())); + std::cout << "Device blocked for " << _delayAfterConnect_ms << " ms" << std::endl; } } catch (const std::exception& e) @@ -54,6 +58,11 @@ int LedRs232Device::open() int LedRs232Device::writeBytes(const unsigned size, const uint8_t * data) { + if (_blockedForDelay) + { + return 0; + } + if (!_rs232Port.isOpen()) { return -1; @@ -102,3 +111,9 @@ int LedRs232Device::writeBytes(const unsigned size, const uint8_t * data) return 0; } + +void LedRs232Device::unblockAfterDelay() +{ + std::cout << "Device unblocked" << std::endl; + _blockedForDelay = false; +} diff --git a/libsrc/leddevice/LedRs232Device.h b/libsrc/leddevice/LedRs232Device.h index f8154581..e11d0a8a 100644 --- a/libsrc/leddevice/LedRs232Device.h +++ b/libsrc/leddevice/LedRs232Device.h @@ -1,5 +1,7 @@ #pragma once +#include + // Serial includes #include @@ -9,8 +11,10 @@ /// /// The LedRs232Device implements an abstract base-class for LedDevices using a RS232-device. /// -class LedRs232Device : public LedDevice +class LedRs232Device : public QObject, public LedDevice { + Q_OBJECT + public: /// /// Constructs the LedDevice attached to a RS232-device @@ -43,6 +47,10 @@ protected: */ int writeBytes(const unsigned size, const uint8_t *data); +private slots: + /// Unblock the device after a connection delay + void unblockAfterDelay(); + private: /// The name of the output device const std::string _deviceName; @@ -55,4 +63,6 @@ private: /// The RS232 serial-device serial::Serial _rs232Port; + + bool _blockedForDelay; }; From f9906845e0efb0919d5476d199c0a4a90d6c4d17 Mon Sep 17 00:00:00 2001 From: johan Date: Sun, 4 May 2014 14:20:25 +0200 Subject: [PATCH 50/78] Update the install script - follow =github redirections when using curl under OpenElec - Use raw.githubusercontent.com iso raw.github.com to prevent a redirection Former-commit-id: af76999acd598d4383b3a64b09e1407cd666ff99 --- bin/install_hyperion.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/install_hyperion.sh b/bin/install_hyperion.sh index 53dd28e6..f697c2f2 100755 --- a/bin/install_hyperion.sh +++ b/bin/install_hyperion.sh @@ -34,13 +34,13 @@ fi echo 'Downloading hyperion' if [ $IS_OPENELEC -eq 1 ]; then # OpenELEC has a readonly file system. Use alternative location - curl --get https://raw.github.com/tvdzwan/hyperion/master/deploy/hyperion.tar.gz | tar -C /storage -xz - curl --get https://raw.github.com/tvdzwan/hyperion/master/deploy/hyperion.deps.openelec-rpi.tar.gz | tar -C /storage/hyperion/bin -xz + curl -L --get https://raw.githubusercontent.com/tvdzwan/hyperion/master/deploy/hyperion.tar.gz | tar -C /storage -xz + curl -L --get https://raw.githubusercontent.com/tvdzwan/hyperion/master/deploy/hyperion.deps.openelec-rpi.tar.gz | tar -C /storage/hyperion/bin -xz # modify the default config to have a correct effect path sed -i 's:/opt:/storage:g' /storage/hyperion/config/hyperion.config.json else - wget https://raw.github.com/tvdzwan/hyperion/master/deploy/hyperion.tar.gz -O - | tar -C /opt -xz + wget https://raw.githubusercontent.com/tvdzwan/hyperion/master/deploy/hyperion.tar.gz -O - | tar -C /opt -xz fi # create links to the binaries @@ -68,9 +68,9 @@ fi if [ $USE_INITCTL -eq 1 ]; then echo 'Installing initctl script' if [ $IS_RASPBMC -eq 1 ]; then - wget -N https://raw.github.com/tvdzwan/hyperion/master/deploy/hyperion.conf -P /etc/init/ + wget -N https://raw.githubusercontent.com/tvdzwan/hyperion/master/deploy/hyperion.conf -P /etc/init/ else - wget -N https://raw.github.com/tvdzwan/hyperion/master/deploy/hyperion.xbian.conf -O /etc/init/hyperion.conf + wget -N https://raw.githubusercontent.com/tvdzwan/hyperion/master/deploy/hyperion.xbian.conf -O /etc/init/hyperion.conf fi elif [ $USE_SERVICE -eq 1 ]; then echo 'Installing startup script in init.d' From 106257181b50b9945cf38cfac31b1922d02dbcea Mon Sep 17 00:00:00 2001 From: Marc Dahlem Date: Tue, 6 May 2014 18:36:34 +0200 Subject: [PATCH 51/78] Added ability to define effect arguments for the bootsequence Former-commit-id: b9827bb7af6c2cfdd8b1b0388d16541612b360d4 --- src/hyperiond/hyperiond.cpp | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/hyperiond/hyperiond.cpp b/src/hyperiond/hyperiond.cpp index c6174fa9..b6365019 100644 --- a/src/hyperiond/hyperiond.cpp +++ b/src/hyperiond/hyperiond.cpp @@ -112,14 +112,31 @@ int main(int argc, char** argv) const std::string effectName = effectConfig["effect"].asString(); const unsigned duration_ms = effectConfig["duration_ms"].asUInt(); const int priority = 0; - - if (hyperion.setEffect(effectName, priority, duration_ms) == 0) + + if (effectConfig.isMember("args")) { - std::cout << "Boot sequence(" << effectName << ") created and started" << std::endl; + 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; + } + } else { - std::cout << "Failed to start boot sequence: " << effectName << std::endl; + + if (hyperion.setEffect(effectName, priority, duration_ms) == 0) + { + std::cout << "Boot sequence(" << effectName << ") created and started" << std::endl; + } + else + { + std::cout << "Failed to start boot sequence: " << effectName << std::endl; + } } } From d50b46bda2c1787b62adf252c235fc58203dfbe4 Mon Sep 17 00:00:00 2001 From: johan Date: Tue, 6 May 2014 22:02:40 +0200 Subject: [PATCH 52/78] Fix integration area = 0 for a led Former-commit-id: b365c4c9605a84ea6b7f88043f6f9c70b7122931 --- libsrc/hyperion/ImageToLedsMap.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libsrc/hyperion/ImageToLedsMap.cpp b/libsrc/hyperion/ImageToLedsMap.cpp index 202f22ff..aa309768 100644 --- a/libsrc/hyperion/ImageToLedsMap.cpp +++ b/libsrc/hyperion/ImageToLedsMap.cpp @@ -35,15 +35,16 @@ ImageToLedsMap::ImageToLedsMap( // skip leds without area if ((led.maxX_frac-led.minX_frac) < 1e-6 || (led.maxY_frac-led.minY_frac) < 1e-6) { + mColorsMap.emplace_back(); continue; } - + // Compute the index boundaries for this led unsigned minX_idx = xOffset + unsigned(std::round(actualWidth * led.minX_frac)); unsigned maxX_idx = xOffset + unsigned(std::round(actualWidth * led.maxX_frac)); unsigned minY_idx = yOffset + unsigned(std::round(actualHeight * led.minY_frac)); unsigned maxY_idx = yOffset + unsigned(std::round(actualHeight * led.maxY_frac)); - + // make sure that the area is at least a single led large minX_idx = std::min(minX_idx, xOffset + actualWidth - 1); if (minX_idx == maxX_idx) From dca9371f6d7261d02e336935c7ef2dc41e2c52af Mon Sep 17 00:00:00 2001 From: johan Date: Tue, 6 May 2014 22:05:41 +0200 Subject: [PATCH 53/78] Update binaries Former-commit-id: d1dc28de9db9a90ecfc185882f994d917bce8e6f --- 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 a76f2a2a..7f4560e3 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -e7867d56a269136c5c795a55b831dca58e962139 \ No newline at end of file +e3e4ea5204c555e64aa909d5bbbd6ac95ebec0dc \ No newline at end of file From 853d00289425d90e71331005d152400a7ae69156 Mon Sep 17 00:00:00 2001 From: ntim Date: Wed, 7 May 2014 15:22:13 +0200 Subject: [PATCH 54/78] Timer to restore light state properly implemented. Lights are not switched on after state has been saved. Former-commit-id: 04959ea3731eb7de9f59b80a789f03cfeb2d4ba3 --- libsrc/leddevice/LedDevicePhilipsHue.cpp | 18 ++++++++++++++---- libsrc/leddevice/LedDevicePhilipsHue.h | 9 ++++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index 35f045f2..f8a9698a 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -13,9 +13,9 @@ LedDevicePhilipsHue::LedDevicePhilipsHue(const std::string& output) : host(output.c_str()), username("newdeveloper") { http = new QHttp(host); - timer.setInterval(1000); + timer.setInterval(3000); timer.setSingleShot(true); - connect(&timer, SIGNAL(timeout()), this, SLOT(restoreStates()())); + connect(&timer, SIGNAL(timeout()), this, SLOT(restoreStates())); } LedDevicePhilipsHue::~LedDevicePhilipsHue() { @@ -26,6 +26,7 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { // Save light states if not done before. if (!statesSaved()) { saveStates(ledValues.size()); + switchOn(ledValues.size()); } // Iterate through colors and set light states. unsigned int lightId = 1; @@ -110,13 +111,22 @@ void LedDevicePhilipsHue::saveStates(unsigned int nLights) { } // Save state object values which are subject to change. Json::Value state(Json::objectValue); - state["xy"] = json["state"]["xy"]; - state["bri"] = json["state"]["bri"]; + state["on"] = json["state"]["on"]; + if (json["state"]["on"] == true) { + state["xy"] = json["state"]["xy"]; + state["bri"] = json["state"]["bri"]; + } // Save state object. states.push_back(QString(writer.write(state).c_str()).trimmed()); } } +void LedDevicePhilipsHue::switchOn(unsigned int nLights) { + for (unsigned int i = 0; i < nLights; i++) { + put(getStateRoute(i + 1), "{\"on\": true}"); + } +} + void LedDevicePhilipsHue::restoreStates() { unsigned int lightId = 1; for (QString state : states) { diff --git a/libsrc/leddevice/LedDevicePhilipsHue.h b/libsrc/leddevice/LedDevicePhilipsHue.h index 435fc800..84e7bd35 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/LedDevicePhilipsHue.h @@ -46,7 +46,7 @@ public: /// virtual int write(const std::vector & ledValues); - /// Switch the leds off + /// Restores the original state of the leds. virtual int switchOff(); private slots: @@ -104,6 +104,13 @@ private: /// void saveStates(unsigned int nLights); + /// + /// Switches the leds on. + /// + /// @param nLights the number of lights + /// + void switchOn(unsigned int nLights); + /// /// @return true if light states have been saved. /// From 2b3a3be0d3b7d3a8c9a734bb61535309c01afd8a Mon Sep 17 00:00:00 2001 From: Fabian Hertwig Date: Sat, 24 May 2014 13:03:46 +0200 Subject: [PATCH 55/78] Added the possibility to change the base color of the mood blobs over time. The base Color is moved 1 degree in baseColorChangeRate seconds if activated. It is moved between baseColorRangeLeft and baseColorRangeRight. These Values are in degrees. When these borders are set to the full circle (eg. 0 and 360), the base color moves around the colorwheel, otherwise it moves from left to right and back again. Furthermore there are three effects script for this feature: "Full color mood blobs" which moves around the full circle, "Warm mood blobs" and "Cold mood blobs" which only shows the warm, cold colors. This update wont change the functionality of the old scripts. Former-commit-id: 0c7a2ad280e49cd1ac0d6a9fbc9d1a9ff0eea236 --- effects/mood-blobs-cold.json | 16 +++++++ effects/mood-blobs-full.json | 16 +++++++ effects/mood-blobs-warm.json | 16 +++++++ effects/mood-blobs.py | 93 ++++++++++++++++++++++++++++-------- 4 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 effects/mood-blobs-cold.json create mode 100644 effects/mood-blobs-full.json create mode 100644 effects/mood-blobs-warm.json diff --git a/effects/mood-blobs-cold.json b/effects/mood-blobs-cold.json new file mode 100644 index 00000000..d259b9f1 --- /dev/null +++ b/effects/mood-blobs-cold.json @@ -0,0 +1,16 @@ +{ + "name" : "Cold mood blobs", + "script" : "mood-blobs.py", + "args" : + { + "rotationTime" : 60.0, + "color" : [0,0,255], + "hueChange" : 30.0, + "blobs" : 5, + "reverse" : false, + "baseChange" : true, + "baseColorRangeLeft" : 160, + "baseColorRangeRight" : 320, + "baseColorChangeRate" : 2.0 + } +} diff --git a/effects/mood-blobs-full.json b/effects/mood-blobs-full.json new file mode 100644 index 00000000..8b230010 --- /dev/null +++ b/effects/mood-blobs-full.json @@ -0,0 +1,16 @@ +{ + "name" : "Full color mood blobs", + "script" : "mood-blobs.py", + "args" : + { + "rotationTime" : 60.0, + "color" : [0,0,255], + "hueChange" : 30.0, + "blobs" : 5, + "reverse" : false, + "baseChange" : true, + "baseColorRangeLeft" : 0, + "baseColorRangeRight" : 360, + "baseColorChangeRate" : 0.2 + } +} diff --git a/effects/mood-blobs-warm.json b/effects/mood-blobs-warm.json new file mode 100644 index 00000000..39443092 --- /dev/null +++ b/effects/mood-blobs-warm.json @@ -0,0 +1,16 @@ +{ + "name" : "Warm mood blobs", + "script" : "mood-blobs.py", + "args" : + { + "rotationTime" : 60.0, + "color" : [255,0,0], + "hueChange" : 30.0, + "blobs" : 5, + "reverse" : false, + "baseChange" : true, + "baseColorRangeLeft" : 333, + "baseColorRangeRight" : 151, + "baseColorChangeRate" : 2.0 + } +} diff --git a/effects/mood-blobs.py b/effects/mood-blobs.py index c0ce3d15..b638f3d3 100644 --- a/effects/mood-blobs.py +++ b/effects/mood-blobs.py @@ -6,14 +6,31 @@ import math # Get the parameters rotationTime = float(hyperion.args.get('rotationTime', 20.0)) color = hyperion.args.get('color', (0,0,255)) -hueChange = float(hyperion.args.get('hueChange', 60.0)) / 360.0 +hueChange = float(hyperion.args.get('hueChange', 60.0)) blobs = int(hyperion.args.get('blobs', 5)) reverse = bool(hyperion.args.get('reverse', False)) +baseColorChange = bool(hyperion.args.get('baseChange', False)) +baseColorRangeLeft = float(hyperion.args.get('baseColorRangeLeft',0.0)) # Degree +baseColorRangeRight = float(hyperion.args.get('baseColorRangeRight',360.0)) # Degree +baseColorChangeRate = float(hyperion.args.get('baseColorChangeRate',10.0)) # Seconds for one Degree + +# switch baseColor change off if left and right are too close together to see a difference in color +if (baseColorRangeRight > baseColorRangeLeft and (baseColorRangeRight - baseColorRangeLeft) < 10) or \ + (baseColorRangeLeft > baseColorRangeRight and ((baseColorRangeRight + 360) - baseColorRangeLeft) < 10): + baseColorChange = False + +# 360 -> 1 +fullColorWheelAvailable = (baseColorRangeRight % 360) == (baseColorRangeLeft % 360) +baseColorChangeIncreaseValue = 1.0 / 360.0 # 1 degree +hueChange /= 360.0 +baseColorRangeLeft = (baseColorRangeLeft / 360.0) +baseColorRangeRight = (baseColorRangeRight / 360.0) # Check parameters rotationTime = max(0.1, rotationTime) hueChange = max(0.0, min(abs(hueChange), .5)) blobs = max(1, blobs) +baseColorChangeRate = max(0, baseColorChangeRate) # > 0 # Calculate the color data baseHsv = colorsys.rgb_to_hsv(color[0]/255.0, color[1]/255.0, color[2]/255.0) @@ -27,6 +44,7 @@ for i in range(hyperion.ledCount): sleepTime = 0.1 amplitudePhaseIncrement = blobs * math.pi * sleepTime / rotationTime colorDataIncrement = 3 +baseColorChangeRate /= sleepTime # Switch direction if needed if reverse: @@ -39,23 +57,58 @@ colors = bytearray(hyperion.ledCount * (0,0,0)) # Start the write data loop amplitudePhase = 0.0 rotateColors = False -while not hyperion.abort(): - # Calculate new colors - for i in range(hyperion.ledCount): - amplitude = max(0.0, math.sin(-amplitudePhase + 2*math.pi * blobs * i / hyperion.ledCount)) - colors[3*i+0] = int(colorData[3*i+0] * amplitude) - colors[3*i+1] = int(colorData[3*i+1] * amplitude) - colors[3*i+2] = int(colorData[3*i+2] * amplitude) +baseColorChangeStepCount = 0 +baseHSVValue = baseHsv[0] +numberOfRotates = 0 - # set colors - hyperion.setColor(colors) - - # increment the phase - amplitudePhase = (amplitudePhase + amplitudePhaseIncrement) % (2*math.pi) - - if rotateColors: - colorData = colorData[-colorDataIncrement:] + colorData[:-colorDataIncrement] - rotateColors = not rotateColors - - # sleep for a while - time.sleep(sleepTime) +while not hyperion.abort(): + + # move the basecolor + if baseColorChange: + # every baseColorChangeRate seconds + if baseColorChangeStepCount >= baseColorChangeRate: + baseColorChangeStepCount = 0 + # cyclic increment when the full colorwheel is available, move up and down otherwise + if fullColorWheelAvailable: + baseHSVValue = (baseHSVValue + baseColorChangeIncreaseValue) % baseColorRangeRight + else: + # switch increment direction if baseHSV <= left or baseHSV >= right + if baseColorChangeIncreaseValue < 0 and baseHSVValue > baseColorRangeLeft and (baseHSVValue + baseColorChangeIncreaseValue) <= baseColorRangeLeft: + baseColorChangeIncreaseValue = abs(baseColorChangeIncreaseValue) + elif baseColorChangeIncreaseValue > 0 and baseHSVValue < baseColorRangeRight and (baseHSVValue + baseColorChangeIncreaseValue) >= baseColorRangeRight : + baseColorChangeIncreaseValue = -abs(baseColorChangeIncreaseValue) + + baseHSVValue = (baseHSVValue + baseColorChangeIncreaseValue) % 1.0 + + # update color values + colorData = bytearray() + for i in range(hyperion.ledCount): + hue = (baseHSVValue + hueChange * math.sin(2*math.pi * i / hyperion.ledCount)) % 1.0 + rgb = colorsys.hsv_to_rgb(hue, baseHsv[1], baseHsv[2]) + colorData += bytearray((int(255*rgb[0]), int(255*rgb[1]), int(255*rgb[2]))) + + # set correct rotation after reinitialisation of the array + colorData = colorData[-colorDataIncrement*numberOfRotates:] + colorData[:-colorDataIncrement*numberOfRotates] + + baseColorChangeStepCount += 1 + + # Calculate new colors + for i in range(hyperion.ledCount): + amplitude = max(0.0, math.sin(-amplitudePhase + 2*math.pi * blobs * i / hyperion.ledCount)) + colors[3*i+0] = int(colorData[3*i+0] * amplitude) + colors[3*i+1] = int(colorData[3*i+1] * amplitude) + colors[3*i+2] = int(colorData[3*i+2] * amplitude) + + # set colors + hyperion.setColor(colors) + + # increment the phase + amplitudePhase = (amplitudePhase + amplitudePhaseIncrement) % (2*math.pi) + + if rotateColors: + colorData = colorData[-colorDataIncrement:] + colorData[:-colorDataIncrement] + numberOfRotates = (numberOfRotates + 1) % hyperion.ledCount + rotateColors = not rotateColors + + # sleep for a while + time.sleep(sleepTime) From 8f1bb8e9db1b43ab5cb9710cbd805b1e04ee2d98 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Sat, 24 May 2014 21:05:59 +0200 Subject: [PATCH 56/78] Updated hyperion release to include philips hue Former-commit-id: d46ccf12d1f2a8059d45f946fe0f4c43e8b5bc9e --- 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 7f4560e3..b04b8f6e 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -e3e4ea5204c555e64aa909d5bbbd6ac95ebec0dc \ No newline at end of file +bfe399d8f3299c6110179fddb9f2b06b475d6a53 \ No newline at end of file From 98dfe7997f9eefa86da2164edd8e659372ec1005 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Mon, 2 Jun 2014 21:38:41 +0200 Subject: [PATCH 57/78] Updated release with updated blob effect Former-commit-id: 3d859fb7283961152ff67eb8101db89bdcd21847 --- 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 dde0b4dd..35028a43 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -e6c826638971667596af0fb18e819514d8390286 \ No newline at end of file +d9eb8f0ef98c76bc54a43cc572183f7c54fc4dc9 \ No newline at end of file From 1d046ab35a8021c7bf5719ed508e50e1c07850f0 Mon Sep 17 00:00:00 2001 From: Gamadril Date: Sun, 15 Jun 2014 02:19:09 +0200 Subject: [PATCH 58/78] Added support for tpm2 protocol. One frame used for all LEDs Former-commit-id: 5b2ed33a50d90999c6f9508ba3782ad73838fb56 --- libsrc/leddevice/CMakeLists.txt | 2 ++ libsrc/leddevice/LedDeviceFactory.cpp | 10 +++++++ libsrc/leddevice/LedDeviceTpm2.cpp | 42 +++++++++++++++++++++++++++ libsrc/leddevice/LedDeviceTpm2.h | 38 ++++++++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 libsrc/leddevice/LedDeviceTpm2.cpp create mode 100644 libsrc/leddevice/LedDeviceTpm2.h diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt index 093975db..441618d2 100755 --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -29,6 +29,7 @@ SET(Leddevice_HEADERS ${CURRENT_SOURCE_DIR}/LedDeviceSedu.h ${CURRENT_SOURCE_DIR}/LedDeviceTest.h ${CURRENT_SOURCE_DIR}/LedDeviceHyperionUsbasp.h + ${CURRENT_SOURCE_DIR}/LedDeviceTpm2.h ) SET(Leddevice_SOURCES @@ -45,6 +46,7 @@ SET(Leddevice_SOURCES ${CURRENT_SOURCE_DIR}/LedDeviceTest.cpp ${CURRENT_SOURCE_DIR}/LedDeviceHyperionUsbasp.cpp ${CURRENT_SOURCE_DIR}/LedDevicePhilipsHue.cpp + ${CURRENT_SOURCE_DIR}/LedDeviceTpm2.cpp ) if(ENABLE_SPIDEV) diff --git a/libsrc/leddevice/LedDeviceFactory.cpp b/libsrc/leddevice/LedDeviceFactory.cpp index 76eb0137..9c42384a 100755 --- a/libsrc/leddevice/LedDeviceFactory.cpp +++ b/libsrc/leddevice/LedDeviceFactory.cpp @@ -29,6 +29,7 @@ #include "LedDeviceTest.h" #include "LedDeviceHyperionUsbasp.h" #include "LedDevicePhilipsHue.h" +#include "LedDeviceTpm2.h" LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) { @@ -171,6 +172,15 @@ LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) const std::string output = deviceConfig["output"].asString(); device = new LedDeviceTest(output); } + else if (type == "tpm2") + { + const std::string output = deviceConfig["output"].asString(); + const unsigned rate = deviceConfig["rate"].asInt(); + + LedDeviceTpm2* deviceTpm2 = new LedDeviceTpm2(output, rate); + deviceTpm2->open(); + device = deviceTpm2; + } else { std::cout << "Unable to create device " << type << std::endl; diff --git a/libsrc/leddevice/LedDeviceTpm2.cpp b/libsrc/leddevice/LedDeviceTpm2.cpp new file mode 100644 index 00000000..49bb6121 --- /dev/null +++ b/libsrc/leddevice/LedDeviceTpm2.cpp @@ -0,0 +1,42 @@ + +// STL includes +#include +#include +#include + +// Linux includes +#include +#include + +// hyperion local includes +#include "LedDeviceTpm2.h" + +LedDeviceTpm2::LedDeviceTpm2(const std::string& outputDevice, const unsigned baudrate) : + LedRs232Device(outputDevice, baudrate), + _ledBuffer(0) +{ + // empty +} + +int LedDeviceTpm2::write(const std::vector &ledValues) +{ + if (_ledBuffer.size() == 0) + { + _ledBuffer.resize(5 + 3*ledValues.size()); + _ledBuffer[0] = 0xC9; // block-start byte + _ledBuffer[1] = 0xDA; // DATA frame + _ledBuffer[2] = ((3 * ledValues.size()) >> 8) & 0xFF; // LED count high byte + _ledBuffer[3] = (3 * ledValues.size()) & 0xFF; // LED count low byte + _ledBuffer.back() = 0x36; // block-end byte + } + + // write data + memcpy(4 + _ledBuffer.data(), ledValues.data(), ledValues.size() * 3); + return writeBytes(_ledBuffer.size(), _ledBuffer.data()); +} + +int LedDeviceTpm2::switchOff() +{ + memset(4 + _ledBuffer.data(), 0, _ledBuffer.size() - 5); + return writeBytes(_ledBuffer.size(), _ledBuffer.data()); +} diff --git a/libsrc/leddevice/LedDeviceTpm2.h b/libsrc/leddevice/LedDeviceTpm2.h new file mode 100644 index 00000000..f7ca8680 --- /dev/null +++ b/libsrc/leddevice/LedDeviceTpm2.h @@ -0,0 +1,38 @@ +#pragma once + +// STL includes +#include + +// hyperion incluse +#include "LedRs232Device.h" + +/// +/// Implementation of the LedDevice interface for writing to serial device using tpm2 protocol. +/// +class LedDeviceTpm2 : public LedRs232Device +{ +public: + /// + /// Constructs the LedDevice for attached serial device using supporting tpm2 protocol + /// All LEDs in the stripe are handled as one frame + /// + /// @param outputDevice The name of the output device (eg '/dev/ttyAMA0') + /// @param baudrate The used baudrate for writing to the output device + /// + LedDeviceTpm2(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 buffer containing the packed RGB values + std::vector _ledBuffer; +}; From 3135b89255f529737da02ce4bde073328a5ee509 Mon Sep 17 00:00:00 2001 From: Gamadril Date: Wed, 18 Jun 2014 13:49:27 +0200 Subject: [PATCH 59/78] Update LedDeviceTpm2.cpp Former-commit-id: 8d51eb00044460002e29687468786e3056afd0bb --- libsrc/leddevice/LedDeviceTpm2.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libsrc/leddevice/LedDeviceTpm2.cpp b/libsrc/leddevice/LedDeviceTpm2.cpp index 49bb6121..a0fcf869 100644 --- a/libsrc/leddevice/LedDeviceTpm2.cpp +++ b/libsrc/leddevice/LedDeviceTpm2.cpp @@ -22,11 +22,11 @@ int LedDeviceTpm2::write(const std::vector &ledValues) { if (_ledBuffer.size() == 0) { - _ledBuffer.resize(5 + 3*ledValues.size()); + _ledBuffer.resize(5 + 3*ledValues.size()); _ledBuffer[0] = 0xC9; // block-start byte _ledBuffer[1] = 0xDA; // DATA frame - _ledBuffer[2] = ((3 * ledValues.size()) >> 8) & 0xFF; // LED count high byte - _ledBuffer[3] = (3 * ledValues.size()) & 0xFF; // LED count low byte + _ledBuffer[2] = ((3 * ledValues.size()) >> 8) & 0xFF; // frame size high byte + _ledBuffer[3] = (3 * ledValues.size()) & 0xFF; // frame size low byte _ledBuffer.back() = 0x36; // block-end byte } From ba4d2b45d5ae3a6b90c0c5ef2e5216328293b1db Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Thu, 19 Jun 2014 22:16:03 +0200 Subject: [PATCH 60/78] Updated release package to include new device (tpm2) Former-commit-id: 24decda30a774b4d3702ec8fa375bc1ac4b849d7 --- 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 35028a43..7729eb84 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -d9eb8f0ef98c76bc54a43cc572183f7c54fc4dc9 \ No newline at end of file +5e8d795d2aa82337e42924c1a5292203d7d4271a \ No newline at end of file From c4c7ed03319220e50ad78e37502d14dcc896cdd3 Mon Sep 17 00:00:00 2001 From: bimsarck Date: Fri, 4 Jul 2014 12:11:37 +0200 Subject: [PATCH 61/78] Improved philip hue device: add lamp types autodetection add full xy-Colorspace implementations reduce http requests to the hue bridge. This prevents DDOS -> 503 add color black -> lamps off save state is temporary disabled Former-commit-id: 5a0328fe80a06a9f670c5190190e239857cbd15c --- libsrc/leddevice/LedDevicePhilipsHue.cpp | 231 +++++++++++++++++++---- libsrc/leddevice/LedDevicePhilipsHue.h | 33 +++- 2 files changed, 220 insertions(+), 44 deletions(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index f8a9698a..ed549b35 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -1,3 +1,4 @@ +#include // Local-Hyperion includes #include "LedDevicePhilipsHue.h" @@ -10,43 +11,91 @@ #include #include -LedDevicePhilipsHue::LedDevicePhilipsHue(const std::string& output) : - host(output.c_str()), username("newdeveloper") { +LedDevicePhilipsHue::LedDevicePhilipsHue(const std::string &output) : + host(output.c_str()), username("newdeveloper") { http = new QHttp(host); - timer.setInterval(3000); +/* timer.setInterval(3000); timer.setSingleShot(true); - connect(&timer, SIGNAL(timeout()), this, SLOT(restoreStates())); + connect(&timer, SIGNAL(timeout()), this, SLOT(restoreStates()));*/ } LedDevicePhilipsHue::~LedDevicePhilipsHue() { delete http; } -int LedDevicePhilipsHue::write(const std::vector & ledValues) { +int LedDevicePhilipsHue::write(const std::vector &ledValues) { // Save light states if not done before. - if (!statesSaved()) { + if (!statesSaved()) saveStates(ledValues.size()); - switchOn(ledValues.size()); - } // Iterate through colors and set light states. - unsigned int lightId = 1; - for (const ColorRgb& color : ledValues) { - float x, y, b; - // Scale colors from [0, 255] to [0, 1] and convert to xy space. - rgbToXYBrightness(color.red / 255.0f, color.green / 255.0f, color.blue / 255.0f, x, y, b); - // Send adjust color command in JSON format. - put(getStateRoute(lightId), QString("{\"xy\": [%1, %2]}").arg(x).arg(y)); - // Send brightness color command in JSON format. - put(getStateRoute(lightId), QString("{\"bri\": %1}").arg(qRound(b * 255.0f))); - // Next light id. + unsigned int lightId = 0; + for (const ColorRgb &color : ledValues) { lightId++; + // Send only request to the brigde if color changed (prevents DDOS --> 503) + if (!oldLedValues.empty()) + if(!hasColorChanged(lightId, &color)) + continue; + + float r = color.red / 255.0f; + float g = color.green / 255.0f; + float b = color.blue / 255.0f; + + //set color gamut triangle + if(std::find(hueBulbs.begin(), hueBulbs.end(), modelIds[(lightId - 1)]) != hueBulbs.end()) { + Red = {0.675f, 0.322f}; + Green = {0.4091f, 0.518f}; + Blue = {0.167f, 0.04f}; + } else if (std::find(livingColors.begin(), + livingColors.end(), modelIds[(lightId - 1)]) != livingColors.end()) { + Red = {0.703f, 0.296f}; + Green = {0.214f, 0.709f}; + Blue = {0.139f, 0.081f}; + } else { + Red = {1.0f, 0.0f}; + Green = {0.0f, 1.0f}; + Blue = {0.0f, 0.0f}; + } + // if color equal black, switch off lamp ... + if (r == 0.0f && g == 0.0f && b == 0.0f) { + switchLampOff(lightId); + continue; + } + // ... and if lamp off, switch on + if (!checkOnStatus(states[(lightId - 1)])) + switchLampOn(lightId); + + float bri; + CGPoint p = CGPointMake(0, 0); + // Scale colors from [0, 255] to [0, 1] and convert to xy space. + rgbToXYBrightness(r, g, b, &p, bri); + // Send adjust color and brightness command in JSON format. + put(getStateRoute(lightId), + QString("{\"xy\": [%1, %2], \"bri\": %3}").arg(p.x).arg(p.y).arg(qRound(b * 255.0f))); } - timer.start(); + oldLedValues = ledValues; + //timer.start(); return 0; } +bool LedDevicePhilipsHue::hasColorChanged(unsigned int lightId, const ColorRgb *color) { + bool matchFound = true; + const ColorRgb &tmpOldColor = oldLedValues[(lightId - 1)]; + if ((*color).red == tmpOldColor.red) + matchFound = false; + if (!matchFound && (*color).green == tmpOldColor.green) + matchFound = false; + else + matchFound = true; + if (!matchFound && (*color).blue == tmpOldColor.blue) + matchFound = false; + else + matchFound = true; + + return matchFound; +} + int LedDevicePhilipsHue::switchOff() { - timer.stop(); + //timer.stop(); // If light states have been saved before, ... if (statesSaved()) { // ... restore them. @@ -55,6 +104,10 @@ int LedDevicePhilipsHue::switchOff() { return 0; } +bool LedDevicePhilipsHue::checkOnStatus(QString status) { + return status.contains("\"on\":true"); +} + void LedDevicePhilipsHue::put(QString route, QString content) { QString url = QString("/api/%1/%2").arg(username).arg(route); QHttpRequestHeader header("PUT", url); @@ -69,6 +122,7 @@ void LedDevicePhilipsHue::put(QString route, QString content) { http->request(header, content.toAscii()); // Go into the loop until the request is finished. loop.exec(); + //std::cout << http->readAll().data() << std::endl; } QByteArray LedDevicePhilipsHue::get(QString route) { @@ -96,6 +150,7 @@ QString LedDevicePhilipsHue::getRoute(unsigned int lightId) { void LedDevicePhilipsHue::saveStates(unsigned int nLights) { // Clear saved light states. states.clear(); + modelIds.clear(); // Use json parser to parse reponse. Json::Reader reader; Json::FastWriter writer; @@ -117,14 +172,19 @@ void LedDevicePhilipsHue::saveStates(unsigned int nLights) { state["bri"] = json["state"]["bri"]; } // Save state object. + modelIds.push_back(QString(writer.write(json["modelid"]).c_str()).trimmed().replace("\"", "")); states.push_back(QString(writer.write(state).c_str()).trimmed()); } } -void LedDevicePhilipsHue::switchOn(unsigned int nLights) { - for (unsigned int i = 0; i < nLights; i++) { - put(getStateRoute(i + 1), "{\"on\": true}"); - } +void LedDevicePhilipsHue::switchLampOn(unsigned int lightId) { + put(getStateRoute(lightId), "{\"on\": true}"); + states[(lightId - 1)].replace("\"on\":false", "\"on\":true"); +} + +void LedDevicePhilipsHue::switchLampOff(unsigned int lightId) { + put(getStateRoute(lightId), "{\"on\": false}"); + states[(lightId - 1)].replace("\"on\":true", "\"on\":false"); } void LedDevicePhilipsHue::restoreStates() { @@ -135,30 +195,119 @@ void LedDevicePhilipsHue::restoreStates() { } // Clear saved light states. states.clear(); + modelIds.clear(); + oldLedValues.clear(); } bool LedDevicePhilipsHue::statesSaved() { return !states.empty(); } -void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, float& x, float& y, float& brightness) { - // Apply gamma correction. - red = (red > 0.04045f) ? qPow((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f); - green = (green > 0.04045f) ? qPow((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f); - blue = (blue > 0.04045f) ? qPow((blue + 0.055f) / (1.0f + 0.055f), 2.4f) : (blue / 12.92f); - // Convert to XYZ space. - float X = red * 0.649926f + green * 0.103455f + blue * 0.197109f; - float Y = red * 0.234327f + green * 0.743075f + blue * 0.022598f; - float Z = red * 0.0000000f + green * 0.053077f + blue * 1.035763f; - // Convert to x,y space. - x = X / (X + Y + Z); - y = Y / (X + Y + Z); - if (isnan(x)) { - x = 0.0f; - } - if (isnan(y)) { - y = 0.0f; +CGPoint LedDevicePhilipsHue::CGPointMake(float x, float y) { + CGPoint p; + p.x = x; + p.y = y; + + return p; +} + +float LedDevicePhilipsHue::CrossProduct(CGPoint p1, CGPoint p2) { + return (p1.x * p2.y - p1.y * p2.x); +} + +bool LedDevicePhilipsHue::CheckPointInLampsReach(CGPoint p) { + CGPoint v1 = CGPointMake(Green.x - Red.x, Green.y - Red.y); + CGPoint v2 = CGPointMake(Blue.x - Red.x, Blue.y - Red.y); + + CGPoint q = CGPointMake(p.x - Red.x, p.y - Red.y); + + float s = CrossProduct(q, v2) / CrossProduct(v1, v2); + float t = CrossProduct(v1, q) / CrossProduct(v1, v2); + if ((s >= 0.0f) && (t >= 0.0f) && (s + t <= 1.0f)) + return true; + else + return false; +} + +CGPoint LedDevicePhilipsHue::GetClosestPointToPoint(CGPoint A, CGPoint B, CGPoint P) { + CGPoint AP = CGPointMake(P.x - A.x, P.y - A.y); + CGPoint AB = CGPointMake(B.x - A.x, B.y - A.y); + float ab2 = AB.x * AB.x + AB.y * AB.y; + float ap_ab = AP.x * AB.x + AP.y * AB.y; + + float t = ap_ab / ab2; + + if (t < 0.0f) + t = 0.0f; + else if (t > 1.0f) + t = 1.0f; + + return CGPointMake(A.x + AB.x * t, A.y + AB.y * t); +} + +float LedDevicePhilipsHue::GetDistanceBetweenTwoPoints(CGPoint one, CGPoint two) { + float dx = one.x - two.x; // horizontal difference + float dy = one.y - two.y; // vertical difference + float dist = sqrt(dx * dx + dy * dy); + + return dist; +} + +void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, CGPoint *xyPoint, float &brightness) { + //Apply gamma correction. + float r = (red > 0.04045f) ? powf((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f); + float g = (green > 0.04045f) ? powf((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f); + float b = (blue > 0.04045f) ? powf((blue + 0.055f) / (1.0f + 0.055f), 2.4f) : (blue / 12.92f); + //Convert to XYZ space. + float X = r * 0.649926f + g * 0.103455f + b * 0.197109f; + float Y = r * 0.234327f + g * 0.743075f + b * 0.022598f; + float Z = r * 0.0000000f + g * 0.053077f + b * 1.035763f; + //Convert to x,y space. + float cx = X / (X + Y + Z + 0.0000001f); + float cy = Y / (X + Y + Z + 0.0000001f); + + if (isnan(cx)) + cx = 0.0f; + if (isnan(cy)) + cy = 0.0f; + + (*xyPoint).x = cx; + (*xyPoint).y = cy; + + //Check if the given XY value is within the colourreach of our lamps. + bool inReachOfLamps = CheckPointInLampsReach(*xyPoint); + + if (!inReachOfLamps) { + //It seems the colour is out of reach + //let's find the closes colour we can produce with our lamp and send this XY value out. + + //Find the closest point on each line in the triangle. + CGPoint pAB = GetClosestPointToPoint(Red, Green, *xyPoint); + CGPoint pAC = GetClosestPointToPoint(Blue, Red, *xyPoint); + CGPoint pBC = GetClosestPointToPoint(Green, Blue, *xyPoint); + + //Get the distances per point and see which point is closer to our Point. + float dAB = GetDistanceBetweenTwoPoints(*xyPoint, pAB); + float dAC = GetDistanceBetweenTwoPoints(*xyPoint, pAC); + float dBC = GetDistanceBetweenTwoPoints(*xyPoint, pBC); + + float lowest = dAB; + CGPoint closestPoint = pAB; + + if (dAC < lowest) { + lowest = dAC; + closestPoint = pAC; + } + if (dBC < lowest) { + lowest = dBC; + closestPoint = pBC; + } + + //Change the xy value to a value which is within the reach of the lamp. + (*xyPoint).x = closestPoint.x; + (*xyPoint).y = closestPoint.y; } + // Brightness is simply Y in the XYZ space. brightness = Y; } diff --git a/libsrc/leddevice/LedDevicePhilipsHue.h b/libsrc/leddevice/LedDevicePhilipsHue.h index 84e7bd35..9760b7ad 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/LedDevicePhilipsHue.h @@ -22,6 +22,11 @@ * * @author ntim (github) */ +struct CGPoint; +struct CGPoint { + float x; + float y; +}; class LedDevicePhilipsHue: public QObject, public LedDevice { Q_OBJECT public: @@ -44,7 +49,7 @@ public: /// /// @return Zero on success else negative /// - virtual int write(const std::vector & ledValues); + virtual int write(const std::vector &ledValues); /// Restores the original state of the leds. virtual int switchOff(); @@ -54,6 +59,19 @@ private slots: void restoreStates(); private: + // ModelIds + const std::vector hueBulbs = {"LCT001", "LCT002", "LCT003"}; + const std::vector livingColors = {"LLC001", "LLC005", "LLC006", "LLC007", + "LLC011", "LLC012", "LLC013", "LST001"}; + /// LivingColors color gamut triangle + CGPoint Red , Green, Blue; + + CGPoint CGPointMake(float x, float y); + float CrossProduct(CGPoint p1, CGPoint p2); + bool CheckPointInLampsReach(CGPoint p); + CGPoint GetClosestPointToPoint(CGPoint A, CGPoint B, CGPoint P); + float GetDistanceBetweenTwoPoints(CGPoint one, CGPoint two); + /// Array to save the light states. std::vector states; /// Ip address of the bridge @@ -65,6 +83,13 @@ private: /// Use timer to reset lights when we got into "GRABBINGMODE_OFF". QTimer timer; + std::vector oldLedValues; + std::vector modelIds; + + bool hasColorChanged(unsigned int lightId, const ColorRgb *color); + + bool checkOnStatus(QString status); + /// /// Sends a HTTP GET request (blocking). /// @@ -109,7 +134,9 @@ private: /// /// @param nLights the number of lights /// - void switchOn(unsigned int nLights); + void switchLampOn(unsigned int lightId); + + void switchLampOff(unsigned int lightId); /// /// @return true if light states have been saved. @@ -132,6 +159,6 @@ private: /// /// @param brightness converted brightness component /// - void rgbToXYBrightness(float red, float green, float blue, float& x, float& y, float& brightness); + void rgbToXYBrightness(float red, float green, float blue, CGPoint *xyPoint, float &brightness); }; From e6d39b047cc6bff6c3b526f3ae48a935f1c58f6a Mon Sep 17 00:00:00 2001 From: bimsarck Date: Sat, 12 Jul 2014 14:56:39 +0200 Subject: [PATCH 62/78] Remove unnecessary code - reenable timer Former-commit-id: b1a175a1f1e5c3a7da753240030815252b1b22bb --- libsrc/leddevice/LedDevicePhilipsHue.cpp | 67 ++++++++++-------------- libsrc/leddevice/LedDevicePhilipsHue.h | 17 +++--- 2 files changed, 37 insertions(+), 47 deletions(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index ed549b35..1fb89e6e 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -14,16 +14,16 @@ LedDevicePhilipsHue::LedDevicePhilipsHue(const std::string &output) : host(output.c_str()), username("newdeveloper") { http = new QHttp(host); -/* timer.setInterval(3000); + timer.setInterval(3000); timer.setSingleShot(true); - connect(&timer, SIGNAL(timeout()), this, SLOT(restoreStates()));*/ + connect(&timer, SIGNAL(timeout()), this, SLOT(restoreStates())); } LedDevicePhilipsHue::~LedDevicePhilipsHue() { delete http; } -int LedDevicePhilipsHue::write(const std::vector &ledValues) { +int LedDevicePhilipsHue::write(const std::vector & ledValues) { // Save light states if not done before. if (!statesSaved()) saveStates(ledValues.size()); @@ -65,15 +65,15 @@ int LedDevicePhilipsHue::write(const std::vector &ledValues) { switchLampOn(lightId); float bri; - CGPoint p = CGPointMake(0, 0); + CGPoint p = {0.0f, 0.0f}; // Scale colors from [0, 255] to [0, 1] and convert to xy space. - rgbToXYBrightness(r, g, b, &p, bri); + rgbToXYBrightness(r, g, b, p, bri); // Send adjust color and brightness command in JSON format. put(getStateRoute(lightId), QString("{\"xy\": [%1, %2], \"bri\": %3}").arg(p.x).arg(p.y).arg(qRound(b * 255.0f))); } oldLedValues = ledValues; - //timer.start(); + timer.start(); return 0; } @@ -95,7 +95,7 @@ bool LedDevicePhilipsHue::hasColorChanged(unsigned int lightId, const ColorRgb * } int LedDevicePhilipsHue::switchOff() { - //timer.stop(); + timer.stop(); // If light states have been saved before, ... if (statesSaved()) { // ... restore them. @@ -122,7 +122,6 @@ void LedDevicePhilipsHue::put(QString route, QString content) { http->request(header, content.toAscii()); // Go into the loop until the request is finished. loop.exec(); - //std::cout << http->readAll().data() << std::endl; } QByteArray LedDevicePhilipsHue::get(QString route) { @@ -203,23 +202,15 @@ bool LedDevicePhilipsHue::statesSaved() { return !states.empty(); } -CGPoint LedDevicePhilipsHue::CGPointMake(float x, float y) { - CGPoint p; - p.x = x; - p.y = y; - - return p; -} - -float LedDevicePhilipsHue::CrossProduct(CGPoint p1, CGPoint p2) { +float LedDevicePhilipsHue::CrossProduct(CGPoint& p1, CGPoint& p2) { return (p1.x * p2.y - p1.y * p2.x); } -bool LedDevicePhilipsHue::CheckPointInLampsReach(CGPoint p) { - CGPoint v1 = CGPointMake(Green.x - Red.x, Green.y - Red.y); - CGPoint v2 = CGPointMake(Blue.x - Red.x, Blue.y - Red.y); +bool LedDevicePhilipsHue::CheckPointInLampsReach(CGPoint& p) { + CGPoint v1 = {Green.x - Red.x, Green.y - Red.y}; + CGPoint v2 = {Blue.x - Red.x, Blue.y - Red.y}; - CGPoint q = CGPointMake(p.x - Red.x, p.y - Red.y); + CGPoint q = {p.x - Red.x, p.y - Red.y}; float s = CrossProduct(q, v2) / CrossProduct(v1, v2); float t = CrossProduct(v1, q) / CrossProduct(v1, v2); @@ -229,9 +220,9 @@ bool LedDevicePhilipsHue::CheckPointInLampsReach(CGPoint p) { return false; } -CGPoint LedDevicePhilipsHue::GetClosestPointToPoint(CGPoint A, CGPoint B, CGPoint P) { - CGPoint AP = CGPointMake(P.x - A.x, P.y - A.y); - CGPoint AB = CGPointMake(B.x - A.x, B.y - A.y); +CGPoint LedDevicePhilipsHue::GetClosestPointToPoint(CGPoint& A, CGPoint& B, CGPoint& P) { + CGPoint AP = {P.x - A.x, P.y - A.y}; + CGPoint AB = {B.x - A.x, B.y - A.y}; float ab2 = AB.x * AB.x + AB.y * AB.y; float ap_ab = AP.x * AB.x + AP.y * AB.y; @@ -242,10 +233,10 @@ CGPoint LedDevicePhilipsHue::GetClosestPointToPoint(CGPoint A, CGPoint B, CGPoin else if (t > 1.0f) t = 1.0f; - return CGPointMake(A.x + AB.x * t, A.y + AB.y * t); + return {A.x + AB.x * t, A.y + AB.y * t}; } -float LedDevicePhilipsHue::GetDistanceBetweenTwoPoints(CGPoint one, CGPoint two) { +float LedDevicePhilipsHue::GetDistanceBetweenTwoPoints(CGPoint& one, CGPoint& two) { float dx = one.x - two.x; // horizontal difference float dy = one.y - two.y; // vertical difference float dist = sqrt(dx * dx + dy * dy); @@ -253,7 +244,7 @@ float LedDevicePhilipsHue::GetDistanceBetweenTwoPoints(CGPoint one, CGPoint two) return dist; } -void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, CGPoint *xyPoint, float &brightness) { +void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, CGPoint& xyPoint, float& brightness) { //Apply gamma correction. float r = (red > 0.04045f) ? powf((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f); float g = (green > 0.04045f) ? powf((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f); @@ -271,25 +262,25 @@ void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, if (isnan(cy)) cy = 0.0f; - (*xyPoint).x = cx; - (*xyPoint).y = cy; + xyPoint.x = cx; + xyPoint.y = cy; //Check if the given XY value is within the colourreach of our lamps. - bool inReachOfLamps = CheckPointInLampsReach(*xyPoint); + bool inReachOfLamps = CheckPointInLampsReach(xyPoint); if (!inReachOfLamps) { //It seems the colour is out of reach //let's find the closes colour we can produce with our lamp and send this XY value out. //Find the closest point on each line in the triangle. - CGPoint pAB = GetClosestPointToPoint(Red, Green, *xyPoint); - CGPoint pAC = GetClosestPointToPoint(Blue, Red, *xyPoint); - CGPoint pBC = GetClosestPointToPoint(Green, Blue, *xyPoint); + CGPoint pAB = GetClosestPointToPoint(Red, Green, xyPoint); + CGPoint pAC = GetClosestPointToPoint(Blue, Red, xyPoint); + CGPoint pBC = GetClosestPointToPoint(Green, Blue, xyPoint); //Get the distances per point and see which point is closer to our Point. - float dAB = GetDistanceBetweenTwoPoints(*xyPoint, pAB); - float dAC = GetDistanceBetweenTwoPoints(*xyPoint, pAC); - float dBC = GetDistanceBetweenTwoPoints(*xyPoint, pBC); + float dAB = GetDistanceBetweenTwoPoints(xyPoint, pAB); + float dAC = GetDistanceBetweenTwoPoints(xyPoint, pAC); + float dBC = GetDistanceBetweenTwoPoints(xyPoint, pBC); float lowest = dAB; CGPoint closestPoint = pAB; @@ -304,8 +295,8 @@ void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, } //Change the xy value to a value which is within the reach of the lamp. - (*xyPoint).x = closestPoint.x; - (*xyPoint).y = closestPoint.y; + xyPoint.x = closestPoint.x; + xyPoint.y = closestPoint.y; } // Brightness is simply Y in the XYZ space. diff --git a/libsrc/leddevice/LedDevicePhilipsHue.h b/libsrc/leddevice/LedDevicePhilipsHue.h index 9760b7ad..11ec4abd 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/LedDevicePhilipsHue.h @@ -49,7 +49,7 @@ public: /// /// @return Zero on success else negative /// - virtual int write(const std::vector &ledValues); + virtual int write(const std::vector & ledValues); /// Restores the original state of the leds. virtual int switchOff(); @@ -59,18 +59,17 @@ private slots: void restoreStates(); private: - // ModelIds + /// Available modelIds const std::vector hueBulbs = {"LCT001", "LCT002", "LCT003"}; const std::vector livingColors = {"LLC001", "LLC005", "LLC006", "LLC007", "LLC011", "LLC012", "LLC013", "LST001"}; - /// LivingColors color gamut triangle + /// Color gamut triangle CGPoint Red , Green, Blue; - CGPoint CGPointMake(float x, float y); - float CrossProduct(CGPoint p1, CGPoint p2); - bool CheckPointInLampsReach(CGPoint p); - CGPoint GetClosestPointToPoint(CGPoint A, CGPoint B, CGPoint P); - float GetDistanceBetweenTwoPoints(CGPoint one, CGPoint two); + float CrossProduct(CGPoint& p1, CGPoint& p2); + bool CheckPointInLampsReach(CGPoint& p); + CGPoint GetClosestPointToPoint(CGPoint& A, CGPoint& B, CGPoint& P); + float GetDistanceBetweenTwoPoints(CGPoint& one, CGPoint& two); /// Array to save the light states. std::vector states; @@ -159,6 +158,6 @@ private: /// /// @param brightness converted brightness component /// - void rgbToXYBrightness(float red, float green, float blue, CGPoint *xyPoint, float &brightness); + void rgbToXYBrightness(float red, float green, float blue, CGPoint& xyPoint, float& brightness); }; From c1998da2ec91515c328d024649751c0e97083dee Mon Sep 17 00:00:00 2001 From: bimsarck Date: Sun, 13 Jul 2014 13:48:49 +0200 Subject: [PATCH 63/78] Fixes saveState feature Former-commit-id: 8158c77521727bf74875a5998c803212aece0645 --- libsrc/leddevice/LedDevicePhilipsHue.cpp | 29 ++++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index 1fb89e6e..6b837eb0 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -11,7 +11,7 @@ #include #include -LedDevicePhilipsHue::LedDevicePhilipsHue(const std::string &output) : +LedDevicePhilipsHue::LedDevicePhilipsHue(const std::string& output) : host(output.c_str()), username("newdeveloper") { http = new QHttp(host); timer.setInterval(3000); @@ -30,23 +30,24 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { // Iterate through colors and set light states. unsigned int lightId = 0; for (const ColorRgb &color : ledValues) { - lightId++; // Send only request to the brigde if color changed (prevents DDOS --> 503) if (!oldLedValues.empty()) - if(!hasColorChanged(lightId, &color)) + if(!hasColorChanged(lightId, &color)) { + lightId++; continue; + } float r = color.red / 255.0f; float g = color.green / 255.0f; float b = color.blue / 255.0f; //set color gamut triangle - if(std::find(hueBulbs.begin(), hueBulbs.end(), modelIds[(lightId - 1)]) != hueBulbs.end()) { + if(std::find(hueBulbs.begin(), hueBulbs.end(), modelIds[lightId]) != hueBulbs.end()) { Red = {0.675f, 0.322f}; Green = {0.4091f, 0.518f}; Blue = {0.167f, 0.04f}; } else if (std::find(livingColors.begin(), - livingColors.end(), modelIds[(lightId - 1)]) != livingColors.end()) { + livingColors.end(), modelIds[lightId]) != livingColors.end()) { Red = {0.703f, 0.296f}; Green = {0.214f, 0.709f}; Blue = {0.139f, 0.081f}; @@ -58,10 +59,11 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { // if color equal black, switch off lamp ... if (r == 0.0f && g == 0.0f && b == 0.0f) { switchLampOff(lightId); + lightId++; continue; } // ... and if lamp off, switch on - if (!checkOnStatus(states[(lightId - 1)])) + if (!checkOnStatus(states[lightId])) switchLampOn(lightId); float bri; @@ -71,6 +73,7 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { // Send adjust color and brightness command in JSON format. put(getStateRoute(lightId), QString("{\"xy\": [%1, %2], \"bri\": %3}").arg(p.x).arg(p.y).arg(qRound(b * 255.0f))); + lightId++; } oldLedValues = ledValues; timer.start(); @@ -79,7 +82,7 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { bool LedDevicePhilipsHue::hasColorChanged(unsigned int lightId, const ColorRgb *color) { bool matchFound = true; - const ColorRgb &tmpOldColor = oldLedValues[(lightId - 1)]; + const ColorRgb &tmpOldColor = oldLedValues[lightId]; if ((*color).red == tmpOldColor.red) matchFound = false; if (!matchFound && (*color).green == tmpOldColor.green) @@ -139,7 +142,7 @@ QByteArray LedDevicePhilipsHue::get(QString route) { } QString LedDevicePhilipsHue::getStateRoute(unsigned int lightId) { - return QString("lights/%1/state").arg(lightId); + return QString("lights/%1/state").arg(lightId + 1); } QString LedDevicePhilipsHue::getRoute(unsigned int lightId) { @@ -178,18 +181,20 @@ void LedDevicePhilipsHue::saveStates(unsigned int nLights) { void LedDevicePhilipsHue::switchLampOn(unsigned int lightId) { put(getStateRoute(lightId), "{\"on\": true}"); - states[(lightId - 1)].replace("\"on\":false", "\"on\":true"); + states[lightId].replace("\"on\":false", "\"on\":true"); } void LedDevicePhilipsHue::switchLampOff(unsigned int lightId) { put(getStateRoute(lightId), "{\"on\": false}"); - states[(lightId - 1)].replace("\"on\":true", "\"on\":false"); + states[lightId].replace("\"on\":true", "\"on\":false"); } void LedDevicePhilipsHue::restoreStates() { - unsigned int lightId = 1; + unsigned int lightId = 0; for (QString state : states) { - put(getStateRoute(lightId), state); + if (!checkOnStatus(states[lightId])) + switchLampOn(lightId); + put(getStateRoute(lightId), states[lightId]); lightId++; } // Clear saved light states. From f5a8174783bab0029751accb5c40250f211195b2 Mon Sep 17 00:00:00 2001 From: Tim Niggemann Date: Tue, 15 Jul 2014 08:51:07 +0200 Subject: [PATCH 64/78] Implemented color triangle calculations. Former-commit-id: dbde6635077a82ace5f4ed1fdf89458a28e7bf05 --- libsrc/leddevice/LedDevicePhilipsHue.cpp | 145 +++++++++++++++++++---- libsrc/leddevice/LedDevicePhilipsHue.h | 27 ++++- 2 files changed, 143 insertions(+), 29 deletions(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index f8a9698a..b2525033 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -10,6 +10,8 @@ #include #include +#include + LedDevicePhilipsHue::LedDevicePhilipsHue(const std::string& output) : host(output.c_str()), username("newdeveloper") { http = new QHttp(host); @@ -24,18 +26,21 @@ LedDevicePhilipsHue::~LedDevicePhilipsHue() { int LedDevicePhilipsHue::write(const std::vector & ledValues) { // Save light states if not done before. - if (!statesSaved()) { - saveStates(ledValues.size()); - switchOn(ledValues.size()); + if (!areStatesSaved()) { + saveStates((unsigned int) ledValues.size()); + switchOn((unsigned int) ledValues.size()); } // Iterate through colors and set light states. unsigned int lightId = 1; for (const ColorRgb& color : ledValues) { - float x, y, b; + // Find triangle. + CGTriangle triangle = triangles.at(lightId - 1); // Scale colors from [0, 255] to [0, 1] and convert to xy space. - rgbToXYBrightness(color.red / 255.0f, color.green / 255.0f, color.blue / 255.0f, x, y, b); + CGPoint xy; + float b; + rgbToXYBrightness(color.red / 255.0f, color.green / 255.0f, color.blue / 255.0f, triangle, xy, b); // Send adjust color command in JSON format. - put(getStateRoute(lightId), QString("{\"xy\": [%1, %2]}").arg(x).arg(y)); + put(getStateRoute(lightId), QString("{\"xy\": [%1, %2]}").arg(xy.x).arg(xy.y)); // Send brightness color command in JSON format. put(getStateRoute(lightId), QString("{\"bri\": %1}").arg(qRound(b * 255.0f))); // Next light id. @@ -48,7 +53,7 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { int LedDevicePhilipsHue::switchOff() { timer.stop(); // If light states have been saved before, ... - if (statesSaved()) { + if (areStatesSaved()) { // ... restore them. restoreStates(); } @@ -93,6 +98,27 @@ QString LedDevicePhilipsHue::getRoute(unsigned int lightId) { return QString("lights/%1").arg(lightId); } +CGTriangle LedDevicePhilipsHue::getTriangle(QString modelId) { + const std::set HUE_BULBS_MODEL_IDS = { "LCT001", "LCT002", "LCT003" }; + const std::set LIVING_COLORS_MODEL_IDS = { "LLC001", "LLC005", "LLC006", "LLC007", "LLC011", "LLC012", + "LLC013", "LST001" }; + CGTriangle triangle; + if (HUE_BULBS_MODEL_IDS.find(modelId) != HUE_BULBS_MODEL_IDS.end()) { + triangle.red = {0.675f, 0.322f}; + triangle.green = {0.4091f, 0.518f}; + triangle.blue = {0.167f, 0.04f}; + } else if (LIVING_COLORS_MODEL_IDS.find(modelId) != LIVING_COLORS_MODEL_IDS.end()) { + triangle.red = {0.703f, 0.296f}; + triangle.green = {0.214f, 0.709f}; + triangle.blue = {0.139f, 0.081f}; + } else { + triangle.red = {1.0f, 0.0f}; + triangle.green = {0.0f, 1.0f}; + triangle.blue = {0.0f, 0.0f}; + } + return triangle; +} + void LedDevicePhilipsHue::saveStates(unsigned int nLights) { // Clear saved light states. states.clear(); @@ -116,8 +142,12 @@ void LedDevicePhilipsHue::saveStates(unsigned int nLights) { state["xy"] = json["state"]["xy"]; state["bri"] = json["state"]["bri"]; } + // Save id. + ids.push_back(QString(writer.write(json["modelid"]).c_str()).trimmed().replace("\"", "")); // Save state object. states.push_back(QString(writer.write(state).c_str()).trimmed()); + // Determine triangle. + triangles.push_back(getTriangle(ids.back())); } } @@ -137,27 +167,94 @@ void LedDevicePhilipsHue::restoreStates() { states.clear(); } -bool LedDevicePhilipsHue::statesSaved() { +bool LedDevicePhilipsHue::areStatesSaved() { return !states.empty(); } -void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, float& x, float& y, float& brightness) { - // Apply gamma correction. - red = (red > 0.04045f) ? qPow((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f); - green = (green > 0.04045f) ? qPow((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f); - blue = (blue > 0.04045f) ? qPow((blue + 0.055f) / (1.0f + 0.055f), 2.4f) : (blue / 12.92f); - // Convert to XYZ space. - float X = red * 0.649926f + green * 0.103455f + blue * 0.197109f; - float Y = red * 0.234327f + green * 0.743075f + blue * 0.022598f; - float Z = red * 0.0000000f + green * 0.053077f + blue * 1.035763f; - // Convert to x,y space. - x = X / (X + Y + Z); - y = Y / (X + Y + Z); - if (isnan(x)) { - x = 0.0f; +float LedDevicePhilipsHue::crossProduct(CGPoint p1, CGPoint p2) { + return p1.x * p2.y - p1.y * p2.x; +} + +bool LedDevicePhilipsHue::isPointInLampsReach(CGTriangle triangle, CGPoint p) { + CGPoint v1 = { triangle.green.x - triangle.red.x, triangle.green.y - triangle.red.y }; + CGPoint v2 = { triangle.blue.x - triangle.red.x, triangle.blue.y - triangle.red.y }; + CGPoint q = { p.x - triangle.red.x, p.y - triangle.red.y }; + float s = crossProduct(q, v2) / crossProduct(v1, v2); + float t = crossProduct(v1, q) / crossProduct(v1, v2); + if ((s >= 0.0f) && (t >= 0.0f) && (s + t <= 1.0f)) { + return true; + } else { + return false; } - if (isnan(y)) { - y = 0.0f; +} + +CGPoint LedDevicePhilipsHue::getClosestPointToPoint(CGPoint A, CGPoint B, CGPoint P) { + CGPoint AP = { P.x - A.x, P.y - A.y }; + CGPoint AB = { B.x - A.x, B.y - A.y }; + float ab2 = AB.x * AB.x + AB.y * AB.y; + float ap_ab = AP.x * AB.x + AP.y * AB.y; + float t = ap_ab / ab2; + if (t < 0.0f) { + t = 0.0f; + } else if (t > 1.0f) { + t = 1.0f; + } + return {A.x + AB.x * t, A.y + AB.y * t}; +} + +float LedDevicePhilipsHue::getDistanceBetweenTwoPoints(CGPoint one, CGPoint two) { + // Horizontal difference. + float dx = one.x - two.x; + // Vertical difference. + float dy = one.y - two.y; + float dist = sqrt(dx * dx + dy * dy); + return dist; +} + +void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, CGTriangle triangle, CGPoint& xyPoint, + float& brightness) { + // Apply gamma correction. + float r = (red > 0.04045f) ? powf((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f); + float g = (green > 0.04045f) ? powf((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f); + float b = (blue > 0.04045f) ? powf((blue + 0.055f) / (1.0f + 0.055f), 2.4f) : (blue / 12.92f); + // Convert to XYZ space. + float X = r * 0.649926f + g * 0.103455f + b * 0.197109f; + float Y = r * 0.234327f + g * 0.743075f + b * 0.022598f; + float Z = r * 0.0000000f + g * 0.053077f + b * 1.035763f; + // Convert to x,y space. + float cx = X / (X + Y + Z + 0.0000001f); + float cy = Y / (X + Y + Z + 0.0000001f); + if (isnan(cx)) { + cx = 0.0f; + } + if (isnan(cy)) { + cy = 0.0f; + } + xyPoint.x = cx; + xyPoint.y = cy; + // Check if the given XY value is within the colourreach of our lamps. + if (!isPointInLampsReach(triangle, xyPoint)) { + // It seems the colour is out of reach let's find the closes colour we can produce with our lamp and send this XY value out. + CGPoint pAB = getClosestPointToPoint(triangle.red, triangle.green, xyPoint); + CGPoint pAC = getClosestPointToPoint(triangle.blue, triangle.red, xyPoint); + CGPoint pBC = getClosestPointToPoint(triangle.green, triangle.blue, xyPoint); + // Get the distances per point and see which point is closer to our Point. + float dAB = getDistanceBetweenTwoPoints(xyPoint, pAB); + float dAC = getDistanceBetweenTwoPoints(xyPoint, pAC); + float dBC = getDistanceBetweenTwoPoints(xyPoint, pBC); + float lowest = dAB; + CGPoint closestPoint = pAB; + if (dAC < lowest) { + lowest = dAC; + closestPoint = pAC; + } + if (dBC < lowest) { + lowest = dBC; + closestPoint = pBC; + } + // Change the xy value to a value which is within the reach of the lamp. + xyPoint.x = closestPoint.x; + xyPoint.y = closestPoint.y; } // Brightness is simply Y in the XYZ space. brightness = Y; diff --git a/libsrc/leddevice/LedDevicePhilipsHue.h b/libsrc/leddevice/LedDevicePhilipsHue.h index 84e7bd35..cc988e97 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/LedDevicePhilipsHue.h @@ -12,6 +12,15 @@ // Leddevice includes #include +struct CGPoint { + float x; + float y; +}; + +struct CGTriangle { + CGPoint red, green, blue; +}; + /** * Implementation for the Philips Hue system. * @@ -56,6 +65,10 @@ private slots: private: /// Array to save the light states. std::vector states; + /// Array to save model ids. + std::vector ids; + /// Color triangles. + std::vector triangles; /// Ip address of the bridge QString host; /// User name for the API ("newdeveloper") @@ -114,7 +127,7 @@ private: /// /// @return true if light states have been saved. /// - bool statesSaved(); + bool areStatesSaved(); /// /// Converts an RGB color to the Hue xy color space and brightness @@ -126,12 +139,16 @@ private: /// /// @param blue the blue component in [0, 1] /// - /// @param x converted x component - /// - /// @param y converted y component + /// @param xyPoint converted xy component /// /// @param brightness converted brightness component /// - void rgbToXYBrightness(float red, float green, float blue, float& x, float& y, float& brightness); + void rgbToXYBrightness(float red, float green, float blue, CGTriangle triangle, CGPoint& xyPoint, float& brightness); + + CGTriangle getTriangle(QString modelId); + float crossProduct(CGPoint p1, CGPoint p2); + bool isPointInLampsReach(CGTriangle triangle, CGPoint p); + CGPoint getClosestPointToPoint(CGPoint a, CGPoint b, CGPoint p); + float getDistanceBetweenTwoPoints(CGPoint one, CGPoint two); }; From 9269b0a1e3c54b3c20f90f2be1b138c3b64715d3 Mon Sep 17 00:00:00 2001 From: Tim Niggemann Date: Tue, 15 Jul 2014 08:54:40 +0200 Subject: [PATCH 65/78] Removed saving of model ids. Save the corresponding color triangles instead for speedup. Former-commit-id: 72e6031234e12a488a5425e80e73dc8b03ec364f --- libsrc/leddevice/LedDevicePhilipsHue.cpp | 7 ++++--- libsrc/leddevice/LedDevicePhilipsHue.h | 2 -- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index b2525033..5c12b667 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -122,6 +122,7 @@ CGTriangle LedDevicePhilipsHue::getTriangle(QString modelId) { void LedDevicePhilipsHue::saveStates(unsigned int nLights) { // Clear saved light states. states.clear(); + triangles.clear(); // Use json parser to parse reponse. Json::Reader reader; Json::FastWriter writer; @@ -142,12 +143,11 @@ void LedDevicePhilipsHue::saveStates(unsigned int nLights) { state["xy"] = json["state"]["xy"]; state["bri"] = json["state"]["bri"]; } - // Save id. - ids.push_back(QString(writer.write(json["modelid"]).c_str()).trimmed().replace("\"", "")); // Save state object. states.push_back(QString(writer.write(state).c_str()).trimmed()); // Determine triangle. - triangles.push_back(getTriangle(ids.back())); + QString modelId = QString(writer.write(json["modelid"]).c_str()).trimmed().replace("\"", ""); + triangles.push_back(getTriangle(modelId)); } } @@ -165,6 +165,7 @@ void LedDevicePhilipsHue::restoreStates() { } // Clear saved light states. states.clear(); + triangles.clear(); } bool LedDevicePhilipsHue::areStatesSaved() { diff --git a/libsrc/leddevice/LedDevicePhilipsHue.h b/libsrc/leddevice/LedDevicePhilipsHue.h index cc988e97..81781db1 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/LedDevicePhilipsHue.h @@ -65,8 +65,6 @@ private slots: private: /// Array to save the light states. std::vector states; - /// Array to save model ids. - std::vector ids; /// Color triangles. std::vector triangles; /// Ip address of the bridge From 67970fce08d60e3bdf10d4126c3c0a43d18a318b Mon Sep 17 00:00:00 2001 From: Tim Niggemann Date: Tue, 15 Jul 2014 09:55:58 +0200 Subject: [PATCH 66/78] Created HueLamp class holding the color space as well as the original state and current color. Former-commit-id: 129c34f6008a68bca6cafb63eb0c0ad6a37f5179 --- libsrc/leddevice/LedDevicePhilipsHue.cpp | 169 ++++++++++++----------- libsrc/leddevice/LedDevicePhilipsHue.h | 37 +++-- 2 files changed, 114 insertions(+), 92 deletions(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index 5c12b667..a678480d 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -31,18 +31,22 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { switchOn((unsigned int) ledValues.size()); } // Iterate through colors and set light states. - unsigned int lightId = 1; + unsigned int lightId = 0; for (const ColorRgb& color : ledValues) { - // Find triangle. - CGTriangle triangle = triangles.at(lightId - 1); + // Get lamp. + HueLamp& lamp = lamps.at(lightId); // Scale colors from [0, 255] to [0, 1] and convert to xy space. - CGPoint xy; - float b; - rgbToXYBrightness(color.red / 255.0f, color.green / 255.0f, color.blue / 255.0f, triangle, xy, b); - // Send adjust color command in JSON format. - put(getStateRoute(lightId), QString("{\"xy\": [%1, %2]}").arg(xy.x).arg(xy.y)); - // Send brightness color command in JSON format. - put(getStateRoute(lightId), QString("{\"bri\": %1}").arg(qRound(b * 255.0f))); + ColorPoint xy; + rgbToXYBrightness(color.red / 255.0f, color.green / 255.0f, color.blue / 255.0f, lamp, xy); + // Write color if color has been changed. + if (xy != lamp.color) { + // Send adjust color command in JSON format. + put(getStateRoute(lightId), QString("{\"xy\": [%1, %2]}").arg(xy.x).arg(xy.y)); + // Send brightness color command in JSON format. + put(getStateRoute(lightId), QString("{\"bri\": %1}").arg(qRound(xy.bri * 255.0f))); + // Remember written color. + lamp.color = xy; + } // Next light id. lightId++; } @@ -98,31 +102,9 @@ QString LedDevicePhilipsHue::getRoute(unsigned int lightId) { return QString("lights/%1").arg(lightId); } -CGTriangle LedDevicePhilipsHue::getTriangle(QString modelId) { - const std::set HUE_BULBS_MODEL_IDS = { "LCT001", "LCT002", "LCT003" }; - const std::set LIVING_COLORS_MODEL_IDS = { "LLC001", "LLC005", "LLC006", "LLC007", "LLC011", "LLC012", - "LLC013", "LST001" }; - CGTriangle triangle; - if (HUE_BULBS_MODEL_IDS.find(modelId) != HUE_BULBS_MODEL_IDS.end()) { - triangle.red = {0.675f, 0.322f}; - triangle.green = {0.4091f, 0.518f}; - triangle.blue = {0.167f, 0.04f}; - } else if (LIVING_COLORS_MODEL_IDS.find(modelId) != LIVING_COLORS_MODEL_IDS.end()) { - triangle.red = {0.703f, 0.296f}; - triangle.green = {0.214f, 0.709f}; - triangle.blue = {0.139f, 0.081f}; - } else { - triangle.red = {1.0f, 0.0f}; - triangle.green = {0.0f, 1.0f}; - triangle.blue = {0.0f, 0.0f}; - } - return triangle; -} - void LedDevicePhilipsHue::saveStates(unsigned int nLights) { - // Clear saved light states. - states.clear(); - triangles.clear(); + // Clear saved lamps. + lamps.clear(); // Use json parser to parse reponse. Json::Reader reader; Json::FastWriter writer; @@ -136,50 +118,48 @@ void LedDevicePhilipsHue::saveStates(unsigned int nLights) { // Error occured, break loop. break; } - // Save state object values which are subject to change. + // Get state object values which are subject to change. Json::Value state(Json::objectValue); state["on"] = json["state"]["on"]; if (json["state"]["on"] == true) { state["xy"] = json["state"]["xy"]; state["bri"] = json["state"]["bri"]; } - // Save state object. - states.push_back(QString(writer.write(state).c_str()).trimmed()); - // Determine triangle. + // Determine the model id. QString modelId = QString(writer.write(json["modelid"]).c_str()).trimmed().replace("\"", ""); - triangles.push_back(getTriangle(modelId)); + QString originalState = QString(writer.write(state).c_str()).trimmed(); + // Save state object. + lamps.push_back(HueLamp(i + 1, originalState, modelId)); } } void LedDevicePhilipsHue::switchOn(unsigned int nLights) { - for (unsigned int i = 0; i < nLights; i++) { - put(getStateRoute(i + 1), "{\"on\": true}"); + for (HueLamp lamp : lamps) { + put(getStateRoute(lamp.id), "{\"on\": true}"); } } void LedDevicePhilipsHue::restoreStates() { - unsigned int lightId = 1; - for (QString state : states) { - put(getStateRoute(lightId), state); - lightId++; + for (HueLamp lamp : lamps) { + put(getStateRoute(lamp.id), lamp.originalState); } // Clear saved light states. - states.clear(); - triangles.clear(); + lamps.clear(); } bool LedDevicePhilipsHue::areStatesSaved() { - return !states.empty(); + return !lamps.empty(); } -float LedDevicePhilipsHue::crossProduct(CGPoint p1, CGPoint p2) { +float LedDevicePhilipsHue::crossProduct(ColorPoint p1, ColorPoint p2) { return p1.x * p2.y - p1.y * p2.x; } -bool LedDevicePhilipsHue::isPointInLampsReach(CGTriangle triangle, CGPoint p) { - CGPoint v1 = { triangle.green.x - triangle.red.x, triangle.green.y - triangle.red.y }; - CGPoint v2 = { triangle.blue.x - triangle.red.x, triangle.blue.y - triangle.red.y }; - CGPoint q = { p.x - triangle.red.x, p.y - triangle.red.y }; +bool LedDevicePhilipsHue::isPointInLampsReach(HueLamp lamp, ColorPoint p) { + ColorTriangle& triangle = lamp.colorSpace; + ColorPoint v1 = { triangle.green.x - triangle.red.x, triangle.green.y - triangle.red.y }; + ColorPoint v2 = { triangle.blue.x - triangle.red.x, triangle.blue.y - triangle.red.y }; + ColorPoint q = { p.x - triangle.red.x, p.y - triangle.red.y }; float s = crossProduct(q, v2) / crossProduct(v1, v2); float t = crossProduct(v1, q) / crossProduct(v1, v2); if ((s >= 0.0f) && (t >= 0.0f) && (s + t <= 1.0f)) { @@ -189,9 +169,9 @@ bool LedDevicePhilipsHue::isPointInLampsReach(CGTriangle triangle, CGPoint p) { } } -CGPoint LedDevicePhilipsHue::getClosestPointToPoint(CGPoint A, CGPoint B, CGPoint P) { - CGPoint AP = { P.x - A.x, P.y - A.y }; - CGPoint AB = { B.x - A.x, B.y - A.y }; +ColorPoint LedDevicePhilipsHue::getClosestPointToPoint(ColorPoint a, ColorPoint b, ColorPoint p) { + ColorPoint AP = { p.x - a.x, p.y - a.y }; + ColorPoint AB = { b.x - a.x, b.y - a.y }; float ab2 = AB.x * AB.x + AB.y * AB.y; float ap_ab = AP.x * AB.x + AP.y * AB.y; float t = ap_ab / ab2; @@ -200,20 +180,19 @@ CGPoint LedDevicePhilipsHue::getClosestPointToPoint(CGPoint A, CGPoint B, CGPoin } else if (t > 1.0f) { t = 1.0f; } - return {A.x + AB.x * t, A.y + AB.y * t}; + return {a.x + AB.x * t, a.y + AB.y * t}; } -float LedDevicePhilipsHue::getDistanceBetweenTwoPoints(CGPoint one, CGPoint two) { +float LedDevicePhilipsHue::getDistanceBetweenTwoPoints(ColorPoint p1, ColorPoint p2) { // Horizontal difference. - float dx = one.x - two.x; + float dx = p1.x - p2.x; // Vertical difference. - float dy = one.y - two.y; - float dist = sqrt(dx * dx + dy * dy); - return dist; + float dy = p1.y - p2.y; + // Absolute value. + return sqrt(dx * dx + dy * dy); } -void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, CGTriangle triangle, CGPoint& xyPoint, - float& brightness) { +void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, HueLamp lamp, ColorPoint& xy) { // Apply gamma correction. float r = (red > 0.04045f) ? powf((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f); float g = (green > 0.04045f) ? powf((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f); @@ -231,20 +210,20 @@ void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, if (isnan(cy)) { cy = 0.0f; } - xyPoint.x = cx; - xyPoint.y = cy; - // Check if the given XY value is within the colourreach of our lamps. - if (!isPointInLampsReach(triangle, xyPoint)) { - // It seems the colour is out of reach let's find the closes colour we can produce with our lamp and send this XY value out. - CGPoint pAB = getClosestPointToPoint(triangle.red, triangle.green, xyPoint); - CGPoint pAC = getClosestPointToPoint(triangle.blue, triangle.red, xyPoint); - CGPoint pBC = getClosestPointToPoint(triangle.green, triangle.blue, xyPoint); + xy.x = cx; + xy.y = cy; + // Check if the given XY value is within the color reach of our lamps. + if (!isPointInLampsReach(lamp, xy)) { + // It seems the color is out of reach let's find the closes colour we can produce with our lamp and send this XY value out. + ColorPoint pAB = getClosestPointToPoint(lamp.colorSpace.red, lamp.colorSpace.green, xy); + ColorPoint pAC = getClosestPointToPoint(lamp.colorSpace.blue, lamp.colorSpace.red, xy); + ColorPoint pBC = getClosestPointToPoint(lamp.colorSpace.green, lamp.colorSpace.blue, xy); // Get the distances per point and see which point is closer to our Point. - float dAB = getDistanceBetweenTwoPoints(xyPoint, pAB); - float dAC = getDistanceBetweenTwoPoints(xyPoint, pAC); - float dBC = getDistanceBetweenTwoPoints(xyPoint, pBC); + float dAB = getDistanceBetweenTwoPoints(xy, pAB); + float dAC = getDistanceBetweenTwoPoints(xy, pAC); + float dBC = getDistanceBetweenTwoPoints(xy, pBC); float lowest = dAB; - CGPoint closestPoint = pAB; + ColorPoint closestPoint = pAB; if (dAC < lowest) { lowest = dAC; closestPoint = pAC; @@ -254,9 +233,41 @@ void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, closestPoint = pBC; } // Change the xy value to a value which is within the reach of the lamp. - xyPoint.x = closestPoint.x; - xyPoint.y = closestPoint.y; + xy.x = closestPoint.x; + xy.y = closestPoint.y; } // Brightness is simply Y in the XYZ space. - brightness = Y; + xy.bri = Y; +} + +HueLamp::HueLamp(unsigned int id, QString originalState, QString modelId) : + id(id), originalState(originalState) { + /// Hue system model ids. + const std::set HUE_BULBS_MODEL_IDS = { "LCT001", "LCT002", "LCT003" }; + const std::set LIVING_COLORS_MODEL_IDS = { "LLC001", "LLC005", "LLC006", "LLC007", "LLC011", "LLC012", + "LLC013", "LST001" }; + /// Find id in the sets and set the appropiate color space. + if (HUE_BULBS_MODEL_IDS.find(modelId) != HUE_BULBS_MODEL_IDS.end()) { + colorSpace.red = {0.675f, 0.322f}; + colorSpace.green = {0.4091f, 0.518f}; + colorSpace.blue = {0.167f, 0.04f}; + } else if (LIVING_COLORS_MODEL_IDS.find(modelId) != LIVING_COLORS_MODEL_IDS.end()) { + colorSpace.red = {0.703f, 0.296f}; + colorSpace.green = {0.214f, 0.709f}; + colorSpace.blue = {0.139f, 0.081f}; + } else { + colorSpace.red = {1.0f, 0.0f}; + colorSpace.green = {0.0f, 1.0f}; + colorSpace.blue = {0.0f, 0.0f}; + } + /// Initialize color with black + color = {0.0f, 0.0f, 0.0f}; +} + +bool operator ==(ColorPoint p1, ColorPoint p2) { + return (p1.x == p2.x) && (p1.y == p2.y) && (p1.bri == p2.bri); +} + +bool operator !=(ColorPoint p1, ColorPoint p2) { + return !(p1 == p2); } diff --git a/libsrc/leddevice/LedDevicePhilipsHue.h b/libsrc/leddevice/LedDevicePhilipsHue.h index 81781db1..219cce85 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/LedDevicePhilipsHue.h @@ -12,13 +12,27 @@ // Leddevice includes #include -struct CGPoint { +struct ColorPoint { float x; float y; + float bri; }; -struct CGTriangle { - CGPoint red, green, blue; +bool operator==(ColorPoint p1, ColorPoint p2); +bool operator!=(ColorPoint p1, ColorPoint p2); + +struct ColorTriangle { + ColorPoint red, green, blue; +}; + +class HueLamp { +public: + unsigned int id; + ColorPoint color; + ColorTriangle colorSpace; + QString originalState; + + HueLamp(unsigned int id, QString originalState, QString modelId); }; /** @@ -63,10 +77,8 @@ private slots: void restoreStates(); private: - /// Array to save the light states. - std::vector states; - /// Color triangles. - std::vector triangles; + /// Array to save the lamps. + std::vector lamps; /// Ip address of the bridge QString host; /// User name for the API ("newdeveloper") @@ -141,12 +153,11 @@ private: /// /// @param brightness converted brightness component /// - void rgbToXYBrightness(float red, float green, float blue, CGTriangle triangle, CGPoint& xyPoint, float& brightness); + void rgbToXYBrightness(float red, float green, float blue, HueLamp lamp, ColorPoint& xy); - CGTriangle getTriangle(QString modelId); - float crossProduct(CGPoint p1, CGPoint p2); - bool isPointInLampsReach(CGTriangle triangle, CGPoint p); - CGPoint getClosestPointToPoint(CGPoint a, CGPoint b, CGPoint p); - float getDistanceBetweenTwoPoints(CGPoint one, CGPoint two); + float crossProduct(ColorPoint p1, ColorPoint p2); + bool isPointInLampsReach(HueLamp lamp, ColorPoint p); + ColorPoint getClosestPointToPoint(ColorPoint a, ColorPoint b, ColorPoint p); + float getDistanceBetweenTwoPoints(ColorPoint one, ColorPoint two); }; From 4af2b11d8fa846c19344685e93175d406574db5e Mon Sep 17 00:00:00 2001 From: Tim Niggemann Date: Tue, 15 Jul 2014 09:58:29 +0200 Subject: [PATCH 67/78] Get the light id from the lamp object. Former-commit-id: dc7aa992386c2511261614a2a8fe3ccf15d9a591 --- libsrc/leddevice/LedDevicePhilipsHue.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index a678480d..a78c7fca 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -41,9 +41,9 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { // Write color if color has been changed. if (xy != lamp.color) { // Send adjust color command in JSON format. - put(getStateRoute(lightId), QString("{\"xy\": [%1, %2]}").arg(xy.x).arg(xy.y)); + put(getStateRoute(lamp.id), QString("{\"xy\": [%1, %2]}").arg(xy.x).arg(xy.y)); // Send brightness color command in JSON format. - put(getStateRoute(lightId), QString("{\"bri\": %1}").arg(qRound(xy.bri * 255.0f))); + put(getStateRoute(lamp.id), QString("{\"bri\": %1}").arg(qRound(xy.bri * 255.0f))); // Remember written color. lamp.color = xy; } From d2542142be8d37c3d290bd5b41e351206448dc36 Mon Sep 17 00:00:00 2001 From: Tim Niggemann Date: Tue, 15 Jul 2014 09:59:01 +0200 Subject: [PATCH 68/78] Get the light id from the lamp object. Former-commit-id: 7120af8551c185979c94b2a186f09c883784a882 --- libsrc/leddevice/LedDevicePhilipsHue.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index a78c7fca..94b6bdab 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -31,10 +31,10 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { switchOn((unsigned int) ledValues.size()); } // Iterate through colors and set light states. - unsigned int lightId = 0; + unsigned int idx = 0; for (const ColorRgb& color : ledValues) { // Get lamp. - HueLamp& lamp = lamps.at(lightId); + HueLamp& lamp = lamps.at(idx); // Scale colors from [0, 255] to [0, 1] and convert to xy space. ColorPoint xy; rgbToXYBrightness(color.red / 255.0f, color.green / 255.0f, color.blue / 255.0f, lamp, xy); @@ -48,7 +48,7 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { lamp.color = xy; } // Next light id. - lightId++; + idx++; } timer.start(); return 0; From 7e049273a84afcf808e510c4847e0e79c1fa16c2 Mon Sep 17 00:00:00 2001 From: Tim Niggemann Date: Tue, 15 Jul 2014 12:06:26 +0200 Subject: [PATCH 69/78] Comments. Former-commit-id: bb4573afa8072bf03a3ae7c1b8ece721c7ea91ff --- libsrc/leddevice/LedDevicePhilipsHue.cpp | 18 ++++---- libsrc/leddevice/LedDevicePhilipsHue.h | 59 ++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index 94b6bdab..fcb6b2a8 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -36,8 +36,7 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { // Get lamp. HueLamp& lamp = lamps.at(idx); // Scale colors from [0, 255] to [0, 1] and convert to xy space. - ColorPoint xy; - rgbToXYBrightness(color.red / 255.0f, color.green / 255.0f, color.blue / 255.0f, lamp, xy); + ColorPoint xy = rgbToXYBrightness(color.red / 255.0f, color.green / 255.0f, color.blue / 255.0f, lamp); // Write color if color has been changed. if (xy != lamp.color) { // Send adjust color command in JSON format. @@ -192,7 +191,7 @@ float LedDevicePhilipsHue::getDistanceBetweenTwoPoints(ColorPoint p1, ColorPoint return sqrt(dx * dx + dy * dy); } -void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, HueLamp lamp, ColorPoint& xy) { +ColorPoint LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, HueLamp lamp) { // Apply gamma correction. float r = (red > 0.04045f) ? powf((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f); float g = (green > 0.04045f) ? powf((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f); @@ -202,16 +201,15 @@ void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, float Y = r * 0.234327f + g * 0.743075f + b * 0.022598f; float Z = r * 0.0000000f + g * 0.053077f + b * 1.035763f; // Convert to x,y space. - float cx = X / (X + Y + Z + 0.0000001f); - float cy = Y / (X + Y + Z + 0.0000001f); + float cx = X / (X + Y + Z); + float cy = Y / (X + Y + Z); if (isnan(cx)) { cx = 0.0f; } if (isnan(cy)) { cy = 0.0f; } - xy.x = cx; - xy.y = cy; + ColorPoint xy = {cx, cy}; // Check if the given XY value is within the color reach of our lamps. if (!isPointInLampsReach(lamp, xy)) { // It seems the color is out of reach let's find the closes colour we can produce with our lamp and send this XY value out. @@ -242,11 +240,11 @@ void LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, HueLamp::HueLamp(unsigned int id, QString originalState, QString modelId) : id(id), originalState(originalState) { - /// Hue system model ids. + // Hue system model ids. const std::set HUE_BULBS_MODEL_IDS = { "LCT001", "LCT002", "LCT003" }; const std::set LIVING_COLORS_MODEL_IDS = { "LLC001", "LLC005", "LLC006", "LLC007", "LLC011", "LLC012", "LLC013", "LST001" }; - /// Find id in the sets and set the appropiate color space. + // Find id in the sets and set the appropiate color space. if (HUE_BULBS_MODEL_IDS.find(modelId) != HUE_BULBS_MODEL_IDS.end()) { colorSpace.red = {0.675f, 0.322f}; colorSpace.green = {0.4091f, 0.518f}; @@ -260,7 +258,7 @@ HueLamp::HueLamp(unsigned int id, QString originalState, QString modelId) : colorSpace.green = {0.0f, 1.0f}; colorSpace.blue = {0.0f, 0.0f}; } - /// Initialize color with black + // Initialize color with black color = {0.0f, 0.0f, 0.0f}; } diff --git a/libsrc/leddevice/LedDevicePhilipsHue.h b/libsrc/leddevice/LedDevicePhilipsHue.h index 219cce85..59d86594 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/LedDevicePhilipsHue.h @@ -12,6 +12,9 @@ // Leddevice includes #include +/** + * A color point in the color space of the hue system. + */ struct ColorPoint { float x; float y; @@ -21,10 +24,16 @@ struct ColorPoint { bool operator==(ColorPoint p1, ColorPoint p2); bool operator!=(ColorPoint p1, ColorPoint p2); +/** + * Color triangle to define an available color space for the hue lamps. + */ struct ColorTriangle { ColorPoint red, green, blue; }; +/** + * Simple class to hold the id, the latest color, the color space and the original state. + */ class HueLamp { public: unsigned int id; @@ -32,6 +41,15 @@ public: ColorTriangle colorSpace; QString originalState; + /// + /// Constructs the lamp. + /// + /// @param id the light id + /// + /// @param originalState the json string of the original state + /// + /// @param modelId the model id of the hue lamp which is used to determine the color space + /// HueLamp(unsigned int id, QString originalState, QString modelId); }; @@ -149,15 +167,50 @@ private: /// /// @param blue the blue component in [0, 1] /// - /// @param xyPoint converted xy component + /// @param lamp the hue lamp instance used for color space checks. /// - /// @param brightness converted brightness component + /// @return color point /// - void rgbToXYBrightness(float red, float green, float blue, HueLamp lamp, ColorPoint& xy); + ColorPoint rgbToXYBrightness(float red, float green, float blue, HueLamp lamp); + /// + /// @param p1 point one + /// + /// @param p2 point tow + /// + /// @return the cross product between p1 and p2 + /// float crossProduct(ColorPoint p1, ColorPoint p2); + + + /// + /// @param lamp the hue lamp instance + /// + /// @param p the color point to check + /// + /// @return true if the color point is covered by the lamp color space + /// bool isPointInLampsReach(HueLamp lamp, ColorPoint p); + + + /// + /// @param a reference point one + /// + /// @param b reference point two + /// + /// @param p the point to which the closest point is to be found + /// + /// @return the closest color point of p to a and b + /// ColorPoint getClosestPointToPoint(ColorPoint a, ColorPoint b, ColorPoint p); + + /// + /// @param p1 point one + /// + /// @param p2 point tow + /// + /// @return the distance between the two points + /// float getDistanceBetweenTwoPoints(ColorPoint one, ColorPoint two); }; From fcb2ff66671fcb9b8a800e62a7da13ef608ed012 Mon Sep 17 00:00:00 2001 From: Tim Niggemann Date: Tue, 15 Jul 2014 12:08:27 +0200 Subject: [PATCH 70/78] Renamed method parameters. Former-commit-id: e10705dd6d93f5c1398a213583bbe349833d2648 --- libsrc/leddevice/LedDevicePhilipsHue.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.h b/libsrc/leddevice/LedDevicePhilipsHue.h index 59d86594..d60c2b32 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/LedDevicePhilipsHue.h @@ -158,7 +158,7 @@ private: bool areStatesSaved(); /// - /// Converts an RGB color to the Hue xy color space and brightness + /// Converts an RGB color to the Hue xy color space and brightness. /// https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/blob/master/ApplicationDesignNotes/RGB%20to%20xy%20Color%20conversion.md /// /// @param red the red component in [0, 1] @@ -211,6 +211,6 @@ private: /// /// @return the distance between the two points /// - float getDistanceBetweenTwoPoints(ColorPoint one, ColorPoint two); + float getDistanceBetweenTwoPoints(ColorPoint p1, ColorPoint p2); }; From b055578759bb7a1f81f08b94c7a6cf8fca90aa31 Mon Sep 17 00:00:00 2001 From: Tim Niggemann Date: Wed, 16 Jul 2014 11:49:34 +0200 Subject: [PATCH 71/78] Added config switch for turing off the lamps if the color black is written. Former-commit-id: bb4f4bc74c035c10a8dc678a11052ea276ea0149 --- libsrc/leddevice/LedDeviceFactory.cpp | 3 ++- libsrc/leddevice/LedDevicePhilipsHue.cpp | 23 ++++++++++++++++------- libsrc/leddevice/LedDevicePhilipsHue.h | 7 ++++++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/libsrc/leddevice/LedDeviceFactory.cpp b/libsrc/leddevice/LedDeviceFactory.cpp index 76eb0137..5a1bd123 100755 --- a/libsrc/leddevice/LedDeviceFactory.cpp +++ b/libsrc/leddevice/LedDeviceFactory.cpp @@ -164,7 +164,8 @@ LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) else if (type == "philipshue") { const std::string output = deviceConfig["output"].asString(); - device = new LedDevicePhilipsHue(output); + const bool switchOffOnBlack = deviceConfig.get("switch_off_on_black", false).asBool(); + device = new LedDevicePhilipsHue(output, switchOffOnBlack); } else if (type == "test") { diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index fcb6b2a8..beda2d71 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -12,8 +12,10 @@ #include -LedDevicePhilipsHue::LedDevicePhilipsHue(const std::string& output) : - host(output.c_str()), username("newdeveloper") { +const ColorPoint LedDevicePhilipsHue::BLACK = {0.0f, 0.0f, 0.0f}; + +LedDevicePhilipsHue::LedDevicePhilipsHue(const std::string& output, bool switchOffOnBlack) : + host(output.c_str()), username("newdeveloper"), switchOffOnBlack(switchOffOnBlack) { http = new QHttp(host); timer.setInterval(3000); timer.setSingleShot(true); @@ -37,12 +39,19 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { HueLamp& lamp = lamps.at(idx); // Scale colors from [0, 255] to [0, 1] and convert to xy space. ColorPoint xy = rgbToXYBrightness(color.red / 255.0f, color.green / 255.0f, color.blue / 255.0f, lamp); + // Switch lamp off if switchOffOnBlack is enabled and the lamp is currently on. + if (switchOffOnBlack && xy == BLACK && lamp.color != BLACK) { + put(getStateRoute(lamp.id), QString("{\"on\": false}")); + } // Write color if color has been changed. - if (xy != lamp.color) { - // Send adjust color command in JSON format. - put(getStateRoute(lamp.id), QString("{\"xy\": [%1, %2]}").arg(xy.x).arg(xy.y)); - // Send brightness color command in JSON format. - put(getStateRoute(lamp.id), QString("{\"bri\": %1}").arg(qRound(xy.bri * 255.0f))); + else if (xy != lamp.color) { + // Switch on if the lamp has been previously switched off. + if (switchOffOnBlack && lamp.color == BLACK) { + put(getStateRoute(lamp.id), QString("{\"on\": true}")); + } + // Send adjust color and brightness command in JSON format. + put(getStateRoute(lamp.id), QString("{\"xy\": [%1, %2], \"bri\": %1}").arg(xy.x).arg(xy.y) + .arg(qRound(xy.bri * 255.0f))); // Remember written color. lamp.color = xy; } diff --git a/libsrc/leddevice/LedDevicePhilipsHue.h b/libsrc/leddevice/LedDevicePhilipsHue.h index d60c2b32..ecbecf3a 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/LedDevicePhilipsHue.h @@ -71,7 +71,9 @@ public: /// /// @param output the ip address of the bridge /// - LedDevicePhilipsHue(const std::string& output); + /// @param switchOffOnBlack kill lights for black + /// + LedDevicePhilipsHue(const std::string& output, bool switchOffOnBlack); /// /// Destructor of this device @@ -95,6 +97,7 @@ private slots: void restoreStates(); private: + const static ColorPoint BLACK; /// Array to save the lamps. std::vector lamps; /// Ip address of the bridge @@ -105,6 +108,8 @@ private: QHttp* http; /// Use timer to reset lights when we got into "GRABBINGMODE_OFF". QTimer timer; + /// + bool switchOffOnBlack; /// /// Sends a HTTP GET request (blocking). From f76b5ffbd885b9190f7f637e40cf88f1cca5717e Mon Sep 17 00:00:00 2001 From: Tim Niggemann Date: Wed, 16 Jul 2014 16:21:11 +0200 Subject: [PATCH 72/78] Added author tag, added missing return statement. Former-commit-id: 4d0a29a8ba3d33de6f86b90a4eaf2f0de12ea59a --- libsrc/leddevice/LedDevicePhilipsHue.cpp | 16 ++++++++-------- libsrc/leddevice/LedDevicePhilipsHue.h | 4 +--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index beda2d71..3e960509 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -12,7 +12,7 @@ #include -const ColorPoint LedDevicePhilipsHue::BLACK = {0.0f, 0.0f, 0.0f}; +const ColorPoint LedDevicePhilipsHue::BLACK = { 0.0f, 0.0f, 0.0f }; LedDevicePhilipsHue::LedDevicePhilipsHue(const std::string& output, bool switchOffOnBlack) : host(output.c_str()), username("newdeveloper"), switchOffOnBlack(switchOffOnBlack) { @@ -42,7 +42,7 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { // Switch lamp off if switchOffOnBlack is enabled and the lamp is currently on. if (switchOffOnBlack && xy == BLACK && lamp.color != BLACK) { put(getStateRoute(lamp.id), QString("{\"on\": false}")); - } + } // Write color if color has been changed. else if (xy != lamp.color) { // Switch on if the lamp has been previously switched off. @@ -50,8 +50,8 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { put(getStateRoute(lamp.id), QString("{\"on\": true}")); } // Send adjust color and brightness command in JSON format. - put(getStateRoute(lamp.id), QString("{\"xy\": [%1, %2], \"bri\": %1}").arg(xy.x).arg(xy.y) - .arg(qRound(xy.bri * 255.0f))); + put(getStateRoute(lamp.id), + QString("{\"xy\": [%1, %2], \"bri\": %1}").arg(xy.x).arg(xy.y).arg(qRound(xy.bri * 255.0f))); // Remember written color. lamp.color = xy; } @@ -218,10 +218,11 @@ ColorPoint LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float if (isnan(cy)) { cy = 0.0f; } - ColorPoint xy = {cx, cy}; + // Brightness is simply Y in the XYZ space. + ColorPoint xy = { cx, cy, Y }; // Check if the given XY value is within the color reach of our lamps. if (!isPointInLampsReach(lamp, xy)) { - // It seems the color is out of reach let's find the closes colour we can produce with our lamp and send this XY value out. + // It seems the color is out of reach let's find the closes color we can produce with our lamp and send this XY value out. ColorPoint pAB = getClosestPointToPoint(lamp.colorSpace.red, lamp.colorSpace.green, xy); ColorPoint pAC = getClosestPointToPoint(lamp.colorSpace.blue, lamp.colorSpace.red, xy); ColorPoint pBC = getClosestPointToPoint(lamp.colorSpace.green, lamp.colorSpace.blue, xy); @@ -243,8 +244,7 @@ ColorPoint LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float xy.x = closestPoint.x; xy.y = closestPoint.y; } - // Brightness is simply Y in the XYZ space. - xy.bri = Y; + return xy; } HueLamp::HueLamp(unsigned int id, QString originalState, QString modelId) : diff --git a/libsrc/leddevice/LedDevicePhilipsHue.h b/libsrc/leddevice/LedDevicePhilipsHue.h index ecbecf3a..c3503705 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/LedDevicePhilipsHue.h @@ -61,7 +61,7 @@ public: * Framegrabber must be limited to 10 Hz / numer of lights to avoid rate limitation by the hue bridge. * Create a new API user name "newdeveloper" on the bridge (http://developers.meethue.com/gettingstarted.html) * - * @author ntim (github) + * @author ntim (github), bimsarck (github) */ class LedDevicePhilipsHue: public QObject, public LedDevice { Q_OBJECT @@ -187,7 +187,6 @@ private: /// float crossProduct(ColorPoint p1, ColorPoint p2); - /// /// @param lamp the hue lamp instance /// @@ -197,7 +196,6 @@ private: /// bool isPointInLampsReach(HueLamp lamp, ColorPoint p); - /// /// @param a reference point one /// From f0d2c15aeb7d077ede26cf684128c38a88cf10cf Mon Sep 17 00:00:00 2001 From: Tim Niggemann Date: Wed, 16 Jul 2014 16:45:45 +0200 Subject: [PATCH 73/78] Fixed QString string formatting, added safety check in case the connection to the bridge might be lost. Former-commit-id: fb1f5fd21cd3873fc1b92d763c682e75f345ab42 --- libsrc/leddevice/LedDevicePhilipsHue.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index 3e960509..df2385ef 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -32,6 +32,11 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { saveStates((unsigned int) ledValues.size()); switchOn((unsigned int) ledValues.size()); } + // If there are less states saved than colors given, then maybe something went wrong before. + if (lamps.size() != ledValues.size()) { + restoreStates(); + return 0; + } // Iterate through colors and set light states. unsigned int idx = 0; for (const ColorRgb& color : ledValues) { @@ -51,7 +56,7 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { } // Send adjust color and brightness command in JSON format. put(getStateRoute(lamp.id), - QString("{\"xy\": [%1, %2], \"bri\": %1}").arg(xy.x).arg(xy.y).arg(qRound(xy.bri * 255.0f))); + QString("{\"xy\": [%1, %2], \"bri\": %3}").arg(xy.x).arg(xy.y).arg(qRound(xy.bri * 255.0f))); // Remember written color. lamp.color = xy; } From dbd7a86665705276e91072e17297716b79ad0f47 Mon Sep 17 00:00:00 2001 From: Tim Niggemann Date: Wed, 16 Jul 2014 20:22:37 +0200 Subject: [PATCH 74/78] Moved color logic to lamp class. Former-commit-id: f450eebc8c9d0f29dc053f2115dac6576a5fa591 --- libsrc/leddevice/LedDeviceFactory.cpp | 2 +- libsrc/leddevice/LedDevicePhilipsHue.cpp | 278 ++++++++++++----------- libsrc/leddevice/LedDevicePhilipsHue.h | 129 ++++++----- 3 files changed, 208 insertions(+), 201 deletions(-) diff --git a/libsrc/leddevice/LedDeviceFactory.cpp b/libsrc/leddevice/LedDeviceFactory.cpp index 5a1bd123..34178421 100755 --- a/libsrc/leddevice/LedDeviceFactory.cpp +++ b/libsrc/leddevice/LedDeviceFactory.cpp @@ -164,7 +164,7 @@ LedDevice * LedDeviceFactory::construct(const Json::Value & deviceConfig) else if (type == "philipshue") { const std::string output = deviceConfig["output"].asString(); - const bool switchOffOnBlack = deviceConfig.get("switch_off_on_black", false).asBool(); + const bool switchOffOnBlack = deviceConfig.get("switchOffOnBlack", true).asBool(); device = new LedDevicePhilipsHue(output, switchOffOnBlack); } else if (type == "test") diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index df2385ef..0a47a24c 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -12,7 +12,125 @@ #include -const ColorPoint LedDevicePhilipsHue::BLACK = { 0.0f, 0.0f, 0.0f }; +bool operator ==(CiColor p1, CiColor p2) { + return (p1.x == p2.x) && (p1.y == p2.y) && (p1.bri == p2.bri); +} + +bool operator !=(CiColor p1, CiColor p2) { + return !(p1 == p2); +} + +PhilipsHueLamp::PhilipsHueLamp(unsigned int id, QString originalState, QString modelId) : + id(id), originalState(originalState) { + // Hue system model ids. + const std::set HUE_BULBS_MODEL_IDS = { "LCT001", "LCT002", "LCT003" }; + const std::set LIVING_COLORS_MODEL_IDS = { "LLC001", "LLC005", "LLC006", "LLC007", "LLC011", "LLC012", + "LLC013", "LST001" }; + // Find id in the sets and set the appropiate color space. + if (HUE_BULBS_MODEL_IDS.find(modelId) != HUE_BULBS_MODEL_IDS.end()) { + colorSpace.red = {0.675f, 0.322f}; + colorSpace.green = {0.4091f, 0.518f}; + colorSpace.blue = {0.167f, 0.04f}; + } else if (LIVING_COLORS_MODEL_IDS.find(modelId) != LIVING_COLORS_MODEL_IDS.end()) { + colorSpace.red = {0.703f, 0.296f}; + colorSpace.green = {0.214f, 0.709f}; + colorSpace.blue = {0.139f, 0.081f}; + } else { + colorSpace.red = {1.0f, 0.0f}; + colorSpace.green = {0.0f, 1.0f}; + colorSpace.blue = {0.0f, 0.0f}; + } + // Initialize black color. + black = rgbToCiColor(0.0f, 0.0f, 0.0f); + // Initialize color with black + color = {black.x, black.y, black.bri}; +} + +float PhilipsHueLamp::crossProduct(CiColor p1, CiColor p2) { + return p1.x * p2.y - p1.y * p2.x; +} + +bool PhilipsHueLamp::isPointInLampsReach(CiColor p) { + CiColor v1 = { colorSpace.green.x - colorSpace.red.x, colorSpace.green.y - colorSpace.red.y }; + CiColor v2 = { colorSpace.blue.x - colorSpace.red.x, colorSpace.blue.y - colorSpace.red.y }; + CiColor q = { p.x - colorSpace.red.x, p.y - colorSpace.red.y }; + float s = crossProduct(q, v2) / crossProduct(v1, v2); + float t = crossProduct(v1, q) / crossProduct(v1, v2); + if ((s >= 0.0f) && (t >= 0.0f) && (s + t <= 1.0f)) { + return true; + } + return false; +} + +CiColor PhilipsHueLamp::getClosestPointToPoint(CiColor a, CiColor b, CiColor p) { + CiColor AP = { p.x - a.x, p.y - a.y }; + CiColor AB = { b.x - a.x, b.y - a.y }; + float ab2 = AB.x * AB.x + AB.y * AB.y; + float ap_ab = AP.x * AB.x + AP.y * AB.y; + float t = ap_ab / ab2; + if (t < 0.0f) { + t = 0.0f; + } else if (t > 1.0f) { + t = 1.0f; + } + return {a.x + AB.x * t, a.y + AB.y * t}; +} + +float PhilipsHueLamp::getDistanceBetweenTwoPoints(CiColor p1, CiColor p2) { + // Horizontal difference. + float dx = p1.x - p2.x; + // Vertical difference. + float dy = p1.y - p2.y; + // Absolute value. + return sqrt(dx * dx + dy * dy); +} + +CiColor PhilipsHueLamp::rgbToCiColor(float red, float green, float blue) { + // Apply gamma correction. + float r = (red > 0.04045f) ? powf((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f); + float g = (green > 0.04045f) ? powf((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f); + float b = (blue > 0.04045f) ? powf((blue + 0.055f) / (1.0f + 0.055f), 2.4f) : (blue / 12.92f); + // Convert to XYZ space. + float X = r * 0.649926f + g * 0.103455f + b * 0.197109f; + float Y = r * 0.234327f + g * 0.743075f + b * 0.022598f; + float Z = r * 0.0000000f + g * 0.053077f + b * 1.035763f; + // Convert to x,y space. + float cx = X / (X + Y + Z); + float cy = Y / (X + Y + Z); + if (isnan(cx)) { + cx = 0.0f; + } + if (isnan(cy)) { + cy = 0.0f; + } + // Brightness is simply Y in the XYZ space. + CiColor xy = { cx, cy, Y }; + // Check if the given XY value is within the color reach of our lamps. + if (!isPointInLampsReach(xy)) { + // It seems the color is out of reach let's find the closes color we can produce with our lamp and send this XY value out. + CiColor pAB = getClosestPointToPoint(colorSpace.red, colorSpace.green, xy); + CiColor pAC = getClosestPointToPoint(colorSpace.blue, colorSpace.red, xy); + CiColor pBC = getClosestPointToPoint(colorSpace.green, colorSpace.blue, xy); + // Get the distances per point and see which point is closer to our Point. + float dAB = getDistanceBetweenTwoPoints(xy, pAB); + float dAC = getDistanceBetweenTwoPoints(xy, pAC); + float dBC = getDistanceBetweenTwoPoints(xy, pBC); + float lowest = dAB; + CiColor closestPoint = pAB; + if (dAC < lowest) { + lowest = dAC; + closestPoint = pAC; + } + if (dBC < lowest) { + lowest = dBC; + closestPoint = pBC; + } + // Change the xy value to a value which is within the reach of the lamp. + xy.x = closestPoint.x; + xy.y = closestPoint.y; + } + return xy; +} LedDevicePhilipsHue::LedDevicePhilipsHue(const std::string& output, bool switchOffOnBlack) : host(output.c_str()), username("newdeveloper"), switchOffOnBlack(switchOffOnBlack) { @@ -41,25 +159,35 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { unsigned int idx = 0; for (const ColorRgb& color : ledValues) { // Get lamp. - HueLamp& lamp = lamps.at(idx); + PhilipsHueLamp& lamp = lamps.at(idx); // Scale colors from [0, 255] to [0, 1] and convert to xy space. - ColorPoint xy = rgbToXYBrightness(color.red / 255.0f, color.green / 255.0f, color.blue / 255.0f, lamp); - // Switch lamp off if switchOffOnBlack is enabled and the lamp is currently on. - if (switchOffOnBlack && xy == BLACK && lamp.color != BLACK) { - put(getStateRoute(lamp.id), QString("{\"on\": false}")); - } + CiColor xy = lamp.rgbToCiColor(color.red / 255.0f, color.green / 255.0f, color.blue / 255.0f); // Write color if color has been changed. - else if (xy != lamp.color) { + if (xy != lamp.color) { // Switch on if the lamp has been previously switched off. - if (switchOffOnBlack && lamp.color == BLACK) { - put(getStateRoute(lamp.id), QString("{\"on\": true}")); + if (switchOffOnBlack && lamp.color == lamp.black) { + } // Send adjust color and brightness command in JSON format. put(getStateRoute(lamp.id), QString("{\"xy\": [%1, %2], \"bri\": %3}").arg(xy.x).arg(xy.y).arg(qRound(xy.bri * 255.0f))); - // Remember written color. - lamp.color = xy; + } + // Switch lamp off if switchOffOnBlack is enabled and the lamp is currently on. + if (switchOffOnBlack) { + // From black to a color. + if (lamp.color == lamp.black && xy != lamp.black) { + put(getStateRoute(lamp.id), QString("{\"on\": true}")); + std::cout << "switchon" << std::endl; + } + // From a color to black. + else if (lamp.color != lamp.black && xy == lamp.black) { + put(getStateRoute(lamp.id), QString("{\"on\": false}")); + std::cout << "switchoff" << std::endl; + } + } + // Remember last color. + lamp.color = xy; // Next light id. idx++; } @@ -142,18 +270,18 @@ void LedDevicePhilipsHue::saveStates(unsigned int nLights) { QString modelId = QString(writer.write(json["modelid"]).c_str()).trimmed().replace("\"", ""); QString originalState = QString(writer.write(state).c_str()).trimmed(); // Save state object. - lamps.push_back(HueLamp(i + 1, originalState, modelId)); + lamps.push_back(PhilipsHueLamp(i + 1, originalState, modelId)); } } void LedDevicePhilipsHue::switchOn(unsigned int nLights) { - for (HueLamp lamp : lamps) { + for (PhilipsHueLamp lamp : lamps) { put(getStateRoute(lamp.id), "{\"on\": true}"); } } void LedDevicePhilipsHue::restoreStates() { - for (HueLamp lamp : lamps) { + for (PhilipsHueLamp lamp : lamps) { put(getStateRoute(lamp.id), lamp.originalState); } // Clear saved light states. @@ -163,123 +291,3 @@ void LedDevicePhilipsHue::restoreStates() { bool LedDevicePhilipsHue::areStatesSaved() { return !lamps.empty(); } - -float LedDevicePhilipsHue::crossProduct(ColorPoint p1, ColorPoint p2) { - return p1.x * p2.y - p1.y * p2.x; -} - -bool LedDevicePhilipsHue::isPointInLampsReach(HueLamp lamp, ColorPoint p) { - ColorTriangle& triangle = lamp.colorSpace; - ColorPoint v1 = { triangle.green.x - triangle.red.x, triangle.green.y - triangle.red.y }; - ColorPoint v2 = { triangle.blue.x - triangle.red.x, triangle.blue.y - triangle.red.y }; - ColorPoint q = { p.x - triangle.red.x, p.y - triangle.red.y }; - float s = crossProduct(q, v2) / crossProduct(v1, v2); - float t = crossProduct(v1, q) / crossProduct(v1, v2); - if ((s >= 0.0f) && (t >= 0.0f) && (s + t <= 1.0f)) { - return true; - } else { - return false; - } -} - -ColorPoint LedDevicePhilipsHue::getClosestPointToPoint(ColorPoint a, ColorPoint b, ColorPoint p) { - ColorPoint AP = { p.x - a.x, p.y - a.y }; - ColorPoint AB = { b.x - a.x, b.y - a.y }; - float ab2 = AB.x * AB.x + AB.y * AB.y; - float ap_ab = AP.x * AB.x + AP.y * AB.y; - float t = ap_ab / ab2; - if (t < 0.0f) { - t = 0.0f; - } else if (t > 1.0f) { - t = 1.0f; - } - return {a.x + AB.x * t, a.y + AB.y * t}; -} - -float LedDevicePhilipsHue::getDistanceBetweenTwoPoints(ColorPoint p1, ColorPoint p2) { - // Horizontal difference. - float dx = p1.x - p2.x; - // Vertical difference. - float dy = p1.y - p2.y; - // Absolute value. - return sqrt(dx * dx + dy * dy); -} - -ColorPoint LedDevicePhilipsHue::rgbToXYBrightness(float red, float green, float blue, HueLamp lamp) { - // Apply gamma correction. - float r = (red > 0.04045f) ? powf((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f); - float g = (green > 0.04045f) ? powf((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f); - float b = (blue > 0.04045f) ? powf((blue + 0.055f) / (1.0f + 0.055f), 2.4f) : (blue / 12.92f); - // Convert to XYZ space. - float X = r * 0.649926f + g * 0.103455f + b * 0.197109f; - float Y = r * 0.234327f + g * 0.743075f + b * 0.022598f; - float Z = r * 0.0000000f + g * 0.053077f + b * 1.035763f; - // Convert to x,y space. - float cx = X / (X + Y + Z); - float cy = Y / (X + Y + Z); - if (isnan(cx)) { - cx = 0.0f; - } - if (isnan(cy)) { - cy = 0.0f; - } - // Brightness is simply Y in the XYZ space. - ColorPoint xy = { cx, cy, Y }; - // Check if the given XY value is within the color reach of our lamps. - if (!isPointInLampsReach(lamp, xy)) { - // It seems the color is out of reach let's find the closes color we can produce with our lamp and send this XY value out. - ColorPoint pAB = getClosestPointToPoint(lamp.colorSpace.red, lamp.colorSpace.green, xy); - ColorPoint pAC = getClosestPointToPoint(lamp.colorSpace.blue, lamp.colorSpace.red, xy); - ColorPoint pBC = getClosestPointToPoint(lamp.colorSpace.green, lamp.colorSpace.blue, xy); - // Get the distances per point and see which point is closer to our Point. - float dAB = getDistanceBetweenTwoPoints(xy, pAB); - float dAC = getDistanceBetweenTwoPoints(xy, pAC); - float dBC = getDistanceBetweenTwoPoints(xy, pBC); - float lowest = dAB; - ColorPoint closestPoint = pAB; - if (dAC < lowest) { - lowest = dAC; - closestPoint = pAC; - } - if (dBC < lowest) { - lowest = dBC; - closestPoint = pBC; - } - // Change the xy value to a value which is within the reach of the lamp. - xy.x = closestPoint.x; - xy.y = closestPoint.y; - } - return xy; -} - -HueLamp::HueLamp(unsigned int id, QString originalState, QString modelId) : - id(id), originalState(originalState) { - // Hue system model ids. - const std::set HUE_BULBS_MODEL_IDS = { "LCT001", "LCT002", "LCT003" }; - const std::set LIVING_COLORS_MODEL_IDS = { "LLC001", "LLC005", "LLC006", "LLC007", "LLC011", "LLC012", - "LLC013", "LST001" }; - // Find id in the sets and set the appropiate color space. - if (HUE_BULBS_MODEL_IDS.find(modelId) != HUE_BULBS_MODEL_IDS.end()) { - colorSpace.red = {0.675f, 0.322f}; - colorSpace.green = {0.4091f, 0.518f}; - colorSpace.blue = {0.167f, 0.04f}; - } else if (LIVING_COLORS_MODEL_IDS.find(modelId) != LIVING_COLORS_MODEL_IDS.end()) { - colorSpace.red = {0.703f, 0.296f}; - colorSpace.green = {0.214f, 0.709f}; - colorSpace.blue = {0.139f, 0.081f}; - } else { - colorSpace.red = {1.0f, 0.0f}; - colorSpace.green = {0.0f, 1.0f}; - colorSpace.blue = {0.0f, 0.0f}; - } - // Initialize color with black - color = {0.0f, 0.0f, 0.0f}; -} - -bool operator ==(ColorPoint p1, ColorPoint p2) { - return (p1.x == p2.x) && (p1.y == p2.y) && (p1.bri == p2.bri); -} - -bool operator !=(ColorPoint p1, ColorPoint p2) { - return !(p1 == p2); -} diff --git a/libsrc/leddevice/LedDevicePhilipsHue.h b/libsrc/leddevice/LedDevicePhilipsHue.h index c3503705..085defb0 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/LedDevicePhilipsHue.h @@ -15,30 +15,34 @@ /** * A color point in the color space of the hue system. */ -struct ColorPoint { +struct CiColor { + /// X component. float x; + /// Y component. float y; + /// The brightness. float bri; }; -bool operator==(ColorPoint p1, ColorPoint p2); -bool operator!=(ColorPoint p1, ColorPoint p2); +bool operator==(CiColor p1, CiColor p2); +bool operator!=(CiColor p1, CiColor p2); /** * Color triangle to define an available color space for the hue lamps. */ -struct ColorTriangle { - ColorPoint red, green, blue; +struct CiColorTriangle { + CiColor red, green, blue; }; /** * Simple class to hold the id, the latest color, the color space and the original state. */ -class HueLamp { +class PhilipsHueLamp { public: unsigned int id; - ColorPoint color; - ColorTriangle colorSpace; + CiColor black; + CiColor color; + CiColorTriangle colorSpace; QString originalState; /// @@ -50,7 +54,57 @@ public: /// /// @param modelId the model id of the hue lamp which is used to determine the color space /// - HueLamp(unsigned int id, QString originalState, QString modelId); + PhilipsHueLamp(unsigned int id, QString originalState, QString modelId); + + /// + /// Converts an RGB color to the Hue xy color space and brightness. + /// https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/blob/master/ApplicationDesignNotes/RGB%20to%20xy%20Color%20conversion.md + /// + /// @param red the red component in [0, 1] + /// + /// @param green the green component in [0, 1] + /// + /// @param blue the blue component in [0, 1] + /// + /// @return color point + /// + CiColor rgbToCiColor(float red, float green, float blue); + + /// + /// @param p the color point to check + /// + /// @return true if the color point is covered by the lamp color space + /// + bool isPointInLampsReach(CiColor p); + + /// + /// @param p1 point one + /// + /// @param p2 point tow + /// + /// @return the cross product between p1 and p2 + /// + float crossProduct(CiColor p1, CiColor p2); + + /// + /// @param a reference point one + /// + /// @param b reference point two + /// + /// @param p the point to which the closest point is to be found + /// + /// @return the closest color point of p to a and b + /// + CiColor getClosestPointToPoint(CiColor a, CiColor b, CiColor p); + + /// + /// @param p1 point one + /// + /// @param p2 point tow + /// + /// @return the distance between the two points + /// + float getDistanceBetweenTwoPoints(CiColor p1, CiColor p2); }; /** @@ -97,9 +151,8 @@ private slots: void restoreStates(); private: - const static ColorPoint BLACK; /// Array to save the lamps. - std::vector lamps; + std::vector lamps; /// Ip address of the bridge QString host; /// User name for the API ("newdeveloper") @@ -162,58 +215,4 @@ private: /// bool areStatesSaved(); - /// - /// Converts an RGB color to the Hue xy color space and brightness. - /// https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/blob/master/ApplicationDesignNotes/RGB%20to%20xy%20Color%20conversion.md - /// - /// @param red the red component in [0, 1] - /// - /// @param green the green component in [0, 1] - /// - /// @param blue the blue component in [0, 1] - /// - /// @param lamp the hue lamp instance used for color space checks. - /// - /// @return color point - /// - ColorPoint rgbToXYBrightness(float red, float green, float blue, HueLamp lamp); - - /// - /// @param p1 point one - /// - /// @param p2 point tow - /// - /// @return the cross product between p1 and p2 - /// - float crossProduct(ColorPoint p1, ColorPoint p2); - - /// - /// @param lamp the hue lamp instance - /// - /// @param p the color point to check - /// - /// @return true if the color point is covered by the lamp color space - /// - bool isPointInLampsReach(HueLamp lamp, ColorPoint p); - - /// - /// @param a reference point one - /// - /// @param b reference point two - /// - /// @param p the point to which the closest point is to be found - /// - /// @return the closest color point of p to a and b - /// - ColorPoint getClosestPointToPoint(ColorPoint a, ColorPoint b, ColorPoint p); - - /// - /// @param p1 point one - /// - /// @param p2 point tow - /// - /// @return the distance between the two points - /// - float getDistanceBetweenTwoPoints(ColorPoint p1, ColorPoint p2); - }; From dc2e173f040e23d80529c11e743e43d20bd89b99 Mon Sep 17 00:00:00 2001 From: ntim Date: Wed, 16 Jul 2014 20:46:59 +0200 Subject: [PATCH 75/78] Remove couts. Former-commit-id: fdc93ea33644313277bd4b01c14e4a63396c077f --- libsrc/leddevice/LedDevicePhilipsHue.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/libsrc/leddevice/LedDevicePhilipsHue.cpp b/libsrc/leddevice/LedDevicePhilipsHue.cpp index 0a47a24c..35607871 100755 --- a/libsrc/leddevice/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/LedDevicePhilipsHue.cpp @@ -178,12 +178,10 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { // From black to a color. if (lamp.color == lamp.black && xy != lamp.black) { put(getStateRoute(lamp.id), QString("{\"on\": true}")); - std::cout << "switchon" << std::endl; } // From a color to black. else if (lamp.color != lamp.black && xy == lamp.black) { put(getStateRoute(lamp.id), QString("{\"on\": false}")); - std::cout << "switchoff" << std::endl; } } // Remember last color. From b6fa306df71da127323b7f9f6dc08cd78248355a Mon Sep 17 00:00:00 2001 From: Johan Date: Fri, 1 Aug 2014 10:13:40 +0200 Subject: [PATCH 76/78] Fix schema requirement for json transforms (use numbers instead of doubles to allow integer values for zero and one) Former-commit-id: f65ea81c217bbf621184152fd913d58247d9b4db --- libsrc/jsonserver/schema/schema-transform.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libsrc/jsonserver/schema/schema-transform.json b/libsrc/jsonserver/schema/schema-transform.json index a135e12f..7b939839 100644 --- a/libsrc/jsonserver/schema/schema-transform.json +++ b/libsrc/jsonserver/schema/schema-transform.json @@ -16,12 +16,12 @@ "required" : false }, "saturationGain" : { - "type" : "double", + "type" : "number", "required" : false, "minimum" : 0.0 }, "valueGain" : { - "type" : "double", + "type" : "number", "required" : false, "minimum" : 0.0 }, @@ -29,7 +29,7 @@ "type": "array", "required": false, "items" : { - "type": "double", + "type": "number", "minimum": 0.0, "maximum": 1.0 }, @@ -40,7 +40,7 @@ "type": "array", "required": false, "items" : { - "type": "double", + "type": "number", "minimum": 0.0 }, "minItems": 3, @@ -50,7 +50,7 @@ "type": "array", "required": false, "items" : { - "type": "double" + "type": "number" }, "minItems": 3, "maxItems": 3 @@ -59,7 +59,7 @@ "type": "array", "required": false, "items" : { - "type": "double" + "type": "number" }, "minItems": 3, "maxItems": 3 From 04c54ee7eb375e8d94a62bfb0b9cea0d75a799c5 Mon Sep 17 00:00:00 2001 From: poljvd Date: Mon, 18 Aug 2014 13:50:19 +0200 Subject: [PATCH 77/78] Fix typo error Former-commit-id: 664fc81f7bcfab58ac543f08725992044e51d8db --- include/utils/VideoMode.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/utils/VideoMode.h b/include/utils/VideoMode.h index f5cae94f..ae727657 100644 --- a/include/utils/VideoMode.h +++ b/include/utils/VideoMode.h @@ -18,7 +18,7 @@ inline VideoMode parse3DMode(std::string videoMode) // convert to lower case std::transform(videoMode.begin(), videoMode.end(), videoMode.begin(), ::tolower); - if (videoMode == "23DTAB") + if (videoMode == "3DTAB") { return VIDEO_3DTAB; } From b6514b73ffa6c43693fcac9289ff97c985222e41 Mon Sep 17 00:00:00 2001 From: "T. van der Zwan" Date: Sun, 24 Aug 2014 21:00:03 +0200 Subject: [PATCH 78/78] Build release Former-commit-id: 00ccb7b1ea58bdcaec77246c5c1c424183f9adbe --- 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 7729eb84..ab29ef88 100644 --- a/deploy/hyperion.tar.gz.REMOVED.git-id +++ b/deploy/hyperion.tar.gz.REMOVED.git-id @@ -1 +1 @@ -5e8d795d2aa82337e42924c1a5292203d7d4271a \ No newline at end of file +9546e335179f732ff68ea9bc47020a19e4b6f44c \ No newline at end of file