Skip to content

Commit a629438

Browse files
committed
feat: PDX 驱动管理
1 parent 2990c25 commit a629438

13 files changed

Lines changed: 571 additions & 17 deletions

File tree

AquaMai

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
using System.ComponentModel;
2+
using System.Diagnostics;
3+
using System.Runtime.InteropServices;
4+
using System.Text;
5+
using System.Text.RegularExpressions;
6+
using Microsoft.AspNetCore.Mvc;
7+
8+
namespace MaiChartManager.Controllers.Mod;
9+
10+
[ApiController]
11+
[Route("MaiChartManagerServlet/[action]Api")]
12+
public class PdxController(ILogger<PdxController> logger) : ControllerBase
13+
{
14+
private static string PdxDriverPath => Path.Combine(StaticSettings.exeDir, "pdx-driver.exe");
15+
16+
public record PdxDriverStatusDto(bool IsUsingWinusb, int DeviceCount, bool Available, string[] DevicePaths);
17+
18+
[HttpGet]
19+
public string[] GetPdxDevicePaths()
20+
{
21+
try
22+
{
23+
return PdxDeviceHelper.GetDevicePaths(PdxDeviceHelper.PdxVid, PdxDeviceHelper.PdxPid);
24+
}
25+
catch (Exception ex)
26+
{
27+
logger.LogError(ex, "Failed to get pdx device paths");
28+
return [];
29+
}
30+
}
31+
32+
[HttpGet]
33+
public PdxDriverStatusDto GetPdxDriverStatus()
34+
{
35+
try
36+
{
37+
var paths = GetPdxDevicePaths();
38+
var isWinUsb = PdxDeviceHelper.IsUsingWinUsb(PdxDeviceHelper.PdxVid, PdxDeviceHelper.PdxPid);
39+
return new PdxDriverStatusDto(
40+
IsUsingWinusb: isWinUsb,
41+
DeviceCount: paths.Length,
42+
Available: System.IO.File.Exists(PdxDriverPath),
43+
DevicePaths: paths
44+
);
45+
}
46+
catch (Exception ex)
47+
{
48+
logger.LogError(ex, "Failed to get pdx driver status");
49+
return new PdxDriverStatusDto(IsUsingWinusb: false, DeviceCount: 0, Available: false, DevicePaths: []);
50+
}
51+
}
52+
53+
[HttpPost]
54+
public IActionResult SwitchPdxDriver([FromQuery] string direction)
55+
{
56+
Process? process = null;
57+
try
58+
{
59+
process = Process.Start(new ProcessStartInfo
60+
{
61+
FileName = PdxDriverPath,
62+
Arguments = direction,
63+
UseShellExecute = true,
64+
Verb = "runas"
65+
});
66+
67+
if (process == null) return StatusCode(500, "无法启动 pdx-driver.exe");
68+
69+
var exited = process.WaitForExit(300000);
70+
if (!exited)
71+
{
72+
process.Kill();
73+
return StatusCode(500, "驱动切换超时");
74+
}
75+
76+
if (process.ExitCode != 0)
77+
return StatusCode(500, $"驱动切换失败,退出码: {process.ExitCode}");
78+
79+
return Ok();
80+
}
81+
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
82+
{
83+
return BadRequest("用户取消了权限请求");
84+
}
85+
catch (Exception ex)
86+
{
87+
return StatusCode(500, ex.Message);
88+
}
89+
finally
90+
{
91+
process?.Dispose();
92+
}
93+
}
94+
95+
/// <summary>
96+
/// 使用 Windows SetupAPI 枚举 PDX USB 设备并检查驱动状态的纯 C# 实现。
97+
/// 替代之前通过 DllImport 调用 pdx-driver.exe 的方式(会导致栈溢出崩溃)。
98+
/// </summary>
99+
private static class PdxDeviceHelper
100+
{
101+
public const ushort PdxVid = 0x3356;
102+
public const ushort PdxPid = 0x3003;
103+
104+
private const int DIGCF_PRESENT = 0x02;
105+
private const int DIGCF_ALLCLASSES = 0x04;
106+
107+
private const int SPDRP_HARDWAREID = 0x01;
108+
private const int SPDRP_SERVICE = 0x04;
109+
private const int SPDRP_LOCATION_PATHS = 0x23;
110+
111+
private static readonly Regex UsbPortRegex = new(@"#USB\((\d+)\)", RegexOptions.Compiled);
112+
113+
[StructLayout(LayoutKind.Sequential)]
114+
private struct SP_DEVINFO_DATA
115+
{
116+
public uint cbSize;
117+
public Guid ClassGuid;
118+
public uint DevInst;
119+
public IntPtr Reserved;
120+
}
121+
122+
[DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)]
123+
private static extern IntPtr SetupDiGetClassDevs(
124+
IntPtr classGuid,
125+
string? enumerator,
126+
IntPtr hwndParent,
127+
int flags);
128+
129+
[DllImport("setupapi.dll", SetLastError = true)]
130+
private static extern bool SetupDiEnumDeviceInfo(
131+
IntPtr deviceInfoSet,
132+
int memberIndex,
133+
ref SP_DEVINFO_DATA deviceInfoData);
134+
135+
[DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)]
136+
private static extern bool SetupDiGetDeviceRegistryProperty(
137+
IntPtr deviceInfoSet,
138+
ref SP_DEVINFO_DATA deviceInfoData,
139+
int property,
140+
out int propertyRegDataType,
141+
byte[]? propertyBuffer,
142+
int propertyBufferSize,
143+
out int requiredSize);
144+
145+
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
146+
private static extern bool SetupDiDestroyDeviceInfoList(IntPtr deviceInfoSet);
147+
148+
/// <summary>
149+
/// 枚举所有匹配指定 VID/PID 的 USB 设备,返回它们的端口链路径(如 "2.2")。
150+
/// </summary>
151+
public static string[] GetDevicePaths(ushort vid, ushort pid)
152+
{
153+
var paths = new HashSet<string>();
154+
var deviceInfoSet = SetupDiGetClassDevs(
155+
IntPtr.Zero, "USB", IntPtr.Zero,
156+
DIGCF_PRESENT | DIGCF_ALLCLASSES);
157+
158+
if (deviceInfoSet == IntPtr.Zero || deviceInfoSet == new IntPtr(-1))
159+
return [];
160+
161+
try
162+
{
163+
var devInfoData = new SP_DEVINFO_DATA
164+
{
165+
cbSize = (uint)Marshal.SizeOf<SP_DEVINFO_DATA>()
166+
};
167+
168+
for (var i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, ref devInfoData); i++)
169+
{
170+
if (!MatchesVidPid(deviceInfoSet, ref devInfoData, vid, pid))
171+
continue;
172+
173+
var portChain = GetPortChain(deviceInfoSet, ref devInfoData);
174+
if (!string.IsNullOrEmpty(portChain))
175+
paths.Add(portChain);
176+
}
177+
}
178+
finally
179+
{
180+
SetupDiDestroyDeviceInfoList(deviceInfoSet);
181+
}
182+
183+
return [.. paths];
184+
}
185+
186+
/// <summary>
187+
/// 检查是否有任何匹配指定 VID/PID 的 USB 设备正在使用 WinUSB 驱动。
188+
/// </summary>
189+
public static bool IsUsingWinUsb(ushort vid, ushort pid)
190+
{
191+
var deviceInfoSet = SetupDiGetClassDevs(
192+
IntPtr.Zero, "USB", IntPtr.Zero,
193+
DIGCF_PRESENT | DIGCF_ALLCLASSES);
194+
195+
if (deviceInfoSet == IntPtr.Zero || deviceInfoSet == new IntPtr(-1))
196+
return false;
197+
198+
try
199+
{
200+
var devInfoData = new SP_DEVINFO_DATA
201+
{
202+
cbSize = (uint)Marshal.SizeOf<SP_DEVINFO_DATA>()
203+
};
204+
205+
for (var i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, ref devInfoData); i++)
206+
{
207+
if (!MatchesVidPid(deviceInfoSet, ref devInfoData, vid, pid))
208+
continue;
209+
210+
var service = GetStringProperty(deviceInfoSet, ref devInfoData, SPDRP_SERVICE);
211+
if (string.Equals(service, "WinUSB", StringComparison.OrdinalIgnoreCase))
212+
return true;
213+
}
214+
}
215+
finally
216+
{
217+
SetupDiDestroyDeviceInfoList(deviceInfoSet);
218+
}
219+
220+
return false;
221+
}
222+
223+
/// <summary>
224+
/// 检查设备的 HardwareID 是否包含指定的 VID/PID。
225+
/// HardwareID 格式如: USB\VID_3356&amp;PID_3003&amp;REV_0200
226+
/// </summary>
227+
private static bool MatchesVidPid(IntPtr deviceInfoSet, ref SP_DEVINFO_DATA devInfoData, ushort vid, ushort pid)
228+
{
229+
var hardwareIds = GetMultiStringProperty(deviceInfoSet, ref devInfoData, SPDRP_HARDWAREID);
230+
if (hardwareIds == null) return false;
231+
232+
var vidStr = $"VID_{vid:X4}";
233+
var pidStr = $"PID_{pid:X4}";
234+
235+
foreach (var id in hardwareIds)
236+
{
237+
if (id.Contains(vidStr, StringComparison.OrdinalIgnoreCase) &&
238+
id.Contains(pidStr, StringComparison.OrdinalIgnoreCase))
239+
return true;
240+
}
241+
242+
return false;
243+
}
244+
245+
/// <summary>
246+
/// 从设备的 LocationPaths 属性提取 USB 端口链。
247+
/// LocationPaths 格式: PCIROOT(0)#PCI(0801)#USBROOT(0)#USB(2)#USB(2)#USBMI(1)
248+
/// 提取 #USB(\d+) 部分并用 "." 连接,如 "2.2"。
249+
/// </summary>
250+
private static string? GetPortChain(IntPtr deviceInfoSet, ref SP_DEVINFO_DATA devInfoData)
251+
{
252+
var locationPaths = GetMultiStringProperty(deviceInfoSet, ref devInfoData, SPDRP_LOCATION_PATHS);
253+
if (locationPaths == null || locationPaths.Length == 0) return null;
254+
255+
// 使用第一个 LocationPath
256+
var path = locationPaths[0];
257+
var matches = UsbPortRegex.Matches(path);
258+
if (matches.Count == 0) return null;
259+
260+
var ports = new string[matches.Count];
261+
for (var i = 0; i < matches.Count; i++)
262+
ports[i] = matches[i].Groups[1].Value;
263+
264+
return string.Join(".", ports);
265+
}
266+
267+
/// <summary>
268+
/// 获取设备的 REG_SZ 类型注册表属性。
269+
/// </summary>
270+
private static string? GetStringProperty(IntPtr deviceInfoSet, ref SP_DEVINFO_DATA devInfoData, int property)
271+
{
272+
// 先查询所需大小(预期返回 false,ERROR_INSUFFICIENT_BUFFER)
273+
SetupDiGetDeviceRegistryProperty(
274+
deviceInfoSet, ref devInfoData, property,
275+
out _, null, 0, out int requiredSize);
276+
277+
if (requiredSize <= 0) return null;
278+
279+
var buffer = new byte[requiredSize];
280+
if (!SetupDiGetDeviceRegistryProperty(
281+
deviceInfoSet, ref devInfoData, property,
282+
out _, buffer, buffer.Length, out _))
283+
return null;
284+
285+
return Encoding.Unicode.GetString(buffer).TrimEnd('\0');
286+
}
287+
288+
/// <summary>
289+
/// 获取设备的 REG_MULTI_SZ 类型注册表属性。
290+
/// REG_MULTI_SZ 格式: 多个 null 终止的字符串,最后以双 null 结尾。
291+
/// </summary>
292+
private static string[]? GetMultiStringProperty(IntPtr deviceInfoSet, ref SP_DEVINFO_DATA devInfoData, int property)
293+
{
294+
// 先查询所需大小(预期返回 false,ERROR_INSUFFICIENT_BUFFER)
295+
SetupDiGetDeviceRegistryProperty(
296+
deviceInfoSet, ref devInfoData, property,
297+
out _, null, 0, out int requiredSize);
298+
299+
if (requiredSize <= 0) return null;
300+
301+
var buffer = new byte[requiredSize];
302+
if (!SetupDiGetDeviceRegistryProperty(
303+
deviceInfoSet, ref devInfoData, property,
304+
out _, buffer, buffer.Length, out _))
305+
return null;
306+
307+
return Encoding.Unicode.GetString(buffer)
308+
.Split('\0', StringSplitOptions.RemoveEmptyEntries);
309+
}
310+
}
311+
}

MaiChartManager/Front/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@iconify/json": "^2.2.443",
1717
"@microsoft/fetch-event-source": "^2.0.1",
1818
"@modyfi/vite-plugin-yaml": "^1.1.1",
19-
"@munet/ui": "^1.0.10",
19+
"@munet/ui": "^1.0.12",
2020
"@sentry/vite-plugin": "^5.1.0",
2121
"@sentry/vue": "^10.40.0",
2222
"@types/color": "^4.2.0",

MaiChartManager/Front/pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)