From 67280b8566f313ce38b2b5334da0c402a91d7d2e Mon Sep 17 00:00:00 2001
From: LordGrey <48840279+Lord-Grey@users.noreply.github.com>
Date: Wed, 17 Nov 2021 19:34:49 +0000
Subject: [PATCH] Razor Chroma Support - Initial version (#1054)
* Razor Chroma Support - Initial version
* Address clang and lgtm findings
* Razer Fixes
* Merge branch 'master' into Razer_Chroma_Support
# Conflicts:
# assets/webconfig/i18n/en.json
# assets/webconfig/js/content_leds.js
# libsrc/leddevice/dev_net/ProviderRestApi.cpp
# libsrc/leddevice/dev_net/ProviderRestApi.h
* Corrections
* Set default config for Razer
* Simplify
* Razer - Support individual LEDs and have default layout per device type
* Differentiate between HWLEDCount and LayoutLEDCount
* Revert "Differentiate between HWLEDCount and LayoutLEDCount"
This reverts commit b147b215a5773a423184618ecb8dc9653d4870cc.
* Correct LGTM finding
* Disable verbose mode
---
assets/webconfig/i18n/en.json | 1 +
assets/webconfig/js/content_leds.js | 45 ++-
include/leddevice/LedDevice.h | 2 +-
libsrc/leddevice/LedDeviceSchemas.qrc | 1 +
libsrc/leddevice/dev_net/LedDeviceRazer.cpp | 399 +++++++++++++++++++
libsrc/leddevice/dev_net/LedDeviceRazer.h | 212 ++++++++++
libsrc/leddevice/dev_net/ProviderRestApi.cpp | 170 ++++++--
libsrc/leddevice/dev_net/ProviderRestApi.h | 98 ++++-
libsrc/leddevice/schemas/schema-razer.json | 28 ++
9 files changed, 888 insertions(+), 68 deletions(-)
create mode 100644 libsrc/leddevice/dev_net/LedDeviceRazer.cpp
create mode 100644 libsrc/leddevice/dev_net/LedDeviceRazer.h
create mode 100644 libsrc/leddevice/schemas/schema-razer.json
diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json
index 58ea4e79..84c5da1e 100644
--- a/assets/webconfig/i18n/en.json
+++ b/assets/webconfig/i18n/en.json
@@ -570,6 +570,7 @@
"edt_dev_spec_port_title": "Port",
"edt_dev_spec_printTimeStamp_title": "Add timestamp",
"edt_dev_spec_pwmChannel_title": "PWM channel",
+ "edt_dev_spec_razer_device_title": "Razer Chroma Device",
"edt_dev_spec_restoreOriginalState_title": "Restore lights' state",
"edt_dev_spec_restoreOriginalState_title_info": "Restore the device's original state when device is disabled",
"edt_dev_spec_serial_title": "Serial number",
diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js
index d7ab3937..d76f5fd3 100755
--- a/assets/webconfig/js/content_leds.js
+++ b/assets/webconfig/js/content_leds.js
@@ -1,4 +1,3 @@
-var ledsCustomCfgInitialized = false;
var onLedLayoutTab = false;
var nonBlacklistLedArray = [];
var ledBlacklist = [];
@@ -13,7 +12,7 @@ var canvas_width;
var devRPiSPI = ['apa102', 'apa104', 'ws2801', 'lpd6803', 'lpd8806', 'p9813', 'sk6812spi', 'sk6822spi', 'sk9822', 'ws2812spi'];
var devRPiPWM = ['ws281x'];
var devRPiGPIO = ['piblaster'];
-var devNET = ['atmoorb', 'cololight', 'fadecandy', 'philipshue', 'nanoleaf', 'tinkerforge', 'tpm2net', 'udpe131', 'udpartnet', 'udph801', 'udpraw', 'wled', 'yeelight'];
+var devNET = ['atmoorb', 'cololight', 'fadecandy', 'philipshue', 'nanoleaf', 'razer', 'tinkerforge', 'tpm2net', 'udpe131', 'udpartnet', 'udph801', 'udpraw', 'wled', 'yeelight'];
var devSerial = ['adalight', 'dmx', 'atmo', 'sedu', 'tpm2', 'karate'];
var devHID = ['hyperionusbasp', 'lightpack', 'paintpack', 'rawhid'];
@@ -614,10 +613,7 @@ $(document).ready(function () {
var target = $(e.target).attr("href") // activated tab
if (target == "#menu_gencfg") {
onLedLayoutTab = true;
- if (!ledsCustomCfgInitialized) {
- $('#leds_custom_updsim').trigger('click');
- ledsCustomCfgInitialized = true;
- }
+ $('#leds_custom_updsim').trigger('click');
} else {
onLedLayoutTab = false;
}
@@ -777,6 +773,16 @@ $(document).ready(function () {
colorOrderDefault = "rgb";
break;
+ case "razer":
+ conf_editor.getEditor("root.generalOptions").disable();
+ hwLedCountDefault = 1;
+ colorOrderDefault = "bgr";
+
+ var subType = conf_editor.getEditor("root.specificOptions.subType").getValue();
+ let params = { subType: subType };
+ getProperties_device(ledType, subType, params);
+ break;
+
default:
}
@@ -1012,6 +1018,19 @@ $(document).ready(function () {
}
});
+ conf_editor.watch('root.specificOptions.subType', () => {
+ var subType = conf_editor.getEditor("root.specificOptions.subType").getValue();
+ let params = {};
+
+ switch (ledType) {
+ case "razer":
+ params = { subType: subType };
+ getProperties_device(ledType, subType, params);
+ break;
+ default:
+ }
+ });
+
conf_editor.watch('root.specificOptions.token', () => {
var token = conf_editor.getEditor("root.specificOptions.token").getValue();
@@ -1612,7 +1631,7 @@ function updateElements(ledType, key) {
var maxLedCount = ledProperties.maxLedCount
if (hardwareLedCount > maxLedCount)
{
- showInfoDialog('warning', $.i18n("conf_leds_config_warning"), $.i18n('conf_leds_error_hwled_gt_maxled', hardwareLedCount, maxLedCount, maxLedCount));
+ showInfoDialog('warning', $.i18n("conf_leds_config_warning"), $.i18n('conf_leds_error_hwled_gt_maxled', hardwareLedCount, maxLedCount, maxLedCount));
hardwareLedCount = maxLedCount;
}
}
@@ -1665,6 +1684,18 @@ function updateElements(ledType, key) {
}
break;
+ case "razer":
+ var ledProperties = devicesProperties[ledType][key];
+ if (ledProperties) {
+ conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(ledProperties.maxLedCount);
+ $("#ip_ma_ledshoriz").val(ledProperties.maxColumn);
+ $("#ip_ma_ledsvert").val(ledProperties.maxRow);
+ $("#ip_ma_cabling").val("parallel");
+ $("#ip_ma_start").val("top-left");
+ createMatrixLeds();
+ }
+ break;
+
default:
}
}
diff --git a/include/leddevice/LedDevice.h b/include/leddevice/LedDevice.h
index aebfcd46..576013ab 100644
--- a/include/leddevice/LedDevice.h
+++ b/include/leddevice/LedDevice.h
@@ -435,7 +435,7 @@ protected slots:
///
/// @return Zero on success else negative
///
- int rewriteLEDs();
+ virtual int rewriteLEDs();
///
/// @brief Set device in error state
diff --git a/libsrc/leddevice/LedDeviceSchemas.qrc b/libsrc/leddevice/LedDeviceSchemas.qrc
index a07064c5..2718c281 100644
--- a/libsrc/leddevice/LedDeviceSchemas.qrc
+++ b/libsrc/leddevice/LedDeviceSchemas.qrc
@@ -35,6 +35,7 @@
schemas/schema-nanoleaf.json
schemas/schema-wled.json
schemas/schema-yeelight.json
+ schemas/schema-razer.json
schemas/schema-cololight.json
diff --git a/libsrc/leddevice/dev_net/LedDeviceRazer.cpp b/libsrc/leddevice/dev_net/LedDeviceRazer.cpp
new file mode 100644
index 00000000..ee9b3385
--- /dev/null
+++ b/libsrc/leddevice/dev_net/LedDeviceRazer.cpp
@@ -0,0 +1,399 @@
+#include "LedDeviceRazer.h"
+#include
+
+#if _WIN32
+#include
+#endif
+
+#include
+
+using namespace Chroma;
+using namespace Chroma::Keyboard;
+using namespace Chroma::Keypad;
+using namespace Chroma::Mouse;
+using namespace Chroma::Mousepad;
+using namespace Chroma::Headset;
+using namespace Chroma::Chromalink;
+
+// Constants
+namespace {
+ bool verbose = false;
+
+ // Configuration settings
+ const char CONFIG_RAZER_DEVICE_TYPE[] = "subType";
+ const char CONFIG_SINGLE_COLOR[] = "singleColor";
+
+ // WLED JSON-API elements
+ const char API_DEFAULT_HOST[] = "localhost";
+ const int API_DEFAULT_PORT = 54235;
+
+ const char API_BASE_PATH[] = "/razer/chromasdk";
+ const char API_RESULT[] = "result";
+
+ constexpr std::chrono::milliseconds HEARTBEAT_INTERVALL{ 1000 };
+
+} //End of constants
+
+LedDeviceRazer::LedDeviceRazer(const QJsonObject& deviceConfig)
+ : LedDevice(deviceConfig)
+ , _restApi(nullptr)
+ , _apiPort(API_DEFAULT_PORT)
+ , _maxRow(Chroma::MAX_ROW)
+ , _maxColumn(Chroma::MAX_COLUMN)
+ , _maxLeds(Chroma::MAX_LEDS)
+{
+}
+
+LedDevice* LedDeviceRazer::construct(const QJsonObject& deviceConfig)
+{
+ return new LedDeviceRazer(deviceConfig);
+}
+
+LedDeviceRazer::~LedDeviceRazer()
+{
+ delete _restApi;
+ _restApi = nullptr;
+}
+
+
+bool LedDeviceRazer::init(const QJsonObject& deviceConfig)
+{
+ bool isInitOK = false;
+ setRewriteTime(HEARTBEAT_INTERVALL.count());
+ connect(_refreshTimer, &QTimer::timeout, this, &LedDeviceRazer::rewriteLEDs);
+
+ // Initialise sub-class
+ if (LedDevice::init(deviceConfig))
+ {
+ // Initialise LedDevice configuration and execution environment
+ uint configuredLedCount = this->getLedCount();
+ Debug(_log, "DeviceType : %s", QSTRING_CSTR(this->getActiveDeviceType()));
+ Debug(_log, "LedCount : %u", configuredLedCount);
+ Debug(_log, "ColorOrder : %s", QSTRING_CSTR(this->getColorOrder()));
+ Debug(_log, "LatchTime : %d", this->getLatchTime());
+ Debug(_log, "RefreshTime : %d", _refreshTimerInterval_ms);
+
+ //Razer Chroma SDK allows localhost connection only
+ _hostname = API_DEFAULT_HOST;
+ _apiPort = API_DEFAULT_PORT;
+
+ Debug(_log, "Hostname : %s", QSTRING_CSTR(_hostname));
+ Debug(_log, "Port : %d", _apiPort);
+
+ _razerDeviceType = deviceConfig[CONFIG_RAZER_DEVICE_TYPE].toString("invalid").toLower();
+ _isSingleColor = deviceConfig[CONFIG_SINGLE_COLOR].toBool();
+
+ Debug(_log, "Razer Device : %s", QSTRING_CSTR(_razerDeviceType));
+ Debug(_log, "Single Color : %d", _isSingleColor);
+
+ if (resolveDeviceProperties(_razerDeviceType))
+ {
+ if (_isSingleColor && configuredLedCount > 1)
+ {
+ Info(_log, "In single color mode only the first LED of the configured [%d] will be used.", configuredLedCount);
+ }
+ else
+ {
+ if (_maxLeds != configuredLedCount)
+ {
+ Warning(_log, "Razer device might not work as expected. The type \"%s\" requires an LED-matrix [%d,%d] configured, i.e. %d LEDs. Currently only %d are defined via the layout.",
+ QSTRING_CSTR(_razerDeviceType),
+ _maxRow, _maxColumn, _maxLeds,
+ configuredLedCount
+ );
+ }
+ }
+
+ if (initRestAPI(_hostname, _apiPort))
+ {
+ isInitOK = true;
+ }
+ }
+ else
+ {
+ Error(_log, "Razer devicetype \"%s\" not supported", QSTRING_CSTR(_razerDeviceType));
+ }
+ }
+
+ return isInitOK;
+}
+
+bool LedDeviceRazer::initRestAPI(const QString& hostname, int port)
+{
+ bool isInitOK = false;
+
+ if (_restApi == nullptr)
+ {
+ _restApi = new ProviderRestApi(hostname, port);
+ _restApi->setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+
+ isInitOK = true;
+ }
+
+ return isInitOK;
+}
+
+bool LedDeviceRazer::checkApiError(const httpResponse& response)
+{
+ bool apiError = false;
+
+ if (response.error())
+ {
+ this->setInError(response.getErrorReason());
+ apiError = true;
+ }
+ else
+ {
+ QString errorReason;
+
+ QString strJson(response.getBody().toJson(QJsonDocument::Compact));
+ //DebugIf(verbose, _log, "Reply: [%s]", strJson.toUtf8().constData());
+
+ QJsonObject jsonObj = response.getBody().object();
+
+ if (!jsonObj[API_RESULT].isNull())
+ {
+ int resultCode = jsonObj[API_RESULT].toInt();
+
+ if (resultCode != 0)
+ {
+ errorReason = QString("Chroma SDK error (%1)").arg(resultCode);
+ this->setInError(errorReason);
+ apiError = true;
+ }
+ }
+ }
+ return apiError;
+}
+
+int LedDeviceRazer::open()
+{
+ int retval = -1;
+ _isDeviceReady = false;
+
+ // Try to open the LedDevice
+
+ QJsonObject obj;
+
+ obj.insert("title", "Hyperion - Razer Chroma");
+ obj.insert("description", "Hyperion to Razer Chroma interface");
+
+ QJsonObject authorDetails;
+ authorDetails.insert("name", "Hyperion Team");
+ authorDetails.insert("contact", "https://github.com/hyperion-project/hyperion.ng");
+
+ obj.insert("author", authorDetails);
+ obj.insert("device_supported", QJsonArray::fromStringList(Chroma::SupportedDevices));
+
+ obj.insert("category", "application");
+
+ _restApi->setPort(API_DEFAULT_PORT);
+ _restApi->setBasePath(API_BASE_PATH);
+
+ httpResponse response = _restApi->post(obj);
+ if (!checkApiError(response))
+ {
+ QJsonObject jsonObj = response.getBody().object();
+ if (jsonObj["uri"].isNull())
+ {
+ this->setInError("Chroma SDK error. No 'uri' received");
+ }
+ else
+ {
+ _uri = jsonObj.value("uri").toString();
+ _restApi->setUrl(_uri);
+
+ DebugIf(verbose, _log, "Session-ID: %d, uri [%s]", jsonObj.value("sessionid").toInt(), QSTRING_CSTR(_uri.toString()));
+
+ QJsonObject effectObj;
+ effectObj.insert("effect", "CHROMA_STATIC");
+ QJsonObject param;
+ param.insert("color", 255);
+ effectObj.insert("param", param);
+
+ _restApi->setPath(_razerDeviceType);
+ response = _restApi->put(effectObj);
+
+ if (!checkApiError(response))
+ {
+ _restApi->setPath(_razerDeviceType);
+ response = _restApi->put(effectObj);
+
+ if (!checkApiError(response))
+ {
+ // Everything is OK, device is ready
+ _isDeviceReady = true;
+ retval = 0;
+ }
+ }
+ }
+ }
+ return retval;
+}
+
+int LedDeviceRazer::close()
+{
+ int retval = -1;
+ _isDeviceReady = false;
+
+ if (!_uri.isEmpty())
+ {
+ httpResponse response = _restApi->deleteResource(_uri);
+ if (!checkApiError(response))
+ {
+ // Everything is OK -> device is closed
+ retval = 0;
+ }
+ }
+ return retval;
+}
+
+int LedDeviceRazer::write(const std::vector& ledValues)
+{
+ int retval = -1;
+
+ QJsonObject effectObj;
+
+ if (_isSingleColor)
+ {
+ //Static effect
+ effectObj.insert("effect", "CHROMA_STATIC");
+
+ ColorRgb color = ledValues[0];
+ int colorParam = (color.red * 65536) + (color.green * 256) + color.blue;
+
+ QJsonObject param;
+ param.insert("color", colorParam);
+ effectObj.insert("param", param);
+ }
+ else
+ {
+ //Custom effect
+ if (_customEffectType == Chroma::CHROMA_CUSTOM2)
+ {
+ effectObj.insert("effect", "CHROMA_CUSTOM2");
+ }
+ else {
+ effectObj.insert("effect", "CHROMA_CUSTOM");
+ }
+
+ QJsonArray rowParams;
+ for (int row = 0; row < _maxRow; row++) {
+ QJsonArray columnParams;
+ for (int col = 0; col < _maxColumn; col++) {
+ int pos = row * _maxColumn + col;
+ int bgrColor;
+ if (pos < ledValues.size())
+ {
+ bgrColor = (ledValues[pos].red * 65536) + (ledValues[pos].green * 256) + ledValues[pos].blue;
+ }
+ else
+ {
+ bgrColor = 0;
+ }
+ columnParams.append(bgrColor);
+ }
+
+ if (_maxRow == 1)
+ {
+ rowParams = columnParams;
+ }
+ else
+ {
+ rowParams.append(columnParams);
+ }
+ }
+ effectObj.insert("param", rowParams);
+ }
+
+ _restApi->setPath(_razerDeviceType);
+ httpResponse response = _restApi->put(effectObj);
+ if (!checkApiError(response))
+ {
+ retval = 0;
+ }
+ return retval;
+}
+
+int LedDeviceRazer::rewriteLEDs()
+{
+ int retval = -1;
+
+ _restApi->setPath("heartbeat");
+ httpResponse response = _restApi->put();
+ if (!checkApiError(response))
+ {
+ retval = 0;
+ }
+ return retval;
+}
+
+bool LedDeviceRazer::resolveDeviceProperties(const QString& deviceType)
+{
+ bool rc = true;
+
+ int typeID = Chroma::SupportedDevices.indexOf(deviceType);
+
+ switch (typeID)
+ {
+ case Chroma::DEVICE_KEYBOARD:
+ _maxRow = Chroma::Keyboard::MAX_ROW;
+ _maxColumn = Chroma::Keyboard::MAX_COLUMN;
+ _customEffectType = Chroma::Keyboard::CUSTOM_EFFECT_TYPE;
+ break;
+ case Chroma::DEVICE_MOUSE:
+ _maxRow = Chroma::Mouse::MAX_ROW;
+ _maxColumn = Chroma::Mouse::MAX_COLUMN;
+ _customEffectType = Chroma::Mouse::CUSTOM_EFFECT_TYPE;
+ break;
+ case Chroma::DEVICE_HEADSET:
+ _maxRow = Chroma::Headset::MAX_ROW;
+ _maxColumn = Chroma::Headset::MAX_COLUMN;
+ _customEffectType = Chroma::Headset::CUSTOM_EFFECT_TYPE;
+ break;
+ case Chroma::DEVICE_MOUSEPAD:
+ _maxRow = Chroma::Mousepad::MAX_ROW;
+ _maxColumn = Chroma::Mousepad::MAX_COLUMN;
+ _customEffectType = Chroma::Mousepad::CUSTOM_EFFECT_TYPE;
+ break;
+ case Chroma::DEVICE_KEYPAD:
+ _maxRow = Chroma::Keypad::MAX_ROW;
+ _maxColumn = Chroma::Keypad::MAX_COLUMN;
+ _customEffectType = Chroma::Keypad::CUSTOM_EFFECT_TYPE;
+ break;
+ case Chroma::DEVICE_CHROMALINK:
+ _maxRow = Chroma::Chromalink::MAX_ROW;
+ _maxColumn = Chroma::Chromalink::MAX_COLUMN;
+ _customEffectType = Chroma::Chromalink::CUSTOM_EFFECT_TYPE;
+ break;
+ default:
+ rc = false;
+ break;
+ }
+
+ if (rc)
+ {
+ _maxLeds = _maxRow * _maxColumn;
+ }
+ return rc;
+}
+
+QJsonObject LedDeviceRazer::getProperties(const QJsonObject& params)
+{
+ DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData());
+ QJsonObject properties;
+
+ _razerDeviceType = params[CONFIG_RAZER_DEVICE_TYPE].toString("invalid").toLower();
+ if (resolveDeviceProperties(_razerDeviceType))
+ {
+ QJsonObject propertiesDetails;
+ propertiesDetails.insert("maxRow", _maxRow);
+ propertiesDetails.insert("maxColumn", _maxColumn);
+ propertiesDetails.insert("maxLedCount", _maxLeds);
+
+ properties.insert("properties", propertiesDetails);
+
+ DebugIf(verbose, _log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData());
+ }
+ return properties;
+}
diff --git a/libsrc/leddevice/dev_net/LedDeviceRazer.h b/libsrc/leddevice/dev_net/LedDeviceRazer.h
new file mode 100644
index 00000000..f76ca957
--- /dev/null
+++ b/libsrc/leddevice/dev_net/LedDeviceRazer.h
@@ -0,0 +1,212 @@
+#ifndef LEDEVICERAZER_H
+#define LEDEVICERAZER_H
+
+// LedDevice includes
+#include
+#include "ProviderRestApi.h"
+
+namespace Chroma
+{
+ typedef enum DeviceType
+ {
+ DEVICE_KEYBOARD = 0,
+ DEVICE_MOUSE = 1,
+ DEVICE_HEADSET = 2,
+ DEVICE_MOUSEPAD = 3,
+ DEVICE_KEYPAD = 4,
+ DEVICE_CHROMALINK = 5,
+ DEVICE_SYSTEM = 6,
+ DEVICE_SPEAKERS = 7,
+ DEVICE_INVALID
+ } DEVICETYPE;
+
+ typedef enum CustomEffectType
+ {
+ CHROMA_CUSTOM = 1,
+ CHROMA_CUSTOM2 = 2,
+ } CUSTOM_EFFECT_TYPE;
+
+ const int MAX_ROW = 30; //!< Maximum rows for custom effects.
+ const int MAX_COLUMN = 30; //!< Maximum columns for custom effects.
+ const int MAX_LEDS = MAX_ROW * MAX_COLUMN;
+
+ namespace Keyboard
+ {
+ const char TYPE_NAME[] = "keyboard";
+ const int MAX_ROW = 6;
+ const int MAX_COLUMN = 22;
+ const CustomEffectType CUSTOM_EFFECT_TYPE = CHROMA_CUSTOM;
+ }
+ namespace Mouse
+ {
+ const char TYPE_NAME[] = "mouse";
+ const int MAX_ROW = 9;
+ const int MAX_COLUMN = 7;
+ const CustomEffectType CUSTOM_EFFECT_TYPE = CHROMA_CUSTOM2;
+ }
+ namespace Headset
+ {
+ const char TYPE_NAME[] = "headset";
+ const int MAX_ROW = 1;
+ const int MAX_COLUMN = 5;
+ const CustomEffectType CUSTOM_EFFECT_TYPE = CHROMA_CUSTOM;
+ }
+ namespace Mousepad
+ {
+ const char TYPE_NAME[] = "mousepad";
+ const int MAX_ROW = 1;
+ const int MAX_COLUMN = 15;
+ const CustomEffectType CUSTOM_EFFECT_TYPE = CHROMA_CUSTOM;
+ }
+ namespace Keypad
+ {
+ const char TYPE_NAME[] = "keypad";
+ const int MAX_ROW = 4;
+ const int MAX_COLUMN = 5;
+ const CustomEffectType CUSTOM_EFFECT_TYPE = CHROMA_CUSTOM;
+ }
+ namespace Chromalink
+ {
+ const char TYPE_NAME[] = "chromalink";
+ const int MAX_ROW = 1;
+ const int MAX_COLUMN = 5;
+ const CustomEffectType CUSTOM_EFFECT_TYPE = CHROMA_CUSTOM;
+ }
+
+ const QStringList SupportedDevices{
+ Keyboard::TYPE_NAME,
+ Mouse::TYPE_NAME,
+ Headset::TYPE_NAME,
+ Mousepad::TYPE_NAME,
+ Keypad::TYPE_NAME,
+ Chromalink::TYPE_NAME
+ };
+}
+
+///
+/// Implementation of a Razer Chroma LedDevice
+/// Supported Razer Chroma device types: Keyboard, Mouse, Headset, Mousepad, Keypad, Chromalink
+///
+class LedDeviceRazer : public LedDevice
+{
+public:
+
+ ///
+ /// @brief Constructs a specific LED-device
+ ///
+ /// @param deviceConfig Device's configuration as JSON-Object
+ ///
+ explicit LedDeviceRazer(const QJsonObject& deviceConfig);
+
+ ///
+ /// @brief Constructs the LED-device
+ ///
+ /// @param[in] deviceConfig Device's configuration as JSON-Object
+ /// @return LedDevice constructed
+ ///
+ static LedDevice* construct(const QJsonObject& deviceConfig);
+
+ ///
+ /// @brief Get a Razer device's resource properties
+ ///
+ /// Following parameters are required
+ /// @code
+ /// {
+ /// "subType" : "razer_device_type",
+ /// }
+ /// @endcode
+ /// @param[in] params Parameters to query device
+ /// @return A JSON structure holding the device's properties
+ ///
+ QJsonObject getProperties(const QJsonObject& params) override;
+
+ ///
+ /// @brief Destructor of the LED-device
+ ///
+ ~LedDeviceRazer() override;
+
+protected:
+
+ ///
+ /// @brief Initialise the device's configuration
+ ///
+ /// @param[in] deviceConfig the JSON device configuration
+ /// @return True, if success
+ ///
+ bool init(const QJsonObject& deviceConfig) override;
+
+ ///
+ /// @brief Opens the output device.
+ ///
+ /// @return Zero on success (i.e. device is ready), else negative
+ ///
+ int open() override;
+
+ ///
+ /// @brief Closes the output device.
+ ///
+ /// @return Zero on success (i.e. device is closed), else negative
+ ///
+ int close() override;
+
+ ///
+ /// @brief Writes the RGB-Color values to the LEDs.
+ ///
+ /// @param[in] ledValues The RGB-color per LED
+ /// @return Zero on success, else negative
+ ///
+ int write(const std::vector& ledValues) override;
+
+protected slots:
+
+ ///
+ /// @brief Write the last data to the LEDs again.
+ ///
+ /// @return Zero on success else negative
+ ///
+ int rewriteLEDs() override;
+
+private:
+
+ ///
+ /// @brief Initialise the access to the REST-API wrapper
+ ///
+ /// @param[in] host
+ /// @param[in] port
+ /// @return True, if success
+ ///
+ bool initRestAPI(const QString& hostname, int port);
+
+ ///
+ /// @brief Check, if Chroma SDK API response failed
+ ///
+ /// @param[in] http response, incl. the response by Chroma SDK in JSON-format
+ /// return True, API call failed
+ ///
+ bool checkApiError(const httpResponse& response);
+
+ ///
+ /// @brief Update object with properties for a given device
+ ///
+ /// @param[in] deviceType
+ /// return True, if success
+ ///
+ bool resolveDeviceProperties(const QString& deviceType);
+
+ ///REST-API wrapper
+ ProviderRestApi* _restApi;
+
+ QString _hostname;
+ int _apiPort;
+ QUrl _uri;
+
+ QString _razerDeviceType;
+ int _maxRow;
+ int _maxColumn;
+ int _maxLeds;
+ Chroma::CustomEffectType _customEffectType;
+
+ bool _isSingleColor;
+};
+
+#endif // LEDEVICERAZER_H
diff --git a/libsrc/leddevice/dev_net/ProviderRestApi.cpp b/libsrc/leddevice/dev_net/ProviderRestApi.cpp
index 7ececc7e..b1185c2a 100644
--- a/libsrc/leddevice/dev_net/ProviderRestApi.cpp
+++ b/libsrc/leddevice/dev_net/ProviderRestApi.cpp
@@ -14,6 +14,8 @@
// Constants
namespace {
+bool verbose = false;
+
const QChar ONE_SLASH = '/';
const int HTTP_STATUS_NO_CONTENT = 204;
@@ -25,22 +27,19 @@ constexpr std::chrono::milliseconds DEFAULT_REST_TIMEOUT{ 400 };
} //End of constants
-ProviderRestApi::ProviderRestApi(const QString &host, int port, const QString &basePath)
+ProviderRestApi::ProviderRestApi(const QString& host, int port, const QString& basePath)
:_log(Logger::getInstance("LEDDEVICE"))
- ,_networkManager(nullptr)
- ,_scheme("http")
- ,_hostname(host)
- ,_port(port)
+ , _networkManager(nullptr)
{
_networkManager = new QNetworkAccessManager();
- _apiUrl.setScheme(_scheme);
+ _apiUrl.setScheme("http");
_apiUrl.setHost(host);
_apiUrl.setPort(port);
_basePath = basePath;
}
-ProviderRestApi::ProviderRestApi(const QString &host, int port)
+ProviderRestApi::ProviderRestApi(const QString& host, int port)
: ProviderRestApi(host, port, "") {}
ProviderRestApi::ProviderRestApi()
@@ -51,40 +50,46 @@ ProviderRestApi::~ProviderRestApi()
delete _networkManager;
}
-void ProviderRestApi::setBasePath(const QString &basePath)
+void ProviderRestApi::setUrl(const QUrl& url)
+{
+ _apiUrl = url;
+ _basePath = url.path();
+}
+
+void ProviderRestApi::setBasePath(const QString& basePath)
{
_basePath.clear();
- appendPath (_basePath, basePath );
+ appendPath(_basePath, basePath);
}
-void ProviderRestApi::setPath ( const QString &path )
+void ProviderRestApi::setPath(const QString& path)
{
_path.clear();
- appendPath (_path, path );
+ appendPath(_path, path);
}
-void ProviderRestApi::appendPath ( const QString &path )
+void ProviderRestApi::appendPath(const QString& path)
{
- appendPath (_path, path );
+ appendPath(_path, path);
}
void ProviderRestApi::appendPath ( QString& path, const QString &appendPath)
{
- if ( !appendPath.isEmpty() && appendPath != ONE_SLASH )
+ if (!appendPath.isEmpty() && appendPath != ONE_SLASH)
{
- if (path.isEmpty() || path == ONE_SLASH )
+ if (path.isEmpty() || path == ONE_SLASH)
{
path.clear();
- if (appendPath[0] != ONE_SLASH )
+ if (appendPath[0] != ONE_SLASH)
{
path.push_back(ONE_SLASH);
}
}
- else if (path[path.size()-1] == ONE_SLASH && appendPath[0] == ONE_SLASH)
+ else if (path[path.size() - 1] == ONE_SLASH && appendPath[0] == ONE_SLASH)
{
path.chop(1);
}
- else if (path[path.size()-1] != ONE_SLASH && appendPath[0] != ONE_SLASH)
+ else if (path[path.size() - 1] != ONE_SLASH && appendPath[0] != ONE_SLASH)
{
path.push_back(ONE_SLASH);
}
@@ -97,12 +102,12 @@ void ProviderRestApi::appendPath ( QString& path, const QString &appendPath)
}
}
-void ProviderRestApi::setFragment(const QString &fragment)
+void ProviderRestApi::setFragment(const QString& fragment)
{
_fragment = fragment;
}
-void ProviderRestApi::setQuery(const QUrlQuery &query)
+void ProviderRestApi::setQuery(const QUrlQuery& query)
{
_query = query;
}
@@ -112,23 +117,25 @@ QUrl ProviderRestApi::getUrl() const
QUrl url = _apiUrl;
QString fullPath = _basePath;
- appendPath (fullPath, _path );
+ appendPath(fullPath, _path);
url.setPath(fullPath);
- url.setFragment( _fragment );
- url.setQuery( _query );
+ url.setFragment(_fragment);
+ url.setQuery(_query);
return url;
}
httpResponse ProviderRestApi::get()
{
- return get( getUrl() );
+ return get(getUrl());
}
-httpResponse ProviderRestApi::get(const QUrl &url)
+httpResponse ProviderRestApi::get(const QUrl& url)
{
// Perform request
- QNetworkRequest request(url);
+ QNetworkRequest request(_networkRequestHeaders);
+ request.setUrl(url);
+
QNetworkReply* reply = _networkManager->get(request);
// Connect requestFinished signal to quit slot of the loop.
@@ -141,7 +148,7 @@ httpResponse ProviderRestApi::get(const QUrl &url)
loop.exec();
httpResponse response;
- if(reply->operation() == QNetworkAccessManager::GetOperation)
+ if (reply->operation() == QNetworkAccessManager::GetOperation)
{
if(reply->error() != QNetworkReply::NoError)
{
@@ -168,7 +175,9 @@ httpResponse ProviderRestApi::put(const QString &body)
httpResponse ProviderRestApi::put(const QUrl &url, const QByteArray &body)
{
// Perform request
- QNetworkRequest request(url);
+ QNetworkRequest request(_networkRequestHeaders);
+ request.setUrl(url);
+
QNetworkReply* reply = _networkManager->put(request, body);
// Connect requestFinished signal to quit slot of the loop.
QEventLoop loop;
@@ -180,7 +189,7 @@ httpResponse ProviderRestApi::put(const QUrl &url, const QByteArray &body)
loop.exec();
httpResponse response;
- if(reply->operation() == QNetworkAccessManager::PutOperation)
+ if (reply->operation() == QNetworkAccessManager::PutOperation)
{
if(reply->error() != QNetworkReply::NoError)
{
@@ -195,20 +204,88 @@ httpResponse ProviderRestApi::put(const QUrl &url, const QByteArray &body)
return response;
}
-httpResponse ProviderRestApi::getResponse(QNetworkReply* const &reply)
+httpResponse ProviderRestApi::post(const QJsonObject& body)
+{
+ return post( getUrl(), QJsonDocument(body).toJson(QJsonDocument::Compact));
+}
+
+httpResponse ProviderRestApi::post(const QString& body)
+{
+ return post( getUrl(), body.toUtf8() );
+}
+
+httpResponse ProviderRestApi::post(const QUrl& url, const QByteArray& body)
+{
+ // Perform request
+ QNetworkRequest request(_networkRequestHeaders);
+ request.setUrl(url);
+
+ QNetworkReply* reply = _networkManager->post(request, body);
+ // Connect requestFinished signal to quit slot of the loop.
+ QEventLoop loop;
+ QEventLoop::connect(reply,&QNetworkReply::finished,&loop,&QEventLoop::quit);
+ // Go into the loop until the request is finished.
+ loop.exec();
+
+ httpResponse response;
+ if (reply->operation() == QNetworkAccessManager::PostOperation)
+ {
+ if(reply->error() != QNetworkReply::NoError)
+ {
+ Debug(_log, "POST: [%s] [%s]", QSTRING_CSTR( url.toString() ),body.constData() );
+ }
+ response = getResponse(reply);
+ }
+ // Free space.
+ reply->deleteLater();
+
+ // Return response
+ return response;
+}
+
+httpResponse ProviderRestApi::deleteResource(const QUrl& url)
+{
+ // Perform request
+ QNetworkRequest request(_networkRequestHeaders);
+ request.setUrl(url);
+
+ QNetworkReply* reply = _networkManager->deleteResource(request);
+ // Connect requestFinished signal to quit slot of the loop.
+ QEventLoop loop;
+ QEventLoop::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
+ // Go into the loop until the request is finished.
+ loop.exec();
+
+ httpResponse response;
+ if (reply->operation() == QNetworkAccessManager::DeleteOperation)
+ {
+ if(reply->error() != QNetworkReply::NoError)
+ {
+ Debug(_log, "DELETE: [%s]", QSTRING_CSTR(url.toString()));
+ }
+ response = getResponse(reply);
+ }
+ // Free space.
+ reply->deleteLater();
+
+ // Return response
+ return response;
+}
+
+httpResponse ProviderRestApi::getResponse(QNetworkReply* const& reply)
{
httpResponse response;
- int httpStatusCode = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
+ int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
response.setHttpStatusCode(httpStatusCode);
response.setNetworkReplyError(reply->error());
- if(reply->error() == QNetworkReply::NoError)
+ if (reply->error() == QNetworkReply::NoError)
{
if ( httpStatusCode != HTTP_STATUS_NO_CONTENT ){
QByteArray replyData = reply->readAll();
- if ( !replyData.isEmpty())
+ if (!replyData.isEmpty())
{
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(replyData, &error);
@@ -222,13 +299,13 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const &reply)
}
else
{
- //std::cout << "Response: [" << QString (jsonDoc.toJson(QJsonDocument::Compact)).toStdString() << "]" << std::endl;
- response.setBody( jsonDoc );
+ //std::cout << "Response: [" << QString(jsonDoc.toJson(QJsonDocument::Compact)).toStdString() << "]" << std::endl;
+ response.setBody(jsonDoc);
}
}
else
{ // Create valid body which is empty
- response.setBody( QJsonDocument() );
+ response.setBody(QJsonDocument());
}
}
}
@@ -236,8 +313,8 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const &reply)
{
Debug(_log, "Reply.httpStatusCode [%d]", httpStatusCode );
QString errorReason;
- if ( httpStatusCode > 0 ) {
- QString httpReason = reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute ).toString();
+ if (httpStatusCode > 0) {
+ QString httpReason = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
QString advise;
switch ( httpStatusCode ) {
case HTTP_STATUS_BAD_REQUEST:
@@ -271,8 +348,23 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const &reply)
}
// Create valid body which is empty
- response.setBody( QJsonDocument() );
+ response.setBody(QJsonDocument());
}
return response;
}
+void ProviderRestApi::setHeader(QNetworkRequest::KnownHeaders header, const QVariant& value)
+{
+ QVariant headerValue = _networkRequestHeaders.header(header);
+ if (headerValue.isNull())
+ {
+ _networkRequestHeaders.setHeader(header, value);
+ }
+ else
+ {
+ if (!headerValue.toString().contains(value.toString()))
+ {
+ _networkRequestHeaders.setHeader(header, headerValue.toString() + "," + value.toString());
+ }
+ }
+}
diff --git a/libsrc/leddevice/dev_net/ProviderRestApi.h b/libsrc/leddevice/dev_net/ProviderRestApi.h
index 79052333..50d8a399 100644
--- a/libsrc/leddevice/dev_net/ProviderRestApi.h
+++ b/libsrc/leddevice/dev_net/ProviderRestApi.h
@@ -60,20 +60,20 @@ class httpResponse
public:
httpResponse() = default;
- bool error() const { return _hasError;}
+ bool error() const { return _hasError; }
void setError(const bool hasError) { _hasError = hasError; }
QJsonDocument getBody() const { return _responseBody; }
- void setBody(const QJsonDocument &body) { _responseBody = body; }
+ void setBody(const QJsonDocument& body) { _responseBody = body; }
QString getErrorReason() const { return _errorReason; }
- void setErrorReason(const QString &errorReason) { _errorReason = errorReason; }
+ void setErrorReason(const QString& errorReason) { _errorReason = errorReason; }
int getHttpStatusCode() const { return _httpStatusCode; }
void setHttpStatusCode(int httpStatusCode) { _httpStatusCode = httpStatusCode; }
QNetworkReply::NetworkError getNetworkReplyError() const { return _networkReplyError; }
- void setNetworkReplyError (const QNetworkReply::NetworkError networkReplyError) { _networkReplyError = networkReplyError; }
+ void setNetworkReplyError(const QNetworkReply::NetworkError networkReplyError) { _networkReplyError = networkReplyError; }
private:
@@ -119,7 +119,7 @@ public:
/// @param[in] host
/// @param[in] port
///
- explicit ProviderRestApi(const QString &host, int port);
+ explicit ProviderRestApi(const QString& host, int port);
///
/// @brief Constructor of the REST-API wrapper
@@ -128,13 +128,34 @@ public:
/// @param[in] port
/// @param[in] API base-path
///
- explicit ProviderRestApi(const QString &host, int port, const QString &basePath);
+ explicit ProviderRestApi(const QString& host, int port, const QString& basePath);
///
/// @brief Destructor of the REST-API wrapper
///
virtual ~ProviderRestApi();
+ ///
+ /// @brief Set an API's host
+ ///
+ /// @param[in] host
+ ///
+ void setHost(const QString& host) { _apiUrl.setHost(host); }
+
+ ///
+ /// @brief Set an API's port
+ ///
+ /// @param[in] port
+ ///
+ void setPort(const int port) { _apiUrl.setPort(port); }
+
+ ///
+ /// @brief Set an API's url
+ ///
+ /// @param[in] url, e.g. "http://locahost:60351/chromalink/"
+ ///
+ void setUrl(const QUrl& url);
+
///
/// @brief Get the URL as defined using scheme, host, port, API-basepath, path, query, fragment
///
@@ -147,35 +168,35 @@ public:
///
/// @param[in] basePath, e.g. "/api/v1/" or "/json"
///
- void setBasePath(const QString &basePath);
+ void setBasePath(const QString& basePath);
///
/// @brief Set an API's path to address resources
///
/// @param[in] path, e.g. "/lights/1/state/"
///
- void setPath ( const QString &path );
+ void setPath(const QString& path);
///
/// @brief Append an API's path element to path set before
///
/// @param[in] path
///
- void appendPath (const QString &appendPath);
+ void appendPath(const QString& appendPath);
///
/// @brief Set an API's fragment
///
/// @param[in] fragment, e.g. "question3"
///
- void setFragment(const QString&fragment);
+ void setFragment(const QString& fragment);
///
/// @brief Set an API's query string
///
/// @param[in] query, e.g. "&A=128&FX=0"
///
- void setQuery(const QUrlQuery &query);
+ void setQuery(const QUrlQuery& query);
///
/// @brief Execute GET request
@@ -190,14 +211,14 @@ public:
/// @param[in] url GET request for URL
/// @return Response The body of the response in JSON
///
- httpResponse get(const QUrl &url);
+ httpResponse get(const QUrl& url);
/// @brief Execute PUT request
///
/// @param[in] body The body of the request in JSON
/// @return Response The body of the response in JSON
///
- httpResponse put(const QJsonObject &body);
+ httpResponse put(const QJsonObject& body);
///
/// @brief Execute PUT request
@@ -205,7 +226,7 @@ public:
/// @param[in] body The body of the request in JSON
/// @return Response The body of the response in JSON
///
- httpResponse put(const QString &body = "");
+ httpResponse put(const QString& body = "");
///
/// @brief Execute PUT request
@@ -214,7 +235,7 @@ public:
/// @param[in] body The body of the request in JSON
/// @return Response The body of the response in JSON
///
- httpResponse put(const QUrl &url, const QByteArray &body);
+ httpResponse put(const QUrl &url, const QByteArray& body);
///
/// @brief Execute POST request
@@ -222,7 +243,31 @@ public:
/// @param[in] body The body of the request in JSON
/// @return Response The body of the response in JSON
///
- httpResponse post(QString body = "");
+ httpResponse post(const QString& body = "");
+
+ /// @brief Execute POST request
+ ///
+ /// @param[in] body The body of the request in JSON
+ /// @return Response The body of the response in JSON
+ ///
+ httpResponse post(const QJsonObject& body);
+
+ ///
+ /// @brief Execute POST request
+ ///
+ /// @param[in] URL for POST request
+ /// @param[in] body The body of the request in JSON
+ /// @return Response The body of the response in JSON
+ ///
+ httpResponse post(const QUrl &url, const QByteArray& body);
+
+ ///
+ /// @brief Execute DELETE request
+ ///
+ /// @param[in] URL (Resource) for DELETE request
+ /// @return Response The body of the response in JSON
+ ///
+ httpResponse deleteResource(const QUrl& url);
///
/// @brief Handle responses for REST requests
@@ -230,7 +275,21 @@ public:
/// @param[in] reply Network reply
/// @return Response The body of the response in JSON
///
- httpResponse getResponse(QNetworkReply* const &reply);
+ httpResponse getResponse(QNetworkReply* const& reply);
+
+ ///
+ /// Adds a header field.
+ ///
+ /// @param[in] The type of the header field.
+ /// @param[in] The value of the header field.
+ /// If the header field exists, the value will be combined as comma separated string.
+
+ void setHeader(QNetworkRequest::KnownHeaders header, const QVariant& value);
+
+ ///
+ /// Remove all header fields.
+ ///
+ void removeAllHeaders() { _networkRequestHeaders = QNetworkRequest(); }
private:
@@ -249,16 +308,13 @@ private:
QUrl _apiUrl;
- QString _scheme;
- QString _hostname;
- int _port;
-
QString _basePath;
QString _path;
QString _fragment;
QUrlQuery _query;
+ QNetworkRequest _networkRequestHeaders;
};
#endif // PROVIDERRESTKAPI_H
diff --git a/libsrc/leddevice/schemas/schema-razer.json b/libsrc/leddevice/schemas/schema-razer.json
new file mode 100644
index 00000000..41202f2a
--- /dev/null
+++ b/libsrc/leddevice/schemas/schema-razer.json
@@ -0,0 +1,28 @@
+{
+ "type": "object",
+ "required": true,
+ "properties": {
+ "subType": {
+ "type": "string",
+ "title": "edt_dev_spec_razer_device_title",
+ "enum": [ "keyboard", "mouse", "headset", "mousepad", "keypad", "chromalink" ],
+ "default": "keyboard",
+ "options": {
+ "enum_titles": [ "Keyboard", "Mouse", "Headset", "Mousepad", "Keypad", "Chromalink" ]
+ },
+ "propertyOrder": 1
+ },
+ "latchTime": {
+ "type": "integer",
+ "title": "edt_dev_spec_latchtime_title",
+ "default": 0,
+ "append": "edt_append_ms",
+ "minimum": 0,
+ "maximum": 1000,
+ "access": "expert",
+ "propertyOrder": 2
+ }
+ },
+ "additionalProperties": true
+}
+