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 +} +