Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions packaging/infopanel-udev.rules
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion src/InfoPanel.Core/ThermaltakePanel/ThermaltakePanelModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ public enum ThermaltakePanelModel
Unknown,
ToughLiquid6Inch, // PID 0x2347, 1480x720
AsrockPhantomGaming360LCD, // VID 0x26CE, PID 0x0A10, 480x480
AsrockSteelLegend360LCD, // VID 0x26CE, PID 0x0A11, 480x480
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ThermaltakePanelModel, ThermaltakePanelModelInfo> Models = new()
Expand All @@ -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)
Expand Down
64 changes: 59 additions & 5 deletions src/InfoPanel.Devices.Thermaltake/ThermaltakeHidDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
{
Expand All @@ -77,13 +78,34 @@ public class ThermaltakeHidDevice : IDisposable
}

/// <summary>
/// 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.
/// </summary>
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"))
Expand All @@ -109,6 +131,18 @@ public bool SetBrightness(int value)
return SendPacketAndCheckResponse(packet, "brightness");
}

private bool IsSteelLegend360 => _productId == ThermaltakePanelModelDatabase.ASROCK_PRODUCT_ID_STEEL_LEGEND_360;

/// <summary>
/// Sends a power event ("resume" or "suspend"). Steel Legend 360 only.
/// </summary>
private bool SetPowerState(string state)
{
var packet = BuildCommandPacket("POST power 1",
body: $"{{\"event\":\"{state}\"}}", contentType: "json");
return SendPacketAndCheckResponse(packet, $"power {state}");
}

/// <summary>
/// Sends a JPEG image frame as chunked image data.
/// </summary>
Expand All @@ -118,16 +152,33 @@ 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;
int payloadSize = Math.Min(IMAGE_PAYLOAD_PER_CHUNK, jpegData.Length - offset);

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
Expand All @@ -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)
{
Expand Down