Add Nanoleaf support

nanoleaf
Nikita Rushmanov 11 months ago committed by Adam Honse
parent 7faa3b4875
commit 79b49873ec
  1. 42
      .gitlab-ci.yml
  2. 211
      Controllers/NanoleafController/NanoleafController.cpp
  3. 62
      Controllers/NanoleafController/NanoleafController.h
  4. 40
      Controllers/NanoleafController/NanoleafControllerDetect.cpp
  5. 139
      Controllers/NanoleafController/RGBController_Nanoleaf.cpp
  6. 32
      Controllers/NanoleafController/RGBController_Nanoleaf.h
  7. 28
      OpenRGB.pro
  8. 6
      README.md
  9. 1539
      dependencies/mdns/mdns.h
  10. 41
      qt/OpenRGBDialog2.cpp
  11. 3
      qt/OpenRGBDialog2.h
  12. 438
      qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafScanningThread.cpp
  13. 21
      qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafScanningThread.h
  14. 93
      qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsEntry.cpp
  15. 34
      qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsEntry.h
  16. 92
      qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsEntry.ui
  17. 82
      qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.cpp
  18. 31
      qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.h
  19. 48
      qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.ui

@ -13,7 +13,7 @@
- shared-windows
- windows
- windows-1809
stages:
- build
- test
@ -37,7 +37,7 @@ before_script:
script:
- export $(dpkg-architecture)
- ./scripts/build-appimage.sh
artifacts:
name: "${CI_PROJECT_NAME}_Linux_32_${CI_COMMIT_SHORT_SHA}"
paths:
@ -56,7 +56,7 @@ before_script:
script:
- export $(dpkg-architecture)
- ./scripts/build-appimage.sh
artifacts:
name: "${CI_PROJECT_NAME}_Linux_64_${CI_COMMIT_SHORT_SHA}"
paths:
@ -183,8 +183,8 @@ before_script:
image: fedora:36
stage: build
script:
- dnf install rpmdevtools dnf-plugins-core -y
- rpmdev-setuptree
- dnf install rpmdevtools dnf-plugins-core libcurl-devel -y
- rpmdev-setuptree
- ls /root/
- cp fedora/OpenRGB.spec /root/rpmbuild/SPECS
- cp ../OpenRGB /root/rpmbuild/SOURCES/ -r
@ -357,42 +357,42 @@ before_script:
- '& cmd.exe /C "vcvarsall.bat x86 & set" | Foreach-Object { if ($_ -match "(.*?)=(.*)") { Set-Item -force -path "Env:\$($matches[1])" -value "$($matches[2])" } }'
- Pop-Location
- _fold_final_
- _fold_start_ 'downloading precompiled versions of qtbase, qttools (for windeployqt) and jom (for a more parallel nmake)'
- mkdir _qt
- mkdir _qt_download
- Push-Location _qt_download
- curl.exe -LJ -o qt-base.7z 'https://qt-mirror.dannhauer.de/online/qtsdkrepository/windows_x86/desktop/qt5_5150/qt.qt5.5150.win32_msvc2019/5.15.0-0-202005150700qtbase-Windows-Windows_10-MSVC2019-Windows-Windows_10-X86.7z'
- curl.exe -LJ -o qt-base.7z 'https://qt-mirror.dannhauer.de/online/qtsdkrepository/windows_x86/desktop/qt5_5150/qt.qt5.5150.win32_msvc2019/5.15.0-0-202005150700qtbase-Windows-Windows_10-MSVC2019-Windows-Windows_10-X86.7z'
- curl.exe -LJ -o qt-tools.7z 'https://qt-mirror.dannhauer.de/online/qtsdkrepository/windows_x86/desktop/qt5_5150/qt.qt5.5150.win32_msvc2019/5.15.0-0-202005150700qttools-Windows-Windows_10-MSVC2019-Windows-Windows_10-X86.7z'
- curl.exe -LJ -o qt-jom.zip 'https://qt-mirror.dannhauer.de/official_releases/jom/jom.zip'
- _fold_final_
- _fold_start_ 'extracting the downloaded qt binaries'
- 7z x qt-base.7z '-o../_qt' -y
- 7z x qt-tools.7z '-o../_qt' -y
- 7z x qt-jom.zip '-o../_qt' -y
- _fold_final_
- _fold_start_ 'turn the qt install from enterprise to foss; remove the licensing checks'
- ${qconfig-pri-folder} = '..\_qt\5.15.0\msvc2019\mkspecs\qconfig.pri'
- (Get-Content ${qconfig-pri-folder}).replace('QT_EDITION = Enterprise', 'QT_EDITION = OpenSource') | Set-Content ${qconfig-pri-folder}
- (Get-Content ${qconfig-pri-folder}).replace('QT_LICHECK = licheck.exe', '') | Set-Content ${qconfig-pri-folder}
- Pop-Location
- _fold_final_
- _fold_start_ 'run qmake and generate the msvc nmake makefile'
- mkdir _build; cd _build
- ..\_qt\5.15.0\msvc2019\bin\qmake ..\OpenRGB.pro
- _fold_final_
- _fold_start_ 'start the actual build with jom instead of nmake; for speed'
- ..\_qt\jom
- _fold_final_
- _fold_start_ 'run windeployqt to automatically copy the needed dll files'
- ..\_qt\5.15.0\msvc2019\bin\windeployqt --no-angle --no-translations --no-opengl-sw --no-system-d3d-compiler --no-compiler-runtime --no-webkit2 .\release\
- _fold_final_
- _fold_start_ 'Moving results for upload'
- mv release ../'OpenRGB Windows 32-bit'
- _fold_final_
@ -426,42 +426,42 @@ before_script:
- '& cmd.exe /C "vcvarsall.bat x64 & set" | Foreach-Object { if ($_ -match "(.*?)=(.*)") { Set-Item -force -path "Env:\$($matches[1])" -value "$($matches[2])" } }'
- Pop-Location
- _fold_final_
- _fold_start_ 'downloading precompiled versions of qtbase, qttools (for windeployqt) and jom (for a more parallel nmake)'
- mkdir _qt
- mkdir _qt_download
- Push-Location _qt_download
- curl.exe -LJ -o qt-base.7z 'https://qt-mirror.dannhauer.de/online/qtsdkrepository/windows_x86/desktop/qt5_5150/qt.qt5.5150.win64_msvc2019_64/5.15.0-0-202005150700qtbase-Windows-Windows_10-MSVC2019-Windows-Windows_10-X86_64.7z'
- curl.exe -LJ -o qt-base.7z 'https://qt-mirror.dannhauer.de/online/qtsdkrepository/windows_x86/desktop/qt5_5150/qt.qt5.5150.win64_msvc2019_64/5.15.0-0-202005150700qtbase-Windows-Windows_10-MSVC2019-Windows-Windows_10-X86_64.7z'
- curl.exe -LJ -o qt-tools.7z 'https://qt-mirror.dannhauer.de/online/qtsdkrepository/windows_x86/desktop/qt5_5150/qt.qt5.5150.win64_msvc2019_64/5.15.0-0-202005150700qttools-Windows-Windows_10-MSVC2019-Windows-Windows_10-X86_64.7z'
- curl.exe -LJ -o qt-jom.zip 'https://qt-mirror.dannhauer.de/official_releases/jom/jom.zip'
- _fold_final_
- _fold_start_ 'extracting the downloaded qt binaries'
- 7z x qt-base.7z '-o../_qt' -y
- 7z x qt-tools.7z '-o../_qt' -y
- 7z x qt-jom.zip '-o../_qt' -y
- _fold_final_
- _fold_start_ 'turn the qt install from enterprise to foss; remove the licensing checks'
- ${qconfig-pri-folder} = '..\_qt\5.15.0\msvc2019_64\mkspecs\qconfig.pri'
- (Get-Content ${qconfig-pri-folder}).replace('QT_EDITION = Enterprise', 'QT_EDITION = OpenSource') | Set-Content ${qconfig-pri-folder}
- (Get-Content ${qconfig-pri-folder}).replace('QT_LICHECK = licheck.exe', '') | Set-Content ${qconfig-pri-folder}
- Pop-Location
- _fold_final_
- _fold_start_ 'run qmake and generate the msvc nmake makefile'
- mkdir _build; cd _build
- ..\_qt\5.15.0\msvc2019_64\bin\qmake ..\OpenRGB.pro
- _fold_final_
- _fold_start_ 'start the actual build with jom instead of nmake; for speed'
- ..\_qt\jom
- _fold_final_
- _fold_start_ 'run windeployqt to automatically copy the needed dll files'
- ..\_qt\5.15.0\msvc2019_64\bin\windeployqt --no-angle --no-translations --no-opengl-sw --no-system-d3d-compiler --no-compiler-runtime --no-webkit2 .\release\
- _fold_final_
- _fold_start_ 'Moving results for upload'
- mv release ../'OpenRGB Windows 64-bit'
- _fold_final_

