Skip to content

Commit e87f2c5

Browse files
committed
完善独占触摸设备初始化与热插拔恢复
- 按 Windows HID 顺序查询 FLTouch Feature Report 3,再切换多点输入模式 - 读取异常时释放旧句柄并自动重新发现、配置和初始化设备 - 启动时提前注册触摸提供器,未发现设备时持续轮询 PDX 与 FLTouch - 记录连接与重连原因,并在游戏退出时终止后台恢复流程
1 parent c99e2e3 commit e87f2c5

3 files changed

Lines changed: 256 additions & 75 deletions

File tree

AquaMai.Mods/GameSystem/ExclusiveTouch/ExclusiveTouch.cs

Lines changed: 173 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,20 @@ public abstract class ExclusiveTouchBase(int playerNo, int vid, int pid, [CanBeN
2020
int timeoutMilliseconds = 20)
2121
{
2222
private UsbDevice device;
23+
private readonly object deviceLock = new();
24+
private volatile bool stopping;
2325
private TouchSensorMapper touchSensorMapper;
2426

25-
public bool IsConnected => device != null;
27+
public bool IsConnected
28+
{
29+
get
30+
{
31+
lock (deviceLock)
32+
{
33+
return device != null;
34+
}
35+
}
36+
}
2637

2738
protected int PlayerNo => playerNo;
2839

@@ -63,116 +74,217 @@ public TouchUpdate(ushort x, ushort y, int fingerId, bool isPressed)
6374

6475
protected virtual string DiagnosticName => "ExclusiveTouch";
6576

66-
public bool Start()
77+
public bool Start(bool logConnectionFailure = true)
6778
{
68-
// 方便组 2P
69-
UsbDeviceFinder finder;
70-
71-
if (!string.IsNullOrWhiteSpace(serialNumber))
79+
stopping = false;
80+
if (!TryConnectDevice())
7281
{
73-
// 优先使用序列号
74-
finder = new UsbDeviceFinder(vid, pid, serialNumber);
82+
if (logConnectionFailure)
83+
{
84+
MelonLogger.Msg($"[ExclusiveTouch] Cannot connect {playerNo + 1}P");
85+
}
86+
return false;
7587
}
76-
else if (!string.IsNullOrWhiteSpace(locationPath))
88+
89+
try
7790
{
78-
// 使用位置路径匹配
79-
finder = new UsbDeviceLocationFinder(vid, pid, locationPath);
91+
touchSensorMapper = new TouchSensorMapper(minX, minY, maxX, maxY, radius, flip,
92+
aExtraRadius, bExtraRadius, cExtraRadius, dExtraRadius, eExtraRadius);
93+
94+
for (int i = 0; i < 256; i++)
95+
{
96+
allFingerPoints[i] = new TouchPoint();
97+
}
98+
99+
Thread readThread = new(ReadThread);
100+
readThread.IsBackground = true;
101+
readThread.Start();
102+
Application.quitting += () =>
103+
{
104+
stopping = true;
105+
CloseCurrentDevice();
106+
};
107+
return true;
80108
}
81-
else
109+
catch (Exception e)
82110
{
83-
// 使用第一个匹配的设备
84-
finder = new UsbDeviceFinder(vid, pid);
111+
MelonLogger.Error($"[ExclusiveTouch] Cannot initialize {playerNo + 1}P: {e}");
112+
CloseCurrentDevice();
113+
return false;
85114
}
86-
87-
device = UsbDevice.OpenUsbDevice(finder);
88-
if (device == null)
115+
}
116+
117+
protected virtual void InitializeDevice(UsbDevice usbDevice) { }
118+
119+
private UsbDeviceFinder CreateFinder()
120+
{
121+
// 方便组 2P
122+
if (!string.IsNullOrWhiteSpace(serialNumber))
89123
{
90-
MelonLogger.Msg($"[ExclusiveTouch] Cannot connect {playerNo + 1}P");
91-
return false;
124+
// 优先使用序列号
125+
return new UsbDeviceFinder(vid, pid, serialNumber);
126+
}
127+
128+
if (!string.IsNullOrWhiteSpace(locationPath))
129+
{
130+
// 使用位置路径匹配
131+
return new UsbDeviceLocationFinder(vid, pid, locationPath);
92132
}
93133

134+
// 使用第一个匹配的设备
135+
return new UsbDeviceFinder(vid, pid);
136+
}
137+
138+
private bool TryConnectDevice()
139+
{
140+
UsbDevice newDevice = null;
94141
try
95142
{
96-
if (device is WinUsbDevice winUsbDevice)
143+
newDevice = UsbDevice.OpenUsbDevice(CreateFinder());
144+
if (newDevice == null) return false;
145+
146+
if (newDevice is WinUsbDevice winUsbDevice)
97147
{
98148
// 触摸屏固件不能可靠处理 WinUSB 的选择性挂起
99149
winUsbDevice.PowerPolicy.AutoSuspend = false;
100150
}
101151

102-
IUsbDevice wholeDevice = device as IUsbDevice;
152+
IUsbDevice wholeDevice = newDevice as IUsbDevice;
103153
if (wholeDevice != null &&
104154
(!wholeDevice.SetConfiguration(configuration) || !wholeDevice.ClaimInterface(interfaceNumber)))
105155
{
106-
MelonLogger.Error($"[ExclusiveTouch] Cannot initialize {playerNo + 1}P");
107-
device.Close();
108-
device = null;
109-
return false;
110-
}
111-
InitializeDevice(device);
112-
touchSensorMapper = new TouchSensorMapper(minX, minY, maxX, maxY, radius, flip,
113-
aExtraRadius, bExtraRadius, cExtraRadius, dExtraRadius, eExtraRadius);
114-
115-
for (int i = 0; i < 256; i++)
116-
{
117-
allFingerPoints[i] = new TouchPoint();
156+
throw new InvalidOperationException("USB interface setup failed");
118157
}
119158

120-
Thread readThread = new(ReadThread);
121-
readThread.Start();
122-
TouchStatusProvider.RegisterTouchStatusProvider(playerNo, GetTouchState);
123-
Application.quitting += () =>
159+
InitializeDevice(newDevice);
160+
lock (deviceLock)
124161
{
125-
var tmpDevice = device;
126-
device = null;
127-
if (wholeDevice != null)
162+
if (stopping)
128163
{
129-
wholeDevice.ReleaseInterface(interfaceNumber);
164+
CloseDevice(newDevice);
165+
return false;
130166
}
131-
tmpDevice.Close();
132-
};
167+
168+
device = newDevice;
169+
}
170+
ExclusiveTouchDiagnostics.Log(
171+
"{0} player={1} connected driver={2}",
172+
DiagnosticName, playerNo + 1, newDevice.DriverMode);
133173
return true;
134174
}
135175
catch (Exception e)
136176
{
137-
MelonLogger.Error($"[ExclusiveTouch] Cannot initialize {playerNo + 1}P: {e}");
138-
device?.Close();
139-
device = null;
177+
MelonLogger.Error($"[ExclusiveTouch] Cannot initialize {playerNo + 1}P: {e.Message}");
178+
if (newDevice != null) CloseDevice(newDevice);
140179
return false;
141180
}
142181
}
143182

144-
protected virtual void InitializeDevice(UsbDevice usbDevice) { }
183+
private void CloseCurrentDevice()
184+
{
185+
UsbDevice oldDevice;
186+
lock (deviceLock)
187+
{
188+
oldDevice = device;
189+
device = null;
190+
}
191+
192+
if (oldDevice != null) CloseDevice(oldDevice);
193+
}
194+
195+
private bool IsCurrentDevice(UsbDevice target)
196+
{
197+
lock (deviceLock)
198+
{
199+
return ReferenceEquals(target, device);
200+
}
201+
}
202+
203+
private void CloseDevice(UsbDevice target)
204+
{
205+
try
206+
{
207+
if (target is IUsbDevice wholeDevice)
208+
{
209+
wholeDevice.ReleaseInterface(interfaceNumber);
210+
}
211+
}
212+
catch (Exception e)
213+
{
214+
MelonLogger.Warning($"[ExclusiveTouch] Cannot release {playerNo + 1}P interface: {e.Message}");
215+
}
216+
217+
try
218+
{
219+
target.Close();
220+
}
221+
catch (Exception e)
222+
{
223+
MelonLogger.Warning($"[ExclusiveTouch] Cannot close {playerNo + 1}P device: {e.Message}");
224+
}
225+
}
145226

146227
private void ReadThread()
147228
{
148229
byte[] buffer = new byte[packetSize];
149-
var reader = device.OpenEndpointReader(endpoint);
150230
using var pinnedBuffer = new PinnedHandle(buffer);
151-
231+
152232
try
153233
{
154-
while (device != null)
234+
while (!stopping)
155235
{
156-
int bytesRead;
157-
ErrorCode ec = reader.Read(pinnedBuffer.Handle, 0, buffer.Length, 100, out bytesRead); // 100ms 超时
236+
UsbDevice currentDevice;
237+
lock (deviceLock)
238+
{
239+
currentDevice = device;
240+
}
158241

159-
if (ec != ErrorCode.None)
242+
if (currentDevice == null)
160243
{
161-
if (ec == ErrorCode.IoTimedOut) continue; // 超时就继续等
162-
MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 读取错误: {ec}");
163-
break;
244+
Thread.Sleep(1000);
245+
TryConnectDevice();
246+
continue;
164247
}
165248

166-
if (bytesRead > 0)
249+
try
250+
{
251+
using var reader = currentDevice.OpenEndpointReader(endpoint);
252+
while (!stopping)
253+
{
254+
int bytesRead;
255+
ErrorCode ec = reader.Read(pinnedBuffer.Handle, 0, buffer.Length, 100, out bytesRead); // 100ms 超时
256+
257+
if (ec == ErrorCode.IoTimedOut) continue; // 超时就继续等
258+
if (ec != ErrorCode.None)
259+
{
260+
MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 读取错误: {ec},尝试重连");
261+
ExclusiveTouchDiagnostics.Log(
262+
"{0} player={1} reconnect reason=read-error code={2}",
263+
DiagnosticName, playerNo + 1, ec);
264+
CloseCurrentDevice();
265+
break;
266+
}
267+
268+
if (bytesRead > 0 && IsCurrentDevice(currentDevice))
269+
{
270+
OnTouchData(buffer);
271+
}
272+
}
273+
}
274+
catch (Exception e)
167275
{
168-
OnTouchData(buffer);
276+
if (stopping) break;
277+
MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 读取异常: {e.Message},尝试重连");
278+
ExclusiveTouchDiagnostics.Log(
279+
"{0} player={1} reconnect reason=read-exception error={2}",
280+
DiagnosticName, playerNo + 1, e.Message);
281+
CloseCurrentDevice();
169282
}
170283
}
171284
}
172285
finally
173286
{
174-
// 确保 reader 被正确释放
175-
reader?.Dispose();
287+
CloseCurrentDevice();
176288
}
177289
}
178290

@@ -261,7 +373,7 @@ private ulong ComputeActiveMask()
261373
}
262374
return mask;
263375
}
264-
private ulong GetTouchState(int player)
376+
internal ulong GetTouchState(int player)
265377
{
266378
if (player != playerNo) return 0;
267379
lock (touchLock)

0 commit comments

Comments
 (0)