From 74338fdb3a07c26a6b79c9c2e08ae97fc288dfa8 Mon Sep 17 00:00:00 2001 From: Tyrone CT Date: Fri, 6 Feb 2026 12:34:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=91=A8=E7=AB=8B=E5=8A=9F=E7=9A=84CAN=20/FD?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CapMachine.Wpf/App.xaml.cs | 1 + CapMachine.Wpf/CanDrive/ZlgCan/ZLGAPI.cs | 4 +- .../CanDrive/ZlgCan/ZlgCanFd200uDriver.cs | 254 ++- .../CanDrive/ZlgCan/ZlgDbcDatabase.cs | 192 +- CapMachine.Wpf/Services/ZlgCanDriveService.cs | 187 ++ CapMachine.Wpf/Services/ZlgLinDriveService.cs | 164 +- .../DialogZlgCanLinRwConfigViewModel.cs | 753 ++++++++ .../ViewModels/ZlgCanDriveConfigViewModel.cs | 1583 ++++++++++++++++- .../ViewModels/ZlgLinDriveConfigViewModel.cs | 139 ++ .../Views/DialogZlgCanLinRwConfigView.xaml | 331 ++++ .../Views/DialogZlgCanLinRwConfigView.xaml.cs | 15 + .../Views/ZlgCanDriveConfigView.xaml | 939 +++++++--- .../Views/ZlgLinDriveConfigView.xaml | 8 + 13 files changed, 4260 insertions(+), 310 deletions(-) create mode 100644 CapMachine.Wpf/ViewModels/DialogZlgCanLinRwConfigViewModel.cs create mode 100644 CapMachine.Wpf/Views/DialogZlgCanLinRwConfigView.xaml create mode 100644 CapMachine.Wpf/Views/DialogZlgCanLinRwConfigView.xaml.cs diff --git a/CapMachine.Wpf/App.xaml.cs b/CapMachine.Wpf/App.xaml.cs index dcfb361..969b0f7 100644 --- a/CapMachine.Wpf/App.xaml.cs +++ b/CapMachine.Wpf/App.xaml.cs @@ -198,6 +198,7 @@ namespace CapMachine.Wpf containerRegistry.RegisterDialog(); containerRegistry.RegisterDialog(); containerRegistry.RegisterDialog(); + containerRegistry.RegisterDialog(); containerRegistry.RegisterDialog(); containerRegistry.RegisterDialog(); containerRegistry.RegisterDialog(); diff --git a/CapMachine.Wpf/CanDrive/ZlgCan/ZLGAPI.cs b/CapMachine.Wpf/CanDrive/ZlgCan/ZLGAPI.cs index 7500067..967cd09 100644 --- a/CapMachine.Wpf/CanDrive/ZlgCan/ZLGAPI.cs +++ b/CapMachine.Wpf/CanDrive/ZlgCan/ZLGAPI.cs @@ -88,10 +88,10 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan #endregion #region 函数 - [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)] + [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall, SetLastError = true)] public static extern IntPtr ZCAN_OpenDevice(uint device_type, uint device_index, uint reserved); - [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)] + [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall, SetLastError = true)] public static extern IntPtr ZCAN_OpenDeviceByName(uint device_type, string name); [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)] diff --git a/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanFd200uDriver.cs b/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanFd200uDriver.cs index 89ee819..5a571ad 100644 --- a/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanFd200uDriver.cs +++ b/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanFd200uDriver.cs @@ -2,8 +2,10 @@ using CapMachine.Wpf.CanDrive; using CapMachine.Wpf.Services; using Prism.Mvvm; using System; +using System.ComponentModel; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -21,6 +23,9 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan private readonly object _sync = new object(); private readonly ILogService _log; + private static IntPtr _preloadedZlgcanHandle = IntPtr.Zero; + private static string? _preloadedZlgcanError; + private IntPtr _deviceHandle = IntPtr.Zero; private readonly IntPtr[] _canChannelHandles = new IntPtr[2]; private IntPtr _linChannelHandle = IntPtr.Zero; @@ -189,16 +194,189 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan EnsureNativeDllExists("zlgcan.dll"); - _deviceHandle = ZLGCAN.ZCAN_OpenDevice(ZLGCAN.ZCAN_USBCANFD_200U, deviceIndex, 0); + var deviceType = ZLGCAN.ZCAN_USBCANFD_200U; + + try + { + _deviceHandle = ZLGCAN.ZCAN_OpenDevice(deviceType, deviceIndex, 0); + } + catch (Exception ex) + { + var baseDir = AppContext.BaseDirectory; + var dllFullPath = Path.Combine(baseDir, "zlgcan.dll"); + var msg = $"ZCAN_OpenDevice 调用异常。deviceType={deviceType}, deviceIndex={deviceIndex}, " + + $"Is64BitProcess={Environment.Is64BitProcess}, ProcessArch={RuntimeInformation.ProcessArchitecture}, " + + $"DllPath={dllFullPath}, BaseDir={baseDir}。异常:{ex.Message}"; + _log.Error(msg); + throw new InvalidOperationException(msg, ex); + } + if (_deviceHandle == IntPtr.Zero) { - throw new InvalidOperationException("ZCAN_OpenDevice 失败,请确认驱动/设备连接/程序位数与 DLL 匹配。"); + var lastError = Marshal.GetLastWin32Error(); + var lastErrorMsg = string.Empty; + try + { + lastErrorMsg = new Win32Exception(lastError).Message; + } + catch + { + } + + var baseDir = AppContext.BaseDirectory; + var dllFullPath = Path.Combine(baseDir, "zlgcan.dll"); + var loadedModulePath = GetLoadedModulePath("zlgcan.dll") ?? string.Empty; + var depDiag = string.Join(" | ", new[] + { + BuildDllDiag(baseDir, "zlgcan.dll"), + BuildDllDiag(baseDir, "USB2XXX.dll"), + BuildDllDiag(baseDir, "libusb-1.0.dll"), + BuildDllDiag(baseDir, "zdbc.dll"), + BuildDllDiag(baseDir, "kerneldlls\\USBCANFD.dll"), + BuildDllDiag(baseDir, "kerneldlls\\CANDevCore.dll"), + BuildDllDiag(baseDir, "kerneldlls\\CANDevice.dll"), + BuildDllDiag(baseDir, "kerneldlls\\CANFDCOM.dll"), + BuildDllDiag(baseDir, "kerneldlls\\CANFDNET.dll"), + BuildDllDiag(baseDir, "kerneldlls\\ZPSCANFD.dll"), + BuildDllDiag(baseDir, "kerneldlls\\USBCANFD800U.dll"), + BuildDllDiag(baseDir, "kerneldlls\\USBCAN.dll"), + BuildDllDiag(baseDir, "kerneldlls\\usbcan.dll"), + }.Where(s => !string.IsNullOrWhiteSpace(s))); + string dllVer = string.Empty; + try + { + if (File.Exists(dllFullPath)) + { + var vi = FileVersionInfo.GetVersionInfo(dllFullPath); + dllVer = vi?.FileVersion ?? string.Empty; + } + } + catch + { + } + + var msg = $"ZCAN_OpenDevice 失败。deviceType={deviceType}, deviceIndex={deviceIndex}。" + + $"Win32LastError={lastError}(0x{lastError:X}){(string.IsNullOrWhiteSpace(lastErrorMsg) ? string.Empty : $"({lastErrorMsg})")}。" + + $"Is64BitProcess={Environment.Is64BitProcess}, ProcessArch={RuntimeInformation.ProcessArchitecture}。" + + $"DllPath={dllFullPath}, DllVersion={dllVer}, LoadedModulePath={loadedModulePath}, BaseDir={baseDir}。" + + $"PreloadError={_preloadedZlgcanError ?? string.Empty}。" + + $"DirDllDiag={depDiag}。" + + "请确认:1) 已安装ZLG驱动;2) 设备已连接且未被其它程序占用;3) 程序位数与 zlgcan.dll 匹配;4) 设备类型/索引正确。"; + + _log.Error(msg); + throw new InvalidOperationException(msg); } OpenState = true; } } + /// + /// 预加载指定路径的原生 DLL,尽量避免被 PATH 中的同名 DLL 干扰。 + /// + /// DLL 完整路径。 + private static void TryPreloadNativeDll(string dllFullPath) + { + try + { + if (_preloadedZlgcanHandle != IntPtr.Zero) + { + return; + } + + if (!string.IsNullOrWhiteSpace(_preloadedZlgcanError)) + { + return; + } + + _preloadedZlgcanHandle = NativeLibrary.Load(dllFullPath); + } + catch (Exception ex) + { + _preloadedZlgcanError = $"{ex.GetType().Name}: {ex.Message}"; + } + } + + /// + /// 构造输出目录下某个 DLL 的诊断信息(架构/大小/版本)。 + /// + /// 输出目录。 + /// DLL 文件名。 + /// 诊断信息字符串;不存在返回 "xxx:not_found";异常返回 null。 + private static string? BuildDllDiag(string baseDir, string fileName) + { + try + { + if (string.IsNullOrWhiteSpace(baseDir) || string.IsNullOrWhiteSpace(fileName)) + { + return null; + } + + var full = Path.Combine(baseDir, fileName); + if (!File.Exists(full)) + { + return $"{fileName}:not_found"; + } + + var arch = TryGetPeArchitecture(full) ?? string.Empty; + long size = 0; + try + { + size = new FileInfo(full).Length; + } + catch + { + } + + var ver = string.Empty; + try + { + var vi = FileVersionInfo.GetVersionInfo(full); + ver = vi?.FileVersion ?? string.Empty; + } + catch + { + } + + return $"{fileName}:arch={arch},size={size},ver={ver}"; + } + catch + { + return null; + } + } + + /// + /// 获取当前进程中已加载模块的完整路径。 + /// + /// 模块文件名(如 zlgcan.dll)。 + /// 完整路径;获取失败返回 null。 + private static string? GetLoadedModulePath(string moduleFileName) + { + try + { + if (string.IsNullOrWhiteSpace(moduleFileName)) + { + return null; + } + + using var p = Process.GetCurrentProcess(); + foreach (ProcessModule m in p.Modules) + { + if (string.Equals(m.ModuleName, moduleFileName, StringComparison.OrdinalIgnoreCase)) + { + return m.FileName; + } + } + + return null; + } + catch + { + return null; + } + } + /// /// 打开设备并初始化 CANFD 通道。 /// @@ -1286,6 +1464,78 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan { throw new FileNotFoundException($"未找到 {dllName},请将其复制到程序输出目录:{baseDir}", full); } + + TryPreloadNativeDll(full); + if (_preloadedZlgcanHandle == IntPtr.Zero && !string.IsNullOrWhiteSpace(_preloadedZlgcanError)) + { + throw new InvalidOperationException($"预加载 {dllName} 失败:{_preloadedZlgcanError}。DLL 路径:{full}"); + } + + var dllArch = TryGetPeArchitecture(full); + if (!string.IsNullOrWhiteSpace(dllArch)) + { + var procArch = Environment.Is64BitProcess ? "x64" : "x86"; + if (Environment.Is64BitProcess && !string.Equals(dllArch, "x64", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"{dllName} 位数不匹配:当前进程为 {procArch},但 DLL 架构为 {dllArch}。请替换为 x64 版本 DLL,或将程序改为 x86 运行。DLL 路径:{full}"); + } + if (!Environment.Is64BitProcess && !string.Equals(dllArch, "x86", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"{dllName} 位数不匹配:当前进程为 {procArch},但 DLL 架构为 {dllArch}。请替换为 x86 版本 DLL,或将程序改为 x64 运行。DLL 路径:{full}"); + } + } + } + + /// + /// 尝试读取 PE 头判断 DLL 架构。 + /// + /// DLL 完整路径。 + /// 返回 "x86"/"x64"/"arm64"/"unknown(...)";读取失败返回 null。 + private static string? TryGetPeArchitecture(string dllFullPath) + { + try + { + using var fs = new FileStream(dllFullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var br = new BinaryReader(fs); + + if (fs.Length < 0x40) + { + return null; + } + + var mz = br.ReadUInt16(); + if (mz != 0x5A4D) + { + return null; + } + + fs.Position = 0x3C; + var peOffset = br.ReadInt32(); + if (peOffset <= 0 || peOffset > fs.Length - 6) + { + return null; + } + + fs.Position = peOffset; + var peSig = br.ReadUInt32(); + if (peSig != 0x00004550) + { + return null; + } + + var machine = br.ReadUInt16(); + return machine switch + { + 0x014c => "x86", + 0x8664 => "x64", + 0xAA64 => "arm64", + _ => $"unknown(0x{machine:X})" + }; + } + catch + { + return null; + } } private static Dictionary BuildDbcModelIndex(IEnumerable models) diff --git a/CapMachine.Wpf/CanDrive/ZlgCan/ZlgDbcDatabase.cs b/CapMachine.Wpf/CanDrive/ZlgCan/ZlgDbcDatabase.cs index 7a063c1..c573902 100644 --- a/CapMachine.Wpf/CanDrive/ZlgCan/ZlgDbcDatabase.cs +++ b/CapMachine.Wpf/CanDrive/ZlgCan/ZlgDbcDatabase.cs @@ -79,6 +79,33 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan throw new FileNotFoundException("DBC 文件不存在。", dbcPath); } + var fileSize = 0L; + try + { + fileSize = new FileInfo(dbcPath).Length; + } + catch + { + } + + var fullPath = dbcPath; + try + { + fullPath = Path.GetFullPath(dbcPath); + } + catch + { + } + + var shortPath = TryGetShortPath(fullPath); + var candidatePaths = new List(); + if (!string.IsNullOrWhiteSpace(fullPath)) candidatePaths.Add(fullPath); + if (!string.IsNullOrWhiteSpace(shortPath) + && !string.Equals(shortPath, fullPath, StringComparison.OrdinalIgnoreCase)) + { + candidatePaths.Add(shortPath); + } + lock (_sync) { if (_dbcHandle == 0 || _dbcHandle == ZDBC.INVALID_DBC_HANDLE) @@ -91,26 +118,42 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan } } - var fileInfo = new ZDBC.FileInfo + var loadOk = false; + foreach (var path in candidatePaths) { - strFilePath = BuildFixedPathBytes(dbcPath, ZDBC._MAX_FILE_PATH_ + 1), - type = protocolType, - merge = (byte)(merge ? 1 : 0) - }; - - IntPtr pFile = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZDBC.FileInfo))); - try - { - Marshal.StructureToPtr(fileInfo, pFile, false); - var ok = ZDBC.ZDBC_LoadFile(_dbcHandle, pFile); - if (!ok) + var fileInfo = new ZDBC.FileInfo { - throw new InvalidOperationException("ZDBC_LoadFile 失败,请检查 DBC 文件格式。"); + strFilePath = BuildFixedPathBytes(path, ZDBC._MAX_FILE_PATH_ + 1), + type = protocolType, + merge = (byte)(merge ? 1 : 0) + }; + + IntPtr pFile = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZDBC.FileInfo))); + try + { + Marshal.StructureToPtr(fileInfo, pFile, false); + loadOk = ZDBC.ZDBC_LoadFile(_dbcHandle, pFile); + if (loadOk) + { + break; + } + } + finally + { + Marshal.FreeHGlobal(pFile); } } - finally + + if (!loadOk) { - Marshal.FreeHGlobal(pFile); + // fallback:直接按内容加载,绕开路径编码问题;同时尝试多种编码以提升兼容性。 + loadOk = TryLoadContentFallback_NoLock(fullPath, merge); + } + + if (!loadOk) + { + var msg = $"ZDBC_LoadFile 失败。DbcPath={fullPath},ShortPath={shortPath ?? string.Empty},Size={fileSize}。请检查 DBC 文件格式,或将 DBC 放到纯英文路径后重试。"; + throw new InvalidOperationException(msg); } RefreshMessageCache_NoLock(); @@ -542,13 +585,128 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan private static byte[] BuildFixedPathBytes(string path, int fixedLen) { - var bytes = Encoding.ASCII.GetBytes(path); + var bytes = Encoding.Default.GetBytes(path); var dst = new byte[fixedLen]; Array.Clear(dst, 0, dst.Length); Array.Copy(bytes, 0, dst, 0, Math.Min(bytes.Length, fixedLen - 1)); return dst; } + /// + /// 当 ZDBC_LoadFile 失败时的兜底加载策略:读取文件内容并调用 ZDBC_LoadContent。 + /// + /// DBC 完整路径。 + /// 是否合并。 + /// 加载成功返回 true。 + private bool TryLoadContentFallback_NoLock(string fullPath, bool merge) + { + // zdbc.dll 入口为 ANSI 字符串,因此这里优先使用 ASCII(将非 ASCII 字符替换为 ?), + // 避免中文注释/特殊字符导致原生库解析失败;同时需要兼容 UTF-16 BOM,否则用 ASCII 读会出现大量 \0 造成内容截断。 + + var encodings = new List(); + + try + { + var bytes = File.ReadAllBytes(fullPath); + if (bytes.Length >= 2) + { + // UTF-16 LE BOM: FF FE + if (bytes[0] == 0xFF && bytes[1] == 0xFE) + { + encodings.Add(Encoding.Unicode); + } + // UTF-16 BE BOM: FE FF + else if (bytes[0] == 0xFE && bytes[1] == 0xFF) + { + encodings.Add(Encoding.BigEndianUnicode); + } + } + + if (bytes.Length >= 3) + { + // UTF-8 BOM: EF BB BF + if (bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) + { + encodings.Add(Encoding.UTF8); + } + } + } + catch + { + } + + // 常规兜底编码序列 + encodings.Add(new UTF8Encoding(false, true)); + encodings.Add(Encoding.UTF8); + encodings.Add(Encoding.Default); + encodings.Add(Encoding.ASCII); + + foreach (var enc in encodings.Distinct()) + { + IntPtr pContent = IntPtr.Zero; + try + { + var content = File.ReadAllText(fullPath, enc); + pContent = Marshal.StringToHGlobalAnsi(content); + var ok = ZDBC.ZDBC_LoadContent(_dbcHandle, pContent, merge ? 1u : 0u); + if (ok) + { + return true; + } + } + catch + { + } + finally + { + if (pContent != IntPtr.Zero) + { + Marshal.FreeHGlobal(pContent); + } + } + } + + return false; + } + + /// + /// 获取 Windows 8.3 短路径。 + /// + /// 长路径。 + /// 短路径缓冲。 + /// 缓冲区大小。 + /// 返回写入的字符数量;0 表示失败。 + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern uint GetShortPathName(string lpszLongPath, StringBuilder lpszShortPath, uint cchBuffer); + + /// + /// 尝试将长路径转换为短路径(8.3),用于兼容部分原生库对中文/特殊字符路径处理不完整的问题。 + /// + /// 长路径。 + /// 短路径;失败返回 null。 + private static string? TryGetShortPath(string fullPath) + { + try + { + if (string.IsNullOrWhiteSpace(fullPath)) + { + return null; + } + + var sb = new StringBuilder(1024); + var ret = GetShortPathName(fullPath, sb, (uint)sb.Capacity); + if (ret == 0) + { + return null; + } + return sb.ToString(); + } + catch + { + return null; + } + } + private static string ByteArrayToAsciiString(byte[]? bytes) { if (bytes == null || bytes.Length == 0) return string.Empty; @@ -556,7 +714,7 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan int len = Array.IndexOf(bytes, (byte)0); if (len < 0) len = bytes.Length; - return Encoding.ASCII.GetString(bytes, 0, len).Trim(); + return Encoding.Default.GetString(bytes, 0, len).Trim(); } private void ThrowIfDisposed() diff --git a/CapMachine.Wpf/Services/ZlgCanDriveService.cs b/CapMachine.Wpf/Services/ZlgCanDriveService.cs index 2dce0d0..4dda130 100644 --- a/CapMachine.Wpf/Services/ZlgCanDriveService.cs +++ b/CapMachine.Wpf/Services/ZlgCanDriveService.cs @@ -1,12 +1,15 @@ using CapMachine.Model.CANLIN; using CapMachine.Wpf.CanDrive; using CapMachine.Wpf.CanDrive.ZlgCan; +using CapMachine.Wpf.Dtos; using ImTools; using Prism.Mvvm; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace CapMachine.Wpf.Services { @@ -109,6 +112,12 @@ namespace CapMachine.Wpf.Services /// public List CmdData { get; } = new List(); + private readonly object _scheduleLock = new object(); + private List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> _scheduleItems = new List<(string, int, int, int)>(); + private CancellationTokenSource? _scheduleCts; + private Task? _scheduleTask; + private bool _scheduleUseConfigItems; + private CanCmdData? SpeedCanCmdData { get; set; } private uint _deviceIndex = 0; @@ -224,6 +233,12 @@ namespace CapMachine.Wpf.Services { try { + // Close 语义:关闭时必须停止循环发送与循环接收。 + // - 循环发送:停止软件调度,并关闭事件驱动发送标志。 + // - 循环接收:Driver.Close 内部会 StopReceiveLoop。 + StopSchedule(); + IsCycleSend = false; + Driver.Close(); } finally @@ -233,6 +248,178 @@ namespace CapMachine.Wpf.Services } } + public void SetScheduleConfigs(IEnumerable configs) + { + var list = configs?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List(); + lock (_scheduleLock) + { + _scheduleItems = list + .Select(a => (a.MsgName!.Trim(), Math.Max(1, a.Cycle), a.OrderSend, a.SchTabIndex)) + .ToList(); + } + } + + public void SetScheduleConfigs(IEnumerable configs) + { + var list = configs?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List(); + lock (_scheduleLock) + { + _scheduleItems = list + .Select(a => (a.MsgName!.Trim(), Math.Max(1, a.Cycle), a.OrderSend, a.SchTabIndex)) + .ToList(); + } + } + + public void StartSchedule() + { + if (!OpenState) + { + throw new InvalidOperationException("设备未连接,无法启动调度表。"); + } + + List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> items; + lock (_scheduleLock) + { + items = _scheduleItems.ToList(); + } + + if (items.Count == 0) + { + throw new InvalidOperationException("调度表为空,无法启动调度表。"); + } + + _scheduleUseConfigItems = true; + StartSoftwareScheduler(items); + } + + public void StartPrecisionCycleSend(int cycleMs) + { + if (!OpenState) + { + throw new InvalidOperationException("设备未连接,无法启动循环发送。"); + } + + var ms = Math.Max(1, cycleMs); + var msgNames = CmdData.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).Select(a => a.MsgName!).Distinct(StringComparer.Ordinal).ToList(); + if (msgNames.Count == 0) + { + throw new InvalidOperationException("CmdData 为空,无法启动循环发送。"); + } + + var items = msgNames.Select(n => (n, ms, 1, 0)).ToList(); + _scheduleUseConfigItems = false; + StartSoftwareScheduler(items); + } + + public void StopSchedule() + { + CancellationTokenSource? cts; + Task? task; + lock (_scheduleLock) + { + cts = _scheduleCts; + task = _scheduleTask; + _scheduleCts = null; + _scheduleTask = null; + } + + try + { + cts?.Cancel(); + if (task != null) + { + task.Wait(TimeSpan.FromSeconds(2)); + } + } + catch + { + } + finally + { + cts?.Dispose(); + } + } + + private void StartSoftwareScheduler(List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> items) + { + StopSchedule(); + + var cts = new CancellationTokenSource(); + lock (_scheduleLock) + { + _scheduleCts = cts; + } + + // 统一:软件调度开启后,等同“循环发送开启” + IsCycleSend = true; + + _scheduleTask = Task.Run(async () => + { + var token = cts.Token; + + // next due time for each msg + var now = DateTime.UtcNow; + var due = new Dictionary(StringComparer.Ordinal); + var cycle = new Dictionary(StringComparer.Ordinal); + var order = new Dictionary(StringComparer.Ordinal); + foreach (var it in items) + { + if (!due.ContainsKey(it.MsgName)) + { + due[it.MsgName] = now; + cycle[it.MsgName] = Math.Max(1, it.CycleMs); + order[it.MsgName] = it.OrderSend; + } + } + + while (!token.IsCancellationRequested) + { + if (!OpenState) + { + await Task.Delay(50, token).ConfigureAwait(false); + continue; + } + + var utcNow = DateTime.UtcNow; + var minDue = due.Values.Min(); + var delay = minDue - utcNow; + if (delay > TimeSpan.Zero) + { + var ms = (int)Math.Min(delay.TotalMilliseconds, 200); + await Task.Delay(ms, token).ConfigureAwait(false); + continue; + } + + // due messages + var ready = due.Where(kv => kv.Value <= utcNow).Select(kv => kv.Key).ToList(); + if (ready.Count == 0) + { + await Task.Delay(1, token).ConfigureAwait(false); + continue; + } + + // 顺序/并行:这里只决定同一 tick 内的发送顺序(并行模式仍按字典序依次发) + ready.Sort(StringComparer.Ordinal); + foreach (var msg in ready) + { + if (token.IsCancellationRequested) break; + try + { + Driver.SendOneMsgByCmdData(msg, 0, Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD); + } + catch (Exception ex) + { + _log.Warn($"调度表发送失败:{msg},{ex.Message}"); + } + finally + { + due[msg] = DateTime.UtcNow.AddMilliseconds(cycle[msg]); + } + } + } + }, cts.Token); + } + /// /// 加载 DBC。 /// diff --git a/CapMachine.Wpf/Services/ZlgLinDriveService.cs b/CapMachine.Wpf/Services/ZlgLinDriveService.cs index 4b6818f..1773bcb 100644 --- a/CapMachine.Wpf/Services/ZlgLinDriveService.cs +++ b/CapMachine.Wpf/Services/ZlgLinDriveService.cs @@ -4,7 +4,10 @@ using Prism.Mvvm; using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; using System.Linq; +using System.Text; +using System.Text.RegularExpressions; namespace CapMachine.Wpf.Services { @@ -150,8 +153,165 @@ namespace CapMachine.Wpf.Services /// LDF 路径。 public ObservableCollection StartLdf(string path) { - _log.Warn("ZLG LIN 当前版本未接入 LDF 解析(项目内仅存在 USB2XXX.dll 的 LDFParser)。"); - throw new NotSupportedException("ZLG LIN 暂未支持 LDF 解析,请后续提供/确认 ZLG 的 LDF DLL 接口(如 zldf.dll)后再接入。"); + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException("LDF 路径为空", nameof(path)); + } + + if (!File.Exists(path)) + { + throw new FileNotFoundException($"LDF 文件不存在:{path}", path); + } + + try + { + var text = File.ReadAllText(path, Encoding.UTF8); + + // 去除单行注释,简化解析 + text = Regex.Replace(text, @"//.*?$", string.Empty, RegexOptions.Multiline); + + var models = ParseLdfFramesAndSignals(text); + + ListLinLdfModel.Clear(); + foreach (var item in models) + { + ListLinLdfModel.Add(item); + } + + LdfParserState = true; + return ListLinLdfModel; + } + catch (Exception ex) + { + _log.Error($"ZLG LIN 解析 LDF 失败:{ex.Message}"); + LdfParserState = false; + throw; + } + } + + private static List ParseLdfFramesAndSignals(string ldfText) + { + // 说明:此解析器只用于生成“帧-信号全集池”,不做位宽/缩放等语义解析。 + // 目标:尽可能容错地从 Frames 区域提取 FrameName 与其包含的 SignalName 列表。 + + var framesBlock = TryExtractNamedBlock(ldfText, "Frames"); + if (string.IsNullOrWhiteSpace(framesBlock)) + { + return new List(); + } + + var result = new List(); + var exists = new HashSet(StringComparer.Ordinal); + + // Frame 定义一般形式:FrameName : ... { ... } + // 这里以非贪婪匹配提取每个 Frame 的 body + var frameRegex = new Regex(@"(?s)(?[A-Za-z_][A-Za-z0-9_]*)\s*:\s*.*?\{(?.*?)\}\s*;?", RegexOptions.Compiled); + var sigRegex = new Regex(@"(?m)^\s*(?[A-Za-z_][A-Za-z0-9_]*)\s*[,;]", RegexOptions.Compiled); + + foreach (Match fm in frameRegex.Matches(framesBlock)) + { + var frameName = fm.Groups["name"].Value; + var body = fm.Groups["body"].Value; + if (string.IsNullOrWhiteSpace(frameName) || string.IsNullOrWhiteSpace(body)) + { + continue; + } + + foreach (Match sm in sigRegex.Matches(body)) + { + var sigName = sm.Groups["sig"].Value; + if (string.IsNullOrWhiteSpace(sigName)) + { + continue; + } + + // 排除明显的关键字(避免误采集) + if (IsReservedKeyword(sigName)) + { + continue; + } + + var key = $"{frameName}:{sigName}"; + if (!exists.Add(key)) + { + continue; + } + + result.Add(new LinLdfModel + { + MsgName = frameName, + SignalName = sigName, + Name = null, + SignalDesc = null, + SignalUnit = null, + IsSeletedInfo = 0, + }); + } + } + + return result; + } + + private static string? TryExtractNamedBlock(string text, string blockName) + { + // 提取形如:blockName { ... } 的块内容(不包含外层大括号)。 + // 基于括号深度扫描,避免正则在嵌套结构上失效。 + var idx = CultureInvariantIndexOf(text, blockName); + if (idx < 0) return null; + + var braceIdx = text.IndexOf('{', idx); + if (braceIdx < 0) return null; + + int depth = 0; + for (int i = braceIdx; i < text.Length; i++) + { + var ch = text[i]; + if (ch == '{') depth++; + else if (ch == '}') + { + depth--; + if (depth == 0) + { + return text.Substring(braceIdx + 1, i - braceIdx - 1); + } + } + } + + return null; + } + + private static int CultureInvariantIndexOf(string text, string value) + { + return text.IndexOf(value, StringComparison.OrdinalIgnoreCase); + } + + private static bool IsReservedKeyword(string token) + { + // LDF 常见关键字/区块名(用于降低误匹配概率) + switch (token) + { + case "Frames": + case "Signals": + case "Signal": + case "Nodes": + case "Master": + case "Slaves": + case "Diagnostic": + case "Diagnostics": + case "Checksum": + case "Event_triggered_frames": + case "Sporadic_frames": + case "Schedule_tables": + case "Node_attributes": + case "Node_composition": + case "LIN_protocol": + case "LIN_protocol_version": + case "LIN_speed": + case "Protocol_version": + return true; + default: + return false; + } } } } diff --git a/CapMachine.Wpf/ViewModels/DialogZlgCanLinRwConfigViewModel.cs b/CapMachine.Wpf/ViewModels/DialogZlgCanLinRwConfigViewModel.cs new file mode 100644 index 0000000..31f8ef5 --- /dev/null +++ b/CapMachine.Wpf/ViewModels/DialogZlgCanLinRwConfigViewModel.cs @@ -0,0 +1,753 @@ +using CapMachine.Core; +using CapMachine.Model.CANLIN; +using CapMachine.Wpf.Dtos; +using CapMachine.Wpf.Services; +using FreeSql; +using Prism.Commands; +using Prism.Services.Dialogs; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Windows; +using System.Windows.Data; + +namespace CapMachine.Wpf.ViewModels +{ + /// + /// ZLG CAN/LIN 读写配置三栏管理弹窗 ViewModel。 + /// 左侧:写入/读取配置;右侧:信号全集候选池;统一保存落库。 + /// + public class DialogZlgCanLinRwConfigViewModel : DialogViewModel + { + private readonly IFreeSql _freeSql; + private readonly ILogService _logService; + private readonly LogicRuleService _logicRuleService; + + private long _canLinConfigProId; + + /// + /// 构造函数。 + /// + /// FreeSql。 + /// 日志。 + /// 逻辑规则服务。 + public DialogZlgCanLinRwConfigViewModel(IFreeSql freeSql, ILogService logService, LogicRuleService logicRuleService) + { + _freeSql = freeSql; + _logService = logService; + _logicRuleService = logicRuleService; + + Title = "读写设置"; + + WriteConfigs = new ObservableCollection(); + ReadConfigs = new ObservableCollection(); + SignalCandidates = new ObservableCollection(); + SignalTree = new ObservableCollection(); + + SignalCandidatesView = CollectionViewSource.GetDefaultView(SignalCandidates); + SignalCandidatesView.Filter = FilterSignalCandidate; + + WriteNameCbxItems = new ObservableCollection() + { + new CbxItems(){ Key="转速",Text="转速"}, + new CbxItems(){ Key="功率限制",Text="功率限制"}, + new CbxItems(){ Key="使能",Text="使能"}, + new CbxItems(){ Key="Anti_Sleep",Text="Anti_Sleep"}, + new CbxItems(){ Key="PTC使能",Text="PTC使能"}, + new CbxItems(){ Key="PTC功率",Text="PTC功率"}, + new CbxItems(){ Key="PTC水流量",Text="PTC水流量"}, + new CbxItems(){ Key="PTC水温",Text="PTC水温"}, + }; + + ReadNameCbxItems = new ObservableCollection() + { + new CbxItems(){ Key="通讯Cmp转速",Text="通讯Cmp转速"}, + new CbxItems(){ Key="通讯Cmp母线电压",Text="通讯Cmp母线电压"}, + new CbxItems(){ Key="通讯Cmp母线电流",Text="通讯Cmp母线电流"}, + new CbxItems(){ Key="通讯Cmp逆变器温度",Text="通讯Cmp逆变器温度"}, + new CbxItems(){ Key="通讯Cmp相电流",Text="通讯Cmp相电流"}, + new CbxItems(){ Key="通讯Cmp功率",Text="通讯Cmp功率"}, + new CbxItems(){ Key="通讯Cmp芯片温度",Text="通讯Cmp芯片温度"}, + new CbxItems(){ Key="通讯PTC入水温度",Text="通讯PTC入水温度"}, + new CbxItems(){ Key="通讯PTC出水温度",Text="通讯PTC出水温度"}, + new CbxItems(){ Key="通讯PTC峰值电流",Text="通讯PTC峰值电流"}, + new CbxItems(){ Key="通讯PTC母线电流",Text="通讯PTC母线电流"}, + new CbxItems(){ Key="通讯PTC膜温",Text="通讯PTC膜温"}, + new CbxItems(){ Key="通讯PTC模块温度",Text="通讯PTC模块温度"}, + }; + + IsEditable = true; + } + + /// + /// 是否允许编辑(由调用方根据 Active/打开状态决定)。 + /// + public bool IsEditable { get; private set; } + + /// + /// 逻辑规则集合(下拉框 ItemsSource)。 + /// + public IReadOnlyList LogicRuleDtos => _logicRuleService.LogicRuleDtos; + + /// + /// 写入配置“名称”下拉框集合(参考 CANConfigViewModel)。 + /// + public ObservableCollection WriteNameCbxItems { get; private set; } + + /// + /// 读取配置“名称”下拉框集合(参考 CANConfigViewModel)。 + /// + public ObservableCollection ReadNameCbxItems { get; private set; } + + /// + /// 写入配置集合。 + /// + public ObservableCollection WriteConfigs { get; private set; } + + /// + /// 读取配置集合。 + /// + public ObservableCollection ReadConfigs { get; private set; } + + /// + /// 信号候选集合(右侧池)。 + /// + public ObservableCollection SignalCandidates { get; private set; } + + /// + /// 信号树(按帧分组)。 + /// + public ObservableCollection SignalTree { get; private set; } + + /// + /// 候选信号视图(含过滤)。 + /// + public ICollectionView SignalCandidatesView { get; private set; } + + private string? _signalFilterText; + + /// + /// 信号过滤文本(按 MsgName/SignalName/Name/Desc 匹配)。 + /// + public string? SignalFilterText + { + get { return _signalFilterText; } + set + { + _signalFilterText = value; + RaisePropertyChanged(); + SignalCandidatesView.Refresh(); + RebuildSignalTree(); + } + } + + /// + /// 当前选中的候选信号。 + /// + public SignalCandidate? SelectedSignalCandidate { get; set; } + + /// + /// 当前选中的写入配置行。 + /// + public CanLinRWConfigDto? SelectedWriteConfig { get; set; } + + /// + /// 当前选中的读取配置行。 + /// + public CanLinRWConfigDto? SelectedReadConfig { get; set; } + + private DelegateCommand? _signalTreeSelectionChangedCmd; + + /// + /// 右侧信号树选中变化(仅当选中叶子节点时回写 SelectedSignalCandidate)。 + /// + public DelegateCommand SignalTreeSelectionChangedCmd => + _signalTreeSelectionChangedCmd ??= new DelegateCommand(SignalTreeSelectionChangedCmdMethod); + + private void SignalTreeSelectionChangedCmdMethod(object par) + { + if (par is SignalCandidate leaf) + { + SelectedSignalCandidate = leaf; + RaisePropertyChanged(nameof(SelectedSignalCandidate)); + return; + } + + if (par is SignalFrameNode) + { + // 选中父节点时不变更 SelectedSignalCandidate + return; + } + } + + private DelegateCommand? _addToWriteCmd; + + /// + /// 将右侧选中信号添加到写入配置。 + /// + public DelegateCommand AddToWriteCmd => _addToWriteCmd ??= new DelegateCommand(AddToWriteCmdMethod); + + private void AddToWriteCmdMethod() + { + if (!IsEditable) + { + MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (SelectedSignalCandidate == null) + { + MessageBox.Show("请先在右侧信号集合中选中一条", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (string.IsNullOrWhiteSpace(SelectedSignalCandidate.SignalName) || string.IsNullOrWhiteSpace(SelectedSignalCandidate.MsgName)) + { + MessageBox.Show("选中的信号数据不完整(MsgName/SignalName 为空)", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (WriteConfigs.Any(a => string.Equals(a.SignalName, SelectedSignalCandidate.SignalName, StringComparison.Ordinal) && a.RWInfo == RW.Write)) + { + MessageBox.Show("该信号已在写入配置中,无需重复添加", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (ReadConfigs.Any(a => string.Equals(a.SignalName, SelectedSignalCandidate.SignalName, StringComparison.Ordinal) && a.RWInfo == RW.Read)) + { + MessageBox.Show("该信号已在读取配置中,同一个信号不允许同时配置为写入与读取", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + WriteConfigs.Add(new CanLinRWConfigDto + { + Id = 0, + RWInfo = RW.Write, + Name = string.IsNullOrWhiteSpace(SelectedSignalCandidate.Name) ? SelectedSignalCandidate.SignalName : SelectedSignalCandidate.Name, + MsgFrameName = SelectedSignalCandidate.MsgName, + SignalName = SelectedSignalCandidate.SignalName, + DefautValue = "0", + LogicRuleId = 0, + }); + + RebuildSignalTree(); + } + + private DelegateCommand? _addToReadCmd; + + /// + /// 将右侧选中信号添加到读取配置。 + /// + public DelegateCommand AddToReadCmd => _addToReadCmd ??= new DelegateCommand(AddToReadCmdMethod); + + private void AddToReadCmdMethod() + { + if (!IsEditable) + { + MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (SelectedSignalCandidate == null) + { + MessageBox.Show("请先在右侧信号集合中选中一条", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (string.IsNullOrWhiteSpace(SelectedSignalCandidate.SignalName) || string.IsNullOrWhiteSpace(SelectedSignalCandidate.MsgName)) + { + MessageBox.Show("选中的信号数据不完整(MsgName/SignalName 为空)", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (ReadConfigs.Any(a => string.Equals(a.SignalName, SelectedSignalCandidate.SignalName, StringComparison.Ordinal) && a.RWInfo == RW.Read)) + { + MessageBox.Show("该信号已在读取配置中,无需重复添加", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (WriteConfigs.Any(a => string.Equals(a.SignalName, SelectedSignalCandidate.SignalName, StringComparison.Ordinal) && a.RWInfo == RW.Write)) + { + MessageBox.Show("该信号已在写入配置中,同一个信号不允许同时配置为写入与读取", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + ReadConfigs.Add(new CanLinRWConfigDto + { + Id = 0, + RWInfo = RW.Read, + Name = string.IsNullOrWhiteSpace(SelectedSignalCandidate.Name) ? SelectedSignalCandidate.SignalName : SelectedSignalCandidate.Name, + MsgFrameName = SelectedSignalCandidate.MsgName, + SignalName = SelectedSignalCandidate.SignalName, + DefautValue = "0", + LogicRuleId = 0, + }); + + RebuildSignalTree(); + } + + private DelegateCommand? _removeWriteCmd; + + /// + /// 从写入配置移除当前选中行。 + /// + public DelegateCommand RemoveWriteCmd => _removeWriteCmd ??= new DelegateCommand(RemoveWriteCmdMethod); + + private void RemoveWriteCmdMethod() + { + if (!IsEditable) + { + MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (SelectedWriteConfig == null) + { + MessageBox.Show("请先选中写入列表中的一行", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + WriteConfigs.Remove(SelectedWriteConfig); + SelectedWriteConfig = null; + RaisePropertyChanged(nameof(SelectedWriteConfig)); + + RebuildSignalTree(); + } + + private DelegateCommand? _removeReadCmd; + + /// + /// 从读取配置移除当前选中行。 + /// + public DelegateCommand RemoveReadCmd => _removeReadCmd ??= new DelegateCommand(RemoveReadCmdMethod); + + private void RemoveReadCmdMethod() + { + if (!IsEditable) + { + MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (SelectedReadConfig == null) + { + MessageBox.Show("请先选中读取列表中的一行", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + ReadConfigs.Remove(SelectedReadConfig); + SelectedReadConfig = null; + RaisePropertyChanged(nameof(SelectedReadConfig)); + + RebuildSignalTree(); + } + + private DelegateCommand? _saveCmd; + + /// + /// 保存并落库。 + /// + public DelegateCommand SaveCmd => _saveCmd ??= new DelegateCommand(SaveCmdMethod); + + private void SaveCmdMethod() + { + if (!IsEditable) + { + MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (_canLinConfigProId <= 0) + { + MessageBox.Show("配置程序ID无效,无法保存", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + try + { + PersistRwConfigs(); + + var pars = new DialogParameters + { + { "Saved", true } + }; + RaiseRequestClose(new DialogResult(ButtonResult.OK, pars)); + } + catch (Exception ex) + { + _logService.Error($"ZLG 读写设置保存失败:{ex}"); + MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private DelegateCommand? _cancelCmd; + + /// + /// 取消。 + /// + public DelegateCommand CancelCmd => _cancelCmd ??= new DelegateCommand(CancelCmdMethod); + + private void CancelCmdMethod() + { + RaiseRequestClose(new DialogResult(ButtonResult.Cancel)); + } + + /// + /// 弹窗打开时接收参数。 + /// + /// 参数。 + public override void OnDialogOpened(IDialogParameters parameters) + { + _canLinConfigProId = parameters.GetValue("CanLinConfigProId"); + + IsEditable = parameters.ContainsKey("IsEditable") ? parameters.GetValue("IsEditable") : true; + RaisePropertyChanged(nameof(IsEditable)); + + if (parameters.ContainsKey("WriteConfigs")) + { + var list = parameters.GetValue>("WriteConfigs") ?? new ObservableCollection(); + WriteConfigs = list; + RaisePropertyChanged(nameof(WriteConfigs)); + } + + if (parameters.ContainsKey("ReadConfigs")) + { + var list = parameters.GetValue>("ReadConfigs") ?? new ObservableCollection(); + ReadConfigs = list; + RaisePropertyChanged(nameof(ReadConfigs)); + } + + if (parameters.ContainsKey("SignalCandidates")) + { + var list = parameters.GetValue>("SignalCandidates") ?? new ObservableCollection(); + SignalCandidates = list; + RaisePropertyChanged(nameof(SignalCandidates)); + + SignalCandidatesView = CollectionViewSource.GetDefaultView(SignalCandidates); + SignalCandidatesView.Filter = FilterSignalCandidate; + RaisePropertyChanged(nameof(SignalCandidatesView)); + } + + RebuildSignalTree(); + + if (parameters.ContainsKey("Title")) + { + Title = parameters.GetValue("Title") ?? Title; + } + } + + private bool FilterSignalCandidate(object obj) + { + if (obj is not SignalCandidate item) + { + return false; + } + + if (string.IsNullOrWhiteSpace(SignalFilterText)) + { + return true; + } + + var key = SignalFilterText.Trim(); + + return ContainsIgnoreCase(item.MsgName, key) + || ContainsIgnoreCase(item.SignalName, key) + || ContainsIgnoreCase(item.Name, key) + || ContainsIgnoreCase(item.Desc, key); + } + + private void RebuildSignalTree() + { + // 依据过滤条件 + 全量候选池生成树。 + // 树数据源独立于 ICollectionView,避免 TreeView 过滤复杂度。 + var filtered = SignalCandidates + .Where(a => FilterSignalCandidate(a)) + .ToList(); + + var groups = filtered + .GroupBy(a => string.IsNullOrWhiteSpace(a.MsgName) ? "(未命名帧)" : a.MsgName!.Trim(), StringComparer.Ordinal) + .OrderBy(a => a.Key, StringComparer.Ordinal) + .ToList(); + + SignalTree.Clear(); + foreach (var g in groups) + { + var signalNodes = g + .OrderBy(a => a.SignalName ?? string.Empty, StringComparer.Ordinal) + .ThenBy(a => a.Name ?? string.Empty, StringComparer.Ordinal) + .Select(a => new SignalCandidate + { + MsgName = a.MsgName, + SignalName = a.SignalName, + Name = a.Name, + Desc = a.Desc, + AddedInfo = ComputeAddedInfo(a), + }) + .ToList(); + + var node = new SignalFrameNode + { + FrameName = g.Key, + Signals = new ObservableCollection(signalNodes) + }; + SignalTree.Add(node); + } + + RaisePropertyChanged(nameof(SignalTree)); + } + + /// + /// 计算候选信号是否已被添加到写入/读取。 + /// 0=未添加,1=已添加到写入,2=已添加到读取,3=同时存在于写入与读取。 + /// + /// 候选信号。 + /// AddedInfo 标志值。 + private int ComputeAddedInfo(SignalCandidate candidate) + { + if (candidate == null || string.IsNullOrWhiteSpace(candidate.SignalName)) + { + return 0; + } + + var signal = candidate.SignalName; + var inWrite = WriteConfigs.Any(a => a.RWInfo == RW.Write && string.Equals(a.SignalName, signal, StringComparison.Ordinal)); + var inRead = ReadConfigs.Any(a => a.RWInfo == RW.Read && string.Equals(a.SignalName, signal, StringComparison.Ordinal)); + + if (inWrite && inRead) return 3; + if (inWrite) return 1; + if (inRead) return 2; + return 0; + } + + private static bool ContainsIgnoreCase(string? src, string key) + { + if (string.IsNullOrEmpty(src)) return false; + return src.IndexOf(key, StringComparison.OrdinalIgnoreCase) >= 0; + } + + private void PersistRwConfigs() + { + // 互斥约束:同一 SignalName 不允许同时出现在写入与读取 + EnsureNoWriteReadConflict(); + + // 规范化 DTO(空值/默认值防御) + NormalizeRwConfigs(WriteConfigs, RW.Write); + NormalizeRwConfigs(ReadConfigs, RW.Read); + + // 防重复:同一 SignalName 在同一 RW 列表中只允许一条 + EnsureNoDuplicateSignal(WriteConfigs, RW.Write); + EnsureNoDuplicateSignal(ReadConfigs, RW.Read); + + var existing = _freeSql.Select() + .Where(a => a.CanLinConfigProId == _canLinConfigProId) + .Where(a => a.RWInfo == RW.Write || a.RWInfo == RW.Read) + .ToList(); + + var desiredWrite = WriteConfigs + .Where(a => !string.IsNullOrWhiteSpace(a.SignalName)) + .Select(a => new DesiredItem(RW.Write, a.SignalName!, a)) + .ToList(); + + var desiredRead = ReadConfigs + .Where(a => !string.IsNullOrWhiteSpace(a.SignalName)) + .Select(a => new DesiredItem(RW.Read, a.SignalName!, a)) + .ToList(); + + var desiredAll = desiredWrite.Concat(desiredRead).ToList(); + + var desiredKeySet = new HashSet(desiredAll.Select(a => BuildKey(a.Rw, a.SignalName)), StringComparer.Ordinal); + var existingByKey = existing.ToDictionary(a => BuildKey(a.RWInfo, a.SignalName ?? string.Empty), a => a, StringComparer.Ordinal); + + // 删除:DB 中存在,但目标集合里不存在 + foreach (var old in existing) + { + var key = BuildKey(old.RWInfo, old.SignalName ?? string.Empty); + if (!desiredKeySet.Contains(key)) + { + _freeSql.Delete(old.Id).ExecuteAffrows(); + } + } + + // Upsert:按 key(RWInfo + SignalName)更新或插入 + foreach (var item in desiredAll) + { + var key = BuildKey(item.Rw, item.SignalName); + if (existingByKey.TryGetValue(key, out var old)) + { + _freeSql.Update(old.Id) + .Set(a => a.Name, item.Dto.Name) + .Set(a => a.MsgFrameName, item.Dto.MsgFrameName) + .Set(a => a.SignalName, item.Dto.SignalName) + .Set(a => a.DefautValue, item.Dto.DefautValue) + .Set(a => a.LogicRuleId, item.Dto.LogicRuleId) + .ExecuteAffrows(); + } + else + { + _freeSql.Insert(new CanLinRWConfig + { + CanLinConfigProId = _canLinConfigProId, + RWInfo = item.Rw, + Name = item.Dto.Name, + MsgFrameName = item.Dto.MsgFrameName, + SignalName = item.Dto.SignalName, + DefautValue = item.Dto.DefautValue, + LogicRuleId = item.Dto.LogicRuleId, + }).ExecuteAffrows(); + } + } + } + + private static void NormalizeRwConfigs(IEnumerable list, RW rw) + { + foreach (var item in list) + { + item.RWInfo = rw; + if (string.IsNullOrWhiteSpace(item.SignalName)) + { + continue; + } + + if (string.IsNullOrWhiteSpace(item.Name)) + { + item.Name = item.SignalName; + } + + if (string.IsNullOrWhiteSpace(item.MsgFrameName)) + { + item.MsgFrameName = string.Empty; + } + + if (string.IsNullOrWhiteSpace(item.DefautValue)) + { + item.DefautValue = "0"; + } + + if (item.LogicRuleId < 0) + { + item.LogicRuleId = 0; + } + } + } + + private static void EnsureNoDuplicateSignal(IEnumerable list, RW rw) + { + var duplicates = list + .Where(a => !string.IsNullOrWhiteSpace(a.SignalName)) + .GroupBy(a => a.SignalName!, StringComparer.Ordinal) + .Where(g => g.Count() > 1) + .Select(g => g.Key) + .ToList(); + + if (duplicates.Count > 0) + { + throw new InvalidOperationException($"{rw} 配置中存在重复信号:{string.Join(",", duplicates)}"); + } + } + + private void EnsureNoWriteReadConflict() + { + var writeSet = new HashSet( + WriteConfigs + .Where(a => a.RWInfo == RW.Write) + .Select(a => a.SignalName) + .Where(a => !string.IsNullOrWhiteSpace(a)) + .Select(a => a!), + StringComparer.Ordinal); + + var readSet = new HashSet( + ReadConfigs + .Where(a => a.RWInfo == RW.Read) + .Select(a => a.SignalName) + .Where(a => !string.IsNullOrWhiteSpace(a)) + .Select(a => a!), + StringComparer.Ordinal); + + writeSet.IntersectWith(readSet); + if (writeSet.Count > 0) + { + throw new InvalidOperationException($"同一信号不允许同时配置为写入与读取,冲突信号:{string.Join(",", writeSet)}"); + } + } + + private static string BuildKey(RW rw, string signalName) + { + return $"{(int)rw}:{signalName}"; + } + + private readonly struct DesiredItem + { + /// + /// 读写类型。 + /// + public RW Rw { get; } + + /// + /// 信号名称。 + /// + public string SignalName { get; } + + /// + /// 原始 DTO。 + /// + public CanLinRWConfigDto Dto { get; } + + public DesiredItem(RW rw, string signalName, CanLinRWConfigDto dto) + { + Rw = rw; + SignalName = signalName; + Dto = dto; + } + } + + /// + /// 右侧信号候选项。 + /// + public class SignalCandidate + { + /// + /// 消息名称/帧名称。 + /// + public string? MsgName { get; set; } + + /// + /// 信号名称。 + /// + public string? SignalName { get; set; } + + /// + /// 配置名称(若解析层已有中文名则传入)。 + /// + public string? Name { get; set; } + + /// + /// 描述。 + /// + public string? Desc { get; set; } + + /// + /// 已添加标记。 + /// 0=未添加,1=已添加到写入,2=已添加到读取,3=同时存在于写入与读取。 + /// + public int AddedInfo { get; set; } + } + + /// + /// 帧节点。 + /// + public class SignalFrameNode + { + /// + /// 帧名。 + /// + public string FrameName { get; set; } = string.Empty; + + /// + /// 帧内信号集合。 + /// + public ObservableCollection Signals { get; set; } = new ObservableCollection(); + } + } +} diff --git a/CapMachine.Wpf/ViewModels/ZlgCanDriveConfigViewModel.cs b/CapMachine.Wpf/ViewModels/ZlgCanDriveConfigViewModel.cs index 2a2c66c..e975951 100644 --- a/CapMachine.Wpf/ViewModels/ZlgCanDriveConfigViewModel.cs +++ b/CapMachine.Wpf/ViewModels/ZlgCanDriveConfigViewModel.cs @@ -4,6 +4,7 @@ using CapMachine.Model.CANLIN; using CapMachine.Wpf.CanDrive; using CapMachine.Wpf.Dtos; using CapMachine.Wpf.Services; +using CapMachine.Wpf.Views; using ImTools; using Microsoft.Win32; using Prism.Commands; @@ -13,6 +14,7 @@ using Prism.Services.Dialogs; using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; using System.Linq; using System.Windows; using System.Windows.Controls; @@ -48,9 +50,151 @@ namespace CapMachine.Wpf.ViewModels Mapper = mapper; SelectedMode = ZlgCanMode.Can; + + ArbBaudRateCbxItems = new ObservableCollection() + { + new CbxItems(){ Key="10000",Text="10 Kbps"}, + new CbxItems(){ Key="20000",Text="20 Kbps"}, + new CbxItems(){ Key="33000",Text="33 Kbps"}, + new CbxItems(){ Key="50000",Text="50 Kbps"}, + new CbxItems(){ Key="83000",Text="83 Kbps"}, + new CbxItems(){ Key="100000",Text="100 Kbps"}, + new CbxItems(){ Key="125000",Text="125 Kbps"}, + new CbxItems(){ Key="150000",Text="150 Kbps"}, + new CbxItems(){ Key="200000",Text="200 Kbps"}, + new CbxItems(){ Key="250000",Text="250 Kbps"}, + new CbxItems(){ Key="300000",Text="300 Kbps"}, + new CbxItems(){ Key="400000",Text="400 Kbps"}, + new CbxItems(){ Key="500000",Text="500 Kbps"}, + new CbxItems(){ Key="666000",Text="666 Kbps"}, + new CbxItems(){ Key="800000",Text="800 Kbps"}, + new CbxItems(){ Key="1000000",Text="1.0 Mbps"}, + }; + + DataBaudRateCbxItems = new ObservableCollection() + { + new CbxItems(){ Key="100000",Text="100 Kbps"}, + new CbxItems(){ Key="125000",Text="125 Kbps"}, + new CbxItems(){ Key="200000",Text="200 Kbps"}, + new CbxItems(){ Key="250000",Text="250 Kbps"}, + new CbxItems(){ Key="400000",Text="400 Kbps"}, + new CbxItems(){ Key="500000",Text="500 Kbps"}, + new CbxItems(){ Key="666000",Text="666 Kbps"}, + new CbxItems(){ Key="800000",Text="800 Kbps"}, + new CbxItems(){ Key="1000000",Text="1.0 Mbps"}, + + new CbxItems(){ Key="1500000",Text="1.5 Mbps"}, + new CbxItems(){ Key="2000000",Text="2.0 Mbps"}, + new CbxItems(){ Key="3000000",Text="3.0 Mbps"}, + new CbxItems(){ Key="4000000",Text="4.0 Mbps"}, + new CbxItems(){ Key="5000000",Text="5.0 Mbps"}, + new CbxItems(){ Key="6700000",Text="6.7 Mbps"}, + new CbxItems(){ Key="8000000",Text="8.0 Mbps"}, + new CbxItems(){ Key="10000000",Text="10.0 Mbps"}, + }; + + InitWriteRuleCbx(); + InitLoadCanConfigPro(); } + private void InitWriteRuleCbx() + { + WriteRuleCbxItems = new ObservableCollection(); + foreach (var itemRule in LogicRuleService.LogicRuleDtos) + { + WriteRuleCbxItems.Add(new CbxItems() + { + Key = itemRule.Id.ToString(), + Text = itemRule.Name + }); + } + } + + private void OpenRwDialog() + { + try + { + if (SelectCanLinConfigPro == null) + { + MessageBox.Show("选中CAN配置名称后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + var writeClones = new ObservableCollection( + (ListWriteCanLinRWConfigDto ?? new ObservableCollection()) + .Select(CloneRwDto)); + + foreach (var item in writeClones) + { + item.RWInfo = RW.Write; + } + + var readClones = new ObservableCollection( + (ListReadCanLinRWConfigDto ?? new ObservableCollection()) + .Select(CloneRwDto)); + + foreach (var item in readClones) + { + item.RWInfo = RW.Read; + } + + var candidates = new ObservableCollection(); + if (ListCanDbcModel != null) + { + foreach (var sig in ListCanDbcModel) + { + candidates.Add(new DialogZlgCanLinRwConfigViewModel.SignalCandidate + { + MsgName = sig.MsgName, + SignalName = sig.SignalName, + Name = sig.Name, + Desc = sig.SignalDesc, + }); + } + } + + var pars = new DialogParameters + { + { "Title", "读写设置" }, + { "CanLinConfigProId", SelectCanLinConfigPro.Id }, + { "IsEditable", IsRwEditable }, + { "WriteConfigs", writeClones }, + { "ReadConfigs", readClones }, + { "SignalCandidates", candidates }, + }; + + DialogService.ShowDialog(nameof(DialogZlgCanLinRwConfigView), pars, r => + { + if (r.Result == ButtonResult.OK) + { + ReloadCurrentConfigPro(); + OpTip = "读写设置已保存"; + } + }); + } + catch (Exception ex) + { + LogService.Error($"ZLG 打开读写设置弹窗失败:{ex}"); + MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private static CanLinRWConfigDto CloneRwDto(CanLinRWConfigDto src) + { + return new CanLinRWConfigDto + { + Id = src.Id, + RWInfo = src.RWInfo, + Name = src.Name, + MsgFrameName = src.MsgFrameName, + SignalName = src.SignalName, + DefautValue = src.DefautValue, + LogicRuleId = src.LogicRuleId, + LogicRuleDto = src.LogicRuleDto, + }; + } + /// /// Dialog 服务。 /// @@ -111,8 +255,52 @@ namespace CapMachine.Wpf.ViewModels /// public IMapper Mapper { get; } + private ObservableCollection _arbBaudRateCbxItems = new ObservableCollection(); + /// + /// 仲裁波特率下拉项(bps)。 + /// + public ObservableCollection ArbBaudRateCbxItems + { + get { return _arbBaudRateCbxItems; } + set { _arbBaudRateCbxItems = value; RaisePropertyChanged(); } + } + + private ObservableCollection _dataBaudRateCbxItems = new ObservableCollection(); + /// + /// 数据波特率下拉项(bps)。 + /// + public ObservableCollection DataBaudRateCbxItems + { + get { return _dataBaudRateCbxItems; } + set { _dataBaudRateCbxItems = value; RaisePropertyChanged(); } + } + private List _canLinConfigPros = new List(); + private ObservableCollection _writeRuleCbxItems = new ObservableCollection(); + public ObservableCollection WriteRuleCbxItems + { + get { return _writeRuleCbxItems; } + set { _writeRuleCbxItems = value; RaisePropertyChanged(); } + } + + private ObservableCollection _listWriteCanLinRWConfigDto = new ObservableCollection(); + public ObservableCollection ListWriteCanLinRWConfigDto + { + get { return _listWriteCanLinRWConfigDto; } + set { _listWriteCanLinRWConfigDto = value; RaisePropertyChanged(); } + } + + private ObservableCollection _listReadCanLinRWConfigDto = new ObservableCollection(); + public ObservableCollection ListReadCanLinRWConfigDto + { + get { return _listReadCanLinRWConfigDto; } + set { _listReadCanLinRWConfigDto = value; RaisePropertyChanged(); } + } + + private CanLinRWConfigDto? SelectedWriteCanLinRWConfigDto { get; set; } + private CanLinRWConfigDto? SelectedReadCanLinRWConfigDto { get; set; } + private string? _opTip; /// /// 操作提示(用于 UI 状态展示)。 @@ -133,6 +321,37 @@ namespace CapMachine.Wpf.ViewModels set { _lastError = value; RaisePropertyChanged(); } } + private bool _isCanConfigProActive; + /// + /// 当前配置程序是否已激活(对齐图莫斯 Active 语义)。 + /// 激活后禁止切换配置程序。 + /// + public bool IsCanConfigProActive + { + get { return _isCanConfigProActive; } + set + { + _isCanConfigProActive = value; + RaisePropertyChanged(); + RaisePropertyChanged(nameof(IsRwEditable)); + } + } + + public bool IsRwEditable + { + get { return !IsCanConfigProActive; } + } + + private bool _isCANConfigDatagridActive = true; + /// + /// 配置程序 DataGrid 是否可操作(与 IsCanConfigProActive 取反)。 + /// + public bool IsCANConfigDatagridActive + { + get { return _isCANConfigDatagridActive; } + set { _isCANConfigDatagridActive = value; RaisePropertyChanged(); } + } + private ZlgCanMode _selectedMode; /// /// 模式选择:CAN/CANFD(单选)。 @@ -142,19 +361,224 @@ namespace CapMachine.Wpf.ViewModels get { return _selectedMode; } set { + // 打开状态下不允许切换模式,避免驱动状态错乱。 + if (ZlgCanDriveService.OpenState && _selectedMode != value) + { + MessageBox.Show("请先关闭 CAN 后再切换 CAN/CANFD 模式", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + RaisePropertyChanged(nameof(SelectedModeKey)); + return; + } + _selectedMode = value; RaisePropertyChanged(); - RaisePropertyChanged(nameof(DbcPathTitle)); RaisePropertyChanged(nameof(SelectedModeKey)); + RaisePropertyChanged(nameof(IsCanMode)); + RaisePropertyChanged(nameof(IsCanFdMode)); + RaisePropertyChanged(nameof(DbcPathTitle)); + RaisePropertyChanged(nameof(ConnectButtonText)); + RaisePropertyChanged(nameof(CloseButtonText)); RaisePropertyChanged(nameof(CurrentDbcPath)); RaisePropertyChanged(nameof(CurrentCycle)); RaisePropertyChanged(nameof(CurrentSchEnable)); - RaisePropertyChanged(nameof(CanBaudRate)); - RaisePropertyChanged(nameof(CanFdArbBaudRate)); - RaisePropertyChanged(nameof(CanFdDataBaudRate)); - RaisePropertyChanged(nameof(ConnectButtonText)); + RaisePropertyChanged(nameof(CurrentArbBaudRate)); + RaisePropertyChanged(nameof(CurrentDataBaudRate)); + RaisePropertyChanged(nameof(CurrentISOEnable)); + RaisePropertyChanged(nameof(CurrentResEnable)); + RaisePropertyChanged(nameof(BaudRateTitle)); + RaisePropertyChanged(nameof(DataBaudRateTitle)); ZlgCanDriveService.Mode = value; - InitLoadCanConfigPro(); + + // 当用户手动切换 CAN/CANFD 时,如果当前配置缺少对应扩展 DTO,需要先生成默认 DTO, + // 否则界面控件无法编辑(setter 会因 DTO 为空直接 return)。 + if (SelectCanLinConfigPro != null) + { + if (value == ZlgCanMode.Can) + { + if (SelectedCANConfigExdDto == null) + { + SelectedCANConfigExdDto = new CANConfigExdDto() + { + Id = 0, + BaudRate = 500000, + DbcPath = string.Empty, + Cycle = 100, + SchEnable = false, + }; + } + } + else + { + if (SelectedCANFdConfigExdDto == null) + { + SelectedCANFdConfigExdDto = new CANFdConfigExdDto() + { + Id = 0, + ArbBaudRate = 500000, + DataBaudRate = 2000000, + ISOEnable = true, + ResEnable = false, + DbcPath = string.Empty, + Cycle = 100, + SchEnable = false, + }; + } + } + } + + // 切换模式后,为避免界面仍显示旧的 DBC 信号列表,清空信号集合。 + // 配置程序列表不应随模式切换而过滤/刷新。 + ListCanDbcModel = new ObservableCollection(); + } + } + + private DelegateCommand? _writeGridSelectionChangedCmd; + public DelegateCommand WriteGridSelectionChangedCmd + { + get + { + if (_writeGridSelectionChangedCmd == null) + { + _writeGridSelectionChangedCmd = new DelegateCommand(WriteGridSelectionChangedCmdMethod); + } + return _writeGridSelectionChangedCmd; + } + } + + private void WriteGridSelectionChangedCmdMethod(object par) + { + if (par is CanLinRWConfigDto dto) + { + SelectedWriteCanLinRWConfigDto = dto; + } + } + + private DelegateCommand? _readGridSelectionChangedCmd; + public DelegateCommand ReadGridSelectionChangedCmd + { + get + { + if (_readGridSelectionChangedCmd == null) + { + _readGridSelectionChangedCmd = new DelegateCommand(ReadGridSelectionChangedCmdMethod); + } + return _readGridSelectionChangedCmd; + } + } + + private void ReadGridSelectionChangedCmdMethod(object par) + { + if (par is CanLinRWConfigDto dto) + { + SelectedReadCanLinRWConfigDto = dto; + } + } + + private DelegateCommand? _writeCmd; + public DelegateCommand WriteCmd + { + get + { + if (_writeCmd == null) + { + _writeCmd = new DelegateCommand(WriteCmdMethod); + } + return _writeCmd; + } + } + + private void WriteCmdMethod(string par) + { + if (SelectCanLinConfigPro == null) + { + MessageBox.Show("选中CAN配置名称后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (IsCanConfigProActive) + { + MessageBox.Show("当前配置已激活,请先取消激活后再修改写入配置", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + switch (par) + { + case "Edit": + foreach (var item in ListWriteCanLinRWConfigDto) + { + FreeSql.Update(item.Id) + .Set(a => a.Name, item.Name) + .Set(a => a.LogicRuleId, item.LogicRuleId) + .Set(a => a.DefautValue, item.DefautValue) + .ExecuteAffrows(); + } + ReloadCurrentConfigPro(); + OpTip = "写入配置已保存"; + break; + + case "Delete": + if (SelectedWriteCanLinRWConfigDto == null) + { + MessageBox.Show("请选中写入列表中的一行后再删除", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + FreeSql.Delete(SelectedWriteCanLinRWConfigDto.Id).ExecuteAffrows(); + ReloadCurrentConfigPro(); + OpTip = "写入配置已删除"; + break; + } + } + + private DelegateCommand? _readCmd; + public DelegateCommand ReadCmd + { + get + { + if (_readCmd == null) + { + _readCmd = new DelegateCommand(ReadCmdMethod); + } + return _readCmd; + } + } + + private void ReadCmdMethod(string par) + { + if (SelectCanLinConfigPro == null) + { + MessageBox.Show("选中CAN配置名称后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (IsCanConfigProActive) + { + MessageBox.Show("当前配置已激活,请先取消激活后再修改读取配置", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + switch (par) + { + case "Edit": + foreach (var item in ListReadCanLinRWConfigDto) + { + FreeSql.Update(item.Id) + .Set(a => a.Name, item.Name) + .Set(a => a.DefautValue, item.DefautValue) + .ExecuteAffrows(); + } + ReloadCurrentConfigPro(); + OpTip = "读取配置已保存"; + break; + + case "Delete": + if (SelectedReadCanLinRWConfigDto == null) + { + MessageBox.Show("请选中读取列表中的一行后再删除", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + FreeSql.Delete(SelectedReadCanLinRWConfigDto.Id).ExecuteAffrows(); + ReloadCurrentConfigPro(); + OpTip = "读取配置已删除"; + break; } } @@ -166,6 +590,46 @@ namespace CapMachine.Wpf.ViewModels get { return SelectedMode == ZlgCanMode.Can ? "连接CAN" : "连接CANFD"; } } + /// + /// 当前是否为 CANFD 模式(用于 UI 启用/禁用某些仅 CANFD 支持的参数)。 + /// + public bool IsCanFdMode + { + get { return SelectedMode == ZlgCanMode.CanFd; } + } + + /// + /// 当前是否为 CAN 模式(用于 UI 启用/禁用某些仅 CANFD 支持的参数)。 + /// + public bool IsCanMode + { + get { return SelectedMode == ZlgCanMode.Can; } + } + + /// + /// 波特率标题:CAN 为“波特率”,CANFD 为“仲裁波特率”。 + /// + public string BaudRateTitle + { + get { return SelectedMode == ZlgCanMode.Can ? "波特率" : "仲裁波特率"; } + } + + /// + /// 数据波特率标题:仅 CANFD 有意义。 + /// + public string DataBaudRateTitle + { + get { return "数据波特率"; } + } + + /// + /// 关闭按钮文字(对齐 Toomoss 风格)。 + /// + public string CloseButtonText + { + get { return SelectedMode == ZlgCanMode.Can ? "关闭CAN" : "关闭CANFD"; } + } + /// /// 绑定用:模式 Key(Can/CanFd)。 /// @@ -174,6 +638,13 @@ namespace CapMachine.Wpf.ViewModels get { return SelectedMode == ZlgCanMode.Can ? "Can" : "CanFd"; } set { + if (ZlgCanDriveService.OpenState) + { + MessageBox.Show("请先关闭 CAN 后再切换 CAN/CANFD 模式", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + RaisePropertyChanged(); + return; + } + if (string.Equals(value, "Can", StringComparison.OrdinalIgnoreCase)) { SelectedMode = ZlgCanMode.Can; @@ -193,7 +664,7 @@ namespace CapMachine.Wpf.ViewModels /// public string DbcPathTitle { - get { return SelectedMode == ZlgCanMode.Can ? "CAN DBC 文件路径:" : "CANFD DBC 文件路径:"; } + get { return SelectedMode == ZlgCanMode.Can ? "CAN-DBC文件路径:" : "CANFD-DBC文件路径:"; } } private ObservableCollection? _listCanLinConfigPro; @@ -209,7 +680,20 @@ namespace CapMachine.Wpf.ViewModels /// /// 选中的配置程序。 /// - public CanLinConfigPro? SelectCanLinConfigPro { get; set; } + private CanLinConfigPro? _selectCanLinConfigPro; + + /// + /// 选中的配置程序。 + /// + public CanLinConfigPro? SelectCanLinConfigPro + { + get { return _selectCanLinConfigPro; } + set + { + _selectCanLinConfigPro = value; + RaisePropertyChanged(); + } + } private ObservableCollection? _listCanDbcModel; /// @@ -224,7 +708,13 @@ namespace CapMachine.Wpf.ViewModels /// /// 当前选中的 DBC 信号。 /// - public CanDbcModel? SelectedCanDbcModel { get; set; } + public CanDbcModel? SelectedCanDbcModel + { + get { return _selectedCanDbcModel; } + set { _selectedCanDbcModel = value; RaisePropertyChanged(); } + } + + private CanDbcModel? _selectedCanDbcModel; private CANConfigExdDto? _selectedCANConfigExdDto; /// @@ -248,18 +738,21 @@ namespace CapMachine.Wpf.ViewModels private void InitLoadCanConfigPro() { - var info = SelectedMode == ZlgCanMode.Can ? CANLIN.CAN : CANLIN.CANFD; - _canLinConfigPros = FreeSql.Select() - .Where(a => a.CANLINInfo == info) + .Where(a => a.CANLINInfo == CANLIN.CAN || a.CANLINInfo == CANLIN.CANFD) .Include(a => a.CANConfigExd) .Include(a => a.CANFdConfigExd) .IncludeMany(a => a.CanLinConfigContents, then => then.Include(b => b.LogicRule)) + .IncludeMany(a => a.CanScheduleConfigs) + .IncludeMany(a => a.CanFdScheduleConfigs) .ToList(); ListCanLinConfigPro = new ObservableCollection(_canLinConfigPros); } + /// + /// 同步选中的配置。 + /// private void SyncSelectedConfig() { if (SelectCanLinConfigPro == null) @@ -267,23 +760,260 @@ namespace CapMachine.Wpf.ViewModels return; } - if (SelectedMode == ZlgCanMode.Can) + // 选中配置程序后,模式应跟随该配置自身的 CANLINInfo,避免“列表全量但右侧模式不匹配”。 + var proMode = SelectCanLinConfigPro.CANLINInfo == CANLIN.CAN ? ZlgCanMode.Can : ZlgCanMode.CanFd; + if (SelectedMode != proMode) { - SelectedCANConfigExdDto = Mapper.Map(SelectCanLinConfigPro.CANConfigExd); - SelectedCANFdConfigExdDto = null; - } - else - { - SelectedCANFdConfigExdDto = Mapper.Map(SelectCanLinConfigPro.CANFdConfigExd); - SelectedCANConfigExdDto = null; + SelectedMode = proMode; } + // 同时映射两套配置(如不存在则为 null),这样用户可以在 UI 上切换模式后再保存。 + SelectedCANConfigExdDto = SelectCanLinConfigPro.CANConfigExd != null + ? Mapper.Map(SelectCanLinConfigPro.CANConfigExd) + : null; + SelectedCANFdConfigExdDto = SelectCanLinConfigPro.CANFdConfigExd != null + ? Mapper.Map(SelectCanLinConfigPro.CANFdConfigExd) + : null; + RaisePropertyChanged(nameof(CurrentDbcPath)); RaisePropertyChanged(nameof(CurrentCycle)); RaisePropertyChanged(nameof(CurrentSchEnable)); + RaisePropertyChanged(nameof(CurrentArbBaudRate)); + RaisePropertyChanged(nameof(CurrentDataBaudRate)); + RaisePropertyChanged(nameof(CurrentISOEnable)); + RaisePropertyChanged(nameof(CurrentResEnable)); RaisePropertyChanged(nameof(CanBaudRate)); RaisePropertyChanged(nameof(CanFdArbBaudRate)); RaisePropertyChanged(nameof(CanFdDataBaudRate)); + + BuildAndLoadCmdDataToDrive(); + + var writeData = SelectCanLinConfigPro.CanLinConfigContents?.Where(a => a.RWInfo == RW.Write).ToList() ?? new List(); + ListWriteCanLinRWConfigDto = new ObservableCollection(Mapper.Map>(writeData)); + + var readData = SelectCanLinConfigPro.CanLinConfigContents?.Where(a => a.RWInfo == RW.Read).ToList() ?? new List(); + ListReadCanLinRWConfigDto = new ObservableCollection(Mapper.Map>(readData)); + + // 调度表(按模式) + if (SelectedMode == ZlgCanMode.Can) + { + if (SelectCanLinConfigPro.CanScheduleConfigs != null && SelectCanLinConfigPro.CanScheduleConfigs.Count > 0) + { + ListCANScheduleConfigDto = new ObservableCollection(Mapper.Map>(SelectCanLinConfigPro.CanScheduleConfigs)); + } + else + { + ListCANScheduleConfigDto = new ObservableCollection(); + } + + RaisePropertyChanged(nameof(ListCANScheduleConfigDto)); + } + else + { + if (SelectCanLinConfigPro.CanFdScheduleConfigs != null && SelectCanLinConfigPro.CanFdScheduleConfigs.Count > 0) + { + ListCANFdScheduleConfigDto = new ObservableCollection(Mapper.Map>(SelectCanLinConfigPro.CanFdScheduleConfigs)); + } + else + { + ListCANFdScheduleConfigDto = new ObservableCollection(); + } + + RaisePropertyChanged(nameof(ListCANFdScheduleConfigDto)); + } + + ResetAndTryLoadDbcSignalsForSelectedConfig(); + } + + /// + /// 切换配置程序时重置并尝试加载当前配置对应的 DBC 信号集合。 + /// + private void ResetAndTryLoadDbcSignalsForSelectedConfig() + { + SelectedCanDbcModel = null; + + // 未连接时:信号集合应以当前配置的 DBC 为准(或为空),禁止沿用上一个配置的信号列表。 + if (!ZlgCanDriveService.OpenState) + { + if (string.IsNullOrWhiteSpace(CurrentDbcPath)) + { + ListCanDbcModel = new ObservableCollection(); + return; + } + + try + { + ListCanDbcModel = ZlgCanDriveService.StartDbc(CurrentDbcPath); + MatchSeletedAndCanDbcModel(); + } + catch (Exception ex) + { + ListCanDbcModel = new ObservableCollection(); + OpTip = "DBC 解析失败"; + LogService.Warn($"切换配置程序时解析 DBC 失败:{CurrentDbcPath},{ex.Message}"); + } + + return; + } + + // 已连接时:禁止切换配置程序,理论上不会执行到这里;保底清空,避免 UI 残留。 + ListCanDbcModel = new ObservableCollection(); + } + + private ObservableCollection _listCANScheduleConfigDto = new ObservableCollection(); + /// + /// CAN 调度表集合。 + /// + public ObservableCollection ListCANScheduleConfigDto + { + get { return _listCANScheduleConfigDto; } + set { _listCANScheduleConfigDto = value; RaisePropertyChanged(); } + } + + private ObservableCollection _listCANFdScheduleConfigDto = new ObservableCollection(); + /// + /// CANFD 调度表集合。 + /// + public ObservableCollection ListCANFdScheduleConfigDto + { + get { return _listCANFdScheduleConfigDto; } + set { _listCANFdScheduleConfigDto = value; RaisePropertyChanged(); } + } + + /// + /// 当前仲裁波特率(对 CAN:等同 BaudRate;对 CANFD:ArbBaudRate)。 + /// + public int CurrentArbBaudRate + { + get + { + return SelectedMode == ZlgCanMode.Can + ? (SelectedCANConfigExdDto?.BaudRate ?? 0) + : (SelectedCANFdConfigExdDto?.ArbBaudRate ?? 0); + } + set + { + if (SelectedMode == ZlgCanMode.Can) + { + if (SelectedCANConfigExdDto == null) return; + SelectedCANConfigExdDto.BaudRate = value; + } + else + { + if (SelectedCANFdConfigExdDto == null) return; + SelectedCANFdConfigExdDto.ArbBaudRate = value; + } + + RaisePropertyChanged(); + RaisePropertyChanged(nameof(CanBaudRate)); + RaisePropertyChanged(nameof(CanFdArbBaudRate)); + } + } + + /// + /// 当前数据波特率(对 CAN:等同 BaudRate;对 CANFD:DataBaudRate)。 + /// + public int CurrentDataBaudRate + { + get + { + return SelectedMode == ZlgCanMode.Can + ? (SelectedCANConfigExdDto?.BaudRate ?? 0) + : (SelectedCANFdConfigExdDto?.DataBaudRate ?? 0); + } + set + { + if (SelectedMode == ZlgCanMode.Can) + { + if (SelectedCANConfigExdDto == null) return; + SelectedCANConfigExdDto.BaudRate = value; + } + else + { + if (SelectedCANFdConfigExdDto == null) return; + SelectedCANFdConfigExdDto.DataBaudRate = value; + } + + RaisePropertyChanged(); + RaisePropertyChanged(nameof(CanBaudRate)); + RaisePropertyChanged(nameof(CanFdDataBaudRate)); + } + } + + /// + /// 当前 ISO 标准使能(仅 CANFD 有意义;CAN 固定为 false)。 + /// + public bool CurrentISOEnable + { + get { return SelectedMode == ZlgCanMode.Can ? false : (SelectedCANFdConfigExdDto?.ISOEnable ?? false); } + set + { + if (SelectedMode == ZlgCanMode.Can) + { + return; + } + + if (SelectedCANFdConfigExdDto == null) return; + SelectedCANFdConfigExdDto.ISOEnable = value; + RaisePropertyChanged(); + } + } + + /// + /// 当前终端电阻使能(仅 CANFD 有意义;CAN 固定为 true(驱动侧可忽略))。 + /// + public bool CurrentResEnable + { + get { return SelectedMode == ZlgCanMode.Can ? true : (SelectedCANFdConfigExdDto?.ResEnable ?? false); } + set + { + if (SelectedMode == ZlgCanMode.Can) + { + return; + } + + if (SelectedCANFdConfigExdDto == null) return; + SelectedCANFdConfigExdDto.ResEnable = value; + RaisePropertyChanged(); + } + } + + /// + /// 构建并加载命令数据到驱动。 + /// + private void BuildAndLoadCmdDataToDrive() + { + try + { + if (SelectCanLinConfigPro?.CanLinConfigContents == null) + { + ZlgCanDriveService.LoadCmdDataToDrive(new List()); + return; + } + + var writeItems = SelectCanLinConfigPro.CanLinConfigContents + .Where(a => a.RWInfo == RW.Write) + .ToList(); + + var cmdList = new List(); + foreach (var item in writeItems) + { + cmdList.Add(new CanCmdData() + { + ConfigName = item.Name, + MsgName = item.MsgFrameName, + SignalName = item.SignalName, + SignalCmdValue = double.TryParse(item.DefautValue, out double result) ? result : 0, + LogicRuleDto = Mapper.Map(item.LogicRule), + }); + } + + ZlgCanDriveService.LoadCmdDataToDrive(cmdList); + } + catch (Exception ex) + { + LogService.Warn($"ZLG CAN 构建/下发 CmdData 失败:{ex.Message}"); + } } /// @@ -412,6 +1142,9 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 匹配选中的配置和 DBC 模型。 + /// private void MatchSeletedAndCanDbcModel() { if (ListCanDbcModel == null || ListCanDbcModel.Count == 0) return; @@ -461,6 +1194,18 @@ namespace CapMachine.Wpf.ViewModels private void CanConfigProGridSelectionChangedCmdMethod(object par) { if (par == null) return; + + if (ZlgCanDriveService.OpenState) + { + // 打开状态下禁止切换配置程序。 + return; + } + + if (IsCanConfigProActive) + { + // 激活状态下禁止切换配置程序。 + return; + } if (par is SelectionChangedEventArgs) return; if (par is CanLinConfigPro) @@ -480,6 +1225,47 @@ namespace CapMachine.Wpf.ViewModels SyncSelectedConfig(); } + private DelegateCommand? _canConfigProGridPreviewMouseLeftButtonDownCmd; + /// + /// 配置程序切换前拦截(对齐图莫斯 PreviewMouseLeftButtonDown)。 + /// 打开/激活状态下禁止切换,防止状态错乱。 + /// + public DelegateCommand CanConfigProGridPreviewMouseLeftButtonDownCmd + { + get + { + if (_canConfigProGridPreviewMouseLeftButtonDownCmd == null) + { + _canConfigProGridPreviewMouseLeftButtonDownCmd = new DelegateCommand(CanConfigProGridPreviewMouseLeftButtonDownCmdMethod); + } + return _canConfigProGridPreviewMouseLeftButtonDownCmd; + } + } + + private void CanConfigProGridPreviewMouseLeftButtonDownCmdMethod(object par) + { + try + { + if (ZlgCanDriveService.OpenState) + { + MessageBox.Show("CAN 已连接,请先关闭后再切换配置程序", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + if (par is System.Windows.Input.MouseButtonEventArgs e) e.Handled = true; + return; + } + + if (IsCanConfigProActive) + { + MessageBox.Show("当前配置程序已激活,请先取消激活后再切换", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + if (par is System.Windows.Input.MouseButtonEventArgs e) e.Handled = true; + return; + } + } + catch (Exception ex) + { + LogService.Warn($"配置切换拦截异常:{ex.Message}"); + } + } + private DelegateCommand? _loadDbcCmd; /// /// 选择 DBC 文件。 @@ -510,14 +1296,31 @@ namespace CapMachine.Wpf.ViewModels openFileDialogInfo.Filter = "(*.dbc;*.dbc)|*.dbc;*.dbc|all|*.*"; openFileDialogInfo.CheckFileExists = true; openFileDialogInfo.CheckPathExists = true; - openFileDialogInfo.ShowDialog(); - string fileName = openFileDialogInfo.FileName; + var dialogResult = openFileDialogInfo.ShowDialog(); + if (dialogResult != true) + { + // 用户取消:保持原值,不清空 CurrentDbcPath + return; + } + + var fileName = openFileDialogInfo.FileName; + if (string.IsNullOrWhiteSpace(fileName)) + { + return; + } + + if (!File.Exists(fileName)) + { + MessageBox.Show("文件不存在,请重新选择", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } CurrentDbcPath = fileName; } - catch + catch (Exception ex) { - MessageBox.Show("可能未选择信息", "提示", MessageBoxButton.OKCancel, MessageBoxImage.Hand); + LogService.Error($"选择 DBC 文件失败:{ex}"); + MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } @@ -543,6 +1346,540 @@ namespace CapMachine.Wpf.ViewModels ZlgCanDriveService.SchEnable = CurrentSchEnable; } + private DelegateCommand? _canLinConfigProCmd; + /// + /// 配置程序操作(新建/修改/删除)。 + /// + public DelegateCommand CanLinConfigProCmd + { + get + { + if (_canLinConfigProCmd == null) + { + _canLinConfigProCmd = new DelegateCommand(CanLinConfigProCmdMethod); + } + return _canLinConfigProCmd; + } + } + + private void CanLinConfigProCmdMethod(string par) + { + if (string.IsNullOrWhiteSpace(par)) return; + + try + { + switch (par) + { + case "Add": + if (IsCanConfigProActive) + { + MessageBox.Show("当前配置已激活,请先取消激活后再新建/修改配置程序", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (ZlgCanDriveService.OpenState) + { + MessageBox.Show("请先关闭 CAN 后再新建配置程序", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + DialogService.ShowDialog("DialogCanLinConfigCreateView", new DialogParameters() { { "Name", "" } }, r => + { + if (r.Result != ButtonResult.OK) return; + + try + { + var name = r.Parameters.GetValue("Name")?.Trim(); + if (string.IsNullOrWhiteSpace(name)) + { + MessageBox.Show("名称不能为空", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + var info = SelectedMode == ZlgCanMode.Can ? CANLIN.CAN : CANLIN.CANFD; + var exists = FreeSql.Select() + .Where(a => a.CANLINInfo == info) + .Where(a => a.ConfigName == name) + .Any(); + if (exists) + { + MessageBox.Show("名称已存在,请更换名称", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + long newProId = 0; + + FreeSql.Transaction(() => + { + if (SelectedMode == ZlgCanMode.Can) + { + var exdList = FreeSql.Insert(new CANConfigExd() + { + BaudRate = 500000, + DbcPath = string.Empty, + Cycle = 100, + SchEnable = false, + }).ExecuteInserted(); + + var exd = exdList?.FirstOrDefault(); + if (exd == null) + { + throw new InvalidOperationException("创建 CAN 扩展配置失败"); + } + + var proList = FreeSql.Insert(new CanLinConfigPro() + { + ConfigName = name, + CANLINInfo = CANLIN.CAN, + CANConfigExdId = exd.Id, + }).ExecuteInserted(); + + var pro = proList?.FirstOrDefault(); + if (pro == null) + { + throw new InvalidOperationException("创建 CAN 配置程序失败"); + } + + newProId = pro.Id; + } + else + { + var exdList = FreeSql.Insert(new CANFdConfigExd() + { + ArbBaudRate = 500000, + DataBaudRate = 2000000, + ISOEnable = true, + ResEnable = false, + DbcPath = string.Empty, + Cycle = 100, + SchEnable = false, + }).ExecuteInserted(); + + var exd = exdList?.FirstOrDefault(); + if (exd == null) + { + throw new InvalidOperationException("创建 CANFD 扩展配置失败"); + } + + var proList = FreeSql.Insert(new CanLinConfigPro() + { + ConfigName = name, + CANLINInfo = CANLIN.CANFD, + CANFdConfigExdId = exd.Id, + }).ExecuteInserted(); + + var pro = proList?.FirstOrDefault(); + if (pro == null) + { + throw new InvalidOperationException("创建 CANFD 配置程序失败"); + } + + newProId = pro.Id; + } + }); + + InitLoadCanConfigPro(); + SelectCanLinConfigPro = _canLinConfigPros.Find(a => a.Id == newProId); + SyncSelectedConfig(); + MatchSeletedAndCanDbcModel(); + } + catch (Exception ex) + { + LogService.Error($"ZLG 新建配置程序失败:{ex}"); + MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + }); + break; + + case "Edit": + if (SelectCanLinConfigPro == null) + { + MessageBox.Show("选中后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (IsCanConfigProActive) + { + MessageBox.Show("当前配置已激活,请先取消激活后再修改配置程序", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (ZlgCanDriveService.OpenState) + { + MessageBox.Show("请先关闭 CAN 后再修改配置程序", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + DialogService.ShowDialog("DialogCanLinConfigCreateView", new DialogParameters() { { "Name", SelectCanLinConfigPro.ConfigName } }, r => + { + if (r.Result != ButtonResult.OK) return; + + try + { + var name = r.Parameters.GetValue("Name")?.Trim(); + if (string.IsNullOrWhiteSpace(name)) + { + MessageBox.Show("名称不能为空", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + var info = SelectedMode == ZlgCanMode.Can ? CANLIN.CAN : CANLIN.CANFD; + var exists = FreeSql.Select() + .Where(a => a.CANLINInfo == info) + .Where(a => a.ConfigName == name) + .Where(a => a.Id != SelectCanLinConfigPro.Id) + .Any(); + if (exists) + { + MessageBox.Show("名称已存在,请更换名称", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + FreeSql.Update() + .Set(a => a.ConfigName, name) + .Where(a => a.Id == SelectCanLinConfigPro.Id) + .ExecuteAffrows(); + + ReloadCurrentConfigPro(); + } + catch (Exception ex) + { + LogService.Error($"ZLG 修改配置程序失败:{ex}"); + MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + }); + break; + + case "Delete": + if (SelectCanLinConfigPro == null) + { + MessageBox.Show("选中后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (IsCanConfigProActive) + { + MessageBox.Show("当前配置已激活,请先取消激活后再删除配置程序", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (ZlgCanDriveService.OpenState) + { + MessageBox.Show("请先关闭 CAN 后再删除配置程序", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (MessageBox.Show($"确定删除配置程序:{SelectCanLinConfigPro.ConfigName}?", "确认", MessageBoxButton.OKCancel, MessageBoxImage.Warning) != MessageBoxResult.OK) + { + return; + } + + // 删除主表与内容 + var repo = FreeSql.GetRepository(); + repo.DbContextOptions.EnableCascadeSave = true; + + var delList = repo.Select + .Include(a => a.CANConfigExd) + .Include(a => a.CANFdConfigExd) + .IncludeMany(a => a.CanLinConfigContents) + .IncludeMany(a => a.CanScheduleConfigs) + .IncludeMany(a => a.CanFdScheduleConfigs) + .Where(a => a.Id == SelectCanLinConfigPro.Id) + .ToList(); + + repo.Delete(delList); + foreach (var item in delList) + { + // 允许配置在 CAN/CANFD 间切换保存后同时具备两套扩展表,此处统一清理避免残留孤儿记录。 + if (item.CANConfigExdId > 0) + { + FreeSql.Delete(item.CANConfigExdId).ExecuteAffrows(); + } + if (item.CANFdConfigExdId > 0) + { + FreeSql.Delete(item.CANFdConfigExdId).ExecuteAffrows(); + } + } + + SelectCanLinConfigPro = null; + SelectedCANConfigExdDto = null; + SelectedCANFdConfigExdDto = null; + InitLoadCanConfigPro(); + ListCanDbcModel = new ObservableCollection(); + break; + + case "Active": + // 激活到取消的状态的判断 + if (IsCanConfigProActive) + { + IsCanConfigProActive = false; + IsCANConfigDatagridActive = true; + + // 停止调度/发送,避免“取消激活但仍在发送” + ZlgCanDriveService.StopSchedule(); + ZlgCanDriveService.IsCycleSend = false; + ZlgCanDriveService.LoadCmdDataToDrive(new List()); + + OpTip = "已取消激活"; + return; + } + + if (!ZlgCanDriveService.OpenState) + { + MessageBox.Show("请确保 CAN 已连接后再激活", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (SelectCanLinConfigPro == null) + { + MessageBox.Show("选中后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (string.IsNullOrWhiteSpace(CurrentDbcPath)) + { + MessageBox.Show("请先选择并解析 DBC 文件后再激活", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + // 激活前强制按“当前配置的 DBC 路径”重新解析,避免 DbcParserState 来自其它配置导致错配。 + try + { + ListCanDbcModel = ZlgCanDriveService.StartDbc(CurrentDbcPath); + MatchSeletedAndCanDbcModel(); + } + catch (Exception ex) + { + LogService.Error($"ZLG 激活前解析 DBC 失败:{ex}"); + MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + IsCanConfigProActive = true; + IsCANConfigDatagridActive = false; + + // 当前使用的配置程序 + ZlgCanDriveService.InitCanConfig(SelectCanLinConfigPro); + + // 下发指令集合与调度表 + BuildAndLoadCmdDataToDrive(); + if (SelectedMode == ZlgCanMode.Can) + { + ZlgCanDriveService.SetScheduleConfigs(ListCANScheduleConfigDto?.ToList() ?? new List()); + } + else + { + ZlgCanDriveService.SetScheduleConfigs(ListCANFdScheduleConfigDto?.ToList() ?? new List()); + } + + OpTip = "已激活当前配置"; + break; + } + } + catch (Exception ex) + { + LogService.Error($"ZLG 配置程序操作失败:{par},{ex}"); + MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private DelegateCommand? _scheduleConfigCmd; + /// + /// 调度表配置(弹窗)。 + /// + public DelegateCommand ScheduleConfigCmd + { + get + { + if (_scheduleConfigCmd == null) + { + _scheduleConfigCmd = new DelegateCommand(ScheduleConfigCmdMethod); + } + return _scheduleConfigCmd; + } + } + + private void ScheduleConfigCmdMethod() + { + try + { + if (SelectCanLinConfigPro == null) + { + MessageBox.Show("选中CAN配置名称后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + var msgList = ZlgCanDriveService.CmdData.GroupBy(a => a.MsgName).Select(a => a.Key).Where(a => !string.IsNullOrWhiteSpace(a)).ToList(); + if (msgList == null || msgList.Count == 0) + { + MessageBox.Show("未发现写入指令数据(CmdData 为空),无法配置调度表", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (SelectedMode == ZlgCanMode.Can) + { + DialogService.ShowDialog("DialogCANSchConfigView", new DialogParameters() + { + { "ListMsg", msgList }, + { "ListCANScheduleConfigDto", ListCANScheduleConfigDto }, + { "SelectCanLinConfigProId", SelectCanLinConfigPro.Id }, + }, r => + { + if (r.Result != ButtonResult.OK) return; + ListCANScheduleConfigDto = r.Parameters.GetValue>("ReturnValue") ?? new ObservableCollection(); + SelectCanLinConfigPro.CanScheduleConfigs = Mapper.Map>(ListCANScheduleConfigDto.ToList()); + ReloadCurrentConfigPro(); + }); + } + else + { + DialogService.ShowDialog("DialogCANFdSchConfigView", new DialogParameters() + { + { "ListMsg", msgList }, + { "ListCANFdScheduleConfigDto", ListCANFdScheduleConfigDto }, + { "SelectCanLinConfigProId", SelectCanLinConfigPro.Id }, + }, r => + { + if (r.Result != ButtonResult.OK) return; + ListCANFdScheduleConfigDto = r.Parameters.GetValue>("ReturnValue") ?? new ObservableCollection(); + SelectCanLinConfigPro.CanFdScheduleConfigs = Mapper.Map>(ListCANFdScheduleConfigDto.ToList()); + ReloadCurrentConfigPro(); + }); + } + } + catch (Exception ex) + { + LogService.Error($"调度表配置失败:{ex}"); + MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private DelegateCommand? _writeReadConfigCmd; + /// + /// 写入/读取/删除 信号配置项。 + /// + public DelegateCommand WriteReadConfigCmd + { + get + { + if (_writeReadConfigCmd == null) + { + _writeReadConfigCmd = new DelegateCommand(WriteReadConfigCmdMethod); + } + return _writeReadConfigCmd; + } + } + + private void WriteReadConfigCmdMethod(string par) + { + if (string.IsNullOrWhiteSpace(par)) return; + + if (SelectCanLinConfigPro == null) + { + MessageBox.Show("选中CAN配置名称后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (string.Equals(par, "OpenDialog", StringComparison.OrdinalIgnoreCase)) + { + if (!ZlgCanDriveService.OpenState) + { + MessageBox.Show("当前未连接 CAN/CANFD,无法打开【读写设置】。请先连接设备后再操作。", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (ListCanDbcModel == null || ListCanDbcModel.Count == 0) + { + MessageBox.Show("当前信号集合为空,无法打开【读写设置】。请先解析 DBC 并确保信号列表已加载后再操作。", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + OpenRwDialog(); + return; + } + + if (IsCanConfigProActive) + { + MessageBox.Show("当前配置已激活,请先取消激活后再修改写入/读取配置", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (SelectedCanDbcModel == null) + { + MessageBox.Show("请在右侧信号表中选中一个信号后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (string.IsNullOrWhiteSpace(SelectedCanDbcModel.SignalName) || string.IsNullOrWhiteSpace(SelectedCanDbcModel.MsgName)) + { + MessageBox.Show("选中的信号数据不完整(SignalName/MsgName 为空)", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + try + { + if (par == "Delete") + { + FreeSql.Delete() + .Where(a => a.CanLinConfigProId == SelectCanLinConfigPro.Id) + .Where(a => a.SignalName == SelectedCanDbcModel.SignalName) + .ExecuteAffrows(); + + ReloadCurrentConfigPro(); + OpTip = "已删除配置项"; + return; + } + + var rw = par == "Write" ? RW.Write : RW.Read; + + // 防重复:同一个 SignalName + RWInfo 只允许一条 + var exists = FreeSql.Select() + .Where(a => a.CanLinConfigProId == SelectCanLinConfigPro.Id) + .Where(a => a.SignalName == SelectedCanDbcModel.SignalName) + .Where(a => a.RWInfo == rw) + .Any(); + + if (exists) + { + OpTip = "配置已存在"; + return; + } + + FreeSql.Insert(new CanLinRWConfig() + { + Name = SelectedCanDbcModel.SignalName, + SignalName = SelectedCanDbcModel.SignalName, + MsgFrameName = SelectedCanDbcModel.MsgName, + CanLinConfigProId = SelectCanLinConfigPro.Id, + DefautValue = "0", + RWInfo = rw, + LogicRuleId = 0, + }).ExecuteInserted(); + + ReloadCurrentConfigPro(); + OpTip = rw == RW.Write ? "已加入写入配置" : "已加入读取配置"; + } + catch (Exception ex) + { + LogService.Error($"ZLG 写入/读取配置操作失败:{par},{ex}"); + MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void ReloadCurrentConfigPro() + { + var id = SelectCanLinConfigPro?.Id; + InitLoadCanConfigPro(); + + if (id != null) + { + SelectCanLinConfigPro = _canLinConfigPros.Find(a => a.Id == id.Value); + SyncSelectedConfig(); + } + + MatchSeletedAndCanDbcModel(); + } + private DelegateCommand? _canOpCmd; /// /// CAN 操作命令。 @@ -633,6 +1970,9 @@ namespace CapMachine.Wpf.ViewModels MatchSeletedAndCanDbcModel(); } + // 将当前配置的 CmdData 下发到驱动(用于事件驱动增量发送) + BuildAndLoadCmdDataToDrive(); + break; case "Close": @@ -640,6 +1980,10 @@ namespace CapMachine.Wpf.ViewModels ConfigService.CanLinRunStateModel.CurSysSelectedCanLin = CanLinEnum.No; LastError = null; OpTip = "CAN 已关闭"; + + // 关闭设备时自动退出激活,恢复可切换状态 + IsCanConfigProActive = false; + IsCANConfigDatagridActive = true; break; case "Parse": @@ -662,8 +2006,98 @@ namespace CapMachine.Wpf.ViewModels break; case "CycleSend": - ZlgCanDriveService.IsCycleSend = !ZlgCanDriveService.IsCycleSend; - OpTip = ZlgCanDriveService.IsCycleSend ? "循环发送:已开启" : "循环发送:已关闭"; + if (!ZlgCanDriveService.OpenState) + { + MessageBox.Show("请先打开 CAN 后再进行循环发送", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (!ZlgCanDriveService.DbcParserState) + { + MessageBox.Show("请先解析 DBC 后再进行循环发送", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + // 与图莫斯一致:第一次点击开启,第二次点击关闭 + if (!ZlgCanDriveService.IsCycleSend) + { + // 开启前:确保最新 CmdData 已下发 + BuildAndLoadCmdDataToDrive(); + + // 同步 SchEnable(防止 UI 勾选但未触发 Command 导致服务端状态不一致) + ZlgCanDriveService.SchEnable = CurrentSchEnable; + + if (ZlgCanDriveService.SchEnable) + { + // 使用调度表:校验每个 MsgName 都配置了周期 + var groupMsg = ZlgCanDriveService.CmdData + .Where(a => !string.IsNullOrWhiteSpace(a.MsgName)) + .GroupBy(a => a.MsgName) + .Select(g => g.Key) + .ToList(); + + if (groupMsg.Count == 0) + { + MessageBox.Show("未发现可发送的消息帧(CmdData.MsgName 为空)", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (SelectedMode == ZlgCanMode.Can) + { + foreach (var msg in groupMsg) + { + if (!ListCANScheduleConfigDto.Any(a => string.Equals(a.MsgName, msg, StringComparison.Ordinal))) + { + MessageBox.Show($"你使能了调度表,但是调度表中没有设置【{msg}】信息,请设置后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + } + + if (ListCANScheduleConfigDto == null || ListCANScheduleConfigDto.Count == 0) + { + MessageBox.Show("调度表配置为空数据,请检查!将无法发送数据", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + ZlgCanDriveService.SetScheduleConfigs(ListCANScheduleConfigDto.ToList()); + } + else + { + foreach (var msg in groupMsg) + { + if (!ListCANFdScheduleConfigDto.Any(a => string.Equals(a.MsgName, msg, StringComparison.Ordinal))) + { + MessageBox.Show($"你使能了调度表,但是调度表中没有设置【{msg}】信息,请设置后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + } + + if (ListCANFdScheduleConfigDto == null || ListCANFdScheduleConfigDto.Count == 0) + { + MessageBox.Show("调度表配置为空数据,请检查!将无法发送数据", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + ZlgCanDriveService.SetScheduleConfigs(ListCANFdScheduleConfigDto.ToList()); + } + + ZlgCanDriveService.StartSchedule(); + } + else + { + // 不使用调度表:按当前配置周期做精确循环发送 + var cycle = Math.Max(1, CurrentCycle); + ZlgCanDriveService.StartPrecisionCycleSend(cycle); + } + + OpTip = "循环发送:已开启"; + } + else + { + ZlgCanDriveService.IsCycleSend = false; + ZlgCanDriveService.StopSchedule(); + OpTip = "循环发送:已关闭"; + } break; case "CycleRecive": @@ -678,33 +2112,80 @@ namespace CapMachine.Wpf.ViewModels return; } - if (SelectedMode == ZlgCanMode.Can) + // 允许在 UI 上修改当前配置的模式(CAN/CANFD),保存时同步更新 CanLinConfigPro.CANLINInfo。 + FreeSql.Transaction(() => { - if (SelectedCANConfigExdDto == null) return; - FreeSql.Update() - .Set(a => a.DbcPath, SelectedCANConfigExdDto.DbcPath) - .Set(a => a.Cycle, SelectedCANConfigExdDto.Cycle) - .Set(a => a.BaudRate, SelectedCANConfigExdDto.BaudRate) - .Set(a => a.SchEnable, SelectedCANConfigExdDto.SchEnable) - .Where(a => a.Id == SelectedCANConfigExdDto.Id) - .ExecuteUpdated(); - } - else - { - if (SelectedCANFdConfigExdDto == null) return; - FreeSql.Update() - .Set(a => a.DbcPath, SelectedCANFdConfigExdDto.DbcPath) - .Set(a => a.Cycle, SelectedCANFdConfigExdDto.Cycle) - .Set(a => a.DataBaudRate, SelectedCANFdConfigExdDto.DataBaudRate) - .Set(a => a.ArbBaudRate, SelectedCANFdConfigExdDto.ArbBaudRate) - .Set(a => a.ISOEnable, SelectedCANFdConfigExdDto.ISOEnable) - .Set(a => a.ResEnable, SelectedCANFdConfigExdDto.ResEnable) - .Set(a => a.SchEnable, SelectedCANFdConfigExdDto.SchEnable) - .Where(a => a.Id == SelectedCANFdConfigExdDto.Id) - .ExecuteUpdated(); - } + if (SelectedMode == ZlgCanMode.Can) + { + if (SelectedCANConfigExdDto == null || SelectedCANConfigExdDto.Id <= 0) + { + // 若原配置为 CANFD,切换到 CAN 时可能尚未存在 CAN 扩展配置,这里按默认值创建。 + var inserted = FreeSql.Insert(new CANConfigExd() + { + BaudRate = 500000, + DbcPath = string.Empty, + Cycle = 100, + SchEnable = false, + }).ExecuteInserted(); + var exd = inserted?.FirstOrDefault(); + if (exd == null) throw new InvalidOperationException("创建 CAN 扩展配置失败"); + SelectedCANConfigExdDto = Mapper.Map(exd); + } - InitLoadCanConfigPro(); + FreeSql.Update() + .Set(a => a.DbcPath, SelectedCANConfigExdDto.DbcPath) + .Set(a => a.Cycle, SelectedCANConfigExdDto.Cycle) + .Set(a => a.BaudRate, SelectedCANConfigExdDto.BaudRate) + .Set(a => a.SchEnable, SelectedCANConfigExdDto.SchEnable) + .Where(a => a.Id == SelectedCANConfigExdDto.Id) + .ExecuteUpdated(); + + FreeSql.Update() + .Set(a => a.CANLINInfo, CANLIN.CAN) + .Set(a => a.CANConfigExdId, SelectedCANConfigExdDto.Id) + .Where(a => a.Id == SelectCanLinConfigPro.Id) + .ExecuteAffrows(); + } + else + { + if (SelectedCANFdConfigExdDto == null || SelectedCANFdConfigExdDto.Id <= 0) + { + // 若原配置为 CAN,切换到 CANFD 时可能尚未存在 CANFD 扩展配置,这里按默认值创建。 + var inserted = FreeSql.Insert(new CANFdConfigExd() + { + ArbBaudRate = 500000, + DataBaudRate = 2000000, + ISOEnable = true, + ResEnable = false, + DbcPath = string.Empty, + Cycle = 100, + SchEnable = false, + }).ExecuteInserted(); + var exd = inserted?.FirstOrDefault(); + if (exd == null) throw new InvalidOperationException("创建 CANFD 扩展配置失败"); + SelectedCANFdConfigExdDto = Mapper.Map(exd); + } + + FreeSql.Update() + .Set(a => a.DbcPath, SelectedCANFdConfigExdDto.DbcPath) + .Set(a => a.Cycle, SelectedCANFdConfigExdDto.Cycle) + .Set(a => a.DataBaudRate, SelectedCANFdConfigExdDto.DataBaudRate) + .Set(a => a.ArbBaudRate, SelectedCANFdConfigExdDto.ArbBaudRate) + .Set(a => a.ISOEnable, SelectedCANFdConfigExdDto.ISOEnable) + .Set(a => a.ResEnable, SelectedCANFdConfigExdDto.ResEnable) + .Set(a => a.SchEnable, SelectedCANFdConfigExdDto.SchEnable) + .Where(a => a.Id == SelectedCANFdConfigExdDto.Id) + .ExecuteUpdated(); + + FreeSql.Update() + .Set(a => a.CANLINInfo, CANLIN.CANFD) + .Set(a => a.CANFdConfigExdId, SelectedCANFdConfigExdDto.Id) + .Where(a => a.Id == SelectCanLinConfigPro.Id) + .ExecuteAffrows(); + } + }); + + ReloadCurrentConfigPro(); LastError = null; OpTip = "配置已保存"; break; diff --git a/CapMachine.Wpf/ViewModels/ZlgLinDriveConfigViewModel.cs b/CapMachine.Wpf/ViewModels/ZlgLinDriveConfigViewModel.cs index 197b48f..0b3e211 100644 --- a/CapMachine.Wpf/ViewModels/ZlgLinDriveConfigViewModel.cs +++ b/CapMachine.Wpf/ViewModels/ZlgLinDriveConfigViewModel.cs @@ -4,6 +4,7 @@ using CapMachine.Model.CANLIN; using CapMachine.Wpf.Dtos; using CapMachine.Wpf.LinDrive; using CapMachine.Wpf.Services; +using CapMachine.Wpf.Views; using ImTools; using Microsoft.Win32; using Prism.Commands; @@ -78,6 +79,25 @@ namespace CapMachine.Wpf.ViewModels /// public CanLinConfigPro? SelectCanLinConfigPro { get; set; } + private ObservableCollection _listWriteCanLinRWConfigDto = new ObservableCollection(); + public ObservableCollection ListWriteCanLinRWConfigDto + { + get { return _listWriteCanLinRWConfigDto; } + set { _listWriteCanLinRWConfigDto = value; RaisePropertyChanged(); } + } + + private ObservableCollection _listReadCanLinRWConfigDto = new ObservableCollection(); + public ObservableCollection ListReadCanLinRWConfigDto + { + get { return _listReadCanLinRWConfigDto; } + set { _listReadCanLinRWConfigDto = value; RaisePropertyChanged(); } + } + + public bool IsRwEditable + { + get { return !ZlgLinDriveService.OpenState; } + } + private LINConfigExdDto? _SelectedLINConfigExdDto; /// /// 选中的 LIN 配置 DTO。 @@ -146,6 +166,14 @@ namespace CapMachine.Wpf.ViewModels { if (SelectCanLinConfigPro == null) return; SelectedLINConfigExdDto = Mapper.Map(SelectCanLinConfigPro.LINConfigExd); + + var writeData = SelectCanLinConfigPro.CanLinConfigContents?.Where(a => a.RWInfo == RW.Write).ToList() ?? new List(); + ListWriteCanLinRWConfigDto = new ObservableCollection(Mapper.Map>(writeData)); + + var readData = SelectCanLinConfigPro.CanLinConfigContents?.Where(a => a.RWInfo == RW.Read).ToList() ?? new List(); + ListReadCanLinRWConfigDto = new ObservableCollection(Mapper.Map>(readData)); + + RaisePropertyChanged(nameof(IsRwEditable)); } private DelegateCommand? _LinConfigProGridSelectionChangedCmd; @@ -270,11 +298,15 @@ namespace CapMachine.Wpf.ViewModels { ConfigService.CanLinRunStateModel.CurSysSelectedCanLin = CanLinEnum.Lin; } + + RaisePropertyChanged(nameof(IsRwEditable)); break; case "Close": ZlgLinDriveService.CloseDevice(); ConfigService.CanLinRunStateModel.CurSysSelectedCanLin = CanLinEnum.No; + + RaisePropertyChanged(nameof(IsRwEditable)); break; case "Save": @@ -315,6 +347,113 @@ namespace CapMachine.Wpf.ViewModels } } + private DelegateCommand? _openRwDialogCmd; + public DelegateCommand OpenRwDialogCmd + { + get + { + if (_openRwDialogCmd == null) + { + _openRwDialogCmd = new DelegateCommand(OpenRwDialogCmdMethod); + } + return _openRwDialogCmd; + } + } + + private void OpenRwDialogCmdMethod() + { + try + { + if (SelectCanLinConfigPro == null) + { + MessageBox.Show("选中LIN配置名称后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + var writeClones = new ObservableCollection( + (ListWriteCanLinRWConfigDto ?? new ObservableCollection()) + .Select(CloneRwDto)); + + foreach (var item in writeClones) + { + item.RWInfo = RW.Write; + } + + var readClones = new ObservableCollection( + (ListReadCanLinRWConfigDto ?? new ObservableCollection()) + .Select(CloneRwDto)); + + foreach (var item in readClones) + { + item.RWInfo = RW.Read; + } + + var candidates = new ObservableCollection(); + if (ZlgLinDriveService.ListLinLdfModel != null) + { + foreach (var sig in ZlgLinDriveService.ListLinLdfModel) + { + candidates.Add(new DialogZlgCanLinRwConfigViewModel.SignalCandidate + { + MsgName = sig.MsgName, + SignalName = sig.SignalName, + Name = sig.Name, + Desc = sig.SignalDesc, + }); + } + } + + var pars = new DialogParameters + { + { "Title", "读写设置" }, + { "CanLinConfigProId", SelectCanLinConfigPro.Id }, + { "IsEditable", IsRwEditable }, + { "WriteConfigs", writeClones }, + { "ReadConfigs", readClones }, + { "SignalCandidates", candidates }, + }; + + DialogService.ShowDialog(nameof(DialogZlgCanLinRwConfigView), pars, r => + { + if (r.Result == ButtonResult.OK) + { + ReloadCurrentConfigPro(); + } + }); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void ReloadCurrentConfigPro() + { + var id = SelectCanLinConfigPro?.Id; + InitLoadLinConfigPro(); + + if (id != null) + { + SelectCanLinConfigPro = linConfigPros.Find(a => a.Id == id.Value); + SyncSelectedConfig(); + } + } + + private static CanLinRWConfigDto CloneRwDto(CanLinRWConfigDto src) + { + return new CanLinRWConfigDto + { + Id = src.Id, + RWInfo = src.RWInfo, + Name = src.Name, + MsgFrameName = src.MsgFrameName, + SignalName = src.SignalName, + DefautValue = src.DefautValue, + LogicRuleId = src.LogicRuleId, + LogicRuleDto = src.LogicRuleDto, + }; + } + private DelegateCommand? _SchEnableCmd; /// /// 调度表使能写入驱动。 diff --git a/CapMachine.Wpf/Views/DialogZlgCanLinRwConfigView.xaml b/CapMachine.Wpf/Views/DialogZlgCanLinRwConfigView.xaml new file mode 100644 index 0000000..7cf235c --- /dev/null +++ b/CapMachine.Wpf/Views/DialogZlgCanLinRwConfigView.xaml @@ -0,0 +1,331 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 写入配置 + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -279,6 +113,7 @@ UniformCornerRadius="5"> + @@ -290,24 +125,118 @@ FontWeight="Bold" Text="配置程序" /> + + + + + + + + + + + - + + + + - + + + @@ -322,7 +251,12 @@ - + + + + + + @@ -331,45 +265,578 @@ VerticalAlignment="Center" FontSize="18" FontWeight="Bold" - Text="参数/信号" /> + Text="参数操作" /> + - - - + + + + - - - - - - - - - - - - - - + Margin="10,0" + VerticalAlignment="Center" + FontSize="18" + FontWeight="Bold" + Text="状态和模式" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CapMachine.Wpf/Views/ZlgLinDriveConfigView.xaml b/CapMachine.Wpf/Views/ZlgLinDriveConfigView.xaml index 19db279..e5ccdf0 100644 --- a/CapMachine.Wpf/Views/ZlgLinDriveConfigView.xaml +++ b/CapMachine.Wpf/Views/ZlgLinDriveConfigView.xaml @@ -174,6 +174,14 @@ VerticalContentAlignment="Center" Text="{Binding LinBaudRate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> + +