周立功的CAN /FD实现

This commit is contained in:
2026-02-06 12:34:34 +08:00
parent 2e8ad1cffa
commit 74338fdb3a
13 changed files with 4260 additions and 310 deletions

View File

@@ -198,6 +198,7 @@ namespace CapMachine.Wpf
containerRegistry.RegisterDialog<DialogExpInfoView, DialogExpInfoViewModel>();
containerRegistry.RegisterDialog<DialogUserView, DialogUserViewModel>();
containerRegistry.RegisterDialog<DialogCanRwConfigView, DialogCanRwConfigViewModel>();
containerRegistry.RegisterDialog<DialogZlgCanLinRwConfigView, DialogZlgCanLinRwConfigViewModel>();
containerRegistry.RegisterDialog<DialogCanLinConfigCreateView, DialogCanLinConfigCreateViewModel>();
containerRegistry.RegisterDialog<DialogPIDConfigView, DialogPIDConfigViewModel>();
containerRegistry.RegisterDialog<DialogLimitConfigView, DialogLimitConfigViewModel>();

View File

@@ -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)]

View File

@@ -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;
}
}
/// <summary>
/// 预加载指定路径的原生 DLL尽量避免被 PATH 中的同名 DLL 干扰。
/// </summary>
/// <param name="dllFullPath">DLL 完整路径。</param>
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}";
}
}
/// <summary>
/// 构造输出目录下某个 DLL 的诊断信息(架构/大小/版本)。
/// </summary>
/// <param name="baseDir">输出目录。</param>
/// <param name="fileName">DLL 文件名。</param>
/// <returns>诊断信息字符串;不存在返回 "xxx:not_found";异常返回 null。</returns>
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;
}
}
/// <summary>
/// 获取当前进程中已加载模块的完整路径。
/// </summary>
/// <param name="moduleFileName">模块文件名(如 zlgcan.dll。</param>
/// <returns>完整路径;获取失败返回 null。</returns>
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;
}
}
/// <summary>
/// 打开设备并初始化 CANFD 通道。
/// </summary>
@@ -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}");
}
}
}
/// <summary>
/// 尝试读取 PE 头判断 DLL 架构。
/// </summary>
/// <param name="dllFullPath">DLL 完整路径。</param>
/// <returns>返回 "x86"/"x64"/"arm64"/"unknown(...)";读取失败返回 null。</returns>
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<string, CanDbcModel> BuildDbcModelIndex(IEnumerable<CanDbcModel> models)

View File

@@ -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<string>();
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;
}
/// <summary>
/// 当 ZDBC_LoadFile 失败时的兜底加载策略:读取文件内容并调用 ZDBC_LoadContent。
/// </summary>
/// <param name="fullPath">DBC 完整路径。</param>
/// <param name="merge">是否合并。</param>
/// <returns>加载成功返回 true。</returns>
private bool TryLoadContentFallback_NoLock(string fullPath, bool merge)
{
// zdbc.dll 入口为 ANSI 字符串,因此这里优先使用 ASCII将非 ASCII 字符替换为 ?
// 避免中文注释/特殊字符导致原生库解析失败;同时需要兼容 UTF-16 BOM否则用 ASCII 读会出现大量 \0 造成内容截断。
var encodings = new List<Encoding>();
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;
}
/// <summary>
/// 获取 Windows 8.3 短路径。
/// </summary>
/// <param name="lpszLongPath">长路径。</param>
/// <param name="lpszShortPath">短路径缓冲。</param>
/// <param name="cchBuffer">缓冲区大小。</param>
/// <returns>返回写入的字符数量0 表示失败。</returns>
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern uint GetShortPathName(string lpszLongPath, StringBuilder lpszShortPath, uint cchBuffer);
/// <summary>
/// 尝试将长路径转换为短路径8.3),用于兼容部分原生库对中文/特殊字符路径处理不完整的问题。
/// </summary>
/// <param name="fullPath">长路径。</param>
/// <returns>短路径;失败返回 null。</returns>
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()

View File

