using Prism.Commands; using System; using System.Collections.ObjectModel; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Win32; using TSMasterCAN; using UIShare.ViewModelBase; using XCPCore; namespace TSMasterXCPModule.ViewModels { /// /// TSMaster XCP 测试视图模型:总线连接(TSMaster)、XCP 会话(CONNECT/GET_STATUS)、 /// 内存读写(SET_MTA/UPLOAD/DOWNLOAD)与 DAQ 采集(FREE_DAQ/ALLOC_DAQ/WRITE_DAQ/START_STOP)。 /// 协议核心来自 XCPCore( + )。 /// public class TSMasterXCPViewModel : NavigateViewModelBase, IDisposable { #region 字段 /// XCP CAN 传输层(XcpConnect 成功后创建) private XcpCanTransport? _transport; /// XCP 主站协议核心(XcpConnect 成功后创建) private XcpMaster? _master; /// 日志缓冲(防止无限增长) private readonly StringBuilder _logBuffer = new(); #endregion #region 属性 private bool _isBusy; /// 是否正在执行异步操作(驱动 Busy 指示条) public bool IsBusy { get => _isBusy; set { SetProperty(ref _isBusy, value); } } private string _responseLog = string.Empty; /// 操作日志(时间戳前缀,只读展示) public string ResponseLog { get => _responseLog; set => SetProperty(ref _responseLog, value); } // ---------- TSMaster 总线 ---------- private string _tsMasterProjectPath = string.Empty; /// TSMaster 工程文件路径(可空:空则初始化默认工程) public string TSMasterProjectPath { get => _tsMasterProjectPath; set => SetProperty(ref _tsMasterProjectPath, value); } private bool _isBusConnected; /// TSMaster 总线是否已连接(tsapp_connect + rbs_start 成功) public bool IsBusConnected { get => _isBusConnected; set => SetProperty(ref _isBusConnected, value); } // ---------- A2L 描述文件 ---------- private string _a2lFilePath = string.Empty; /// A2L 文件路径 public string A2lFilePath { get => _a2lFilePath; set => SetProperty(ref _a2lFilePath, value); } private string _a2lSummaryText = "未加载 A2L 文件"; /// A2L 解析结果摘要 public string A2lSummaryText { get => _a2lSummaryText; set => SetProperty(ref _a2lSummaryText, value); } /// 解析出的测量量列表(选中后自动填入 DAQ 地址与长度) public ObservableCollection A2lMeasurements { get; } = new ObservableCollection(); /// 解析出的标定量列表(选中后自动填入内存读写地址与长度) public ObservableCollection A2lCharacteristics { get; } = new ObservableCollection(); private A2lMeasurement? _selectedMeasurement; /// 当前选中的测量量:自动带入 DAQ 的 ODT 地址与长度 public A2lMeasurement? SelectedMeasurement { get => _selectedMeasurement; set { if (!SetProperty(ref _selectedMeasurement, value) || value == null) return; OdtAddressText = value.AddressText; if (value.SizeBytes > 0) OdtLength = value.SizeBytes.ToString(); Log($"A2L 测量量 {value.Name}:地址 {value.AddressText},长度 {value.SizeText}(已填入 DAQ 配置)"); } } private A2lCharacteristic? _selectedCharacteristic; /// 当前选中的标定量:自动带入内存读写的地址与长度 public A2lCharacteristic? SelectedCharacteristic { get => _selectedCharacteristic; set { if (!SetProperty(ref _selectedCharacteristic, value) || value == null) return; MemAddressText = value.AddressText; if (value.SizeBytes > 0 && value.SizeBytes <= 200) MemReadLength = value.SizeBytes.ToString(); Log($"A2L 标定量 {value.Name}({value.CharacteristicType}):地址 {value.AddressText},长度 {value.SizeText}(已填入内存读写)"); } } // ---------- XCP 连接配置 ---------- private string _canChannel = "0"; /// CAN 通道号(0 起始) public string CanChannel { get => _canChannel; set => SetProperty(ref _canChannel, value); } private string _croIdText = "0x7E0"; /// CRO CAN ID(命令发送,hex) public string CroIdText { get => _croIdText; set => SetProperty(ref _croIdText, value); } private string _dtoIdText = "0x7E8"; /// DTO/RES CAN ID(响应接收,hex) public string DtoIdText { get => _dtoIdText; set => SetProperty(ref _dtoIdText, value); } private bool _isExtendedFrame; /// 是否扩展帧(29 位 ID) public bool IsExtendedFrame { get => _isExtendedFrame; set => SetProperty(ref _isExtendedFrame, value); } private bool _isCanFd; /// 是否 CAN FD 帧 public bool IsCanFd { get => _isCanFd; set => SetProperty(ref _isCanFd, value); } private string _commandTimeoutMs = "1000"; /// XCP 命令响应超时(毫秒) public string CommandTimeoutMs { get => _commandTimeoutMs; set => SetProperty(ref _commandTimeoutMs, value); } private bool _isConnected; /// XCP 逻辑连接是否建立(CONNECT 成功) public bool IsConnected { get => _isConnected; set => SetProperty(ref _isConnected, value); } private string _slaveInfoText = "未连接"; /// 从站能力信息(CONNECT 响应解析) public string SlaveInfoText { get => _slaveInfoText; set => SetProperty(ref _slaveInfoText, value); } // ---------- 内存读写 ---------- private string _memAddressText = "0x00000000"; /// 内存地址(hex) public string MemAddressText { get => _memAddressText; set => SetProperty(ref _memAddressText, value); } private string _memReadLength = "8"; /// 读取长度(字节) public string MemReadLength { get => _memReadLength; set => SetProperty(ref _memReadLength, value); } private string _memWriteDataText = string.Empty; /// 写入数据(hex 字节序列) public string MemWriteDataText { get => _memWriteDataText; set => SetProperty(ref _memWriteDataText, value); } private string _readResultText = string.Empty; /// 最近一次读取结果(hex) public string ReadResultText { get => _readResultText; set => SetProperty(ref _readResultText, value); } // ---------- DAQ 采集 ---------- private string _daqListNumber = "0"; /// DAQ 列表编号 public string DaqListNumber { get => _daqListNumber; set => SetProperty(ref _daqListNumber, value); } private string _eventChannel = "0"; /// DAQ 事件通道编号 public string EventChannel { get => _eventChannel; set => SetProperty(ref _eventChannel, value); } private string _prescaler = "1"; /// DAQ 预分频(每 N 个事件采集一次) public string Prescaler { get => _prescaler; set => SetProperty(ref _prescaler, value); } private string _odtAddressText = "0x00000000"; /// ODT 条目地址(hex,信号在从站内存中的地址) public string OdtAddressText { get => _odtAddressText; set => SetProperty(ref _odtAddressText, value); } private string _odtLength = "4"; /// ODT 条目长度(字节) public string OdtLength { get => _odtLength; set => SetProperty(ref _odtLength, value); } private string _latestDtoText = string.Empty; /// 最近收到的 DTO 帧(hex) public string LatestDtoText { get => _latestDtoText; set => SetProperty(ref _latestDtoText, value); } private int _dtoFrameCount; /// 已接收 DTO 帧计数 public int DtoFrameCount { get => _dtoFrameCount; set => SetProperty(ref _dtoFrameCount, value); } #endregion #region 命令 /// 浏览并加载 A2L 描述文件 public DelegateCommand BrowseA2lCommand { get; } /// 按当前路径重新解析 A2L 描述文件 public DelegateCommand ReloadA2lCommand { get; } /// 初始化 TSMaster 驱动(可选加载工程) public DelegateCommand InitDriverCommand { get; } /// 释放 TSMaster 驱动 public DelegateCommand ReleaseDriverCommand { get; } /// 连接 TSMaster CAN 总线 public DelegateCommand ConnectBusCommand { get; } /// 断开 TSMaster CAN 总线 public DelegateCommand DisconnectBusCommand { get; } /// 建立 XCP 逻辑连接(CONNECT) public DelegateCommand XcpConnectCommand { get; } /// 断开 XCP 逻辑连接(DISCONNECT + 传输层关闭) public DelegateCommand XcpDisconnectCommand { get; } /// 查询从站状态(GET_STATUS) public DelegateCommand GetStatusCommand { get; } /// 读取内存(SET_MTA + UPLOAD) public DelegateCommand ReadMemoryCommand { get; } /// 写入内存(SET_MTA + DOWNLOAD) public DelegateCommand WriteMemoryCommand { get; } /// 配置 DAQ 列表(FREE_DAQ → ALLOC_DAQ → SET_DAQ_PTR/WRITE_DAQ → SET_DAQ_LIST_MODE) public DelegateCommand ConfigureDaqCommand { get; } /// 启动 DAQ 采集 public DelegateCommand StartDaqCommand { get; } /// 停止 DAQ 采集 public DelegateCommand StopDaqCommand { get; } /// 接收一帧 DTO 数据 public DelegateCommand ReceiveDtoCommand { get; } /// 清空日志 public DelegateCommand ClearLogCommand { get; } #endregion /// 创建视图模型 public TSMasterXCPViewModel(IContainerProvider containerProvider) : base(containerProvider) { BrowseA2lCommand = new DelegateCommand(BrowseA2l); ReloadA2lCommand = new DelegateCommand(ReloadA2l); InitDriverCommand = new DelegateCommand(() => RunGuarded(InitDriverAsync, "初始化驱动")); ReleaseDriverCommand = new DelegateCommand(() => RunGuarded(ReleaseDriverAsync, "释放驱动")); ConnectBusCommand = new DelegateCommand(() => RunGuarded(ConnectBusAsync, "连接总线")); DisconnectBusCommand = new DelegateCommand(() => RunGuarded(DisconnectBusAsync, "断开总线")); XcpConnectCommand = new DelegateCommand(() => RunGuarded(XcpConnectAsync, "XCP 连接")); XcpDisconnectCommand = new DelegateCommand(() => RunGuarded(XcpDisconnectAsync, "XCP 断开")); GetStatusCommand = new DelegateCommand(() => RunGuarded(GetStatusAsync, "查询状态")); ReadMemoryCommand = new DelegateCommand(() => RunGuarded(ReadMemoryAsync, "读取内存")); WriteMemoryCommand = new DelegateCommand(() => RunGuarded(WriteMemoryAsync, "写入内存")); ConfigureDaqCommand = new DelegateCommand(() => RunGuarded(ConfigureDaqAsync, "配置 DAQ")); StartDaqCommand = new DelegateCommand(() => RunGuarded(StartDaqAsync, "启动 DAQ")); StopDaqCommand = new DelegateCommand(() => RunGuarded(StopDaqAsync, "停止 DAQ")); ReceiveDtoCommand = new DelegateCommand(() => RunGuarded(ReceiveDtoAsync, "接收 DTO")); ClearLogCommand = new DelegateCommand(() => { _logBuffer.Clear(); ResponseLog = string.Empty; }); Log("TSMaster XCP 测试界面就绪。使用顺序:初始化驱动 → 连接总线 → XCP 连接 → 内存读写 / DAQ。"); } #region 命令处理 —— A2L 描述文件 /// 浏览选择 A2L 文件并解析 private void BrowseA2l() { var dialog = new OpenFileDialog { Title = "选择 A2L 描述文件", Filter = "A2L 描述文件 (*.a2l)|*.a2l|所有文件 (*.*)|*.*", CheckFileExists = true, Multiselect = false }; var currentDir = Path.GetDirectoryName(A2lFilePath); if (!string.IsNullOrEmpty(currentDir) && Directory.Exists(currentDir)) dialog.InitialDirectory = currentDir; if (dialog.ShowDialog() != true) return; LoadA2l(dialog.FileName); } /// 按当前路径重新解析 A2L private void ReloadA2l() { var path = A2lFilePath?.Trim() ?? string.Empty; if (path.Length == 0) { Log("[提示] 请先选择 A2L 文件(点击「浏览...」)。"); return; } LoadA2l(path); } /// 解析并刷新测量量 / 标定量列表 private void LoadA2l(string filePath) { if (!File.Exists(filePath)) { A2lSummaryText = "A2L 文件不存在"; Log($"[错误] A2L 文件不存在: {filePath}"); return; } try { var db = A2lParser.LoadFile(filePath); A2lFilePath = filePath; A2lMeasurements.Clear(); foreach (var item in db.Measurements) A2lMeasurements.Add(item); A2lCharacteristics.Clear(); foreach (var item in db.Characteristics) A2lCharacteristics.Add(item); SelectedMeasurement = null; SelectedCharacteristic = null; A2lSummaryText = $"{db.Summary} · {Path.GetFileName(filePath)}"; Log($"已加载 A2L:{filePath}({db.Summary})"); if (db.Measurements.Count == 0 && db.Characteristics.Count == 0) Log("[警告] 未解析出可用条目,请确认该文件是 A2L(ASAM MCD-2 MC)描述文件。"); } catch (Exception ex) { A2lMeasurements.Clear(); A2lCharacteristics.Clear(); A2lSummaryText = "A2L 解析失败"; Log($"[错误] 解析 A2L 失败:{ex.Message}"); } } #endregion #region 命令处理 —— TSMaster 总线 private async Task InitDriverAsync() { var project = TSMasterProjectPath?.Trim() ?? string.Empty; int re; if (string.IsNullOrEmpty(project)) { re = CAN.Init("TSMasterXCP"); Log("初始化 TSMaster 驱动(默认工程)..."); } else { if (!File.Exists(project)) { Log($"[错误] TSMaster 工程文件不存在: {project}"); return; } re = CAN.Init("TSMasterXCP", project); Log($"初始化 TSMaster 驱动(工程: {project})..."); } if (re != 0) { Log($"[错误] 驱动初始化失败,返回码 {re}:{CAN.GetErrorDescription(re)}"); return; } Log("TSMaster 驱动初始化成功。"); await Task.CompletedTask; } private async Task ReleaseDriverAsync() { if (_master != null) await XcpDisconnectAsync(); CAN.Release(); IsBusConnected = false; Log("TSMaster 驱动已释放。"); await Task.CompletedTask; } private async Task ConnectBusAsync() { if (!CAN.ConnectFlag) { Log("连接 TSMaster CAN 总线..."); int re = CAN.Connect(); if (re != 0) { Log($"[错误] 总线连接失败,返回码 {re}:{CAN.GetErrorDescription(re)}(请检查硬件通道映射)"); return; } } IsBusConnected = true; Log("TSMaster CAN 总线已连接。"); await Task.CompletedTask; } private async Task DisconnectBusAsync() { if (_master != null) await XcpDisconnectAsync(); int re = CAN.DisConnect(); IsBusConnected = false; Log(re == 0 ? "TSMaster CAN 总线已断开。" : $"[警告] 总线断开返回码 {re}:{CAN.GetErrorDescription(re)}"); await Task.CompletedTask; } #endregion #region 命令处理 —— XCP 会话 private async Task XcpConnectAsync() { if (!CAN.ConnectFlag) { Log("[错误] TSMaster 总线未连接,请先执行「初始化驱动 → 连接总线」。"); return; } if (_master != null && _master.State != XcpSessionState.Disconnected) { Log("XCP 已处于连接状态,如需重连请先断开。"); return; } if (!TryGetChannel(out byte channel)) return; if (!TryGetHexUint(CroIdText, "CRO ID", out uint croId)) return; if (!TryGetHexUint(DtoIdText, "DTO ID", out uint dtoId)) return; int timeoutMs = ParseInt(CommandTimeoutMs, 1000); // 每次连接按当前界面参数重建传输层与主站 _transport = new XcpCanTransport(channel, croId, dtoId, IsExtendedFrame, IsCanFd, timeoutMs); _master = new XcpMaster(_transport, new XcpConfig { CommandTimeoutMs = timeoutMs }); Log($"XCP CONNECT: CH={channel}, CRO=0x{croId:X}, DTO=0x{dtoId:X}, {(IsCanFd ? "CAN FD" : "经典 CAN")}..."); bool ok = await _master.ConnectAsync(0x00); if (!ok) { Log("[错误] XCP CONNECT 失败(无有效响应,请检查从站上电、ID 配置与接线)。"); _master = null; _transport?.Close(); _transport = null; IsConnected = false; return; } IsConnected = true; var info = _master.SlaveInfo; SlaveInfoText = $"MaxCto={info.MaxCtoSize} MaxDto={info.MaxDtoSize} 资源=0x{info.Resource:X2}" + $" 协议版本={info.ProtocolVersion} 传输层版本={info.TransportLayerVersion}"; Log($"XCP CONNECT 成功。{SlaveInfoText}"); } private async Task XcpDisconnectAsync() { if (_master != null) { await _master.FullDisconnectAsync(); Log("XCP 已断开(DISCONNECT + 传输层关闭)。"); } _master = null; _transport = null; IsConnected = false; SlaveInfoText = "未连接"; } private async Task GetStatusAsync() { if (!RequireMaster()) return; var status = await _master!.GetStatusAsync(); if (status == null) { Log("[错误] GET_STATUS 无响应。"); return; } // 正响应: [FF][SESSION_STATUS][PROTECTION_STATUS][SEQ][DAQ_COUNT(2)][EVENT_COUNT(2)] var sb = new StringBuilder("GET_STATUS: "); sb.Append($"会话状态=0x{status[1]:X2}"); if (status.Length > 2) sb.Append($", 保护状态=0x{status[2]:X2}"); if (status.Length > 4) sb.Append($", 配置的 DAQ 列表数={(status[4] << 8) | status[5]}"); if (status.Length > 6) sb.Append($", 事件通道数={(status[6] << 8) | status[7]}"); Log(sb.ToString()); } #endregion #region 命令处理 —— 内存读写 private async Task ReadMemoryAsync() { if (!RequireMaster()) return; if (!TryGetHexUint(MemAddressText, "地址", out uint address)) return; int length = ParseInt(MemReadLength, 8); if (length <= 0 || length > 200) { Log("[错误] 读取长度须在 1~200 字节之间。"); return; } var data = await _master!.ReadMemoryAsync(address, length); if (data == null) { Log($"[错误] 读取内存 0x{address:X8} 失败。"); return; } ReadResultText = ToHexString(data); Log($"读取 0x{address:X8} × {data.Length} 字节: {ReadResultText}"); } private async Task WriteMemoryAsync() { if (!RequireMaster()) return; if (!TryGetHexUint(MemAddressText, "地址", out uint address)) return; byte[] data; try { data = ParseHexBytes(MemWriteDataText); } catch (FormatException) { Log("[错误] 写入数据格式无效,请输入 hex 字节序列(如 FF 00 1A)。"); return; } if (data.Length == 0) { Log("[错误] 写入数据为空。"); return; } bool ok = await _master!.WriteMemoryAsync(address, data); Log(ok ? $"写入 0x{address:X8} × {data.Length} 字节成功: {ToHexString(data)}" : $"[错误] 写入内存 0x{address:X8} 失败。"); } #endregion #region 命令处理 —— DAQ 采集 private async Task ConfigureDaqAsync() { if (!RequireMaster()) return; ushort daqList = (ushort)Math.Clamp(ParseInt(DaqListNumber, 0), 0, 0xFFFF); ushort eventChannel = (ushort)Math.Clamp(ParseInt(EventChannel, 0), 0, 0xFFFF); ushort prescaler = (ushort)Math.Clamp(ParseInt(Prescaler, 1), 1, 0xFFFF); if (!TryGetHexUint(OdtAddressText, "ODT 地址", out uint address)) return; int odtLen = ParseInt(OdtLength, 4); if (odtLen <= 0 || odtLen > 64) { Log("[错误] ODT 条目长度须在 1~64 字节之间。"); return; } var config = new DaqListConfig { DaqListNumber = daqList, EventChannel = eventChannel, Prescaler = prescaler, Mode = 0x00, Enable = true, OdEntries = { new OdEntry { Address = address, AddressExtension = 0, Length = (byte)odtLen, Name = $"ODT0_{address:X8}" } } }; bool ok = await _master!.ConfigureDaqListAsync(config); Log(ok ? $"DAQ 配置成功:列表 {daqList},事件通道 {eventChannel},预分频 {prescaler},条目 0x{address:X8} × {odtLen}B。" : "[错误] DAQ 配置失败(FREE_DAQ/ALLOC_DAQ/WRITE_DAQ/SET_DAQ_LIST_MODE 某一步被从站拒绝)。"); } private async Task StartDaqAsync() { if (!RequireMaster()) return; ushort daqList = (ushort)Math.Clamp(ParseInt(DaqListNumber, 0), 0, 0xFFFF); bool ok = await _master!.StartStopDaqAsync(true, daqList); Log(ok ? $"DAQ 列表 {daqList} 已启动采集。" : $"[错误] 启动 DAQ 列表 {daqList} 失败。"); } private async Task StopDaqAsync() { if (!RequireMaster()) return; ushort daqList = (ushort)Math.Clamp(ParseInt(DaqListNumber, 0), 0, 0xFFFF); bool ok = await _master!.StartStopDaqAsync(false, daqList); Log(ok ? $"DAQ 列表 {daqList} 已停止。" : $"[错误] 停止 DAQ 列表 {daqList} 失败。"); } private async Task ReceiveDtoAsync() { if (!RequireMaster()) return; var dto = await _master!.ReceiveDtoAsync(500); if (dto == null) { Log("500ms 内未收到 DTO 帧(确认 DAQ 是否已启动、DTO ID 是否正确)。"); return; } DtoFrameCount++; LatestDtoText = ToHexString(dto); Log($"DTO #{DtoFrameCount}: {LatestDtoText}"); } #endregion #region 通用辅助 /// 统一命令入口:Busy 指示 + 异常捕获 + 日志 private async void RunGuarded(Func action, string actionName) { if (IsBusy) return; IsBusy = true; try { await action(); } catch (TimeoutException tex) { Log($"[错误] {actionName} 超时:{tex.Message}"); } catch (XcpProtocolException xex) { Log($"[错误] {actionName} 协议错误:{xex.Message}"); } catch (InvalidOperationException ioex) { Log($"[错误] {actionName}:{ioex.Message}"); } catch (Exception ex) { Log($"[错误] {actionName} 异常:{ex.Message}"); } finally { IsBusy = false; } } /// 确认 XCP 主站已创建(界面校验) private bool RequireMaster() { if (_master == null || _master.State == XcpSessionState.Disconnected) { Log("[错误] XCP 未连接,请先执行「XCP 连接」。"); return false; } return true; } /// 解析 CAN 通道号(0 起始) private bool TryGetChannel(out byte channel) { channel = 0; if (!int.TryParse(CanChannel?.Trim(), out int value) || value < 0 || value > 255) { Log($"[错误] CAN 通道号无效: {CanChannel}(0~255)。"); return false; } channel = (byte)value; return true; } /// 解析 hex 整数:支持 0x 前缀 / 纯 hex(如 7E0 / 0x7E0) private bool TryGetHexUint(string text, string fieldName, out uint value) { value = 0; var cleaned = (text ?? string.Empty).Trim().Replace(" ", ""); if (cleaned.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) cleaned = cleaned[2..]; if (cleaned.Length == 0 || !uint.TryParse(cleaned, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out value)) { Log($"[错误] {fieldName} 格式无效: {text}(应为 hex,如 0x7E0)。"); return false; } return true; } /// 解析 hex 字节序列:FF 00 1A / FF,00,1A / 0xFF-0x00 均可 private static byte[] ParseHexBytes(string text) { var tokens = (text ?? string.Empty).Split(new[] { ' ', ',', '-', ';' }, StringSplitOptions.RemoveEmptyEntries); var result = new byte[tokens.Length]; for (int i = 0; i < tokens.Length; i++) { var token = tokens[i]; if (token.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) token = token[2..]; result[i] = byte.Parse(token, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture); } return result; } /// 字节数组转 hex 字符串(大写、空格分隔) private static string ToHexString(byte[] data) { if (data == null || data.Length == 0) return string.Empty; var sb = new StringBuilder(data.Length * 3); foreach (var b in data) sb.Append(b.ToString("X2")).Append(' '); return sb.ToString().TrimEnd(); } /// 安全解析整数(失败用默认值) private static int ParseInt(string? text, int defaultValue) => int.TryParse(text?.Trim(), out int value) ? value : defaultValue; /// 追加一行带时间戳的日志(超 200 行自动截断头部) private void Log(string message) { var line = $"[{DateTime.Now:HH:mm:ss.fff}] {message}"; _logBuffer.AppendLine(line); var text = _logBuffer.ToString(); var lines = text.Split('\n'); if (lines.Length > 200) { _logBuffer.Clear(); _logBuffer.Append(string.Join("\n", lines[^200..])); } ResponseLog = _logBuffer.ToString().TrimEnd(); } #endregion /// 释放资源:断开 XCP 连接并关闭传输层 public void Dispose() { try { if (_master != null) _master.FullDisconnectAsync().Wait(2000); else _transport?.Close(); } catch { } _master = null; _transport = null; } } }