Skip to content
Open
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
15 changes: 13 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` |

See [V360 protocol and hardware validation](docs/VALKYRIE-V360.md) and
[MC360 protocol and validation limits](docs/VALKYRIE-MC360.md). Only identified
retail models are listed; matching a screen size alone does not add compatibility.

### 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
64 changes: 64 additions & 0 deletions docs/VALKYRIE-MC360.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Valkyrie MC360 LCD

This is an **experimental Linux implementation** for one identified retail model.
The device title is `Valkyrie MC360 LCD`; the device-row subtitle marks its
experimental status. No physical MC360 has been tested with this implementation.
The existing V360 hardware-tested HID/UYVY path remains separate.

## Identification

All of these must match before a device can be added or started:

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

A serial, when available, selects the exact device. Otherwise its USB device path
is used. The screen geometry is queried again on every connection. Other BoWei
PIDs, other product strings and other geometries are not assigned an MC360 name.
Discovery only handshakes and reads info; it does not set brightness or send images.
The `374a:a021` USB udev rule covers this model; there is no HID requirement.

## Wire contract

Interface 0 uses bulk OUT `02` and bulk IN `81`. Requests have a little-endian
16-bit command and 32-bit payload length. Responses must echo command OR `8000`,
with the declared and actual response lengths matching. Four-byte nonzero status
responses fail the operation.

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

A frame is little-endian BGR565. Command `1001` announces its four-byte byte count;
only after its successful acknowledgement are raw pixels sent in chunks up to
`20000` hex bytes. Every transfer aligned to `1000` hex bytes gets a zero-length
packet. Static profiles update when content/options change; idle sessions send
`000F` with `78 56 34 12`. Screen-off sends a black image; exit retains the last
image and releases interface 0, without rebooting or sending the vendor close
command that blanks the screen.

## Evidence and scope

- The [MythCool-Lite compatibility table](https://github.com/ZGMFX01A/MythCool-Lite/blob/342f8a330a5e8d83e229395fecf11bafd56a1158/README.md#硬件与驱动兼容性清单)
identifies MC360, its USB descriptor and 640×480 geometry, with author-reported
physical validation on Windows. Its current device core is private.
- [Published BoWei wire research](https://github.com/codehz/msdisplay/blob/34dd469d83c199a16832994755d2352495148a4e/bowei_aio/display.py)
documents A021 mode-2 packets and BGR565 framing. This transport was written
independently from those protocol facts; it does not include that source,
a private dependency, vendor binaries or firmware.
- Automated tests cover descriptor and geometry restrictions, command byte order,
malformed replies, screen-info rejection, BGR565 channel order and row stride,
transfer chunk/ZLP order, failed acknowledgements and XML settings.

These checks verify software behavior, not the physical MC360 display. A tester
must still confirm initialization, colors, orientation, static-image retention,
brightness, screen-off, reconnect and suspend/resume on Linux.

The catalog deliberately excludes B360/B360-GT shared material-code profiles and
unmapped MS resolutions. The [vendor download page](https://valkyrie.com.cn/)
mentions R/AL/ML/DX air-cooler families, but a family name or advertised resolution
is insufficient to assign a protocol to a retail model. No air-cooler entry is
added without an explicit model-to-protocol mapping.
58 changes: 58 additions & 0 deletions docs/VALKYRIE-V360.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Valkyrie V360 LCD (320×240)

The V360 screen uses the MacroSilicon MS9132 bridge (`345f:9132`). It is managed
under its own **Valkyrie** section, reusing the existing UYVY renderer and device
supervisor. Valkyrie devices have a separate device list and streaming switch;
previous Valkyrie entries in the VMAX collection migrate with their settings and hotkeys.

## Identification and access

The same USB ID appears on several unrelated screens. Discovery reads the first
EDID detailed timing descriptor: 320×240 selects V360, 320×960 selects VMAX,
and 376×960 belongs to the Jonsbo DS339 path. Unknown or unreadable geometry is skipped instead of defaulting to Jonsbo or VMAX.
A confirmed V360 scan repairs a matching stale DS339 configuration. A stable USB serial pairs the HID control
interface with the video interface; V360 open rechecks the geometry before init.
The existing `345f:9132` udev rules cover both USB and hidraw nodes. RPM installation
reapplies the rules to already-connected V360/MC360 USB and HID nodes.

## Transport

- HID feature reports use LinuxHidRawFeature, retaining the kernel HID driver.
Both ioctl buffers include report ID 0 plus eight payload bytes.
- The V360-specific 77-command initialization is followed by complete UYVY frames
on interface 3 / bulk OUT endpoint 0x04.
- Each frame is 153616 bytes: `FF 00 00 00 00 14 00 F0`, 153600 UYVY pixel bytes,
and `FF C0 00 00 00 00 00 00`.
- Only after the first successful complete frame are the post-frame commands sent:
`B5 F5 07` (read), `B6 F5 07 00`, `A6 05 01 00`, `B5 C5 55` (read).
- `B5 00 32` is polled every 30 subsequent frames. Static profiles are resent,
and brightness/rotation/profile changes invalidate the cached conversion.
- Brightness is software dimming. JPEG quality is irrelevant to raw UYVY and is
hidden in this family's panel options. No pump/fan/RGB commands are sent.

## Validation

I tested a standalone Linux implementation on my V360 under Fedora 44 / Linux 7.2
and confirmed that its test clock, text and color bars displayed correctly.
Its 77-command initialization was compared byte-for-byte with LCD Fusion.
The regression fixture digest includes every padded payload and read-reply flag.

I then tested the integrated InfoPanel AppHost and profile renderer on the same
hardware. Discovery selected V360 and excluded the Jonsbo path. My profile with
text, a live clock and CPU temperature streamed at approximately 9 FPS with a
10 FPS target, and I confirmed correct colors and orientation. I changed and
restored software brightness and 180-degree rotation during streaming. I also
verified that stopping and restarting the device task reacquired both interfaces
and that clean shutdown released them. I have not tested physical suspend/resume.

The solution test suite passes 361 tests, including geometry routing, the
initialization trace digest, post-frame command order, XML settings round-trip,
separate family switches, stale-configuration migration and device-page grouping.
Other Valkyrie models or panels using the same USB ID have not been verified.
The [MC360](VALKYRIE-MC360.md) has a separate experimental bulk transport and does
not use this initialization. Unmapped generic resolutions are not model entries.

## Attribution

The initialization sequence is adapted from [LCD Fusion](https://github.com/itsmylife44/LcdFusion),
MIT © 2026 itsmylife44. The full notice is preserved in `ValkyrieV360Protocol.cs`.
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
81 changes: 81 additions & 0 deletions src/InfoPanel.App/Services/PanelDiscoveryReconciler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using InfoPanel.JonsboPanel;
using InfoPanel.Models;
using InfoPanel.VmaxPanel;

namespace InfoPanel.Services;

/// <summary>Apply confirmed discovery results, repairing earlier MS9132 misidentification.</summary>
public static class PanelDiscoveryReconciler
{
public static (int Added, int Updated) Apply(Settings settings,
IEnumerable<JonsboPanelDiscoveryInfo> jonsbo, IEnumerable<VmaxPanelDiscoveryInfo> displays)
{
ValkyriePanelSettings.Migrate(settings);
int added = 0, updated = 0;
foreach (var found in jonsbo)
{
if (found.Model == JonsboPanelModel.DS339 && !found.Ms9132Confirmed) continue;
var existing = settings.JonsboPanelDevices.FirstOrDefault(d => Same(d.DeviceId, found.DeviceId));
if (existing == null)
{
settings.JonsboPanelDevices.Add(new JonsboPanelDevice
{
DeviceId = found.DeviceId, DeviceLocation = found.DeviceLocation, Model = found.Model,
});
added++;
}
else
{
existing.DeviceLocation = found.DeviceLocation;
existing.Model = found.Model;
updated++;
}
}
foreach (var found in displays)
{
var isValkyrie = found.ModelInfo?.IsValkyrie == true;
var family = isValkyrie ? settings.ValkyriePanelDevices : settings.VmaxPanelDevices;
var wasEmpty = family.Count == 0;
var existing = family.FirstOrDefault(d => Same(d.DeviceId, found.DeviceId));
// Only confirmed V360 geometry permits removing the old erroneous DS339.
var stale = found.Model == VmaxPanelModel.ValkyrieV360
? settings.JonsboPanelDevices.FirstOrDefault(d => d.Model == JonsboPanelModel.DS339 && Same(d.DeviceId, found.DeviceId))
: null;
if (existing == null)
{
existing = new VmaxPanelDevice
{
DeviceId = found.DeviceId, DeviceLocation = found.DeviceLocation, Model = found.Model,
TargetFrameRate = isValkyrie ? 10 : 1,
// Preserve assignments, but don't start a previously disabled device.
ProfileGuid = stale?.ProfileGuid ?? Guid.Empty,
Enabled = stale?.Enabled ?? false,
Brightness = stale?.Brightness ?? 99,
};
family.Add(existing);
added++;
}
else
{
existing.DeviceLocation = found.DeviceLocation;
existing.Model = found.Model;
updated++;
}
if (stale != null)
{
settings.JonsboPanelDevices.Remove(stale);
foreach (var binding in settings.HotkeyBindings.Where(b => b.DeviceType == "Jonsbo" && Same(b.DeviceId, stale.DeviceId)))
binding.DeviceType = "Valkyrie";
if (settings.JonsboPanelDevices.Count == 0) settings.JonsboPanelMultiDeviceMode = false;
}
if (wasEmpty)
{
if (isValkyrie) settings.ValkyriePanelMultiDeviceMode = true;
else settings.VmaxPanelMultiDeviceMode = true;
}
}
return (added, updated);
}

private static bool Same(string a, string b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
}
3 changes: 3 additions & 0 deletions src/InfoPanel.App/ViewModels/ProfileCardViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ public string Subtitle
outputs.AddRange(host.Settings.JlPanelDevices
.Where(d => d.Enabled && d.ProfileGuid == Profile.Guid)
.Select(d => d.ModelInfo?.Name ?? d.Model.ToString()));
outputs.AddRange(host.Settings.ValkyriePanelDevices
.Where(d => d.Enabled && d.ProfileGuid == Profile.Guid)
.Select(d => d.ModelInfo?.Name ?? d.Model.ToString()));
outputs.AddRange(host.Settings.VmaxPanelDevices
.Where(d => d.Enabled && d.ProfileGuid == Profile.Guid)
.Select(d => d.ModelInfo?.Name ?? d.Model.ToString()));
Expand Down
Loading