You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2984 lines
115 KiB
2984 lines
115 KiB
using AIK.Common.AIKProtocol;
|
|
using AIK.Common.Enum;
|
|
using AIK.Common.Event;
|
|
using AIK.Common.Language;
|
|
using AIK.Common.SysCommon;
|
|
using AIK.Models.HidModels;
|
|
using AIK.Service.Interop;
|
|
using AIK.Service.IService;
|
|
using HidSharp;
|
|
using HidSharp.Reports;
|
|
using HidSharp.Reports.Input;
|
|
using Microsoft.Extensions.Logging;
|
|
using SharpCompress.Archives;
|
|
using SharpCompress.Common;
|
|
using SharpCompress.Readers;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.Eventing.Reader;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Linq;
|
|
using System.Management;
|
|
using System.Security;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
|
|
namespace AIK.Service.Service
|
|
{
|
|
public class HidCommunicationService : IHidCommunicationService, IDisposable
|
|
{
|
|
|
|
#region 常量定义
|
|
private const int REPORT_LENGTH = 65;
|
|
private int _reconnectAttempts = 0;
|
|
private const int MAX_RECONNECT_ATTEMPTS = 5;
|
|
private const int RECONNECT_INTERVAL_MS = 100;
|
|
private const int DEVICE_MONITOR_INTERVAL_MS = 300;
|
|
private const int DEVICE_MONITOR_ERROR_DELAY_MS = 500;
|
|
private const int KEEP_ALIVE_INTERVAL_MS = 50000;
|
|
private const int HID_RESPONSE_WARNING_MS = 90000;
|
|
private const int COPY_BUFFER_SIZE = 32 * 1024;
|
|
private const int COPY_CHUNK_DELAY_MS = 5;
|
|
private const int COPY_FILE_DELAY_MS = 50;
|
|
private const int FILE_FLUSH_INTERVAL_BYTES = 4 * 1024 * 1024;
|
|
#endregion
|
|
|
|
#region 公共属性
|
|
public BigTransferPhase CurrentBigPhase => _currentBigPhase;
|
|
public int CompletedBigPhases => _bigPhaseTasks.Count(t => t.IsCompleted);
|
|
public int TotalBigPhases => _bigPhaseTasks.Count;
|
|
public bool IsConnected => _stream != null && _isRunning;
|
|
public TransferState State { get; } = new TransferState();
|
|
#endregion
|
|
|
|
#region 数据传输字段
|
|
//写入文件(本地文件)
|
|
private byte[]? _filelocalData;
|
|
private string? _fileName;
|
|
//key钥匙bin文件
|
|
private byte[]? _fileBinData;
|
|
private string? _fileBinName;
|
|
#endregion
|
|
|
|
#region 设备配置
|
|
//private readonly int _vendorId, _productId; //设备的硬件Id
|
|
private readonly int _vendorId = 0x28E9;
|
|
private readonly int _productId = 0xFFF0;
|
|
private readonly string usbdiskDir = ComStringHelper.K3ToolDirectory;
|
|
#endregion
|
|
|
|
#region 核心状态字段
|
|
private HidDevice? _currentDevice;
|
|
private HidStream? _stream;
|
|
|
|
private volatile bool _isRunning;
|
|
private IProgress<double>? _currentProgress;
|
|
private double _currentBigPhaseProgress;
|
|
private int _currentDataPacketIndex = 0;
|
|
private List<byte> resNotity = new List<byte>();
|
|
// 自动重连相关字段
|
|
private bool _isMonitoring = false;
|
|
private bool _autoReconnectEnabled = true;
|
|
private volatile bool _isProcessingResponse = false;
|
|
private volatile bool _isReconnecting = false;
|
|
private bool _hasSentConnectRequest = false; // 👈 关键标志
|
|
|
|
//单开线程接收设备响应(CPU占用高/备用方法适应老版本库)
|
|
//private Thread? _receiveThread;
|
|
|
|
private HidDeviceInputReceiver? _inputReceiver;
|
|
private byte[] _inputBuffer = Array.Empty<byte>();
|
|
private DateTime _lastHidResponseUtc = DateTime.MinValue;
|
|
private int _consecutiveKeepAliveFailures = 0;
|
|
|
|
private HidOperationTypeEnum _hidOperationType;
|
|
#endregion
|
|
|
|
#region 异步控制
|
|
private CancellationTokenSource? _cts;
|
|
private Thread? _monitorThread;
|
|
private CancellationToken _currentCancellationToken;
|
|
//用于针对大阶段任务进度
|
|
private readonly Dictionary<BigTransferPhase, TaskCompletionSource<bool>> _phaseTcsDict = new Dictionary<BigTransferPhase, TaskCompletionSource<bool>>();
|
|
private BigTransferPhase _currentBigPhase = BigTransferPhase.None; // 当前大阶段
|
|
private List<BigPhaseTask> _bigPhaseTasks = new(); // 大阶段任务队列
|
|
|
|
//第一次连接
|
|
private TaskCompletionSource<bool>? _connectTcs;
|
|
// (用于传输进度)
|
|
private TaskCompletionSource<bool>? _transferTcs;
|
|
// 全局大阶段任务完成源
|
|
private TaskCompletionSource<bool>? _allBigPhasesTcs;
|
|
// 标记是否所有大阶段已完成
|
|
private volatile bool _allBigPhasesCompleted = false;
|
|
|
|
#region 锁
|
|
private readonly object _streamLock = new();
|
|
private readonly object _responseLock = new object();
|
|
private readonly object _reconnectLock = new object();
|
|
// 加锁保护TCS字典
|
|
private readonly object _phaseTcsLock = new object();
|
|
private readonly object _bigPhaseLock = new(); // 大阶段锁
|
|
#endregion
|
|
|
|
#endregion
|
|
|
|
#region 事件定义
|
|
// 新增:大阶段变更事件
|
|
public event EventHandler<BigPhaseChangedEventArgs>? BigPhaseChanged;
|
|
// 新增:所有大阶段完成事件
|
|
public event EventHandler? AllBigPhasesCompleted;
|
|
public event EventHandler<HidResponseReceivedEventArgs>? ResponseReceived;
|
|
public event EventHandler<HidLogMessageEventArgs>? LogMessage;
|
|
// 自动重连相关事件
|
|
public event EventHandler? DeviceConnected;
|
|
public event EventHandler? DeviceDisconnected;
|
|
public event EventHandler<HidReconnectEventArgs>? ReconnectAttempt;
|
|
|
|
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
|
#endregion
|
|
|
|
private readonly ILogger<HidCommunicationService> _logger;
|
|
public HidCommunicationService(ILogger<HidCommunicationService> logger)
|
|
{
|
|
_logger = logger;
|
|
HidStart();
|
|
}
|
|
|
|
private void HidStart()
|
|
{
|
|
try
|
|
{
|
|
if (_isMonitoring || _isReconnecting)
|
|
{
|
|
_logger.LogInformation("设备监视器已在运行或正在重连中,跳过启动。");
|
|
}
|
|
else
|
|
{
|
|
_logger.LogInformation("启动设备监视器...");
|
|
StartDeviceMonitoring();
|
|
// 启动时立即尝试连接,不等轮询
|
|
_ = TryConnectIfDevicePresentAsync();
|
|
// 启用事件驱动监听(设备插入/拔出即时响应)
|
|
StartHidDeviceMonitoring();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "启动 HID 设备监控失败");
|
|
}
|
|
|
|
}
|
|
#region 连接状态事件
|
|
private void OnConnectionStateChanged(bool isConnected, string message = "")
|
|
{
|
|
ConnectionStateChanged?.Invoke(this,
|
|
new ConnectionStateChangedEventArgs(isConnected, message));
|
|
}
|
|
#endregion
|
|
#region 自动重连实现
|
|
|
|
#region 新单开线程监听设备状态 (轮询方式CPU占用稍高)
|
|
/// <summary>
|
|
/// 轮询模式
|
|
/// </summary>
|
|
private void StartDeviceMonitoring()
|
|
{
|
|
if (_isMonitoring) return;
|
|
|
|
_isMonitoring = true;
|
|
_monitorThread = new Thread(DeviceMonitorLoop)
|
|
{
|
|
IsBackground = true,
|
|
Name = "HID-DeviceMonitor"
|
|
};
|
|
_monitorThread.Start();
|
|
|
|
OnLog("🔍 开始设备监视...");
|
|
}
|
|
|
|
private void StopDeviceMonitoring()
|
|
{
|
|
_isMonitoring = false;
|
|
if (_monitorThread != null && _monitorThread.IsAlive)
|
|
{
|
|
_monitorThread.Join(DEVICE_MONITOR_INTERVAL_MS);
|
|
}
|
|
}
|
|
|
|
private void DeviceMonitorLoop()
|
|
{
|
|
while (_isMonitoring)
|
|
{
|
|
try
|
|
{
|
|
// 检查设备连接状态
|
|
CheckDeviceConnection();
|
|
|
|
// 报告实际连接状态(IsConnected = 物理连接,IsDataConnected = 数据交互已建立)
|
|
OnConnectionStateChanged(State.IsDataConnected,
|
|
State.IsDataConnected ? "设备已就绪" :
|
|
State.IsConnected ? "设备已连接,等待应答..." :
|
|
"设备未连接");
|
|
|
|
// 每秒检查一次
|
|
Thread.Sleep(DEVICE_MONITOR_INTERVAL_MS);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning($"设备监视异常: {ex.Message}");
|
|
|
|
Thread.Sleep(DEVICE_MONITOR_ERROR_DELAY_MS); // 异常时等待更久
|
|
}
|
|
}
|
|
}
|
|
private void CheckDeviceConnection()
|
|
{
|
|
lock (_reconnectLock)
|
|
{
|
|
var isDevicePresent = CheckIfDevicePresent();
|
|
// 去掉慢速WMI查询,HidSharp枚举已足够可靠
|
|
|
|
if (isDevicePresent)
|
|
{
|
|
_autoReconnectEnabled = true;
|
|
}
|
|
if ((State.IsConnected || State.IsDataConnected) && !isDevicePresent)
|
|
{
|
|
// 设备意外断开(IsConnected 物理连接 或 IsDataConnected 数据连接,任一成立都应触发)
|
|
OnLog("⚠️ 检测到设备断开", true);
|
|
HandleUnexpectedDisconnection();
|
|
}
|
|
else if (!State.IsConnected && !State.IsDataConnected && isDevicePresent && _autoReconnectEnabled)
|
|
{
|
|
// 设备重新插入,尝试重连
|
|
_ = AttemptReconnect(isDevicePresent);
|
|
}
|
|
else if (!State.IsConnected && !State.IsDataConnected && !isDevicePresent && _autoReconnectEnabled)
|
|
{
|
|
// 设备不存在,但允许重连尝试
|
|
if (_reconnectAttempts < MAX_RECONNECT_ATTEMPTS)
|
|
{
|
|
_ = AttemptReconnect(isDevicePresent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 新增:通过WMI检测USB设备(绕过HidSharp缓存,更精准)
|
|
private bool CheckDeviceByManagementScope()
|
|
{
|
|
try
|
|
{
|
|
using var searcher = new ManagementObjectSearcher(
|
|
"root\\CIMV2",
|
|
$"SELECT * FROM Win32_USBHub WHERE PNPDeviceID LIKE '%VID_{_vendorId:X4}&PID_{_productId:X4}%'");
|
|
|
|
foreach (var queryObj in searcher.Get())
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning($"WMI设备检测失败: {ex.Message}");
|
|
return CheckIfDevicePresent(); // 降级使用HidSharp检测
|
|
}
|
|
}
|
|
private bool CheckIfDevicePresent()
|
|
{
|
|
try
|
|
{
|
|
var devices = DeviceList.Local.GetHidDevices(_vendorId, _productId);
|
|
return devices.Any();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog($"检查设备存在性失败: {ex.Message}", true);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void HandleUnexpectedDisconnection()
|
|
{
|
|
_isRunning = false;
|
|
State.IsDataConnected = false;
|
|
State.IsConnected = false;
|
|
State.IsTransferring = false;
|
|
_hasSentConnectRequest = false;
|
|
_lastHidResponseUtc = DateTime.MinValue;
|
|
_consecutiveKeepAliveFailures = 0;
|
|
lock (_streamLock)
|
|
{
|
|
_stream?.Dispose();
|
|
_stream = null;
|
|
}
|
|
|
|
DeviceDisconnected?.Invoke(this, EventArgs.Empty);
|
|
_reconnectAttempts = 0; // 重置重连计数
|
|
_inputReceiver = null;
|
|
OnLog("设备连接已断开", true);
|
|
CleanupTransfer(_currentBigPhase);
|
|
}
|
|
|
|
private async Task AttemptReconnect(bool isDevicePresent)
|
|
{
|
|
if (_reconnectAttempts >= MAX_RECONNECT_ATTEMPTS && !isDevicePresent)
|
|
{
|
|
OnLog($"❌ 已达到最大重连次数({MAX_RECONNECT_ATTEMPTS}),停止重连", true);
|
|
_autoReconnectEnabled = false; // 禁用自动重连
|
|
return;
|
|
}
|
|
if (State.IsDataConnected && State.IsConnected) return;
|
|
_reconnectAttempts++;
|
|
var currentAttempt = _reconnectAttempts;
|
|
|
|
ReconnectAttempt?.Invoke(this, new HidReconnectEventArgs(currentAttempt, MAX_RECONNECT_ATTEMPTS));
|
|
|
|
//OnLog($"🔄 尝试重连... ({currentAttempt}/{MAX_RECONNECT_ATTEMPTS})");
|
|
|
|
try
|
|
{
|
|
// 等待一段时间再重连
|
|
await Task.Delay(RECONNECT_INTERVAL_MS);
|
|
// 指数退避策略
|
|
//int delayMs = Math.Min(500 * _reconnectAttempts, RECONNECT_INTERVAL_MS);
|
|
//await Task.Delay(delayMs);
|
|
|
|
var success = await ConnectAsync(_vendorId, _productId);
|
|
if (success)
|
|
{
|
|
OnLog("✅ 重连成功");
|
|
_reconnectAttempts = 0; // 重置计数
|
|
DeviceConnected?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
else
|
|
{
|
|
OnLog($"❌ 重连失败 ({currentAttempt}/{MAX_RECONNECT_ATTEMPTS})", true);
|
|
if (State.IsDataConnected == false)
|
|
{
|
|
_ = DisconnectAsync();
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog($"重连异常: {ex.Message}", true);
|
|
_ = DisconnectAsync();
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region HidSharp 热拔插监听机制(设备只响应变更,每次打开页面需要重新执行连接方法,单例模式不适合)
|
|
/// <summary>
|
|
/// 事件驱动模式
|
|
/// </summary>
|
|
private void StartHidDeviceMonitoring()
|
|
{
|
|
DeviceList.Local.Changed += OnDeviceListChanged;
|
|
}
|
|
public void StopHidDeviceMonitoring()
|
|
{
|
|
DeviceList.Local.Changed -= OnDeviceListChanged;
|
|
}
|
|
|
|
private async void OnDeviceListChanged(object? sender, EventArgs e)
|
|
{
|
|
try
|
|
{
|
|
// 检查目标设备是否存在
|
|
var deviceExists = DeviceList.Local.GetHidDevices(_vendorId, _productId).Any();
|
|
bool currentlyConnected = IsConnected;
|
|
// 如果当前未连接,且设备出现了 → 尝试重连
|
|
if (!currentlyConnected && deviceExists)
|
|
{
|
|
OnLog("🔌 检测到设备插入,尝试自动重连...");
|
|
await TryReconnectAsync();
|
|
}
|
|
// 如果已连接,但设备消失了 → 触发断开
|
|
else if (currentlyConnected && !deviceExists)
|
|
{
|
|
OnLog("⚠️ 检测到设备拔出", true);
|
|
// 立即断开并清理状态
|
|
HandleUnexpectedDisconnection();
|
|
}
|
|
|
|
OnConnectionStateChanged(IsConnected, $"设备连接状态:{IsConnected}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog($"设备监听回调异常: {ex.Message}", true);
|
|
}
|
|
}
|
|
private async Task TryReconnectAsync()
|
|
{
|
|
if (_isReconnecting) return; // 防止并发重连
|
|
_isReconnecting = true;
|
|
|
|
try
|
|
{
|
|
await ConnectAsync(_vendorId, _productId);
|
|
}
|
|
finally
|
|
{
|
|
_isReconnecting = false;
|
|
}
|
|
}
|
|
private async Task TryConnectIfDevicePresentAsync()
|
|
{
|
|
// 防止重复连接
|
|
if (IsConnected) return;
|
|
|
|
var devices = DeviceList.Local.GetHidDevices(_vendorId, _productId);
|
|
if (devices.Any())
|
|
{
|
|
OnLog("🔌 检测到启动时设备已插入,尝试连接...");
|
|
await ConnectAsync(_vendorId, _productId);
|
|
}
|
|
else
|
|
{
|
|
OnLog("📭 启动时未检测到设备");
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
|
|
#endregion
|
|
#region 新增:大阶段状态通知
|
|
private void OnBigPhaseChanged(BigTransferPhase newPhase)
|
|
{
|
|
var previous = _currentBigPhase;
|
|
_currentBigPhase = newPhase;
|
|
BigPhaseChanged?.Invoke(this, new BigPhaseChangedEventArgs(
|
|
newPhase, previous, CompletedBigPhases, TotalBigPhases));
|
|
OnLog($"📌 大阶段切换:{previous} → {newPhase}");
|
|
}
|
|
|
|
private void OnAllBigPhasesCompleted()
|
|
{
|
|
OnLog($"🎉 所有大阶段执行完成(共{TotalBigPhases}个)");
|
|
AllBigPhasesCompleted?.Invoke(this, EventArgs.Empty);
|
|
// 标记全局完成
|
|
_allBigPhasesCompleted = true;
|
|
// 双重保障:触发全局TCS
|
|
_allBigPhasesTcs?.TrySetResult(true);
|
|
// 重置大阶段状态
|
|
lock (_bigPhaseLock)
|
|
{
|
|
_bigPhaseTasks.Clear();
|
|
_currentBigPhase = BigTransferPhase.None;
|
|
}
|
|
}
|
|
#endregion
|
|
#region 新增的自动重连控制方法
|
|
|
|
/// <summary>
|
|
/// 启用自动重连
|
|
/// </summary>
|
|
public void EnableAutoReconnect()
|
|
{
|
|
_autoReconnectEnabled = true;
|
|
_reconnectAttempts = 0;
|
|
OnLog("✅ 自动重连已启用");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 禁用自动重连
|
|
/// </summary>
|
|
public void DisableAutoReconnect()
|
|
{
|
|
_autoReconnectEnabled = false;
|
|
OnLog("❌ 自动重连已禁用");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 立即尝试重连
|
|
/// </summary>
|
|
public async Task<bool> ReconnectNowAsync()
|
|
{
|
|
_reconnectAttempts = 0; // 重置计数
|
|
_hasSentConnectRequest = false;
|
|
return await ConnectAsync(_vendorId, _productId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取重连状态
|
|
/// </summary>
|
|
public ReconnectStatus GetReconnectStatus()
|
|
{
|
|
return new ReconnectStatus
|
|
{
|
|
IsAutoReconnectEnabled = _autoReconnectEnabled,
|
|
ReconnectAttempts = _reconnectAttempts,
|
|
MaxReconnectAttempts = MAX_RECONNECT_ATTEMPTS,
|
|
IsMonitoring = _isMonitoring
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
#region 基础发送及响应
|
|
public async Task<bool> ConnectAsync(int vendorId = 0x28E9, int productId = 0xFFF0)
|
|
{
|
|
var devices = DeviceList.Local.GetHidDevices(vendorId, productId);
|
|
if (!devices.Any())
|
|
{
|
|
OnLog("未找到设备", true);
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
var device = devices.First();
|
|
if (!device.TryOpen(out _stream))
|
|
{
|
|
OnLog("无法打开设备流", true);
|
|
return false;
|
|
}
|
|
Debug.Write($"设备已打开,输出报告长度: {device.GetMaxOutputReportLength()},输入报告长度: {device.GetMaxInputReportLength()}");
|
|
_isRunning = true;
|
|
_cts = new CancellationTokenSource();
|
|
_lastHidResponseUtc = DateTime.UtcNow;
|
|
_consecutiveKeepAliveFailures = 0;
|
|
|
|
#region 事件异步接收
|
|
// 初始化接收器
|
|
var descriptor = device.GetReportDescriptor();
|
|
_inputReceiver = descriptor.CreateHidDeviceInputReceiver();
|
|
_inputBuffer = new byte[device.GetMaxInputReportLength()];
|
|
|
|
// 订阅异步接收事件(自动在线程池触发)
|
|
_inputReceiver.Received += OnInputReceived;
|
|
|
|
// 启动接收(非阻塞)
|
|
_inputReceiver.Start(_stream);
|
|
#endregion
|
|
|
|
State.IsConnected = true;
|
|
_reconnectAttempts = 0;
|
|
|
|
if (_hasSentConnectRequest == false)
|
|
{
|
|
_hasSentConnectRequest = true;
|
|
await SendAsync(K3ToolUSBProtocol.ConnectData);
|
|
}
|
|
|
|
// ✅ 适配 .NET Framework 4.6.1 的超时等待机制
|
|
var localConnectTcs = new TaskCompletionSource<bool>();
|
|
using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token))
|
|
{
|
|
timeoutCts.CancelAfter(2500); // 2.5秒超时
|
|
|
|
// 临时保存到字段,供 ProcessIncomingResponse 使用
|
|
_connectTcs = localConnectTcs;
|
|
|
|
try
|
|
{
|
|
// 经典超时等待模式:将 TCS 任务与超时取消令牌结合
|
|
var completedTask = await Task.WhenAny(localConnectTcs.Task, Task.Delay(Timeout.Infinite, timeoutCts.Token));
|
|
await completedTask; // 如果超时,此行会抛出 OperationCanceledException
|
|
return true;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
if (timeoutCts.IsCancellationRequested)
|
|
OnLog("连接超时:未收到设备应答(设备可能未就绪)", false);
|
|
return false;
|
|
}
|
|
//finally
|
|
//{
|
|
// _connectTcs = null; // 清理
|
|
//}
|
|
} // using 块结束,自动释放 timeoutCts
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog($"连接失败: {ex.Message}", true);
|
|
await DisconnectAsync();
|
|
Dispose();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 轮询等待复合设备(U盘+HID)完全加载完成
|
|
/// </summary>
|
|
/// <param name="vendorId">厂商ID</param>
|
|
/// <param name="productId">产品ID</param>
|
|
/// <param name="maxWaitMs">最大等待时间(毫秒)</param>
|
|
/// <param name="pollIntervalMs">轮询间隔(毫秒)</param>
|
|
/// <returns>true 表示设备已就绪;false 表示超时</returns>
|
|
public async Task<bool> WaitForDeviceReadyAsync(
|
|
int vendorId = 0x28E9,
|
|
int productId = 0xFFF0,
|
|
int maxWaitMs = 6000,
|
|
int pollIntervalMs = 300)
|
|
{
|
|
var startTime = DateTime.UtcNow;
|
|
var timeout = TimeSpan.FromMilliseconds(maxWaitMs);
|
|
|
|
while ((DateTime.UtcNow - startTime) < timeout)
|
|
{
|
|
try
|
|
{
|
|
// Step 1: 枚举设备
|
|
var devices = DeviceList.Local.GetHidDevices(vendorId, productId);
|
|
if (!devices.Any())
|
|
{
|
|
await Task.Delay(pollIntervalMs);
|
|
continue;
|
|
}
|
|
|
|
var device = devices.First();
|
|
|
|
// Step 2: 尝试打开设备(可选,但建议)
|
|
if (!device.TryOpen(out var stream))
|
|
{
|
|
await Task.Delay(pollIntervalMs);
|
|
continue;
|
|
}
|
|
|
|
// Step 3: 【核心】读取报告描述符
|
|
var descriptor = null as ReportDescriptor;
|
|
try
|
|
{
|
|
descriptor = device.GetReportDescriptor(); // HidSharp API
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"GetReportDescriptor failed: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
stream?.Dispose(); // 立即释放流
|
|
}
|
|
|
|
// ✅ 描述符有效 = 设备 ready!
|
|
if (descriptor != null && descriptor.DeviceItems.Count > 0)
|
|
{
|
|
OnLog("✅ HID 报告描述符读取成功,设备已就绪", false);
|
|
return true;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"轮询异常: {ex.Message}");
|
|
}
|
|
|
|
await Task.Delay(pollIntervalMs);
|
|
}
|
|
|
|
OnLog($"⚠️ 设备未在 {maxWaitMs}ms 内就绪", true);
|
|
return false;
|
|
}
|
|
#region 接收线程(兼容 .NET Framework 4.6.2,同步 Read 方式)
|
|
private void ReceiveLoop()
|
|
{
|
|
var buffer = new byte[REPORT_LENGTH];
|
|
while (_isRunning && _stream != null)
|
|
{
|
|
try
|
|
{
|
|
_stream.ReadTimeout = 0;
|
|
int n = _stream.Read(buffer, 0, REPORT_LENGTH);
|
|
|
|
if (n > 0)
|
|
{
|
|
var copy = new byte[n];
|
|
Array.Copy(buffer, copy, n);
|
|
|
|
// 触发响应事件
|
|
ResponseReceived?.Invoke(this, new HidResponseReceivedEventArgs(copy));
|
|
|
|
// 处理协议响应
|
|
ProcessIncomingResponse(copy);
|
|
}
|
|
}
|
|
catch (TimeoutException)
|
|
{
|
|
// 正常:无数据,继续循环
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog($"接收异常: {ex.Message}", true);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 自动断开
|
|
if (_isRunning)
|
|
{
|
|
OnLog("设备断开", true);
|
|
_isRunning = false;
|
|
State.IsDataConnected = false;
|
|
State.IsConnected = false;
|
|
_transferTcs?.TrySetResult(false);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region HidSharp 接收设备响应事件驱动
|
|
private void OnInputReceived(object? sender, EventArgs e)
|
|
{
|
|
if (_inputReceiver == null || _inputBuffer.Length == 0) return;
|
|
|
|
Report report;
|
|
while (_inputReceiver.TryRead(_inputBuffer, 0, out report))
|
|
{
|
|
if (report.Length > 0)
|
|
{
|
|
var copy = new byte[report.Length];
|
|
Array.Copy(_inputBuffer, copy, report.Length);
|
|
_lastHidResponseUtc = DateTime.UtcNow;
|
|
_consecutiveKeepAliveFailures = 0;
|
|
|
|
// 触发响应事件(注意:此方法在后台线程调用!)
|
|
ResponseReceived?.Invoke(this, new HidResponseReceivedEventArgs(copy));
|
|
//var s = BitConverter.ToString(copy);
|
|
//if (copy[0] == 0xA5 && copy[0] == 0x5A)
|
|
//{
|
|
// ProcessSpecialFrame(copy);
|
|
//}
|
|
// 处理协议(如果 ProcessIncomingResponse 涉及 UI,需调度)
|
|
ProcessIncomingResponse(copy);
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
public async Task SendAsync(byte[] data)
|
|
{
|
|
if (_stream == null || !_isRunning)
|
|
throw new InvalidOperationException(GetLanguageValueOrDefault("DeviceDisconnected") ?? string.Empty);
|
|
|
|
try
|
|
{
|
|
if (data.Length == 0) return;
|
|
|
|
// 分包发送
|
|
for (int i = 0; i < data.Length; i += REPORT_LENGTH - 1)
|
|
{
|
|
int chunkSize = Math.Min(REPORT_LENGTH - 1, data.Length - i);
|
|
var packet = new byte[REPORT_LENGTH];
|
|
packet[0] = 0x00; // Report ID
|
|
Array.Copy(data, i, packet, 1, chunkSize);
|
|
|
|
lock (_streamLock)
|
|
{
|
|
_stream?.Write(packet);
|
|
}
|
|
|
|
OnLog($"📤 发送包: {BitConverter.ToString(packet)}");
|
|
//await Task.Delay(1); // 避免总线过载
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new InvalidOperationException($"{GetLanguageValueOrDefault("HidSendPacketError") ?? string.Empty}:{ex.Message}");
|
|
}
|
|
|
|
}
|
|
|
|
public async Task DisconnectAsync()
|
|
{
|
|
_isRunning = false;
|
|
_cts?.Cancel();
|
|
|
|
#region (CPU占用高/备用方法适应老版本库)
|
|
//if (_receiveThread != null && _receiveThread.IsAlive)
|
|
//{
|
|
// _receiveThread.Join(DEVICE_MONITOR_INTERVAL_MS);
|
|
//}
|
|
#endregion
|
|
_hasSentConnectRequest = false;
|
|
_inputReceiver = null;
|
|
lock (_streamLock)
|
|
{
|
|
_stream?.Dispose();
|
|
_stream = null;
|
|
}
|
|
_lastHidResponseUtc = DateTime.MinValue;
|
|
_consecutiveKeepAliveFailures = 0;
|
|
State.IsDataConnected = false;
|
|
State.IsConnected = false;
|
|
OnLog("🔌 设备已断开");
|
|
}
|
|
|
|
// 响应处理方法
|
|
public void ProcessIncomingResponse(byte[] data)
|
|
{
|
|
lock (_responseLock)
|
|
{
|
|
if (_isProcessingResponse) return;
|
|
_isProcessingResponse = true;
|
|
try
|
|
{
|
|
if (resNotity.Count < 195)
|
|
{
|
|
resNotity.AddRange(data);
|
|
}
|
|
if (resNotity.Count != 195) return;
|
|
|
|
var responseType = ParseResponseType(resNotity);
|
|
if (responseType == ResponseType.ReadyForData)
|
|
{
|
|
if (State.IsDataConnected == false)
|
|
{
|
|
State.IsDataConnected = true;
|
|
_connectTcs.TrySetResult(true);
|
|
}
|
|
else
|
|
{
|
|
//只有写入钥匙Bin文件时才走下面逻辑
|
|
if (_currentBigPhase == BigTransferPhase.Phase3_Write)
|
|
{
|
|
switch (State.CurrentPhase)
|
|
{
|
|
case TransferPhase.Phase2_WaitingForHeaderAck:
|
|
HandleHeaderAck();
|
|
break;
|
|
case TransferPhase.Phase4_WaitingForDataFrameAck: // 中间确认
|
|
HandleDataFrameAck();
|
|
break;
|
|
case TransferPhase.Phase5_WaitingForFinalDataAck: // 最终确认
|
|
HandleFinalDataAck();
|
|
break;
|
|
case TransferPhase.Phase11_WaitingForComplete:
|
|
HandleFinalComplete();
|
|
break;
|
|
default:
|
|
State.StatusMessage = $"⚠️ 阶段 {State.CurrentPhase} 收到意外的ACK";
|
|
CleanupTransfer(_currentBigPhase);
|
|
break;
|
|
}
|
|
}
|
|
else if (_currentBigPhase == BigTransferPhase.Phase1_Connect || _currentBigPhase == BigTransferPhase.Phase2_Generate)
|
|
{
|
|
HandleFinalComplete();
|
|
}
|
|
else
|
|
{
|
|
//未知情况的ACK回复
|
|
State.StatusMessage = $"⚠️ 大阶段{_currentBigPhase},阶段 {State.CurrentPhase} 收到意外的ACK";
|
|
CleanupTransfer(_currentBigPhase);
|
|
}
|
|
}
|
|
}
|
|
else if (responseType == ResponseType.Nak)
|
|
{
|
|
if (State.CurrentPhase == TransferPhase.Phase7_WaitingForNak)
|
|
{
|
|
HandleNakResponse();
|
|
}
|
|
else
|
|
{
|
|
State.StatusMessage = $"⚠️ 阶段 {State.CurrentPhase} 收到意外的 NAK";
|
|
CleanupTransfer(_currentBigPhase);
|
|
}
|
|
}
|
|
else if (responseType == ResponseType.Ackc)
|
|
{
|
|
if (State.CurrentPhase == TransferPhase.Phase9_WaitingForAckc)
|
|
{
|
|
HandleAckcResponse();
|
|
}
|
|
else
|
|
{
|
|
State.StatusMessage = $"⚠️ 阶段 {State.CurrentPhase} 收到意外的 ACKC";
|
|
CleanupTransfer(_currentBigPhase);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
State.StatusMessage = $"⚠️ 阶段 {State.CurrentPhase}未知的错误,{BitConverter.ToString(resNotity.ToArray())}";
|
|
CleanupTransfer(_currentBigPhase);
|
|
}
|
|
resNotity.Clear();
|
|
}
|
|
finally
|
|
{
|
|
_isProcessingResponse = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
private ResponseType ParseResponseType(List<byte> buffer)
|
|
{
|
|
if (buffer.Count == 195)
|
|
{
|
|
// 检查 ReadyForData(ACK)
|
|
if (K3ToolUSBProtocol.IsReadyForDataFrameResponse(buffer))
|
|
{
|
|
return ResponseType.ReadyForData;
|
|
}
|
|
// 检查 NAK
|
|
else if (K3ToolUSBProtocol.IsNakResponse(buffer))
|
|
{
|
|
return ResponseType.Nak;
|
|
}
|
|
// 检查 ACKC
|
|
else if (K3ToolUSBProtocol.IsAckcResponse(buffer))
|
|
{
|
|
return ResponseType.Ackc;
|
|
}
|
|
}
|
|
return ResponseType.Unknown;
|
|
}
|
|
/// <summary>
|
|
/// 处理特殊帧(协议待定)
|
|
/// </summary>
|
|
private void ProcessSpecialFrame(byte[] frame)
|
|
{
|
|
if (frame.Length < 2) return;
|
|
|
|
byte header = frame[0];
|
|
byte type = frame[1];
|
|
|
|
if (header != 0xA5)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (type)
|
|
{
|
|
case 0x5A: // 固件版本
|
|
if (frame.Length >= 4)
|
|
{
|
|
string version = $"{frame[2]}.{frame[3]}.0";
|
|
}
|
|
break;
|
|
|
|
case 0x5B: // 电池电量
|
|
break;
|
|
|
|
case 0x5C: // 序列号
|
|
if (frame.Length >= 10)
|
|
{
|
|
string serialNumber = BitConverter.ToString(frame, 2, 8).Replace("-", "");
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
private async Task HandleHeaderAck()
|
|
{
|
|
OnLog("收到头帧确认,开始发送数据帧");
|
|
State.StatusMessage = "开始发送数据帧";
|
|
State.CurrentPhase = TransferPhase.Phase3_SendingDataFrames;
|
|
_currentDataPacketIndex = 0;
|
|
|
|
if (_fileBinData == null || State.TotalPackets <= 0) return;
|
|
|
|
await SendNextDataFrameAsync();
|
|
}
|
|
|
|
private async Task SendNextDataFrameAsync()
|
|
{
|
|
if (_currentDataPacketIndex >= State.TotalPackets || !IsConnected) return;
|
|
var frame = K3ToolUSBProtocol.GetSendDataFrame(_currentDataPacketIndex, _fileBinData!).ToArray();
|
|
await SendAsync(frame);
|
|
_currentBigPhaseProgress += (60.0 / State.TotalPackets);
|
|
_currentProgress?.Report(_currentBigPhaseProgress);
|
|
OnLog($"数据帧 {_currentDataPacketIndex + 1}/{State.TotalPackets} 已发送");
|
|
|
|
if (_currentDataPacketIndex == State.TotalPackets - 1)
|
|
{
|
|
State.CurrentPhase = TransferPhase.Phase5_WaitingForFinalDataAck;
|
|
}
|
|
else
|
|
{
|
|
State.CurrentPhase = TransferPhase.Phase4_WaitingForDataFrameAck;
|
|
}
|
|
|
|
_currentDataPacketIndex++;
|
|
}
|
|
|
|
private async Task HandleDataFrameAck()
|
|
{
|
|
OnLog($"收到数据帧确认 {_currentDataPacketIndex - 1}");
|
|
|
|
if (_currentDataPacketIndex < State.TotalPackets)
|
|
{
|
|
State.CurrentPhase = TransferPhase.Phase3_SendingDataFrames;
|
|
await SendNextDataFrameAsync();
|
|
}
|
|
else
|
|
{
|
|
OnLog("所有数据帧发送完成,等待最终确认");
|
|
State.CurrentPhase = TransferPhase.Phase5_WaitingForFinalDataAck;
|
|
}
|
|
}
|
|
|
|
private async Task HandleFinalDataAck()
|
|
{
|
|
OnLog("收到最终数据帧确认,准备发送EOT");
|
|
State.CurrentPhase = TransferPhase.Phase6_SendingEOT;
|
|
_currentBigPhaseProgress += (5.0) / 3;
|
|
_currentProgress?.Report(_currentBigPhaseProgress);
|
|
await SendAsync(K3ToolUSBProtocol.EndEOTData);
|
|
State.CurrentPhase = TransferPhase.Phase7_WaitingForNak;
|
|
}
|
|
|
|
private async Task HandleNakResponse()
|
|
{
|
|
OnLog("收到NAK确认,发送最终EOT");
|
|
State.CurrentPhase = TransferPhase.Phase8_SendingFinalEOT;
|
|
_currentBigPhaseProgress += (5.0) / 3;
|
|
_currentProgress?.Report(_currentBigPhaseProgress);
|
|
await SendAsync(K3ToolUSBProtocol.EndEOTData);
|
|
State.CurrentPhase = TransferPhase.Phase9_WaitingForAckc;
|
|
}
|
|
|
|
private async Task HandleAckcResponse()
|
|
{
|
|
OnLog("收到ACKC确认,发送结束帧");
|
|
State.CurrentPhase = TransferPhase.Phase10_SendEndFrame;
|
|
_currentBigPhaseProgress += (5.0) / 3;
|
|
_currentProgress?.Report(_currentBigPhaseProgress);
|
|
await SendAsync(K3ToolUSBProtocol.GetEndDataFrame());
|
|
State.CurrentPhase = TransferPhase.Phase11_WaitingForComplete;
|
|
if (_hidOperationType == HidOperationTypeEnum.DeviceUpdate)
|
|
{
|
|
await HandleFinalComplete();
|
|
}
|
|
}
|
|
|
|
private async Task HandleFinalComplete()
|
|
{
|
|
OnLog("文件传输完成!");
|
|
State.CurrentPhase = TransferPhase.Completed;
|
|
State.IsTransferring = false;
|
|
State.StatusMessage = "传输完成";
|
|
// 标记当前大阶段任务完成
|
|
lock (_bigPhaseLock)
|
|
{
|
|
var currentTask = _bigPhaseTasks.FirstOrDefault(t => t.Phase == _currentBigPhase);
|
|
if (currentTask != null)
|
|
{
|
|
currentTask.IsCompleted = true;
|
|
//if (_currentBigPhase == BigTransferPhase.Phase3_Write)
|
|
//{
|
|
// // ✅ 报告 100%
|
|
// _currentProgress?.Report(100.0);
|
|
//}
|
|
}
|
|
// ========== 触发当前阶段的独立TCS完成 ==========
|
|
lock (_phaseTcsLock)
|
|
{
|
|
if (_phaseTcsDict.TryGetValue(_currentBigPhase, out var currentPhaseTcs) && !currentPhaseTcs.Task.IsCompleted)
|
|
{
|
|
currentPhaseTcs.TrySetResult(true);
|
|
}
|
|
}
|
|
// ========== 关键:检查是否所有大阶段都已完成 ==========
|
|
_allBigPhasesCompleted = _bigPhaseTasks.All(t => t.IsCompleted);
|
|
if (_allBigPhasesCompleted)
|
|
{
|
|
OnAllBigPhasesCompleted();
|
|
// 触发全局TCS完成(只有所有阶段都完成才会执行这行)
|
|
_allBigPhasesTcs?.TrySetResult(true);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#endregion
|
|
private void OnLog(string message, bool isError = false)
|
|
{
|
|
LogMessage?.Invoke(this, new HidLogMessageEventArgs(message, isError));
|
|
}
|
|
|
|
|
|
#region 进度条拓展(烧录文件)
|
|
/// <summary>
|
|
/// 用于测试(本地路径文件),线上采用读取文件方式
|
|
/// </summary>
|
|
/// <param name="filePath"></param>
|
|
/// <param name="progress"></param>
|
|
/// <param name="ct"></param>
|
|
/// <returns></returns>
|
|
[Obsolete]
|
|
public async Task<(bool, string)> SendFileAsync(string filePath, IProgress<double> progress, CancellationToken ct)
|
|
{
|
|
_currentProgress?.Report(0.0);
|
|
if (!IsConnected)
|
|
{
|
|
return (false, "设备未找到");
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!File.Exists(filePath))
|
|
{
|
|
OnLog("文件不存在", true);
|
|
return (false, $"烧录的文件不存在");
|
|
}
|
|
|
|
_filelocalData = File.ReadAllBytes(filePath);
|
|
K3ToolUSBProtocol.SetFileData(_filelocalData);
|
|
var totalPackets = (int)Math.Ceiling(_filelocalData.Length / 1024.0);
|
|
State.TotalPackets = totalPackets;
|
|
State.IsTransferring = true;
|
|
// 初始化传输上下文
|
|
_currentProgress = progress;
|
|
_currentCancellationToken = ct;
|
|
_transferTcs = new TaskCompletionSource<bool>();
|
|
|
|
// 注册取消监听
|
|
ct.Register(() =>
|
|
{
|
|
if (!_transferTcs.Task.IsCompleted)
|
|
{
|
|
OnLog("文件传输已取消", true);
|
|
_transferTcs.TrySetCanceled();
|
|
CleanupTransfer(_currentBigPhase);
|
|
}
|
|
});
|
|
|
|
// 报告初始进度(比如 5%)
|
|
progress?.Report(5.0);
|
|
// 发送头帧
|
|
var fileName = Path.GetFileName(filePath);
|
|
var header = K3ToolUSBProtocol.GetSendHeader(fileName);
|
|
|
|
await SendAsync(header);
|
|
|
|
State.CurrentPhase = TransferPhase.Phase2_WaitingForHeaderAck;
|
|
State.StatusMessage = "等待头帧确认";
|
|
// 等待传输完成(通过 TCS)
|
|
bool success = await _transferTcs.Task;
|
|
|
|
if (success)
|
|
{
|
|
return (true, "✅ 文件传输完成!");
|
|
}
|
|
else
|
|
{
|
|
// 可以从 State.StatusMessage 获取更具体的错误信息
|
|
var msg = State.StatusMessage ?? "传输失败";
|
|
if (IsConnected == false)
|
|
{
|
|
return (false, $"❌【设备断开连接,请勿在烧录中拔插设备】");
|
|
}
|
|
else
|
|
{
|
|
return (false, $"❌【烧录失败】 {msg}");
|
|
}
|
|
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog($"发送文件失败: {ex.Message}", true);
|
|
CleanupTransfer(_currentBigPhase);
|
|
return (false, $"发送文件失败:{ex.Message}");
|
|
}
|
|
}
|
|
|
|
public async Task<(bool, string)> SendFileAsync(string fileName, byte[] filebin, IProgress<double> progress, CancellationToken ct)
|
|
{
|
|
if (!IsConnected)
|
|
{
|
|
return (false, "设备未找到");
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!IsValidHasFile(filebin))
|
|
{
|
|
OnLog("文件不存在", true);
|
|
return (false, $"烧录的文件不存在");
|
|
}
|
|
K3ToolUSBProtocol.SetFileData(filebin);
|
|
var totalPackets = (int)Math.Ceiling(filebin.Length / 1024.0);
|
|
State.TotalPackets = totalPackets;
|
|
State.IsTransferring = true;
|
|
// 初始化传输上下文
|
|
_currentProgress = progress;
|
|
_currentCancellationToken = ct;
|
|
_transferTcs = new TaskCompletionSource<bool>();
|
|
|
|
// 注册取消监听
|
|
ct.Register(() =>
|
|
{
|
|
if (!_transferTcs.Task.IsCompleted)
|
|
{
|
|
OnLog("文件传输已取消", true);
|
|
CleanupTransfer(_currentBigPhase);
|
|
}
|
|
});
|
|
|
|
// 报告初始进度(比如 5%)
|
|
progress?.Report(5.0);
|
|
// 发送头帧
|
|
var header = K3ToolUSBProtocol.GetSendHeader(fileName);
|
|
|
|
await SendAsync(header);
|
|
|
|
State.CurrentPhase = TransferPhase.Phase2_WaitingForHeaderAck;
|
|
State.StatusMessage = "等待头帧确认";
|
|
// 等待传输完成(通过 TCS)
|
|
bool success = await _transferTcs.Task;
|
|
|
|
if (success)
|
|
{
|
|
return (true, "✅ 烧录成功!");
|
|
}
|
|
else
|
|
{
|
|
// 可以从 State.StatusMessage 获取更具体的错误信息
|
|
var msg = State.StatusMessage ?? "传输失败";
|
|
if (IsConnected == false)
|
|
{
|
|
return (false, $"❌【设备断开连接,请勿在烧录中拔插设备】");
|
|
}
|
|
else
|
|
{
|
|
return (false, $"❌【烧录失败】 {msg}");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog($"发送文件失败: {ex.Message}", true);
|
|
CleanupTransfer(_currentBigPhase);
|
|
return (false, $"发送文件失败:{ex.Message}");
|
|
}
|
|
}
|
|
|
|
#region 阶段性烧录(连接/生成/写入)
|
|
/// <summary>
|
|
/// 固定单命令(如连接/生成 等命令)
|
|
/// </summary>
|
|
/// <param name="fileName"></param>
|
|
/// <param name="filebin"></param>
|
|
/// <param name="progress"></param>
|
|
/// <param name="ct"></param>
|
|
/// <returns></returns>
|
|
private async Task<(bool, string)> SendSingleCommandAsync(string fileName, byte[] filebin, IProgress<double> progress, CancellationToken ct)
|
|
{
|
|
if (!IsConnected)
|
|
{
|
|
return (false, "设备未找到");
|
|
}
|
|
try
|
|
{
|
|
// ========== 关键:为当前大阶段创建独立TCS ==========
|
|
TaskCompletionSource<bool> currentPhaseTcs;
|
|
lock (_phaseTcsLock)
|
|
{
|
|
// 确保每个阶段只有一个TCS
|
|
if (!_phaseTcsDict.TryGetValue(_currentBigPhase, out currentPhaseTcs) || currentPhaseTcs.Task.IsCompleted)
|
|
{
|
|
currentPhaseTcs = new TaskCompletionSource<bool>();
|
|
_phaseTcsDict[_currentBigPhase] = currentPhaseTcs;
|
|
}
|
|
}
|
|
if (!IsValidHasFile(filebin))
|
|
{
|
|
OnLog("文件不存在", true);
|
|
return (false, $"烧录的文件不存在");
|
|
}
|
|
State.IsTransferring = true;
|
|
State.CurrentPhase = TransferPhase.Phase11_WaitingForComplete;
|
|
await SendAsync(filebin);
|
|
// 等待传输完成(通过 TCS)
|
|
bool success = await currentPhaseTcs.Task;
|
|
|
|
if (success)
|
|
{
|
|
GetCurrentPhaseStartProgress();
|
|
_currentProgress?.Report(_currentBigPhaseProgress);
|
|
return (true, $"基础命令:{_currentBigPhase}完成");
|
|
}
|
|
else
|
|
{
|
|
// 可以从 State.StatusMessage 获取更具体的错误信息
|
|
var msg = State.StatusMessage ?? "传输失败";
|
|
if (IsConnected == false)
|
|
{
|
|
return (false, $"❌【设备断开连接,请勿在烧录中拔插设备】");
|
|
}
|
|
else
|
|
{
|
|
return (false, $"❌【烧录失败】 {msg}");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog($"发送文件失败: {ex.Message}", true);
|
|
lock (_phaseTcsLock)
|
|
{
|
|
if (_phaseTcsDict.TryGetValue(_currentBigPhase, out var tcs))
|
|
{
|
|
tcs.TrySetCanceled();
|
|
}
|
|
}
|
|
CleanupTransfer(_currentBigPhase);
|
|
return (false, $"发送文件失败:{ex.Message}");
|
|
}
|
|
}
|
|
private async Task<(bool, string)> SendSingleFileAsync(string fileName, byte[] filebin, IProgress<double> progress, CancellationToken ct)
|
|
{
|
|
if (!IsConnected)
|
|
{
|
|
return (false, "设备未找到");
|
|
}
|
|
// ========== 关键:为当前大阶段创建独立TCS ==========
|
|
TaskCompletionSource<bool> currentPhaseTcs;
|
|
lock (_phaseTcsLock)
|
|
{
|
|
if (!_phaseTcsDict.TryGetValue(_currentBigPhase, out currentPhaseTcs) || currentPhaseTcs.Task.IsCompleted)
|
|
{
|
|
currentPhaseTcs = new TaskCompletionSource<bool>();
|
|
_phaseTcsDict[_currentBigPhase] = currentPhaseTcs;
|
|
}
|
|
}
|
|
try
|
|
{
|
|
if (!IsValidHasFile(filebin))
|
|
{
|
|
OnLog("文件不存在", true);
|
|
return (false, $"烧录的文件不存在");
|
|
}
|
|
K3ToolUSBProtocol.SetFileData(filebin);
|
|
var totalPackets = (int)Math.Ceiling(filebin.Length / 1024.0);
|
|
State.TotalPackets = totalPackets;
|
|
State.IsTransferring = true;
|
|
|
|
// 发送头帧
|
|
var header = K3ToolUSBProtocol.GetSendHeader(fileName);
|
|
|
|
await SendAsync(header);
|
|
GetCurrentPhaseStartProgress();
|
|
State.CurrentPhase = TransferPhase.Phase2_WaitingForHeaderAck;
|
|
State.StatusMessage = "等待头帧确认";
|
|
// 等待传输完成(通过 TCS)
|
|
bool success = await currentPhaseTcs.Task;
|
|
|
|
if (success)
|
|
{
|
|
double scopedProgressVal = _currentBigPhaseProgress + 5.0;
|
|
_currentProgress?.Report(scopedProgressVal);
|
|
return (true, "✅ 烧录成功!");
|
|
}
|
|
else
|
|
{
|
|
// 可以从 State.StatusMessage 获取更具体的错误信息
|
|
var msg = State.StatusMessage ?? "传输失败";
|
|
if (IsConnected == false)
|
|
{
|
|
return (false, $"❌【设备断开连接,请勿在烧录中拔插设备】");
|
|
}
|
|
else
|
|
{
|
|
return (false, $"❌【烧录失败】 {msg}");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog($"发送文件失败: {ex.Message}", true);
|
|
CleanupTransfer(_currentBigPhase);
|
|
return (false, $"发送文件失败:{ex.Message}");
|
|
}
|
|
}
|
|
public async Task<(bool, string)> SendFileProcessAsync(string fileName, byte[] filebin, IProgress<double> progress, CancellationToken ct)
|
|
{
|
|
_currentProgress?.Report(0.0);
|
|
if (!IsConnected)
|
|
{
|
|
return (false, "设备未找到");
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!IsValidHasFile(filebin))
|
|
{
|
|
OnLog("文件不存在", true);
|
|
return (false, $"烧录的文件不存在");
|
|
}
|
|
//========== 关键:初始化全局大阶段任务完成源 ==========
|
|
_allBigPhasesTcs = new TaskCompletionSource<bool>();
|
|
_allBigPhasesCompleted = false;
|
|
// 初始化大阶段任务队列
|
|
lock (_bigPhaseLock)
|
|
{
|
|
_bigPhaseTasks.Clear();
|
|
_bigPhaseTasks.Add(new BigPhaseTask
|
|
{
|
|
Phase = BigTransferPhase.Phase1_Connect,
|
|
IsCompleted = false
|
|
});
|
|
_bigPhaseTasks.Add(new BigPhaseTask
|
|
{
|
|
Phase = BigTransferPhase.Phase2_Generate,
|
|
IsCompleted = false
|
|
});
|
|
_bigPhaseTasks.Add(new BigPhaseTask
|
|
{
|
|
Phase = BigTransferPhase.Phase3_Write,
|
|
IsCompleted = false
|
|
});
|
|
}
|
|
_fileBinData = filebin;
|
|
_fileBinName = fileName;
|
|
// 初始化传输上下文
|
|
_currentProgress = progress;
|
|
_currentCancellationToken = ct;
|
|
|
|
// ========== 关键:取消监听改为触发全局TCS ==========
|
|
ct.Register(() =>
|
|
{
|
|
if (_allBigPhasesTcs != null && !_allBigPhasesTcs.Task.IsCompleted)
|
|
{
|
|
OnLog("多阶段传输已取消", true);
|
|
_allBigPhasesTcs.TrySetCanceled();
|
|
CleanupTransfer();
|
|
}
|
|
});
|
|
|
|
// 启动第一个大阶段
|
|
await SwitchToNextBigPhaseAsync();
|
|
|
|
// 等待所有大阶段完成
|
|
// ========== 关键:等待所有大阶段完成(直到HandleFinalComplete触发所有阶段完成) ==========
|
|
bool allSuccess = false;
|
|
try
|
|
{
|
|
// 等待全局TCS完成(只有所有大阶段执行HandleFinalComplete并标记完成后才会触发)
|
|
allSuccess = await _allBigPhasesTcs.Task;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
OnLog("多阶段传输被取消", true);
|
|
return (false, "传输已取消");
|
|
}
|
|
|
|
// 收集结果
|
|
string errorMsg = string.Empty;
|
|
lock (_bigPhaseLock)
|
|
{
|
|
errorMsg = string.Join("; ", _bigPhaseTasks.Where(t => !string.IsNullOrEmpty(t.ErrorMessage)).Select(t => $"{t.Phase}: {t.ErrorMessage}"));
|
|
}
|
|
|
|
if (allSuccess)
|
|
{
|
|
_currentProgress.Report(100.00);
|
|
return (true, GetLanguageValueOrDefault("BurnSuccess"));
|
|
}
|
|
else
|
|
{
|
|
return (false, string.IsNullOrEmpty(errorMsg) ? GetLanguageValueOrDefault("BurnError") : errorMsg);
|
|
}
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog($"发送文件失败: {ex.Message}", true);
|
|
CleanupTransfer();
|
|
return (false, $"发送文件失败:{ex.Message}");
|
|
}
|
|
}
|
|
private async Task SwitchToNextBigPhaseAsync()
|
|
{
|
|
BigPhaseTask? nextTask = null;
|
|
// 先在锁内获取下一个任务(避免锁内执行异步逻辑)
|
|
lock (_bigPhaseLock)
|
|
{
|
|
nextTask = _bigPhaseTasks.FirstOrDefault(t => !t.IsCompleted);
|
|
if (nextTask == null)
|
|
{
|
|
OnAllBigPhasesCompleted();
|
|
return;
|
|
}
|
|
// 切换大阶段状态(提前切换,避免重复执行)
|
|
OnBigPhaseChanged(nextTask.Phase);
|
|
}
|
|
|
|
// 锁外执行异步逻辑(避免阻塞其他线程)
|
|
try
|
|
{
|
|
bool phaseSuccess = false;
|
|
string phaseError = string.Empty;
|
|
|
|
switch (nextTask.Phase)
|
|
{
|
|
case BigTransferPhase.Phase1_Connect:
|
|
(phaseSuccess, phaseError) = await SendSingleCommandAsync(
|
|
K3ToolUSBProtocol.ConnectFileName,
|
|
K3ToolUSBProtocol.ConnectData,
|
|
_currentProgress!,
|
|
_currentCancellationToken);
|
|
if (phaseSuccess)
|
|
{
|
|
// 延迟,避免设备未就绪(可选,根据设备特性调整)
|
|
await Task.Delay(1000);
|
|
}
|
|
else
|
|
{
|
|
|
|
}
|
|
break;
|
|
case BigTransferPhase.Phase2_Generate:
|
|
(phaseSuccess, phaseError) = await SendSingleCommandAsync(
|
|
K3ToolUSBProtocol.CreateRemoteFileName,
|
|
K3ToolUSBProtocol.CreateRemoteData,
|
|
_currentProgress!,
|
|
_currentCancellationToken);
|
|
if (phaseSuccess)
|
|
{
|
|
// 延迟,避免设备未就绪(可选,根据设备特性调整)
|
|
await Task.Delay(500);
|
|
}
|
|
break;
|
|
case BigTransferPhase.Phase3_Write:
|
|
(phaseSuccess, phaseError) = await SendSingleFileAsync(
|
|
_fileBinName,
|
|
_fileBinData!,
|
|
_currentProgress!,
|
|
_currentCancellationToken);
|
|
if (phaseSuccess)
|
|
{
|
|
// 延迟,避免设备未就绪(可选,根据设备特性调整)
|
|
//await Task.Delay(500);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// 再次加锁更新任务状态(关键:确保状态更新原子性)
|
|
lock (_bigPhaseLock)
|
|
{
|
|
nextTask.IsCompleted = true;
|
|
nextTask.ErrorMessage = phaseError;
|
|
}
|
|
|
|
if (!phaseSuccess)
|
|
{
|
|
OnLog($"❌ {nextTask.Phase} 执行失败: {phaseError}", true);
|
|
// ========== 触发当前阶段TCS失败 + 全局TCS失败 ==========
|
|
lock (_phaseTcsLock)
|
|
{
|
|
if (_phaseTcsDict.TryGetValue(nextTask.Phase, out var tcs))
|
|
{
|
|
tcs.TrySetResult(false);
|
|
}
|
|
}
|
|
_allBigPhasesTcs?.TrySetResult(false);
|
|
return;
|
|
}
|
|
|
|
OnLog($"✅ {nextTask.Phase} 执行完成,准备进入下一阶段");
|
|
|
|
// 递归调用,进入下一个大阶段
|
|
await SwitchToNextBigPhaseAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
lock (_bigPhaseLock)
|
|
{
|
|
nextTask.IsCompleted = true;
|
|
nextTask.ErrorMessage = ex.Message;
|
|
}
|
|
OnLog($"❌ {nextTask.Phase} 执行异常: {ex.Message}", true);
|
|
_allBigPhasesTcs?.TrySetResult(false);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region 辅助方法
|
|
public void Dispose()
|
|
{
|
|
_isRunning = false;
|
|
_cts?.Cancel();
|
|
#region (CPU占用高/备用方法适应老版本库)
|
|
//if (_receiveThread != null && _receiveThread.IsAlive)
|
|
//{
|
|
// _receiveThread.Join(DEVICE_MONITOR_INTERVAL_MS);
|
|
//}
|
|
#endregion
|
|
_inputReceiver = null;
|
|
lock (_streamLock)
|
|
{
|
|
_stream?.Dispose();
|
|
_stream = null;
|
|
}
|
|
CleanupTransfer();
|
|
_cts?.Dispose();
|
|
}
|
|
private void CleanupTransfer(BigTransferPhase? phase = null)
|
|
{
|
|
State.IsTransferring = false;
|
|
|
|
// 清理指定阶段的TCS
|
|
if (phase.HasValue)
|
|
{
|
|
lock (_phaseTcsLock)
|
|
{
|
|
if (_phaseTcsDict.ContainsKey(phase.Value))
|
|
{
|
|
_phaseTcsDict[phase.Value].TrySetCanceled();
|
|
_phaseTcsDict.Remove(phase.Value);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 清理所有阶段的TCS
|
|
lock (_phaseTcsLock)
|
|
{
|
|
foreach (var tcs in _phaseTcsDict.Values)
|
|
{
|
|
tcs.TrySetCanceled();
|
|
}
|
|
_phaseTcsDict.Clear();
|
|
}
|
|
// 清理全局TCS
|
|
_allBigPhasesTcs?.TrySetCanceled();
|
|
_allBigPhasesTcs = null;
|
|
_allBigPhasesCompleted = false;
|
|
}
|
|
|
|
_currentProgress = null;
|
|
_fileBinData = null;
|
|
_fileBinName = null;
|
|
}
|
|
private bool IsValidHasFile(byte[] filebin)
|
|
{
|
|
if (filebin == null || filebin.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#region 阶段进度条数据
|
|
private double GetCurrentPhaseStartProgress()
|
|
{
|
|
switch (_currentBigPhase)
|
|
{
|
|
case BigTransferPhase.Phase1_Connect:
|
|
//连接指令较快
|
|
_currentBigPhaseProgress = 10;
|
|
break;
|
|
case BigTransferPhase.Phase2_Generate:
|
|
//指令较快
|
|
_currentBigPhaseProgress += 10;
|
|
break;
|
|
case BigTransferPhase.Phase3_Write:
|
|
_currentBigPhaseProgress += 10;
|
|
break;
|
|
default:
|
|
return 0.0;
|
|
}
|
|
return _currentBigPhaseProgress;
|
|
}
|
|
#endregion
|
|
#endregion
|
|
|
|
#endregion
|
|
|
|
#region 设备固件升级
|
|
public async Task<(bool, string)> SendFileProcessAsync(string fileName, byte[] filebin, HidOperationTypeEnum hidOperationType, IProgress<double> progress, CancellationToken ct)
|
|
{
|
|
_hidOperationType = hidOperationType;
|
|
_currentProgress?.Report(0.0);
|
|
if (!IsConnected)
|
|
{
|
|
return (false, "设备未找到");
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!IsValidHasFile(filebin))
|
|
{
|
|
OnLog("文件不存在", true);
|
|
return (false, $"烧录的文件不存在");
|
|
}
|
|
//========== 关键:初始化全局大阶段任务完成源 ==========
|
|
_allBigPhasesTcs = new TaskCompletionSource<bool>();
|
|
_allBigPhasesCompleted = false;
|
|
// 初始化大阶段任务队列
|
|
lock (_bigPhaseLock)
|
|
{
|
|
_bigPhaseTasks.Clear();
|
|
_bigPhaseTasks.Add(new BigPhaseTask
|
|
{
|
|
Phase = BigTransferPhase.Phase1_Connect,
|
|
IsCompleted = false
|
|
});
|
|
_bigPhaseTasks.Add(new BigPhaseTask
|
|
{
|
|
Phase = BigTransferPhase.Phase2_Generate,
|
|
IsCompleted = false
|
|
});
|
|
_bigPhaseTasks.Add(new BigPhaseTask
|
|
{
|
|
Phase = BigTransferPhase.Phase3_Write,
|
|
IsCompleted = false
|
|
});
|
|
}
|
|
_fileBinData = filebin;
|
|
_fileBinName = fileName;
|
|
// 初始化传输上下文
|
|
_currentProgress = progress;
|
|
_currentCancellationToken = ct;
|
|
|
|
// ========== 关键:取消监听改为触发全局TCS ==========
|
|
ct.Register(() =>
|
|
{
|
|
if (_allBigPhasesTcs != null && !_allBigPhasesTcs.Task.IsCompleted)
|
|
{
|
|
OnLog("多阶段传输已取消", true);
|
|
_allBigPhasesTcs.TrySetCanceled();
|
|
CleanupTransfer();
|
|
}
|
|
});
|
|
|
|
// 启动第一个大阶段
|
|
await SwitchToNextBigPhaseAsync();
|
|
|
|
// 等待所有大阶段完成
|
|
// ========== 关键:等待所有大阶段完成(直到HandleFinalComplete触发所有阶段完成) ==========
|
|
bool allSuccess = false;
|
|
try
|
|
{
|
|
// 等待全局TCS完成(只有所有大阶段执行HandleFinalComplete并标记完成后才会触发)
|
|
allSuccess = await _allBigPhasesTcs.Task;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
OnLog("多阶段传输被取消", true);
|
|
return (false, "传输已取消");
|
|
}
|
|
|
|
// 收集结果
|
|
string errorMsg = string.Empty;
|
|
lock (_bigPhaseLock)
|
|
{
|
|
errorMsg = string.Join("; ", _bigPhaseTasks.Where(t => !string.IsNullOrEmpty(t.ErrorMessage)).Select(t => $"{t.Phase}: {t.ErrorMessage}"));
|
|
}
|
|
|
|
if (allSuccess)
|
|
{
|
|
_currentProgress.Report(100.00);
|
|
return (true, GetLanguageValueOrDefault("BurnSuccess"));
|
|
}
|
|
else
|
|
{
|
|
return (false, string.IsNullOrEmpty(errorMsg) ? GetLanguageValueOrDefault("BurnError") : errorMsg);
|
|
}
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnLog($"发送文件失败: {ex.Message}", true);
|
|
CleanupTransfer();
|
|
return (false, $"发送文件失败:{ex.Message}");
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region 获取设备版本
|
|
public async Task<(bool, string)> GetDeviceVersion(HidOperationTypeEnum hidOperationType)
|
|
{
|
|
if (!IsConnected)
|
|
{
|
|
return (false, "设备未找到");
|
|
}
|
|
await Task.Delay(5000);
|
|
await SendAsync(K3ToolUSBProtocol.VersionQuery());
|
|
return (true, "获取设备版本指令已发送");
|
|
}
|
|
#endregion
|
|
|
|
#region 获取K3ToolUSB设备对应的U盘盘符
|
|
public async Task<string> FindAssociatedUsbDisk(int vid, int pid)
|
|
{
|
|
string model = "K3 TOOL";
|
|
try
|
|
{
|
|
// 步骤1:获取所有可移动磁盘盘符(确保设备已挂载)
|
|
var removableDrives = DriveInfo.GetDrives()
|
|
.Where(d => d.DriveType == DriveType.Removable && d.IsReady)
|
|
.ToList();
|
|
|
|
if (!removableDrives.Any())
|
|
return null;
|
|
|
|
// 步骤2:查询所有 USB 磁盘的 Model 和 Index
|
|
using var searcher = new ManagementObjectSearcher(
|
|
"SELECT Index, Model FROM Win32_DiskDrive WHERE InterfaceType='USB'");
|
|
|
|
var usbDisks = new List<(uint Index, string Model)>();
|
|
foreach (ManagementObject disk in searcher.Get())
|
|
{
|
|
if (disk["Model"] is string diskModel &&
|
|
diskModel.IndexOf(model, StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
uint index = (uint)(disk["Index"] ?? 0);
|
|
usbDisks.Add((index, diskModel));
|
|
}
|
|
}
|
|
|
|
// 步骤3:根据匹配数量决定策略
|
|
if (usbDisks.Count == 0)
|
|
return null; // 无匹配设备
|
|
|
|
if (usbDisks.Count == 1 && removableDrives.Count == 1)
|
|
{
|
|
// 唯一匹配 → 返回唯一盘符
|
|
return removableDrives[0].RootDirectory.FullName;
|
|
}
|
|
|
|
// 多个匹配?返回第一个(或扩展为用户选择)
|
|
return removableDrives.FirstOrDefault()?.RootDirectory.FullName;
|
|
}
|
|
catch (ManagementException ex) when (ex.Message.Contains("Invalid class"))
|
|
{
|
|
// WMI 损坏,降级处理
|
|
// 如果只有一个可移动磁盘,且您确定它是目标设备,可直接返回
|
|
var drives = DriveInfo.GetDrives()
|
|
.Where(d => d.DriveType == DriveType.Removable && d.IsReady)
|
|
.ToList();
|
|
|
|
if (drives.Count == 1)
|
|
return drives[0].RootDirectory.FullName;
|
|
|
|
return null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"FindUsbDiskByModel error: {ex}");
|
|
return null;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region 复制文件到HID U盘
|
|
public async Task<(bool, string)> CopyToUdiskAsync(string sourcePath, bool isDirectory, IProgress<double> progress,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await OpenUDiskAndDetectDriveAsync();
|
|
var targetDrive = await FindAssociatedUsbDisk(_vendorId, _productId);
|
|
if (string.IsNullOrEmpty(targetDrive))
|
|
throw new InvalidOperationException("未检测到 HID U盘设备");
|
|
// 目标路径改为 {targetDrive}\received_data
|
|
string receivedDataDir = Path.Combine(targetDrive, usbdiskDir);
|
|
|
|
// 自动创建 received_data 目录(如果不存在)
|
|
Directory.CreateDirectory(receivedDataDir);
|
|
// 启动保活任务(每60秒发送一次 ConnectData)
|
|
using var keepAliveCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
var keepAliveTask = KeepDeviceAliveAsync(keepAliveCts.Token);
|
|
try
|
|
{
|
|
if (isDirectory)
|
|
{
|
|
//文件夹复制
|
|
string sourceFolderName = Path.GetFileName(sourcePath);
|
|
|
|
string destDir;
|
|
if (string.Equals(sourceFolderName, usbdiskDir, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// 源文件夹名 == usbdiskDir(如 "received_data")
|
|
// → 直接复制其内部内容到 receivedDataDir,不嵌套
|
|
destDir = receivedDataDir;
|
|
}
|
|
else
|
|
{
|
|
// 正常:在 received_data 下创建子文件夹
|
|
destDir = Path.Combine(receivedDataDir, sourceFolderName);
|
|
}
|
|
int skipped = await CopyDirectoryWithProgressAsync(sourcePath, destDir, progress, cancellationToken);
|
|
progress?.Report(100.0);
|
|
return skipped > 0
|
|
? (true, $"数据同步完成({skipped} 个文件因损坏被跳过,建议对U盘执行 chkdsk 修复)")
|
|
: (true, "数据同步完成");
|
|
}
|
|
else
|
|
{
|
|
//单文件复制
|
|
string destFile = Path.Combine(receivedDataDir, Path.GetFileName(sourcePath));
|
|
long totalSize = new FileInfo(sourcePath).Length;
|
|
var context = new CopyProgressContext(totalSize, progress);
|
|
await CopyFileWithProgressAsync(sourcePath, destFile, context, cancellationToken);
|
|
|
|
progress?.Report(100.0);
|
|
return (true, "数据同步完成");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var failureCategory = UsbCopyGuard.ClassifyCopyException(ex);
|
|
var failureLabel = UsbCopyGuard.GetFailureLabel(failureCategory);
|
|
_logger.LogWarning(ex,
|
|
"复制到K3Tool设备失败。类别:{FailureCategory},目标盘符:{TargetDrive},源路径:{SourcePath},连接状态: IsConnected={IsConnected}, IsDataConnected={IsDataConnected}",
|
|
failureLabel,
|
|
targetDrive,
|
|
sourcePath,
|
|
State.IsConnected,
|
|
State.IsDataConnected);
|
|
return (false, $"错误信息:[{failureLabel}] {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
// 复制完成后,停止保活
|
|
keepAliveCts.Cancel();
|
|
//try
|
|
//{
|
|
// // 无论成功与否,都先尝试弹出卷;即使失败也继续发送关闭U盘指令。
|
|
// await WindowsVolumeEjector.EjectVolumeAsync(targetDrive);
|
|
//}
|
|
//catch (OperationCanceledException) { /* 忽略 */ }
|
|
//catch (Exception ex)
|
|
//{
|
|
// _logger.LogWarning(ex, "弹出K3Tool卷失败,继续执行关闭U盘指令。目标盘符:{TargetDrive}", targetDrive);
|
|
//}
|
|
|
|
try
|
|
{
|
|
await CloseUDisk();
|
|
}
|
|
catch (OperationCanceledException) { /* 忽略 */ }
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
await keepAliveTask; // 等待保活任务结束
|
|
}
|
|
catch (OperationCanceledException) { /* 忽略 */ }
|
|
}
|
|
|
|
// 同步完成后执行 devcon remove+rescan(应急方案):把设备从 U 盘模式"软件拔插"恢复为 HID 模式,
|
|
// 清理 Win7 残留的设备状态,避免下次切换再次 Code 10。失败静默降级,不影响返回结果。
|
|
try
|
|
{
|
|
bool restoreOk = await UsbDevconHelper.RemoveAndRescanAsync(_vendorId, _productId);
|
|
OnLog(restoreOk
|
|
? "✅ 同步完成,devcon remove+rescan 已执行,设备已恢复为 HID 模式"
|
|
: "⚠️ 同步完成但 devcon remove+rescan 未执行(需要管理员权限,且 devcon.exe 需随程序分发到运行目录)");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogWarning(ex, "同步完成后 devcon remove+rescan 执行失败");
|
|
}
|
|
|
|
}
|
|
}
|
|
#region 普通复制方法
|
|
//private async Task CopyFileWithCancellation(string src, string dest, CancellationToken ct)
|
|
//{
|
|
// const int bufferSize = 64 * 1024; // 64KB
|
|
// var buffer = new byte[bufferSize];
|
|
|
|
// Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
|
|
|
|
// using var sourceStream = File.OpenRead(src);
|
|
// using var destStream = File.Create(dest);
|
|
|
|
// int bytesRead;
|
|
// while ((bytesRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length, ct)) > 0)
|
|
// {
|
|
// await destStream.WriteAsync(buffer, 0, bytesRead, ct);
|
|
// // 可选:定期检查取消(ReadAsync 已支持)
|
|
// }
|
|
//}
|
|
//private async Task CopyDirectoryRecursive(string src, string dest, CancellationToken ct)
|
|
//{
|
|
// Directory.CreateDirectory(dest);
|
|
|
|
// foreach (var file in Directory.GetFiles(src))
|
|
// {
|
|
// ct.ThrowIfCancellationRequested(); // 关键:检查取消
|
|
// var destFile = Path.Combine(dest, Path.GetFileName(file));
|
|
// await CopyFileWithCancellation(file, destFile, ct);
|
|
// }
|
|
|
|
// foreach (var dir in Directory.GetDirectories(src))
|
|
// {
|
|
// ct.ThrowIfCancellationRequested();
|
|
// await CopyDirectoryRecursive(dir, Path.Combine(dest, Path.GetFileName(dir)), ct);
|
|
// }
|
|
//}
|
|
#endregion
|
|
#region 带进度的复制方法
|
|
private async Task CopyFileWithProgressAsync(
|
|
string src,
|
|
string dest,
|
|
CopyProgressContext context,
|
|
CancellationToken ct)
|
|
{
|
|
var throttle = UsbCopyGuard.GetCopyThrottleSettings();
|
|
int bufferSize = throttle.BufferSize;
|
|
try
|
|
{
|
|
UsbCopyGuard.ProbeCopyTargetHealth(dest);
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
_logger?.LogError("目录损坏或无法访问:{Dest}, {Message}", dest, ex.Message);
|
|
throw;
|
|
}
|
|
if (File.Exists(dest))
|
|
{
|
|
FileInfo fi = new FileInfo(dest);
|
|
|
|
// 长度读取失败(损坏条目)按损坏文件处理,走下面的清理逻辑
|
|
long existingLength;
|
|
try
|
|
{
|
|
existingLength = fi.Length;
|
|
}
|
|
catch
|
|
{
|
|
existingLength = 0;
|
|
}
|
|
|
|
// 正常文件 → 直接跳过
|
|
if (existingLength > 0)
|
|
{
|
|
context.ReportCopied(existingLength);
|
|
//_logger?.LogInformation("文件已存在,跳过:{Dest}", dest);
|
|
return;
|
|
}
|
|
|
|
// =============== 0KB/损坏文件:删除 → 重命名 → 强制覆盖 ===============
|
|
if (UsbCopyGuard.TryClearCorruptedTarget(_logger, dest, out var clearDetail))
|
|
{
|
|
// 给设备一点点刷新时间(非常短,不影响速度)
|
|
await Task.Delay(20, ct);
|
|
}
|
|
else
|
|
{
|
|
_logger?.LogError("损坏文件清理失败,跳过:{Dest}, 错误:{Detail}", dest, clearDetail);
|
|
return;
|
|
}
|
|
}
|
|
|
|
byte[] buffer = new byte[bufferSize];
|
|
Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
|
|
|
|
using var sourceStream = new FileStream(src, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, true);
|
|
using var destStream = new FileStream(dest, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, true);
|
|
|
|
int bytesRead;
|
|
int chunkIndex = 0;
|
|
|
|
try
|
|
{
|
|
while ((bytesRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length, ct)) > 0)
|
|
{
|
|
chunkIndex++;
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
UsbCopyGuard.ThrowIfCopyTargetUnavailable(_logger, State, Path.GetPathRoot(dest), dest, chunkIndex, context.CopiedBytes);
|
|
await UsbCopyGuard.WriteChunkWithTimeoutAsync(destStream, buffer, bytesRead, ct, dest, chunkIndex, context.CopiedBytes);
|
|
|
|
context.ReportCopied(bytesRead);
|
|
|
|
// ====================== 加速关键:5ms 超短延迟 ======================
|
|
await Task.Delay(throttle.ChunkDelayMs, ct);
|
|
}
|
|
|
|
// 整个文件写完只刷 1 次
|
|
await destStream.FlushAsync(CancellationToken.None);
|
|
}
|
|
finally
|
|
{
|
|
// 文件间隔 50ms 防掉盘
|
|
//await Task.Delay(throttle.FileDelayMs, ct);
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// 检测目录健康状态,确保目标路径可访问且没有损坏。
|
|
/// </summary>
|
|
/// <param name="destinationPath"></param>
|
|
/// <exception cref="IOException"></exception>
|
|
public static void ProbeCopyTargetHealth(string destinationPath)
|
|
{
|
|
string? dir = Path.GetDirectoryName(destinationPath);
|
|
if (string.IsNullOrEmpty(dir)) return;
|
|
|
|
try
|
|
{
|
|
// 仅检查目录是否存在,最轻量操作
|
|
bool exists = Directory.Exists(dir);
|
|
if (!exists) Directory.CreateDirectory(dir);
|
|
}
|
|
catch
|
|
{
|
|
// 出错代表目录损坏
|
|
throw new IOException("目标目录已损坏,请修复U盘后重试");
|
|
}
|
|
}
|
|
private async Task<int> CopyDirectoryWithProgressAsync(
|
|
string src,
|
|
string dest,
|
|
IProgress<double> progress,
|
|
CancellationToken ct)
|
|
{
|
|
long totalBytes = CalculateTotalSize(src);
|
|
var context = new CopyProgressContext(totalBytes, progress);
|
|
|
|
int skipped = await CopyDirectoryRecursiveInternal(src, dest, context, ct);
|
|
|
|
// 确保最终进度为 100%
|
|
progress?.Report(100.0);
|
|
return skipped;
|
|
}
|
|
|
|
private async Task<int> CopyDirectoryRecursiveInternal(
|
|
string src,
|
|
string dest,
|
|
CopyProgressContext context,
|
|
CancellationToken ct)
|
|
{
|
|
int skipped = 0;
|
|
|
|
// 目标目录本身损坏/不可创建 → 跳过整个子目录,不影响其他文件
|
|
try
|
|
{
|
|
Directory.CreateDirectory(dest);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogError("目标目录损坏或无法创建,跳过该目录:{Dest}, 错误:{Msg}", dest, ex.Message);
|
|
return 1;
|
|
}
|
|
|
|
string[] files;
|
|
try
|
|
{
|
|
files = Directory.GetFiles(src);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogError("源目录枚举失败,跳过该目录:{Src}, 错误:{Msg}", src, ex.Message);
|
|
return 1;
|
|
}
|
|
|
|
foreach (var file in files)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
var destFile = Path.Combine(dest, Path.GetFileName(file));
|
|
try
|
|
{
|
|
await CopyFileWithProgressAsync(file, destFile, context, ct);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var category = UsbCopyGuard.ClassifyCopyException(ex);
|
|
// 设备级问题(断联/写入超时):重试无意义,中断整个同步
|
|
if (category == UsbCopyGuard.CopyFailureCategory.DeviceDisconnected ||
|
|
category == UsbCopyGuard.CopyFailureCategory.WriteTimeout)
|
|
{
|
|
throw;
|
|
}
|
|
_logger?.LogWarning("文件复制失败,跳过继续:{Src} → {Dest}, 类别:{Label}, 错误:{Msg}",
|
|
file, destFile, UsbCopyGuard.GetFailureLabel(category), ex.Message);
|
|
skipped++;
|
|
}
|
|
}
|
|
|
|
string[] dirs;
|
|
try
|
|
{
|
|
dirs = Directory.GetDirectories(src);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogError("源子目录枚举失败,跳过该目录:{Src}, 错误:{Msg}", src, ex.Message);
|
|
return skipped + 1;
|
|
}
|
|
|
|
foreach (var dir in dirs)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
var destSubDir = Path.Combine(dest, Path.GetFileName(dir));
|
|
skipped += await CopyDirectoryRecursiveInternal(dir, destSubDir, context, ct);
|
|
}
|
|
|
|
return skipped;
|
|
}
|
|
|
|
#endregion
|
|
/// <summary>
|
|
/// 递归计算目录中所有文件的总字节数。
|
|
/// </summary>
|
|
private long CalculateTotalSize(string path)
|
|
{
|
|
long total = 0;
|
|
try
|
|
{
|
|
// 文件
|
|
if (File.Exists(path))
|
|
return new FileInfo(path).Length;
|
|
|
|
// 目录
|
|
foreach (var file in Directory.GetFiles(path, "*", SearchOption.AllDirectories))
|
|
{
|
|
total += new FileInfo(file).Length;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 可选:记录日志
|
|
System.Diagnostics.Debug.WriteLine($"计算大小失败: {ex.Message}");
|
|
}
|
|
return total;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 打开/关闭Hid MSC U盘
|
|
private async Task OpenUDisk()
|
|
{
|
|
await SendAsync(K3ToolUSBProtocol.OpentheUdiskData);
|
|
}
|
|
|
|
private async Task CloseUDisk()
|
|
{
|
|
await SendAsync(K3ToolUSBProtocol.ClosetheUdiskData);
|
|
_logger?.LogWarning($"{DateTime.Now}关闭U盘指令");
|
|
|
|
}
|
|
public async Task<string> OpenUDiskAndDetectDriveAsync(int maxWaitSeconds = 50, int pollIntervalMs = 500, CancellationToken cancellationToken = default)
|
|
{
|
|
// 1. 发送打开U盘指令
|
|
await OpenUDisk(); // 这个方法负责发 HID 指令
|
|
|
|
// 2. 设备固件收到指令后会重新枚举 USB(HID 模式 → U 盘模式)。
|
|
// Win7 的 USB 栈对热切换支持差(典型表现为 Code 10),模式切换与重新枚举需要时间,
|
|
// 先固定等待 2 秒让 USB 栈完成切换,再开始轮询,避免过早轮询全部落空。
|
|
try
|
|
{
|
|
await Task.Delay(2000, cancellationToken);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
throw;
|
|
}
|
|
|
|
// 3. 轮询检测盘符,直到出现或超时
|
|
var stopwatch = Stopwatch.StartNew();
|
|
var lastProgressLogUtc = DateTime.UtcNow;
|
|
bool removeRescanAttempted = false;
|
|
while (stopwatch.Elapsed.TotalSeconds < maxWaitSeconds)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
string drive = await FindAssociatedUsbDisk(_vendorId, _productId);
|
|
if (!string.IsNullOrEmpty(drive))
|
|
{
|
|
// 可选:再验证该盘是否可写(防“假挂载”)
|
|
try
|
|
{
|
|
var testFile = Path.Combine(drive, ".k3tool_test");
|
|
File.WriteAllText(testFile, "ok");
|
|
File.Delete(testFile);
|
|
return drive; // 成功
|
|
}
|
|
catch
|
|
{
|
|
// 盘符存在但不可写(可能还在初始化),继续等待
|
|
}
|
|
}
|
|
|
|
// 每 5 秒输出一次等待进度,便于用户判断设备是否仍在切换中
|
|
if (DateTime.UtcNow - lastProgressLogUtc >= TimeSpan.FromSeconds(5))
|
|
{
|
|
lastProgressLogUtc = DateTime.UtcNow;
|
|
OnLog($"⏳ 正在等待 U 盘出现(已等待 {stopwatch.Elapsed.TotalSeconds:F0}s / {maxWaitSeconds}s)..."
|
|
+ "若长时间无盘符,请尝试拔插设备或重启设备");
|
|
}
|
|
|
|
// devcon 兜底(Win7 Code 10 场景):10 秒仍无盘符时直接执行
|
|
// remove + rescan(删除设备实例后重新扫描,等价于软件拔插,最彻底)。
|
|
// devcon.exe 未分发或非管理员时 UsbDevconHelper 返回 false,静默降级不影响原流程。
|
|
if (!removeRescanAttempted && stopwatch.Elapsed.TotalSeconds >= 10)
|
|
{
|
|
removeRescanAttempted = true;
|
|
OnLog("🔄 尚未检测到 U 盘,直接执行 devcon remove + rescan(软件拔插)...");
|
|
bool removeOk = await UsbDevconHelper.RemoveAndRescanAsync(_vendorId, _productId);
|
|
OnLog(removeOk
|
|
? "✅ devcon remove+rescan 已完成,等待设备重新枚举..."
|
|
: "⚠️ devcon remove+rescan 未执行(需要管理员权限,且 devcon.exe 需随程序分发到运行目录)");
|
|
}
|
|
|
|
await Task.Delay(pollIntervalMs, cancellationToken);
|
|
}
|
|
|
|
throw new TimeoutException(
|
|
$"在 {maxWaitSeconds} 秒内未检测到 U 盘设备(VID={_vendorId}, PID={_productId})。"
|
|
+ "若设备管理器中该设备显示黄色感叹号(Code 10),请拔插设备或重启设备后重试。");
|
|
}
|
|
|
|
public async Task CloseUDiskSafeDriveAsync(string targetDrive="")
|
|
{
|
|
//无论成功与否,都尝试关闭U盘并发送关闭指令
|
|
//await WindowsVolumeEjector.EjectVolumeAsync(targetDrive);
|
|
|
|
// 按固定时序执行:发指令 → 关闭 HID 句柄 → 等待切换 → 重试连接。
|
|
// 注:发送"关闭U盘"指令依赖 HID 流,故先发指令、再断开句柄(与物理拔插同理)。
|
|
|
|
// Step 1: 发送"关闭U盘"指令给 HID
|
|
await CloseUDisk();
|
|
|
|
// Step 2: 关闭所有 HID 句柄(释放设备流,让 Windows 能干净地重新枚举)
|
|
try
|
|
{
|
|
await DisconnectAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogWarning(ex, "关闭U盘后断开 HID 失败");
|
|
}
|
|
|
|
// Step 3: 等待固件完成切换 + Win7 反应
|
|
await Task.Delay(600);
|
|
|
|
// Step 4: devcon remove+rescan 应急清理(Win7 残留幽灵节点,等价于软件拔插)
|
|
try
|
|
{
|
|
bool ok = await UsbDevconHelper.RemoveAndRescanAsync(_vendorId, _productId);
|
|
OnLog(ok
|
|
? "✅ devcon remove+rescan 已执行,等待设备重新枚举..."
|
|
: "⚠️ devcon remove+rescan 未执行(需要管理员权限,且 devcon.exe 需随程序分发到运行目录)");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogWarning(ex, "关闭U盘后 devcon remove+rescan 执行失败");
|
|
}
|
|
|
|
// Step 5: 重试打开 HID 设备(等待就绪 + 重连)
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
await Task.Delay(500);
|
|
try
|
|
{
|
|
bool deviceExists = DeviceList.Local.GetHidDevices(_vendorId, _productId).Any();
|
|
if (deviceExists && IsConnected)
|
|
{
|
|
OnLog("✅ 设备已恢复为 HID 模式并就绪");
|
|
return;
|
|
}
|
|
if (deviceExists && !IsConnected)
|
|
{
|
|
OnLog("🔌 设备已出现,尝试重新连接...");
|
|
await TryReconnectAsync();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogWarning(ex, "重试连接 HID 设备异常");
|
|
}
|
|
}
|
|
OnLog("⚠️ 等待设备就绪超时,将依赖自动重连机制");
|
|
}
|
|
#endregion
|
|
#region Hid设备保活
|
|
private async Task KeepDeviceAliveAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
//if (_lastHidResponseUtc != DateTime.MinValue)
|
|
//{
|
|
// var silenceDuration = DateTime.UtcNow - _lastHidResponseUtc;
|
|
// if (silenceDuration.TotalMilliseconds >= HID_RESPONSE_WARNING_MS)
|
|
// {
|
|
// var warning = $"⚠️ HID 已 {silenceDuration.TotalSeconds:F0} 秒未收到设备响应,设备可能忙碌或即将断联";
|
|
// _logger.LogWarning(warning);
|
|
// OnLog(warning, true);
|
|
// }
|
|
//}
|
|
|
|
// 发送连接心跳命令
|
|
await SendAsync(K3ToolUSBProtocol.ConnectData);
|
|
_consecutiveKeepAliveFailures = 0;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//_consecutiveKeepAliveFailures++;
|
|
//if (UsbCopyGuard.ShouldEmitRepeatedWarning(_consecutiveKeepAliveFailures))
|
|
//{
|
|
// var warning = $"⚠️ 保活命令发送失败,第{_consecutiveKeepAliveFailures}次。{ex.Message}";
|
|
// _logger.LogWarning(ex, warning);
|
|
// OnLog(warning, true);
|
|
//}
|
|
}
|
|
|
|
// 等待 50 秒,或被取消
|
|
await Task.Delay(KEEP_ALIVE_INTERVAL_MS, cancellationToken);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// 正常取消,不处理
|
|
}
|
|
}
|
|
|
|
public async Task KeepDeviceAliveAsync()
|
|
{
|
|
CancellationToken cancellationToken = default;
|
|
// 启动保活任务(每50秒发送一次 ConnectData)
|
|
using var keepAliveCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
var keepAliveTask = KeepDeviceAliveAsync(keepAliveCts.Token);
|
|
}
|
|
#endregion
|
|
|
|
#region 文件复制进度上下文
|
|
private class CopyProgressContext
|
|
{
|
|
public long CopiedBytes { get; set; }
|
|
public long TotalBytes { get; }
|
|
public IProgress<double> Progress { get; }
|
|
|
|
public CopyProgressContext(long totalBytes, IProgress<double> progress)
|
|
{
|
|
TotalBytes = totalBytes;
|
|
Progress = progress;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 更新已复制字节数并上报进度
|
|
/// </summary>
|
|
public void ReportCopied(long additionalBytes)
|
|
{
|
|
CopiedBytes += additionalBytes;
|
|
if (TotalBytes > 0)
|
|
{
|
|
double percentage = Math.Min(100.0, (double)CopiedBytes / TotalBytes * 100 * 0.79) + 20;
|
|
Progress?.Report(percentage);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static class UsbCopyGuard
|
|
{
|
|
private const int WriteTimeoutMs = 30000;
|
|
|
|
public readonly record struct CopyThrottleSettings(
|
|
int BufferSize,
|
|
int ChunkDelayMs,
|
|
int FileDelayMs,
|
|
int FlushIntervalBytes);
|
|
|
|
public enum CopyFailureCategory
|
|
{
|
|
Unknown,
|
|
DeviceDisconnected,
|
|
WriteTimeout,
|
|
MscFileSystemCorruption,
|
|
IoFailure
|
|
}
|
|
|
|
public static void ThrowIfCopyTargetUnavailable(
|
|
ILogger logger,
|
|
TransferState state,
|
|
string? driveRoot,
|
|
string destinationPath,
|
|
int chunkIndex,
|
|
long copiedBytes)
|
|
{
|
|
if (state.IsConnected && state.IsDataConnected)
|
|
return;
|
|
|
|
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
|
var driveText = string.IsNullOrWhiteSpace(driveRoot) ? "未知盘符" : driveRoot;
|
|
var message = $"[{timestamp}] 设备断联,停止复制。目标盘符:{driveText},文件:{destinationPath},chunk:{chunkIndex},已复制:{copiedBytes}字节";
|
|
logger.LogWarning(message);
|
|
throw new IOException(message);
|
|
}
|
|
|
|
public static void ProbeCopyTargetHealth(string destinationPath)
|
|
{
|
|
var directoryPath = Path.GetDirectoryName(destinationPath);
|
|
if (string.IsNullOrWhiteSpace(directoryPath))
|
|
{
|
|
throw new IOException($"无法确定目标目录。文件:{destinationPath}");
|
|
}
|
|
|
|
var directoryInfo = new DirectoryInfo(directoryPath);
|
|
_ = directoryInfo.Exists;
|
|
_ = directoryInfo.GetFiles().Length;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 清理MSC上损坏的目标文件,按“删除 → 重命名 → 强制覆盖”逐级降级。
|
|
/// 返回 true 表示目标已清理(或原本不存在),可以继续复制;false 表示清理失败,应跳过该文件。
|
|
/// </summary>
|
|
public static bool TryClearCorruptedTarget(ILogger logger, string destPath, out string detail)
|
|
{
|
|
detail = string.Empty;
|
|
|
|
// 1. 直接删除(文件不存在时 File.Delete 不会抛异常)
|
|
try
|
|
{
|
|
if (File.Exists(destPath))
|
|
{
|
|
File.Delete(destPath);
|
|
return true;
|
|
}
|
|
return true; // 目标不存在,无需清理
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger?.LogWarning("损坏目标文件直接删除失败:{Dest},错误:{Msg}", destPath, ex.Message);
|
|
}
|
|
|
|
// 2. 重命名 → 再删除(FAT 目录项重写通常比直接删除更宽容)
|
|
string newName = destPath + ".corrupt_" + Guid.NewGuid().ToString("N").Substring(0, 8);
|
|
try
|
|
{
|
|
File.Move(destPath, newName);
|
|
logger?.LogWarning("损坏目标文件已重命名:{Dest} → {NewName}", destPath, newName);
|
|
try { File.Delete(newName); } catch { }
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger?.LogWarning("损坏目标文件重命名失败:{Dest} → {NewName},错误:{Msg}", destPath, newName, ex.Message);
|
|
}
|
|
|
|
// 3. 强制覆盖:FileMode.Create 重建文件(可能跳过损坏的目录项)
|
|
try
|
|
{
|
|
using (var fs = new FileStream(destPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096))
|
|
{
|
|
fs.SetLength(0);
|
|
}
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
detail = ex.Message;
|
|
logger?.LogWarning("损坏目标文件强制覆盖失败:{Dest},错误:{Msg}", destPath, ex.Message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static CopyFailureCategory ClassifyCopyException(Exception ex)
|
|
{
|
|
if (ex is IOException ioEx)
|
|
{
|
|
var message = ioEx.Message ?? string.Empty;
|
|
if (message.IndexOf("设备断联", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
return CopyFailureCategory.DeviceDisconnected;
|
|
}
|
|
|
|
if (message.IndexOf("设备写入超时", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
return CopyFailureCategory.WriteTimeout;
|
|
}
|
|
|
|
if (message.IndexOf("文件或目录损坏且无法读取", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
return CopyFailureCategory.MscFileSystemCorruption;
|
|
}
|
|
|
|
return CopyFailureCategory.IoFailure;
|
|
}
|
|
|
|
return CopyFailureCategory.Unknown;
|
|
}
|
|
|
|
public static string GetFailureLabel(CopyFailureCategory category)
|
|
{
|
|
return category switch
|
|
{
|
|
CopyFailureCategory.DeviceDisconnected => "HID断联",
|
|
CopyFailureCategory.WriteTimeout => "写入超时",
|
|
CopyFailureCategory.MscFileSystemCorruption => "MSC文件系统损坏",
|
|
CopyFailureCategory.IoFailure => "MSC写入异常",
|
|
_ => "未知异常"
|
|
};
|
|
}
|
|
|
|
public static CopyThrottleSettings GetCopyThrottleSettings()
|
|
{
|
|
return new CopyThrottleSettings(
|
|
COPY_BUFFER_SIZE,
|
|
COPY_CHUNK_DELAY_MS,
|
|
COPY_FILE_DELAY_MS,
|
|
FILE_FLUSH_INTERVAL_BYTES);
|
|
}
|
|
|
|
public static bool ShouldEmitRepeatedWarning(int occurrence)
|
|
{
|
|
return occurrence <= 1 || occurrence % 3 == 0;
|
|
}
|
|
|
|
public static async Task WriteChunkWithTimeoutAsync(
|
|
FileStream destStream,
|
|
byte[] buffer,
|
|
int bytesToWrite,
|
|
CancellationToken cancellationToken,
|
|
string destinationPath,
|
|
int chunkIndex,
|
|
long copiedBytes)
|
|
{
|
|
var writeTask = destStream.WriteAsync(buffer, 0, bytesToWrite, cancellationToken);
|
|
var delayTask = Task.Delay(WriteTimeoutMs);
|
|
|
|
var completedTask = await Task.WhenAny(writeTask, delayTask);
|
|
|
|
if (completedTask == delayTask)
|
|
{
|
|
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
|
var message = $"[{timestamp}] 设备写入超时,停止复制。文件:{destinationPath},chunk:{chunkIndex},已复制:{copiedBytes}字节";
|
|
throw new IOException(message);
|
|
}
|
|
|
|
// 确保写入任务完成,避免数据丢失
|
|
await writeTask;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region 压缩包操作
|
|
public async Task CopyDeltaDirectoryToUdiskAsync(
|
|
string localRootDir,
|
|
IProgress<double> progress,
|
|
CancellationToken ct)
|
|
{
|
|
// 内部打开U盘
|
|
await OpenUDiskAndDetectDriveAsync();
|
|
|
|
string? targetDrive = await FindAssociatedUsbDisk(_vendorId, _productId);
|
|
if (string.IsNullOrEmpty(targetDrive))
|
|
throw new InvalidOperationException("未检测到U盘设备");
|
|
|
|
// 保活机制(防止断开)
|
|
using var keepAliveCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
|
var keepAliveTask = KeepDeviceAliveAsync(keepAliveCts.Token);
|
|
|
|
try
|
|
{
|
|
string destRoot = Path.Combine(targetDrive, usbdiskDir);
|
|
Directory.CreateDirectory(destRoot);
|
|
|
|
// ==============================
|
|
// 差异对比
|
|
// ==============================
|
|
var localFiles = ScanLocalFiles(localRootDir);
|
|
var diskFiles = await ScanUdiskFilesAsync(ct);
|
|
var changedFiles = GetChangedFiles(localFiles, diskFiles);
|
|
|
|
if (changedFiles.Count == 0)
|
|
{
|
|
progress.Report(100);
|
|
return;
|
|
}
|
|
|
|
// 计算总大小(精准进度)
|
|
long totalBytes = 0;
|
|
foreach (var rel in changedFiles)
|
|
{
|
|
string file = Path.Combine(localRootDir, rel);
|
|
if (File.Exists(file))
|
|
totalBytes += new FileInfo(file).Length;
|
|
}
|
|
|
|
// ==============================
|
|
// 只复制差异文件 → 直写U盘(超快)
|
|
// ==============================
|
|
long processed = 0;
|
|
int skippedFiles = 0;
|
|
foreach (var relPath in changedFiles)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
string srcFile = Path.Combine(localRootDir, relPath);
|
|
string destFile = Path.Combine(destRoot, relPath);
|
|
|
|
var (ok, copied) = await CopyDeltaFileWithRecoveryAsync(srcFile, destFile, ct);
|
|
if (!ok) skippedFiles++;
|
|
processed += copied;
|
|
progress.Report(totalBytes > 0 ? (double)processed / totalBytes * 100 : 100);
|
|
}
|
|
|
|
progress.Report(100);
|
|
|
|
if (skippedFiles > 0)
|
|
{
|
|
OnLog($"⚠️ 云数据同步完成,但 {skippedFiles} 个文件因损坏被跳过,建议对U盘执行 chkdsk 修复");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
keepAliveCts.Cancel();
|
|
try { await keepAliveTask; } catch { }
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 复制单个差异文件;若目标文件损坏(删除/重命名/覆盖失败等)则清理后重试一次。
|
|
/// 返回 (是否成功, 复制的字节数)。设备断联/写入超时等设备级异常会原样抛出,由上层中断整个同步。
|
|
/// </summary>
|
|
private async Task<(bool ok, long copied)> CopyDeltaFileWithRecoveryAsync(
|
|
string srcFile,
|
|
string destFile,
|
|
CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
long copied = await CopyDeltaFileCoreAsync(srcFile, destFile, ct);
|
|
return (true, copied);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var category = UsbCopyGuard.ClassifyCopyException(ex);
|
|
if (category == UsbCopyGuard.CopyFailureCategory.DeviceDisconnected ||
|
|
category == UsbCopyGuard.CopyFailureCategory.WriteTimeout)
|
|
{
|
|
throw; // 设备级问题:重试无意义,中断整个同步
|
|
}
|
|
|
|
_logger?.LogWarning("差异文件复制失败({Label}),尝试清理损坏目标后重试:{Dest},错误:{Msg}",
|
|
UsbCopyGuard.GetFailureLabel(category), destFile, ex.Message);
|
|
|
|
if (UsbCopyGuard.TryClearCorruptedTarget(_logger, destFile, out var clearDetail))
|
|
{
|
|
try
|
|
{
|
|
long copied = await CopyDeltaFileCoreAsync(srcFile, destFile, ct);
|
|
return (true, copied);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception retryEx)
|
|
{
|
|
var retryCategory = UsbCopyGuard.ClassifyCopyException(retryEx);
|
|
if (retryCategory == UsbCopyGuard.CopyFailureCategory.DeviceDisconnected ||
|
|
retryCategory == UsbCopyGuard.CopyFailureCategory.WriteTimeout)
|
|
{
|
|
throw;
|
|
}
|
|
_logger?.LogWarning("清理后重试仍失败,跳过该文件:{Dest},错误:{Msg}", destFile, retryEx.Message);
|
|
return (false, 0);
|
|
}
|
|
}
|
|
|
|
_logger?.LogError("损坏目标文件无法清理,跳过:{Dest},错误:{Detail}", destFile, clearDetail);
|
|
return (false, 0);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 差异文件直写U盘核心逻辑(不含容错)。
|
|
/// </summary>
|
|
private async Task<long> CopyDeltaFileCoreAsync(string srcFile, string destFile, CancellationToken ct)
|
|
{
|
|
string destDir = Path.GetDirectoryName(destFile)!;
|
|
Directory.CreateDirectory(destDir);
|
|
|
|
using var fsRead = new FileStream(srcFile, FileMode.Open, FileAccess.Read, FileShare.Read, 32768, true);
|
|
using var fsWrite = new FileStream(destFile, FileMode.Create, FileAccess.Write, FileShare.None, 32768, true);
|
|
|
|
byte[] buffer = new byte[32768];
|
|
long copied = 0;
|
|
int read;
|
|
while ((read = await fsRead.ReadAsync(buffer, 0, buffer.Length, ct)) > 0)
|
|
{
|
|
await fsWrite.WriteAsync(buffer, 0, read, ct);
|
|
copied += read;
|
|
}
|
|
|
|
return copied;
|
|
}
|
|
|
|
// 扫描U盘中所有文件 (相对路径 + 文件信息)
|
|
public async Task<Dictionary<string, FileInfo>> ScanUdiskFilesAsync(CancellationToken ct)
|
|
{
|
|
var dict = new Dictionary<string, FileInfo>(StringComparer.OrdinalIgnoreCase);
|
|
string? drive = await FindAssociatedUsbDisk(_vendorId, _productId);
|
|
if (string.IsNullOrEmpty(drive)) return dict;
|
|
|
|
string root = Path.Combine(drive, usbdiskDir);
|
|
if (!Directory.Exists(root)) return dict;
|
|
|
|
await Task.Run(() =>
|
|
{
|
|
// 逐目录容错递归:单个目录损坏时跳过该目录,不影响其余文件
|
|
ScanUdiskDirectoryRecursive(root, root, dict, ct);
|
|
}, ct);
|
|
|
|
return dict;
|
|
}
|
|
|
|
// 逐目录容错递归扫描:某个目录损坏/无法枚举时跳过该目录,保证整体扫描不中断
|
|
private void ScanUdiskDirectoryRecursive(
|
|
string dir,
|
|
string root,
|
|
Dictionary<string, FileInfo> dict,
|
|
CancellationToken ct)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
string[] files;
|
|
try
|
|
{
|
|
files = Directory.GetFiles(dir);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogWarning("U盘目录枚举失败,跳过该目录:{Dir},错误:{Msg}", dir, ex.Message);
|
|
return;
|
|
}
|
|
|
|
foreach (var file in files)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
try
|
|
{
|
|
var fi = new FileInfo(file);
|
|
string rel = GetRelativePath(root, file);
|
|
if (rel == ".DS_Store") continue;
|
|
dict[rel] = fi;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogWarning("U盘文件信息读取失败,跳过:{File},错误:{Msg}", file, ex.Message);
|
|
}
|
|
}
|
|
|
|
string[] subDirs;
|
|
try
|
|
{
|
|
subDirs = Directory.GetDirectories(dir);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogWarning("U盘子目录枚举失败,跳过该目录:{Dir},错误:{Msg}", dir, ex.Message);
|
|
return;
|
|
}
|
|
|
|
foreach (var subDir in subDirs)
|
|
{
|
|
ScanUdiskDirectoryRecursive(subDir, root, dict, ct);
|
|
}
|
|
}
|
|
|
|
// 扫描本地文件
|
|
public Dictionary<string, FileInfo> ScanLocalFiles(string localRootDir)
|
|
{
|
|
var dict = new Dictionary<string, FileInfo>(StringComparer.OrdinalIgnoreCase);
|
|
if (!Directory.Exists(localRootDir)) return dict;
|
|
|
|
foreach (var file in Directory.EnumerateFiles(localRootDir, "*.*", SearchOption.AllDirectories))
|
|
{
|
|
var fi = new FileInfo(file);
|
|
string rel = GetRelativePath(localRootDir, file);
|
|
if (rel == ".DS_Store") continue;
|
|
dict[rel] = fi;
|
|
}
|
|
return dict;
|
|
}
|
|
|
|
// 对比 → 得到需要更新的文件列表
|
|
public List<string> GetChangedFiles(
|
|
Dictionary<string, FileInfo> localFiles,
|
|
Dictionary<string, FileInfo> udiskFiles)
|
|
{
|
|
var changed = new List<string>();
|
|
|
|
foreach (var kv in localFiles)
|
|
{
|
|
string rel = kv.Key;
|
|
var local = kv.Value;
|
|
|
|
if (!udiskFiles.TryGetValue(rel, out var disk))
|
|
{
|
|
changed.Add(rel); // 新增
|
|
continue;
|
|
}
|
|
|
|
if (local.Length != disk.Length)
|
|
{
|
|
changed.Add(rel); // 大小不一样
|
|
continue;
|
|
}
|
|
|
|
//if (local.LastWriteTimeUtc > disk.LastWriteTimeUtc.AddSeconds(5))
|
|
//{
|
|
// changed.Add(rel); // 本地更新
|
|
//}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
public async Task<(string zipPath, int count)> CreateDeltaZipAsync(
|
|
string localRootDir,
|
|
List<string> changedRelativePaths,
|
|
CancellationToken ct)
|
|
{
|
|
string zipPath = Path.Combine(Path.GetTempPath(), $"delta_{Guid.NewGuid():N}.zip");
|
|
|
|
await Task.Run(() =>
|
|
{
|
|
using var zip = ZipFile.Open(zipPath, ZipArchiveMode.Create);
|
|
foreach (var rel in changedRelativePaths)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
string src = Path.Combine(localRootDir, rel);
|
|
if (!File.Exists(src)) continue;
|
|
|
|
zip.CreateEntryFromFile(src, rel, CompressionLevel.Fastest);
|
|
}
|
|
}, ct);
|
|
|
|
return (zipPath, changedRelativePaths.Count);
|
|
}
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// 从语言字典中获取值,兼容 .NET Framework 4.8(替代 Dictionary.GetValueOrDefault)
|
|
/// </summary>
|
|
private static string GetLanguageValueOrDefault(string key)
|
|
{
|
|
return AiKLanguage.LanguageKey.TryGetValue(key, out var value) ? value : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 计算相对路径,兼容 .NET Framework 4.8(替代 Path.GetRelativePath)
|
|
/// </summary>
|
|
private static string GetRelativePath(string relativeTo, string path)
|
|
{
|
|
if (!relativeTo.EndsWith(Path.DirectorySeparatorChar.ToString()))
|
|
relativeTo += Path.DirectorySeparatorChar;
|
|
|
|
var relUri = new Uri(relativeTo).MakeRelativeUri(new Uri(path));
|
|
var relPath = Uri.UnescapeDataString(relUri.ToString());
|
|
|
|
return relPath.Replace('/', Path.DirectorySeparatorChar);
|
|
}
|
|
}
|
|
}
|
|
|