From f6bc993d303248ca78047aa1932a4600a558578d Mon Sep 17 00:00:00 2001 From: Ulfy <30026440+Ulfy@users.noreply.github.com> Date: Thu, 17 Sep 2026 01:09:34 +0900 Subject: [PATCH 1/2] Add support for ASRock Steel Legend 360 LCD --- packaging/infopanel-udev.rules | 3 + .../ThermaltakePanel/ThermaltakePanelModel.cs | 3 +- .../ThermaltakePanelModelDatabase.cs | 11 +++ .../ThermaltakeHidDevice.cs | 94 ++++++++++++++++++- 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/packaging/infopanel-udev.rules b/packaging/infopanel-udev.rules index b1f4238..7d552dd 100644 --- a/packaging/infopanel-udev.rules +++ b/packaging/infopanel-udev.rules @@ -67,6 +67,9 @@ KERNEL=="hidraw*", ATTRS{idVendor}=="264a", ATTRS{idProduct}=="2347", MODE="0666 # ASRock Phantom Gaming 360 LCD (hidraw) SUBSYSTEM=="usb", ATTR{idVendor}=="26ce", ATTR{idProduct}=="0a10", MODE="0666" KERNEL=="hidraw*", ATTRS{idVendor}=="26ce", ATTRS{idProduct}=="0a10", MODE="0666" +# ASRock Steel Legend 360 LCD (hidraw) +SUBSYSTEM=="usb", ATTR{idVendor}=="26ce", ATTR{idProduct}=="0a11", MODE="0666" +KERNEL=="hidraw*", ATTRS{idVendor}=="26ce", ATTRS{idProduct}=="0a11", MODE="0666" # Jungle Leopard / Hongtai cooler displays and Jonsbo DS916 (CDC serial - grants # access without dialout membership; both are Artinchip 33c3 SKUs) diff --git a/src/InfoPanel.Core/ThermaltakePanel/ThermaltakePanelModel.cs b/src/InfoPanel.Core/ThermaltakePanel/ThermaltakePanelModel.cs index 4d99192..602b4da 100644 --- a/src/InfoPanel.Core/ThermaltakePanel/ThermaltakePanelModel.cs +++ b/src/InfoPanel.Core/ThermaltakePanel/ThermaltakePanelModel.cs @@ -5,5 +5,6 @@ public enum ThermaltakePanelModel Unknown, ToughLiquid6Inch, // PID 0x2347, 1480x720 AsrockPhantomGaming360LCD, // VID 0x26CE, PID 0x0A10, 480x480 + AsrockSteelLegend360LCD, // VID 0x26CE, PID 0x0A11, 480x480 } -} +} \ No newline at end of file diff --git a/src/InfoPanel.Core/ThermaltakePanel/ThermaltakePanelModelDatabase.cs b/src/InfoPanel.Core/ThermaltakePanel/ThermaltakePanelModelDatabase.cs index ad3d3f1..9323ccd 100644 --- a/src/InfoPanel.Core/ThermaltakePanel/ThermaltakePanelModelDatabase.cs +++ b/src/InfoPanel.Core/ThermaltakePanel/ThermaltakePanelModelDatabase.cs @@ -10,11 +10,13 @@ public static class ThermaltakePanelModelDatabase public const int ASROCK_VENDOR_ID = 0x26CE; public const int ASROCK_PRODUCT_ID_PG360 = 0x0A10; + public const int ASROCK_PRODUCT_ID_STEEL_LEGEND_360 = 0x0A11; public static readonly (int Vid, int Pid)[] SupportedDevices = [ (THERMALTAKE_VENDOR_ID, THERMALTAKE_PRODUCT_ID_6INCH), (ASROCK_VENDOR_ID, ASROCK_PRODUCT_ID_PG360), + (ASROCK_VENDOR_ID, ASROCK_PRODUCT_ID_STEEL_LEGEND_360), ]; public static readonly Dictionary Models = new() @@ -37,6 +39,15 @@ public static readonly (int Vid, int Pid)[] SupportedDevices = VendorId = ASROCK_VENDOR_ID, ProductId = ASROCK_PRODUCT_ID_PG360, }, + [ThermaltakePanelModel.AsrockSteelLegend360LCD] = new ThermaltakePanelModelInfo + { + Model = ThermaltakePanelModel.AsrockSteelLegend360LCD, + Name = "ASRock Steel Legend 360 LCD", + Width = 480, + Height = 480, + VendorId = ASROCK_VENDOR_ID, + ProductId = ASROCK_PRODUCT_ID_STEEL_LEGEND_360, + }, }; public static ThermaltakePanelModelInfo? GetModelByVidPid(int vid, int pid) diff --git a/src/InfoPanel.Devices.Thermaltake/ThermaltakeHidDevice.cs b/src/InfoPanel.Devices.Thermaltake/ThermaltakeHidDevice.cs index d4653a6..54caa16 100644 --- a/src/InfoPanel.Devices.Thermaltake/ThermaltakeHidDevice.cs +++ b/src/InfoPanel.Devices.Thermaltake/ThermaltakeHidDevice.cs @@ -27,6 +27,7 @@ public class ThermaltakeHidDevice : IDisposable private const int FRAME_OVERHEAD = 5; // 5A 00 [len] ... [checksum] 5A 00 private HidStream? _stream; + private int _productId; private bool _disposed; private int _seqNumber = 100; @@ -64,7 +65,11 @@ public class ThermaltakeHidDevice : IDisposable stream.ReadTimeout = 2000; Logger.Information("ThermaltakeHidDevice: Opened {Path}", device.DevicePath); - return new ThermaltakeHidDevice { _stream = stream }; + return new ThermaltakeHidDevice + { + _stream = stream, + _productId = productId + }; } catch (Exception ex) { @@ -84,6 +89,32 @@ public bool Handshake() { Logger.Information("ThermaltakeHidDevice: Starting handshake"); + if (_productId == ThermaltakePanelModelDatabase.ASROCK_PRODUCT_ID_STEEL_LEGEND_360) + { + if (!SetPowerState("resume")) + return false; + + // ASRock software waits ~78 ms after the resume response before beginning image transmission. + System.Threading.Thread.Sleep(100); + + var steelLegendEnablePacket = BuildCommandPacket( + "POST realtimeDisplay 1", + body: "{\"enable\":true}", + contentType: "json"); + + if (!SendPacketAndCheckResponse( + steelLegendEnablePacket, + "realtimeDisplay")) + return false; + + System.Threading.Thread.Sleep(100); + + Logger.Information( + "ThermaltakeHidDevice: Steel Legend handshake complete"); + + return true; + } + // POST conn var connPacket = BuildCommandPacket("POST conn 1", hasBody: false); if (!SendPacketAndCheckResponse(connPacket, "conn")) @@ -109,15 +140,38 @@ public bool SetBrightness(int value) return SendPacketAndCheckResponse(packet, "brightness"); } + // Ensures the LCD turns on + private bool SetPowerState(string state) + { + var packet = BuildCommandPacket( + "POST power 1", + body: $"{{\"event\":\"{state}\"}}", + contentType: "json"); + + return SendPacketAndCheckResponse(packet, $"power {state}"); + } + /// /// Sends a JPEG image frame as chunked image data. /// + /// + + public void SendJpegFrame(byte[] jpegData) { + + if (_stream == null) throw new InvalidOperationException("Device not open"); + Logger.Information( + "ThermaltakeHidDevice: Sending JPEG frame, {Bytes} bytes", + jpegData.Length); + int totalChunks = (jpegData.Length + IMAGE_PAYLOAD_PER_CHUNK - 1) / IMAGE_PAYLOAD_PER_CHUNK; + bool isSteelLegend360 = _productId == ThermaltakePanelModelDatabase.ASROCK_PRODUCT_ID_STEEL_LEGEND_360; + + for (int chunk = 0; chunk < totalChunks; chunk++) { int offset = chunk * IMAGE_PAYLOAD_PER_CHUNK; @@ -125,9 +179,24 @@ public void SendJpegFrame(byte[] jpegData) var wireData = new byte[WIRE_PACKET_SIZE]; wireData[0] = 0x5C; // Image magic - wireData[1] = 0x03; - wireData[2] = 0xFD; // CMD: image data - wireData[3] = 0x00; // area index + + if (isSteelLegend360) + { + + // Steel Legend 360 LCD (26CE:0A11): + int packetLength = payloadSize + 21; + + wireData[1] = (byte)((packetLength >> 8) & 0xFF); + wireData[2] = (byte)(packetLength & 0xFF); + wireData[3] = (byte)(DateTimeOffset.UtcNow.ToUnixTimeSeconds() & 0xFF); + } + else + { + wireData[1] = 0x03; + wireData[2] = 0xFD; // CMD: image data + wireData[3] = 0x00; // area index + } + wireData[4] = (byte)((totalChunks >> 8) & 0xFF); // total chunks BE16 wireData[5] = (byte)(totalChunks & 0xFF); wireData[6] = (byte)((chunk >> 8) & 0xFF); // chunk index BE16 @@ -147,6 +216,23 @@ public void DisableRealtimeDisplay() { try { + + if (_productId == + ThermaltakePanelModelDatabase.ASROCK_PRODUCT_ID_STEEL_LEGEND_360) + { + var steelLegendDisablePacket = BuildCommandPacket( + "POST realtimeDisplay 1", + body: "{\"enable\":false}", + contentType: "json"); + + SendPacketAndCheckResponse( + steelLegendDisablePacket, + "realtimeDisplay disable"); + + SetPowerState("suspend"); + return; + } + var disablePacket = BuildCommandPacket("POST realtimeDisplay 1", body: "{\"enable\":false}", contentType: "json"); SendPacketAndCheckResponse(disablePacket, "realtimeDisplay disable"); From 5746974b1ca736c9eae6143222cf257718a7bad2 Mon Sep 17 00:00:00 2001 From: emaspa Date: Thu, 17 Sep 2026 04:17:18 +0100 Subject: [PATCH 2/2] Steel Legend 360: drop per-frame log, stable frame tag, tidy - Remove the Information log in SendJpegFrame; it fired on every frame for every panel model. - Sample the byte-3 timestamp once per frame so all chunks of a frame carry the same value. - Share the realtimeDisplay disable path, then suspend for Steel Legend. - Formatting cleanup; add the panel to the README device table. --- README.md | 1 + .../ThermaltakeHidDevice.cs | 82 ++++++------------- 2 files changed, 26 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 175bbc8..a0c6e7d 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,7 @@ BY-OEM HID protocol (JPEG over 1024-byte reports): |---|---|---| | Thermaltake 6" LCD (ToughLiquid Ultra) ✓ | 1480×720 | `264A:2347` | | ASRock Phantom Gaming 360 LCD | 480×480 | `26CE:0A10` | +| ASRock Steel Legend 360 LCD | 480×480 | `26CE:0A11` | ### Jungle Leopard / Hongtai diff --git a/src/InfoPanel.Devices.Thermaltake/ThermaltakeHidDevice.cs b/src/InfoPanel.Devices.Thermaltake/ThermaltakeHidDevice.cs index 54caa16..86583be 100644 --- a/src/InfoPanel.Devices.Thermaltake/ThermaltakeHidDevice.cs +++ b/src/InfoPanel.Devices.Thermaltake/ThermaltakeHidDevice.cs @@ -65,11 +65,7 @@ public class ThermaltakeHidDevice : IDisposable stream.ReadTimeout = 2000; Logger.Information("ThermaltakeHidDevice: Opened {Path}", device.DevicePath); - return new ThermaltakeHidDevice - { - _stream = stream, - _productId = productId - }; + return new ThermaltakeHidDevice { _stream = stream, _productId = productId }; } catch (Exception ex) { @@ -82,36 +78,31 @@ public class ThermaltakeHidDevice : IDisposable } /// - /// Performs the full handshake: conn, realtimeDisplay enable. + /// Performs the full handshake: conn, realtimeDisplay enable + /// (Steel Legend 360: power resume, realtimeDisplay enable). /// Returns true if all commands received 200 OK responses. /// public bool Handshake() { Logger.Information("ThermaltakeHidDevice: Starting handshake"); - if (_productId == ThermaltakePanelModelDatabase.ASROCK_PRODUCT_ID_STEEL_LEGEND_360) + if (IsSteelLegend360) { + // Required after a full power cycle, or the LCD stays dark. if (!SetPowerState("resume")) return false; // ASRock software waits ~78 ms after the resume response before beginning image transmission. System.Threading.Thread.Sleep(100); - var steelLegendEnablePacket = BuildCommandPacket( - "POST realtimeDisplay 1", - body: "{\"enable\":true}", - contentType: "json"); - - if (!SendPacketAndCheckResponse( - steelLegendEnablePacket, - "realtimeDisplay")) + var steelLegendEnablePacket = BuildCommandPacket("POST realtimeDisplay 1", + body: "{\"enable\":true}", contentType: "json"); + if (!SendPacketAndCheckResponse(steelLegendEnablePacket, "realtimeDisplay")) return false; System.Threading.Thread.Sleep(100); - Logger.Information( - "ThermaltakeHidDevice: Steel Legend handshake complete"); - + Logger.Information("ThermaltakeHidDevice: Steel Legend handshake complete"); return true; } @@ -140,37 +131,30 @@ public bool SetBrightness(int value) return SendPacketAndCheckResponse(packet, "brightness"); } - // Ensures the LCD turns on + private bool IsSteelLegend360 => _productId == ThermaltakePanelModelDatabase.ASROCK_PRODUCT_ID_STEEL_LEGEND_360; + + /// + /// Sends a power event ("resume" or "suspend"). Steel Legend 360 only. + /// private bool SetPowerState(string state) { - var packet = BuildCommandPacket( - "POST power 1", - body: $"{{\"event\":\"{state}\"}}", - contentType: "json"); - + var packet = BuildCommandPacket("POST power 1", + body: $"{{\"event\":\"{state}\"}}", contentType: "json"); return SendPacketAndCheckResponse(packet, $"power {state}"); } /// /// Sends a JPEG image frame as chunked image data. /// - /// - - public void SendJpegFrame(byte[] jpegData) { - - if (_stream == null) throw new InvalidOperationException("Device not open"); - Logger.Information( - "ThermaltakeHidDevice: Sending JPEG frame, {Bytes} bytes", - jpegData.Length); - int totalChunks = (jpegData.Length + IMAGE_PAYLOAD_PER_CHUNK - 1) / IMAGE_PAYLOAD_PER_CHUNK; - bool isSteelLegend360 = _productId == ThermaltakePanelModelDatabase.ASROCK_PRODUCT_ID_STEEL_LEGEND_360; - + bool isSteelLegend360 = IsSteelLegend360; + // Sampled once so every chunk of a frame carries the same byte 3. + byte frameTag = (byte)(DateTimeOffset.UtcNow.ToUnixTimeSeconds() & 0xFF); for (int chunk = 0; chunk < totalChunks; chunk++) { @@ -179,16 +163,14 @@ public void SendJpegFrame(byte[] jpegData) var wireData = new byte[WIRE_PACKET_SIZE]; wireData[0] = 0x5C; // Image magic - + if (isSteelLegend360) { - - // Steel Legend 360 LCD (26CE:0A11): + // Steel Legend 360 LCD (26CE:0A11): BE16 packet length, then a frame tag int packetLength = payloadSize + 21; - wireData[1] = (byte)((packetLength >> 8) & 0xFF); wireData[2] = (byte)(packetLength & 0xFF); - wireData[3] = (byte)(DateTimeOffset.UtcNow.ToUnixTimeSeconds() & 0xFF); + wireData[3] = frameTag; } else { @@ -216,26 +198,12 @@ public void DisableRealtimeDisplay() { try { - - if (_productId == - ThermaltakePanelModelDatabase.ASROCK_PRODUCT_ID_STEEL_LEGEND_360) - { - var steelLegendDisablePacket = BuildCommandPacket( - "POST realtimeDisplay 1", - body: "{\"enable\":false}", - contentType: "json"); - - SendPacketAndCheckResponse( - steelLegendDisablePacket, - "realtimeDisplay disable"); - - SetPowerState("suspend"); - return; - } - var disablePacket = BuildCommandPacket("POST realtimeDisplay 1", body: "{\"enable\":false}", contentType: "json"); SendPacketAndCheckResponse(disablePacket, "realtimeDisplay disable"); + + if (IsSteelLegend360) + SetPowerState("suspend"); } catch (Exception ex) {