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
29 changes: 29 additions & 0 deletions LICENSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,35 @@ release build and their licenses.

## MIT License

### LCD Fusion (Valkyrie V360 protocol initialization)
- **Copyright**: © 2026 itsmylife44
- **Project**: https://github.com/itsmylife44/LcdFusion
- **Notice**: Also retained in `src/InfoPanel.Devices.VmaxPanel/ValkyrieV360Protocol.cs`.

```text
MIT License

Copyright (c) 2026 itsmylife44

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

### Avalonia (including Avalonia.Desktop, Avalonia.Controls.ColorPicker, Avalonia.Fonts.Inter)
- **Copyright**: © AvaloniaUI OÜ and contributors
- **Project**: https://avaloniaui.net/
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,17 @@ CDC serial JPEG protocol at 2 Mbaud (VID `33C3`):
|---|---|---|
| VMAX 4.6" LCD (AuyiHomu HY-001, HY-002) | 320×960 | `345F:9132` |

### Valkyrie

| Model | Resolution | USB ID |
|---|---|---|
| Valkyrie V360 LCD ✓ | 320×240 | `345F:9132` |
| Valkyrie MC360 LCD (experimental) | 640×480 | `374A:A021`, `Vk-Copl-08` |

Protocol details and test status are in [docs/VALKYRIE-V360.md](docs/VALKYRIE-V360.md)
and [docs/VALKYRIE-MC360.md](docs/VALKYRIE-MC360.md). Only identified retail
models are listed; a matching screen size alone doesn't make a panel supported.

### Jonsbo

Displays shipped with Jonsbo AIO coolers; protocols reverse-engineered from
Expand All @@ -326,8 +337,8 @@ the OEM JONSBO-AIO app:
| Jonsbo DS916 ✓ | 462×1920 | Serial `33C3:F101` (HLVMAX, raw JPEG frames) |
| Jonsbo DS339 ✓ | 376×960 | USB `345F:9132` (MacroSilicon MS9132, HID + bulk UYVY422) |

The DS339 shares its USB ID with the VMAX 4.6"; the device scan reads the
panel EDID to tell them apart.
The DS339 shares its USB ID with the VMAX 4.6" and Valkyrie V360; the device
scan reads the panel EDID to tell them apart.

### Turing Smart Screen

Expand Down Expand Up @@ -575,6 +586,8 @@ rendering backend for diagnosing platform issues.
(Universal Screen and HydroShift II models).
- [yattuLizard](https://github.com/yattuLizard): VMAX / AuyiHomu panel
support.
- [EsiksonX](https://github.com/EsiksonX): Valkyrie V360 and MC360 panel
support.
- [fweepa](https://github.com/fweepa): stopwatch plugin and hotkeys.
- [Orkunowski](https://github.com/Orkunowski): designer UX improvements.
- [LACT](https://github.com/ilya-zlobintsev/LACT): the NvAPI-on-Linux
Expand Down
22 changes: 21 additions & 1 deletion aur/infopanel-bin.install
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
_trigger_lcds() {
[ -S /run/udev/control ] || return 0
udevadm control --reload-rules || :
# Apply permissions to already-connected Valkyrie/shared MS9132 LCDs.
# Reloading rules alone only affects the next plug-in event.
for device in /sys/bus/usb/devices/*; do
[ -r "$device/idVendor" ] && [ -r "$device/idProduct" ] || continue
read -r vendor < "$device/idVendor"
read -r product < "$device/idProduct"
case "$vendor:$product" in
345f:9132|374a:a021)
udevadm trigger --action=change "$device" || :
udevadm trigger --action=change --subsystem-match=hidraw --parent-match="$(readlink -f "$device")" || :
;;
esac
done
udevadm settle --timeout=5 || :
}

post_install() {
_trigger_lcds
cat <<'EOF'
==> InfoPanel setup:
USB panels: replug the panel once (or run: udevadm trigger) so the
Expand All @@ -14,5 +34,5 @@ EOF
}

post_upgrade() {
return 0
_trigger_lcds
}
2 changes: 1 addition & 1 deletion docs/USER-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ To keep idle cost low, a plugin whose sensors are not shown on any streaming pan

## Devices

The Devices page detects supported USB LCD panels: BeadaPanel, Turing Smart Screen (including Rev 4.6"), Thermalright (40+ models), Thermaltake / ASRock, JL / Hongtai, VMAX, Jonsbo (DS916, DS339) and Lian Li (Universal Screen 8.8"/9.2", HydroShift II). For each panel you can assign a profile, set rotation and brightness, and watch the live frame rate and latency. To stream, a panel needs its row's Enabled switch on and a profile assigned; each family also has a streaming master switch in its section header, which turns on automatically when a scan finds the family's first panel.
The Devices page detects supported USB LCD panels: BeadaPanel, Turing Smart Screen (including Rev 4.6"), Thermalright (40+ models), Thermaltake / ASRock, JL / Hongtai, VMAX, Valkyrie (V360, MC360 experimental), Jonsbo (DS916, DS339) and Lian Li (Universal Screen 8.8"/9.2", HydroShift II). For each panel you can assign a profile, set rotation and brightness, and watch the live frame rate and latency. To stream, a panel needs its row's Enabled switch on and a profile assigned; each family also has a streaming master switch in its section header, which turns on automatically when a scan finds the family's first panel.

Some panels share one USB ID across many models (for example most Thermalright HID panels). The scan identifies the exact model by briefly talking to the panel; if that is not possible the row shows a placeholder model until the first connection, when the panel reports what it really is.

Expand Down
63 changes: 63 additions & 0 deletions docs/VALKYRIE-MC360.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Valkyrie MC360 LCD (640×480, experimental)

This driver has not run on a physical MC360 yet. The device row on the Devices
page says so. It shares nothing with the V360 HID/UYVY path.

## Identification

A device has to pass all of these checks before it's added or started:

- USB `374A:A021` with product string `Vk-Copl-08` (case-insensitive).
- A valid mode-2 reply to the open handshake and the screen-info command.
- Screen info reporting width 640, height 480 and a 614400-byte framebuffer.
The `F001` pixel-format variant is rejected.

The serial, when there is one, selects the device. Otherwise the USB device path
does. Geometry is queried again on every connection. Other BoWei product IDs,
product strings or geometries never get the MC360 name. A scan only handshakes
and reads screen info; it doesn't set brightness or send an image. The
`374a:a021` USB udev rule is enough, since there is no HID interface.

## Wire protocol

Interface 0 uses bulk OUT `02` and bulk IN `81`. A request is a little-endian
16-bit command followed by a 32-bit payload length. A reply echoes the command
ORed with `8000`, and its declared length must match what arrived. A nonzero
four-byte status reply fails the operation.

Opening sends command `1003` with payload `45 23 01 00`, then command `0001` for
the 28-byte screen-info structure. Width, height and framebuffer length sit at
byte offsets 12, 16 and 24. A streaming connection sets brightness (`0002`) to
100, and the profile's brightness setting dims the pixels in software.

Frames are little-endian BGR565. Command `1001` announces the four-byte byte
count, and raw pixels follow only after it's acknowledged, in chunks of up to
`0x20000` bytes. Every transfer that is a multiple of `0x1000` bytes is followed
by a zero-length packet. Static profiles are sent when their content or options
change, and idle connections send `000F` with `78 56 34 12` once a second.
Screen-off sends a black frame. On exit the driver releases interface 0 and
leaves the last image up. It doesn't reboot the device or send the vendor close
command, which blanks the screen.

## Sources and what's left to test

The [MythCool-Lite compatibility table](https://github.com/ZGMFX01A/MythCool-Lite/blob/342f8a330a5e8d83e229395fecf11bafd56a1158/README.md#硬件与驱动兼容性清单)
lists the MC360 with this USB descriptor and 640×480 geometry, and its author
reports testing it on Windows. That project's device code is private. The A021
mode-2 packets and BGR565 framing come from
[codehz/msdisplay](https://github.com/codehz/msdisplay/blob/34dd469d83c199a16832994755d2352495148a4e/bowei_aio/display.py).
This transport was written from those protocol facts and includes none of that
source, vendor binaries or firmware.

Tests cover the descriptor and geometry checks, command byte order, malformed
replies, screen-info rejection, BGR565 channel order and row stride, chunk and
zero-length-packet order, failed acknowledgements and settings serialization.
Someone with the hardware still has to check initialization, colors,
orientation, static-image retention, brightness, screen-off, reconnect and
suspend/resume on Linux.

The B360 and B360-GT are left out on purpose: their profiles share material
codes, and other MS resolutions aren't mapped to a retail model. The
[vendor download page](https://valkyrie.com.cn/) also lists R, AL, ML and DX air
coolers, but a family name or an advertised resolution isn't enough to know
which protocol a model speaks, so none of them are listed.
69 changes: 69 additions & 0 deletions docs/VALKYRIE-V360.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Valkyrie V360 LCD (320×240)

The V360 screen sits behind a MacroSilicon MS9132 bridge (`345f:9132`), the same
chip the VMAX 4.6" and the Jonsbo DS339 use. It has its own Valkyrie section on
the Devices page with a separate streaming switch, and it reuses the VMAX UYVY
renderer and device supervisor.

## Identification

The USB ID alone can't tell these panels apart, so the scan reads the first
detailed timing descriptor of the panel EDID. 320×240 is a V360 and 376×960 is a
DS339. Any other readable geometry stays on the VMAX path, as it did before the
V360 existed, because no VMAX EDID has been captured yet. A panel whose EDID
can't be read is skipped, and the scan status asks you to check USB/HID
permissions.

The V360 driver pairs the HID control interface with the video interface by USB
serial, and rechecks the geometry before it initializes the panel. The VMAX
driver skips any HID interface whose EDID says V360 or DS339, so a VMAX entry
never initializes the wrong screen.

Earlier releases added a V360 as a VMAX 4.6", or as a DS339 when the EDID was
unreadable. A scan that confirms the V360 replaces that entry and keeps its
profile, enabled state, brightness, rotation and hotkeys.

The `345f:9132` udev rules cover both the USB and hidraw nodes. The deb, RPM and
AUR packages re-trigger udev for already-connected V360 and MC360 screens, so
they don't need a replug after install.

## Transport

HID feature reports go through `LinuxHidRawFeature`, which keeps the kernel HID
driver bound. Each ioctl buffer holds report ID 0 plus eight payload bytes.

After a V360-specific sequence of 77 commands, the driver sends complete UYVY
frames to interface 3, bulk OUT endpoint `0x04`. A frame is 153616 bytes:
`FF 00 00 00 00 14 00 F0`, then 153600 bytes of UYVY pixels, then
`FF C0 00 00 00 00 00 00`.

Scan-out starts after the first complete frame, with `B5 F5 07` (read),
`B6 F5 07 00`, `A6 05 01 00` and `B5 C5 55` (read). After that the driver polls
`B5 00 32` every 30 frames. Static profiles are resent, and a brightness,
rotation or profile change invalidates the cached conversion.

Brightness is software dimming. JPEG quality doesn't apply to raw UYVY, so the
panel options hide it. The driver sends no pump, fan or RGB commands.

## Testing

[EsiksonX](https://github.com/EsiksonX) tested this on a V360 under Fedora 44
with Linux 7.2. Discovery picked the V360 and left the Jonsbo path alone. A
profile with text, a live clock and CPU temperature streamed at about 9 FPS
against a 10 FPS target, with correct colors and orientation. Brightness and
180° rotation changed live while streaming. Stopping and restarting the device
task reacquired both interfaces, and a clean shutdown released them. Suspend and
resume have not been tested on the V360.

Tests pin the 77-command sequence with a digest of every padded payload and
read flag, compared byte for byte with LCD Fusion. They also cover the EDID
routing, the post-frame command order, settings round-trips, the separate family
switches and the migration of stale entries. No other Valkyrie model on this USB
ID has been verified. The [MC360](VALKYRIE-MC360.md) uses a different bulk
transport.

## Attribution

The initialization sequence is adapted from
[LCD Fusion](https://github.com/itsmylife44/LcdFusion), MIT © 2026 itsmylife44.
The full notice is in `ValkyrieV360Protocol.cs` and `LICENSES.md`.
14 changes: 14 additions & 0 deletions packaging/debian/postinst
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,19 @@ if [[ "${1:-}" == configure ]]; then
fi
if [[ -S /run/udev/control ]] && command -v udevadm >/dev/null 2>&1; then
udevadm control --reload-rules || :
# Apply permissions to already-connected Valkyrie/shared MS9132 LCDs.
# Reloading rules alone only affects the next plug-in event.
for device in /sys/bus/usb/devices/*; do
[ -r "$device/idVendor" ] && [ -r "$device/idProduct" ] || continue
read -r vendor < "$device/idVendor"
read -r product < "$device/idProduct"
case "$vendor:$product" in
345f:9132|374a:a021)
udevadm trigger --action=change "$device" || :
udevadm trigger --action=change --subsystem-match=hidraw --parent-match="$(readlink -f "$device")" || :
;;
esac
done
udevadm settle --timeout=5 || :
fi
fi
5 changes: 4 additions & 1 deletion packaging/infopanel-udev.rules
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ KERNEL=="hidraw*", ATTRS{idVendor}=="26ce", ATTRS{idProduct}=="0a11", MODE="0666
# access without dialout membership; both are Artinchip 33c3 SKUs)
SUBSYSTEM=="tty", ATTRS{idVendor}=="33c3", MODE="0666"

# VMAX / AuyiHomu 4.6" LCD (HY-001, HY-002) and Jonsbo DS339 (both MacroSilicon MS9132)
# VMAX / AuyiHomu 4.6" LCD, Jonsbo DS339 and Valkyrie V360 (MacroSilicon MS9132)
SUBSYSTEM=="usb", ATTR{idVendor}=="345f", ATTR{idProduct}=="9132", MODE="0666"
KERNEL=="hidraw*", ATTRS{idVendor}=="345f", ATTRS{idProduct}=="9132", MODE="0666"

# Valkyrie MC360 LCD (mode-2 bulk protocol, experimental)
SUBSYSTEM=="usb", ATTR{idVendor}=="374a", ATTR{idProduct}=="a021", MODE="0666"
14 changes: 14 additions & 0 deletions packaging/rpm/infopanel.spec
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ if [ -d /run/systemd/system ]; then
fi
if [ -S /run/udev/control ]; then
udevadm control --reload-rules || :
# Apply permissions to already-connected Valkyrie/shared MS9132 LCDs.
# Reloading rules alone only affects the next plug-in event.
for device in /sys/bus/usb/devices/*; do
[ -r "$device/idVendor" ] && [ -r "$device/idProduct" ] || continue
read -r vendor < "$device/idVendor"
read -r product < "$device/idProduct"
case "$vendor:$product" in
345f:9132|374a:a021)
udevadm trigger --action=change "$device" || :
udevadm trigger --action=change --subsystem-match=hidraw --parent-match="$(readlink -f "$device")" || :
;;
esac
done
udevadm settle --timeout=5 || :
fi

%preun
Expand Down
4 changes: 3 additions & 1 deletion src/InfoPanel.App/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ public void Initialize()
// Configuration
Settings = ConfigPersistence.LoadSettings() ?? new Settings();
MigrateLianLiDevices();
if (ValkyriePanelSettings.Migrate(Settings)) SaveSettings();
foreach (var profile in ConfigPersistence.LoadProfiles() ?? [])
{
Profiles.Add(profile);
Expand Down Expand Up @@ -152,8 +153,9 @@ public void Initialize()
if (Settings.JlPanelMultiDeviceMode) await JlPanelTask.Instance.StartAsync();
break;
case nameof(Settings.VmaxPanelMultiDeviceMode):
case nameof(Settings.ValkyriePanelMultiDeviceMode):
await VmaxPanelTask.Instance.StopAsync();
if (Settings.VmaxPanelMultiDeviceMode) await VmaxPanelTask.Instance.StartAsync();
if (Settings.VmaxPanelMultiDeviceMode || Settings.ValkyriePanelMultiDeviceMode) await VmaxPanelTask.Instance.StartAsync();
break;
case nameof(Settings.JonsboPanelMultiDeviceMode):
await JonsboPanelTask.Instance.StopAsync();
Expand Down
1 change: 1 addition & 0 deletions src/InfoPanel.App/Services/HotkeyManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ private void ApplyHotkey(HotkeyBinding binding)
"Thermalright" => SetProfile(_settings.ThermalrightPanelDevices.FirstOrDefault(d => d.DeviceId == binding.DeviceId), d => d.ProfileGuid = binding.ProfileGuid),
"Thermaltake" => SetProfile(_settings.ThermaltakePanelDevices.FirstOrDefault(d => d.DeviceId == binding.DeviceId), d => d.ProfileGuid = binding.ProfileGuid),
"Jl" => SetProfile(_settings.JlPanelDevices.FirstOrDefault(d => d.DeviceId == binding.DeviceId), d => d.ProfileGuid = binding.ProfileGuid),
"Valkyrie" => SetProfile(_settings.ValkyriePanelDevices.FirstOrDefault(d => d.DeviceId == binding.DeviceId), d => d.ProfileGuid = binding.ProfileGuid),
"Vmax" => SetProfile(_settings.VmaxPanelDevices.FirstOrDefault(d => d.DeviceId == binding.DeviceId), d => d.ProfileGuid = binding.ProfileGuid),
"Jonsbo" => SetProfile(_settings.JonsboPanelDevices.FirstOrDefault(d => d.DeviceId == binding.DeviceId), d => d.ProfileGuid = binding.ProfileGuid),
"LianLi" => SetProfile(_settings.LianLiPanelDevices.FirstOrDefault(d => d.DeviceId == binding.DeviceId), d => d.ProfileGuid = binding.ProfileGuid),
Expand Down
Loading