diff --git a/CapMachine.Wpf/CanDrive/CanDbcModel.cs b/CapMachine.Wpf/CanDrive/CanDbcModel.cs index fa7f0ea..97d6fb8 100644 --- a/CapMachine.Wpf/CanDrive/CanDbcModel.cs +++ b/CapMachine.Wpf/CanDrive/CanDbcModel.cs @@ -1,4 +1,4 @@ -using Prism.Mvvm; +using Prism.Mvvm; using System; using System.Collections.Generic; using System.Linq; @@ -47,8 +47,18 @@ namespace CapMachine.Wpf.CanDrive private string? _SignalRtValue = "--"; /// - /// 信号实时值 + /// 信号实时值(供 UI 绑定显示)。 /// + /// + /// - 仅当文本发生变化时才触发通知,避免无谓 UI 刷新。 + /// - 若期望进一步降低分配开销,建议在调用方(接收循环)先做“数值层去重”, + /// 即:仅在数值变化(可设置容差)时才调用 ToString() 格式化并赋值到本属性。 + /// 这样可以显著减少字符串分配与 GC 压力。 + /// + /// + /// // 示例:数值层去重后再格式化(接收循环中使用) + /// // if (Math.Abs(newVal - lastVal) > 1e-3) { model.SignalRtValue = newVal.ToString("F3"); lastVal = newVal; } + /// public string? SignalRtValue { get { return _SignalRtValue; } @@ -62,21 +72,56 @@ namespace CapMachine.Wpf.CanDrive } } - private StringBuilder _SignalRtValueSb = new StringBuilder(10); + private StringBuilder _SignalRtValueSb = new StringBuilder(16); /// - /// 信号实时值 StringBuilder + /// 信号实时值的可变文本缓冲接口。 + /// 设计目的: + /// - 避免外部传入的 StringBuilder 被直接保存(引用别名问题),统一复制文本到内部缓冲 _SignalRtValueSb; + /// - 仅当文本内容变化时才更新 SignalRtValue 并 RaisePropertyChanged,减少 UI 抖动与无谓刷新; + /// - 对 value == null 做防御:清空内部缓冲并置空显示文本(如需显示 “--” 可按需替换)。 + /// 使用方式: + /// - 接收环路可重复复用同一个外部 StringBuilder 作为临时缓存,调用本属性进行更新不会产生实例共享风险。 /// + /// + /// 注意事项: + /// 1) 本属性会复制文本,不会保存外部 StringBuilder 引用;这可避免多个模型共享同一实例导致的数据串扰和并发问题。 + /// 2) 若你的 UI 需要统一的“空值占位符”,可将 setter 中的空字符串替换为 "--",与字段初始值保持一致。 + /// 3) 线程模型:建议仍在 UI 线程更新以避免跨线程通知问题;如需后台线程更新,请确保有合适的调度(Dispatcher/同步上下文)。 + /// 4) 性能建议:结合 SignalRtValue 的备注,在进入本 setter 前尽量做数值层去重,进一步减少字符串分配。 + /// + /// + /// // 示例:接收循环中复用临时缓存 + /// // var tmp = new StringBuilder(32); + /// // ... 填充 tmp ... + /// // model.SignalRtValueSb = tmp; // 本属性会复制 tmp 的内容,安全且不会产生实例共享 + /// public StringBuilder SignalRtValueSb { get { return _SignalRtValueSb; } set { - //if (_SignalRtValueSb != value) - //{ - SignalRtValue = value.ToString(); - _SignalRtValueSb = value; - //} + // 防御:若外部传入 null,清空内部状态并复位显示文本 + // 注意:此处将显示文本设为空字符串 "",如果希望与初始占位符 "--" 一致,可改为 SignalRtValue = "--"。 + if (value == null) + { + if (_SignalRtValue != string.Empty) + { + _SignalRtValueSb.Clear(); + SignalRtValue = string.Empty; + } + return; + } + // 复制策略:不保存外部 StringBuilder 的引用,改为复制其当前文本内容 + // 这样可避免多个模型共享同一 StringBuilder 实例导致的数据串扰与线程安全问题 + var str = value.ToString(); + // 仅当文本内容确实发生变化时,才更新内部缓冲与绑定属性,减少无谓的 UI 刷新与字符串分配 + if (!string.Equals(_SignalRtValue, str, StringComparison.Ordinal)) + { + _SignalRtValueSb.Clear(); + _SignalRtValueSb.Append(str); + SignalRtValue = str; + } } } diff --git a/CapMachine.Wpf/CanDrive/ToomossCan.cs b/CapMachine.Wpf/CanDrive/ToomossCan.cs index 615d953..95dd74e 100644 --- a/CapMachine.Wpf/CanDrive/ToomossCan.cs +++ b/CapMachine.Wpf/CanDrive/ToomossCan.cs @@ -1,4 +1,4 @@ -using CapMachine.Core; +using CapMachine.Core; using CapMachine.Model.CANLIN; using CapMachine.Wpf.Dtos; using CapMachine.Wpf.Models.Tag; @@ -12,11 +12,13 @@ using Prism.Mvvm; using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Collections.Concurrent; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using System.Windows.Interop; @@ -500,6 +502,32 @@ namespace CapMachine.Wpf.CanDrive StringBuilder ValueSb = new StringBuilder(16); double[] ValueDouble = new double[5]; + // 接收缓冲池(重用,避免每轮分配) + private IntPtr RecvMsgBufferPtr = IntPtr.Zero; + private int RecvMsgBufferCapacity = 1024; + private readonly int CanMsgSize = Marshal.SizeOf(typeof(USB2CAN.CAN_MSG)); + + // 名称 StringBuilder 缓存(DBC 调用复用,避免频繁分配) + private readonly Dictionary MsgNameSBCache = new Dictionary(StringComparer.Ordinal); + private readonly Dictionary SigNameSBCache = new Dictionary(StringComparer.Ordinal); + + // 控制台调试输出开关(默认关闭,防止日志风暴) + public bool EnableConsoleDebugLog { get; set; } = false; + + // 保护接收缓冲的并发锁(接收读与关闭释放之间的互斥) + private readonly object RecvBufferSync = new object(); + + private StringBuilder GetCachedSB(Dictionary cache, string key) + { + key ??= string.Empty; + if (cache.TryGetValue(key, out var sb)) return sb; + var nsb = new StringBuilder(key); + cache[key] = nsb; + return nsb; + } + private StringBuilder GetMsgSB(string key) => GetCachedSB(MsgNameSBCache, key); + private StringBuilder GetSigSB(string key) => GetCachedSB(SigNameSBCache, key); + private bool _IsSendOk; /// /// 发送报文是否OK @@ -1258,101 +1286,117 @@ namespace CapMachine.Wpf.CanDrive /// public void StartCycleReviceCanMsg() { + // 防止重复启动,若已有任务在运行则直接返回 + if (CycleReviceTask != null && !CycleReviceTask.IsCompleted) + { + return; + } + CycleReviceTask = Task.Run(async () => { - while (IsCycleRevice) + try { - await Task.Delay(ReviceCycle); - try + while (IsCycleRevice) { - //另外一个CAN通道读取数据 - USB2CAN.CAN_MSG[] CanMsgBuffer = new USB2CAN.CAN_MSG[1024]; - //申请数据缓冲区 - IntPtr msgPtRead = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(USB2CAN.CAN_MSG)) * CanMsgBuffer.Length); - int CanNum = USB2CAN.CAN_GetMsgWithSize(DevHandle, ReadCANIndex, msgPtRead, CanMsgBuffer.Length); - //int CanNum = USB2CAN.CAN_GetMsgWithSize(DevHandle, 1, msgPtRead, CanMsgBuffer.Length);//测试用,CAN卡 CAN1和CAN2 短接时测试用 - if (CanNum > 0) + await Task.Delay(ReviceCycle); + try { - IsReviceOk = true; - Console.WriteLine("Read CanMsgNum = {0}", CanNum); - for (int i = 0; i < CanNum; i++) + // 另一个CAN通道读取数据(与 CloseDevice 释放互斥,保护指针安全) + IntPtr msgPtRead; + int CanNum; + lock (RecvBufferSync) + { + if (RecvMsgBufferPtr == IntPtr.Zero) { - //CanMsgBuffer[i] = (USB2CAN.CAN_MSG)Marshal.PtrToStructure((IntPtr)((UInt32)msgPtRead + i * Marshal.SizeOf(typeof(USB2CAN.CAN_MSG))), typeof(USB2CAN.CAN_MSG)); //有溢出报错 - CanMsgBuffer[i] = (USB2CAN.CAN_MSG)Marshal.PtrToStructure((IntPtr)(msgPtRead + i * Marshal.SizeOf(typeof(USB2CAN.CAN_MSG))), typeof(USB2CAN.CAN_MSG)); - - Console.WriteLine("CanMsg[{0}].ID = 0x{1}", i, CanMsgBuffer[i].ID.ToString("X8")); - Console.WriteLine("CanMsg[{0}].TimeStamp = {1}", i, CanMsgBuffer[i].TimeStamp); - Console.Write("CanMsg[{0}].Data = ", i); - for (int j = 0; j < CanMsgBuffer[i].DataLen; j++) - { - Console.Write("{0} ", CanMsgBuffer[i].Data[j].ToString("X2")); - } - Console.WriteLine(""); - - //报文给高速记录的服务 - HighSpeedDataService.AppendOrUpdateMsg(new Models.HighSpeed.CommMsg() - { - Category = "CAN", - MsgInfo = "0x" + CanMsgBuffer[i].ID.ToString("X8"), - MsgData = BitConverter.ToString(CanMsgBuffer[i].Data), - Time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") - }); + RecvMsgBufferPtr = Marshal.AllocHGlobal(CanMsgSize * RecvMsgBufferCapacity); } - } - else if (CanNum == 0) - { - IsReviceOk = false; - Console.WriteLine("No CAN data!"); - } - else - { - IsReviceOk = false; - Console.WriteLine("Get CAN data error!"); - } - //Console.WriteLine(""); + msgPtRead = RecvMsgBufferPtr; + CanNum = USB2CAN.CAN_GetMsgWithSize(DevHandle, ReadCANIndex, msgPtRead, RecvMsgBufferCapacity); + //int CanNum = USB2CAN.CAN_GetMsgWithSize(DevHandle, 1, msgPtRead, RecvMsgBufferCapacity);//测试用,CAN卡 CAN1和CAN2 短接时测试用 + if (CanNum > 0) + { + IsReviceOk = true; + if (EnableConsoleDebugLog) Console.WriteLine("Read CanMsgNum = {0}", CanNum); + for (int i = 0; i < CanNum; i++) + { + var msgPtr = (IntPtr)(msgPtRead + i * CanMsgSize); + var msg = (USB2CAN.CAN_MSG)Marshal.PtrToStructure(msgPtr, typeof(USB2CAN.CAN_MSG)); - //将CAN消息数据填充到信号里面,用DBC解析数据 - CAN_DBCParser.DBC_SyncCANMsgToValue(DBCHandle, msgPtRead, CanNum); + if (EnableConsoleDebugLog) + { + Console.WriteLine("CanMsg[{0}].ID = 0x{1}", i, msg.ID.ToString("X8")); + Console.WriteLine("CanMsg[{0}].TimeStamp = {1}", i, msg.TimeStamp); + Console.Write("CanMsg[{0}].Data = ", i); + for (int j = 0; j < msg.DataLen; j++) + { + Console.Write("{0} ", msg.Data[j].ToString("X2")); + } + Console.WriteLine(""); + } + + // 报文给高速记录的服务 + HighSpeedDataService.AppendOrUpdateMsg(new Models.HighSpeed.CommMsg() + { + Category = "CAN", + MsgInfo = "0x" + msg.ID.ToString("X8"), + MsgData = BitConverter.ToString(msg.Data), + Time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + }); + } + } + else if (CanNum == 0) + { + IsReviceOk = false; + if (EnableConsoleDebugLog) Console.WriteLine("No CAN data!"); + } + else + { + IsReviceOk = false; + if (EnableConsoleDebugLog) Console.WriteLine("Get CAN data error!"); + } + // 将CAN消息数据填充到信号里面,用DBC解析数据(仍在锁内,避免指针被并发释放) + CAN_DBCParser.DBC_SyncCANMsgToValue(DBCHandle, msgPtRead, CanNum); + } //循环获取消息的数据 foreach (var item in ListCanDbcModel) { - //有配置的名称的,认为是有用的,则需要读取数据 - //if (!string.IsNullOrEmpty(item.Name)) - //{ - //CAN_DBCParser.DBC_GetSignalValueStr(DBCHandle, new StringBuilder(item.MsgName), new StringBuilder(item.SignalName), ValueSb); - //double[] ValueDouble; - CAN_DBCParser.DBC_GetSignalValue(DBCHandle, new StringBuilder(item.MsgName), new StringBuilder(item.SignalName), ValueDouble); - //item.SignalRtValueSb = ValueSb; + // 复用 StringBuilder 缓存,避免频繁分配 + var msgNameSB = GetMsgSB(item.MsgName); + var sigNameSB = GetSigSB(item.SignalName); + CAN_DBCParser.DBC_GetSignalValue(DBCHandle, msgNameSB, sigNameSB, ValueDouble); item.SignalRtValue = ValueDouble[0].ToString(); //Console.Write(ValueSb.ToString()); - //} } + // 缓冲区在 CloseDevice 或任务退出的 finally 中统一释放,避免频繁申请/释放 - //释放数据缓冲区,必须释放,否则程序运行一段时间后会报内存不足 - Marshal.FreeHGlobal(msgPtRead); - Thread.Sleep(10); - ////获取信号值并打印出来 - //StringBuilder ValueStr = new StringBuilder(32); - //CAN_DBCParser.DBC_GetSignalValueStr(DBCHandle, new StringBuilder("msg_moto_speed"), new StringBuilder("moto_speed"), ValueStr); - //Console.WriteLine("moto_speed = {0}", ValueStr); - //CAN_DBCParser.DBC_GetSignalValueStr(DBCHandle, new StringBuilder("msg_oil_pressure"), new StringBuilder("oil_pressure"), ValueStr); - //Console.WriteLine("oil_pressure = {0}", ValueStr); - //CAN_DBCParser.DBC_GetSignalValueStr(DBCHandle, new StringBuilder("msg_speed_can"), new StringBuilder("speed_can"), ValueStr); - //Console.WriteLine("speed_can = {0}", ValueStr); - } - catch (Exception ex) - { - IsReviceOk = false; - LoggerService.Info("接收出现异常"); - } + } + catch (Exception ex) + { + IsReviceOk = false; + LoggerService.Info("接收出现异常"); + } //finally //{ // IsReviceOk = false; //} + } + } + finally + { + IsReviceOk = false; + // 接收任务退出时释放接收缓冲,避免仅停止接收时的缓冲常驻 + lock (RecvBufferSync) + { + if (RecvMsgBufferPtr != IntPtr.Zero) + { + try { Marshal.FreeHGlobal(RecvMsgBufferPtr); } + catch { } + finally { RecvMsgBufferPtr = IntPtr.Zero; } + } + } } - IsReviceOk = false; }); } @@ -1364,43 +1408,55 @@ namespace CapMachine.Wpf.CanDrive //另外一个CAN通道读取数据 USB2CAN.CAN_MSG[] CanMsgBuffer = new USB2CAN.CAN_MSG[10]; msgPt = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(USB2CAN.CAN_MSG)) * CanMsgBuffer.Length); - int CanNum = USB2CAN.CAN_GetMsgWithSize(DevHandle, ReadCANIndex, msgPt, CanMsgBuffer.Length); - if (CanNum > 0) + try { - Console.WriteLine("Read CanMsgNum = {0}", CanNum); - for (int i = 0; i < CanNum; i++) + int CanNum = USB2CAN.CAN_GetMsgWithSize(DevHandle, ReadCANIndex, msgPt, CanMsgBuffer.Length); + if (CanNum > 0) { - CanMsgBuffer[i] = (USB2CAN.CAN_MSG)Marshal.PtrToStructure((IntPtr)((UInt32)msgPt + i * Marshal.SizeOf(typeof(USB2CAN.CAN_MSG))), typeof(USB2CAN.CAN_MSG)); - Console.WriteLine("CanMsg[{0}].ID = 0x{1}", i, CanMsgBuffer[i].ID.ToString("X8")); - //Console.WriteLine("CanMsg[{0}].TimeStamp = {1}",i,CanMsgBuffer[i].TimeStamp); - Console.Write("CanMsg[{0}].Data = ", i); - for (int j = 0; j < CanMsgBuffer[i].DataLen; j++) + Console.WriteLine("Read CanMsgNum = {0}", CanNum); + for (int i = 0; i < CanNum; i++) { - Console.Write("{0} ", CanMsgBuffer[i].Data[j].ToString("X2")); + CanMsgBuffer[i] = (USB2CAN.CAN_MSG)Marshal.PtrToStructure((IntPtr)((UInt32)msgPt + i * Marshal.SizeOf(typeof(USB2CAN.CAN_MSG))), typeof(USB2CAN.CAN_MSG)); + Console.WriteLine("CanMsg[{0}].ID = 0x{1}", i, CanMsgBuffer[i].ID.ToString("X8")); + //Console.WriteLine("CanMsg[{0}].TimeStamp = {1}",i,CanMsgBuffer[i].TimeStamp); + Console.Write("CanMsg[{0}].Data = ", i); + for (int j = 0; j < CanMsgBuffer[i].DataLen; j++) + { + Console.Write("{0} ", CanMsgBuffer[i].Data[j].ToString("X2")); + } + Console.WriteLine(""); } - Console.WriteLine(""); + } + else if (CanNum == 0) + { + Console.WriteLine("No CAN data!"); + } + else + { + Console.WriteLine("Get CAN data error!"); + } + Console.WriteLine(""); + + //将CAN消息数据填充到信号里面 + CAN_DBCParser.DBC_SyncCANMsgToValue(DBCHandle, msgPt, CanNum); + //获取信号值并打印出来 + StringBuilder ValueStr = new StringBuilder(32); + CAN_DBCParser.DBC_GetSignalValueStr(DBCHandle, new StringBuilder("msg_moto_speed"), new StringBuilder("moto_speed"), ValueStr); + Console.WriteLine("moto_speed = {0}", ValueStr); + CAN_DBCParser.DBC_GetSignalValueStr(DBCHandle, new StringBuilder("msg_oil_pressure"), new StringBuilder("oil_pressure"), ValueStr); + Console.WriteLine("oil_pressure = {0}", ValueStr); + CAN_DBCParser.DBC_GetSignalValueStr(DBCHandle, new StringBuilder("msg_speed_can"), new StringBuilder("speed_can"), ValueStr); + Console.WriteLine("speed_can = {0}", ValueStr); + } + finally + { + if (msgPt != IntPtr.Zero) + { + try { Marshal.FreeHGlobal(msgPt); } + catch { } + finally { msgPt = IntPtr.Zero; } } } - else if (CanNum == 0) - { - Console.WriteLine("No CAN data!"); - } - else - { - Console.WriteLine("Get CAN data error!"); - } - Console.WriteLine(""); - - //将CAN消息数据填充到信号里面 - CAN_DBCParser.DBC_SyncCANMsgToValue(DBCHandle, msgPt, CanNum); - //获取信号值并打印出来 - StringBuilder ValueStr = new StringBuilder(32); - CAN_DBCParser.DBC_GetSignalValueStr(DBCHandle, new StringBuilder("msg_moto_speed"), new StringBuilder("moto_speed"), ValueStr); - Console.WriteLine("moto_speed = {0}", ValueStr); - CAN_DBCParser.DBC_GetSignalValueStr(DBCHandle, new StringBuilder("msg_oil_pressure"), new StringBuilder("oil_pressure"), ValueStr); - Console.WriteLine("oil_pressure = {0}", ValueStr); - CAN_DBCParser.DBC_GetSignalValueStr(DBCHandle, new StringBuilder("msg_speed_can"), new StringBuilder("speed_can"), ValueStr); - Console.WriteLine("speed_can = {0}", ValueStr); } @@ -1419,6 +1475,27 @@ namespace CapMachine.Wpf.CanDrive { StopSchedule(); } + + // 等待接收任务结束后释放非托管缓冲区,避免并发释放 + try + { + var task = CycleReviceTask; + if (task != null && !task.IsCompleted) + { + task.Wait(TimeSpan.FromMilliseconds(ReviceCycle + 500)); + } + } + catch { } + // 在锁内安全释放,避免与接收线程并发访问同一指针 + lock (RecvBufferSync) + { + if (RecvMsgBufferPtr != IntPtr.Zero) + { + try { Marshal.FreeHGlobal(RecvMsgBufferPtr); } + catch { } + finally { RecvMsgBufferPtr = IntPtr.Zero; } + } + } } } diff --git a/CapMachine.Wpf/CapMachine.Wpf.csproj b/CapMachine.Wpf/CapMachine.Wpf.csproj index 38d28f8..1fcae89 100644 --- a/CapMachine.Wpf/CapMachine.Wpf.csproj +++ b/CapMachine.Wpf/CapMachine.Wpf.csproj @@ -174,6 +174,7 @@ + PreserveNewest