KWD 7 months ago
parent
commit
37189108aa

+ 3 - 0
TeamAAS-VM/Core/FilePath.cs

@@ -61,5 +61,8 @@ namespace TeamAAS_VP.Core
         /// 后台脚本存放文件夹
         /// </summary>
         public static string BackgroundScriptPath = "..//Background Script";
+
+        public static string ScanRS232ConfigPath = "..//Config//ScanRS232Config.cfg";
+
     }
 }

+ 144 - 1
TeamAAS-VM/Core/Management.cs

@@ -52,6 +52,7 @@ using TeamAAS_VP.Resources.Languages;
 using TeamAAS_VP.Services;
 using TeamAAS_VP.ViewModels.Home;
 using TouchSocket.Core;
+using TouchSocket.SerialPorts;
 using TouchSocket.Sockets;
 using static MaterialDesignThemes.Wpf.Theme.ToolBar;
 using static Org.BouncyCastle.Math.EC.ECCurve;
@@ -165,6 +166,14 @@ namespace TeamAAS_VP.Core
             set { SetProperty(ref _CurrentUPH, value); }
         }
 
+        private SerialPortClient _Scan_Client;
+
+        public SerialPortClient Scan_Client
+        {
+            get { return _Scan_Client; }
+            set { _Scan_Client = value; }
+        }
+
         private XYD_ScrewDriver _ScrewDriver;
         public XYD_ScrewDriver ScrewDriver
         {
@@ -578,7 +587,66 @@ namespace TeamAAS_VP.Core
             }
         }
 
+        public async Task InitScan()
+        {
+            if (!File.Exists(FilePath.ScanRS232ConfigPath))
+            {
+                var scan = new ScanRs232ConfigModel();
+                FileHelper.WriteJsonFile(scan, FilePath.ScanRS232ConfigPath);
+            }
+
+            try
+            {
+                var res = FileHelper.ReadJsonFile<ScanRs232ConfigModel>(FilePath.ScanRS232ConfigPath);
+                if (res != null)
+                {
+                    Scan_Client = new SerialPortClient();
+                    Scan_Client.Connecting = (client1, e) => { return EasyTask.CompletedTask; };//即将连接到端口
+                    Scan_Client.Connected = (client1, e) => { return EasyTask.CompletedTask; };//成功连接到端口
+                    Scan_Client.Closing = (client1, e) => { return EasyTask.CompletedTask; };//即将从端口断开连接。此处仅主动断开才有效。
+                    Scan_Client.Closed = (client1, e) => { return EasyTask.CompletedTask; };//从端口断开连接,当连接不成功时不会触发。
+                    Scan_Client.Received = async (c, e) =>
+                    {
+                        //await Console.Out.WriteLineAsync(Encoding.UTF8.GetString(e.ByteBlock.Buffer));
+                        await Console.Out.WriteLineAsync(e.ByteBlock.Span.ToString(Encoding.ASCII));
+                    };
+                    Scan_Client.Received = Scan_DataReceived;
+
+                    await Scan_Client.SetupAsync(new TouchSocketConfig()
+                         .SetSerialPortOption(new SerialPortOption()
+                         {
+                             BaudRate = res.BaudRate,//波特率
+                             DataBits = res.DataBits,//数据位
+                             Parity = res.Parity,//校验位
+                             PortName = res.PortName,//COM
+                             StopBits = res.StopBits//停止位
+                         })
+                         .SetSerialDataHandlingAdapter(() => new PeriodPackageAdapter() { CacheTimeout = TimeSpan.FromMilliseconds(100) })
+                         );
+
+                    await Scan_Client.ConnectAsync();
+
+                    if (Scan_Client.Online)
+                    {
+                        SendTaskMessage($"扫码机已连接", MessageLevel.Debug);
+                    }
+                    else
+                    {
+                        SendTaskMessage($"扫码机未连接", MessageLevel.Debug);
+                    }
+                }
+                else
+                {
+                    SendTaskMessage("请设置扫码机串口!", MessageLevel.Error);
+                }
+            }
+            catch (Exception ex)
+            {
+                LogHelper.WriteLogError("初始化扫码枪时出错!", ex);
+                SendTaskMessage("初始化扫码枪时出错!" + ex.Message, MessageLevel.Error);
+            }
 
+        }
 
         #endregion
 
@@ -948,6 +1016,26 @@ namespace TeamAAS_VP.Core
         {
             SendTaskMessage($"{Lang.服务器}[{client.ServicePort}]{Lang.客户端}[{client.Id}:{client.IP}]{Lang.连接成功}!", MessageLevel.Debug);
         }
+
+        private async Task Scan_DataReceived(ISerialPortClient client, ReceivedDataEventArgs e)
+        {
+            try
+            {
+                string poname = client.MainSerialPort.PortName;
+                string data = e.ByteBlock.Span.ToString(Encoding.ASCII);
+                await Console.Out.WriteLineAsync(data);
+                string response = data.Split('\r')[0];
+                SendTaskMessage($"扫码机:{poname}Receive:{response}", MessageLevel.Info);
+                ScanValue = response;
+                _scanDoneEvent.Set();
+            }
+            catch (Exception ex)
+            {
+                LogHelper.WriteLogError("扭力测量仪接收信息时出错!", ex);
+                SendTaskMessage("扭力测量仪接收信息时出错!" + ex.Message, MessageLevel.Error);
+            }
+        }
+
         #endregion
 
         #region PLC
@@ -1110,7 +1198,10 @@ namespace TeamAAS_VP.Core
         PointF DownTool = new PointF();
         double _FixedUpCameraAngle = 0;
         double _MoveCameraAngle = 0;