@@ -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
/// </summary>
public List<CanCmdData> CmdData { get; } = new List<CanCmdData>();
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<CANScheduleConfigDto> configs)
{
var list = configs?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List<CANScheduleConfigDto>();
lock (_scheduleLock)
{
_scheduleItems = list
.Select(a => (a.MsgName!.Trim(), Math.Max(1, a.Cycle), a.OrderSend, a.SchTabIndex))
.ToList();
}
}
public void SetScheduleConfigs(IEnumerable<CANFdScheduleConfigDto> configs)
{
var list = configs?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List<CANFdScheduleConfigDto>();
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<string, DateTime>(StringComparer.Ordinal);
var cycle = new Dictionary<string, int>(StringComparer.Ordinal);
var order = new Dictionary<string, int>(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);
}
/// <summary>
/// 加载 DBC。
/// </summary>

View File

@@ -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
/// <param name="path">LDF 路径。</param>
public ObservableCollection<LinLdfModel> 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<LinLdfModel> ParseLdfFramesAndSignals(string ldfText)
{
// 说明:此解析器只用于生成“帧-信号全集池”,不做位宽/缩放等语义解析。
// 目标:尽可能容错地从 Frames 区域提取 FrameName 与其包含的 SignalName 列表。
var framesBlock = TryExtractNamedBlock(ldfText, "Frames");
if (string.IsNullOrWhiteSpace(framesBlock))
{
return new List<LinLdfModel>();
}
var result = new List<LinLdfModel>();
var exists = new HashSet<string>(StringComparer.Ordinal);
// Frame 定义一般形式FrameName : ... { ... }
// 这里以非贪婪匹配提取每个 Frame 的 body
var frameRegex = new Regex(@"(?s)(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*:\s*.*?\{(?<body>.*?)\}\s*;?", RegexOptions.Compiled);
var sigRegex = new Regex(@"(?m)^\s*(?<sig>[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;
}
}
}
}

View File