@ -0,0 +1,211 @@
/*-----------------------------------------*\
| NanoleafController.cpp |
| |
| API Interface for Nanoleaf devices |
| |
| Nikita Rushmanov 01/13/2022 |
\*-----------------------------------------*/
#include "NanoleafController.h"
#include "LogManager.h"
#include <curl/curl.h>
std::size_t WriteMemoryCallback(const char* in, std::size_t size, std::size_t num, std::string* out)
{
const std::size_t totalBytes(size * num);
out->append(in, totalBytes);
return totalBytes;
}
long APIRequest(std::string method, std::string location, std::string URI, json* request_data = nullptr, json* response_data = nullptr)
{
const std::string url("http://"+location+URI);
CURL* curl = curl_easy_init();
// Set remote URL.
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.c_str());
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
// Don't bother trying IPv6, which would increase DNS resolution time.
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
// Don't wait forever, time out after 10 seconds.
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10);
// Follow HTTP redirects if necessary.
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
if(request_data)
{
// LOG_DEBUG("[Nanoleaf] Sending data: %s", request_data->dump().c_str());
curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, request_data->dump().c_str());
}
// Response information.
long httpCode(0);
std::unique_ptr<std::string> httpData(new std::string());
// Hook up data handling function.
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
/*---------------------------------------------------------*\
| Hook up data container (will be passed as the last |
| parameter to the callback handling function). Can be any |
| pointer type, since it will internally be passed as a |
| void pointer. |
\*---------------------------------------------------------*/
curl_easy_setopt(curl, CURLOPT_WRITEDATA, httpData.get());
// Run our HTTP GET command, capture the HTTP response code, and clean up.
curl_easy_perform(curl);
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
curl_easy_cleanup(curl);
if (httpCode/100 == 2)
{
if(response_data)
{
*response_data = json::parse(*httpData.get());
}
}
else
{
LOG_DEBUG("[Nanoleaf] HTTP %i:Could not %s from %s", httpCode, method, url);
}
return httpCode;
}
NanoleafController::NanoleafController(std::string a_address, int a_port, std::string a_auth_token)
{
address = a_address;
port = a_port;
auth_token = a_auth_token;
location = address+":"+std::to_string(port);
json data;
if(APIRequest("GET", location, "/api/v1/"+auth_token, nullptr, &data) == 200)
{
name = data["name"];
serial = data["serialNo"];
manufacturer = data["manufacturer"];
firmware_version = data["firmwareVersion"];
model = data["model"];
brightness = data["state"]["brightness"]["value"];
selectedEffect = data["effects"]["select"];
for(json::const_iterator it = data["effects"]["effectsList"].begin(); it != data["effects"]["effectsList"].end(); ++it)
{
effects.push_back(it.value());
}
for(json::const_iterator it = data["panelLayout"]["layout"]["positionData"].begin(); it != data["panelLayout"]["layout"]["positionData"].end(); ++it)
{
panel_ids.push_back(it.value()["panelId"].get<int>());
}
}
else
{
throw std::exception();
}
}
std::string NanoleafController::Pair(std::string address, int port)
{
const std::string location = address+":"+std::to_string(port);
json data;
if(APIRequest("POST", location, "/api/v1/new", nullptr, &data) == 200)
{
return data["auth_token"];
}
else
{
throw std::exception();
}
}
void NanoleafController::Unpair(std::string address, int port, std::string auth_token)
{
const std::string location = address+":"+std::to_string(port);
// We really don't care if this fails.
APIRequest("DELETE", location, "/api/v1/"+auth_token, nullptr, nullptr);
}
void NanoleafController::UpdateLEDs(std::vector<RGBColor>& colors)
{
// Requires StartExternalControl() to have been called prior.
if(model == NANOLEAF_LIGHT_PANELS_MODEL)
{
uint8_t size = panel_ids.size();
uint8_t* message = (uint8_t*)malloc(size*7+6+1);
message[0] = (uint8_t)size;
for (int i = 0; i < size; i++)
{
message[7*i+0+1] = (uint8_t)panel_ids[i];
message[7*i+1+1] = (uint8_t)1;
message[7*i+2+1] = (uint8_t)RGBGetRValue(colors[i]);
message[7*i+3+1] = (uint8_t)RGBGetGValue(colors[i]);
message[7*i+4+1] = (uint8_t)RGBGetBValue(colors[i]);
message[7*i+5+1] = (uint8_t)0;
message[7*i+6+1] = (uint8_t)0;
}
external_control_socket.udp_write(reinterpret_cast<char*>(message), size*7+6+1);
}
else if(model == NANOLEAF_CANVAS_MODEL)
{
// Insert V2 protocol implementation here.
}
}
void NanoleafController::StartExternalControl()
{
json request;
request["write"]["command"] = "display";
request["write"]["animType"] = "extControl";
if(model == NANOLEAF_LIGHT_PANELS_MODEL)
{
request["write"]["extControlVersion"] = "v1";
}
else if(model == NANOLEAF_CANVAS_MODEL)
{
request["write"]["extControlVersion"] = "v2";
}
json response;
if(APIRequest("PUT", location, "/api/v1/"+auth_token+"/effects", &request, &response)/100 == 2)
{
external_control_socket.udp_client(response["streamControlIpAddr"].get<std::string>().c_str(), std::to_string(response["streamControlPort"].get<int>()).c_str());
selectedEffect = NANOLEAF_DIRECT_MODE_EFFECT_NAME;
}
}
void NanoleafController::SelectEffect(std::string effect_name)
{
json request;
request["select"] = effect_name;
if(APIRequest("PUT", location, "/api/v1/"+auth_token+"/effects", &request)/100 == 2)
{
selectedEffect = effect_name;
}
}
void NanoleafController::SetBrightness(int a_brightness)
{
json request;
request["brightness"]["value"] = a_brightness;
if(APIRequest("PUT", location, "/api/v1/"+auth_token+"/state", &request)/100 == 2)
{
brightness = a_brightness;
}
}