-
+        // 扫码完成事件
+        private readonly ManualResetEventSlim _scanDoneEvent = new ManualResetEventSlim(false);
+        // 扫码结果
+        private string ScanValue = "";
         bool _LastDowmCameraExe = false;
         bool _LastScrewTeachExe = false;
         bool _LastUpCameraPickExe = false;
@@ -1119,6 +1210,7 @@ namespace TeamAAS_VP.Core
         bool _LastFixedCameraPickExe = false;
         bool _LastFixedCameraPutExe = false;
         bool _LastFixedCameracheckExe = false;
+        Int16 _LastQrCodePhotoRequest = 0;
         Int16[] _LastWorkStart = new Int16[7];
         Int16[] _LastWorkDone = new Int16[7];
         Int16 _LastRequestPosition = 0;
@@ -1996,6 +2088,57 @@ namespace TeamAAS_VP.Core
 
                     }
                 }
+                // PLC请求二维码拍照
+                else if (addressConfig.In_QrCodePhotoRequest.Address.Contains(tuple.nodeId))
+                {
+                    if (tuple.value is Int16 state)
+                    {
+                        if (_LastQrCodePhotoRequest != state)
+                        {
+                            _LastQrCodePhotoRequest = state;
+                        }
+                        else
+                        {
+                            return;
+                        }
+                        if (state == 1)
+                        {
+                            SendTaskMessage("收到PLC二维码拍照请求", MessageLevel.Debug);
+                            // 四、几个非常重要的细节(工程级)
+                            //1.Reset() 一定在发送命令前
+                            //_scanDoneEvent.Reset();      
+                            //否则:                            
+                            //上一次扫码的 Set() 还在
+                            //本次 Wait() 会直接通过(严重 bug)
+                            // 2.Wait() 不要直接在 UI 线程
+                            //你这里是 async,用:
+                            //await Task.Run(() => _scanDoneEvent.Wait(3000));
+                            ScanValue = "";
+                            _scanDoneEvent.Reset(); // 重要:先关门
+
+                            await Scan_Client.SendAsync("T\r\n");
+
+                            // 等待扫码结果,最多 3 秒
+                            bool scanOk = await Task.Run(() => _scanDoneEvent.Wait(3000));
+
+                            if (scanOk && !ScanValue.IsNullOrWhiteSpace())
+                            {
+                                SendTaskMessage($"二维码拍照成功,接收到值:{ScanValue}", MessageLevel.Alarm);
+                                await plc.WriteNodeAsync(addressConfig.Out_QrCodePhotoResault.Address, (short)1);
+                            }
+                            else
+                            {
+                                SendTaskMessage("二维码拍照失败或超时", MessageLevel.Alarm);
+                                await plc.WriteNodeAsync(addressConfig.Out_QrCodePhotoResault.Address, (short)2);
+                            }
+                        }
+                        else
+                        {
+                            SendTaskMessage("二维码拍照点位发送信号已关闭", MessageLevel.Alarm);
+                            await plc.WriteNodeAsync(addressConfig.Out_QrCodePhotoResault.Address, (Int16)0);
+                        }
+                    }
+                }
                 else
                 {
                     // 过站检查

+ 77 - 0
TeamAAS-VM/Models/PLC/PlcAddressConfig.cs

@@ -11,6 +11,28 @@ namespace TeamAAS_VP.Models.PLC
     public class PlcAddressConfig : BindableBase
     {
         #region 写入到PLC的地址
+
+
+        private PlcPointAddress _QrCodeCamera = new PlcPointAddress();
+        /// <summary>
+        /// 二维码拍照点位参数
+        /// </summary>
+        public PlcPointAddress Out_QrCodeCamera
+        {
+            get { return _QrCodeCamera; }
+            set { SetProperty(ref _QrCodeCamera, value); }
+        }
+
+        private PlcAddress _QrCodePhotoResault = new PlcAddress();
+        /// <summary>
+        /// pc发送二维码拍照点位成功后 =1 失败=2 未执行=0
+        /// </summary>
+        public PlcAddress Out_QrCodePhotoResault
+        {
+            get { return _QrCodePhotoResault; }
+            set { SetProperty(ref _QrCodePhotoResault, value); }
+        }
+
         private int _PlcNo;
         public int PlcNo
         {
@@ -320,6 +342,27 @@ namespace TeamAAS_VP.Models.PLC
         #endregion
 
         #region 从PLC读取的地址
+
+        private PlcAddress _In_QrCodePhotoRequest = new PlcAddress();
+        /// <summary>
+        /// 二维码拍照请求
+        /// </summary>
+        public PlcAddress In_QrCodePhotoRequest
+        {
+            get { return _In_QrCodePhotoRequest; }
+            set { SetProperty(ref _In_QrCodePhotoRequest, value); }
+        }
+
+        private PlcAddress _In_QrCodeCamera_ScanNum = new PlcAddress();
+        /// <summary>
+        /// 二维码拍照请求序号
+        /// </summary>
+        public PlcAddress In_QrCodeCamera_ScanNum
+        {
+            get { return _In_QrCodeCamera_ScanNum; }
+            set { SetProperty(ref _In_QrCodeCamera_ScanNum, value); }
+        }
+
         private PlcAddress _In_Code = new PlcAddress();
         /// <summary>
         /// 产品编码
@@ -597,6 +640,40 @@ namespace TeamAAS_VP.Models.PLC
                 return p;
             }
 
+            // Out_QrCodeCamera(二维码拍照点位)初始化并赋地址
+            Out_QrCodeCamera = EnsurePoint(Out_QrCodeCamera);
+            Out_QrCodeCamera.Description = "二维码拍照点位参数";
+            Out_QrCodeCamera.X_Position.Address = "ns=4;s=DB_Communication|FromPC.Point_Camera_Scan[{0}].X_Position";
+            Out_QrCodeCamera.X_Velocity.Address = "ns=4;s=DB_Communication|FromPC.Point_Camera_Scan[{0}].X_Velocity";
+            Out_QrCodeCamera.Y_Position.Address = "ns=4;s=DB_Communication|FromPC.Point_Camera_Scan[{0}].Y_Position";
+            Out_QrCodeCamera.Y_Velocity.Address = "ns=4;s=DB_Communication|FromPC.Point_Camera_Scan[{0}].Y_Velocity";
+            Out_QrCodeCamera.Z_Position_Start.Address = "ns=4;s=DB_Communication|FromPC.Point_Camera_Scan[{0}].Z_Position_Start";
+            Out_QrCodeCamera.Z_Velocity_Start.Address = "ns=4;s=DB_Communication|FromPC.Point_Camera_Scan[{0}].Z_Velocity_Start";
+            Out_QrCodeCamera.Z_Position_Stop.Address = "ns=4;s=DB_Communication|FromPC.Point_Camera_Scan[{0}].Z_Position_Stop";
+            Out_QrCodeCamera.Z_Velocity_Stop.Address = "ns=4;s=DB_Communication|FromPC.Point_Camera_Scan[{0}].Z_Velocity_Stop";
+            Out_QrCodeCamera.U_Position.Address = "ns=4;s=DB_Communication|FromPC.Point_Camera_Scan[{0}].U_Position";
+            Out_QrCodeCamera.U_Velocity.Address = "ns=4;s=DB_Communication|FromPC.Point_Camera_Scan[{0}].U_Velocity";
+            Out_QrCodeCamera.R_Position.Address = "ns=4;s=DB_Communication|FromPC.Point_Camera_Scan[{0}].R_Position";
+            Out_QrCodeCamera.R_Velocity.Address = "ns=4;s=DB_Communication|FromPC.Point_Camera_Scan[{0}].R_Velocity";
+            Out_QrCodeCamera.Torque.Address = "ns=4;s=DB_Communication|FromPC.Point_Camera_Scan[{0}].Torque";
+            Out_QrCodeCamera.Feeder.Address = "ns=4;s=DB_Communication|FromPC.Point_Camera_Scan[{0}].Feeder";
+            Out_QrCodeCamera.ScrewProNum.Address = "ns=4;s=DB_Communication|FromPC.Point_Camera_Scan[{0}].ScrewProNum";
+
+            In_QrCodePhotoRequest = Ensure(In_QrCodePhotoRequest);
+            In_QrCodePhotoRequest.Address = "ns=4;s=DB_Communication|ToPC.Camera_ScanExe";
+            In_QrCodePhotoRequest.DataType = "int16";
+            In_QrCodePhotoRequest.Description = "二维码拍照请求";
+
+            In_QrCodeCamera_ScanNum = Ensure(In_QrCodeCamera_ScanNum);
+            In_QrCodeCamera_ScanNum.Address = "ns=4;s=DB_Communication|Camera_ScanNum";
+            In_QrCodeCamera_ScanNum.DataType = "int16";
+            In_QrCodeCamera_ScanNum.Description = "二维码拍照编号";
+
+            Out_QrCodePhotoResault = Ensure(Out_QrCodePhotoResault);
+            Out_QrCodePhotoResault.Address = "ns=4;s=DB_Communication|FromPC.Camera_ScanStatus";
+            Out_QrCodePhotoResault.DataType = "int16";
+            Out_QrCodePhotoResault.Description = "二维码拍照结果";
+
             // 2. 按 JSON 提供的数据设置默认值(以当前属性名为准)
             Out_WorkMode = Ensure(Out_WorkMode);
             Out_WorkMode.Address = "ns=4;s=DB_Communication|FromPC.WorkMode";

+ 44 - 0
TeamAAS-VM/Models/ScanRs232ConfigModel.cs

@@ -0,0 +1,44 @@
+using Prism.Mvvm;
+using System;
+using System.Collections.Generic;
+using System.IO.Ports;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TeamAAS_VP.Models
+{
+    public class ScanRs232ConfigModel : BindableBase
+    {
+        private string _PortName = "COM9";
+        public string PortName
+        {
+            get { return _PortName; }
+            set { SetProperty(ref _PortName, value); }
+        }
+        private int _BaudRate = 9600;
+        public int BaudRate
+        {
+            get { return _BaudRate; }
+            set { SetProperty(ref _BaudRate, value); }
+        }
+        private Parity _Parity = Parity.None;
+        public Parity Parity
+        {
+            get { return _Parity; }
+            set { SetProperty(ref _Parity, value); }
+        }
+        private StopBits _StopBits = StopBits.One;
+        public StopBits StopBits
+        {
+            get { return _StopBits; }
+            set { SetProperty(ref _StopBits, value); }
+        }
+        private int _DataBits = 8;
+        public int DataBits
+        {
+            get { return _DataBits; }
+            set { SetProperty(ref _DataBits, value); }
+        }
+    }
+}

+ 1 - 9
TeamAAS-VM/Resources/Languages/Lang.Designer.cs

@@ -7925,15 +7925,7 @@ namespace TeamAAS_VP.Resources.Languages {
                 return ResourceManager.GetString("角点拖动调整大小", resourceCulture);
             }
         }
-        
-        /// <summary>
-        ///   查找类似 视觉静态精度分析 的本地化字符串。
-        /// </summary>
-        public static string 视觉静态精度分析 {
-            get {
-                return ResourceManager.GetString("视觉静态精度分析", resourceCulture);
-            }
-        }
+       
         
         /// <summary>
         ///   查找类似 触发命令 的本地化字符串。

+ 1 - 0
TeamAAS-VM/TeamAAS-VP.csproj

@@ -541,6 +541,7 @@
     <Compile Include="Models\DeviceInfo.cs" />
     <Compile Include="Models\Feeder\ScrewFeederInfo.cs" />
     <Compile Include="Models\PhotoCaptureRecord.cs" />
+    <Compile Include="Models\ScanRs232ConfigModel.cs" />
     <Compile Include="Models\ScrewFeederBatchRecord.cs" />
     <Compile Include="Services\MesService.cs" />
     <Compile Include="ValueConverter\CountToColumnsConverter.cs" />

+ 18 - 0
TeamAAS-VM/ViewModels/SettingViewModel.cs

@@ -14,6 +14,7 @@ using System.Collections.ObjectModel;
 using System.ComponentModel;
 using System.Drawing.Drawing2D;
 using System.Globalization;
+using System.IO;
 using System.Linq;
 using System.Text;
 using System.Windows;
@@ -340,6 +341,15 @@ namespace TeamAAS_VP.ViewModels
             get { return _CardReaderConfig; }
             set { SetProperty(ref _CardReaderConfig, value); }
         }
