From 5cfd064da1a08bdb72dc4e7d4462d58a9b5ff5fb Mon Sep 17 00:00:00 2001 From: Richard Harris Date: Tue, 6 Jan 2026 14:18:42 +0000 Subject: [PATCH] Robobloq Monitor Light Strip support --- .../RGBController_RobobloqLightStrip.cpp | 224 ++++++++++ .../RGBController_RobobloqLightStrip.h | 37 ++ .../RobobloqLightStripController.cpp | 421 ++++++++++++++++++ .../RobobloqLightStripController.h | 128 ++++++ .../RobobloqLightStripControllerDetect.cpp | 39 ++ .../RobobloqRangeMerger.cpp | 155 +++++++ .../RobobloqRangeMerger.h | 15 + 7 files changed, 1019 insertions(+) create mode 100644 Controllers/RobobloqLightStripController/RGBController_RobobloqLightStrip.cpp create mode 100644 Controllers/RobobloqLightStripController/RGBController_RobobloqLightStrip.h create mode 100644 Controllers/RobobloqLightStripController/RobobloqLightStripController.cpp create mode 100644 Controllers/RobobloqLightStripController/RobobloqLightStripController.h create mode 100644 Controllers/RobobloqLightStripController/RobobloqLightStripControllerDetect.cpp create mode 100644 Controllers/RobobloqLightStripController/RobobloqRangeMerger.cpp create mode 100644 Controllers/RobobloqLightStripController/RobobloqRangeMerger.h diff --git a/Controllers/RobobloqLightStripController/RGBController_RobobloqLightStrip.cpp b/Controllers/RobobloqLightStripController/RGBController_RobobloqLightStrip.cpp new file mode 100644 index 000000000..02df76a79 --- /dev/null +++ b/Controllers/RobobloqLightStripController/RGBController_RobobloqLightStrip.cpp @@ -0,0 +1,224 @@ +/*---------------------------------------------------------*\ +| RGBController_RobobloqLightStrip.cpp | +| | +| Detector for Robobloq Monitor Light Strip | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#include "RGBController_RobobloqLightStrip.h" +#include "RobobloqLightStripController.h" +#include + +/**--------------------------------------------------------------------*\ + @name Robobloq Monitor Light Strip + @category LEDStrip + @type USB + @save :robot: + @direct :white_check_mark: + @effects :white_check_mark: + @detectors DetectRobobloqLightStripController + @comment +\*---------------------------------------------------------------------*/ + +RGBController_RobobloqLightStrip::RGBController_RobobloqLightStrip(RobobloqLightStripController* controller_ptr) +{ + controller = controller_ptr; + + name = controller->GetDeviceName(); + vendor = "Robobloq"; + description = "Robobloq Monitor Light Strip (" + std::to_string(controller->GetPhysicalSizeInInches()) + "\")"; + type = DEVICE_TYPE_LEDSTRIP; + location = controller->GetDeviceLocation(); + serial = controller->GetSerialString(); + version = controller->GetFirmwareVersion(); + + mode Static; + Static.name = "Static"; + Static.value = ROBOBLOQ_MODE_STATIC; + Static.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS; + Static.color_mode = MODE_COLORS_MODE_SPECIFIC; + Static.brightness_min = 1; + Static.brightness_max = 255; + Static.brightness = 255; + Static.colors_min = 1; + Static.colors_max = 1; + Static.colors.resize(1); + modes.push_back(Static); + + mode Direct; + Direct.name = "Direct"; + Direct.value = ROBOBLOQ_MODE_DIRECT; + Direct.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_HAS_BRIGHTNESS; + Direct.color_mode = MODE_COLORS_PER_LED; + Direct.brightness_min = 1; + Direct.brightness_max = 255; + Direct.brightness = 255; + modes.push_back(Direct); + + /*-----------------------------------------------------*\ + | Add dynamic modes | + \*-----------------------------------------------------*/ + struct DynamicMode + { + const char* name; + int value; + }; + + DynamicMode dynamic_modes[] = + { + { "Rainbow Wave", ROBOBLOQ_MODE_DYNAMIC_RAINBOW }, + { "Breathing", ROBOBLOQ_MODE_DYNAMIC_BREATHING }, + { "Twist", ROBOBLOQ_MODE_DYNAMIC_TWIST }, + { "Beat", ROBOBLOQ_MODE_DYNAMIC_BEAT }, + { "Twirl", ROBOBLOQ_MODE_DYNAMIC_TWIRL }, + { "Lemon", ROBOBLOQ_MODE_DYNAMIC_LEMON }, + { "Electric", ROBOBLOQ_MODE_DYNAMIC_ELECTRIC }, + }; + + for(unsigned int i = 0; i < (sizeof(dynamic_modes) / sizeof(DynamicMode)); i++) + { + mode new_mode; + new_mode.name = dynamic_modes[i].name; + new_mode.value = dynamic_modes[i].value; + new_mode.flags = MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_BRIGHTNESS; + new_mode.color_mode = MODE_COLORS_NONE; + new_mode.speed_min = 0; + new_mode.speed_max = 100; + new_mode.speed = 50; + new_mode.brightness_min = 1; + new_mode.brightness_max = 255; + new_mode.brightness = 255; + modes.push_back(new_mode); + } + + mode Off; + Off.name = "Off"; + Off.value = ROBOBLOQ_MODE_OFF; + Off.flags = 0; + Off.color_mode = MODE_COLORS_NONE; + modes.push_back(Off); + + SetupZones(); +}; + +RGBController_RobobloqLightStrip::~RGBController_RobobloqLightStrip() +{ + delete controller; +} + +void RGBController_RobobloqLightStrip::SetupZones() +{ + int led_count = controller->GetLEDCount(); + int leds_per_side = controller->GetLEDsPerSide(); + + struct Side + { + const char* name; + int count; + }; + + std::vector sides = { { "Light Strip", led_count } }; + if(leds_per_side > 0) + { + sides = { + { "Right", leds_per_side }, + { "Top", led_count - (leds_per_side * 2) }, + { "Left", leds_per_side }, + }; + } + + zones.clear(); + zones.resize(sides.size()); + leds.clear(); + leds.resize(led_count); + + for(unsigned int i = 0; i < sides.size(); i++) + { + zone& zone = zones[i]; + zone.name = sides[i].name; + zone.type = ZONE_TYPE_LINEAR; + zone.leds_count = sides[i].count; + zone.leds_min = zone.leds_count; + zone.leds_max = zone.leds_count; + zone.matrix_map = NULL; + } + + for(int i = 0; i < led_count; i++) + { + led& new_led = leds[i]; + new_led.name = "LED " + std::to_string(i + 1); + new_led.value = i; + } + + SetupColors(); +} + +void RGBController_RobobloqLightStrip::ResizeZone(int /*zone*/, int /*new_size*/) +{ + /*-----------------------------------------------------*\ + | This device does not support resizing zones | + \*-----------------------------------------------------*/ +} + +void RGBController_RobobloqLightStrip::DeviceUpdateLEDs() +{ + if(modes[active_mode].value == ROBOBLOQ_MODE_DIRECT) + { + controller->SetCustom(colors); + } +} + +void RGBController_RobobloqLightStrip::UpdateZoneLEDs(int /*zone*/) +{ + DeviceUpdateLEDs(); +} + +void RGBController_RobobloqLightStrip::UpdateSingleLED(int led) +{ + controller->SetLEDColor(led, modes[active_mode].colors[0]); +} + +void RGBController_RobobloqLightStrip::DeviceUpdateMode() +{ + bool mode_changed = false; + + /*-----------------------------------------------------*\ + | Cache mode to avoid repeated SetDynamicEffect calls | + | when adjusting speed/brightness | + \*-----------------------------------------------------*/ + if(modes[active_mode].value != cur_mode) + { + cur_mode = modes[active_mode].value; + mode_changed = true; + } + + if(cur_mode == ROBOBLOQ_MODE_OFF) + { + controller->TurnOff(); + } + else if(cur_mode == ROBOBLOQ_MODE_DIRECT) + { + controller->SetBrightness(modes[active_mode].brightness); + } + else if(cur_mode == ROBOBLOQ_MODE_STATIC) + { + controller->SetColor(modes[active_mode].colors[0]); + controller->SetBrightness(modes[active_mode].brightness); + } + else if(ROBOBLOQ_IS_DYNAMIC_EFFECT(cur_mode)) + { + if(mode_changed) + { + controller->SetDynamicEffect(cur_mode); + } + + controller->SetBrightness(modes[active_mode].brightness); + controller->SetDynamicSpeed(modes[active_mode].speed); + } + else + { + LOG_ERROR("[Robobloq] Requested mode (%02x) is not supported", cur_mode); + } +} diff --git a/Controllers/RobobloqLightStripController/RGBController_RobobloqLightStrip.h b/Controllers/RobobloqLightStripController/RGBController_RobobloqLightStrip.h new file mode 100644 index 000000000..3b3d50f63 --- /dev/null +++ b/Controllers/RobobloqLightStripController/RGBController_RobobloqLightStrip.h @@ -0,0 +1,37 @@ +/*---------------------------------------------------------*\ +| RGBController_RobobloqLightStrip.h | +| | +| Detector for Robobloq Monitor Light Strip | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#pragma once + +#include "RGBController.h" +#include "RobobloqLightStripController.h" + +class RGBController_RobobloqLightStrip : public RGBController +{ +public: + RGBController_RobobloqLightStrip(RobobloqLightStripController* controller_ptr); + ~RGBController_RobobloqLightStrip(); + + void SetupZones(); + + void ResizeZone(int zone, int new_size); + + void DeviceUpdateLEDs(); + void UpdateZoneLEDs(int zone); + void UpdateSingleLED(int led); + + void DeviceUpdateMode(); + +private: + /*-----------------------------------------------------*\ + | Last mode set via this controller | + \*-----------------------------------------------------*/ + int cur_mode = -1; + RobobloqLightStripController* controller; +}; diff --git a/Controllers/RobobloqLightStripController/RobobloqLightStripController.cpp b/Controllers/RobobloqLightStripController/RobobloqLightStripController.cpp new file mode 100644 index 000000000..f9b1e366d --- /dev/null +++ b/Controllers/RobobloqLightStripController/RobobloqLightStripController.cpp @@ -0,0 +1,421 @@ +/*---------------------------------------------------------*\ +| RobobloqLightStripController.cpp | +| | +| Detector for Robobloq Monitor Light Strip | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "LogManager.h" +#include "RobobloqLightStripController.h" +#include "RobobloqRangeMerger.h" +#include "RGBController.h" +#include + +using namespace std::chrono_literals; + +RobobloqLightStripController::RobobloqLightStripController(hid_device* dev_handle, const char* path, std::string dev_name) +{ + dev = dev_handle; + location = path; + name = dev_name; + packet_index = 0x02; + led_count = 0; + + RequestDeviceInfo(); + Initialize(); +} + +RobobloqLightStripController::~RobobloqLightStripController() +{ + hid_close(dev); +} + +std::string RobobloqLightStripController::GetDeviceLocation() +{ + return location; +} + +std::string RobobloqLightStripController::GetDeviceName() +{ + return name; +} + +std::string RobobloqLightStripController::GetSerialString() +{ + return uuid; +} + +std::string RobobloqLightStripController::GetFirmwareVersion() +{ + return firmware_version; +} + +int RobobloqLightStripController::GetLEDCount() +{ + return led_count; +} + +int RobobloqLightStripController::GetLEDsPerSide() +{ + switch(physical_size) + { + case 34: + return 15; + default: + return 0; /* unknown */ + } +} + + +int RobobloqLightStripController::GetPhysicalSizeInInches() +{ + return physical_size; +} + +void RobobloqLightStripController::Initialize() +{ + RequestDeviceInfo(); + + /*-----------------------------------------------------*\ + | This tells the device (permanently) not to try to use | + | its keyboard to open a URL to the driver download | + | page. Yes, it really does that. | + \*-----------------------------------------------------*/ + SendPacket({ROBOBLOQ_CMD_SET_OPEN_URL, 0x00}); + + SetBrightness(0xF9); + SetDynamicSpeed(0x32); +} + +/*---------------------------------------------------------*\ +| Set the entire light strip to the specified color. | +\*---------------------------------------------------------*/ +void RobobloqLightStripController::SetColor(RGBColor c) +{ + std::vector payload = { + 0x86, + 0x01, + (unsigned char)RGBGetRValue(c), + (unsigned char)RGBGetGValue(c), + (unsigned char)RGBGetBValue(c), + led_count, + /*-------------------------------------------------*\ + | Add dummy range (matches original application) | + \*-------------------------------------------------*/ + (unsigned char)(led_count + 1), + 0x00, + 0x00, + 0x00, + 0xFE, + }; + SendPacket(payload, false); +} + +/*---------------------------------------------------------*\ +| Set a single LED to the specified color. LED is 0-indexed | +\*---------------------------------------------------------*/ +void RobobloqLightStripController::SetLEDColor(int led, RGBColor c) +{ + std::vector payload = { + 0x86, + (unsigned char)(led + 1), + (unsigned char)RGBGetRValue(c), + (unsigned char)RGBGetGValue(c), + (unsigned char)RGBGetBValue(c), + (unsigned char)(led + 1), + }; + SendPacket(payload, false); +} + +void RobobloqLightStripController::SetColorRanges(const std::vector& ranges) +{ + /*-----------------------------------------------------*\ + | 6 bytes overhead (header + command + checksum) plus | + | ranges must fit in 64 bytes | + \*-----------------------------------------------------*/ + if((ranges.size() + 6) > 64) + { + LOG_ERROR("[Robobloq] SetColorRanges: Too many ranges (%d) for packet size", (int)ranges.size()); + return; + } + std::vector payload = { 0x86 }; + payload.insert(payload.end(), ranges.begin(), ranges.end()); + SendPacket(payload, false); +} + +void RobobloqLightStripController::SetBrightness(unsigned char brightness) +{ + /*-----------------------------------------------------*\ + | 0 value is interpreted as max (255) | + \*-----------------------------------------------------*/ + if(brightness == 0) + { + brightness = 1; + } + + SendPacket({ROBOBLOQ_CMD_SET_BRIGHTNESS, brightness}); +} + +void RobobloqLightStripController::SetDynamicEffect(unsigned char effect) +{ + SendPacket({ROBOBLOQ_CMD_SET_EFFECT, ROBOBLOQ_EFFECT_DYNAMIC, effect}); +} + +void RobobloqLightStripController::SetDynamicSpeed(unsigned char speed) +{ + /*-----------------------------------------------------*\ + | Device expects 0x00 = fast, 0x64 = slow | + \*-----------------------------------------------------*/ + speed = ROBOBLOQ_DYNAMIC_SPEED_MAX - speed; + + SendPacket({ROBOBLOQ_CMD_SET_DYNAMIC_SPEED, speed}); +} + +void RobobloqLightStripController::TurnOff() +{ + /*-----------------------------------------------------*\ + | The device doesn't really turn off: we send a command | + | to disable any device-side animations, then set all | + | LEDs to black. | + \*-----------------------------------------------------*/ + SendPacket({ROBOBLOQ_CMD_TURN_OFF}); + SetColor(COLOR_BLACK); +} + +/*---------------------------------------------------------*\ +| Update all LED colors at once. | +| | +| The official app reduces 71 LED values to 34 distinct | +| ranges before sending them to the device, so we do the | +| same. SendSyncScreen is capable of sending any number of | +| ranges (e.g 71 - 1 per pixel) but this has not been | +| properly tested. | +\*---------------------------------------------------------*/ +void RobobloqLightStripController::SetCustom(const std::vector& colors) +{ + int num_leds = (int)colors.size(); + if(num_leds != this->led_count) + { + LOG_ERROR("[Robobloq] SetCustom: Number of colors (%d) does not match LED count (%d), rejecting", num_leds, this->led_count); + return; + } + + std::vector color_bytes = MergeRobobloqRanges(colors, ROBOBLOQ_TUPLE_COUNT); + + SendSyncScreen(color_bytes); +} + +/*---------------------------------------------------------*\ +| Internal method to send color ranges to the device. | +\*---------------------------------------------------------*/ +void RobobloqLightStripController::SendSyncScreen(const std::vector& color_bytes) +{ + if(color_bytes.size() % 5 != 0) + { + LOG_ERROR("[Robobloq] SendSyncScreen: color_bytes size (%d) is not a multiple of 5, rejecting", (int)color_bytes.size()); + return; + } + + SendMultiPacket(ROBOBLOQ_CMD_SET_SYNC_SCREEN, color_bytes); +} + +/*---------------------------------------------------------*\ +| Sends a multi-packet command to the device. | +| | +| This allows payloads >64 bytes, e.g. SendSyncScreen | +\*---------------------------------------------------------*/ +void RobobloqLightStripController::SendMultiPacket(unsigned char command, const std::vector& payload) +{ + /*-----------------------------------------------------*\ + | Allow for 6-byte header and trailing checksum | + \*-----------------------------------------------------*/ + unsigned short len = (unsigned short)(payload.size() + 7); + + std::vector data = {0x53, 0x43, (unsigned char)(len >> 8), (unsigned char)(len & 0xFF), packet_index, command}; + data.insert(data.end(), payload.begin(), payload.end()); + + /*-----------------------------------------------------*\ + | Calculate checksum | + \*-----------------------------------------------------*/ + unsigned int sum = 0; + for(std::size_t i = 0; i < data.size(); i++) + { + sum += data[i]; + } + data.push_back(sum & 0xFF); + + /*-----------------------------------------------------*\ + | Pad to a multiple of 64 bytes | + \*-----------------------------------------------------*/ + size_t remainder = data.size() % 64; + if(remainder > 0) + { + data.resize(data.size() + 64 - remainder, 0x00); + } + + /*-----------------------------------------------------*\ + | Send in chunks of 64 bytes | + \*-----------------------------------------------------*/ + for(size_t i = 0; i < data.size(); i += 64) + { + WriteReport(&data[i]); + + /*-------------------------------------------------*\ + | Microsleep to avoid overloading the device | + \*-------------------------------------------------*/ + std::this_thread::sleep_for(1ms); + } + + IncPacketIndex(); +} + +void RobobloqLightStripController::IncPacketIndex() +{ + packet_index = (packet_index + 1) & 0xFF; +} + +void RobobloqLightStripController::WriteReport(const unsigned char* data /* 64 bytes*/) +{ + /*-----------------------------------------------------*\ + | Add report ID | + \*-----------------------------------------------------*/ + std::vector report(65); + report[0] = 0x00; + memcpy(&report[1], data, 64); + + hid_write(dev, report.data(), report.size()); +} + +void RobobloqLightStripController::SendPacket(const std::vector& command, bool flush) +{ + unsigned char length = (unsigned char)(command.size() + 5); + + if(length > 64) + { + LOG_ERROR("[Robobloq] SendPacket: command size (%d) is too large, rejecting", (int)command.size()); + return; + } + + std::vector packet = {0x52, 0x42, length, packet_index}; + packet.insert(packet.end(), command.begin(), command.end()); + + unsigned int csum = 0; + for(std::size_t i = 0; i < packet.size(); i++) + { + csum += packet[i]; + } + packet.push_back(csum & 0xFF); + + packet.resize(64, 0x00); + + WriteReport(packet.data()); + IncPacketIndex(); + + if(flush) + { + /*-------------------------------------------------*\ + | Flush away any awaiting IN packets | + \*-------------------------------------------------*/ + unsigned char buf[64]; + + int res = 1; + while(res > 0) + { + res = hid_read_timeout(dev, buf, 64, 0); + } + } +} + +std::vector RobobloqLightStripController::SendPacketWithReply(const std::vector& command) +{ + unsigned char expected_id = packet_index; + LOG_DEBUG("[Robobloq] WithReply: sending command %02x, expecting reply for packet ID %02x", command[0], expected_id); + SendPacket(command, false); + + int tries = 3; + while(tries--) + { + unsigned char buf[64]; + int res = 1; + while(res > 0) + { + std::this_thread::sleep_for(10ms); + res = hid_read_timeout(dev, buf, 64, 1000); + LOG_DEBUG("[Robobloq] WithReply: read call returned %d packet ID %02x", res, buf[3]); + + if(buf[3] == expected_id) + { + return std::vector(buf, buf + res); + } + } + + /*-------------------------------------------------*\ + | Sometimes device gets stuck and we need to send a | + | new command before we can receive the previous | + | command's reply, so send a no-op | + \*-------------------------------------------------*/ + LOG_DEBUG("[Robobloq] No matching reply, sending no-op to try to unstick device"); + SendPacket({ROBOBLOQ_CMD_SET_OPEN_URL, 0x00}, false); + } + + /*-----------------------------------------------------*\ + | We weren't able to get a reply for this message | + \*-----------------------------------------------------*/ + return {}; +} + +/*** + * Asks device for information and update our attributes. When sent 0x82, the device + * returns information in the following form: + * + * header ID uuid v1.8.2 + * /------------\ /======\ /-----------------------\ /------\ + * 52 42 19 02 82 00 05 01 | 22 01 00 47 cd ab c5 74 | 25 bc b7 dc e5 01 08 02 + * | ? ? | ? + * | led count (71) + * display size (34") + * v1.8.2 is device fw version + */ +bool RobobloqLightStripController::RequestDeviceInfo() +{ + std::vector data = SendPacketWithReply({ROBOBLOQ_CMD_READ_DEVICE_INFO}); + if(data.size() < 24) + { + LOG_DEBUG("[Robobloq] Device Info Data too small! LED count -> 0. Non-Direct modes will work"); + return false; + } + + this->physical_size = data[8]; + this->led_count = data[11]; + + char id_buf[7]; + snprintf(id_buf, sizeof(id_buf), "%02x%02x%02x", data[5], data[6], data[7]); + this->id = std::string(id_buf); + + char uuid_buf[17]; + for(int i = 0; i < 8; i++) + { + sprintf(uuid_buf + (i * 2), "%02x", data[12 + i]); + } + this->uuid = std::string(uuid_buf); + + char fw_buf[16]; + snprintf(fw_buf, sizeof(fw_buf), "%d.%d.%d", data[21], data[22], data[23]); + this->firmware_version = std::string(fw_buf); + + LOG_DEBUG("[Robobloq] Got device uuid: %s, fw: %s, size: %d\", leds: %d", + this->uuid.c_str(), this->firmware_version.c_str(), + this->physical_size, this->led_count); + + return true; +} diff --git a/Controllers/RobobloqLightStripController/RobobloqLightStripController.h b/Controllers/RobobloqLightStripController/RobobloqLightStripController.h new file mode 100644 index 000000000..2642700b2 --- /dev/null +++ b/Controllers/RobobloqLightStripController/RobobloqLightStripController.h @@ -0,0 +1,128 @@ +/*---------------------------------------------------------*\ +| RobobloqLightStripController.h | +| | +| Detector for Robobloq Monitor Light Strips | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#pragma once + +#include +#include +#include +#include "RGBController.h" + +/*-----------------------------------------*\ +| Lighting modes | +\*-----------------------------------------*/ +enum +{ + /*-------------------------------------*\ + | Dynamic | + \*-------------------------------------*/ + ROBOBLOQ_MODE_DYNAMIC_RAINBOW = 0x00, + ROBOBLOQ_MODE_DYNAMIC_BREATHING = 0x01, + ROBOBLOQ_MODE_DYNAMIC_TWIST = 0x02, + ROBOBLOQ_MODE_DYNAMIC_BEAT = 0x03, + ROBOBLOQ_MODE_DYNAMIC_TWIRL = 0x04, + ROBOBLOQ_MODE_DYNAMIC_LEMON = 0x05, + ROBOBLOQ_MODE_DYNAMIC_ELECTRIC = 0x06, + /*-------------------------------------*\ + | Rhythm (synced to music) | + \*-------------------------------------*/ + ROBOBLOQ_MODE_RHYTHM_CIRCLES = 0x07, + ROBOBLOQ_MODE_RHYTHM_TWIRL = 0x08, + ROBOBLOQ_MODE_RHYTHM_SPARKLE = 0x09, + ROBOBLOQ_MODE_RHYTHM_BUBBLES = 0x10, + ROBOBLOQ_MODE_RHYTHM_SPOTLIGHT = 0x11, + ROBOBLOQ_MODE_RHYTHM_RAINBOW = 0x12, + ROBOBLOQ_MODE_RHYTHM_BLAST = 0x13, + /*-------------------------------------*\ + | Virtual modes | + \*-------------------------------------*/ + ROBOBLOQ_MODE_OFF = 0xFFFD, + ROBOBLOQ_MODE_STATIC = 0xFFFE, + ROBOBLOQ_MODE_DIRECT = 0xFFFF, +}; + +#define ROBOBLOQ_IS_DYNAMIC_EFFECT(x) ((x) >= ROBOBLOQ_MODE_DYNAMIC_RAINBOW && (x) <= ROBOBLOQ_MODE_DYNAMIC_ELECTRIC) +#define ROBOBLOQ_IS_RHYTHM_EFFECT(x) ((x) >= ROBOBLOQ_MODE_RHYTHM_CIRCLES && (x) <= ROBOBLOQ_MODE_RHYTHM_BLAST) + +/*-----------------------------------------*\ +| Commands | +\*-----------------------------------------*/ +enum +{ + ROBOBLOQ_CMD_SET_SYNC_SCREEN = 0x80, + ROBOBLOQ_CMD_READ_DEVICE_INFO = 0x82, + ROBOBLOQ_CMD_SET_EFFECT = 0x85, + ROBOBLOQ_CMD_SET_COLOR = 0x86, + ROBOBLOQ_CMD_SET_BRIGHTNESS = 0x87, + ROBOBLOQ_CMD_SET_DYNAMIC_SPEED = 0x8A, + ROBOBLOQ_CMD_SET_OPEN_URL = 0x93, + ROBOBLOQ_CMD_TURN_OFF = 0x97, +}; + +/*-----------------------------------------*\ +| Effect categories | +\*-----------------------------------------*/ +enum +{ + ROBOBLOQ_EFFECT_DYNAMIC = 0x02, + ROBOBLOQ_EFFECT_RHYTHM = 0x03, +}; + +#define ROBOBLOQ_DYNAMIC_SPEED_MAX 0x64 + +/*---------------------------------------------------------*\ +| Number of (start, end, R, G, B) tuples that will be sent | +| in a SetCustom call | +\*---------------------------------------------------------*/ +#define ROBOBLOQ_TUPLE_COUNT 34 + +class RobobloqLightStripController +{ +public: + RobobloqLightStripController(hid_device* dev_handle, const char* path, std::string dev_name); + ~RobobloqLightStripController(); + + std::string GetDeviceLocation(); + std::string GetDeviceName(); + std::string GetSerialString(); + std::string GetFirmwareVersion(); + int GetLEDCount(); + int GetLEDsPerSide(); + int GetPhysicalSizeInInches(); + + void Initialize(); + void SetColor(RGBColor c); + void SetLEDColor(int led, RGBColor c); + void SetColorRanges(const std::vector& ranges); + void SetBrightness(unsigned char brightness); + void SetDynamicEffect(unsigned char effect); + void SetCustom(const std::vector& colors); + void SetDynamicSpeed(unsigned char speed); + void TurnOff(); + + +private: + hid_device* dev; + std::string name; + std::string location; + std::string id; + std::string uuid; + std::string firmware_version; + unsigned char led_count; + unsigned char physical_size; + unsigned char packet_index; + + void SendPacket(const std::vector& command, bool flush = true); + std::vector SendPacketWithReply(const std::vector& command); + void SendMultiPacket(unsigned char command, const std::vector& payload); + bool RequestDeviceInfo(); + void SendSyncScreen(const std::vector& color_bytes); + void IncPacketIndex(); + void WriteReport(const unsigned char* data); +}; diff --git a/Controllers/RobobloqLightStripController/RobobloqLightStripControllerDetect.cpp b/Controllers/RobobloqLightStripController/RobobloqLightStripControllerDetect.cpp new file mode 100644 index 000000000..1217bb0ac --- /dev/null +++ b/Controllers/RobobloqLightStripController/RobobloqLightStripControllerDetect.cpp @@ -0,0 +1,39 @@ +/*---------------------------------------------------------*\ +| RobobloqLightStripControllerDetect.cpp | +| | +| Detector for Robobloq RGB Light Strips | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#include "Detector.h" +#include "RobobloqLightStripController.h" +#include "RGBController_RobobloqLightStrip.h" + +#define ROBOBLOQ_USB_VID 0x1A86 + +/*----------------------------------------------------------*\ +| | +| DetectRobobloqLightStripController | +| | +| Detect Robobloq RGB Light Strips | +| | +\*----------------------------------------------------------*/ + +void DetectRobobloqLightStripController + ( + hid_device_info* info, + const std::string& name + ) +{ + hid_device* dev = hid_open_path(info->path); + if(dev != nullptr) + { + RobobloqLightStripController* controller = new RobobloqLightStripController(dev, info->path, name); + RGBController_RobobloqLightStrip* rgb_controller = new RGBController_RobobloqLightStrip(controller); + ResourceManager::get()->RegisterRGBController(rgb_controller); + } +} + +REGISTER_HID_DETECTOR_PU("Robobloq Monitor Light Strip", DetectRobobloqLightStripController, ROBOBLOQ_USB_VID, 0xFE07, 0xFF00, 0x01); diff --git a/Controllers/RobobloqLightStripController/RobobloqRangeMerger.cpp b/Controllers/RobobloqLightStripController/RobobloqRangeMerger.cpp new file mode 100644 index 000000000..db5649e62 --- /dev/null +++ b/Controllers/RobobloqLightStripController/RobobloqRangeMerger.cpp @@ -0,0 +1,155 @@ +/*---------------------------------------------------------*\ +| RobobloqRangeMerger.cpp | +| | +| Helper for merging LED ranges for Robobloq | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#include "RobobloqRangeMerger.h" +#include +#include +#include + + +/*** + * The official application does not send a full set of LED values to the device (i.e. 71 in the + * 34" case) but rather compresses the 71 RGB values down to exactly 34 ranges. It seems to use + * pre-configured ranges, but we can do better by calculating the ranges to use that create the + * least error, preserving single pixel detail. + * + * We use a greedy merge algorithm: initially define a 1-length range for each pixel. Try to merge + * any 2 adjacent ranges and pick the merge that creates the least difference. Repeat until we + * have 34. + */ +std::vector MergeRobobloqRanges(const std::vector& colors, int tuple_count) +{ + if(tuple_count == 0) + { + LOG_ERROR("[Robobloq] MergeRobobloqRanges called with tuple_count == 0"); + return {}; + } + + struct LEDRange + { + int start; /* Start LED (1-indexed) */ + int end; /* End LED */ + int n; /* Number of LEDs in range */ + double sum_r; /* Sum of R values */ + double sum_g; + double sum_b; + double term; /* = sum_r^2 + sum_g^2 + sum_b^2 / n */ + }; + + int num_leds = (int)colors.size(); + std::vector ranges; + ranges.reserve(num_leds); + + /*-----------------------------------------------------*\ + | 1. Initialize ranges (one per pixel) | + \*-----------------------------------------------------*/ + for(int i = 0; i < num_leds; i++) + { + LEDRange r; + r.start = i + 1; /* 1-based index */ + r.end = i + 1; + r.n = 1; + r.sum_r = RGBGetRValue(colors[i]); + r.sum_g = RGBGetGValue(colors[i]); + r.sum_b = RGBGetBValue(colors[i]); + r.term = (r.sum_r * r.sum_r + r.sum_g * r.sum_g + r.sum_b * r.sum_b); + ranges.push_back(r); + } + + /*-----------------------------------------------------*\ + | 2. Merge until we have tuple_count tuples | + \*-----------------------------------------------------*/ + while((int)ranges.size() > tuple_count) + { + double best_delta = std::numeric_limits::max(); + int best_idx = -1; + double best_merged_term = 0; + + /*-----------------------------------------------------*\ + | Find best adjacent pair to merge | + \*-----------------------------------------------------*/ + + /*** + * Minimise Sum of Squared Errors (SSE) = sum(pixel - average)^2 + * = sum(pixel^2) - sum(n*average^2) + * + * As sum(pixel^2) is constant, we need to maximise sum(n*average^2). + * + * Since average = sum / n: + * n * average^2 = n * (sum / n)^2 = n * (sum^2 / n^2) = sum^2 / n + * + * We need to maximise sum(sum_k^2/n_k) for all ranges k. We cache the + * sum^2/n value as 'term'. + **/ + for(size_t i = 0; i < ranges.size() - 1; i++) + { + const LEDRange& r1 = ranges[i]; + const LEDRange& r2 = ranges[i+1]; + + double sum_r = r1.sum_r + r2.sum_r; + double sum_g = r1.sum_g + r2.sum_g; + double sum_b = r1.sum_b + r2.sum_b; + int n = r1.n + r2.n; + + double term_merged = (sum_r * sum_r + sum_g * sum_g + sum_b * sum_b) / n; + double delta = r1.term + r2.term - term_merged; + + if(delta < best_delta) + { + best_delta = delta; + best_idx = (int)i; + best_merged_term = term_merged; + } + } + + if(best_idx != -1) + { + /*---------------------------------------------*\ + | Merge best_idx and best_idx+1 | + \*---------------------------------------------*/ + LEDRange& r_left = ranges[best_idx]; + const LEDRange& r_right = ranges[best_idx+1]; + + r_left.end = r_right.end; + r_left.n += r_right.n; + r_left.sum_r += r_right.sum_r; + r_left.sum_g += r_right.sum_g; + r_left.sum_b += r_right.sum_b; + r_left.term = best_merged_term; + + ranges.erase(ranges.begin() + best_idx + 1); + } + else + { + /*---------------------------------------------*\ + | No merge possible | + \*---------------------------------------------*/ + break; + } + } + + std::vector color_bytes; + color_bytes.reserve(tuple_count * 5); + + for(size_t i = 0; i < ranges.size(); i++) + { + const LEDRange& r = ranges[i]; + unsigned char avg_r = (unsigned char)std::round(r.sum_r / r.n); + unsigned char avg_g = (unsigned char)std::round(r.sum_g / r.n); + unsigned char avg_b = (unsigned char)std::round(r.sum_b / r.n); + + color_bytes.push_back((unsigned char)r.start); + color_bytes.push_back(avg_r); + color_bytes.push_back(avg_g); + color_bytes.push_back(avg_b); + color_bytes.push_back((unsigned char)r.end); + } + + return color_bytes; +} diff --git a/Controllers/RobobloqLightStripController/RobobloqRangeMerger.h b/Controllers/RobobloqLightStripController/RobobloqRangeMerger.h new file mode 100644 index 000000000..6278cc714 --- /dev/null +++ b/Controllers/RobobloqLightStripController/RobobloqRangeMerger.h @@ -0,0 +1,15 @@ +/*---------------------------------------------------------*\ +| RobobloqRangeMerger.h | +| | +| Helper for merging LED ranges for Robobloq | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#pragma once + +#include +#include "RGBController.h" + +std::vector MergeRobobloqRanges(const std::vector& colors, int tuple_count);