@ -0,0 +1,62 @@
/*-----------------------------------------*\
| NanoleafController.h |
| |
| API Interface for Nanoleaf devices |
| |
| Nikita Rushmanov 01/13/2022 |
\*-----------------------------------------*/
#pragma once
#include "RGBController.h"
#include "net_port.h"
#define NANOLEAF_DIRECT_MODE_EFFECT_NAME "*Dynamic*"
#define NANOLEAF_LIGHT_PANELS_MODEL "NL22"
#define NANOLEAF_CANVAS_MODEL "NL29"
class NanoleafController
{
public:
static std::string Pair(std::string address, int port);
static void Unpair(std::string address, int port, std::string auth_token);
NanoleafController(std::string a_address, int a_port, std::string a_auth_token);
void SelectEffect(std::string effect_name);
void StartExternalControl();
void SetBrightness(int a_brightness);
// Requires External Control to have been started.
void UpdateLEDs(std::vector<RGBColor>& colors);
std::string GetAuthToken() { return auth_token; };
std::string GetName() { return name; };
std::string GetSerial() { return serial; };
std::string GetManufacturer() { return manufacturer; };
std::string GetFirmwareVersion() { return firmware_version; };
std::string GetModel() { return model; };
std::vector<std::string>& GetEffects() { return effects; };
std::vector<int>& GetPanelIds() { return panel_ids; };
std::string GetSelectedEffect() { return selectedEffect; };
int GetBrightness() { return brightness; };
private:
net_port external_control_socket;
std::string address;
int port;
std::string location;
std::string auth_token;
std::string name;
std::string serial;
std::string manufacturer;
std::string firmware_version;
std::string model;
std::vector<std::string> effects;
std::vector<int> panel_ids;
std::string selectedEffect;
int brightness;
};

@ -0,0 +1,40 @@
#include "Detector.h"
#include "RGBController_Nanoleaf.h"
#include "SettingsManager.h"
#include "LogManager.h"
/*----------------------------------------------------------------------------------------*\
| |
| DetectNanoleafControllers |
| |
| Connect to paired Nanoleaf devices |
| |
\*----------------------------------------------------------------------------------------*/
void DetectNanoleafControllers(std::vector<RGBController*> &rgb_controllers)
{
json nanoleaf_settings = ResourceManager::get()->GetSettingsManager()->GetSettings("NanoleafDevices");
if(nanoleaf_settings.contains("devices"))
{
for(json::const_iterator it = nanoleaf_settings["devices"].begin(); it != nanoleaf_settings["devices"].end(); ++it)
{
const json& device = it.value();
if(device.contains("ip") && device.contains("port") && device.contains("auth_token"))
{
try
{
RGBController_Nanoleaf* rgb_controller = new RGBController_Nanoleaf(device["ip"], device["port"], device["auth_token"]);
rgb_controllers.push_back(rgb_controller);
}
catch(...)
{
LOG_DEBUG("[Nanoleaf] Could not connect to device at %s:%s using auth_token %s", device["ip"], device["port"], device["auth_token"]);
}
}
}
}
} /* DetectNanoleafControllers() */
REGISTER_DETECTOR("Nanoleaf", DetectNanoleafControllers);

