Skip to content

Commit 1f9e63d

Browse files
Lexonight1claude
andcommitted
v4.2.4: fix device resolution discovery — remove hardcoded 320x320 default
DeviceInfo.resolution now defaults to (0,0) instead of (320,320). All protocols discover resolution via handshake — no assumptions. Fixes HID devices (e.g. Assassin Spirit 120 Vision ARGB, PM=36) receiving wrong-sized 320x320 frames instead of their actual 240x240. CLI commands also handshake before sending. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c18e4d5 commit 1f9e63d

File tree

9 files changed

+54
-23
lines changed

9 files changed

+54
-23
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "trcc-linux"
7-
version = "4.2.3"
7+
version = "4.2.4"
88
description = "Linux implementation of Thermalright LCD Control Center"
99
readme = "README.md"
1010
license = "GPL-3.0-or-later"

src/trcc/__version__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""TRCC Linux version information."""
22

3-
__version__ = "4.2.3"
3+
__version__ = "4.2.4"
44
__version_info__ = tuple(int(x) for x in __version__.split("."))
55

66
# Version history:
@@ -150,3 +150,8 @@
150150
# corruption: add wire remap table for PM=49 (93 LEDs). C# SendHidVal
151151
# reorders LEDs from logical to hardware wire positions — our code was
152152
# missing this remap, sending colors to wrong physical LEDs. 2308 tests.
153+
# 4.2.4 - Fix device resolution discovery: remove hardcoded 320x320 default.
154+
# All protocols (SCSI, HID, Bulk) now start at (0,0) and discover
155+
# resolution via handshake. Fixes HID devices like Assassin Spirit
156+
# 120 Vision ARGB (PM=36, 240x240) getting wrong-sized frames.
157+
# CLI commands also handshake before sending. 2308 tests.

src/trcc/adapters/device/scsi.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,8 @@ def find_lcd_devices() -> List[Dict]:
218218
if not dev.scsi_device:
219219
continue
220220

221-
# Default resolution — actual resolution detected on handshake
222-
# via _init_device() -> poll byte[0] -> fbl_to_resolution().
223-
resolution = (320, 320)
221+
# Resolution discovered via handshake (_init_device → poll → fbl_to_resolution)
222+
resolution = (0, 0)
224223

225224
# SCSI poll byte[0] = resolution code = PM (matches USBLCD.exe).
226225
# Use it to resolve variant-specific button image.

src/trcc/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ async def send_image(device_id: int, image: UploadFile, rotation: int = 0,
164164
img = ImageService.apply_brightness(img, brightness)
165165

166166
# Resize to device resolution
167-
w, h = dev.resolution or (320, 320)
167+
w, h = dev.resolution if dev.resolution != (0, 0) else (320, 320)
168168
img = ImageService.resize(img, w, h)
169169

170170
# Convert and send

src/trcc/cli.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -563,13 +563,13 @@ class DeviceCommands:
563563

564564
@staticmethod
565565
def _get_service(device_path: Optional[str] = None):
566-
"""Create a DeviceService, detect devices, and select by path.
566+
"""Create a DeviceService, detect devices, select, and handshake.
567567
568568
Args:
569569
device_path: SCSI path (/dev/sgX) or None to use saved selection.
570570
571571
Returns:
572-
DeviceService with a selected device.
572+
DeviceService with a selected device (resolution discovered).
573573
"""
574574
from trcc.services import DeviceService
575575

@@ -592,6 +592,19 @@ def _get_service(device_path: Optional[str] = None):
592592
if match:
593593
svc.select(match)
594594

595+
# Discover resolution via handshake if not yet known
596+
dev = svc.selected
597+
if dev and dev.resolution == (0, 0):
598+
try:
599+
from trcc.adapters.device.factory import DeviceProtocolFactory
600+
protocol = DeviceProtocolFactory.get_protocol(dev)
601+
result = protocol.handshake()
602+
res = getattr(result, 'resolution', None) if result else None
603+
if isinstance(res, tuple) and len(res) == 2 and res != (0, 0):
604+
dev.resolution = res
605+
except Exception:
606+
pass # Handshake may fail if device not ready
607+
595608
return svc
596609

597610
@staticmethod
@@ -1228,6 +1241,18 @@ def resume():
12281241
if dev.protocol != "scsi":
12291242
continue
12301243

1244+
# Discover resolution via handshake
1245+
if dev.resolution == (0, 0):
1246+
try:
1247+
from trcc.adapters.device.factory import DeviceProtocolFactory
1248+
proto = DeviceProtocolFactory.get_protocol(dev)
1249+
result = proto.handshake()
1250+
res = getattr(result, 'resolution', None) if result else None
1251+
if isinstance(res, tuple) and len(res) == 2 and res != (0, 0):
1252+
dev.resolution = res
1253+
except Exception:
1254+
continue
1255+
12311256
key = Settings.device_config_key(dev.device_index, dev.vid, dev.pid)
12321257
cfg = Settings.get_device_config(key)
12331258
theme_path = cfg.get("theme_path")

