Skip to content
Closed
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
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,11 +310,17 @@ CDC serial JPEG protocol at 2 Mbaud (VID `33C3`):
| JL Chill Arc 360 | 480×960 |
| JL Strip Display (OEM SKUs, PID 7791–7810) | 1920×462 |

### VMAX / AuyiHomu
### VMAX / AuyiHomu / Valkyrie

| Model | Resolution | USB ID |
|---|---|---|
| VMAX 4.6" LCD (AuyiHomu HY-001, HY-002) | 320×960 | `345F:9132` |
| 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

Expand All @@ -326,8 +332,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.
55 changes: 55 additions & 0 deletions docs/VALKYRIE-V360.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Valkyrie V360 LCD (320×240)

The V360 screen uses the MacroSilicon MS9132 bridge (`345f:9132`). It is managed
under **VMAX / AuyiHomu / Valkyrie**, reusing the existing UYVY renderer and device
supervisor. Existing VMAX configuration names and XML remain compatible.

## 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 readable geometry is skipped.
An unreadable EDID does not select V360. 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.

## 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 351 tests, including geometry routing, the
initialization trace digest, post-frame command order and XML settings round-trip.
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"
22 changes: 11 additions & 11 deletions src/InfoPanel.App/Views/Pages/DevicesPage.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,14 +229,14 @@ private void RebuildRows()
device.JpegQuality, q => { device.JpegQuality = q; _app.Host.SaveSettings(); });
}

AddFamilyHeader("VMAX / AuyiHomu", settings.VmaxPanelMultiDeviceMode,
AddFamilyHeader("VMAX / AuyiHomu / Valkyrie", settings.VmaxPanelMultiDeviceMode,
v => { settings.VmaxPanelMultiDeviceMode = v; _app.Host.SaveSettings(); });
foreach (var device in settings.VmaxPanelDevices.ToList())
{
AddRow(
title: device.ModelInfo?.Name ?? device.Model.ToString(),
subtitle: Subtitle(device.DeviceId, device.DeviceLocation,
device.DisplayWidth > 0 ? $"renders at {device.DisplayWidth}×{device.DisplayHeight}" : null),
device.DisplayWidth > 0 ? $"renders at {device.DisplayWidth}×{device.DisplayHeight}{(device.ModelInfo?.Experimental == true ? "; experimental, not hardware-tested on Linux" : "")}" : null),
isEnabled: device.Enabled,
setEnabled: v => { device.Enabled = v; _app.Host.SaveSettings(); },
profileGuid: device.ProfileGuid,
Expand All @@ -250,7 +250,7 @@ private void RebuildRows()
setRotation: r => { device.Rotation = r; _app.Host.SaveSettings(); });
AddPanelOptions(device.Brightness, b => { device.Brightness = b; _app.Host.SaveSettings(); },
device.TargetFrameRate, f => { device.TargetFrameRate = f; _app.Host.SaveSettings(); },
device.JpegQuality, q => { device.JpegQuality = q; _app.Host.SaveSettings(); });
null, null);
}

AddFamilyHeader("Jonsbo", settings.JonsboPanelMultiDeviceMode,
Expand Down Expand Up @@ -1004,8 +1004,6 @@ private async void Scan_Click(object? sender, RoutedEventArgs e)

// Jonsbo AIO (DS916 HLVMAX serial, DS339 MS9132)
var jonsbo = await Task.Run(JonsboPanelHelper.ScanDevices);
var ds339Confirmed = jonsbo.Any(f => f.Model == JonsboPanelModel.DS339 && f.Ms9132Confirmed);
var ms9132Ambiguous = jonsbo.Any(f => f.Model == JonsboPanelModel.DS339 && !f.Ms9132Confirmed);
foreach (var found in jonsbo)
{
var existing = settings.JonsboPanelDevices.FirstOrDefault(d => d.DeviceId == found.DeviceId);
Expand Down Expand Up @@ -1033,12 +1031,12 @@ private async void Scan_Click(object? sender, RoutedEventArgs e)
}
}