+
+        private ScanRs232ConfigModel _ScanConfig;
+
+        public ScanRs232ConfigModel ScanConfig
+        {
+            get { return _ScanConfig; }
+            set { SetProperty(ref _ScanConfig, value); }
+        }
+
         #endregion
 
         #region 命令
@@ -1186,6 +1196,12 @@ namespace TeamAAS_VP.ViewModels
 
         private void OnLoad()
         {
+            if (!File.Exists(FilePath.ScanRS232ConfigPath))
+            {
+                var scan = new ScanRs232ConfigModel();
+                FileHelper.WriteJsonFile(scan, FilePath.ScanRS232ConfigPath);
+            }
+            this.ScanConfig = FileHelper.ReadJsonFile<ScanRs232ConfigModel>(FilePath.ScanRS232ConfigPath);
 
         }
 
@@ -1536,6 +1552,8 @@ namespace TeamAAS_VP.ViewModels
             {
                 try
                 {
+                    FileHelper.WriteJsonFile(this.ScanConfig, FilePath.ScanRS232ConfigPath);
+                    ExecuteSaveCardReaderConfigCommand();
                     var sysConfig = _configService.GetSystemConfiguration();
                     sysConfig.WorkMode = WorkMode;
                     sysConfig.PickPointNum = PickPointNum;

+ 241 - 39
TeamAAS-VM/Views/Home/ShowVisionRender.xaml.cs

@@ -6,6 +6,7 @@ using System;
 using System.Collections.Generic;
 using System.Drawing;
 using System.IO;
+using System.Threading;
 using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
@@ -35,6 +36,10 @@ namespace TeamAAS_VP.Views.Home
         private readonly ISystemDatabaseService _systemDatabaseService;
         private readonly IProductService _productService;
 
+        // ======================= 【新增】保存任务控制:防闪退/防资源泄露 =======================
+        private CancellationTokenSource _saveImageCts = new CancellationTokenSource();
+        private readonly SemaphoreSlim _saveSemaphore = new SemaphoreSlim(2, 2);
+
         public ShowVisionRender()
         {
             InitializeComponent();
@@ -51,6 +56,19 @@ namespace TeamAAS_VP.Views.Home
             viewModel.UpdateLayout += ViewModel_UpdateLayout;
             _eventAggregator.GetEvent<RenderUpdateNotification>().Subscribe(UpdateRenderModuleSource);
             _eventAggregator.GetEvent<ProductChangedNotification>().Subscribe(ProductChanged);
+
+            // 【可选】控件卸载时取消所有保存任务,避免页面关闭后仍在截图/保存导致闪退
+            this.Unloaded += ShowVisionRender_Unloaded;
+        }
+
+        private void ShowVisionRender_Unloaded(object sender, RoutedEventArgs e)
+        {
+            try
+            {
+                _saveImageCts.Cancel();
+                _saveImageCts.Dispose();
+            }
+            catch { }
         }
 
         /// <summary>
@@ -77,25 +95,9 @@ namespace TeamAAS_VP.Views.Home
                             if (render.Record != null)
                                 disp.Record = render.Record;
 
-                            if (render.IsSaveImage)
-                            {
-                                // 注意:CreateContentBitmap 可能比较耗时,但你这里已经在 UI 线程里调用了;
-                                // 如果卡顿明显,建议把 bitmap 生成放到后台线程(需谨慎处理线程访问)
-                                App.Current.Dispatcher.BeginInvoke(new Action(() =>
-                                {
-                                    var bmp = (Bitmap)disp.CreateContentBitmap(Cognex.VisionPro.Display.CogDisplayContentBitmapConstants.Custom);
-
-                                    SaveImage(
-                                        render.SaveImageModel,
-                                        render.SaveImagePathModel,
-                                        render.SavePath,
-                                        render.Image,
-                                        bmp,
-                                        render.Result,
-                                        render.IsCompress
-                                    );
-                                }));
-                            }
+                            // ======================= 【关键优化】保存图像走安全入口 =======================
+                            RequestSaveImage(render, disp);
+
                             break;
                         }
                     }