src/trcc/core/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ class DeviceInfo:
156156
"""
157157
name: str
158158
path: str # /dev/sgX
159-
resolution: Tuple[int, int] = (320, 320)
159+
resolution: Tuple[int, int] = (0, 0) # Discovered via handshake
160160

161161
# Device properties (from detection)
162162
vendor: Optional[str] = None

src/trcc/qt_components/qt_app_mvc.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -791,20 +791,13 @@ def _on_device_selected(self, device: DeviceInfo):
791791
device.device_index, device.vid, device.pid)
792792
self.uc_preview.set_status(f"Device: {device.path}")
793793

794-
# HID/Bulk devices start with resolution (0,0) — need handshake to discover it.
795-
# Detection only scans USB; the handshake requires opening the transport.
794+
# Resolution (0,0) = not yet discovered — handshake to find it.
796795
w, h = device.resolution
797-
if (w, h) == (0, 0) and device.protocol in ('hid', 'bulk'):
796+
if (w, h) == (0, 0):
798797
self.uc_preview.set_status("Connecting to device...")
799798
self._start_handshake(device)
800799
return
801800

802-
if (w, h) == (0, 0):
803-
log.warning("Device resolution (0,0) — handshake not supported for protocol %s",
804-
device.protocol)
805-
self.uc_preview.set_status("Handshake failed — replug device and restart")
806-
return
807-
808801
self._apply_device_config(device, w, h)
809802

810803
def _start_handshake(self, device: DeviceInfo):
@@ -1020,7 +1013,7 @@ def _on_device_widget_clicked(self, device_info: dict):
10201013
device = DeviceInfo(
10211014
name=device_info.get('name', 'LCD'),
10221015
path=device_info.get('path', ''),
1023-
resolution=device_info.get('resolution', (320, 320)),
1016+
resolution=device_info.get('resolution', (0, 0)),
10241017
model=device_info.get('model'),
10251018
vid=device_info.get('vid', 0),
10261019
pid=device_info.get('pid', 0),
@@ -2069,7 +2062,7 @@ def _on_device_poll(self):
20692062
device = DeviceInfo(
20702063
name=d.get('name', 'LCD'),
20712064
path=d.get('path', ''),
2072-
resolution=d.get('resolution', (320, 320)),
2065+
resolution=d.get('resolution', (0, 0)),
20732066
vendor=d.get('vendor'),
20742067
product=d.get('product'),
20752068
model=d.get('model'),

tests/test_device_scsi.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ def test_returns_device_dicts(self, mock_detect, mock_driver_cls):
344344
self.assertEqual(len(devices), 1)
345345
self.assertEqual(devices[0]['name'], 'Thermalright LCD')
346346
self.assertEqual(devices[0]['path'], '/dev/sg0')
347-
self.assertEqual(devices[0]['resolution'], (320, 320))
347+
self.assertEqual(devices[0]['resolution'], (0, 0))
348348
self.assertEqual(devices[0]['device_index'], 0)
349349

350350
@patch('trcc.adapters.device.detector.detect_devices')
@@ -358,7 +358,7 @@ def test_skips_devices_without_scsi(self, mock_detect):
358358

359359
@patch('trcc.adapters.device.lcd.LCDDriver', side_effect=Exception('driver fail'))
360360
@patch('trcc.adapters.device.detector.detect_devices')
361-
def test_driver_error_uses_default_resolution(self, mock_detect, _):
361+
def test_driver_error_uses_unresolved_resolution(self, mock_detect, _):
362362
dev = MagicMock()
363363
dev.scsi_device = '/dev/sg0'
364364
dev.vendor_name = 'Test'
@@ -372,7 +372,7 @@ def test_driver_error_uses_default_resolution(self, mock_detect, _):
372372
mock_detect.return_value = [dev]
373373

374374
devices = find_lcd_devices()
375-
self.assertEqual(devices[0]['resolution'], (320, 320))
375+
self.assertEqual(devices[0]['resolution'], (0, 0))
376376

377377

378378
# -- send_image_to_device --

tests/test_integration.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,13 @@ class TestCLISendPipeline(unittest.TestCase):
107107
def test_cli_send_image(self, mock_detect, mock_get_protocol):
108108
"""trcc send image.png end-to-end via DeviceService."""
109109
from trcc.cli import send_image
110+
from trcc.core.models import HandshakeResult
110111

111112
mock_detect.return_value = [_make_device()]
112113
mock_protocol = MagicMock()
113114
mock_protocol.send_image.return_value = True
115+
mock_protocol.handshake.return_value = HandshakeResult(
116+
resolution=(320, 320))
114117
mock_get_protocol.return_value = mock_protocol
115118

116119
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
@@ -136,10 +139,13 @@ def test_cli_send_missing_file(self):
136139
def test_cli_send_color(self, mock_detect, mock_get_protocol):
137140
"""trcc color ff0000 end-to-end via DeviceService."""
138141
from trcc.cli import send_color
142+
from trcc.core.models import HandshakeResult
139143

140144
mock_detect.return_value = [_make_device()]
141145
mock_protocol = MagicMock()
142146
mock_protocol.send_image.return_value = True
147+
mock_protocol.handshake.return_value = HandshakeResult(
148+
resolution=(320, 320))
143149
mock_get_protocol.return_value = mock_protocol
144150

145151
result = send_color("ff0000", device="/dev/sg0")
@@ -169,12 +175,15 @@ def test_resume_with_saved_theme(self, mock_key, mock_cfg, mock_detect,
169175
mock_get_protocol):
170176
"""resume() sends last theme with brightness and rotation applied."""
171177
from trcc.cli import resume
178+
from trcc.core.models import HandshakeResult
172179

173180
mock_detect.return_value = [_make_device()]
174181
mock_key.return_value = "0:87cd_70db"
175182

176183
mock_protocol = MagicMock()
177184
mock_protocol.send_image.return_value = True
185+
mock_protocol.handshake.return_value = HandshakeResult(
186+
resolution=(320, 320))
178187
mock_get_protocol.return_value = mock_protocol
179188

180189
with tempfile.TemporaryDirectory() as td:

0 commit comments

Comments
 (0)