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/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..86583be 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,7 @@ 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) { @@ -77,13 +78,34 @@ 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 (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")) + 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,6 +131,18 @@ public bool SetBrightness(int value) return SendPacketAndCheckResponse(packet, "brightness"); } + 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"); + return SendPacketAndCheckResponse(packet, $"power {state}"); + } + /// /// Sends a JPEG image frame as chunked image data. /// @@ -118,6 +152,10 @@ public void SendJpegFrame(byte[] jpegData) int totalChunks = (jpegData.Length + IMAGE_PAYLOAD_PER_CHUNK - 1) / IMAGE_PAYLOAD_PER_CHUNK; + 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++) { int offset = chunk * IMAGE_PAYLOAD_PER_CHUNK; @@ -125,9 +163,22 @@ 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): BE16 packet length, then a frame tag + int packetLength = payloadSize + 21; + wireData[1] = (byte)((packetLength >> 8) & 0xFF); + wireData[2] = (byte)(packetLength & 0xFF); + wireData[3] = frameTag; + } + 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 @@ -150,6 +201,9 @@ public void DisableRealtimeDisplay() var disablePacket = BuildCommandPacket("POST realtimeDisplay 1", body: "{\"enable\":false}", contentType: "json"); SendPacketAndCheckResponse(disablePacket, "realtimeDisplay disable"); + + if (IsSteelLegend360) + SetPowerState("suspend"); } catch (Exception ex) {