@@ -108,6 +110,15 @@ namespace TeamAAS_VP.Views.Home
         /// </summary>
         private async void ViewModel_UpdateLayout(System.Collections.ObjectModel.ObservableCollection<Models.ShowRender> obj)
         {
+            // ======================= 【关键优化】布局刷新前先取消旧保存任务 =======================
+            try
+            {
+                _saveImageCts.Cancel();
+                _saveImageCts.Dispose();
+            }
+            catch { }
+            _saveImageCts = new CancellationTokenSource();
+
             System.Windows.Media.FontFamily font = Application.Current.Resources["DefaultFont"] as System.Windows.Media.FontFamily;
 
             // 1) 清理旧控件
@@ -132,15 +143,7 @@ namespace TeamAAS_VP.Views.Home
             // 3) 限制最大 16(防止越界)
             int count = Math.Min(obj.Count, 16);
 
-            // 4) 计算行列(1~16自动铺满:尽量接近正方形;最大 4*4)
-            //    1  -> 1*1
-            //    2  -> 1*2
-            //    3  -> 2*2
-            //    4  -> 2*2
-            //    5~6 -> 2*3
-            //    7~9 -> 3*3(9满)
-            //    10~12 -> 3*4(12满)
-            //    13~16 -> 4*4(16满)
+            // 4) 计算行列(最大 4*4)
             GetGridSize(count, out int rows, out int cols);
 
             // 5) 创建主Grid的行列
@@ -158,13 +161,39 @@ namespace TeamAAS_VP.Views.Home
                 var sr = obj[i];
 
                 // (1) 创建显示控件(WindowsFormsHost + CogRecordDisplay)
-                var host = new WindowsFormsHost()
+                var disp = new CogRecordDisplay()
                 {
                     Tag = sr.ProceductName,
-                    Child = new CogRecordDisplay()
+                };
+
+                disp.HandleCreated += (s, e) =>
+                {
+                    try
                     {
-                        Tag = sr.ProceductName,
+                        ConfigureCogDisplay(disp); // 句柄创建后再配置,最稳
+                        // 绑定 WinForms 双击事件
+                        //disp.MouseDoubleClick += (r, w) =>
+                        //{
+                        //    try
+                        //    {
+                        //        OnDisplayDoubleClick(disp);
+                        //    }
+                        //    catch (Exception ex)
+                        //    {
+                        //        LogHelper.WriteLogError("双击预览弹窗出错", ex);
+                        //    }
+                        //};
                     }
+                    catch (Exception ex)
+                    {
+                        LogHelper.WriteLogError("ConfigureCogDisplay 失败", ex);
+                    }
+                };
+
+                var host = new WindowsFormsHost()
+                {
+                    Tag = sr.ProceductName,
+                    Child = disp
                 };
                 vmRenders.Add(sr.Id, host);
 
@@ -180,9 +209,6 @@ namespace TeamAAS_VP.Views.Home
                 };
                 Titles.Add(sr.Id, title);
 
