Stream your desktop to Thermalright Vision USB displays — without running TRCC.
The TL-M10 Vision / Mjolnir Vision panels plug into an internal USB 2.0 header and are normally driven by TRCC, Thermalright's own Windows app. I wanted the panels to show what I choose — a region of my desktop, live — without keeping TRCC running in the background. So trvision talks to the panels directly over USB: grab the screen, encode a frame, push it out.
Working. It drives both panels on my machine (a 320×240 and a 1920×462) — capture → JPEG → USB, targeting 30 FPS. The protocol details come from community reverse engineering (credited below) and are confirmed empirically on this hardware; other panel revisions may behave differently.
| Device | USBDISPLAY — VID 0x87AD, PID 0x70DB (ChiZhu Tech family) |
| Driver | WinUSB, bound automatically via MS OS descriptors — Zadig is not needed |
| Interface | Vendor-specific class 0xFF, bulk endpoints |
| Panel size | Derived from the handshake reply, or from a per-serial table in config.py |
- Handshake — a 64-byte packet; the device replies with 1024 bytes. Two bytes of that reply (
PM,SUB) describe the panel.PM=32panels want raw RGB565 (big-endian); the rest accept JPEG. - Frame — a 64-byte header (magic, command, width, height, payload length) followed by the encoded image.
- Transfer — written in 16 KiB bulk chunks, with a zero-length packet when the total is an exact multiple of 512.
- The firmware blanks the panel as soon as the frame stream stops, so trvision keeps sending continuously.
Screen capture uses dxcam (GPU Desktop Duplication) and falls back to mss if dxcam is unavailable.
Windows with Python 3.12 (dxcam wheels are happiest there):
py -3.12 -m venv venv
venv\Scripts\activate
pip install -r requirements.txtClose TRCC completely (tray → exit) before running — it holds the USB device open and trvision won't be able to claim it.
python tools\usb_enum.py # list devices and endpoints
python tools\live_test.py --send-frame # handshake + push one solid color
python main.py --dry-run # capture/encode speed, no device needed
python main.py --serial 2d1d021814a37504 --seconds 10 # 10-second test on one panel
python main.py --region 0,0,800,480 # stream a desktop region, foreverEverything else lives in config.py: USB identity, per-serial panel resolutions, capture region, target FPS, JPEG quality, per-panel transform (rotate 180° / mirror for upside-down mounts), and whether to draw the mouse cursor (dxcam doesn't capture it).
tools/analyze_pcap.py— a standalone USBPcap.pcapngparser with no dependencies and no Wireshark required. It walks the capture, decodes the frame headers, and can extract the transferred images (--extract). This is the tool to reach for when adding support for a panel that behaves differently.tools/usb_enum.py— endpoint discovery.tools/live_test.py— the smallest possible end-to-end check: handshake, printPM/SUB, optionally push a single solid-color frame.
The wire protocol for these 87AD:70DB panels was reverse-engineered and documented by the community:
- thermalright-trcc-linux — GPL-3.0,
doc/PROTOCOL_USBLCDNEW.md - thermalright-lcd-control — Apache-2.0
- Neru-screen-control — MIT
trvision was written from that protocol documentation; no GPL-licensed code was copied into this project.
- Windows-only for now — the capture layer uses Desktop Duplication. The USB side is portable; a Linux capture backend would be the missing piece.
- Only tested against
87AD:70DBpanels. Other Thermalright displays may use a different protocol entirely. - USB 2.0 bandwidth is the real limit, which is why frames go out as JPEG on panels that accept it.
MIT © Osman Demir · osmandemir.net