/*---------------------------------------------------------*\ | MSIMonitorController.cpp | | | | Driver for MSI monitor (gaming controller) | | | | Andy Herbert 2026 June 4 | | Ken Sanislo 2026 July 20 | | | | This file is part of the OpenRGB project | | SPDX-License-Identifier: GPL-2.0-or-later | \*---------------------------------------------------------*/ #include #include #include #include "MSIMonitorController.h" #include "StringUtils.h" /*---------------------------------------------------------*\ | Feature reports are control transfers and this panel | | holds one outstanding for tens of milliseconds. Back | | to back writes pin the shared control pipe at full | | duty and starve other non periodic traffic: a device | | with no interrupt OUT endpoint writes by SET_REPORT | | and stops answering, while its isochronous endpoints | | keep running. | | | | Idle a fixed interval after each write to leave the | | control pipe free. | \*---------------------------------------------------------*/ void MSIMonitorController::SendFeatureReport(uint8_t *data, size_t length) { hid_send_feature_report(dev, data, length); std::this_thread::sleep_for(std::chrono::milliseconds(MSI_MONITOR_PIPE_IDLE_MS)); } MSIMonitorController::MSIMonitorController(hid_device* dev_handle, const hid_device_info& info, std::string dev_name) { dev = dev_handle; location = info.path; name = dev_name; } MSIMonitorController::~MSIMonitorController() { hid_close(dev); } std::string MSIMonitorController::GetDeviceLocation() { return("HID: " + location); } std::string MSIMonitorController::GetNameString() { return(name); } std::string MSIMonitorController::GetSerialString() { wchar_t serial_string[128]; int ret = hid_get_serial_number_string(dev, serial_string, 128); if(ret != 0) { return(""); } return(StringUtils::wstring_to_string(serial_string)); } void MSIMonitorController::Set(uint8_t mode_value, const std::vector colors, uint8_t last_bit) { /*---------------------------------------------------------*\ | Prepare color data | \*---------------------------------------------------------*/ uint8_t data[MSI_MONITOR_PACKET_SIZE]; memset(data, 0x00, MSI_MONITOR_PACKET_SIZE); unsigned int offset = 0; data[offset++] = 0x71; data[offset++] = 0x01; for(int i = 0; i < 3; i++) { data[offset++] = 0x00; } data[offset++] = 0x01; data[offset++] = 0x64; for(int i = 0; i < 5; i++) { data[offset++] = 0x00; } /*---------------------------------------------------------*\ | put mode_value | \*---------------------------------------------------------*/ data[offset++] = mode_value; for(int i = 0; i < 3; i++) { data[offset++] = 0x00; } data[offset++] = 0x01; data[offset++] = 0x64; for(int i = 0; i < 5; i++) { data[offset++] = 0x00; } /*-----------------------------------------------------------------------*\ | this data looks like placeholder for additional LEDs in other monitors | \*-----------------------------------------------------------------------*/ for(int i = 0; i < 9; i++) { data[offset++] = 0xff; data[offset++] = 0x00; data[offset++] = 0x00; } //RGB values begin for(const RGBColor color: colors) { data[offset++] = RGBGetRValue(color); data[offset++] = RGBGetGValue(color); data[offset++] = RGBGetBValue(color); } /*-------------------------------------------------------------------------------*\ | last bit is probably a write to device bit- 0x01 saves to device, 0x00 doesn't. | | For direct mode, bit is set to 0x00, otherwise lights will flicker | \*-------------------------------------------------------------------------------*/ data[offset++] = last_bit; /*---------------------------------------------------------*\ | Send the data (1 packet) | \*---------------------------------------------------------*/ SendFeatureReport(data, MSI_MONITOR_PACKET_SIZE); } uint8_t MSIMonitorController::GetLayoutVersion() { /*-----------------------------------------------------*\ | Ask the MCU which buffer layout it speaks. Request | | is [0x01][opcode] zero padded, reply is [0x01][0x5A] | | then data with the version at byte 3. | \*-----------------------------------------------------*/ uint8_t request[MSI_MONITOR_QUERY_PACKET_SIZE]; uint8_t reply[MSI_MONITOR_QUERY_PACKET_SIZE]; /*-----------------------------------------------------*\ | Unimplemented opcodes never answer, so a reply left | | queued by an earlier exchange would be read here and | | parsed as this query's answer. Drain first. | \*-----------------------------------------------------*/ while(hid_read_timeout(dev, reply, MSI_MONITOR_QUERY_PACKET_SIZE, 0) > 0) { continue; } memset(request, 0x00, MSI_MONITOR_QUERY_PACKET_SIZE); request[0] = MSI_MONITOR_QUERY_REPORT_ID; request[1] = MSI_MONITOR_OPCODE_GET_VERSION; if(hid_write(dev, request, MSI_MONITOR_QUERY_PACKET_SIZE) < 0) { return(MSI_MONITOR_VERSION_UNKNOWN); } memset(reply, 0x00, MSI_MONITOR_QUERY_PACKET_SIZE); if(hid_read_timeout(dev, reply, MSI_MONITOR_QUERY_PACKET_SIZE, 500) <= 0) { return(MSI_MONITOR_VERSION_UNKNOWN); } if(reply[1] != MSI_MONITOR_QUERY_ACK) { return(MSI_MONITOR_VERSION_UNKNOWN); } return(reply[3]); } void MSIMonitorController::SetMode72(uint8_t mode_value, uint8_t speed, uint8_t brightness, RGBColor color1, RGBColor color2, bool user_palette, bool save, const std::vector& led_colors, bool fill_both_arrays) { uint8_t data[MSI_MONITOR_72_PACKET_SIZE]; memset(data, 0x00, MSI_MONITOR_72_PACKET_SIZE); data[0] = 0x72; /*-----------------------------------------------------*\ | Identical 11 byte control blocks at index 1 and 12 | \*-----------------------------------------------------*/ for(unsigned int base = 1; base <= 12; base += 11) { data[base + 0] = mode_value; data[base + 1] = RGBGetRValue(color1); data[base + 2] = RGBGetGValue(color1); data[base + 3] = RGBGetBValue(color1); data[base + 4] = speed; data[base + 5] = brightness; data[base + 6] = RGBGetRValue(color2); data[base + 7] = RGBGetGValue(color2); data[base + 8] = RGBGetBValue(color2); data[base + 9] = user_palette ? 0x80 : 0x00; data[base + 10] = 0x00; } /*-----------------------------------------------------*\ | The engine paints from the second inline colour array | | at index 95, not from Colour1 and not from the first | | array at index 23: on a 322URX a buffer carrying only | | Colour1 renders black, and walking one lit slot | | through the buffer only lights LEDs for the second | | array. That pairs with the control block at index 12 | | being the operative one. Built in palette modes | | leave the array clear so the firmware effect is not | | painted over. | | | | fill_both_arrays also writes the first array at index | | 23, for the dual block versions (11/13/15) whose two | | control blocks each drive their own run. On a single | | block layout the first array is inert, so filling it | | is harmless. Those versions also route colour index | | above 15 to a separate DragonShield emblem region not | | handled here - one reason they stay disabled. | \*-----------------------------------------------------*/ if(user_palette) { for(unsigned int slot = 0; slot < MSI_MONITOR_72_ARRAY_SLOTS && slot < led_colors.size(); slot++) { unsigned int offset_b = MSI_MONITOR_72_ARRAY_B_INDEX + (slot * 3); data[offset_b + 0] = RGBGetRValue(led_colors[slot]); data[offset_b + 1] = RGBGetGValue(led_colors[slot]); data[offset_b + 2] = RGBGetBValue(led_colors[slot]); if(fill_both_arrays) { unsigned int offset_a = MSI_MONITOR_72_ARRAY_A_INDEX + (slot * 3); data[offset_a + 0] = RGBGetRValue(led_colors[slot]); data[offset_a + 1] = RGBGetGValue(led_colors[slot]); data[offset_a + 2] = RGBGetBValue(led_colors[slot]); } } } /*-----------------------------------------------------*\ | Final byte is store to flash: 0x01 persists across | | power cycles, 0x00 applies without a flash write. | | Keep 0x00 for streaming to avoid flicker and wear. | \*-----------------------------------------------------*/ data[MSI_MONITOR_72_STORE_INDEX] = save ? 0x01 : 0x00; SendFeatureReport(data, MSI_MONITOR_72_PACKET_SIZE); }