-                // (3) 配置 VisionPro 显示属性
-                //ConfigureCogDisplay((CogRecordDisplay)host.Child);
-
                 // (3) 子Grid(标题 + 显示)
                 var cell = new Grid();
                 cell.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) });  // 标题
@@ -212,7 +238,6 @@ namespace TeamAAS_VP.Views.Home
         /// </summary>
         private void GetGridSize(int count, out int rows, out int cols)
         {
-            // 默认值
             rows = 1;
             cols = 1;
 
@@ -223,7 +248,6 @@ namespace TeamAAS_VP.Views.Home
             if (count <= 9) { rows = 3; cols = 3; return; }
             if (count <= 12) { rows = 3; cols = 4; return; }
 
-            // 13~16
             rows = 4;
             cols = 4;
         }
@@ -236,11 +260,94 @@ namespace TeamAAS_VP.Views.Home
             disp.HorizontalScrollBar = false;
             disp.VerticalScrollBar = false;
             disp.AutoFit = true;
+            disp.AutoFitWithGraphics = true;
             disp.BackColor = System.Drawing.SystemColors.ActiveCaption;
         }
 
+        // ======================= 【新增】安全保存入口:解决闪退 =======================
+        private void RequestSaveImage(ShowRender render, CogRecordDisplay disp)
+        {
+            if (render == null) return;
+            if (disp == null) return;
+            if (!render.IsSaveImage) return;
+
+            // Original 模式不需要 CreateContentBitmap(你的 SaveImage 里 Original 只写 ICogImage)
+            bool needRecordedBitmap =
+                render.SaveImageModel == ProcedureSaveImageModel.Recorded ||
+                render.SaveImageModel == ProcedureSaveImageModel.OriginalAndRecorded;
+            _saveImageCts = new CancellationTokenSource();
+            var token = _saveImageCts.Token;
+
+            // 必须在控件的 UI 线程上执行 CreateContentBitmap
+            disp.BeginInvoke(new Action(() =>
+            {
+                Bitmap clonedBitmap = null;
+
+                try
+                {
+                    token.ThrowIfCancellationRequested();
+
+                    if (needRecordedBitmap)
+                    {
+                        // UI线程创建
+                        Bitmap uiBitmap = (Bitmap)disp.CreateContentBitmap(
+                            Cognex.VisionPro.Display.CogDisplayContentBitmapConstants.Custom);
+
+                        // 立刻 Clone,后台线程只用 Clone(防止控件刷新/释放导致崩溃)
+                        clonedBitmap = (Bitmap)uiBitmap.Clone();
+
+                        // UI线程立刻释放
+                        uiBitmap.Dispose();
+                    }
+
+                    // 后台保存(限并发,防止GDI/内存瞬时飙升)
+                    Task.Run(async () =>
+                    {
+                        await _saveSemaphore.WaitAsync(token);
+                        try
+                        {
+                            token.ThrowIfCancellationRequested();
+
+                            SaveImage(
+                                render.SaveImageModel,
+                                render.SaveImagePathModel,
+                                render.SavePath,
+                                render.Image,
+                                clonedBitmap,
+                                render.Result,
+                                render.IsCompress
+                            );
+                        }
+                        catch (OperationCanceledException)
+                        {
+                            // 取消属于正常情况,不记录
+                        }
+                        catch (Exception ex)
+                        {
+                            LogHelper.WriteLogError("后台保存图像任务出错!", ex);
+                        }
+                        finally
+                        {
+                            // 确保释放 Clone 的 Bitmap,避免 GDI 泄漏导致闪退
+                            try { clonedBitmap?.Dispose(); } catch { }
+                            _saveSemaphore.Release();
+                        }
+                    }, token);
+                }
+                catch (OperationCanceledException)
+                {
+                    try { clonedBitmap?.Dispose(); } catch { }
+                }
+                catch (Exception ex)
+                {
+                    try { clonedBitmap?.Dispose(); } catch { }
+                    LogHelper.WriteLogError("RequestSaveImage(UI截图阶段) 出错!", ex);
+                }
+            }));
+        }
+
         /// <summary>
