diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f59af68e..1728de3d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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_ diff --git a/Controllers/NanoleafController/NanoleafController.cpp b/Controllers/NanoleafController/NanoleafController.cpp new file mode 100644 index 00000000..86a810a2 --- /dev/null +++ b/Controllers/NanoleafController/NanoleafController.cpp @@ -0,0 +1,211 @@ +/*-----------------------------------------*\ +| NanoleafController.cpp | +| | +| API Interface for Nanoleaf devices | +| | +| Nikita Rushmanov 01/13/2022 | +\*-----------------------------------------*/ + +#include "NanoleafController.h" +#include "LogManager.h" +#include + +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 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()); + } + } + 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& 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(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().c_str(), std::to_string(response["streamControlPort"].get()).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; + } +} diff --git a/Controllers/NanoleafController/NanoleafController.h b/Controllers/NanoleafController/NanoleafController.h new file mode 100644 index 00000000..37bcaabc --- /dev/null +++ b/Controllers/NanoleafController/NanoleafController.h @@ -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& 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& GetEffects() { return effects; }; + std::vector& 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 effects; + std::vector panel_ids; + + std::string selectedEffect; + int brightness; +}; diff --git a/Controllers/NanoleafController/NanoleafControllerDetect.cpp b/Controllers/NanoleafController/NanoleafControllerDetect.cpp new file mode 100644 index 00000000..55b6780f --- /dev/null +++ b/Controllers/NanoleafController/NanoleafControllerDetect.cpp @@ -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 &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); diff --git a/Controllers/NanoleafController/RGBController_Nanoleaf.cpp b/Controllers/NanoleafController/RGBController_Nanoleaf.cpp new file mode 100644 index 00000000..2bf8b1a5 --- /dev/null +++ b/Controllers/NanoleafController/RGBController_Nanoleaf.cpp @@ -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 +#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::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::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); + } +} diff --git a/Controllers/NanoleafController/RGBController_Nanoleaf.h b/Controllers/NanoleafController/RGBController_Nanoleaf.h new file mode 100644 index 00000000..368898fe --- /dev/null +++ b/Controllers/NanoleafController/RGBController_Nanoleaf.h @@ -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; +}; diff --git a/OpenRGB.pro b/OpenRGB.pro index e65d4e88..4a2c9bae 100644 --- a/OpenRGB.pro +++ b/OpenRGB.pro @@ -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 \ diff --git a/README.md b/README.md index 29d9a96a..25cb346a 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/dependencies/mdns/mdns.h b/dependencies/mdns/mdns.h new file mode 100644 index 00000000..73ca3809 --- /dev/null +++ b/dependencies/mdns/mdns.h @@ -0,0 +1,1539 @@ +/* mdns.h - mDNS/DNS-SD library - Public Domain - 2017 Mattias Jansson + * + * This library provides a cross-platform mDNS and DNS-SD library in C. + * The implementation is based on RFC 6762 and RFC 6763. + * + * The latest source code is always available at + * + * https://github.com/mjansson/mdns + * + * This library is put in the public domain; you can redistribute it and/or modify it without any + * restrictions. + * + */ + +#pragma once + +#include +#include +#include +#include + +#include +#ifdef _WIN32 +#include +#include +#define strncasecmp _strnicmp +#else +#include +#include +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#define MDNS_INVALID_POS ((size_t)-1) + +#define MDNS_STRING_CONST(s) (s), (sizeof((s)) - 1) +#define MDNS_STRING_ARGS(s) s.str, s.length +#define MDNS_STRING_FORMAT(s) (int)((s).length), s.str + +#define MDNS_POINTER_OFFSET(p, ofs) ((void*)((char*)(p) + (ptrdiff_t)(ofs))) +#define MDNS_POINTER_OFFSET_CONST(p, ofs) ((const void*)((const char*)(p) + (ptrdiff_t)(ofs))) +#define MDNS_POINTER_DIFF(a, b) ((size_t)((const char*)(a) - (const char*)(b))) + +#define MDNS_PORT 5353 +#define MDNS_UNICAST_RESPONSE 0x8000U +#define MDNS_CACHE_FLUSH 0x8000U +#define MDNS_MAX_SUBSTRINGS 64 + +enum mdns_record_type { + MDNS_RECORDTYPE_IGNORE = 0, + // Address + MDNS_RECORDTYPE_A = 1, + // Domain Name pointer + MDNS_RECORDTYPE_PTR = 12, + // Arbitrary text string + MDNS_RECORDTYPE_TXT = 16, + // IP6 Address [Thomson] + MDNS_RECORDTYPE_AAAA = 28, + // Server Selection [RFC2782] + MDNS_RECORDTYPE_SRV = 33, + // Any available records + MDNS_RECORDTYPE_ANY = 255 +}; + +enum mdns_entry_type { + MDNS_ENTRYTYPE_QUESTION = 0, + MDNS_ENTRYTYPE_ANSWER = 1, + MDNS_ENTRYTYPE_AUTHORITY = 2, + MDNS_ENTRYTYPE_ADDITIONAL = 3 +}; + +enum mdns_class { MDNS_CLASS_IN = 1, MDNS_CLASS_ANY = 255 }; + +typedef enum mdns_record_type mdns_record_type_t; +typedef enum mdns_entry_type mdns_entry_type_t; +typedef enum mdns_class mdns_class_t; + +typedef int (*mdns_record_callback_fn)(int sock, const struct sockaddr* from, size_t addrlen, + mdns_entry_type_t entry, uint16_t query_id, uint16_t rtype, + uint16_t rclass, uint32_t ttl, const void* data, size_t size, + size_t name_offset, size_t name_length, size_t record_offset, + size_t record_length, void* user_data); + +typedef struct mdns_string_t mdns_string_t; +typedef struct mdns_string_pair_t mdns_string_pair_t; +typedef struct mdns_string_table_item_t mdns_string_table_item_t; +typedef struct mdns_string_table_t mdns_string_table_t; +typedef struct mdns_record_t mdns_record_t; +typedef struct mdns_record_srv_t mdns_record_srv_t; +typedef struct mdns_record_ptr_t mdns_record_ptr_t; +typedef struct mdns_record_a_t mdns_record_a_t; +typedef struct mdns_record_aaaa_t mdns_record_aaaa_t; +typedef struct mdns_record_txt_t mdns_record_txt_t; + +#ifdef _WIN32 +typedef int mdns_size_t; +typedef int mdns_ssize_t; +#else +typedef size_t mdns_size_t; +typedef ssize_t mdns_ssize_t; +#endif + +struct mdns_string_t { + const char* str; + size_t length; +}; + +struct mdns_string_pair_t { + size_t offset; + size_t length; + int ref; +}; + +struct mdns_string_table_t { + size_t offset[16]; + size_t count; + size_t next; +}; + +struct mdns_record_srv_t { + uint16_t priority; + uint16_t weight; + uint16_t port; + mdns_string_t name; +}; + +struct mdns_record_ptr_t { + mdns_string_t name; +}; + +struct mdns_record_a_t { + struct sockaddr_in addr; +}; + +struct mdns_record_aaaa_t { + struct sockaddr_in6 addr; +}; + +struct mdns_record_txt_t { + mdns_string_t key; + mdns_string_t value; +}; + +struct mdns_record_t { + mdns_string_t name; + mdns_record_type_t type; + union mdns_record_data { + mdns_record_ptr_t ptr; + mdns_record_srv_t srv; + mdns_record_a_t a; + mdns_record_aaaa_t aaaa; + mdns_record_txt_t txt; + } data; +}; + +struct mdns_header_t { + uint16_t query_id; + uint16_t flags; + uint16_t questions; + uint16_t answer_rrs; + uint16_t authority_rrs; + uint16_t additional_rrs; +}; + +// mDNS/DNS-SD public API + +//! Open and setup a IPv4 socket for mDNS/DNS-SD. To bind the socket to a specific interface, pass +//! in the appropriate socket address in saddr, otherwise pass a null pointer for INADDR_ANY. To +//! send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign a +//! random user level ephemeral port. To run discovery service listening for incoming discoveries +//! and queries, you must set MDNS_PORT as port. +static inline int +mdns_socket_open_ipv4(const struct sockaddr_in* saddr); + +//! Setup an already opened IPv4 socket for mDNS/DNS-SD. To bind the socket to a specific interface, +//! pass in the appropriate socket address in saddr, otherwise pass a null pointer for INADDR_ANY. +//! To send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign a +//! random user level ephemeral port. To run discovery service listening for incoming discoveries +//! and queries, you must set MDNS_PORT as port. +static inline int +mdns_socket_setup_ipv4(int sock, const struct sockaddr_in* saddr); + +//! Open and setup a IPv6 socket for mDNS/DNS-SD. To bind the socket to a specific interface, pass +//! in the appropriate socket address in saddr, otherwise pass a null pointer for in6addr_any. To +//! send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign a +//! random user level ephemeral port. To run discovery service listening for incoming discoveries +//! and queries, you must set MDNS_PORT as port. +static inline int +mdns_socket_open_ipv6(const struct sockaddr_in6* saddr); + +//! Setup an already opened IPv6 socket for mDNS/DNS-SD. To bind the socket to a specific interface, +//! pass in the appropriate socket address in saddr, otherwise pass a null pointer for in6addr_any. +//! To send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign a +//! random user level ephemeral port. To run discovery service listening for incoming discoveries +//! and queries, you must set MDNS_PORT as port. +static inline int +mdns_socket_setup_ipv6(int sock, const struct sockaddr_in6* saddr); + +//! Close a socket opened with mdns_socket_open_ipv4 and mdns_socket_open_ipv6. +static inline void +mdns_socket_close(int sock); + +//! Listen for incoming multicast DNS-SD and mDNS query requests. The socket should have been opened +//! on port MDNS_PORT using one of the mdns open or setup socket functions. Buffer must be 32 bit +//! aligned. Parsing is stopped when callback function returns non-zero. Returns the number of +//! queries parsed. +static inline size_t +mdns_socket_listen(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data); + +//! Send a multicast DNS-SD reqeuest on the given socket to discover available services. Returns 0 +//! on success, or <0 if error. +static inline int +mdns_discovery_send(int sock); + +//! Recieve unicast responses to a DNS-SD sent with mdns_discovery_send. Any data will be piped to +//! the given callback for parsing. Buffer must be 32 bit aligned. Parsing is stopped when callback +//! function returns non-zero. Returns the number of responses parsed. +static inline size_t +mdns_discovery_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data); + +//! Send a multicast mDNS query on the given socket for the given service name. The supplied buffer +//! will be used to build the query packet and must be 32 bit aligned. The query ID can be set to +//! non-zero to filter responses, however the RFC states that the query ID SHOULD be set to 0 for +//! multicast queries. The query will request a unicast response if the socket is bound to an +//! ephemeral port, or a multicast response if the socket is bound to mDNS port 5353. Returns the +//! used query ID, or <0 if error. +static inline int +mdns_query_send(int sock, mdns_record_type_t type, const char* name, size_t length, void* buffer, + size_t capacity, uint16_t query_id); + +//! Receive unicast responses to a mDNS query sent with mdns_discovery_recv, optionally filtering +//! out any responses not matching the given query ID. Set the query ID to 0 to parse all responses, +//! even if it is not matching the query ID set in a specific query. Any data will be piped to the +//! given callback for parsing. Buffer must be 32 bit aligned. Parsing is stopped when callback +//! function returns non-zero. Returns the number of responses parsed. +static inline size_t +mdns_query_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data, int query_id); + +//! Send a variable unicast mDNS query answer to any question with variable number of records to the +//! given address. Use the top bit of the query class field (MDNS_UNICAST_RESPONSE) in the query +//! recieved to determine if the answer should be sent unicast (bit set) or multicast (bit not set). +//! Buffer must be 32 bit aligned. The record type and name should match the data from the query +//! recieved. Returns 0 if success, or <0 if error. +static inline int +mdns_query_answer_unicast(int sock, const void* address, size_t address_size, void* buffer, + size_t capacity, uint16_t query_id, mdns_record_type_t record_type, + const char* name, size_t name_length, mdns_record_t answer, + mdns_record_t* authority, size_t authority_count, + mdns_record_t* additional, size_t additional_count); + +//! Send a variable multicast mDNS query answer to any question with variable number of records. Use +//! the top bit of the query class field (MDNS_UNICAST_RESPONSE) in the query recieved to determine +//! if the answer should be sent unicast (bit set) or multicast (bit not set). Buffer must be 32 bit +//! aligned. Returns 0 if success, or <0 if error. +static inline int +mdns_query_answer_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + mdns_record_t* authority, size_t authority_count, + mdns_record_t* additional, size_t additional_count); + +//! Send a variable multicast mDNS announcement (as an unsolicited answer) with variable number of +//! records.Buffer must be 32 bit aligned. Returns 0 if success, or <0 if error. Use this on service +//! startup to announce your instance to the local network. +static inline int +mdns_announce_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + mdns_record_t* authority, size_t authority_count, mdns_record_t* additional, + size_t additional_count); + +//! Send a variable multicast mDNS announcement. Use this on service end for removing the resource +//! from the local network. The records must be identical to the according announcement. +static inline int +mdns_goodbye_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + mdns_record_t* authority, size_t authority_count, mdns_record_t* additional, + size_t additional_count); + +// Parse records functions + +//! Parse a PTR record, returns the name in the record +static inline mdns_string_t +mdns_record_parse_ptr(const void* buffer, size_t size, size_t offset, size_t length, + char* strbuffer, size_t capacity); + +//! Parse a SRV record, returns the priority, weight, port and name in the record +static inline mdns_record_srv_t +mdns_record_parse_srv(const void* buffer, size_t size, size_t offset, size_t length, + char* strbuffer, size_t capacity); + +//! Parse an A record, returns the IPv4 address in the record +static inline struct sockaddr_in* +mdns_record_parse_a(const void* buffer, size_t size, size_t offset, size_t length, + struct sockaddr_in* addr); + +//! Parse an AAAA record, returns the IPv6 address in the record +static inline struct sockaddr_in6* +mdns_record_parse_aaaa(const void* buffer, size_t size, size_t offset, size_t length, + struct sockaddr_in6* addr); + +//! Parse a TXT record, returns the number of key=value records parsed and stores the key-value +//! pairs in the supplied buffer +static inline size_t +mdns_record_parse_txt(const void* buffer, size_t size, size_t offset, size_t length, + mdns_record_txt_t* records, size_t capacity); + +// Internal functions + +static inline mdns_string_t +mdns_string_extract(const void* buffer, size_t size, size_t* offset, char* str, size_t capacity); + +static inline int +mdns_string_skip(const void* buffer, size_t size, size_t* offset); + +static inline size_t +mdns_string_find(const char* str, size_t length, char c, size_t offset); + +static inline int +mdns_string_equal(const void* buffer_lhs, size_t size_lhs, size_t* ofs_lhs, const void* buffer_rhs, + size_t size_rhs, size_t* ofs_rhs); + +static inline void* +mdns_string_make(void* buffer, size_t capacity, void* data, const char* name, size_t length, + mdns_string_table_t* string_table); + +static inline size_t +mdns_string_table_find(mdns_string_table_t* string_table, const void* buffer, size_t capacity, + const char* str, size_t first_length, size_t total_length); + +// Implementations + +static inline uint16_t +mdns_ntohs(const void* data) { + uint16_t aligned; + memcpy(&aligned, data, sizeof(uint16_t)); + return ntohs(aligned); +} + +static inline uint32_t +mdns_ntohl(const void* data) { + uint32_t aligned; + memcpy(&aligned, data, sizeof(uint32_t)); + return ntohl(aligned); +} + +static inline void* +mdns_htons(void* data, uint16_t val) { + val = htons(val); + memcpy(data, &val, sizeof(uint16_t)); + return MDNS_POINTER_OFFSET(data, sizeof(uint16_t)); +} + +static inline void* +mdns_htonl(void* data, uint32_t val) { + val = htonl(val); + memcpy(data, &val, sizeof(uint32_t)); + return MDNS_POINTER_OFFSET(data, sizeof(uint32_t)); +} + +static inline int +mdns_socket_open_ipv4(const struct sockaddr_in* saddr) { + int sock = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) + return -1; + if (mdns_socket_setup_ipv4(sock, saddr)) { + mdns_socket_close(sock); + return -1; + } + return sock; +} + +static inline int +mdns_socket_setup_ipv4(int sock, const struct sockaddr_in* saddr) { + unsigned char ttl = 1; + unsigned char loopback = 1; + unsigned int reuseaddr = 1; + struct ip_mreq req; + + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuseaddr, sizeof(reuseaddr)); +#ifdef SO_REUSEPORT + setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, (const char*)&reuseaddr, sizeof(reuseaddr)); +#endif + setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, (const char*)&ttl, sizeof(ttl)); + setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP, (const char*)&loopback, sizeof(loopback)); + + memset(&req, 0, sizeof(req)); + req.imr_multiaddr.s_addr = htonl((((uint32_t)224U) << 24U) | ((uint32_t)251U)); + if (saddr) + req.imr_interface = saddr->sin_addr; + if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&req, sizeof(req))) + return -1; + + struct sockaddr_in sock_addr; + if (!saddr) { + memset(&sock_addr, 0, sizeof(struct sockaddr_in)); + sock_addr.sin_family = AF_INET; + sock_addr.sin_addr.s_addr = INADDR_ANY; +#ifdef __APPLE__ + sock_addr.sin_len = sizeof(struct sockaddr_in); +#endif + } else { + memcpy(&sock_addr, saddr, sizeof(struct sockaddr_in)); + setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF, (const char*)&sock_addr.sin_addr, + sizeof(sock_addr.sin_addr)); +#ifndef _WIN32 + sock_addr.sin_addr.s_addr = INADDR_ANY; +#endif + } + + if (bind(sock, (struct sockaddr*)&sock_addr, sizeof(struct sockaddr_in))) + return -1; + +#ifdef _WIN32 + unsigned long param = 1; + ioctlsocket(sock, FIONBIO, ¶m); +#else + const int flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, flags | O_NONBLOCK); +#endif + + return 0; +} + +static inline int +mdns_socket_open_ipv6(const struct sockaddr_in6* saddr) { + int sock = (int)socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) + return -1; + if (mdns_socket_setup_ipv6(sock, saddr)) { + mdns_socket_close(sock); + return -1; + } + return sock; +} + +static inline int +mdns_socket_setup_ipv6(int sock, const struct sockaddr_in6* saddr) { + int hops = 1; + unsigned int loopback = 1; + unsigned int reuseaddr = 1; + struct ipv6_mreq req; + + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuseaddr, sizeof(reuseaddr)); +#ifdef SO_REUSEPORT + setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, (const char*)&reuseaddr, sizeof(reuseaddr)); +#endif + setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, (const char*)&hops, sizeof(hops)); + setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, (const char*)&loopback, sizeof(loopback)); + + memset(&req, 0, sizeof(req)); + req.ipv6mr_multiaddr.s6_addr[0] = 0xFF; + req.ipv6mr_multiaddr.s6_addr[1] = 0x02; + req.ipv6mr_multiaddr.s6_addr[15] = 0xFB; + if (setsockopt(sock, IPPROTO_IPV6, IPV6_JOIN_GROUP, (char*)&req, sizeof(req))) + return -1; + + struct sockaddr_in6 sock_addr; + if (!saddr) { + memset(&sock_addr, 0, sizeof(struct sockaddr_in6)); + sock_addr.sin6_family = AF_INET6; + sock_addr.sin6_addr = in6addr_any; +#ifdef __APPLE__ + sock_addr.sin6_len = sizeof(struct sockaddr_in6); +#endif + } else { + memcpy(&sock_addr, saddr, sizeof(struct sockaddr_in6)); + unsigned int ifindex = 0; + setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_IF, (const char*)&ifindex, sizeof(ifindex)); +#ifndef _WIN32 + sock_addr.sin6_addr = in6addr_any; +#endif + } + + if (bind(sock, (struct sockaddr*)&sock_addr, sizeof(struct sockaddr_in6))) + return -1; + +#ifdef _WIN32 + unsigned long param = 1; + ioctlsocket(sock, FIONBIO, ¶m); +#else + const int flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, flags | O_NONBLOCK); +#endif + + return 0; +} + +static inline void +mdns_socket_close(int sock) { +#ifdef _WIN32 + closesocket(sock); +#else + close(sock); +#endif +} + +static inline int +mdns_is_string_ref(uint8_t val) { + return (0xC0 == (val & 0xC0)); +} + +static inline mdns_string_pair_t +mdns_get_next_substring(const void* rawdata, size_t size, size_t offset) { + const uint8_t* buffer = (const uint8_t*)rawdata; + mdns_string_pair_t pair = {MDNS_INVALID_POS, 0, 0}; + if (offset >= size) + return pair; + if (!buffer[offset]) { + pair.offset = offset; + return pair; + } + int recursion = 0; + while (mdns_is_string_ref(buffer[offset])) { + if (size < offset + 2) + return pair; + + offset = mdns_ntohs(MDNS_POINTER_OFFSET(buffer, offset)) & 0x3fff; + if (offset >= size) + return pair; + + pair.ref = 1; + if (++recursion > 16) + return pair; + } + + size_t length = (size_t)buffer[offset++]; + if (size < offset + length) + return pair; + + pair.offset = offset; + pair.length = length; + + return pair; +} + +static inline int +mdns_string_skip(const void* buffer, size_t size, size_t* offset) { + size_t cur = *offset; + mdns_string_pair_t substr; + unsigned int counter = 0; + do { + substr = mdns_get_next_substring(buffer, size, cur); + if ((substr.offset == MDNS_INVALID_POS) || (counter++ > MDNS_MAX_SUBSTRINGS)) + return 0; + if (substr.ref) { + *offset = cur + 2; + return 1; + } + cur = substr.offset + substr.length; + } while (substr.length); + + *offset = cur + 1; + return 1; +} + +static inline int +mdns_string_equal(const void* buffer_lhs, size_t size_lhs, size_t* ofs_lhs, const void* buffer_rhs, + size_t size_rhs, size_t* ofs_rhs) { + size_t lhs_cur = *ofs_lhs; + size_t rhs_cur = *ofs_rhs; + size_t lhs_end = MDNS_INVALID_POS; + size_t rhs_end = MDNS_INVALID_POS; + mdns_string_pair_t lhs_substr; + mdns_string_pair_t rhs_substr; + unsigned int counter = 0; + do { + lhs_substr = mdns_get_next_substring(buffer_lhs, size_lhs, lhs_cur); + rhs_substr = mdns_get_next_substring(buffer_rhs, size_rhs, rhs_cur); + if ((lhs_substr.offset == MDNS_INVALID_POS) || (rhs_substr.offset == MDNS_INVALID_POS) || + (counter++ > MDNS_MAX_SUBSTRINGS)) + return 0; + if (lhs_substr.length != rhs_substr.length) + return 0; + if (strncasecmp((const char*)MDNS_POINTER_OFFSET_CONST(buffer_rhs, rhs_substr.offset), + (const char*)MDNS_POINTER_OFFSET_CONST(buffer_lhs, lhs_substr.offset), + rhs_substr.length)) + return 0; + if (lhs_substr.ref && (lhs_end == MDNS_INVALID_POS)) + lhs_end = lhs_cur + 2; + if (rhs_substr.ref && (rhs_end == MDNS_INVALID_POS)) + rhs_end = rhs_cur + 2; + lhs_cur = lhs_substr.offset + lhs_substr.length; + rhs_cur = rhs_substr.offset + rhs_substr.length; + } while (lhs_substr.length); + + if (lhs_end == MDNS_INVALID_POS) + lhs_end = lhs_cur + 1; + *ofs_lhs = lhs_end; + + if (rhs_end == MDNS_INVALID_POS) + rhs_end = rhs_cur + 1; + *ofs_rhs = rhs_end; + + return 1; +} + +static inline mdns_string_t +mdns_string_extract(const void* buffer, size_t size, size_t* offset, char* str, size_t capacity) { + size_t cur = *offset; + size_t end = MDNS_INVALID_POS; + mdns_string_pair_t substr; + mdns_string_t result; + result.str = str; + result.length = 0; + char* dst = str; + unsigned int counter = 0; + size_t remain = capacity; + do { + substr = mdns_get_next_substring(buffer, size, cur); + if ((substr.offset == MDNS_INVALID_POS) || (counter++ > MDNS_MAX_SUBSTRINGS)) + return result; + if (substr.ref && (end == MDNS_INVALID_POS)) + end = cur + 2; + if (substr.length) { + size_t to_copy = (substr.length < remain) ? substr.length : remain; + memcpy(dst, (const char*)buffer + substr.offset, to_copy); + dst += to_copy; + remain -= to_copy; + if (remain) { + *dst++ = '.'; + --remain; + } + } + cur = substr.offset + substr.length; + } while (substr.length); + + if (end == MDNS_INVALID_POS) + end = cur + 1; + *offset = end; + + result.length = capacity - remain; + return result; +} + +static inline size_t +mdns_string_table_find(mdns_string_table_t* string_table, const void* buffer, size_t capacity, + const char* str, size_t first_length, size_t total_length) { + if (!string_table) + return MDNS_INVALID_POS; + + for (size_t istr = 0; istr < string_table->count; ++istr) { + if (string_table->offset[istr] >= capacity) + continue; + size_t offset = 0; + mdns_string_pair_t sub_string = + mdns_get_next_substring(buffer, capacity, string_table->offset[istr]); + if (!sub_string.length || (sub_string.length != first_length)) + continue; + if (memcmp(str, MDNS_POINTER_OFFSET(buffer, sub_string.offset), sub_string.length)) + continue; + + // Initial substring matches, now match all remaining substrings + offset += first_length + 1; + while (offset < total_length) { + size_t dot_pos = mdns_string_find(str, total_length, '.', offset); + if (dot_pos == MDNS_INVALID_POS) + dot_pos = total_length; + size_t current_length = dot_pos - offset; + + sub_string = + mdns_get_next_substring(buffer, capacity, sub_string.offset + sub_string.length); + if (!sub_string.length || (sub_string.length != current_length)) + break; + if (memcmp(str + offset, MDNS_POINTER_OFFSET(buffer, sub_string.offset), + sub_string.length)) + break; + + offset = dot_pos + 1; + } + + // Return reference offset if entire string matches + if (offset >= total_length) + return string_table->offset[istr]; + } + + return MDNS_INVALID_POS; +} + +static inline void +mdns_string_table_add(mdns_string_table_t* string_table, size_t offset) { + if (!string_table) + return; + + string_table->offset[string_table->next] = offset; + + size_t table_capacity = sizeof(string_table->offset) / sizeof(string_table->offset[0]); + if (++string_table->count > table_capacity) + string_table->count = table_capacity; + if (++string_table->next >= table_capacity) + string_table->next = 0; +} + +static inline size_t +mdns_string_find(const char* str, size_t length, char c, size_t offset) { + const void* found; + if (offset >= length) + return MDNS_INVALID_POS; + found = memchr(str + offset, c, length - offset); + if (found) + return (size_t)MDNS_POINTER_DIFF(found, str); + return MDNS_INVALID_POS; +} + +static inline void* +mdns_string_make_ref(void* data, size_t capacity, size_t ref_offset) { + if (capacity < 2) + return 0; + return mdns_htons(data, 0xC000 | (uint16_t)ref_offset); +} + +static inline void* +mdns_string_make(void* buffer, size_t capacity, void* data, const char* name, size_t length, + mdns_string_table_t* string_table) { + size_t last_pos = 0; + size_t remain = capacity - MDNS_POINTER_DIFF(data, buffer); + if (name[length - 1] == '.') + --length; + while (last_pos < length) { + size_t pos = mdns_string_find(name, length, '.', last_pos); + size_t sub_length = ((pos != MDNS_INVALID_POS) ? pos : length) - last_pos; + size_t total_length = length - last_pos; + + size_t ref_offset = + mdns_string_table_find(string_table, buffer, capacity, + (char*)MDNS_POINTER_OFFSET(name, last_pos), sub_length, + total_length); + if (ref_offset != MDNS_INVALID_POS) + return mdns_string_make_ref(data, remain, ref_offset); + + if (remain <= (sub_length + 1)) + return 0; + + *(unsigned char*)data = (unsigned char)sub_length; + memcpy(MDNS_POINTER_OFFSET(data, 1), name + last_pos, sub_length); + mdns_string_table_add(string_table, MDNS_POINTER_DIFF(data, buffer)); + + data = MDNS_POINTER_OFFSET(data, sub_length + 1); + last_pos = ((pos != MDNS_INVALID_POS) ? pos + 1 : length); + remain = capacity - MDNS_POINTER_DIFF(data, buffer); + } + + if (!remain) + return 0; + + *(unsigned char*)data = 0; + return MDNS_POINTER_OFFSET(data, 1); +} + +static inline size_t +mdns_records_parse(int sock, const struct sockaddr* from, size_t addrlen, const void* buffer, + size_t size, size_t* offset, mdns_entry_type_t type, uint16_t query_id, + size_t records, mdns_record_callback_fn callback, void* user_data) { + size_t parsed = 0; + for (size_t i = 0; i < records; ++i) { + size_t name_offset = *offset; + mdns_string_skip(buffer, size, offset); + if (((*offset) + 10) > size) + return parsed; + size_t name_length = (*offset) - name_offset; + const uint16_t* data = (const uint16_t*)MDNS_POINTER_OFFSET(buffer, *offset); + + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + uint32_t ttl = mdns_ntohl(data); + data += 2; + uint16_t length = mdns_ntohs(data++); + + *offset += 10; + + if (length <= (size - (*offset))) { + ++parsed; + if (callback && + callback(sock, from, addrlen, type, query_id, rtype, rclass, ttl, buffer, size, + name_offset, name_length, *offset, length, user_data)) + break; + } + + *offset += length; + } + return parsed; +} + +static inline int +mdns_unicast_send(int sock, const void* address, size_t address_size, const void* buffer, + size_t size) { + if (sendto(sock, (const char*)buffer, (mdns_size_t)size, 0, (const struct sockaddr*)address, + (socklen_t)address_size) < 0) + return -1; + return 0; +} + +static inline int +mdns_multicast_send(int sock, const void* buffer, size_t size) { + struct sockaddr_storage addr_storage; + struct sockaddr_in addr; + struct sockaddr_in6 addr6; + struct sockaddr* saddr = (struct sockaddr*)&addr_storage; + socklen_t saddrlen = sizeof(struct sockaddr_storage); + if (getsockname(sock, saddr, &saddrlen)) + return -1; + if (saddr->sa_family == AF_INET6) { + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; +#ifdef __APPLE__ + addr6.sin6_len = sizeof(addr6); +#endif + addr6.sin6_addr.s6_addr[0] = 0xFF; + addr6.sin6_addr.s6_addr[1] = 0x02; + addr6.sin6_addr.s6_addr[15] = 0xFB; + addr6.sin6_port = htons((unsigned short)MDNS_PORT); + saddr = (struct sockaddr*)&addr6; + saddrlen = sizeof(addr6); + } else { + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; +#ifdef __APPLE__ + addr.sin_len = sizeof(addr); +#endif + addr.sin_addr.s_addr = htonl((((uint32_t)224U) << 24U) | ((uint32_t)251U)); + addr.sin_port = htons((unsigned short)MDNS_PORT); + saddr = (struct sockaddr*)&addr; + saddrlen = sizeof(addr); + } + + if (sendto(sock, (const char*)buffer, (mdns_size_t)size, 0, saddr, saddrlen) < 0) + return -1; + return 0; +} + +static inline const uint8_t mdns_services_query[] = { + // Query ID + 0x00, 0x00, + // Flags + 0x00, 0x00, + // 1 question + 0x00, 0x01, + // No answer, authority or additional RRs + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // _services._dns-sd._udp.local. + 0x09, '_', 's', 'e', 'r', 'v', 'i', 'c', 'e', 's', 0x07, '_', 'd', 'n', 's', '-', 's', 'd', + 0x04, '_', 'u', 'd', 'p', 0x05, 'l', 'o', 'c', 'a', 'l', 0x00, + // PTR record + 0x00, MDNS_RECORDTYPE_PTR, + // QU (unicast response) and class IN + 0x80, MDNS_CLASS_IN}; + +static inline int +mdns_discovery_send(int sock) { + return mdns_multicast_send(sock, mdns_services_query, sizeof(mdns_services_query)); +} + +static inline size_t +mdns_discovery_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data) { + struct sockaddr_in6 addr; + struct sockaddr* saddr = (struct sockaddr*)&addr; + socklen_t addrlen = sizeof(addr); + memset(&addr, 0, sizeof(addr)); +#ifdef __APPLE__ + saddr->sa_len = sizeof(addr); +#endif + mdns_ssize_t ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen); + if (ret <= 0) + return 0; + + size_t data_size = (size_t)ret; + size_t records = 0; + const uint16_t* data = (uint16_t*)buffer; + + uint16_t query_id = mdns_ntohs(data++); + uint16_t flags = mdns_ntohs(data++); + uint16_t questions = mdns_ntohs(data++); + uint16_t answer_rrs = mdns_ntohs(data++); + uint16_t authority_rrs = mdns_ntohs(data++); + uint16_t additional_rrs = mdns_ntohs(data++); + + // According to RFC 6762 the query ID MUST match the sent query ID (which is 0 in our case) + if (query_id || (flags != 0x8400)) + return 0; // Not a reply to our question + + // It seems some implementations do not fill the correct questions field, + // so ignore this check for now and only validate answer string + /* + if (questions != 1) + return 0; + */ + + int i; + for (i = 0; i < questions; ++i) { + size_t ofs = MDNS_POINTER_DIFF(data, buffer); + size_t verify_ofs = 12; + // Verify it's our question, _services._dns-sd._udp.local. + if (!mdns_string_equal(buffer, data_size, &ofs, mdns_services_query, + sizeof(mdns_services_query), &verify_ofs)) + return 0; + data = (const uint16_t*)MDNS_POINTER_OFFSET(buffer, ofs); + + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + + // Make sure we get a reply based on our PTR question for class IN + if ((rtype != MDNS_RECORDTYPE_PTR) || ((rclass & 0x7FFF) != MDNS_CLASS_IN)) + return 0; + } + + for (i = 0; i < answer_rrs; ++i) { + size_t ofs = MDNS_POINTER_DIFF(data, buffer); + size_t verify_ofs = 12; + // Verify it's an answer to our question, _services._dns-sd._udp.local. + size_t name_offset = ofs; + int is_answer = mdns_string_equal(buffer, data_size, &ofs, mdns_services_query, + sizeof(mdns_services_query), &verify_ofs); + size_t name_length = ofs - name_offset; + if ((ofs + 10) > data_size) + return records; + data = (const uint16_t*)MDNS_POINTER_OFFSET(buffer, ofs); + + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + uint32_t ttl = mdns_ntohl(data); + data += 2; + uint16_t length = mdns_ntohs(data++); + if (length > (data_size - ofs)) + return 0; + + if (is_answer) { + ++records; + ofs = MDNS_POINTER_DIFF(data, buffer); + if (callback && + callback(sock, saddr, addrlen, MDNS_ENTRYTYPE_ANSWER, query_id, rtype, rclass, ttl, + buffer, data_size, name_offset, name_length, ofs, length, user_data)) + return records; + } + data = (const uint16_t*)MDNS_POINTER_OFFSET_CONST(data, length); + } + + size_t total_records = records; + size_t offset = MDNS_POINTER_DIFF(data, buffer); + records = + mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_AUTHORITY, query_id, authority_rrs, callback, user_data); + total_records += records; + if (records != authority_rrs) + return total_records; + + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ADDITIONAL, query_id, additional_rrs, callback, + user_data); + total_records += records; + if (records != additional_rrs) + return total_records; + + return total_records; +} + +static inline size_t +mdns_socket_listen(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data) { + struct sockaddr_in6 addr; + struct sockaddr* saddr = (struct sockaddr*)&addr; + socklen_t addrlen = sizeof(addr); + memset(&addr, 0, sizeof(addr)); +#ifdef __APPLE__ + saddr->sa_len = sizeof(addr); +#endif + mdns_ssize_t ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen); + if (ret <= 0) + return 0; + + size_t data_size = (size_t)ret; + const uint16_t* data = (const uint16_t*)buffer; + + uint16_t query_id = mdns_ntohs(data++); + uint16_t flags = mdns_ntohs(data++); + uint16_t questions = mdns_ntohs(data++); + /* + This data is unused at the moment, skip + uint16_t answer_rrs = mdns_ntohs(data++); + uint16_t authority_rrs = mdns_ntohs(data++); + uint16_t additional_rrs = mdns_ntohs(data++); + */ + data += 3; + + size_t parsed = 0; + for (int iquestion = 0; iquestion < questions; ++iquestion) { + size_t question_offset = MDNS_POINTER_DIFF(data, buffer); + size_t offset = question_offset; + size_t verify_ofs = 12; + int dns_sd = 0; + if (mdns_string_equal(buffer, data_size, &offset, mdns_services_query, + sizeof(mdns_services_query), &verify_ofs)) { + dns_sd = 1; + } else { + offset = question_offset; + if (!mdns_string_skip(buffer, data_size, &offset)) + break; + } + size_t length = offset - question_offset; + data = (const uint16_t*)MDNS_POINTER_OFFSET_CONST(buffer, offset); + + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + uint16_t class_without_flushbit = rclass & ~MDNS_CACHE_FLUSH; + + // Make sure we get a question of class IN + if (!((class_without_flushbit == MDNS_CLASS_IN) || + (class_without_flushbit == MDNS_CLASS_ANY))) { + break; + } + + if (dns_sd && flags) + continue; + + ++parsed; + if (callback && callback(sock, saddr, addrlen, MDNS_ENTRYTYPE_QUESTION, query_id, rtype, + rclass, 0, buffer, data_size, question_offset, length, + question_offset, length, user_data)) + break; + } + + return parsed; +} + +static inline int +mdns_query_send(int sock, mdns_record_type_t type, const char* name, size_t length, void* buffer, + size_t capacity, uint16_t query_id) { + if (capacity < (17 + length)) + return -1; + + // Ask for a unicast response since it's a one-shot query + uint16_t rclass = MDNS_CLASS_IN | MDNS_UNICAST_RESPONSE; + + struct sockaddr_storage addr_storage; + struct sockaddr* saddr = (struct sockaddr*)&addr_storage; + socklen_t saddrlen = sizeof(addr_storage); + if (getsockname(sock, saddr, &saddrlen) == 0) { + if ((saddr->sa_family == AF_INET) && + (ntohs(((struct sockaddr_in*)saddr)->sin_port) == MDNS_PORT)) + rclass &= ~MDNS_UNICAST_RESPONSE; + else if ((saddr->sa_family == AF_INET6) && + (ntohs(((struct sockaddr_in6*)saddr)->sin6_port) == MDNS_PORT)) + rclass &= ~MDNS_UNICAST_RESPONSE; + } + + struct mdns_header_t* header = (struct mdns_header_t*)buffer; + // Query ID + header->query_id = htons(query_id); + // Flags + header->flags = 0; + // Questions + header->questions = htons(1); + // No answer, authority or additional RRs + header->answer_rrs = 0; + header->authority_rrs = 0; + header->additional_rrs = 0; + // Fill in question + // Name string + void* data = MDNS_POINTER_OFFSET(buffer, sizeof(struct mdns_header_t)); + data = mdns_string_make(buffer, capacity, data, name, length, 0); + if (!data) + return -1; + // Record type + data = mdns_htons(data, type); + //! Optional unicast response based on local port, class IN + data = mdns_htons(data, rclass); + + size_t tosend = MDNS_POINTER_DIFF(data, buffer); + if (mdns_multicast_send(sock, buffer, (size_t)tosend)) + return -1; + return query_id; +} + +static inline size_t +mdns_query_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data, int only_query_id) { + struct sockaddr_in6 addr; + struct sockaddr* saddr = (struct sockaddr*)&addr; + socklen_t addrlen = sizeof(addr); + memset(&addr, 0, sizeof(addr)); +#ifdef __APPLE__ + saddr->sa_len = sizeof(addr); +#endif + mdns_ssize_t ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen); + if (ret <= 0) + return 0; + + size_t data_size = (size_t)ret; + const uint16_t* data = (const uint16_t*)buffer; + + uint16_t query_id = mdns_ntohs(data++); + uint16_t flags = mdns_ntohs(data++); + uint16_t questions = mdns_ntohs(data++); + uint16_t answer_rrs = mdns_ntohs(data++); + uint16_t authority_rrs = mdns_ntohs(data++); + uint16_t additional_rrs = mdns_ntohs(data++); + (void)sizeof(flags); + + if ((only_query_id > 0) && (query_id != only_query_id)) + return 0; // Not a reply to the wanted one-shot query + + if (questions > 1) + return 0; + + // Skip questions part + int i; + for (i = 0; i < questions; ++i) { + size_t ofs = MDNS_POINTER_DIFF(data, buffer); + if (!mdns_string_skip(buffer, data_size, &ofs)) + return 0; + data = (const uint16_t*)MDNS_POINTER_OFFSET_CONST(buffer, ofs); + /* Record type and class not used, skip + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++);*/ + data += 2; + } + + size_t records = 0; + size_t total_records = 0; + size_t offset = MDNS_POINTER_DIFF(data, buffer); + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ANSWER, query_id, answer_rrs, callback, user_data); + total_records += records; + if (records != answer_rrs) + return total_records; + + records = + mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_AUTHORITY, query_id, authority_rrs, callback, user_data); + total_records += records; + if (records != authority_rrs) + return total_records; + + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ADDITIONAL, query_id, additional_rrs, callback, + user_data); + total_records += records; + if (records != additional_rrs) + return total_records; + + return total_records; +} + +static inline void* +mdns_answer_add_question_unicast(void* buffer, size_t capacity, void* data, + mdns_record_type_t record_type, const char* name, + size_t name_length, mdns_string_table_t* string_table) { + data = mdns_string_make(buffer, capacity, data, name, name_length, string_table); + if (!data) + return 0; + size_t remain = capacity - MDNS_POINTER_DIFF(data, buffer); + if (remain < 4) + return 0; + + data = mdns_htons(data, record_type); + data = mdns_htons(data, MDNS_UNICAST_RESPONSE | MDNS_CLASS_IN); + + return data; +} + +static inline void* +mdns_answer_add_record_header(void* buffer, size_t capacity, void* data, mdns_record_t record, + uint16_t rclass, uint32_t ttl, mdns_string_table_t* string_table) { + data = mdns_string_make(buffer, capacity, data, record.name.str, record.name.length, string_table); + if (!data) + return 0; + size_t remain = capacity - MDNS_POINTER_DIFF(data, buffer); + if (remain < 10) + return 0; + + data = mdns_htons(data, record.type); + data = mdns_htons(data, rclass); + data = mdns_htonl(data, ttl); + data = mdns_htons(data, 0); // Length, to be filled later + return data; +} + +static inline void* +mdns_answer_add_record(void* buffer, size_t capacity, void* data, mdns_record_t record, + uint16_t rclass, uint32_t ttl, mdns_string_table_t* string_table) { + // TXT records will be coalesced into one record later + if (!data || (record.type == MDNS_RECORDTYPE_TXT)) + return data; + + data = mdns_answer_add_record_header(buffer, capacity, data, record, rclass, ttl, string_table); + if (!data) + return 0; + + // Pointer to length of record to be filled at end + void* record_length = MDNS_POINTER_OFFSET(data, -2); + void* record_data = data; + + size_t remain = capacity - MDNS_POINTER_DIFF(data, buffer); + switch (record.type) { + case MDNS_RECORDTYPE_PTR: + data = mdns_string_make(buffer, capacity, data, record.data.ptr.name.str, + record.data.ptr.name.length, string_table); + break; + + case MDNS_RECORDTYPE_SRV: + if (remain <= 6) + return 0; + data = mdns_htons(data, record.data.srv.priority); + data = mdns_htons(data, record.data.srv.weight); + data = mdns_htons(data, record.data.srv.port); + data = mdns_string_make(buffer, capacity, data, record.data.srv.name.str, + record.data.srv.name.length, string_table); + break; + + case MDNS_RECORDTYPE_A: + if (remain < 4) + return 0; + memcpy(data, &record.data.a.addr.sin_addr.s_addr, 4); + data = MDNS_POINTER_OFFSET(data, 4); + break; + + case MDNS_RECORDTYPE_AAAA: + if (remain < 16) + return 0; + memcpy(data, &record.data.aaaa.addr.sin6_addr, 16); // ipv6 address + data = MDNS_POINTER_OFFSET(data, 16); + break; + + default: + break; + } + + if (!data) + return 0; + + // Fill record length + mdns_htons(record_length, (uint16_t)MDNS_POINTER_DIFF(data, record_data)); + return data; +} + +static inline void* +mdns_answer_add_txt_record(void* buffer, size_t capacity, void* data, mdns_record_t* records, + size_t record_count, uint16_t rclass, uint32_t ttl, + mdns_string_table_t* string_table) { + // Pointer to length of record to be filled at end + void* record_length = 0; + void* record_data = 0; + + size_t remain = 0; + for (size_t irec = 0; data && (irec < record_count); ++irec) { + if (records[irec].type != MDNS_RECORDTYPE_TXT) + continue; + + if (!record_data) { + data = mdns_answer_add_record_header(buffer, capacity, data, records[irec], rclass, ttl, + string_table); + record_length = MDNS_POINTER_OFFSET(data, -2); + record_data = data; + } + + // TXT strings are unlikely to be shared, just make then raw. Also need one byte for + // termination, thus the <= check + size_t string_length = + records[irec].data.txt.key.length + records[irec].data.txt.value.length + 1; + if (!data) + return 0; + remain = capacity - MDNS_POINTER_DIFF(data, buffer); + if ((remain <= string_length) || (string_length > 0x3FFF)) + return 0; + + unsigned char* strdata = (unsigned char*)data; + *strdata++ = (unsigned char)string_length; + memcpy(strdata, records[irec].data.txt.key.str, records[irec].data.txt.key.length); + strdata += records[irec].data.txt.key.length; + *strdata++ = '='; + memcpy(strdata, records[irec].data.txt.value.str, records[irec].data.txt.value.length); + strdata += records[irec].data.txt.value.length; + + data = strdata; + } + + // Fill record length + if (record_data) + mdns_htons(record_length, (uint16_t)MDNS_POINTER_DIFF(data, record_data)); + + return data; +} + +static inline uint16_t +mdns_answer_get_record_count(mdns_record_t* records, size_t record_count) { + // TXT records will be coalesced into one record + uint16_t total_count = 0; + uint16_t txt_record = 0; + for (size_t irec = 0; irec < record_count; ++irec) { + if (records[irec].type == MDNS_RECORDTYPE_TXT) + txt_record = 1; + else + ++total_count; + } + return total_count + txt_record; +} + +static inline int +mdns_query_answer_unicast(int sock, const void* address, size_t address_size, void* buffer, + size_t capacity, uint16_t query_id, mdns_record_type_t record_type, + const char* name, size_t name_length, mdns_record_t answer, + mdns_record_t* authority, size_t authority_count, + mdns_record_t* additional, size_t additional_count) { + if (capacity < (sizeof(struct mdns_header_t) + 32 + 4)) + return -1; + + uint16_t rclass = MDNS_CACHE_FLUSH | MDNS_CLASS_IN; + uint32_t ttl = 10; + + // Basic answer structure + struct mdns_header_t* header = (struct mdns_header_t*)buffer; + header->query_id = htons(query_id); + header->flags = htons(0x8400); + header->questions = htons(1); + header->answer_rrs = htons(1); + header->authority_rrs = htons(mdns_answer_get_record_count(authority, authority_count)); + header->additional_rrs = htons(mdns_answer_get_record_count(additional, additional_count)); + + mdns_string_table_t string_table = {{0}, 0, 0}; + void* data = MDNS_POINTER_OFFSET(buffer, sizeof(struct mdns_header_t)); + + // Fill in question + data = mdns_answer_add_question_unicast(buffer, capacity, data, record_type, name, name_length, + &string_table); + + // Fill in answer + data = mdns_answer_add_record(buffer, capacity, data, answer, rclass, ttl, &string_table); + + // Fill in authority records + for (size_t irec = 0; data && (irec < authority_count); ++irec) + data = mdns_answer_add_record(buffer, capacity, data, authority[irec], rclass, ttl, + &string_table); + data = mdns_answer_add_txt_record(buffer, capacity, data, authority, authority_count, rclass, + ttl, &string_table); + + // Fill in additional records + for (size_t irec = 0; data && (irec < additional_count); ++irec) + data = mdns_answer_add_record(buffer, capacity, data, additional[irec], rclass, ttl, + &string_table); + data = mdns_answer_add_txt_record(buffer, capacity, data, additional, additional_count, rclass, + ttl, &string_table); + if (!data) + return -1; + + size_t tosend = MDNS_POINTER_DIFF(data, buffer); + return mdns_unicast_send(sock, address, address_size, buffer, tosend); +} + + +static inline int +mdns_answer_multicast_rclass_ttl(int sock, void* buffer, size_t capacity, uint16_t rclass, + mdns_record_t answer, mdns_record_t* authority, size_t authority_count, + mdns_record_t* additional, size_t additional_count, uint32_t ttl) { + if (capacity < (sizeof(struct mdns_header_t) + 32 + 4)) + return -1; + + // Basic answer structure + struct mdns_header_t* header = (struct mdns_header_t*)buffer; + header->query_id = 0; + header->flags = htons(0x8400); + header->questions = 0; + header->answer_rrs = htons(1); + header->authority_rrs = htons(mdns_answer_get_record_count(authority, authority_count)); + header->additional_rrs = htons(mdns_answer_get_record_count(additional, additional_count)); + + mdns_string_table_t string_table = {{0}, 0, 0}; + void* data = MDNS_POINTER_OFFSET(buffer, sizeof(struct mdns_header_t)); + + // Fill in answer + data = mdns_answer_add_record(buffer, capacity, data, answer, rclass, ttl, &string_table); + + // Fill in authority records + for (size_t irec = 0; data && (irec < authority_count); ++irec) + data = mdns_answer_add_record(buffer, capacity, data, authority[irec], rclass, ttl, + &string_table); + data = mdns_answer_add_txt_record(buffer, capacity, data, authority, authority_count, rclass, + ttl, &string_table); + + // Fill in additional records + for (size_t irec = 0; data && (irec < additional_count); ++irec) + data = mdns_answer_add_record(buffer, capacity, data, additional[irec], rclass, ttl, + &string_table); + data = mdns_answer_add_txt_record(buffer, capacity, data, additional, additional_count, rclass, + ttl, &string_table); + if (!data) + return -1; + + size_t tosend = MDNS_POINTER_DIFF(data, buffer); + return mdns_multicast_send(sock, buffer, tosend); +} + +static inline int +mdns_answer_multicast_rclass(int sock, void* buffer, size_t capacity, uint16_t rclass, + mdns_record_t answer, mdns_record_t* authority, size_t authority_count, + mdns_record_t* additional, size_t additional_count) { + return mdns_answer_multicast_rclass_ttl(sock, buffer, capacity, rclass, answer, authority, + authority_count, additional, additional_count, 60); +} + +static inline int +mdns_query_answer_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + mdns_record_t* authority, size_t authority_count, + mdns_record_t* additional, size_t additional_count) { + uint16_t rclass = MDNS_CLASS_IN; + return mdns_answer_multicast_rclass(sock, buffer, capacity, rclass, answer, authority, + authority_count, additional, additional_count); +} + +static inline int +mdns_announce_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + mdns_record_t* authority, size_t authority_count, mdns_record_t* additional, + size_t additional_count) { + uint16_t rclass = MDNS_CLASS_IN | MDNS_CACHE_FLUSH; + return mdns_answer_multicast_rclass(sock, buffer, capacity, rclass, answer, authority, + authority_count, additional, additional_count); +} + +static inline int +mdns_goodbye_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + mdns_record_t* authority, size_t authority_count, mdns_record_t* additional, + size_t additional_count) { + uint16_t rclass = MDNS_CLASS_IN | MDNS_CACHE_FLUSH; + return mdns_answer_multicast_rclass_ttl(sock, buffer, capacity, rclass, answer, authority, + authority_count, additional, additional_count, 0); +} + +static inline mdns_string_t +mdns_record_parse_ptr(const void* buffer, size_t size, size_t offset, size_t length, + char* strbuffer, size_t capacity) { + // PTR record is just a string + if ((size >= offset + length) && (length >= 2)) + return mdns_string_extract(buffer, size, &offset, strbuffer, capacity); + mdns_string_t empty = {0, 0}; + return empty; +} + +static inline mdns_record_srv_t +mdns_record_parse_srv(const void* buffer, size_t size, size_t offset, size_t length, + char* strbuffer, size_t capacity) { + mdns_record_srv_t srv; + memset(&srv, 0, sizeof(mdns_record_srv_t)); + // Read the service priority, weight, port number and the discovery name + // SRV record format (http://www.ietf.org/rfc/rfc2782.txt): + // 2 bytes network-order unsigned priority + // 2 bytes network-order unsigned weight + // 2 bytes network-order unsigned port + // string: discovery (domain) name, minimum 2 bytes when compressed + if ((size >= offset + length) && (length >= 8)) { + const uint16_t* recorddata = (const uint16_t*)MDNS_POINTER_OFFSET_CONST(buffer, offset); + srv.priority = mdns_ntohs(recorddata++); + srv.weight = mdns_ntohs(recorddata++); + srv.port = mdns_ntohs(recorddata++); + offset += 6; + srv.name = mdns_string_extract(buffer, size, &offset, strbuffer, capacity); + } + return srv; +} + +static inline struct sockaddr_in* +mdns_record_parse_a(const void* buffer, size_t size, size_t offset, size_t length, + struct sockaddr_in* addr) { + memset(addr, 0, sizeof(struct sockaddr_in)); + addr->sin_family = AF_INET; +#ifdef __APPLE__ + addr->sin_len = sizeof(struct sockaddr_in); +#endif + if ((size >= offset + length) && (length == 4)) + memcpy(&addr->sin_addr.s_addr, MDNS_POINTER_OFFSET(buffer, offset), 4); + return addr; +} + +static inline struct sockaddr_in6* +mdns_record_parse_aaaa(const void* buffer, size_t size, size_t offset, size_t length, + struct sockaddr_in6* addr) { + memset(addr, 0, sizeof(struct sockaddr_in6)); + addr->sin6_family = AF_INET6; +#ifdef __APPLE__ + addr->sin6_len = sizeof(struct sockaddr_in6); +#endif + if ((size >= offset + length) && (length == 16)) + memcpy(&addr->sin6_addr, MDNS_POINTER_OFFSET(buffer, offset), 16); + return addr; +} + +static inline size_t +mdns_record_parse_txt(const void* buffer, size_t size, size_t offset, size_t length, + mdns_record_txt_t* records, size_t capacity) { + size_t parsed = 0; + const char* strdata; + size_t end = offset + length; + + if (size < end) + end = size; + + while ((offset < end) && (parsed < capacity)) { + strdata = (const char*)MDNS_POINTER_OFFSET(buffer, offset); + size_t sublength = *(const unsigned char*)strdata; + + ++strdata; + offset += sublength + 1; + + size_t separator = 0; + for (size_t c = 0; c < sublength; ++c) { + // DNS-SD TXT record keys MUST be printable US-ASCII, [0x20, 0x7E] + if ((strdata[c] < 0x20) || (strdata[c] > 0x7E)) + break; + if (strdata[c] == '=') { + separator = c; + break; + } + } + + if (!separator) + continue; + + if (separator < sublength) { + records[parsed].key.str = strdata; + records[parsed].key.length = separator; + records[parsed].value.str = strdata + separator + 1; + records[parsed].value.length = sublength - (separator + 1); + } else { + records[parsed].key.str = strdata; + records[parsed].key.length = sublength; + } + + ++parsed; + } + + return parsed; +} + +#ifdef _WIN32 +#undef strncasecmp +#endif + +#ifdef __cplusplus +} +#endif diff --git a/qt/OpenRGBDialog2.cpp b/qt/OpenRGBDialog2.cpp index b04b76cd..7bf2c228 100644 --- a/qt/OpenRGBDialog2.cpp +++ b/qt/OpenRGBDialog2.cpp @@ -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(); @@ -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) { /*-----------------------------------------------------*\ diff --git a/qt/OpenRGBDialog2.h b/qt/OpenRGBDialog2.h index c015ef84..84cdc140 100644 --- a/qt/OpenRGBDialog2.h +++ b/qt/OpenRGBDialog2.h @@ -16,6 +16,7 @@ #include "OpenRGBQMKORGBSettingsPage/OpenRGBQMKORGBSettingsPage.h" #include "OpenRGBSerialSettingsPage/OpenRGBSerialSettingsPage.h" #include "OpenRGBYeelightSettingsPage/OpenRGBYeelightSettingsPage.h" +#include "OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.h" #include "PluginManager.h" #include @@ -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(); diff --git a/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafScanningThread.cpp b/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafScanningThread.cpp new file mode 100644 index 00000000..cc66f6ca --- /dev/null +++ b/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafScanningThread.cpp @@ -0,0 +1,438 @@ +#ifdef _WIN32 +#define _CRT_SECURE_NO_WARNINGS 1 +#endif + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#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(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 = 5; + timeout.tv_usec = 0; + + int nfds = 0; + fd_set readfs; + FD_ZERO(&readfs); + for (int isock = 0; isock < num_sockets; ++isock) + { + if (sockets[isock] >= nfds) + nfds = sockets[isock] + 1; + FD_SET(sockets[isock], &readfs); + } + + records = 0; + res = select(nfds, &readfs, 0, 0, &timeout); + if (res > 0) + { + for (int isock = 0; isock < num_sockets; ++isock) + { + if (FD_ISSET(sockets[isock], &readfs)) + { + records += mdns_query_recv(sockets[isock], buffer, capacity, query_callback, + this, query_id[isock]); + } + FD_SET(sockets[isock], &readfs); + } + } + } + while (res > 0); + + free(buffer); + + for (int isock = 0; isock < num_sockets; ++isock) + mdns_socket_close(sockets[isock]); + + return 0; +} + +void OpenRGBNanoleafScanningThread::run() +{ + SendMDNSQuery(); +} \ No newline at end of file diff --git a/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafScanningThread.h b/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafScanningThread.h new file mode 100644 index 00000000..0666816d --- /dev/null +++ b/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafScanningThread.h @@ -0,0 +1,21 @@ +#ifndef OPENRGBNanoleafScanningThread_H +#define OPENRGBNanoleafScanningThread_H + +#include + +class OpenRGBNanoleafScanningThread : public QThread +{ + Q_OBJECT + + void run(); + + int SendMDNSQuery(); + +signals: + void DeviceFound(QString address, int port); + +public: + void EmitDeviceFound(QString address, int port); +}; + +#endif // OPENRGBNanoleafScanningThread_H diff --git a/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsEntry.cpp b/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsEntry.cpp new file mode 100644 index 00000000..6091e775 --- /dev/null +++ b/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsEntry.cpp @@ -0,0 +1,93 @@ +#include "OpenRGBNanoleafSettingsEntry.h" +#include "ui_OpenRGBNanoleafSettingsEntry.h" +#include "ResourceManager.h" +#include "NanoleafController.h" +#include "json.hpp" +using json = nlohmann::json; + +using namespace Ui; + +OpenRGBNanoleafSettingsEntry::OpenRGBNanoleafSettingsEntry(QWidget *parent) : + QWidget(parent), + ui(new Ui::OpenRGBNanoleafSettingsEntryUi), + paired(false) +{ + ui->setupUi(this); +} + +OpenRGBNanoleafSettingsEntry::OpenRGBNanoleafSettingsEntry(QString a_address, int a_port) : + OpenRGBNanoleafSettingsEntry(nullptr) +{ + address = a_address; + port = a_port; + const std::string location = address.toStdString()+":"+std::to_string(port); + + ui->IPValue->setText(address); + ui->PortValue->setText(QString::fromStdString(std::to_string(a_port))); + + json nanoleaf_settings = ResourceManager::get()->GetSettingsManager()->GetSettings("NanoleafDevices"); + + if(nanoleaf_settings["devices"].contains(location) && + nanoleaf_settings["devices"][location].contains("auth_token") && + nanoleaf_settings["devices"][location]["auth_token"].size()) + { + paired = true; + auth_token = nanoleaf_settings["devices"][location]["auth_token"]; + ui->AuthKeyValue->setText(QString::fromStdString(auth_token)); + ui->PairButton->hide(); + } + else + { + ui->UnpairButton->hide(); + } +} + +OpenRGBNanoleafSettingsEntry::~OpenRGBNanoleafSettingsEntry() +{ + delete ui; +} + +void OpenRGBNanoleafSettingsEntry::on_PairButton_clicked() +{ + try + { + auth_token = NanoleafController::Pair(address.toStdString(), port); + + // Save auth token. + const std::string location = address.toStdString()+":"+std::to_string(port); + json nanoleaf_settings = ResourceManager::get()->GetSettingsManager()->GetSettings("NanoleafDevices"); + nanoleaf_settings["devices"][location]["ip"] = address.toStdString(); + nanoleaf_settings["devices"][location]["port"] = port; + nanoleaf_settings["devices"][location]["auth_token"] = auth_token; + ResourceManager::get()->GetSettingsManager()->SetSettings("NanoleafDevices", nanoleaf_settings); + ResourceManager::get()->GetSettingsManager()->SaveSettings(); + + // Update UI. + paired = true; + ui->AuthKeyValue->setText(QString::fromStdString(auth_token)); + ui->PairButton->hide(); + ui->UnpairButton->show(); + } + catch(const std::exception& e) + { + paired = false; + ui->AuthKeyValue->setText("PAIRING FAILED"); + } +} + +void OpenRGBNanoleafSettingsEntry::on_UnpairButton_clicked() +{ + NanoleafController::Unpair(address.toStdString(), port, auth_token); + + const std::string location = address.toStdString()+":"+std::to_string(port); + json nanoleaf_settings = ResourceManager::get()->GetSettingsManager()->GetSettings("NanoleafDevices"); + nanoleaf_settings["devices"].erase(location); + ResourceManager::get()->GetSettingsManager()->SetSettings("NanoleafDevices", nanoleaf_settings); + ResourceManager::get()->GetSettingsManager()->SaveSettings(); + + paired = false; + ui->AuthKeyValue->setText(""); + ui->PairButton->show(); + ui->UnpairButton->hide(); +} + diff --git a/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsEntry.h b/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsEntry.h new file mode 100644 index 00000000..f89645e2 --- /dev/null +++ b/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsEntry.h @@ -0,0 +1,34 @@ +#ifndef OPENRGBNanoleafSETTINGSENTRY_H +#define OPENRGBNanoleafSETTINGSENTRY_H + +#include "ui_OpenRGBNanoleafSettingsEntry.h" +#include "OpenRGBNanoleafScanningThread.h" +#include + +namespace Ui +{ +class OpenRGBNanoleafSettingsEntry; +} + +class Ui::OpenRGBNanoleafSettingsEntry : public QWidget +{ + Q_OBJECT + +public: + explicit OpenRGBNanoleafSettingsEntry(QWidget *parent = nullptr); + OpenRGBNanoleafSettingsEntry(QString a_address, int a_port); + ~OpenRGBNanoleafSettingsEntry(); + Ui::OpenRGBNanoleafSettingsEntryUi *ui; + +private slots: + void on_UnpairButton_clicked(); + void on_PairButton_clicked(); + +private: + QString address; + int port; + std::string auth_token; + bool paired; +}; + +#endif // OPENRGBNanoleafSETTINGSENTRY_H diff --git a/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsEntry.ui b/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsEntry.ui new file mode 100644 index 00000000..e91e909a --- /dev/null +++ b/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsEntry.ui @@ -0,0 +1,92 @@ + + + OpenRGBNanoleafSettingsEntryUi + + + + 0 + 0 + 287 + 207 + + + + + 0 + 0 + + + + Form + + + + + + + + + + + + IP: + + + + + + + + + + + + + + Port: + + + + + + + + + + + + + + Auth Key: + + + + + + + + + + + + + + Unpair + + + + + + + Pair + + + + + + + + + + + diff --git a/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.cpp b/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.cpp new file mode 100644 index 00000000..78f5678d --- /dev/null +++ b/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.cpp @@ -0,0 +1,82 @@ +#include "OpenRGBNanoleafSettingsPage.h" +#include "ui_OpenRGBNanoleafSettingsPage.h" +#include "ResourceManager.h" +#include "json.hpp" +using json = nlohmann::json; + +using namespace Ui; + +OpenRGBNanoleafSettingsPage::OpenRGBNanoleafSettingsPage(QWidget *parent) : + QWidget(parent), + ui(new Ui::OpenRGBNanoleafSettingsPageUi) +{ + ui->setupUi(this); + + 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(); + const std::string& location = it.key(); + + if(device.contains("ip") && device.contains("port")) + { + OpenRGBNanoleafSettingsEntry* entry = new OpenRGBNanoleafSettingsEntry(QString::fromStdString(device["ip"]), device["port"]); + + entries[location] = entry; + + QListWidgetItem* item = new QListWidgetItem; + + item->setSizeHint(entry->sizeHint()); + + ui->NanoleafDeviceList->addItem(item); + ui->NanoleafDeviceList->setItemWidget(item, entry); + ui->NanoleafDeviceList->show(); + } + } + } +} + +OpenRGBNanoleafSettingsPage::~OpenRGBNanoleafSettingsPage() +{ + delete ui; +} + +void Ui::OpenRGBNanoleafSettingsPage::on_ScanForNanoleafDevicesButton_clicked() +{ + /*-----------------------------------------------------*\ + | Create a worker thread for the mDNS query and hookup | + | callbacks for when it finds devices | + \*-----------------------------------------------------*/ + OpenRGBNanoleafScanningThread *scanThread = new OpenRGBNanoleafScanningThread; + + connect(scanThread, SIGNAL(DeviceFound(QString, int)), + SLOT(on_DeviceFound(QString, int))); + + connect(scanThread, SIGNAL(finished()), + scanThread, SLOT(deleteLater())); + + scanThread->start(); +} + +void Ui::OpenRGBNanoleafSettingsPage::on_DeviceFound(QString address, int port) +{ + std::string location = address.toStdString()+":"+std::to_string(port); + + if(entries.find(location) == entries.end()) + { + OpenRGBNanoleafSettingsEntry* entry = new OpenRGBNanoleafSettingsEntry(address, port); + + entries[location] = entry; + + QListWidgetItem* item = new QListWidgetItem; + + item->setSizeHint(entry->sizeHint()); + + ui->NanoleafDeviceList->addItem(item); + ui->NanoleafDeviceList->setItemWidget(item, entry); + ui->NanoleafDeviceList->show(); + } +} diff --git a/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.h b/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.h new file mode 100644 index 00000000..af76e043 --- /dev/null +++ b/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.h @@ -0,0 +1,31 @@ +#ifndef OPENRGBNanoleafSETTINGSPAGE_H +#define OPENRGBNanoleafSETTINGSPAGE_H + +#include "ui_OpenRGBNanoleafSettingsPage.h" +#include + +#include "OpenRGBNanoleafSettingsEntry.h" + +namespace Ui +{ +class OpenRGBNanoleafSettingsPage; +} + +class Ui::OpenRGBNanoleafSettingsPage : public QWidget +{ + Q_OBJECT + +public: + explicit OpenRGBNanoleafSettingsPage(QWidget *parent = nullptr); + ~OpenRGBNanoleafSettingsPage(); + +private slots: + void on_ScanForNanoleafDevicesButton_clicked(); + void on_DeviceFound(QString address, int port); + +private: + Ui::OpenRGBNanoleafSettingsPageUi *ui; + std::map entries; +}; + +#endif // OPENRGBNanoleafSETTINGSPAGE_H diff --git a/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.ui b/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.ui new file mode 100644 index 00000000..b43f195c --- /dev/null +++ b/qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.ui @@ -0,0 +1,48 @@ + + + OpenRGBNanoleafSettingsPageUi + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + Scan + + + + + + + QAbstractItemView::ScrollPerPixel + + + + + + + To pair, hold the on-off button down for 5-7 seconds until the LED starts flashing in a pattern, then click the "Pair" button within 30 seconds. + + + Qt::AlignCenter + + + true + + + + + + + +