Files
OpenRGB/Controllers/SkydimoSerialController/SkydimoSerialController.cpp
2026-07-14 21:42:42 -05:00

549 lines
17 KiB
C++

/*---------------------------------------------------------*\
| SkydimoSerialController.cpp |
| |
| Driver for Skydimo serial devices |
| |
| Bartholomew Ho <qiangqiang101@hotmail.com> 30 Jun 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include <algorithm>
#include <cctype>
#include "LogManager.h"
#include "SkydimoSerialController.h"
enum SkydimoLayoutKind
{
SKYDIMO_LAYOUT_NONE,
SKYDIMO_LAYOUT_STRIP_1,
SKYDIMO_LAYOUT_PERIMETER_3,
SKYDIMO_LAYOUT_PERIMETER_4,
SKYDIMO_LAYOUT_SIDES_2
};
struct SkydimoModelEntry
{
const char* model;
SkydimoLayoutKind layout;
unsigned int zone_leds[4];
unsigned int total_leds;
unsigned int max_leds;
};
static const SkydimoModelEntry skydimo_model_entries[] =
{
{ "SK0121", SKYDIMO_LAYOUT_PERIMETER_3, { 13, 25, 13, 0 }, 51, 0 },
{ "SK0124", SKYDIMO_LAYOUT_PERIMETER_3, { 14, 26, 14, 0 }, 54, 0 },
{ "SK0127", SKYDIMO_LAYOUT_PERIMETER_3, { 17, 31, 17, 0 }, 65, 0 },
{ "SK0132", SKYDIMO_LAYOUT_PERIMETER_3, { 20, 37, 20, 0 }, 77, 0 },
{ "SK0134", SKYDIMO_LAYOUT_PERIMETER_3, { 15, 41, 15, 0 }, 71, 0 },
{ "SK0149", SKYDIMO_LAYOUT_PERIMETER_3, { 19, 69, 19, 0 }, 107, 0 },
{ "SK0201", SKYDIMO_LAYOUT_SIDES_2, { 20, 20, 0, 0 }, 40, 0 },
{ "SK0202", SKYDIMO_LAYOUT_SIDES_2, { 30, 30, 0, 0 }, 60, 0 },
{ "SK0204", SKYDIMO_LAYOUT_SIDES_2, { 25, 25, 0, 0 }, 50, 0 },
{ "SK0402", SKYDIMO_LAYOUT_STRIP_1, { 72, 0, 0, 0 }, 72, 0 },
{ "SK0403", SKYDIMO_LAYOUT_STRIP_1, { 96, 0, 0, 0 }, 96, 0 },
{ "SK0404", SKYDIMO_LAYOUT_STRIP_1, { 144, 0, 0, 0 }, 144, 0 },
{ "SK0410", SKYDIMO_LAYOUT_NONE, { 0, 0, 0, 0 }, 0, 300 },
{ "SK0801", SKYDIMO_LAYOUT_STRIP_1, { 2, 0, 0, 0 }, 2, 0 },
{ "SK0802", SKYDIMO_LAYOUT_STRIP_1, { 18, 0, 0, 0 }, 18, 0 },
{ "SK0803", SKYDIMO_LAYOUT_STRIP_1, { 10, 0, 0, 0 }, 10, 0 },
{ "SK0901", SKYDIMO_LAYOUT_STRIP_1, { 14, 0, 0, 0 }, 14, 0 },
{ "SK0E01", SKYDIMO_LAYOUT_STRIP_1, { 16, 0, 0, 0 }, 16, 0 },
{ "SK0F01", SKYDIMO_LAYOUT_SIDES_2, { 29, 29, 0, 0 }, 58, 0 },
{ "SK0F02", SKYDIMO_LAYOUT_SIDES_2, { 25, 25, 0, 0 }, 50, 0 },
{ "SK0H01", SKYDIMO_LAYOUT_STRIP_1, { 2, 0, 0, 0 }, 2, 0 },
{ "SK0H02", SKYDIMO_LAYOUT_STRIP_1, { 4, 0, 0, 0 }, 4, 0 },
{ "SK0K01", SKYDIMO_LAYOUT_STRIP_1, { 120, 0, 0, 0 }, 120, 0 },
{ "SK0K02", SKYDIMO_LAYOUT_STRIP_1, { 15, 0, 0, 0 }, 15, 0 },
{ "SK0L21", SKYDIMO_LAYOUT_PERIMETER_4, { 13, 25, 13, 25 }, 76, 0 },
{ "SK0L24", SKYDIMO_LAYOUT_PERIMETER_4, { 14, 26, 14, 26 }, 80, 0 },
{ "SK0L27", SKYDIMO_LAYOUT_PERIMETER_4, { 17, 31, 17, 31 }, 96, 0 },
{ "SK0L32", SKYDIMO_LAYOUT_PERIMETER_4, { 20, 37, 20, 37 }, 114, 0 },
{ "SK0L34", SKYDIMO_LAYOUT_PERIMETER_4, { 15, 41, 15, 41 }, 112, 0 },
{ "SK0M01", SKYDIMO_LAYOUT_STRIP_1, { 24, 0, 0, 0 }, 24, 0 },
{ "SK0N01", SKYDIMO_LAYOUT_STRIP_1, { 256, 0, 0, 0 }, 256, 0 },
{ "SK0N02", SKYDIMO_LAYOUT_STRIP_1, { 1024, 0, 0, 0 }, 1024, 0 },
{ "SK0N03", SKYDIMO_LAYOUT_STRIP_1, { 253, 0, 0, 0 }, 253, 0 },
{ "SK0S01", SKYDIMO_LAYOUT_STRIP_1, { 32, 0, 0, 0 }, 32, 0 },
{ "SKA124", SKYDIMO_LAYOUT_PERIMETER_3, { 18, 34, 18, 0 }, 70, 0 },
{ "SKA127", SKYDIMO_LAYOUT_PERIMETER_3, { 20, 41, 20, 0 }, 81, 0 },
{ "SKA132", SKYDIMO_LAYOUT_PERIMETER_3, { 25, 45, 25, 0 }, 95, 0 },
{ "SKA134", SKYDIMO_LAYOUT_PERIMETER_3, { 21, 51, 21, 0 }, 93, 0 },
{ "SKB124", SKYDIMO_LAYOUT_PERIMETER_4, { 18, 34, 18, 34 }, 104, 0 },
{ "SKB127", SKYDIMO_LAYOUT_PERIMETER_4, { 20, 41, 20, 41 }, 122, 0 },
{ "SKB132", SKYDIMO_LAYOUT_PERIMETER_4, { 25, 44, 25, 44 }, 138, 0 },
{ "SKB134", SKYDIMO_LAYOUT_PERIMETER_4, { 21, 50, 21, 50 }, 142, 0 }
};
#define SKYDIMO_MODEL_ENTRY_COUNT (sizeof(skydimo_model_entries) / sizeof(skydimo_model_entries[0]))
static const unsigned char skydimo_serial_frame_header[] =
{
0x41, 0x64, 0x61, 0x00
};
static std::string StringToUpper(const std::string& value)
{
std::string result = value;
for(unsigned int char_idx = 0; char_idx < result.size(); char_idx++)
{
result[char_idx] = (char)std::toupper((unsigned char)result[char_idx]);
}
return(result);
}
static const SkydimoModelEntry* FindModelEntry(const std::string& model)
{
for(unsigned int entry_idx = 0; entry_idx < SKYDIMO_MODEL_ENTRY_COUNT; entry_idx++)
{
if(model == skydimo_model_entries[entry_idx].model)
{
return(&skydimo_model_entries[entry_idx]);
}
}
return(nullptr);
}
static std::string BytesToHex(const std::vector<unsigned char>& bytes, unsigned int offset)
{
static const char hex[] = "0123456789ABCDEF";
std::string result;
result.reserve((bytes.size() - offset) * 2);
for(unsigned int byte_idx = offset; byte_idx < bytes.size(); byte_idx++)
{
result.push_back(hex[(bytes[byte_idx] >> 4) & 0x0F]);
result.push_back(hex[bytes[byte_idx] & 0x0F]);
}
return(result);
}
SkydimoSerialController::SkydimoSerialController(const std::string& port_name_arg)
{
port_name = port_name_arg;
model_name = "Skydimo UNKNOWN";
model_id = "UNKNOWN";
serial_id = "000000";
port_open = serial_port_interface.serial_open(port_name.c_str(), SKYDIMO_SERIAL_BAUD_RATE);
present = false;
resizable = true;
leds_count = SKYDIMO_SERIAL_DEFAULT_LED_COUNT;
leds_min = SKYDIMO_SERIAL_DEFAULT_LED_COUNT;
leds_max = SKYDIMO_SERIAL_DEFAULT_MAX_LED_COUNT;
matrix_width = 0;
matrix_height = 0;
if(!port_open)
{
LOG_DEBUG("[SkydimoSerialController] Failed to open serial port %s", port_name.c_str());
}
}
SkydimoSerialController::~SkydimoSerialController()
{
}
bool SkydimoSerialController::IsPresent()
{
if(present)
{
return(true);
}
present = QueryInfo();
return(present);
}
bool SkydimoSerialController::QueryInfo()
{
if(!port_open)
{
return(false);
}
serial_port_interface.serial_flush_rx();
char query[] = SKYDIMO_SERIAL_QUERY;
int bytes_written = serial_port_interface.serial_write(query, SKYDIMO_SERIAL_QUERY_LENGTH);
if(bytes_written != SKYDIMO_SERIAL_QUERY_LENGTH)
{
LOG_DEBUG("[SkydimoSerialController] Failed to send identification query to %s (%d/%u bytes)",
port_name.c_str(), bytes_written, (unsigned int)SKYDIMO_SERIAL_QUERY_LENGTH);
return(false);
}
std::vector<unsigned char> response;
for(unsigned int attempt = 0; attempt < SKYDIMO_SERIAL_QUERY_ATTEMPTS; attempt++)
{
char buffer[SKYDIMO_SERIAL_READ_BUFFER_SIZE];
int bytes_read = serial_port_interface.serial_read(buffer, SKYDIMO_SERIAL_READ_BUFFER_SIZE);
if(bytes_read < 0)
{
LOG_DEBUG("[SkydimoSerialController] Failed to read identification response from %s", port_name.c_str());
return(false);
}
if(bytes_read > 0)
{
response.insert(response.end(), buffer, buffer + bytes_read);
}
if(response.size() >= SKYDIMO_SERIAL_MAX_RESPONSE_SIZE)
{
break;
}
if(!response.empty())
{
unsigned char last = response[response.size() - 1];
unsigned char prev = response.size() >= 2 ? response[response.size() - 2] : 0;
if((last == '\r') || (last == '\n') || (prev == '\r'))
{
break;
}
}
}
while(!response.empty() && ((response.back() == '\r') || (response.back() == '\n')))
{
response.pop_back();
}
if(response.size() < SKYDIMO_SERIAL_MODEL_ID_LENGTH)
{
LOG_TRACE("[SkydimoSerialController] No Skydimo identification response from %s", port_name.c_str());
return(false);
}
if((std::toupper(response[0]) != 'S') || (std::toupper(response[1]) != 'K'))
{
LOG_TRACE("[SkydimoSerialController] Ignoring non-Skydimo serial device at %s", port_name.c_str());
return(false);
}
model_id = StringToUpper(std::string(response.begin(), response.begin() + SKYDIMO_SERIAL_MODEL_ID_LENGTH));
model_name = "Skydimo " + model_id;
serial_id = "000000";
unsigned int comma_offset = (unsigned int)response.size();
for(unsigned int byte_idx = 0; byte_idx < response.size(); byte_idx++)
{
if(response[byte_idx] == ',')
{
comma_offset = byte_idx;
break;
}
}
if(comma_offset < response.size())
{
if(comma_offset < SKYDIMO_SERIAL_MODEL_ID_LENGTH)
{
LOG_DEBUG("[SkydimoSerialController] Invalid identification response from %s", port_name.c_str());
return(false);
}
if((comma_offset + 1) < response.size())
{
serial_id = BytesToHex(response, comma_offset + 1);
}
}
ConfigureOutput();
return(true);
}
void SkydimoSerialController::ConfigureOutput()
{
const SkydimoModelEntry* entry = FindModelEntry(model_id);
resizable = true;
leds_count = SKYDIMO_SERIAL_DEFAULT_LED_COUNT;
leds_min = SKYDIMO_SERIAL_DEFAULT_LED_COUNT;
leds_max = SKYDIMO_SERIAL_DEFAULT_MAX_LED_COUNT;
matrix_width = 0;
matrix_height = 0;
matrix_map.clear();
if((entry != nullptr) && (entry->max_leds != 0))
{
leds_max = entry->max_leds;
}
if((entry == nullptr) || (entry->layout == SKYDIMO_LAYOUT_NONE))
{
return;
}
resizable = false;
leds_count = entry->total_leds;
leds_min = entry->total_leds;
leds_max = entry->total_leds;
if(entry->layout == SKYDIMO_LAYOUT_STRIP_1)
{
return;
}
const unsigned int zone_1_leds = entry->zone_leds[0];
const unsigned int zone_2_leds = entry->zone_leds[1];
const unsigned int zone_3_leds = entry->zone_leds[2];
const unsigned int zone_4_leds = entry->zone_leds[3];
if(entry->layout == SKYDIMO_LAYOUT_PERIMETER_4)
{
matrix_height = std::max(zone_1_leds, zone_3_leds) + 2;
matrix_width = std::max(zone_2_leds, zone_4_leds) + 2;
}
else if(entry->layout == SKYDIMO_LAYOUT_PERIMETER_3)
{
matrix_height = std::max(zone_1_leds, zone_3_leds) + 1;
matrix_width = zone_2_leds + 2;
}
else
{
matrix_height = std::max(zone_1_leds, zone_2_leds) + 2;
matrix_width = std::max(3U, ((16 * matrix_height) + 4) / 9);
}
matrix_map.assign(matrix_width * matrix_height, SKYDIMO_SERIAL_MATRIX_NA);
unsigned int led_index = 0;
if(entry->layout == SKYDIMO_LAYOUT_SIDES_2)
{
unsigned int placed = 0;
int y = (int)matrix_height - 2;
while((placed < zone_1_leds) && (y >= 1))
{
SetMatrixCell((unsigned int)y, 0, led_index);
placed++;
y--;
}
}
else
{
unsigned int placed = 0;
int y = entry->layout == SKYDIMO_LAYOUT_PERIMETER_3 ? (int)matrix_height - 1
: (int)matrix_height - 2;
while((placed < zone_1_leds) && (y >= 1))
{
SetMatrixCell((unsigned int)y, matrix_width - 1, led_index);
placed++;
y--;
}
}
if((entry->layout == SKYDIMO_LAYOUT_PERIMETER_3) || (entry->layout == SKYDIMO_LAYOUT_PERIMETER_4))
{
unsigned int placed = 0;
int x = (int)matrix_width - 2;
while((placed < zone_2_leds) && (x >= 1))
{
SetMatrixCell(0, (unsigned int)x, led_index);
placed++;
x--;
}
}
else if(entry->layout == SKYDIMO_LAYOUT_SIDES_2)
{
unsigned int placed = 0;
unsigned int y = 1;
while((placed < zone_2_leds) && (y <= (matrix_height - 2)))
{
SetMatrixCell(y, matrix_width - 1, led_index);
placed++;
y++;
}
}
unsigned int end_y = entry->layout == SKYDIMO_LAYOUT_PERIMETER_3 ? matrix_height - 1 : matrix_height - 2;
unsigned int placed = 0;
unsigned int y = 1;
while((placed < zone_3_leds) && (y <= end_y))
{
SetMatrixCell(y, 0, led_index);
placed++;
y++;
}
if(entry->layout == SKYDIMO_LAYOUT_PERIMETER_4)
{
placed = 0;
unsigned int x = 1;
while((placed < zone_4_leds) && (x <= (matrix_width - 2)))
{
SetMatrixCell(matrix_height - 1, x, led_index);
placed++;
x++;
}
}
if(led_index != entry->total_leds)
{
LOG_ERROR("[SkydimoSerialController] Invalid matrix map for %s (%u/%u LEDs placed)",
model_id.c_str(), led_index, entry->total_leds);
}
}
void SkydimoSerialController::SetMatrixCell(unsigned int y, unsigned int x, unsigned int& led_index)
{
if((y < matrix_height) && (x < matrix_width))
{
matrix_map[(y * matrix_width) + x] = led_index++;
}
}
bool SkydimoSerialController::WriteFrame(const std::vector<RGBColor>& colors, unsigned int count)
{
std::lock_guard<std::mutex> guard(device_mutex);
if(!port_open)
{
return(false);
}
if(count > colors.size())
{
count = (unsigned int)colors.size();
}
std::vector<unsigned char> packet;
packet.reserve(sizeof(skydimo_serial_frame_header) + SKYDIMO_SERIAL_FRAME_COUNT_SIZE + (count * SKYDIMO_SERIAL_BYTES_PER_LED));
for(unsigned int header_idx = 0; header_idx < sizeof(skydimo_serial_frame_header); header_idx++)
{
packet.push_back(skydimo_serial_frame_header[header_idx]);
}
packet.push_back((unsigned char)((count >> 8) & 0xFF));
packet.push_back((unsigned char)(count & 0xFF));
for(unsigned int led_idx = 0; led_idx < count; led_idx++)
{
packet.push_back(RGBGetRValue(colors[led_idx]));
packet.push_back(RGBGetGValue(colors[led_idx]));
packet.push_back(RGBGetBValue(colors[led_idx]));
}
int packet_size = (int)packet.size();
int total_bytes_written = 0;
while(total_bytes_written < packet_size)
{
int bytes_written = serial_port_interface.serial_write((char*)packet.data() + total_bytes_written,
packet_size - total_bytes_written);
if(bytes_written <= 0)
{
LOG_ERROR("[SkydimoSerialController] Failed to write RGB frame to %s (%d/%d bytes)",
port_name.c_str(), total_bytes_written, packet_size);
return(false);
}
total_bytes_written += bytes_written;
}
return(true);
}
void SkydimoSerialController::SetLEDs(const std::vector<RGBColor>& colors)
{
WriteFrame(colors, leds_count);
}
void SkydimoSerialController::SetBlack()
{
std::vector<RGBColor> black_colors;
black_colors.resize(leds_count, ToRGBColor(0, 0, 0));
WriteFrame(black_colors, leds_count);
}
void SkydimoSerialController::SetLEDCount(unsigned int led_count)
{
if(!resizable)
{
return;
}
if((led_count < leds_min) || (led_count > leds_max))
{
LOG_WARNING("[SkydimoSerialController] Ignoring invalid LED count %u for %s (valid range %u-%u)",
led_count, model_id.c_str(), leds_min, leds_max);
return;
}
leds_count = led_count;
}
std::string SkydimoSerialController::GetName() const
{
return(model_name);
}
std::string SkydimoSerialController::GetSerial() const
{
return(serial_id);
}
std::string SkydimoSerialController::GetLocation() const
{
return("Serial: " + port_name);
}
bool SkydimoSerialController::IsResizable() const
{
return(resizable);
}
unsigned int SkydimoSerialController::GetLEDCount() const
{
return(leds_count);
}
unsigned int SkydimoSerialController::GetMinLEDCount() const
{
return(leds_min);
}
unsigned int SkydimoSerialController::GetMaxLEDCount() const
{
return(leds_max);
}
unsigned int SkydimoSerialController::GetMatrixWidth() const
{
return(matrix_width);
}
unsigned int SkydimoSerialController::GetMatrixHeight() const
{
return(matrix_height);
}
const std::vector<unsigned int>& SkydimoSerialController::GetMatrixMap() const
{
return(matrix_map);
}