From 95b029d49da4440175a7d5b906fe3deeea405064 Mon Sep 17 00:00:00 2001 From: Adam Honse Date: Fri, 16 May 2025 01:02:44 -0500 Subject: [PATCH] Initial support for Govee devices --- .../GoveeController/GoveeController.cpp | 321 ++++++++++++++++++ Controllers/GoveeController/GoveeController.h | 71 ++++ .../GoveeController/GoveeControllerDetect.cpp | 92 +++++ .../GoveeController/RGBController_Govee.cpp | 142 ++++++++ .../GoveeController/RGBController_Govee.h | 39 +++ Controllers/GoveeController/base64.hpp | 111 ++++++ .../PhilipsWizController.cpp | 4 +- net_port/net_port.cpp | 39 ++- net_port/net_port.h | 6 +- qt/OpenRGBDialog/OpenRGBDialog.cpp | 22 ++ qt/OpenRGBDialog/OpenRGBDialog.h | 3 + .../OpenRGBGoveeSettingsEntry.cpp | 35 ++ .../OpenRGBGoveeSettingsEntry.h | 33 ++ .../OpenRGBGoveeSettingsEntry.ui | 46 +++ .../OpenRGBGoveeSettingsPage.cpp | 125 +++++++ .../OpenRGBGoveeSettingsPage.h | 43 +++ .../OpenRGBGoveeSettingsPage.ui | 49 +++ 17 files changed, 1164 insertions(+), 17 deletions(-) create mode 100644 Controllers/GoveeController/GoveeController.cpp create mode 100644 Controllers/GoveeController/GoveeController.h create mode 100644 Controllers/GoveeController/GoveeControllerDetect.cpp create mode 100644 Controllers/GoveeController/RGBController_Govee.cpp create mode 100644 Controllers/GoveeController/RGBController_Govee.h create mode 100644 Controllers/GoveeController/base64.hpp create mode 100644 qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsEntry.cpp create mode 100644 qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsEntry.h create mode 100644 qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsEntry.ui create mode 100644 qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsPage.cpp create mode 100644 qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsPage.h create mode 100644 qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsPage.ui diff --git a/Controllers/GoveeController/GoveeController.cpp b/Controllers/GoveeController/GoveeController.cpp new file mode 100644 index 00000000..7ead83f6 --- /dev/null +++ b/Controllers/GoveeController/GoveeController.cpp @@ -0,0 +1,321 @@ +/*---------------------------------------------------------*\ +| GoveeController.cpp | +| | +| Driver for Govee wireless lighting devices | +| | +| Adam Honse (calcprogrammer1@gmail.com) 01 Dec 2023 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-only | +\*---------------------------------------------------------*/ + +#include +#include "base64.hpp" +#include "GoveeController.h" + +using json = nlohmann::json; +using namespace std::chrono_literals; + +base64::byte CalculateXorChecksum(std::vector packet) +{ + base64::byte checksum = 0; + + for(unsigned int i = 0; i < packet.size(); i++) + { + checksum ^= packet[i]; + } + + return(checksum); +} + +GoveeController::GoveeController(std::string ip) +{ + /*-----------------------------------------------------*\ + | Fill in location string with device's IP address | + \*-----------------------------------------------------*/ + ip_address = ip; + + /*-----------------------------------------------------*\ + | Register callback for receiving broadcasts | + \*-----------------------------------------------------*/ + RegisterReceiveBroadcastCallback(this); + + broadcast_received = false; + + /*-----------------------------------------------------*\ + | Request device information | + \*-----------------------------------------------------*/ + SendScan(); + + /*-----------------------------------------------------*\ + | Wait up to 5s for device information to be received | + \*-----------------------------------------------------*/ + for(unsigned int wait_count = 0; wait_count < 500; wait_count++) + { + if(broadcast_received) + { + break; + } + + std::this_thread::sleep_for(10ms); + } + + /*-----------------------------------------------------*\ + | Open a UDP client sending to the Govee device IP, | + | port 4003 | + \*-----------------------------------------------------*/ + port.udp_client(ip_address.c_str(), "4003"); +} + +GoveeController::~GoveeController() +{ + UnregisterReceiveBroadcastCallback(this); +} + +std::string GoveeController::GetLocation() +{ + return("IP: " + ip_address); +} + +std::string GoveeController::GetSku() +{ + return(sku); +} + +std::string GoveeController::GetVersion() +{ + return("BLE Hardware Version: " + bleVersionHard + "\r\n" + + "BLE Software Version: " + bleVersionSoft + "\r\n" + + "WiFi Hardware Version: " + wifiVersionHard + "\r\n" + + "WiFI Software Version: " + wifiVersionSoft + "\r\n"); +} + +void GoveeController::ReceiveBroadcast(char* recv_buf, int size) +{ + if(broadcast_received) + { + return; + } + + /*-----------------------------------------------------*\ + | Responses are not null-terminated, so add termination | + \*-----------------------------------------------------*/ + recv_buf[size] = '\0'; + + /*-----------------------------------------------------*\ + | Convert null-terminated response to JSON | + \*-----------------------------------------------------*/ + json response = json::parse(recv_buf); + + /*-----------------------------------------------------*\ + | Check if the response contains the method name | + \*-----------------------------------------------------*/ + if(response.contains("msg")) + { + /*-------------------------------------------------*\ + | Handle responses for scan command | + | This command's response should contain a msg | + | object containing a data member with ip, device, | + | sku, among others. | + \*-------------------------------------------------*/ + if(response["msg"].contains("cmd")) + { + if(response["msg"]["cmd"] == "scan") + { + if(response["msg"].contains("data")) + { + if(response["msg"]["data"].contains("ip")) + { + if(response["msg"]["data"]["ip"] == ip_address) + { + if(response["msg"]["data"].contains("sku")) + { + sku = response["msg"]["data"]["sku"]; + } + + if(response["msg"]["data"].contains("bleVersionHard")) + { + bleVersionHard = response["msg"]["data"]["bleVersionHard"]; + } + + if(response["msg"]["data"].contains("bleVersionSoft")) + { + bleVersionSoft = response["msg"]["data"]["bleVersionSoft"]; + } + + if(response["msg"]["data"].contains("wifiVersionHard")) + { + wifiVersionHard = response["msg"]["data"]["wifiVersionHard"]; + } + + if(response["msg"]["data"].contains("wifiVersionSoft")) + { + wifiVersionSoft = response["msg"]["data"]["wifiVersionSoft"]; + } + + broadcast_received = true; + } + } + } + } + } + } +} + +void GoveeController::SetColor(unsigned char red, unsigned char green, unsigned char blue, unsigned char brightness) +{ + json command; + + command["msg"]["cmd"] = "colorwc"; + command["msg"]["data"]["color"]["r"] = red; + command["msg"]["data"]["color"]["g"] = green; + command["msg"]["data"]["color"]["b"] = blue; + command["msg"]["data"]["colorTemInKelvin"] = "0"; + + /*-----------------------------------------------------*\ + | Convert the JSON object to a string and write it | + \*-----------------------------------------------------*/ + std::string command_str = command.dump(); + + port.udp_write((char *)command_str.c_str(), command_str.length() + 1); +} + +void GoveeController::SendRazerData(RGBColor* colors, unsigned int size) +{ + std::vector pkt = { 0xBB, 0x00, 0x00, 0xB0, 0x00, 0x00 }; + json command; + + pkt[2] = 2 + (3 * size); + pkt[5] = size; + pkt.resize(6 + (3 * size)); + + for(std::size_t led_idx = 0; led_idx < size; led_idx++) + { + pkt[6 + (led_idx * 3)] = RGBGetRValue(colors[led_idx]); + pkt[7 + (led_idx * 3)] = RGBGetGValue(colors[led_idx]); + pkt[8 + (led_idx * 3)] = RGBGetBValue(colors[led_idx]); + } + + pkt.push_back(CalculateXorChecksum(pkt)); + + command["msg"]["cmd"] = "razer"; + command["msg"]["data"]["pt"] = base64::encode(pkt); + + /*-----------------------------------------------------*\ + | Convert the JSON object to a string and write it | + \*-----------------------------------------------------*/ + std::string command_str = command.dump(); + + port.udp_write((char *)command_str.c_str(), command_str.length() + 1); +} + +void GoveeController::SendRazerDisable() +{ + const std::vector pkt = { 0xBB, 0x00, 0x01, 0xB1, 0x00, 0x0B }; + json command; + + command["msg"]["cmd"] = "razer"; + command["msg"]["data"]["pt"] = base64::encode(pkt); + + /*-----------------------------------------------------*\ + | Convert the JSON object to a string and write it | + \*-----------------------------------------------------*/ + std::string command_str = command.dump(); + + port.udp_write((char *)command_str.c_str(), command_str.length() + 1); +} + +void GoveeController::SendRazerEnable() +{ + const std::vector pkt = { 0xBB, 0x00, 0x01, 0xB1, 0x01, 0x0A }; + json command; + + command["msg"]["cmd"] = "razer"; + command["msg"]["data"]["pt"] = base64::encode(pkt); + + /*-----------------------------------------------------*\ + | Convert the JSON object to a string and write it | + \*-----------------------------------------------------*/ + std::string command_str = command.dump(); + + port.udp_write((char *)command_str.c_str(), command_str.length() + 1); +} + +void GoveeController::SendScan() +{ + json command; + + command["msg"]["cmd"] = "scan"; + command["msg"]["data"]["account_topic"] = "GA/123456789"; + + /*-----------------------------------------------------*\ + | Convert the JSON object to a string and write it | + \*-----------------------------------------------------*/ + std::string command_str = command.dump(); + + broadcast_port.udp_write((char *)command_str.c_str(), command_str.length() + 1); +} + +/*---------------------------------------------------------*\ +| Static class members for shared broadcast receiver | +\*---------------------------------------------------------*/ +net_port GoveeController::broadcast_port; +std::vector GoveeController::callbacks; +std::thread* GoveeController::ReceiveThread; +std::atomic GoveeController::ReceiveThreadRun; + +void GoveeController::ReceiveBroadcastThreadFunction() +{ + char recv_buf[1024]; + + broadcast_port.set_receive_timeout(1, 0); + + while(ReceiveThreadRun.load()) + { + /*-------------------------------------------------*\ + | Receive up to 1024 bytes from the device with a | + | 1s timeout | + \*-------------------------------------------------*/ + int size = broadcast_port.udp_listen(recv_buf, 1024); + + /*-------------------------------------------------*\ + | If data was received, loop through registered | + | callback controllers and call the | + | ReceiveBroadcast function for the controller | + | matching the received data | + | | + | NOTE: As implemented, it doesn't actually match | + | the intended controller and just calls all | + | registered controllers. As they are all called | + | sequence, this should work, but if parallel calls | + | are ever needed, receives should be filtered by | + | IP address | + \*-------------------------------------------------*/ + if(size > 0) + { + for(std::size_t callback_idx = 0; callback_idx < callbacks.size(); callback_idx++) + { + GoveeController* controller = callbacks[callback_idx]; + + controller->ReceiveBroadcast(recv_buf, size); + } + } + } +} + +void GoveeController::RegisterReceiveBroadcastCallback(GoveeController* controller_ptr) +{ + callbacks.push_back(controller_ptr); +} + +void GoveeController::UnregisterReceiveBroadcastCallback(GoveeController* controller_ptr) +{ + for(std::size_t callback_idx = 0; callback_idx < callbacks.size(); callback_idx++) + { + if(callbacks[callback_idx] == controller_ptr) + { + callbacks.erase(callbacks.begin() + callback_idx); + break; + } + } +} diff --git a/Controllers/GoveeController/GoveeController.h b/Controllers/GoveeController/GoveeController.h new file mode 100644 index 00000000..ba0ff1d0 --- /dev/null +++ b/Controllers/GoveeController/GoveeController.h @@ -0,0 +1,71 @@ +/*---------------------------------------------------------*\ +| GoveeController.h | +| | +| Driver for Govee wireless lighting devices | +| | +| Adam Honse (calcprogrammer1@gmail.com) 01 Dec 2023 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-only | +\*---------------------------------------------------------*/ + +#pragma once + +#include +#include +#include +#include "RGBController.h" +#include "net_port.h" + +class GoveeController +{ +public: + GoveeController(std::string ip); + ~GoveeController(); + + std::string GetLocation(); + std::string GetSku(); + std::string GetVersion(); + + void ReceiveBroadcast(char* recv_buf, int size); + + void SendRazerData(RGBColor* colors, unsigned int size); + void SendRazerDisable(); + void SendRazerEnable(); + + void SendScan(); + + void SetColor(unsigned char red, unsigned char green, unsigned char blue, unsigned char brightness); + +private: + std::string firmware_version; + std::string ip_address; + std::string module_name; + std::string module_mac; + + std::string sku; + std::string bleVersionHard; + std::string bleVersionSoft; + std::string wifiVersionHard; + std::string wifiVersionSoft; + + bool broadcast_received; + + net_port port; + +public: + /*-----------------------------------------------------*\ + | One receive thread is shared among all instances of | + | GoveeController, so the receive thread function is | + | static and the thread is initialized in the detector | + | if any GoveeControllers are created. | + \*-----------------------------------------------------*/ + static net_port broadcast_port; + static std::vector callbacks; + static std::thread* ReceiveThread; + static std::atomic ReceiveThreadRun; + + static void ReceiveBroadcastThreadFunction(); + static void RegisterReceiveBroadcastCallback(GoveeController* controller_ptr); + static void UnregisterReceiveBroadcastCallback(GoveeController* controller_ptr); +}; diff --git a/Controllers/GoveeController/GoveeControllerDetect.cpp b/Controllers/GoveeController/GoveeControllerDetect.cpp new file mode 100644 index 00000000..e949b99a --- /dev/null +++ b/Controllers/GoveeController/GoveeControllerDetect.cpp @@ -0,0 +1,92 @@ +/*---------------------------------------------------------*\ +| GoveeControllerDetect.cpp | +| | +| Detector for Govee wireless lighting devices | +| | +| Adam Honse (calcprogrammer1@gmail.com) 01 Dec 2023 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-only | +\*---------------------------------------------------------*/ + +#include +#include +#include +#include "Detector.h" +#include "GoveeController.h" +#include "RGBController.h" +#include "RGBController_Govee.h" +#include "SettingsManager.h" + +/******************************************************************************************\ +* * +* DetectGoveeControllers * +* * +* Detect Govee devices * +* * +\******************************************************************************************/ + +void DetectGoveeControllers() +{ + json govee_settings; + + /*-----------------------------------------------------*\ + | Get Govee settings from settings manager | + \*-----------------------------------------------------*/ + govee_settings = ResourceManager::get()->GetSettingsManager()->GetSettings("GoveeDevices"); + + /*-----------------------------------------------------*\ + | If the Govee settings contains devices, process | + \*-----------------------------------------------------*/ + if(govee_settings.contains("devices")) + { + GoveeController::ReceiveThreadRun = false; + + if(govee_settings["devices"].size() > 0) + { + /*---------------------------------------------*\ + | Open a UDP client sending to and receiving | + | from the Govee Multicast IP, send port 4001 | + | and receive port 4002 | + \*---------------------------------------------*/ + GoveeController::broadcast_port.udp_client("239.255.255.250", "4001", "4002"); + GoveeController::broadcast_port.udp_join_multicast_group("239.255.255.250"); + + /*---------------------------------------------*\ + | Start a thread to handle responses received | + | from the Govee device | + \*---------------------------------------------*/ + GoveeController::ReceiveThreadRun = true; + GoveeController::ReceiveThread = new std::thread(&GoveeController::ReceiveBroadcastThreadFunction); + } + + for(unsigned int device_idx = 0; device_idx < govee_settings["devices"].size(); device_idx++) + { + if(govee_settings["devices"][device_idx].contains("ip")) + { + std::string govee_ip = govee_settings["devices"][device_idx]["ip"]; + + GoveeController* controller = new GoveeController(govee_ip); + RGBController_Govee* rgb_controller = new RGBController_Govee(controller); + + ResourceManager::get()->RegisterRGBController(rgb_controller); + } + } + + /*-------------------------------------------------*\ + | All controllers have been created, the broadcast | + | receiver thread is no longer needed and can be | + | shut down | + \*-------------------------------------------------*/ + if(GoveeController::ReceiveThreadRun) + { + GoveeController::ReceiveThreadRun = false; + GoveeController::ReceiveThread->join(); + delete GoveeController::ReceiveThread; + GoveeController::broadcast_port.tcp_close(); + } + } + +} /* DetectGoveeControllers() */ + +REGISTER_DETECTOR("Govee", DetectGoveeControllers); diff --git a/Controllers/GoveeController/RGBController_Govee.cpp b/Controllers/GoveeController/RGBController_Govee.cpp new file mode 100644 index 00000000..26a9bc62 --- /dev/null +++ b/Controllers/GoveeController/RGBController_Govee.cpp @@ -0,0 +1,142 @@ +/*---------------------------------------------------------*\ +| RGBController_Govee.cpp | +| | +| RGBController for Govee wireless lighting devices | +| | +| Adam Honse (calcprogrammer1@gmail.com) 27 Dec 2023 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-only | +\*---------------------------------------------------------*/ + +#include +#include "RGBController_Govee.h" + +using namespace std::chrono_literals; + +static std::map govee_led_counts +{ + { "H619A", 20 }, + { "H70B1", 20 }, +}; + +RGBController_Govee::RGBController_Govee(GoveeController* controller_ptr) +{ + controller = controller_ptr; + + name = "Govee " + controller->GetSku(); + vendor = "Govee"; + type = DEVICE_TYPE_LIGHT; + description = "Govee Device"; + location = controller->GetLocation(); + version = controller->GetVersion(); + + mode Static; + Static.name = "Static"; + Static.value = 1; + Static.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR; + Static.color_mode = MODE_COLORS_MODE_SPECIFIC; + Static.colors_min = 1; + Static.colors_max = 1; + Static.colors.resize(1); + modes.push_back(Static); + + mode Direct; + Direct.name = "Direct"; + Direct.value = 0; + Direct.flags = MODE_FLAG_HAS_PER_LED_COLOR; + Direct.color_mode = MODE_COLORS_PER_LED; + modes.push_back(Direct); + + SetupZones(); + + keepalive_thread_run = 1; + keepalive_thread = new std::thread(&RGBController_Govee::KeepaliveThread, this); +} + +RGBController_Govee::~RGBController_Govee() +{ + keepalive_thread_run = 0; + keepalive_thread->join(); + delete keepalive_thread; + + delete controller; +} + +void RGBController_Govee::SetupZones() +{ + unsigned int led_count = govee_led_counts[controller->GetSku()]; + + zone strip; + strip.name = "Govee Strip"; + strip.type = ZONE_TYPE_LINEAR; + strip.leds_count = led_count; + strip.leds_min = led_count; + strip.leds_max = led_count; + strip.matrix_map = NULL; + zones.push_back(strip); + + for(std::size_t led_idx = 0; led_idx < strip.leds_count; led_idx++) + { + led strip_led; + strip_led.name = "Govee LED"; + leds.push_back(strip_led); + } + + SetupColors(); +} + +void RGBController_Govee::ResizeZone(int /*zone*/, int /*new_size*/) +{ + /*---------------------------------------------------------*\ + | This device does not support resizing zones | + \*---------------------------------------------------------*/ +} + +void RGBController_Govee::DeviceUpdateLEDs() +{ + last_update_time = std::chrono::steady_clock::now(); + + if(modes[active_mode].color_mode == MODE_COLORS_PER_LED) + { + controller->SendRazerData(&colors[0], colors.size()); + } +} + +void RGBController_Govee::UpdateZoneLEDs(int /*zone*/) +{ + DeviceUpdateLEDs(); +} + +void RGBController_Govee::UpdateSingleLED(int /*led*/) +{ + DeviceUpdateLEDs(); +} + +void RGBController_Govee::DeviceUpdateMode() +{ + if(modes[active_mode].color_mode == MODE_COLORS_MODE_SPECIFIC) + { + unsigned char red = RGBGetRValue(modes[active_mode].colors[0]); + unsigned char grn = RGBGetGValue(modes[active_mode].colors[0]); + unsigned char blu = RGBGetBValue(modes[active_mode].colors[0]); + controller->SetColor(red, grn, blu, 255); + } + else + { + controller->SendRazerEnable(); + DeviceUpdateLEDs(); + } +} + +void RGBController_Govee::KeepaliveThread() +{ + while(keepalive_thread_run.load()) + { + if((std::chrono::steady_clock::now() - last_update_time) > std::chrono::seconds(30)) + { + DeviceUpdateLEDs(); + } + std::this_thread::sleep_for(10s); + } +} diff --git a/Controllers/GoveeController/RGBController_Govee.h b/Controllers/GoveeController/RGBController_Govee.h new file mode 100644 index 00000000..6c1809ba --- /dev/null +++ b/Controllers/GoveeController/RGBController_Govee.h @@ -0,0 +1,39 @@ +/*---------------------------------------------------------*\ +| RGBController_Govee.h | +| | +| RGBController for Govee wireless lighting devices | +| | +| Adam Honse (calcprogrammer1@gmail.com) 01 Dec 2023 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-only | +\*---------------------------------------------------------*/ + +#pragma once + +#include "RGBController.h" +#include "GoveeController.h" + +class RGBController_Govee : public RGBController +{ +public: + RGBController_Govee(GoveeController* controller_ptr); + ~RGBController_Govee(); + + void SetupZones(); + void ResizeZone(int zone, int new_size); + + void DeviceUpdateLEDs(); + void UpdateZoneLEDs(int zone); + void UpdateSingleLED(int led); + + void DeviceUpdateMode(); + + void KeepaliveThread(); + +private: + GoveeController* controller; + std::thread* keepalive_thread; + std::atomic keepalive_thread_run; + std::chrono::time_point last_update_time; +}; diff --git a/Controllers/GoveeController/base64.hpp b/Controllers/GoveeController/base64.hpp new file mode 100644 index 00000000..ffffae17 --- /dev/null +++ b/Controllers/GoveeController/base64.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include +#include + +namespace base64 +{ + inline static const char kEncodeLookup[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + inline static const char kPadCharacter = '='; + + using byte = std::uint8_t; + + inline std::string encode(const std::vector& input) + { + std::string encoded; + encoded.reserve(((input.size() / 3) + (input.size() % 3 > 0)) * 4); + + std::uint32_t temp{}; + auto it = input.begin(); + + for(std::size_t i = 0; i < input.size() / 3; ++i) + { + temp = (*it++) << 16; + temp += (*it++) << 8; + temp += (*it++); + encoded.append(1, kEncodeLookup[(temp & 0x00FC0000) >> 18]); + encoded.append(1, kEncodeLookup[(temp & 0x0003F000) >> 12]); + encoded.append(1, kEncodeLookup[(temp & 0x00000FC0) >> 6 ]); + encoded.append(1, kEncodeLookup[(temp & 0x0000003F) ]); + } + + switch(input.size() % 3) + { + case 1: + temp = (*it++) << 16; + encoded.append(1, kEncodeLookup[(temp & 0x00FC0000) >> 18]); + encoded.append(1, kEncodeLookup[(temp & 0x0003F000) >> 12]); + encoded.append(2, kPadCharacter); + break; + case 2: + temp = (*it++) << 16; + temp += (*it++) << 8; + encoded.append(1, kEncodeLookup[(temp & 0x00FC0000) >> 18]); + encoded.append(1, kEncodeLookup[(temp & 0x0003F000) >> 12]); + encoded.append(1, kEncodeLookup[(temp & 0x00000FC0) >> 6 ]); + encoded.append(1, kPadCharacter); + break; + } + + return encoded; + } + + inline std::vector decode(const std::string& input) + { + if(input.length() % 4) + throw std::runtime_error("Invalid base64 length!"); + + std::size_t padding{}; + + if(input.length()) + { + if(input[input.length() - 1] == kPadCharacter) padding++; + if(input[input.length() - 2] == kPadCharacter) padding++; + } + + std::vector decoded; + decoded.reserve(((input.length() / 4) * 3) - padding); + + std::uint32_t temp{}; + auto it = input.begin(); + + while(it < input.end()) + { + for(std::size_t i = 0; i < 4; ++i) + { + temp <<= 6; + if (*it >= 0x41 && *it <= 0x5A) temp |= *it - 0x41; + else if(*it >= 0x61 && *it <= 0x7A) temp |= *it - 0x47; + else if(*it >= 0x30 && *it <= 0x39) temp |= *it + 0x04; + else if(*it == 0x2B) temp |= 0x3E; + else if(*it == 0x2F) temp |= 0x3F; + else if(*it == kPadCharacter) + { + switch(input.end() - it) + { + case 1: + decoded.push_back((temp >> 16) & 0x000000FF); + decoded.push_back((temp >> 8 ) & 0x000000FF); + return decoded; + case 2: + decoded.push_back((temp >> 10) & 0x000000FF); + return decoded; + default: + throw std::runtime_error("Invalid padding in base64!"); + } + } + else throw std::runtime_error("Invalid character in base64!"); + + ++it; + } + + decoded.push_back((temp >> 16) & 0x000000FF); + decoded.push_back((temp >> 8 ) & 0x000000FF); + decoded.push_back((temp ) & 0x000000FF); + } + + return decoded; + } +} diff --git a/Controllers/PhilipsWizController/PhilipsWizController.cpp b/Controllers/PhilipsWizController/PhilipsWizController.cpp index aef1648a..efb92fb6 100644 --- a/Controllers/PhilipsWizController/PhilipsWizController.cpp +++ b/Controllers/PhilipsWizController/PhilipsWizController.cpp @@ -175,12 +175,14 @@ void PhilipsWizController::ReceiveThreadFunction() { char recv_buf[1025]; + port.set_receive_timeout(1, 0); + while(ReceiveThreadRun.load()) { /*-----------------------------------------------------------------*\ | Receive up to 1024 bytes from the device with a 1s timeout | \*-----------------------------------------------------------------*/ - int size = port.udp_listen_timeout(recv_buf, 1024, 1, 0); + int size = port.udp_listen(recv_buf, 1024); if(size > 0) { diff --git a/net_port/net_port.cpp b/net_port/net_port.cpp index a369c42e..8810a512 100644 --- a/net_port/net_port.cpp +++ b/net_port/net_port.cpp @@ -52,7 +52,12 @@ net_port::~net_port() } } -bool net_port::udp_client(const char * client_name, const char * port) +bool net_port::udp_client(const char* client_name, const char * port) +{ + return(udp_client(client_name, port, "0")); +} + +bool net_port::udp_client(const char * client_name, const char * send_port, const char * recv_port) { sockaddr_in myAddress; @@ -73,7 +78,7 @@ bool net_port::udp_client(const char * client_name, const char * port) myAddress.sin_family = AF_INET; myAddress.sin_addr.s_addr = inet_addr("0.0.0.0"); - myAddress.sin_port = htons(0); + myAddress.sin_port = htons(atoi(recv_port)); if(bind(sock, (sockaddr*)&myAddress, sizeof(myAddress)) == SOCKET_ERROR) { @@ -85,7 +90,7 @@ bool net_port::udp_client(const char * client_name, const char * port) addrinfo hints = {}; hints.ai_family = AF_INET; hints.ai_socktype = SOCK_DGRAM; - if(getaddrinfo(client_name, port, &hints, &result_list) == 0) + if(getaddrinfo(client_name, send_port, &hints, &result_list) == 0) { memcpy(&addrDest, result_list->ai_addr, result_list->ai_addrlen); freeaddrinfo(result_list); @@ -99,28 +104,32 @@ bool net_port::udp_client(const char * client_name, const char * port) } } +void net_port::udp_join_multicast_group(const char * group_name) +{ + struct ip_mreq group; + + group.imr_multiaddr.s_addr = inet_addr(group_name); + group.imr_interface.s_addr = inet_addr("0.0.0.0"); + + setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char *)&group, sizeof(group)); +} + int net_port::udp_listen(char * recv_data, int length) { return(recvfrom(sock, recv_data, length, 0, NULL, NULL)); } -int net_port::udp_listen_timeout(char * recv_data, int length, int sec, int usec) +void net_port::set_receive_timeout(int sec, int usec) { - fd_set fds; +#ifdef WIN32 + DWORD tv = ( sec * 1000 ) + ( usec / 1000 ); +#else struct timeval tv; - - FD_ZERO(&fds); - FD_SET(sock, &fds); - tv.tv_sec = sec; tv.tv_usec = usec; +#endif - if(select((int)sock, &fds, NULL, NULL, &tv) <= 0) - { - return(0); - } - - return(recvfrom(sock, recv_data, length, 0, NULL, NULL)); + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv); } int net_port::udp_write(char * buffer, int length) diff --git a/net_port/net_port.h b/net_port/net_port.h index 86eea078..7f0ff414 100644 --- a/net_port/net_port.h +++ b/net_port/net_port.h @@ -58,6 +58,7 @@ public: //Function to open the port bool udp_client(const char* client_name, const char * port); + bool udp_client(const char * client_name, const char * send_port, const char * recv_port); bool tcp_client(const char* client_name, const char * port); bool tcp_client_connect(); @@ -67,8 +68,9 @@ public: SOCKET * tcp_server_get_client(std::size_t client_idx); SOCKET * tcp_server_listen(); + void udp_join_multicast_group(const char * group_name); + int udp_listen(char * recv_data, int length); - int udp_listen_timeout(char * recv_data, int length, int sec, int usec); int tcp_listen(char * recv_data, int length); //Function to write data to the serial port @@ -78,6 +80,8 @@ public: void tcp_close(); + void set_receive_timeout(int sec, int usec); + bool connected; SOCKET sock; diff --git a/qt/OpenRGBDialog/OpenRGBDialog.cpp b/qt/OpenRGBDialog/OpenRGBDialog.cpp index fbdb32d4..a160615b 100644 --- a/qt/OpenRGBDialog/OpenRGBDialog.cpp +++ b/qt/OpenRGBDialog/OpenRGBDialog.cpp @@ -494,6 +494,11 @@ OpenRGBDialog::OpenRGBDialog(QWidget *parent) : QMainWindow(parent), ui(new Open \*-----------------------------------------------------*/ AddE131SettingsPage(); + /*-----------------------------------------------------*\ + | Add the Govee settings page | + \*-----------------------------------------------------*/ + AddGoveeSettingsPage(); + /*-----------------------------------------------------*\ | Add the Kasa Smart settings page | \*-----------------------------------------------------*/ @@ -833,6 +838,23 @@ void OpenRGBDialog::AddE131SettingsPage() ui->SettingsTabBar->tabBar()->setTabButton(ui->SettingsTabBar->tabBar()->count() - 1, QTabBar::LeftSide, SettingsTabLabel); } +void OpenRGBDialog::AddGoveeSettingsPage() +{ + /*-----------------------------------------------------*\ + | Create the Settings page | + \*-----------------------------------------------------*/ + GoveeSettingsPage = new OpenRGBGoveeSettingsPage(); + + ui->SettingsTabBar->addTab(GoveeSettingsPage, ""); + + /*-----------------------------------------------------*\ + | Create the tab label | + \*-----------------------------------------------------*/ + TabLabel* SettingsTabLabel = new TabLabel(OpenRGBFont::bulb, tr("Govee Devices"), (char *)"Govee Devices", (char *)context); + + ui->SettingsTabBar->tabBar()->setTabButton(ui->SettingsTabBar->tabBar()->count() - 1, QTabBar::LeftSide, SettingsTabLabel); +} + void OpenRGBDialog::AddKasaSmartSettingsPage() { /*-----------------------------------------------------*\ diff --git a/qt/OpenRGBDialog/OpenRGBDialog.h b/qt/OpenRGBDialog/OpenRGBDialog.h index 718a2e88..00d81426 100644 --- a/qt/OpenRGBDialog/OpenRGBDialog.h +++ b/qt/OpenRGBDialog/OpenRGBDialog.h @@ -29,6 +29,7 @@ #include "OpenRGBE131SettingsPage/OpenRGBE131SettingsPage.h" #include "OpenRGBElgatoKeyLightSettingsPage/OpenRGBElgatoKeyLightSettingsPage.h" #include "OpenRGBElgatoLightStripSettingsPage/OpenRGBElgatoLightStripSettingsPage.h" +#include "OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsPage.h" #include "OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.h" #include "OpenRGBLIFXSettingsPage/OpenRGBLIFXSettingsPage.h" #include "OpenRGBPhilipsHueSettingsPage/OpenRGBPhilipsHueSettingsPage.h" @@ -103,6 +104,7 @@ private: OpenRGBE131SettingsPage *E131SettingsPage; OpenRGBElgatoKeyLightSettingsPage *ElgatoKeyLightSettingsPage; OpenRGBElgatoLightStripSettingsPage *ElgatoLightStripSettingsPage; + OpenRGBGoveeSettingsPage *GoveeSettingsPage; OpenRGBKasaSmartSettingsPage *KasaSmartSettingsPage; OpenRGBLIFXSettingsPage *LIFXSettingsPage; OpenRGBPhilipsHueSettingsPage *PhilipsHueSettingsPage; @@ -134,6 +136,7 @@ private: void AddE131SettingsPage(); void AddElgatoKeyLightSettingsPage(); void AddElgatoLightStripSettingsPage(); + void AddGoveeSettingsPage(); void AddKasaSmartSettingsPage(); void AddLIFXSettingsPage(); void AddPhilipsHueSettingsPage(); diff --git a/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsEntry.cpp b/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsEntry.cpp new file mode 100644 index 00000000..4b8e31fd --- /dev/null +++ b/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsEntry.cpp @@ -0,0 +1,35 @@ +/*---------------------------------------------------------*\ +| OpenRGBGoveeSettingsEntry.cpp | +| | +| User interface for OpenRGB Govee settings entry | +| | +| Adam Honse (calcprogrammer1@gmail.com) 15 May 2025 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-only | +\*---------------------------------------------------------*/ + +#include "OpenRGBGoveeSettingsEntry.h" +#include "ui_OpenRGBGoveeSettingsEntry.h" + +using namespace Ui; + +OpenRGBGoveeSettingsEntry::OpenRGBGoveeSettingsEntry(QWidget *parent) : + QWidget(parent), + ui(new Ui::OpenRGBGoveeSettingsEntryUi) +{ + ui->setupUi(this); +} + +OpenRGBGoveeSettingsEntry::~OpenRGBGoveeSettingsEntry() +{ + delete ui; +} + +void OpenRGBGoveeSettingsEntry::changeEvent(QEvent *event) +{ + if(event->type() == QEvent::LanguageChange) + { + ui->retranslateUi(this); + } +} diff --git a/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsEntry.h b/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsEntry.h new file mode 100644 index 00000000..d12cb5cf --- /dev/null +++ b/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsEntry.h @@ -0,0 +1,33 @@ +/*---------------------------------------------------------*\ +| OpenRGBGoveeSettingsEntry.h | +| | +| User interface for OpenRGB Govee settings entry | +| | +| Adam Honse (calcprogrammer1@gmail.com) 15 May 2025 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-only | +\*---------------------------------------------------------*/ + +#pragma once + +#include +#include "ui_OpenRGBGoveeSettingsEntry.h" + +namespace Ui +{ + class OpenRGBGoveeSettingsEntry; +} + +class Ui::OpenRGBGoveeSettingsEntry : public QWidget +{ + Q_OBJECT + +public: + explicit OpenRGBGoveeSettingsEntry(QWidget *parent = nullptr); + ~OpenRGBGoveeSettingsEntry(); + Ui::OpenRGBGoveeSettingsEntryUi *ui; + +private slots: + void changeEvent(QEvent *event); +}; diff --git a/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsEntry.ui b/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsEntry.ui new file mode 100644 index 00000000..19b7995b --- /dev/null +++ b/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsEntry.ui @@ -0,0 +1,46 @@ + + + OpenRGBGoveeSettingsEntryUi + + + + 0 + 0 + 328 + 72 + + + + + 0 + 0 + + + + Philips Wiz Settings Entry + + + + + + + + + + + + IP: + + + + + + + + + + + + + + diff --git a/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsPage.cpp b/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsPage.cpp new file mode 100644 index 00000000..5e612e4e --- /dev/null +++ b/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsPage.cpp @@ -0,0 +1,125 @@ +/*---------------------------------------------------------*\ +| OpenRGBGoveeSettingsPage.cpp | +| | +| User interface for OpenRGB Govee settings page | +| | +| Adam Honse (calcprogrammer1@gmail.com) 15 May 2025 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-only | +\*---------------------------------------------------------*/ + +#include "OpenRGBGoveeSettingsPage.h" +#include "ui_OpenRGBGoveeSettingsPage.h" +#include "ResourceManager.h" +#include "SettingsManager.h" + +using namespace Ui; + +OpenRGBGoveeSettingsPage::OpenRGBGoveeSettingsPage(QWidget *parent) : + QWidget(parent), + ui(new Ui::OpenRGBGoveeSettingsPageUi) +{ + ui->setupUi(this); + + json govee_settings; + + /*-------------------------------------------------*\ + | Get Govee settings | + \*-------------------------------------------------*/ + govee_settings = ResourceManager::get()->GetSettingsManager()->GetSettings("GoveeDevices"); + + /*-------------------------------------------------*\ + | If the Govee settings contains devices, process | + \*-------------------------------------------------*/ + if(govee_settings.contains("devices")) + { + for(unsigned int device_idx = 0; device_idx < govee_settings["devices"].size(); device_idx++) + { + OpenRGBGoveeSettingsEntry* entry = new OpenRGBGoveeSettingsEntry; + + if(govee_settings["devices"][device_idx].contains("ip")) + { + entry->ui->IPEdit->setText(QString::fromStdString(govee_settings["devices"][device_idx]["ip"])); + } + + entries.push_back(entry); + + QListWidgetItem* item = new QListWidgetItem; + + item->setSizeHint(entry->sizeHint()); + + ui->GoveeDeviceList->addItem(item); + ui->GoveeDeviceList->setItemWidget(item, entry); + ui->GoveeDeviceList->show(); + } + } +} + +OpenRGBGoveeSettingsPage::~OpenRGBGoveeSettingsPage() +{ + delete ui; +} + +void OpenRGBGoveeSettingsPage::changeEvent(QEvent *event) +{ + if(event->type() == QEvent::LanguageChange) + { + ui->retranslateUi(this); + } +} + +void Ui::OpenRGBGoveeSettingsPage::on_AddGoveeDeviceButton_clicked() +{ + OpenRGBGoveeSettingsEntry* entry = new OpenRGBGoveeSettingsEntry; + entries.push_back(entry); + + QListWidgetItem* item = new QListWidgetItem; + + item->setSizeHint(entry->sizeHint()); + + ui->GoveeDeviceList->addItem(item); + ui->GoveeDeviceList->setItemWidget(item, entry); + ui->GoveeDeviceList->show(); +} + +void Ui::OpenRGBGoveeSettingsPage::on_RemoveGoveeDeviceButton_clicked() +{ + int cur_row = ui->GoveeDeviceList->currentRow(); + + if(cur_row < 0) + { + return; + } + + QListWidgetItem* item = ui->GoveeDeviceList->takeItem(cur_row); + + ui->GoveeDeviceList->removeItemWidget(item); + delete item; + + delete entries[cur_row]; + entries.erase(entries.begin() + cur_row); +} + +void Ui::OpenRGBGoveeSettingsPage::on_SaveGoveeConfigurationButton_clicked() +{ + json govee_settings; + + /*-------------------------------------------------*\ + | Get Govee settings | + \*-------------------------------------------------*/ + govee_settings = ResourceManager::get()->GetSettingsManager()->GetSettings("GoveeDevices"); + + govee_settings["devices"].clear(); + + for(unsigned int device_idx = 0; device_idx < entries.size(); device_idx++) + { + /*-------------------------------------------------*\ + | Required parameters | + \*-------------------------------------------------*/ + govee_settings["devices"][device_idx]["ip"] = entries[device_idx]->ui->IPEdit->text().toStdString(); + } + + ResourceManager::get()->GetSettingsManager()->SetSettings("GoveeDevices", govee_settings); + ResourceManager::get()->GetSettingsManager()->SaveSettings(); +} diff --git a/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsPage.h b/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsPage.h new file mode 100644 index 00000000..5e3a2560 --- /dev/null +++ b/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsPage.h @@ -0,0 +1,43 @@ +/*---------------------------------------------------------*\ +| OpenRGBGoveeSettingsPage.h | +| | +| User interface for OpenRGB Govee settings page | +| | +| Adam Honse (calcprogrammer1@gmail.com) 15 May 2025 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-only | +\*---------------------------------------------------------*/ + +#pragma once + +#include +#include "ui_OpenRGBGoveeSettingsPage.h" +#include "OpenRGBGoveeSettingsEntry.h" + +namespace Ui +{ + class OpenRGBGoveeSettingsPage; +} + +class Ui::OpenRGBGoveeSettingsPage : public QWidget +{ + Q_OBJECT + +public: + explicit OpenRGBGoveeSettingsPage(QWidget *parent = nullptr); + ~OpenRGBGoveeSettingsPage(); + +private slots: + void changeEvent(QEvent *event); + void on_AddGoveeDeviceButton_clicked(); + + void on_RemoveGoveeDeviceButton_clicked(); + + void on_SaveGoveeConfigurationButton_clicked(); + +private: + Ui::OpenRGBGoveeSettingsPageUi *ui; + std::vector entries; + +}; diff --git a/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsPage.ui b/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsPage.ui new file mode 100644 index 00000000..49158873 --- /dev/null +++ b/qt/OpenRGBGoveeSettingsPage/OpenRGBGoveeSettingsPage.ui @@ -0,0 +1,49 @@ + + + OpenRGBGoveeSettingsPageUi + + + + 0 + 0 + 400 + 300 + + + + Philips Wiz Settings Page + + + + + + Add + + + + + + + Remove + + + + + + + Save + + + + + + + QAbstractItemView::ScrollPerPixel + + + + + + + +