@ -0,0 +1,139 @@
/*-----------------------------------------*\
| RGBController_Nanoleaf.cpp |
| |
| Generic RGB Interface for Nanoleaf |
| |
| Nikita Rushmanov 01/13/2022 |
\*-----------------------------------------*/
#include "RGBController_Nanoleaf.h"
#include "ResourceManager.h"
#include "LogManager.h"
#include <curl/curl.h>
#include "json.hpp"
using json = nlohmann::json;
RGBController_Nanoleaf::RGBController_Nanoleaf(std::string a_address, int a_port, std::string a_auth_token) :
nanoleaf(a_address, a_port, a_auth_token)
{
location = a_address+":"+std::to_string(a_port);
name = nanoleaf.GetName();
serial = nanoleaf.GetSerial();
vendor = nanoleaf.GetManufacturer();
version = nanoleaf.GetFirmwareVersion();
description = nanoleaf.GetModel();
type = DEVICE_TYPE_LIGHT;
// Direct mode currently only supported for Nanoleaf Panels.
if(nanoleaf.GetModel() == NANOLEAF_LIGHT_PANELS_MODEL)
{
mode Direct;
Direct.name = "Direct";
Direct.flags = MODE_FLAG_HAS_PER_LED_COLOR;
Direct.color_mode = MODE_COLORS_PER_LED;
modes.push_back(Direct);
// Set this effect as current if the name is selected.
if(nanoleaf.GetSelectedEffect() == NANOLEAF_DIRECT_MODE_EFFECT_NAME)
{
// If the direct mode is active, we need to call this method to open the socket.
nanoleaf.StartExternalControl();
active_mode = 0;
}
}
for(std::vector<std::string>::const_iterator it = nanoleaf.GetEffects().begin(); it != nanoleaf.GetEffects().end(); ++it)
{
mode effect;
effect.name = *it;
effect.flags = MODE_FLAG_HAS_BRIGHTNESS;
effect.color_mode = MODE_COLORS_NONE;
effect.brightness_max = 100;
effect.brightness_min = 0;
effect.brightness = 100;
modes.push_back(effect);
// Set this effect as current if the name is selected.
if(nanoleaf.GetSelectedEffect() == effect.name)
{
active_mode = modes.size() - 1;
}
}
SetupZones();
}
void RGBController_Nanoleaf::SetupZones()
{
zone led_zone;
led_zone.name = "Nanoleaf Layout";
led_zone.type = ZONE_TYPE_LINEAR;
led_zone.leds_count = nanoleaf.GetPanelIds().size();
led_zone.leds_min = led_zone.leds_count;
led_zone.leds_max = led_zone.leds_count;
led_zone.matrix_map = NULL;
for(std::vector<int>::const_iterator it = nanoleaf.GetPanelIds().begin(); it != nanoleaf.GetPanelIds().end(); ++it)
{
led new_led;
new_led.name = std::to_string(*it);
leds.push_back(new_led);
}
zones.push_back(led_zone);
SetupColors();
}
void RGBController_Nanoleaf::ResizeZone(int /*zone*/, int /*new_size*/)
{
/*---------------------------------------------------------*\
| This device does not support resizing zones |
\*---------------------------------------------------------*/
}
void RGBController_Nanoleaf::DeviceUpdateLEDs()
{
if(nanoleaf.GetModel() == NANOLEAF_LIGHT_PANELS_MODEL)
{
nanoleaf.UpdateLEDs(colors);
}
}
void RGBController_Nanoleaf::UpdateZoneLEDs(int /*zone*/)
{
DeviceUpdateLEDs();
}
void RGBController_Nanoleaf::UpdateSingleLED(int /*led*/)
{
DeviceUpdateLEDs();
}
void RGBController_Nanoleaf::SetCustomMode()
{
if(nanoleaf.GetModel() == NANOLEAF_LIGHT_PANELS_MODEL)
{
// Put the Nanoleaf into direct mode.
nanoleaf.StartExternalControl();
}
}
void RGBController_Nanoleaf::DeviceUpdateMode()
{
// 0 mode is reserved for Direct mode
if(active_mode == 0 && nanoleaf.GetModel() == NANOLEAF_LIGHT_PANELS_MODEL)
{
nanoleaf.StartExternalControl();
}
// Update normal effects.
else
{
// Select effect.
nanoleaf.SelectEffect(modes[active_mode].name);
// Update brightness.
nanoleaf.SetBrightness(modes[active_mode].brightness);
}
}

@ -0,0 +1,32 @@
/*-----------------------------------------*\
| RGBController_Nanoleaf.h |
| |
| Generic RGB Interface for Nanoleaf |
| |
| Nikita Rushmanov 01/13/2022 |
\*-----------------------------------------*/
#pragma once
#include "RGBController.h"
#include "NanoleafController.h"
class RGBController_Nanoleaf : public RGBController
{
public:
RGBController_Nanoleaf(std::string a_address, int a_port, std::string a_auth_token);
void SetupZones();
void ResizeZone(int zone, int new_size);
void DeviceUpdateLEDs();
void UpdateZoneLEDs(int zone);
void UpdateSingleLED(int led);
void SetCustomMode();
void DeviceUpdateMode();
private:
NanoleafController nanoleaf;
};

