Skip to content

Commit 93fcb5a

Browse files
Lexonight1claude
andcommitted
fix: resolution-dependent RGB565 byte order + GUI crash on HID handshake failure
RGB565 byte order: Windows TRCC ImageTo565 uses big-endian only for 320x320, little-endian for all other resolutions (240x240, 480x480, etc.). Our code was hardcoded to big-endian, causing swapped R/B channels on non-320x320 SCSI devices like FROZEN WARFRAME. Fixes #17. GUI crash: HID devices with failed handshake keep resolution (0,0). The GUI propagated this into setFixedSize(0,0) and theme directory lookups, causing a PyQt6 segfault ("Memory access error"). Now guards against (0,0) and shows a status message instead. Fixes #1 (PantherX12max crash). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2720dfe commit 93fcb5a

File tree

5 files changed

+48
-13
lines changed

5 files changed

+48
-13
lines changed

doc/USBLCD_PROTOCOL.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,13 +206,18 @@ Triggered when shared memory byte[3] == 0x7F.
206206

207207
## Pixel Format
208208

209-
RGB565 big-endian, 2 bytes per pixel:
209+
RGB565, 2 bytes per pixel. **Byte order is resolution-dependent** (from Windows TRCC `ImageTo565`):
210+
211+
- **320×320** (`is320x320`): big-endian — byte[0]=RRRRRGGG, byte[1]=GGGBBBBB
212+
- **Other resolutions** (240×240, 480×480, etc.): little-endian — byte[0]=GGGBBBBB, byte[1]=RRRRRGGG
210213

211214
```python
212215
r5 = (r >> 3) & 0x1F # 5 bits red
213216
g6 = (g >> 2) & 0x3F # 6 bits green
214217
b5 = (b >> 3) & 0x1F # 5 bits blue
215-
pixel = (r5 << 11) | (g6 << 5) | b5 # uint16, stored big-endian
218+
pixel = (r5 << 11) | (g6 << 5) | b5 # uint16
219+
# 320x320: struct.pack('>H', pixel) — big-endian
220+
# other: struct.pack('<H', pixel) — little-endian
216221
```
217222

218223
Total frame size = width × height × 2 bytes.

src/trcc/core/controllers.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,15 @@
4141
)
4242

4343

44-
def image_to_rgb565(img: Any) -> bytes:
45-
"""Convert PIL Image to RGB565 bytes (big-endian, masked)."""
44+
def image_to_rgb565(img: Any, byte_order: str = '>') -> bytes:
45+
"""Convert PIL Image to RGB565 bytes.
46+
47+
Windows TRCC ImageTo565: big-endian for 320x320, little-endian otherwise.
48+
49+
Args:
50+
img: PIL Image.
51+
byte_order: '>' for big-endian, '<' for little-endian.
52+
"""
4653
if img.mode != 'RGB':
4754
img = img.convert('RGB')
4855

@@ -51,7 +58,7 @@ def image_to_rgb565(img: Any) -> bytes:
5158
g = (arr[:, :, 1] >> 2) & 0x3F
5259
b = (arr[:, :, 2] >> 3) & 0x1F
5360
rgb565 = (r << 11) | (g << 5) | b
54-
return rgb565.astype('>u2').tobytes()
61+
return rgb565.astype(f'{byte_order}u2').tobytes()
5562

5663

5764
def apply_rotation(image: Any, rotation: int) -> Any:
@@ -1095,8 +1102,16 @@ def _send_frame_to_lcd(self, image: Any):
10951102
self._handle_error(f"LCD send error: {e}")
10961103

10971104
def _image_to_rgb565(self, img: Any) -> bytes:
1098-
"""Convert PIL Image to RGB565 bytes."""
1099-
return image_to_rgb565(img)
1105+
"""Convert PIL Image to RGB565 bytes.
1106+
1107+
Byte order matches Windows TRCC ImageTo565:
1108+
big-endian for 320x320, little-endian for other resolutions.
1109+
"""
1110+
byte_order = '>'
1111+
device = self.devices.get_selected()
1112+
if device and device.protocol == 'scsi' and device.resolution != (320, 320):
1113+
byte_order = '<'
1114+
return image_to_rgb565(img, byte_order)
11001115

11011116
# =========================================================================
11021117
# Callbacks from sub-controllers

src/trcc/core/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ def detect_devices(self) -> List[DeviceInfo]:
289289
device_index=d.get('device_index', 0),
290290
protocol=d.get('protocol', 'scsi'),
291291
device_type=d.get('device_type', 1),
292+
implementation=d.get('implementation', 'generic'),
292293
)
293294
for d in raw_devices
294295
]

src/trcc/device_implementations.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,21 @@ def get_frame_delay(self) -> float:
7373
"""Delay between frames (seconds)"""
7474
return 0.0
7575

76+
@property
77+
def pixel_byte_order(self) -> str:
78+
"""RGB565 byte order: '>' big-endian or '<' little-endian.
79+
80+
Windows TRCC ImageTo565 uses big-endian only for 320x320 (is320x320)
81+
and SPIMode=2 (FBL 51/53). All other resolutions use little-endian.
82+
"""
83+
if (self.width, self.height) == (320, 320):
84+
return '>'
85+
return '<'
86+
7687
def rgb_to_bytes(self, r: int, g: int, b: int) -> bytes:
77-
"""Convert RGB to device pixel format"""
78-
# RGB565 big-endian (default)
88+
"""Convert RGB to device pixel format (RGB565)."""
7989
pixel = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
80-
return struct.pack('>H', pixel)
90+
return struct.pack(f'{self.pixel_byte_order}H', pixel)
8191

8292
def detect_resolution(self, device_path: str, verbose: bool = False) -> bool:
8393
"""

src/trcc/qt_components/qt_app_mvc.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -797,9 +797,13 @@ def _on_device_selected(self, device: DeviceInfo):
797797
device.device_index, device.vid, device.pid)
798798
self.uc_preview.set_status(f"Device: {device.path}")
799799

800-
# Update resolution if changed
801-
if device.resolution != (self.controller.lcd_width, self.controller.lcd_height):
802-
self._on_resolution_changed(*device.resolution)
800+
# Update resolution if changed (skip (0,0) — HID handshake failed)
801+
w, h = device.resolution
802+
if (w, h) == (0, 0):
803+
self.uc_preview.set_status("Handshake failed — replug device and restart")
804+
return
805+
if (w, h) != (self.controller.lcd_width, self.controller.lcd_height):
806+
self._on_resolution_changed(w, h)
803807

804808
# Restore per-device brightness and rotation
805809
cfg = get_device_config(self._active_device_key)

0 commit comments

Comments
 (0)