// VMAX / AuyiHomu - shares the MS9132 bridge (345F:9132) with the Jonsbo
// DS339, so skip when the EDID identified the panel as a DS339, or when
// it is unreadable but the device is already configured as a DS339.
var skipVmax = ds339Confirmed
|| (ms9132Ambiguous && settings.JonsboPanelDevices.Any(d => d.Model == JonsboPanelModel.DS339));
var vmax = skipVmax ? new List<VmaxPanelDiscoveryInfo>() : await Task.Run(VmaxPanelHelper.ScanDevices);
// All three panels share 345F:9132. The EDID routes known geometry;
// when unreadable, keep the existing per-device Jonsbo/VMAX choice.
var vmax = await Task.Run(VmaxPanelHelper.ScanDevices);
vmax.RemoveAll(found => jonsbo.Any(j => j.Model == JonsboPanelModel.DS339
&& j.DeviceLocation == found.SerialNumber
&& (j.Ms9132Confirmed || !settings.VmaxPanelDevices.Any(v => v.DeviceId == found.DeviceId))));
foreach (var found in vmax)
{
var existing = settings.VmaxPanelDevices.FirstOrDefault(d => d.DeviceId == found.DeviceId);
Expand All @@ -1049,12 +1047,14 @@ private async void Scan_Click(object? sender, RoutedEventArgs e)
DeviceId = found.DeviceId,
DeviceLocation = found.DeviceLocation,
Model = found.Model,
TargetFrameRate = found.ModelInfo?.IsValkyrie == true ? 10 : 1,
});
added++;
}
else
{
existing.DeviceLocation = found.DeviceLocation;
existing.Model = found.Model;
updated++;
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/InfoPanel.Core/VmaxPanel/VmaxPanelModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ public enum VmaxPanelModel
{
Unknown,
Vmax46Inch,
ValkyrieV360,
ValkyrieMc360,
}
}
21 changes: 21 additions & 0 deletions src/InfoPanel.Core/VmaxPanel/VmaxPanelModelDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public static class VmaxPanelModelDatabase
public static readonly (int Vid, int Pid)[] SupportedDevices =
[
(VMAX_VENDOR_ID, VMAX_PRODUCT_ID_46INCH),
(0x374A, 0xA021),
];

public static readonly Dictionary<VmaxPanelModel, VmaxPanelModelInfo> Models = new()
Expand All @@ -24,8 +25,28 @@ public static readonly (int Vid, int Pid)[] SupportedDevices =
VendorId = VMAX_VENDOR_ID,
ProductId = VMAX_PRODUCT_ID_46INCH,
},
[VmaxPanelModel.ValkyrieV360] = new VmaxPanelModelInfo
{
Model = VmaxPanelModel.ValkyrieV360,
Name = "Valkyrie V360 LCD",
Width = 320,
Height = 240,
VendorId = VMAX_VENDOR_ID,
ProductId = VMAX_PRODUCT_ID_46INCH,
},
[VmaxPanelModel.ValkyrieMc360] = new VmaxPanelModelInfo
{
Model = VmaxPanelModel.ValkyrieMc360,
Name = "Valkyrie MC360 LCD",
Width = 640, Height = 480, VendorId = 0x374A, ProductId = 0xA021,
UsesBgr565 = true, Experimental = true,
},
};

// VID/PID identifies the shared MS9132 bridge, not the attached panel.
public static VmaxPanelModelInfo? GetModelByPanelSize(int width, int height) =>
Models.Values.FirstOrDefault(m => m.VendorId == VMAX_VENDOR_ID && m.Width == width && m.Height == height);

public static VmaxPanelModelInfo? GetModelByVidPid(int vid, int pid)
{
return Models.Values.FirstOrDefault(m => m.VendorId == vid && m.ProductId == pid);
Expand Down
3 changes: 3 additions & 0 deletions src/InfoPanel.Core/VmaxPanel/VmaxPanelModelInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ public class VmaxPanelModelInfo
public int Height { get; init; }
public int VendorId { get; init; }
public int ProductId { get; init; }
public bool UsesBgr565 { get; init; }
public bool Experimental { get; init; }
public bool IsValkyrie => Model == VmaxPanelModel.ValkyrieV360 || Model == VmaxPanelModel.ValkyrieMc360;
}
}
Loading