-        /// 保存图像(保持你原有逻辑不变)
+        /// 保存图像(保持你原有逻辑不变,补充 Dispose 兜底,防止压缩分支不释放导致闪退
         /// </summary>
         private void SaveImage(ProcedureSaveImageModel saveImageModel,
                                ProcedureSaveImagePathModel saveImagePathModel,
@@ -361,7 +468,6 @@ namespace TeamAAS_VP.Views.Home
                         else
                         {
                             reimage.Save(Recordedpath);
-                            reimage.Dispose();
                         }
                     }
                     else if (saveImageModel == ProcedureSaveImageModel.OriginalAndRecorded)
@@ -441,7 +547,6 @@ namespace TeamAAS_VP.Views.Home
                         else
                         {
                             reimage.Save(Recordedpath);
-                            reimage.Dispose();
                         }
                     }
                 }
@@ -449,6 +554,11 @@ namespace TeamAAS_VP.Views.Home
                 {
                     LogHelper.WriteLogError("保存流程运行结果的图片时出错!", ex);
                 }
+                finally
+                {
+                    // ======================= 【关键修复】无论是否压缩都释放Bitmap =======================
+                    try { reimage?.Dispose(); } catch { }
+                }
             });
         }
 
@@ -481,5 +591,97 @@ namespace TeamAAS_VP.Views.Home
                 }
             }));
         }
+        private void OnDisplayDoubleClick(CogRecordDisplay disp)
+        {
+            if (disp == null) return;
+
+            // 必须在 WinForms/ActiveX 线程截图
+            disp.BeginInvoke(new Action(() =>
+            {
+                Bitmap bmp = null;
+
+                try
+                {
+                    if (disp.IsDisposed || !disp.IsHandleCreated)
+                        return;
+
+                    // 优先使用显示内容截图(带图形/record叠加)
+                    try
+                    {
+                        var uiBmp = (Bitmap)disp.CreateContentBitmap(
+                            Cognex.VisionPro.Display.CogDisplayContentBitmapConstants.Custom);
+
+                        bmp = (Bitmap)uiBmp.Clone();
+                        uiBmp.Dispose();
+                    }
+                    catch
+                    {
+                        // 如果 ActiveX 状态不允许截图,退化:尝试从 Image 转 Bitmap
+                        bmp = TryConvertCogImageToBitmap(disp.Image);
+                    }
+
+                    if (bmp == null) return;
+
+                    // 回到 WPF UI 线程弹窗
+                    this.Dispatcher.BeginInvoke(new Action(() =>
+                    {
+                        //var win = new ImagePreviewWindow(bmp, $"预览:{disp.Tag ?? "Display"}");
+                        //win.Owner = Window.GetWindow(this); // 让弹窗归属当前窗口
+                        //win.Show();
+                    }));
+                }
+                catch (Exception ex)
+                {
+                    LogHelper.WriteLogError("OnDisplayDoubleClick 截图/弹窗出错", ex);
+                }
+                finally
+                {
+                    // 这里 bmp 不要 Dispose,因为传给窗口了
+                    // 窗口关闭时会释放
+                }
+            }));
+        }
+
+        /// <summary>
+        /// 将 VisionPro 的 ICogImage 尝试转换成 Bitmap(作为 CreateContentBitmap 失败时的兜底)
+        /// </summary>
+        private Bitmap TryConvertCogImageToBitmap(ICogImage cogImage)
+        {
+            try
+            {
+                if (cogImage == null) return null;
+
+                // VisionPro 常用转换:CogImage8Grey / CogImage24PlanarColor / CogImage24PackedColor 等
+                // 这里给通用兜底:用 CogImageFileBMP 落地到内存流,再读回 Bitmap
+                using (var ms = new MemoryStream())
+                {
+                    // 需要引用 Cognex.VisionPro.ImageFile
+                    using (var bmpFile = new CogImageFileBMP())
+                    {
+                        // CogImageFileBMP 只能写文件路径,不直接写流
+                        // 所以用临时文件方式最稳(如你不想落盘,可以另写转换器)
+                        var tmp = Path.Combine(Path.GetTempPath(), $"vp_preview_{Guid.NewGuid():N}.bmp");
+                        try
+                        {
+                            bmpFile.Open(tmp, CogImageFileModeConstants.Write);
+                            bmpFile.Append(cogImage);
+                            bmpFile.Close();
+
+                            var bitmap = (Bitmap)Bitmap.FromFile(tmp);
+                            return (Bitmap)bitmap.Clone();
+                        }
+                        finally
+                        {
+                            try { if (File.Exists(tmp)) File.Delete(tmp); } catch { }
+                        }
+                    }
+                }
+            }
+            catch
+            {
+                return null;
+            }
+        }
+
     }
 }

+ 162 - 80
TeamAAS-VM/Views/SettingView.xaml

@@ -1844,7 +1844,7 @@ Grid.Row="1">
                     </ScrollViewer>
                 </Grid>
             </TabItem>
-            
+
             <!--螺丝供料器-->
             <TabItem>
                 <TabItem.Header>
@@ -2055,7 +2055,7 @@ Grid.Row="1">
                                       IsChecked="{Binding SelectScrewFeeder.LowLevelAlarmEnabled,Mode=TwoWay}"
                                       VerticalAlignment="Center" />
 