@ -64,6 +64,7 @@ INCLUDEPATH +=
dependencies/json/ \
dependencies/libe131/src/ \
dependencies/libcmmk/include/ \
dependencies/mdns \
i2c_smbus/ \
i2c_tools/ \
net_port/ \
@ -130,6 +131,7 @@ INCLUDEPATH +=
Controllers/MSIMysticLightController/ \
Controllers/MSIOptixController/ \
Controllers/MSIRGBController/ \
Controllers/NanoleafController/ \
Controllers/NZXTHue2Controller/ \
Controllers/NZXTHuePlusController/ \
Controllers/NZXTKrakenController/ \
@ -207,6 +209,9 @@ HEADERS +=
qt/OpenRGBE131SettingsPage/OpenRGBE131SettingsPage.h \
qt/OpenRGBLIFXSettingsPage/OpenRGBLIFXSettingsEntry.h \
qt/OpenRGBLIFXSettingsPage/OpenRGBLIFXSettingsPage.h \
qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsEntry.h \
qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.h \
qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafScanningThread.h \
qt/OpenRGBPhilipsHueSettingsPage/OpenRGBPhilipsHueSettingsEntry.h \
qt/OpenRGBPhilipsHueSettingsPage/OpenRGBPhilipsHueSettingsPage.h \
qt/OpenRGBPhilipsWizSettingsPage/OpenRGBPhilipsWizSettingsEntry.h \
@ -290,7 +295,7 @@ HEADERS +=
Controllers/CorsairLightingNodeController/CorsairLightingNodeController.h \
Controllers/CorsairLightingNodeController/RGBController_CorsairLightingNode.h \
Controllers/CorsairPeripheralController/CorsairPeripheralController.h \
Controllers/CorsairPeripheralController/CorsairK100Controller.h \
Controllers/CorsairPeripheralController/CorsairK100Controller.h \
Controllers/CorsairPeripheralController/CorsairK55RGBPROController.h \
Controllers/CorsairPeripheralController/CorsairK65MiniController.h \
Controllers/CorsairPeripheralController/RGBController_CorsairPeripheral.h \
@ -324,7 +329,7 @@ HEADERS +=
Controllers/ENESMBusController/ENESMBusController.h \
Controllers/ENESMBusController/RGBController_ENESMBus.h \
Controllers/ENESMBusController/ENESMBusInterface/ENESMBusInterface.h \
Controllers/ENESMBusController/ENESMBusInterface/ENESMBusInterface_i2c_smbus.h \
Controllers/ENESMBusController/ENESMBusInterface/ENESMBusInterface_i2c_smbus.h \
Controllers/EspurnaController/EspurnaController.h \
Controllers/EspurnaController/RGBController_Espurna.h \
Controllers/EVGAGP102GPUController/EVGAGP102Controller.h \
@ -406,7 +411,7 @@ HEADERS +=
Controllers/LogitechController/LogitechG560Controller.h \
Controllers/LogitechController/LogitechG933Controller.h \
Controllers/LogitechController/LogitechG810Controller.h \
Controllers/LogitechController/LogitechGProKeyboardController.h \
Controllers/LogitechController/LogitechGProKeyboardController.h \
Controllers/LogitechController/LogitechG910Controller.h \
Controllers/LogitechController/LogitechG815Controller.h \
Controllers/LogitechController/LogitechG915Controller.h \
@ -439,8 +444,10 @@ HEADERS +=
Controllers/MSIOptixController/MSIOptixController.h \
Controllers/MSIOptixController/RGBController_MSIOptix.h \
Controllers/MSIRGBController/MSIRGBController.h \
Controllers/NanoleafController/NanoleafController.h \
Controllers/MSIRGBController/RGBController_MSIRGB.h \
Controllers/NvidiaESAController/NvidiaESAController.h \
Controllers/NanoleafController/RGBController_Nanoleaf.h \
Controllers/NvidiaESAController/RGBController_NvidiaESA.h \
Controllers/NZXTHue2Controller/NZXTHue2Controller.h \
Controllers/NZXTHue2Controller/RGBController_NZXTHue2.h \
@ -631,6 +638,9 @@ SOURCES +=
qt/OpenRGBE131SettingsPage/OpenRGBE131SettingsPage.cpp \
qt/OpenRGBLIFXSettingsPage/OpenRGBLIFXSettingsEntry.cpp \
qt/OpenRGBLIFXSettingsPage/OpenRGBLIFXSettingsPage.cpp \
qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsEntry.cpp \
qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.cpp \
qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafScanningThread.cpp \
qt/OpenRGBPhilipsHueSettingsPage/OpenRGBPhilipsHueSettingsEntry.cpp \
qt/OpenRGBPhilipsHueSettingsPage/OpenRGBPhilipsHueSettingsPage.cpp \
qt/OpenRGBPhilipsWizSettingsPage/OpenRGBPhilipsWizSettingsEntry.cpp \
@ -773,7 +783,7 @@ SOURCES +=
Controllers/ENESMBusController/ENESMBusController.cpp \
Controllers/ENESMBusController/ENESMBusControllerDetect.cpp \
Controllers/ENESMBusController/RGBController_ENESMBus.cpp \
Controllers/ENESMBusController/ENESMBusInterface/ENESMBusInterface_i2c_smbus.cpp \
Controllers/ENESMBusController/ENESMBusInterface/ENESMBusInterface_i2c_smbus.cpp \
Controllers/EspurnaController/EspurnaController.cpp \
Controllers/EspurnaController/EspurnaControllerDetect.cpp \
Controllers/EspurnaController/RGBController_Espurna.cpp \
@ -922,6 +932,9 @@ SOURCES +=
Controllers/MSIRGBController/MSIRGBController.cpp \
Controllers/MSIRGBController/MSIRGBControllerDetect.cpp \
Controllers/MSIRGBController/RGBController_MSIRGB.cpp \
Controllers/NanoleafController/NanoleafController.cpp \
Controllers/NanoleafController/NanoleafControllerDetect.cpp \
Controllers/NanoleafController/RGBController_Nanoleaf.cpp \
Controllers/NvidiaESAController/NvidiaESAController.cpp \
Controllers/NvidiaESAController/NvidiaESAControllerDetect.cpp \
Controllers/NvidiaESAController/RGBController_NvidiaESA.cpp \
@ -1088,6 +1101,8 @@ FORMS +=
qt/OpenRGBE131SettingsPage/OpenRGBE131SettingsPage.ui \
qt/OpenRGBLIFXSettingsPage/OpenRGBLIFXSettingsEntry.ui \
qt/OpenRGBLIFXSettingsPage/OpenRGBLIFXSettingsPage.ui \
qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.ui \
qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsEntry.ui \
qt/OpenRGBPhilipsHueSettingsPage/OpenRGBPhilipsHueSettingsEntry.ui \
qt/OpenRGBPhilipsHueSettingsPage/OpenRGBPhilipsHueSettingsPage.ui \
qt/OpenRGBPhilipsWizSettingsPage/OpenRGBPhilipsWizSettingsEntry.ui \
@ -1101,6 +1116,9 @@ FORMS +=
qt/OpenRGBZonesBulkResizer.ui \
qt/TabLabel.ui \
LIBS += \
-lcurl \
#-----------------------------------------------------------------------------------------------#
# Windows-specific Configuration #
#-----------------------------------------------------------------------------------------------#
@ -1540,7 +1558,7 @@ macx {
HEADERS += \
AutoStart/AutoStart-MacOS.h \
qt/macutils.h \
SOURCES += \
dependencies/hueplusplus-1.0.0/src/LinHttpHandler.cpp \
serial_port/find_usb_serial_port_linux.cpp \

@ -289,7 +289,7 @@ There have been two instances of hardware damage in OpenRGB's development and we
* Effects Engine Plugin (by herosilas12, morg): https://gitlab.com/OpenRGBDevelopers/OpenRGBEffectsPlugin
* OpenRGB Visual Map Plugin (by morg): https://gitlab.com/OpenRGBDevelopers/OpenRGBVisualMapPlugin
* Scheduler Plugin (by morg): https://gitlab.com/OpenRGBDevelopers/OpenRGBSchedulerPlugin
* Skin Plugin (by morg): https://gitlab.com/OpenRGBDevelopers/openrgbskinplugin
* Skin Plugin (by morg): https://gitlab.com/OpenRGBDevelopers/openrgbskinplugin
* Hardware Sync Plugin (by morg): https://gitlab.com/OpenRGBDevelopers/OpenRGBHardwareSyncPlugin
* Http Hook Plugin (by morg): https://gitlab.com/OpenRGBDevelopers/OpenRGBHttpHookPlugin
* Razer extras Plugin (by morg): https://gitlab.com/OpenRGBDevelopers/OpenRGBRazerExtrasPlugin
@ -308,7 +308,9 @@ There have been two instances of hardware damage in OpenRGB's development and we
* AMD ADL Libraries: https://github.com/GPUOpen-LibrariesAndSDKs/display-library
* libcmmk: https://github.com/chmod222/libcmmk
* hueplusplus: https://github.com/enwi/hueplusplus
* httplib: https://github.com/yhirose/cpp-httplib
* mdns: https://github.com/mjansson/mdns
## Projects Researched
While no code from these projects directly made its way into OpenRGB, these projects have been invaluable resources for protocol information.

File diff suppressed because it is too large Load Diff

@ -174,14 +174,14 @@ OpenRGBDialog2::OpenRGBDialog2(QWidget *parent) : QMainWindow(parent), ui(new Op
if(!ui_settings.contains("geometry"))
{
json geometry_settings;
geometry_settings["load_geometry"] = false;
geometry_settings["save_on_exit"] = false;
geometry_settings["x"] = 0;
geometry_settings["y"] = 0;
geometry_settings["width"] = 0;
geometry_settings["height"] = 0;
ui_settings["geometry"] = geometry_settings;
settings_manager->SetSettings(ui_string, ui_settings);
@ -192,7 +192,7 @@ OpenRGBDialog2::OpenRGBDialog2(QWidget *parent) : QMainWindow(parent), ui(new Op
| If geometry information exists in settings, apply it |
\*-----------------------------------------------------*/
bool load_geometry = false;
if(ui_settings["geometry"].contains("load_geometry"))
{
load_geometry = ui_settings["geometry"]["load_geometry"].get<bool>();
@ -422,13 +422,18 @@ OpenRGBDialog2::OpenRGBDialog2(QWidget *parent) : QMainWindow(parent), ui(new Op
\*-----------------------------------------------------*/
AddYeelightSettingsPage();
/*-----------------------------------------------------*\
| Add the Nanoleaf settings page |
\*-----------------------------------------------------*/
AddNanoleafSettingsPage();
/*-----------------------------------------------------*\
| Add the SMBus Tools page if enabled |
\*-----------------------------------------------------*/
if(ShowI2CTools)
{
AddI2CToolsPage();
}
}
/*-----------------------------------------------------*\
| If log console is enabled in settings, enable it |
@ -813,6 +818,34 @@ void OpenRGBDialog2::AddYeelightSettingsPage()
ui->SettingsTabBar->tabBar()->setTabButton(ui->SettingsTabBar->tabBar()->count() - 1, QTabBar::LeftSide, SettingsTabLabel);
}
void OpenRGBDialog2::AddNanoleafSettingsPage()
{
/*-----------------------------------------------------*\
| Create the Settings page |
\*-----------------------------------------------------*/
NanoleafSettingsPage = new OpenRGBNanoleafSettingsPage();
ui->SettingsTabBar->addTab(NanoleafSettingsPage, "");
QString SettingsLabelString;
if(OpenRGBThemeManager::IsDarkTheme())
{
SettingsLabelString = "light_dark.png";
}
else
{
SettingsLabelString = "light.png";
}
/*-----------------------------------------------------*\
| Create the tab label |
\*-----------------------------------------------------*/
TabLabel* SettingsTabLabel = new TabLabel(SettingsLabelString, "Nanoleaf Devices");
ui->SettingsTabBar->tabBar()->setTabButton(ui->SettingsTabBar->tabBar()->count() - 1, QTabBar::LeftSide, SettingsTabLabel);
}
void OpenRGBDialog2::AddPlugin(OpenRGBPluginEntry* plugin)
{
/*-----------------------------------------------------*\

@ -16,6 +16,7 @@
#include "OpenRGBQMKORGBSettingsPage/OpenRGBQMKORGBSettingsPage.h"
#include "OpenRGBSerialSettingsPage/OpenRGBSerialSettingsPage.h"
#include "OpenRGBYeelightSettingsPage/OpenRGBYeelightSettingsPage.h"
#include "OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.h"
#include "PluginManager.h"
#include <vector>
@ -78,6 +79,7 @@ private:
OpenRGBQMKORGBSettingsPage *QMKORGBSettingsPage;
OpenRGBSerialSettingsPage *SerialSettingsPage;
OpenRGBYeelightSettingsPage *YeelightSettingsPage;
OpenRGBNanoleafSettingsPage *NanoleafSettingsPage;
bool ShowI2CTools = false;
@ -103,6 +105,7 @@ private:
void AddQMKORGBSettingsPage();
void AddSerialSettingsPage();
void AddYeelightSettingsPage();
void AddNanoleafSettingsPage();
void AddPluginsPage();
void AddConsolePage();

@ -0,0 +1,438 @@
#ifdef _WIN32
#define _CRT_SECURE_NO_WARNINGS 1
#endif
#ifdef _WIN32
#include <winsock2.h>
#include <iphlpapi.h>
#else
#include <netdb.h>
#include <ifaddrs.h>
#endif
#include "mdns.h"
#include "OpenRGBNanoleafScanningThread.h"
static char namebuffer[256];
static struct sockaddr_in service_address_ipv4;
static struct sockaddr_in6 service_address_ipv6;
static int has_ipv4;
static int has_ipv6;
static mdns_string_t ipv4_address_to_string(char* buffer, size_t capacity, const struct sockaddr_in* addr, size_t addrlen)
{
char host[NI_MAXHOST] = {0};
char service[NI_MAXSERV] = {0};
int ret = getnameinfo((const struct sockaddr*)addr, (socklen_t)addrlen, host, NI_MAXHOST,
service, NI_MAXSERV, NI_NUMERICSERV | NI_NUMERICHOST);
int len = 0;
if (ret == 0)
{
len = snprintf(buffer, capacity, "%s", host);
}
if (len >= (int)capacity)
len = (int)capacity - 1;
mdns_string_t str;
str.str = buffer;
str.length = len;
return str;
}
static mdns_string_t ipv6_address_to_string(char* buffer, size_t capacity, const struct sockaddr_in6* addr, size_t addrlen)
{
char host[NI_MAXHOST] = {0};
char service[NI_MAXSERV] = {0};
int ret = getnameinfo((const struct sockaddr*)addr, (socklen_t)addrlen, host, NI_MAXHOST,
service, NI_MAXSERV, NI_NUMERICSERV | NI_NUMERICHOST);
int len = 0;
if (ret == 0)
{
if (addr->sin6_port != 0)
len = snprintf(buffer, capacity, "[%s]:%s", host, service);
else
len = snprintf(buffer, capacity, "%s", host);
}
if (len >= (int)capacity)
len = (int)capacity - 1;
mdns_string_t str;
str.str = buffer;
str.length = len;
return str;
}
/*-----------------------------------------------------*\
| Open sockets for sending one-shot multicast queries |
| from an ephemeral port |
\*-----------------------------------------------------*/
static int open_client_sockets(int* sockets, int max_sockets, int port)
{
/*-----------------------------------------------------*\
| When sending, each socket can only send to one |
| network interface from an ephemeral port, thus we |
| need to open one socket for each interface and |
| address family |
\*-----------------------------------------------------*/
int num_sockets = 0;
#ifdef _WIN32
IP_ADAPTER_ADDRESSES* adapter_address = 0;
ULONG address_size = 8000;
unsigned int ret;
unsigned int num_retries = 4;
do
{
adapter_address = (IP_ADAPTER_ADDRESSES*)malloc(address_size);
ret = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_ANYCAST, 0,
adapter_address, &address_size);
if (ret == ERROR_BUFFER_OVERFLOW)
{
free(adapter_address);
adapter_address = 0;
address_size *= 2;
}
else
{
break;
}
}
while (num_retries-- > 0);
if (!adapter_address || (ret != NO_ERROR))
{
free(adapter_address);
return num_sockets;
}
int first_ipv4 = 1;
int first_ipv6 = 1;
for (PIP_ADAPTER_ADDRESSES adapter = adapter_address; adapter; adapter = adapter->Next)
{
if (adapter->TunnelType == TUNNEL_TYPE_TEREDO)
continue;
if (adapter->OperStatus != IfOperStatusUp)
continue;
for (IP_ADAPTER_UNICAST_ADDRESS* unicast = adapter->FirstUnicastAddress; unicast;
unicast = unicast->Next)
{
if (unicast->Address.lpSockaddr->sa_family == AF_INET)
{
struct sockaddr_in* saddr = (struct sockaddr_in*)unicast->Address.lpSockaddr;
if ((saddr->sin_addr.S_un.S_un_b.s_b1 != 127) ||
(saddr->sin_addr.S_un.S_un_b.s_b2 != 0) ||
(saddr->sin_addr.S_un.S_un_b.s_b3 != 0) ||
(saddr->sin_addr.S_un.S_un_b.s_b4 != 1))
{
int log_addr = 0;
if (first_ipv4)
{
service_address_ipv4 = *saddr;
first_ipv4 = 0;
log_addr = 1;
}
has_ipv4 = 1;
if (num_sockets < max_sockets)
{
saddr->sin_port = htons((unsigned short)port);
int sock = mdns_socket_open_ipv4(saddr);
if (sock >= 0)
{
sockets[num_sockets++] = sock;
log_addr = 1;
}
else
{
log_addr = 0;
}
}
if (log_addr)
{
char buffer[128];
mdns_string_t addr = ipv4_address_to_string(buffer, sizeof(buffer), saddr,
sizeof(struct sockaddr_in));
}
}
}
else if (unicast->Address.lpSockaddr->sa_family == AF_INET6)
{
struct sockaddr_in6* saddr = (struct sockaddr_in6*)unicast->Address.lpSockaddr;
static const unsigned char localhost[] = {0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1};
static const unsigned char localhost_mapped[] = {0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0xff, 0xff, 0x7f, 0, 0, 1};
if ((unicast->DadState == NldsPreferred) &&
memcmp(saddr->sin6_addr.s6_addr, localhost, 16) &&
memcmp(saddr->sin6_addr.s6_addr, localhost_mapped, 16))
{
int log_addr = 0;
if (first_ipv6)
{
service_address_ipv6 = *saddr;
first_ipv6 = 0;
log_addr = 1;
}
has_ipv6 = 1;
if (num_sockets < max_sockets)
{
saddr->sin6_port = htons((unsigned short)port);
int sock = mdns_socket_open_ipv6(saddr);
if (sock >= 0)
{
sockets[num_sockets++] = sock;
log_addr = 1;
}
else
{
log_addr = 0;
}
}
if (log_addr)
{
char buffer[128];
mdns_string_t addr = ipv6_address_to_string(buffer, sizeof(buffer), saddr,
sizeof(struct sockaddr_in6));
}
}
}
}
}
free(adapter_address);
#else
struct ifaddrs* ifaddr = 0;
struct ifaddrs* ifa = 0;
getifaddrs(&ifaddr);
int first_ipv4 = 1;
int first_ipv6 = 1;
for (ifa = ifaddr; ifa; ifa = ifa->ifa_next)
{
if (!ifa->ifa_addr)
continue;
if (ifa->ifa_addr->sa_family == AF_INET)
{
struct sockaddr_in* saddr = (struct sockaddr_in*)ifa->ifa_addr;
if (saddr->sin_addr.s_addr != htonl(INADDR_LOOPBACK))
{
int log_addr = 0;
if (first_ipv4)
{
service_address_ipv4 = *saddr;
first_ipv4 = 0;
log_addr = 1;
}
has_ipv4 = 1;
if (num_sockets < max_sockets)
{
saddr->sin_port = htons(port);
int sock = mdns_socket_open_ipv4(saddr);
if (sock >= 0)
{
sockets[num_sockets++] = sock;
log_addr = 1;
}
else
{
log_addr = 0;
}
}
if (log_addr)
{
char buffer[128];
mdns_string_t addr = ipv4_address_to_string(buffer, sizeof(buffer), saddr,
sizeof(struct sockaddr_in));
}
}
}
else if (ifa->ifa_addr->sa_family == AF_INET6)
{
struct sockaddr_in6* saddr = (struct sockaddr_in6*)ifa->ifa_addr;
static const unsigned char localhost[] = {0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1};
static const unsigned char localhost_mapped[] = {0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0xff, 0xff, 0x7f, 0, 0, 1};
if (memcmp(saddr->sin6_addr.s6_addr, localhost, 16) &&
memcmp(saddr->sin6_addr.s6_addr, localhost_mapped, 16))
{
int log_addr = 0;
if (first_ipv6)
{
service_address_ipv6 = *saddr;
first_ipv6 = 0;
log_addr = 1;
}
has_ipv6 = 1;
if (num_sockets < max_sockets)
{
saddr->sin6_port = htons(port);
int sock = mdns_socket_open_ipv6(saddr);
if (sock >= 0)
{
sockets[num_sockets++] = sock;
log_addr = 1;
}
else
{
log_addr = 0;
}
}
if (log_addr)
{
char buffer[128];
mdns_string_t addr = ipv6_address_to_string(buffer, sizeof(buffer), saddr,
sizeof(struct sockaddr_in6));
}
}
}
}
freeifaddrs(ifaddr);
#endif
return num_sockets;
}
/*-----------------------------------------------------*\
| Callback handling parsing answers to queries sent |
\*-----------------------------------------------------*/
static int query_callback(
int sock,
[[maybe_unused]] const struct sockaddr* from,
[[maybe_unused]] size_t addrlen,
[[maybe_unused]] mdns_entry_type_t entry,
uint16_t query_id,
uint16_t rtype,
[[maybe_unused]] uint16_t rclass,
[[maybe_unused]] uint32_t ttl,
const void* data,
size_t size,
[[maybe_unused]] size_t name_offset,
size_t name_length,
size_t record_offset,
size_t record_length,
void* user_data)
{
(void)sizeof(sock);
(void)sizeof(query_id);
(void)sizeof(name_length);
(void)sizeof(user_data);
if (rtype == MDNS_RECORDTYPE_A)
{
struct sockaddr_in address;
mdns_record_parse_a(data, size, record_offset, record_length, &address);
if (address.sin_port == 0)
address.sin_port = 16021; // Default Nanoleaf port.
mdns_string_t addrstr =
ipv4_address_to_string(namebuffer, sizeof(namebuffer), &address, sizeof(address));
// printf("A %.*s:%u\n", MDNS_STRING_FORMAT(addrstr), address.sin_port);
(static_cast<OpenRGBNanoleafScanningThread*>(user_data))->EmitDeviceFound(addrstr.str, address.sin_port);
}
return 0;
}
void OpenRGBNanoleafScanningThread::EmitDeviceFound(QString address, int port)
{
emit DeviceFound(address, port);
}
/*-----------------------------------------------------*\
| Send a mDNS query |
\*-----------------------------------------------------*/
int OpenRGBNanoleafScanningThread::SendMDNSQuery()
{
const char* service = "_nanoleafapi._tcp.local.";
mdns_record_type record = MDNS_RECORDTYPE_PTR;
int sockets[32];
int query_id[32];
int num_sockets = open_client_sockets(sockets, sizeof(sockets) / sizeof(sockets[0]), 0);
if (num_sockets <= 0)
{
return -1;
}
size_t capacity = 2048;
void* buffer = malloc(capacity);
size_t records;
const char* record_name = "PTR";
if (record == MDNS_RECORDTYPE_SRV)
record_name = "SRV";
else if (record == MDNS_RECORDTYPE_A)
record_name = "A";
else if (record == MDNS_RECORDTYPE_AAAA)
record_name = "AAAA";
else
record = MDNS_RECORDTYPE_PTR;
for (int isock = 0; isock < num_sockets; ++isock)
{
query_id[isock] =
mdns_query_send(sockets[isock], record, service, strlen(service), buffer, capacity, 0);
}
/*-----------------------------------------------------*\
| This is a simple implementation that loops for |
| 5 seconds or as long as we get replies |
\*-----------------------------------------------------*/
int res;
do
{
struct timeval timeout;
timeout.tv_sec