@@ -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
{
/// <summary>
/// ZLG CAN/LIN 读写配置三栏管理弹窗 ViewModel。
/// 左侧:写入/读取配置;右侧:信号全集候选池;统一保存落库。
/// </summary>
public class DialogZlgCanLinRwConfigViewModel : DialogViewModel
{
private readonly IFreeSql _freeSql;
private readonly ILogService _logService;
private readonly LogicRuleService _logicRuleService;
private long _canLinConfigProId;
/// <summary>
/// 构造函数。
/// </summary>
/// <param name="freeSql">FreeSql。</param>
/// <param name="logService">日志。</param>
/// <param name="logicRuleService">逻辑规则服务。</param>
public DialogZlgCanLinRwConfigViewModel(IFreeSql freeSql, ILogService logService, LogicRuleService logicRuleService)
{
_freeSql = freeSql;
_logService = logService;
_logicRuleService = logicRuleService;
Title = "读写设置";
WriteConfigs = new ObservableCollection<CanLinRWConfigDto>();
ReadConfigs = new ObservableCollection<CanLinRWConfigDto>();
SignalCandidates = new ObservableCollection<SignalCandidate>();
SignalTree = new ObservableCollection<SignalFrameNode>();
SignalCandidatesView = CollectionViewSource.GetDefaultView(SignalCandidates);
SignalCandidatesView.Filter = FilterSignalCandidate;
WriteNameCbxItems = new ObservableCollection<CbxItems>()
{
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<CbxItems>()
{
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;
}
/// <summary>
/// 是否允许编辑(由调用方根据 Active/打开状态决定)。
/// </summary>
public bool IsEditable { get; private set; }
/// <summary>
/// 逻辑规则集合(下拉框 ItemsSource
/// </summary>
public IReadOnlyList<LogicRuleDto> LogicRuleDtos => _logicRuleService.LogicRuleDtos;
/// <summary>
/// 写入配置“名称”下拉框集合(参考 CANConfigViewModel
/// </summary>
public ObservableCollection<CbxItems> WriteNameCbxItems { get; private set; }
/// <summary>
/// 读取配置“名称”下拉框集合(参考 CANConfigViewModel
/// </summary>
public ObservableCollection<CbxItems> ReadNameCbxItems { get; private set; }
/// <summary>
/// 写入配置集合。
/// </summary>
public ObservableCollection<CanLinRWConfigDto> WriteConfigs { get; private set; }
/// <summary>
/// 读取配置集合。
/// </summary>
public ObservableCollection<CanLinRWConfigDto> ReadConfigs { get; private set; }
/// <summary>
/// 信号候选集合(右侧池)。
/// </summary>
public ObservableCollection<SignalCandidate> SignalCandidates { get; private set; }
/// <summary>
/// 信号树(按帧分组)。
/// </summary>
public ObservableCollection<SignalFrameNode> SignalTree { get; private set; }
/// <summary>
/// 候选信号视图(含过滤)。
/// </summary>
public ICollectionView SignalCandidatesView { get; private set; }
private string? _signalFilterText;
/// <summary>
/// 信号过滤文本(按 MsgName/SignalName/Name/Desc 匹配)。
/// </summary>
public string? SignalFilterText
{
get { return _signalFilterText; }
set
{
_signalFilterText = value;
RaisePropertyChanged();
SignalCandidatesView.Refresh();
RebuildSignalTree();
}
}
/// <summary>
/// 当前选中的候选信号。
/// </summary>
public SignalCandidate? SelectedSignalCandidate { get; set; }
/// <summary>
/// 当前选中的写入配置行。
/// </summary>
public CanLinRWConfigDto? SelectedWriteConfig { get; set; }
/// <summary>
/// 当前选中的读取配置行。
/// </summary>
public CanLinRWConfigDto? SelectedReadConfig { get; set; }
private DelegateCommand<object>? _signalTreeSelectionChangedCmd;
/// <summary>
/// 右侧信号树选中变化(仅当选中叶子节点时回写 SelectedSignalCandidate
/// </summary>
public DelegateCommand<object> SignalTreeSelectionChangedCmd =>
_signalTreeSelectionChangedCmd ??= new DelegateCommand<object>(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;
/// <summary>
/// 将右侧选中信号添加到写入配置。
/// </summary>
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;
/// <summary>
/// 将右侧选中信号添加到读取配置。
/// </summary>
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;
/// <summary>
/// 从写入配置移除当前选中行。
/// </summary>
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;
/// <summary>
/// 从读取配置移除当前选中行。
/// </summary>
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;
/// <summary>
/// 保存并落库。
/// </summary>
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;
/// <summary>
/// 取消。
/// </summary>
public DelegateCommand CancelCmd => _cancelCmd ??= new DelegateCommand(CancelCmdMethod);
private void CancelCmdMethod()
{
RaiseRequestClose(new DialogResult(ButtonResult.Cancel));
}
/// <summary>
/// 弹窗打开时接收参数。
/// </summary>
/// <param name="parameters">参数。</param>
public override void OnDialogOpened(IDialogParameters parameters)
{
_canLinConfigProId = parameters.GetValue<long>("CanLinConfigProId");
IsEditable = parameters.ContainsKey("IsEditable") ? parameters.GetValue<bool>("IsEditable") : true;
RaisePropertyChanged(nameof(IsEditable));
if (parameters.ContainsKey("WriteConfigs"))
{
var list = parameters.GetValue<ObservableCollection<CanLinRWConfigDto>>("WriteConfigs") ?? new ObservableCollection<CanLinRWConfigDto>();
WriteConfigs = list;
RaisePropertyChanged(nameof(WriteConfigs));
}
if (parameters.ContainsKey("ReadConfigs"))
{
var list = parameters.GetValue<ObservableCollection<CanLinRWConfigDto>>("ReadConfigs") ?? new ObservableCollection<CanLinRWConfigDto>();
ReadConfigs = list;
RaisePropertyChanged(nameof(ReadConfigs));
}
if (parameters.ContainsKey("SignalCandidates"))
{
var list = parameters.GetValue<ObservableCollection<SignalCandidate>>("SignalCandidates") ?? new ObservableCollection<SignalCandidate>();
SignalCandidates = list;
RaisePropertyChanged(nameof(SignalCandidates));
SignalCandidatesView = CollectionViewSource.GetDefaultView(SignalCandidates);
SignalCandidatesView.Filter = FilterSignalCandidate;
RaisePropertyChanged(nameof(SignalCandidatesView));
}
RebuildSignalTree();
if (parameters.ContainsKey("Title"))
{
Title = parameters.GetValue<string>("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<SignalCandidate>(signalNodes)
};
SignalTree.Add(node);
}
RaisePropertyChanged(nameof(SignalTree));
}
/// <summary>
/// 计算候选信号是否已被添加到写入/读取。
/// 0=未添加1=已添加到写入2=已添加到读取3=同时存在于写入与读取。
/// </summary>
/// <param name="candidate">候选信号。</param>
/// <returns>AddedInfo 标志值。</returns>
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<CanLinRWConfig>()
.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<string>(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<CanLinRWConfig>(old.Id).ExecuteAffrows();
}
}
// Upsert按 keyRWInfo + SignalName更新或插入
foreach (var item in desiredAll)
{
var key = BuildKey(item.Rw, item.SignalName);
if (existingByKey.TryGetValue(key, out var old))
{
_freeSql.Update<CanLinRWConfig>(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<CanLinRWConfig>(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<CanLinRWConfigDto> 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<CanLinRWConfigDto> 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<string>(
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<string>(
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
{
/// <summary>
/// 读写类型。
/// </summary>
public RW Rw { get; }
/// <summary>
/// 信号名称。
/// </summary>
public string SignalName { get; }
/// <summary>
/// 原始 DTO。
/// </summary>
public CanLinRWConfigDto Dto { get; }
public DesiredItem(RW rw, string signalName, CanLinRWConfigDto dto)
{
Rw = rw;
SignalName = signalName;
Dto = dto;
}
}
/// <summary>
/// 右侧信号候选项。
/// </summary>
public class SignalCandidate
{
/// <summary>
/// 消息名称/帧名称。
/// </summary>
public string? MsgName { get; set; }
/// <summary>
/// 信号名称。
/// </summary>
public string? SignalName { get; set; }
/// <summary>
/// 配置名称(若解析层已有中文名则传入)。
/// </summary>
public string? Name { get; set; }
/// <summary>
/// 描述。
/// </summary>
public string? Desc { get; set; }
/// <summary>
/// 已添加标记。
/// 0=未添加1=已添加到写入2=已添加到读取3=同时存在于写入与读取。
/// </summary>
public int AddedInfo { get; set; }
}
/// <summary>
/// 帧节点。
/// </summary>
public class SignalFrameNode
{
/// <summary>
/// 帧名。
/// </summary>
public string FrameName { get; set; } = string.Empty;
/// <summary>
/// 帧内信号集合。
/// </summary>
public ObservableCollection<SignalCandidate> Signals { get; set; } = new ObservableCollection<SignalCandidate>();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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
/// </summary>
public CanLinConfigPro? SelectCanLinConfigPro { get; set; }
private ObservableCollection<CanLinRWConfigDto> _listWriteCanLinRWConfigDto = new ObservableCollection<CanLinRWConfigDto>();
public ObservableCollection<CanLinRWConfigDto> ListWriteCanLinRWConfigDto
{
get { return _listWriteCanLinRWConfigDto; }
set { _listWriteCanLinRWConfigDto = value; RaisePropertyChanged(); }
}
private ObservableCollection<CanLinRWConfigDto> _listReadCanLinRWConfigDto = new ObservableCollection<CanLinRWConfigDto>();
public ObservableCollection<CanLinRWConfigDto> ListReadCanLinRWConfigDto
{
get { return _listReadCanLinRWConfigDto; }
set { _listReadCanLinRWConfigDto = value; RaisePropertyChanged(); }
}
public bool IsRwEditable
{
get { return !ZlgLinDriveService.OpenState; }
}
private LINConfigExdDto? _SelectedLINConfigExdDto;
/// <summary>
/// 选中的 LIN 配置 DTO。
@@ -146,6 +166,14 @@ namespace CapMachine.Wpf.ViewModels
{
if (SelectCanLinConfigPro == null) return;
SelectedLINConfigExdDto = Mapper.Map<LINConfigExdDto>(SelectCanLinConfigPro.LINConfigExd);
var writeData = SelectCanLinConfigPro.CanLinConfigContents?.Where(a => a.RWInfo == RW.Write).ToList() ?? new List<CanLinRWConfig>();
ListWriteCanLinRWConfigDto = new ObservableCollection<CanLinRWConfigDto>(Mapper.Map<List<CanLinRWConfigDto>>(writeData));
var readData = SelectCanLinConfigPro.CanLinConfigContents?.Where(a => a.RWInfo == RW.Read).ToList() ?? new List<CanLinRWConfig>();
ListReadCanLinRWConfigDto = new ObservableCollection<CanLinRWConfigDto>(Mapper.Map<List<CanLinRWConfigDto>>(readData));
RaisePropertyChanged(nameof(IsRwEditable));
}
private DelegateCommand<object>? _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<CanLinRWConfigDto>(
(ListWriteCanLinRWConfigDto ?? new ObservableCollection<CanLinRWConfigDto>())
.Select(CloneRwDto));
foreach (var item in writeClones)
{
item.RWInfo = RW.Write;
}
var readClones = new ObservableCollection<CanLinRWConfigDto>(
(ListReadCanLinRWConfigDto ?? new ObservableCollection<CanLinRWConfigDto>())
.Select(CloneRwDto));
foreach (var item in readClones)
{
item.RWInfo = RW.Read;
}
var candidates = new ObservableCollection<DialogZlgCanLinRwConfigViewModel.SignalCandidate>();
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<object>? _SchEnableCmd;
/// <summary>
/// 调度表使能写入驱动。

View File

@@ -0,0 +1,331 @@
<UserControl
x:Class="CapMachine.Wpf.Views.DialogZlgCanLinRwConfigView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:local="clr-namespace:CapMachine.Wpf.Views"
xmlns:localEx="clr-namespace:CapMachine.Wpf"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
xmlns:vm="clr-namespace:CapMachine.Wpf.ViewModels"
Width="1900"
Height="900"
mc:Ignorable="d">
<UserControl.Resources>
<localEx:BindingProxy x:Key="Proxy" Data="{Binding}" />
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1.3*" />
<ColumnDefinition Width="15" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="15" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<GroupBox Grid.Row="0">
<GroupBox.Header>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2,0,5,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Foreground="White"
Text="&#xe9f8;" />
<TextBlock FontSize="18" Foreground="White">写入配置</TextBlock>
</StackPanel>
</GroupBox.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel
Grid.Row="0"
Margin="5"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="5,0"
Command="{Binding RemoveWriteCmd}"
Content="从写入移除"
Foreground="White"
IsEnabled="{Binding IsEditable}" />
</StackPanel>
<DataGrid
Grid.Row="1"
Margin="5"
AutoGenerateColumns="False"
CanUserAddRows="False"
HeadersVisibility="Column"
ItemsSource="{Binding WriteConfigs}"
SelectedItem="{Binding SelectedWriteConfig, Mode=TwoWay}"
SelectionMode="Single"
SelectionUnit="FullRow">
<DataGrid.Columns>
<DataGridTemplateColumn Width="180" Header="名称">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox
DisplayMemberPath="Text"
IsEditable="True"
IsEnabled="{Binding Source={StaticResource Proxy}, Path=Data.IsEditable}"
ItemsSource="{Binding Source={StaticResource Proxy}, Path=Data.WriteNameCbxItems}"
SelectedValue="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="Text" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="200"
Binding="{Binding MsgFrameName}"
Header="消息名称"
IsReadOnly="True" />
<DataGridTextColumn
Width="200"
Binding="{Binding SignalName}"
Header="信号名称"
IsReadOnly="True" />
<DataGridTextColumn
Width="120"
Binding="{Binding DefautValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Header="默认值" />
<DataGridTemplateColumn Width="200" Header="规则名称">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox
DisplayMemberPath="Name"
IsEnabled="{Binding Source={StaticResource Proxy}, Path=Data.IsEditable}"
ItemsSource="{Binding Source={StaticResource Proxy}, Path=Data.LogicRuleDtos}"
SelectedValue="{Binding LogicRuleId, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="Id" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</GroupBox>
<GroupBox Grid.Row="2">
<GroupBox.Header>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2,0,5,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Foreground="White"
Text="&#xe9f8;" />
<TextBlock FontSize="18" Foreground="White">读取配置</TextBlock>
</StackPanel>
</GroupBox.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel
Grid.Row="0"
Margin="5"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="5,0"
Command="{Binding RemoveReadCmd}"
Content="从读取移除"
Foreground="White"
IsEnabled="{Binding IsEditable}" />
</StackPanel>
<DataGrid
Grid.Row="1"
Margin="5"
AutoGenerateColumns="False"
CanUserAddRows="False"
HeadersVisibility="Column"
ItemsSource="{Binding ReadConfigs}"
SelectedItem="{Binding SelectedReadConfig, Mode=TwoWay}"
SelectionMode="Single"
SelectionUnit="FullRow">
<DataGrid.Columns>
<DataGridTemplateColumn Width="180" Header="名称">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox
DisplayMemberPath="Text"
IsEditable="True"
IsEnabled="{Binding Source={StaticResource Proxy}, Path=Data.IsEditable}"
ItemsSource="{Binding Source={StaticResource Proxy}, Path=Data.ReadNameCbxItems}"
SelectedValue="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="Text" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="200"
Binding="{Binding MsgFrameName}"
Header="消息名称"
IsReadOnly="True" />
<DataGridTextColumn
Width="200"
Binding="{Binding SignalName}"
Header="信号名称"
IsReadOnly="True" />
<DataGridTextColumn
Width="120"
Binding="{Binding DefautValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Header="默认值" />
<DataGridTemplateColumn Width="200" Header="规则名称">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox
DisplayMemberPath="Name"
IsEnabled="{Binding Source={StaticResource Proxy}, Path=Data.IsEditable}"
ItemsSource="{Binding Source={StaticResource Proxy}, Path=Data.LogicRuleDtos}"
SelectedValue="{Binding LogicRuleId, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="Id" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</GroupBox>
</Grid>
<GroupBox Grid.Column="2">
<GroupBox.Header>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2,0,5,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Foreground="White"
Text="&#xe9f8;" />
<TextBlock FontSize="18" Foreground="White">信号集合</TextBlock>
</StackPanel>
</GroupBox.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBox
Grid.Row="0"
Margin="5"
VerticalContentAlignment="Center"
Text="{Binding SignalFilterText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<StackPanel
Grid.Row="1"
Margin="5"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="5,0"
Command="{Binding AddToWriteCmd}"
Content="添加到写入"
Foreground="White"
IsEnabled="{Binding IsEditable}" />
<Button
Margin="5,0"
Command="{Binding AddToReadCmd}"
Content="添加到读取"
Foreground="White"
IsEnabled="{Binding IsEditable}" />
</StackPanel>
<TreeView
x:Name="SignalTreeView"
Grid.Row="2"
Margin="5"
ItemsSource="{Binding SignalTree}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type vm:DialogZlgCanLinRwConfigViewModel+SignalFrameNode}" ItemsSource="{Binding Signals}">
<TextBlock FontWeight="Bold" Text="{Binding FrameName}" />
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type vm:DialogZlgCanLinRwConfigViewModel+SignalCandidate}">
<Grid>
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Background" Value="Transparent" />
<Style.Triggers>
<DataTrigger Binding="{Binding AddedInfo}" Value="1">
<Setter Property="Background" Value="LightGreen" />
</DataTrigger>
<DataTrigger Binding="{Binding AddedInfo}" Value="2">
<Setter Property="Background" Value="SkyBlue" />
</DataTrigger>
<DataTrigger Binding="{Binding AddedInfo}" Value="3">
<Setter Property="Background" Value="Orange" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180" />
<ColumnDefinition Width="160" />
<ColumnDefinition Width="120" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding MsgName}" />
<TextBlock Grid.Column="1" Text="{Binding SignalName}" />
<TextBlock Grid.Column="2" Text="{Binding Name}" />
<TextBlock
Grid.Column="3"
Text="{Binding Desc}"
TextTrimming="CharacterEllipsis" />
</Grid>
</DataTemplate>
</TreeView.Resources>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<prism:InvokeCommandAction Command="{Binding SignalTreeSelectionChangedCmd}" CommandParameter="{Binding ElementName=SignalTreeView, Path=SelectedItem}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TreeView>
</Grid>
</GroupBox>
</Grid>
<StackPanel
Grid.Row="1"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="10,10"
Command="{Binding SaveCmd}"
Content="保存"
Foreground="White"
IsEnabled="{Binding IsEditable}" />
<Button
Margin="10,0"
Command="{Binding CancelCmd}"
Content="取消"
Foreground="White" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,15 @@
using System.Windows.Controls;
namespace CapMachine.Wpf.Views
{
/// <summary>
/// DialogZlgCanLinRwConfigView.xaml 的交互逻辑
/// </summary>
public partial class DialogZlgCanLinRwConfigView : UserControl
{
public DialogZlgCanLinRwConfigView()
{
InitializeComponent();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -174,6 +174,14 @@
VerticalContentAlignment="Center"
Text="{Binding LinBaudRate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Button
Margin="0,0,20,0"
Command="{Binding OpenRwDialogCmd}"
Foreground="White"
IsEnabled="{Binding IsRwEditable}">
<TextBlock FontSize="14" Text="读写设置" />
</Button>
<CheckBox
VerticalAlignment="Center"
Content="调度使能"