-                            
+
                         </Grid>
                     </ScrollViewer>
                 </Grid>
@@ -2642,83 +2642,165 @@ Grid.Row="1">
                             </StackPanel>
                         </Grid>
                         <!--刷卡参数配置-->
-                        <GroupBox Grid.Row="5"
-                                  Grid.ColumnSpan="2"
-                                  Header="刷卡器配置"
-                                  HorizontalAlignment="Left"
-                                  MinWidth="400"
-                                  Margin="0,12,0,0">
-                            <Grid Margin="10">
-                                <Grid.ColumnDefinitions>
-                                    <ColumnDefinition Width="auto" />
-                                    <ColumnDefinition Width="*" />
-                                </Grid.ColumnDefinitions>
-                                <Grid.RowDefinitions>
-                                    <RowDefinition Height="Auto" />
-                                    <RowDefinition Height="Auto" />
-                                    <RowDefinition Height="Auto" />
-                                    <RowDefinition Height="Auto" />
-                                    <RowDefinition Height="Auto" />
-                                    <RowDefinition Height="Auto" />
-                                </Grid.RowDefinitions>
-                                <TextBlock Text="PortName:"
-                                           Grid.Row="0"
-                                           Grid.Column="0"
-                                           HorizontalAlignment="Right" />
-                                <TextBox Text="{Binding CardReaderConfig.PortName,Mode=TwoWay}"
-                                         Grid.Row="0"
-                                         Grid.Column="1"
-                                         Margin="6,0" />
-                                <TextBlock Text="BaudRate:"
-                                           Grid.Row="1"
-                                           Grid.Column="0"
-                                           HorizontalAlignment="Right" />
-                                <TextBox Text="{Binding CardReaderConfig.BaudRate,Mode=TwoWay}"
-                                         Grid.Row="1"
-                                         Grid.Column="1"
-                                         Margin="6,0" />
-                                <TextBlock Text="Parity:"
-                                           Grid.Row="2"
-                                           Grid.Column="0"
-                                           HorizontalAlignment="Right" />
-                                <ComboBox Grid.Row="2"
-                                          Grid.Column="1"
-                                          SelectedItem="{Binding CardReaderConfig.Parity,Mode=TwoWay}"
-                                          ItemsSource="{Binding Source={StaticResource Parity}}" />
-                                <TextBlock Text="StopBits:"
-                                           Grid.Row="3"
-                                           Grid.Column="0"
-                                           HorizontalAlignment="Right" />
-                                <ComboBox Grid.Row="3"
-                                          Grid.Column="1"
-                                          SelectedItem="{Binding CardReaderConfig.StopBits,Mode=TwoWay}"
-                                          ItemsSource="{Binding Source={StaticResource StopBits}}" />
-                                <TextBlock Text="DataBits:"
-                                           Grid.Row="4"
-                                           Grid.Column="0"
-                                           HorizontalAlignment="Right" />
-                                <TextBox Text="{Binding CardReaderConfig.DataBits,Mode=TwoWay}"
-                                         Grid.Row="4"
-                                         Grid.Column="1"
-                                         Margin="6,0" />
-
-                                <Button Grid.Row="5"
-                                        Grid.Column="1"
-                                        Margin="0,6"
-                                        Style="{StaticResource MaterialDesignRaisedButton}"
-                                        materialDesign:ButtonAssist.CornerRadius="10"
-                                        MinWidth="120"
-                                        Command="{Binding SaveCardReaderConfigCommand}">
-                                    <StackPanel Orientation="Horizontal"
-                                                HorizontalAlignment="Center">
-                                        <materialDesign:PackIcon Kind="ContentSaveCheck"
-                                                                 Margin="0,0,6,0" />
-                                        <TextBlock VerticalAlignment="Center"
-                                                   Text="保存刷卡器配置" />
-                                    </StackPanel>
-                                </Button>
-                            </Grid>
-                        </GroupBox>
+                        <StackPanel Grid.Row="5"
+                Orientation="Horizontal">
+                            <GroupBox Grid.Row="5"
+                  Grid.ColumnSpan="2"
+                  Header="刷卡器配置"
+                  HorizontalAlignment="Left"
+                  MinWidth="170"
+                  Margin="0,12,20,0">
+                                <Grid Margin="10">
+                                    <Grid.ColumnDefinitions>
+                                        <ColumnDefinition Width="auto" />
+                                        <ColumnDefinition Width="*" />
+                                    </Grid.ColumnDefinitions>
+                                    <Grid.RowDefinitions>
+                                        <RowDefinition Height="Auto" />
+                                        <RowDefinition Height="Auto" />
+                                        <RowDefinition Height="Auto" />
+                                        <RowDefinition Height="Auto" />
+                                        <RowDefinition Height="Auto" />
+                                        <RowDefinition Height="Auto" />
+                                    </Grid.RowDefinitions>
+                                    <TextBlock Text="PortName:"
+                           Grid.Row="0"
+                           Grid.Column="0"
+                           HorizontalAlignment="Right" />
+                                    <TextBox Text="{Binding CardReaderConfig.PortName,Mode=TwoWay}"
+                         Grid.Row="0"
+                         Grid.Column="1"
+                         Margin="6,0" />
+                                    <TextBlock Text="BaudRate:"
+                           Grid.Row="1"
+                           Grid.Column="0"
+                           HorizontalAlignment="Right" />
+                                    <TextBox Text="{Binding CardReaderConfig.BaudRate,Mode=TwoWay}"
+                         Grid.Row="1"
+                         Grid.Column="1"
+                         Margin="6,0" />
+                                    <TextBlock Text="Parity:"
+                           Grid.Row="2"
+                           Grid.Column="0"
+                           HorizontalAlignment="Right" />
+                                    <ComboBox Grid.Row="2"
+                          Grid.Column="1"
+                          SelectedItem="{Binding CardReaderConfig.Parity,Mode=TwoWay}"
+                          ItemsSource="{Binding Source={StaticResource Parity}}" />
+                                    <TextBlock Text="StopBits:"
+                           Grid.Row="3"
+                           Grid.Column="0"
+                           HorizontalAlignment="Right" />
+                                    <ComboBox Grid.Row="3"
+                          Grid.Column="1"
+                          SelectedItem="{Binding CardReaderConfig.StopBits,Mode=TwoWay}"
+                          ItemsSource="{Binding Source={StaticResource StopBits}}" />
+                                    <TextBlock Text="DataBits:"
+                           Grid.Row="4"
+                           Grid.Column="0"
+                           HorizontalAlignment="Right" />
+                                    <TextBox Text="{Binding CardReaderConfig.DataBits,Mode=TwoWay}"
+                         Grid.Row="4"
+                         Grid.Column="1"
+                         Margin="6,0" />
+
+                                    <!--<Button Grid.Row="5"
+                        Grid.Column="0"
+                        Grid.ColumnSpan="2"
+                        Margin="0,10,0,0"
+                        Style="{StaticResource MaterialDesignRaisedButton}"
+                        materialDesign:ButtonAssist.CornerRadius="10"
+                        MinWidth="120"
+                        Command="{Binding SaveCardReaderConfigCommand}">
+                    <StackPanel Orientation="Horizontal"
+                                HorizontalAlignment="Center">
+                        <materialDesign:PackIcon Kind="ContentSaveCheck"
+                                                 Margin="0,0,6,0" />
+                        <TextBlock VerticalAlignment="Center"
+                                   Text="保存刷卡器配置" />
+                    </StackPanel>
+                </Button>-->
+                                </Grid>
+                            </GroupBox>
+                            <GroupBox Grid.Row="5"
+                  Grid.ColumnSpan="2"
+                  Header="扫码机配置"
+                  HorizontalAlignment="Left"
+                  MinWidth="170"
+                  Margin="0,12,0,0">
+                                <Grid Margin="10">
+                                    <Grid.ColumnDefinitions>
+                                        <ColumnDefinition Width="auto" />
+                                        <ColumnDefinition Width="*" />
+                                    </Grid.ColumnDefinitions>
+                                    <Grid.RowDefinitions>
+                                        <RowDefinition Height="Auto" />
+                                        <RowDefinition Height="Auto" />
+                                        <RowDefinition Height="Auto" />
+                                        <RowDefinition Height="Auto" />
+                                        <RowDefinition Height="Auto" />
+                                        <RowDefinition Height="Auto" />
+                                    </Grid.RowDefinitions>
+                                    <TextBlock Text="PortName:"
+                           Grid.Row="0"
+                           Grid.Column="0"
+                           HorizontalAlignment="Right" />
+                                    <TextBox Text="{Binding ScanConfig.PortName,Mode=TwoWay}"
+                         Grid.Row="0"
+                         Grid.Column="1"
+                         Margin="6,0" />
+                                    <TextBlock Text="BaudRate:"
+                           Grid.Row="1"
+                           Grid.Column="0"
+                           HorizontalAlignment="Right" />
+                                    <TextBox Text="{Binding ScanConfig.BaudRate,Mode=TwoWay}"
+                         Grid.Row="1"
+                         Grid.Column="1"
+                         Margin="6,0" />
+                                    <TextBlock Text="Parity:"
+                           Grid.Row="2"
+                           Grid.Column="0"
+                           HorizontalAlignment="Right" />
+                                    <ComboBox Grid.Row="2"
+                          Grid.Column="1"
+                          SelectedItem="{Binding ScanConfig.Parity,Mode=TwoWay}"
+                          ItemsSource="{Binding Source={StaticResource Parity}}" />
+                                    <TextBlock Text="StopBits:"
+                           Grid.Row="3"
+                           Grid.Column="0"
+                           HorizontalAlignment="Right" />
+                                    <ComboBox Grid.Row="3"
+                          Grid.Column="1"
+                          SelectedItem="{Binding ScanConfig.StopBits,Mode=TwoWay}"
+                          ItemsSource="{Binding Source={StaticResource StopBits}}" />
+                                    <TextBlock Text="DataBits:"
+                           Grid.Row="4"
+                           Grid.Column="0"
+                           HorizontalAlignment="Right" />
+                                    <TextBox Text="{Binding ScanConfig.DataBits,Mode=TwoWay}"
+                         Grid.Row="4"
+                         Grid.Column="1"
+                         Margin="6,0" />
+
+                                    <!--<Button Grid.Row="5"
+                        Grid.Column="0"
+                        Grid.ColumnSpan="2"
+                        Margin="0,10,0,0"
+                        Style="{StaticResource MaterialDesignRaisedButton}"
+                        materialDesign:ButtonAssist.CornerRadius="10"
+                        MinWidth="120"
+                        Command="{Binding SaveScanConfigConfigCommand}">
+                    <StackPanel Orientation="Horizontal"
+                                HorizontalAlignment="Center">
+                        <materialDesign:PackIcon Kind="ContentSaveCheck"
+                                                 Margin="0,0,6,0" />
+                        <TextBlock VerticalAlignment="Center"
+                                   Text="保存扫码机配置" />
+                    </StackPanel>
+                </Button>-->
+                                </Grid>
+                            </GroupBox>
+                        </StackPanel>
                     </StackPanel>
                 </ScrollViewer>
             </TabItem>
@@ -3041,7 +3123,7 @@ Grid.Row="1">
                     </Button>
                 </StackPanel>
             </TabItem>
-            
+
         </TabControl>
 
     </Grid>