Sfoglia il codice sorgente

统一层序,优化产品切换功能

zly 3 mesi fa
parent
commit
b9293f5f5b

+ 1 - 1
TeamAAS-VM/Core/ImageHelper.cs

@@ -20,7 +20,7 @@ namespace TeamAAS_VP.Core
         /// <param name="bitmap"></param>
         /// <param name="path">目标图像存储路径JPEG</param>
         /// <param name="quality">压缩等级,0到100,0 最差质量,100 最佳</param>
-        public static void CompressImage(Bitmap bitmap, string path, long quality = 100)
+        public static void CompressImage(Bitmap bitmap, string path, long quality = 20)
         {
             ImageCodecInfo codecInfo = GetEncoderInfo("image/jpeg");
             System.Drawing.Imaging.Encoder encoder = System.Drawing.Imaging.Encoder.Quality;

+ 193 - 10
TeamAAS-VM/Core/Management.cs

@@ -212,6 +212,17 @@ namespace TeamAAS_VP.Core
             set { SetProperty(ref _dfc, value); }
         }
 
+        private ObservableCollection<DataFormCtq> _dfc2 = new ObservableCollection<DataFormCtq>();
+        /// <summary>
+        /// CTQ数据集合
+        /// </summary>
+        public ObservableCollection<DataFormCtq> DFC2
+        {
+            get { return _dfc2; }
+            set { SetProperty(ref _dfc2, value); }
+        }
+
+
         private Int64 _ThrowNumber;
         /// <summary>
         /// 抛料数
@@ -1087,7 +1098,7 @@ namespace TeamAAS_VP.Core
                 monitorNodeIds.Add(string.Format(addressConfig.In_WorkDone.Address, i));
             }
             //订阅PLC地址变化
-            plc.SubscribeNodes("AutoSensor", monitorNodeIds, PlcCommandTrigger);
+            plc.SubscribeNodes("AutoSensor", monitorNodeIds, PlcCommandTrigger, OpcUaSubscriptionMode.CustomPolling);
         }
 
         /// <summary>
@@ -1286,6 +1297,13 @@ namespace TeamAAS_VP.Core
         /// </summary>
         Dictionary<string, string> gapDict = new Dictionary<string, string>();
 
+        string[] dis = new string[10];
+
+        /// <summary>
+        /// 定位结果
+        /// </summary>
+        Dictionary<string, string> DISDict = new Dictionary<string, string>();
+
         List<string> fileresult { get; set; } = new List<string>();
         /// <summary>
         /// 二次定位相机的工具坐标
@@ -1469,6 +1487,7 @@ namespace TeamAAS_VP.Core
                                         float distance = (float)Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
 
                                         SendTaskMessage($"屏幕长度: {distance:F4}", MessageLevel.Info);
+                                        dis[5] = distance.ToString();
 
                                         _FixedUpCameraAngle = Common.CalculateAngleBetweenPointsDegrees(new PointF((float)visionResults[0].X, (float)visionResults[0].Y), new PointF((float)visionResults[1].X, (float)visionResults[1].Y));
                                         SendTaskMessage($"屏幕中心点位:{toolX:F3},{toolY:F4},{_FixedUpCameraAngle:F4}", MessageLevel.Info);
@@ -1491,6 +1510,7 @@ namespace TeamAAS_VP.Core
                                                 item.X_Position += offsetX;
                                                 item.Y_Position += offsetY;
                                                 item.U_Position += offsetAngle;
+                                                await item.WriteToPlcAddress(addressConfig.Out_RemovePos, plc);
                                             }
                                         }                                                                               
 
@@ -2211,6 +2231,11 @@ namespace TeamAAS_VP.Core
                                 var dist3 = RectangleCenterCalculator.Distance(center.BottomLeft, center.BottomRight);//长2
                                 var dist4 = RectangleCenterCalculator.Distance(center.TopLeft, center.BottomLeft);//宽2
                                 LogHelper.WriteLogInfo($"dist1={dist1};dist2={dist2};dist3={dist3};dist4={dist4};");
+
+                                dis[0] = dist1.ToString();
+                                dis[1] = dist2.ToString();
+                                dis[2] = dist3.ToString();
+                                dis[3] = dist4.ToString();
                                 if (currentProduct.GlobalParamListCollection.Count >= 4)
                                 {
                                     var param = currentProduct.GlobalParamListCollection.FirstOrDefault(p => p.Number == 0);
@@ -2242,9 +2267,47 @@ namespace TeamAAS_VP.Core
                                         SendTaskMessage($"外壳宽度防呆:宽1={dist2},宽2={dist4},para1={length},para2={limtDown},para3={LimtUp}", MessageLevel.Info);
                                     }
                                 }
+                                SendTaskMessage($"固定相机锁附/组装拍照检测OK", MessageLevel.Debug);
+                                SendTaskMessage($"固定相机流程1结果:{visionResults[0].dis}", MessageLevel.Debug);
+                                SendTaskMessage($"固定相机流程2结果:{visionResults[1].dis}", MessageLevel.Debug);
+                                SendTaskMessage($"固定相机流程3结果:{visionResults[2].dis}", MessageLevel.Debug);
+                                SendTaskMessage($"固定相机流程4结果:{visionResults[3].dis}", MessageLevel.Debug);
+                                DISDict.Clear();
+                                DISDict.Add("DownDis1", visionResults[0].dis.Split(',')[1]);
+                                DISDict.Add("LeftDis1", visionResults[0].dis.Split(',')[2]);
+
+                                DISDict.Add("LeftDis2", visionResults[1].dis.Split(',')[1]);
+                                DISDict.Add("UpDis1", visionResults[1].dis.Split(',')[2]);
+
+                                DISDict.Add("DownDis2", visionResults[2].dis.Split(',')[1]);
+                                DISDict.Add("RightDis1", visionResults[2].dis.Split(',')[2]);
+
+                                DISDict.Add("UpDis2", visionResults[3].dis.Split(',')[1]);
+                                DISDict.Add("RightDis2", visionResults[3].dis.Split(',')[2]);
+
+
                             }
                             else
                             {
+                                SendTaskMessage($"固定相机锁附/组装拍照检测NG", MessageLevel.Error);
+                                SendTaskMessage($"固定相机流程1结果:{visionResults[0].dis}", MessageLevel.Debug);
+                                SendTaskMessage($"固定相机流程2结果:{visionResults[1].dis}", MessageLevel.Debug);
+                                SendTaskMessage($"固定相机流程3结果:{visionResults[2].dis}", MessageLevel.Debug);
+                                SendTaskMessage($"固定相机流程4结果:{visionResults[3].dis}", MessageLevel.Debug);
+                                DISDict.Clear();
+                                DISDict.Add("DownDis1", visionResults[0].dis.Split(',')[1]);
+                                DISDict.Add("LeftDis1", visionResults[0].dis.Split(',')[2]);
+
+                                DISDict.Add("LeftDis2", visionResults[1].dis.Split(',')[1]);
+                                DISDict.Add("UpDis1", visionResults[1].dis.Split(',')[2]);
+
+                                DISDict.Add("DownDis2", visionResults[2].dis.Split(',')[1]);
+                                DISDict.Add("RightDis1", visionResults[2].dis.Split(',')[2]);
+
+                                DISDict.Add("UpDis2", visionResults[3].dis.Split(',')[1]);
+                                DISDict.Add("RightDis2", visionResults[3].dis.Split(',')[2]);
+
+
                                 await plc.WriteNodeAsync(addressConfig.Out_FixedCameraPutStatus.Address, (Int16)2);
                                 return;
                             }
@@ -2790,7 +2853,7 @@ namespace TeamAAS_VP.Core
                                                         if (currentProduct.ID != productId)
                                                         {
                                                             //切换产品
-                                                            await _productService.LoadProductAsync(productId);
+                                                             _productService.LoadProduct(productId);
                                                             _productService.SetCurrentProduct(productId);
                                                             await WriteParameterToPlcExAsync();
                                                             SendTaskMessage($"切换产品成功", MessageLevel.Debug);
@@ -2897,7 +2960,7 @@ namespace TeamAAS_VP.Core
                                                     if (currentProduct.ID != productId)
                                                     {
                                                         //切换产品
-                                                        await _productService.LoadProductAsync(productId);
+                                                         _productService.LoadProduct(productId);
                                                         _productService.SetCurrentProduct(productId);
                                                         await WriteParameterToPlcExAsync();
                                                         SendTaskMessage($"切换产品成功", MessageLevel.Debug);
@@ -3047,7 +3110,7 @@ namespace TeamAAS_VP.Core
                                         }
                                         else assyPresRes = false;
                                         DFC.Clear();
-
+                                        DFC2.Clear();
                                         string upLimitValue = "NA", downLimitValue = "NA", midValue = "NA", saveppid = "NA";
                                         if (stationConfig.SaveCsvPPID) saveppid = ppid[i];
                                         foreach (var item in gapDict)
@@ -3113,12 +3176,12 @@ namespace TeamAAS_VP.Core
                                             {
                                                 PPID = saveppid,
                                                 PROCESS_NAME = stationConfig.DataInfo.PROCESS_NAME,
-                                                PROCESS_START_TIME = _LastMesCheckTime[i].ToString("yyyy-MM-ddTHH:mm:ss.fffffff") + "+08:00",
+                                                PROCESS_START_TIME = _LastMesCheckTime[i].ToString("yyyy-MM-ddTHH:mm:ss") + "+08:00",
                                                 CTQ_NAME = item.Key,
                                                 CTQ_VALUE = item.Value,
                                                 CTQ_LSL = downLimitValue,
                                                 CTQ_USL = upLimitValue,
-                                                PROCESS_END_TIME = serverTimeResult.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffffff") + "+08:00",
+                                                PROCESS_END_TIME = serverTimeResult.Now.ToString("yyyy-MM-ddTHH:mm:ss") + "+08:00",
                                                 SUPPLIER_NAME = stationConfig.DataInfo.SUPPLIER_NAME,
                                                 COMMODITY_TYPE = stationConfig.DataInfo.COMMODITY_TYPE,
                                                 REVISION = stationConfig.DataInfo.REVISION+"_"+ revision[i],
@@ -3142,12 +3205,12 @@ namespace TeamAAS_VP.Core
                                         {
                                             PPID = saveppid,
                                             PROCESS_NAME = stationConfig.DataInfo.PROCESS_NAME,
-                                            PROCESS_START_TIME = _LastMesCheckTime[i].ToString("yyyy-MM-ddTHH:mm:ss.fffffff") + "+08:00",
+                                            PROCESS_START_TIME = _LastMesCheckTime[i].ToString("yyyy-MM-ddTHH:mm:ss") + "+08:00",
                                             CTQ_NAME = "Assy_Pressure",
                                             CTQ_VALUE = assypress,
                                             CTQ_LSL = currentProduct.GlobalParamEx.AssemblyPressure.PressureDownLimit.ToString("F3"),
                                             CTQ_USL = currentProduct.GlobalParamEx.AssemblyPressure.PressureUpLimit.ToString("F3"),
-                                            PROCESS_END_TIME = serverTimeResult.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffffff") + "+08:00",
+                                            PROCESS_END_TIME = serverTimeResult.Now.ToString("yyyy-MM-ddTHH:mm:ss") + "+08:00",
                                             SUPPLIER_NAME = stationConfig.DataInfo.SUPPLIER_NAME,
                                             COMMODITY_TYPE = stationConfig.DataInfo.COMMODITY_TYPE,
                                             REVISION = stationConfig.DataInfo.REVISION + "_" + revision[i],
@@ -3164,6 +3227,53 @@ namespace TeamAAS_VP.Core
                                             PD_ATTR_04 = code,
                                             PD_ATTR_05 = "NA",
                                         });
+
+                                        try
+                                        {
+                                            SendTaskMessage($"左1:{DISDict["LeftDis1"]},下1:{DISDict["DownDis1"]},左2:{DISDict["LeftDis2"]},上1:{DISDict["UpDis1"]},右1:{DISDict["RightDis1"]},下2:{DISDict["DownDis2"]},右2:{DISDict["RightDis2"]},上2:{DISDict["UpDis2"]}", MessageLevel.Info);
+                                            if (i == 0)
+                                            {
+                                                DFC2.Add(new DataFormCtq
+                                                {
+                                                    PPID = saveppid,
+                                                    PROCESS_NAME = ProductMainSN[i],
+                                                    PROCESS_START_TIME = _LastMesCheckTime[i].ToString("yyyy-MM-ddTHH:mm:ss") + "+08:00",
+                                                    CTQ_NAME = dis[0],//长1
+                                                    CTQ_VALUE = dis[1],//宽1
+                                                    CTQ_LSL = dis[2],//长2
+                                                    CTQ_USL = dis[3],//宽2
+                                                    PROCESS_END_TIME = serverTimeResult.Now.ToString("yyyy-MM-ddTHH:mm:ss") + "+08:00",
+                                                    SUPPLIER_NAME = stationConfig.DeviceName,
+                                                    COMMODITY_TYPE = code,
+                                                    REVISION = dis[5],//屏幕长度
+                                                    WO = "",//
+                                                    MFG_LOT = stationConfig.DataInfo.MFG_LOT,//
+                                                    MFG_ASSY_LINE = stationConfig.DataInfo.MFG_ASSY_LINE,
+                                                    PROCESS_OUTCOME = "Pass",
+                                                    PROCESS_MACHINE_ID = DISDict["LeftDis1"],//左1
+                                                    CTQ_UNIT_OF_MEASURE = DISDict["DownDis1"],//下1
+                                                    ERROR_MESSAGE = DISDict["LeftDis2"],///左2
+                                                    PD_ATTR_01 = DISDict["UpDis1"],//上1
+                                                    PD_ATTR_02 = DISDict["RightDis1"],//右1
+                                                    PD_ATTR_03 = DISDict["DownDis2"],//下2
+                                                    PD_ATTR_04 = DISDict["RightDis2"],//右2
+                                                    PD_ATTR_05 = DISDict["UpDis2"],//上2
+                                                });
+                                            }
+                                            bool recordResult2 = RecordStationResult2(i, DFC2, ProductMainSN[i], serverTimeResult.Now.Subtract(_LastMesCheckTime[i]).TotalSeconds, _LastMesCheckTime[i], stationConfig.SaveCsvPath);
+                                            if (recordResult2)
+                                            {
+                                                SendTaskMessage("保存外壳屏幕数据成功", MessageLevel.Debug);
+                                            }
+                                            else
+                                            {
+                                                SendTaskMessage("保存外壳屏幕数据失败", MessageLevel.Alarm);
+                                            }
+                                        }
+                                        catch
+                                        {
+                                            SendTaskMessage("保存外壳屏幕数据出错", MessageLevel.Error);
+                                        }
                                         #endregion
                                     }
                                     else if (i == 1)
@@ -3257,7 +3367,9 @@ namespace TeamAAS_VP.Core
                                             PD_ATTR_05 = "NA",
                                         });
                                         #endregion
+
                                     }
+
                                     //gap-复检gap参数, press-保压站实际压力值, presstime-保压站保压时间, assypress-组装站实际压力值
                                     (bool isSuccess, string response) = await _mesService.SubmitGetAsync(i, ProductMainSN[i], comp, resultIndex == 1 ? true : false, data,fileresult, _LastMesCheckTime[i], serverTimeResult.Now);
                                     if (!stationConfig.EnableMES || isSuccess)
@@ -3391,7 +3503,7 @@ namespace TeamAAS_VP.Core
                                     }
                                     LogHelper.WriteLogMes($"【mes结束ADD】");
                                     SendTaskMessage($"mes结束ADD", MessageLevel.Info);
-                                    fileresult.Clear();
+                                    fileresult.Clear();                                   
                                     await plc.WriteNodeAsync(string.Format(addressConfig.Out_MesStatus.Address, i), (Int16)3);
                                     return;
                                 }
@@ -3491,7 +3603,6 @@ namespace TeamAAS_VP.Core
                 LogHelper.WriteLogError("PLC命令执行时出错!", ex);
             }
         }
-
         #endregion
 
         #region 方法
@@ -3897,6 +4008,78 @@ namespace TeamAAS_VP.Core
                 return false;                
             }
         }
+        private bool RecordStationResult2(int stationIndex, IEnumerable<DataFormCtq> dfcs, string productMainSN, double ct, DateTime timestamp, string savePath, bool isFirstEnter = true)
+        {
+            try
+            {
+                //获取当前工位的配置
+                DateTime dt = DateTime.Now;
+                var stationConfig = _configService.GetDeviceInfo(stationIndex);
+                if (stationConfig == null) return false;
+                //如果保存路径为空,则不保存
+                if (string.IsNullOrEmpty(savePath)) return false;
+                //savePath = $"D:\\Data\\{dt.ToString("yyyy-MM")}\\{dt.ToString("yyyy-MM-dd")}\\外壳数据\\{productMainSN}";
+                string fullPath = savePath;
+                //如果保存路径是一个文件路径,则直接保存,否则按照工位和日期进行保存
+                if (!File.Exists(savePath) || !Path.HasExtension(savePath))
+                {
+                    fullPath = savePath;
+                    //如果路径不存在,则创建
+                    // 创建目录
+                    string directory = Path.GetDirectoryName(fullPath);
+                    if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+                    {
+                        Directory.CreateDirectory(directory);
+                    }
+                }
+                string name = "屏幕外壳数据";
+                //结果保存至CSV文件,文件名格式:savePath\yyyy-MM\工位编号_工位名称_yyyy-MM-dd.csv
+                string datePath = timestamp.ToString("yyyy-MM") + "\\";
+                string dirPath = Path.Combine(savePath, datePath);
+                if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath);
+                string fileName = $"{name}_{timestamp.ToString("yyyy-MM-dd")}.csv";
+                fullPath = Path.Combine(dirPath, fileName);
+
+                StringBuilder sb = new StringBuilder();
+                if (!File.Exists(fullPath))
+                {
+                    sb.Append("PPID,SN,PROCESS_START_TIME,dis1,dis2,dis3,dis4,PROCESS_END_TIME,Product_Name,CodeSn,dis5,WO#,MFG_LOT#," +
+                        "MFG_ASSY_LINE,PROCESS_OUTCOME,Left1,Down1,Left2,Up1,Right1,Down2,Right2,Up2");
+                    sb.AppendLine();
+                }
+                if (dfcs == null || dfcs.Count() == 0)
+                {
+                    return false;
+                }
+                foreach (var dfc in dfcs)
+                {
+                    sb.Append($"{dfc.PPID},{dfc.PROCESS_NAME},{dfc.PROCESS_START_TIME},{dfc.CTQ_NAME},{dfc.CTQ_VALUE},{dfc.CTQ_LSL},{dfc.CTQ_USL},{dfc.PROCESS_END_TIME},{dfc.SUPPLIER_NAME},{dfc.COMMODITY_TYPE},{dfc.REVISION},{dfc.WO},{dfc.MFG_LOT},{dfc.MFG_ASSY_LINE},{dfc.PROCESS_OUTCOME},{dfc.PROCESS_MACHINE_ID},{dfc.CTQ_UNIT_OF_MEASURE},{dfc.ERROR_MESSAGE},{dfc.PD_ATTR_01},{dfc.PD_ATTR_02},{dfc.PD_ATTR_03},{dfc.PD_ATTR_04},{dfc.PD_ATTR_05}");
+                    sb.AppendLine();
+                }
+                // 使用 StreamWriter 以追加模式打开文件
+                StreamWriter sw = new StreamWriter(fullPath, true, Encoding.UTF8);
+                sw.Write(sb.ToString());
+                sw.Close();
+                sw.Dispose();
+                return true;
+            }
+            catch (Exception ex)
+            {
+                if (!isFirstEnter)
+                {
+                    return false;
+                }
+                LogHelper.WriteLogError("记录其他工位结果时出错!", ex);
+                //我希望在这里生成一个临时文件来保存这次的记录,因为如果本地文件被打开时可能会导致无法写入,如果不保存的话可能会丢失这次的记录,所以我想在这里生成一个临时文件来保存这次的记录
+                //生成临时文件的路径,格式为savePath\yyyy-MM\工位编号_yyyy-MM-dd_temp\工位编号_yyyy-MM-dd_HH-mm-ss-fff_temp.csv
+                string datePath = timestamp.ToString("yyyy-MM") + "\\";
+                string tempDirPath = Path.Combine(savePath, datePath, $"{stationIndex}_{timestamp.ToString("yyyy-MM-dd")}_temp");
+                string tempFileName = $"{stationIndex}_{timestamp.ToString("yyyy-MM-dd_HH-mm-ss-fff")}_temp.csv";
+                RecordStationResult(stationIndex, dfcs, productMainSN, ct, timestamp, tempFileName, false);
+                return false;
+            }
+        }
+
 
         public async Task<string> CheckConsecutiveLowCapacityAsync(string productName, int consecutiveHours, int targetPerHour)
         {

+ 382 - 19
TeamAAS-VM/Core/PLCs/OPCuaClientPLC.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
+using System.Threading;
 using System.Threading.Tasks;
 using TeamAAS_VP.Enums;
 using TeamAAS_VP.Interfaces;
@@ -12,15 +13,59 @@ using TeamAAS_VP.Models;
 
 namespace TeamAAS_VP.Core.PLCs
 {
+    /// <summary>
+    /// OPC UA 节点订阅模式。
+    /// </summary>
+    public enum OpcUaSubscriptionMode
+    {
+        /// <summary>
+        /// 使用 OPC UA 原生订阅机制,由服务端主动推送节点变更。
+        /// </summary>
+        Native,
+
+        /// <summary>
+        /// 使用客户端轮询方式定时读取节点值,并在检测到变化时触发通知。
+        /// </summary>
+        CustomPolling
+    }
+
+    /// <summary>
+    /// OPC UA 订阅配置选项,用于控制订阅模式、轮询间隔以及首次扫描通知行为。
+    /// </summary>
+    public class OpcUaSubscriptionOptions
+    {
+        /// <summary>
+        /// 获取或设置订阅模式。
+        /// 默认为 <see cref="OpcUaSubscriptionMode.Native"/>。
+        /// </summary>
+        public OpcUaSubscriptionMode Mode { get; set; } = OpcUaSubscriptionMode.Native;
+
+        /// <summary>
+        /// 获取或设置轮询间隔,单位为毫秒。
+        /// 仅在 <see cref="Mode"/> 为 <see cref="OpcUaSubscriptionMode.CustomPolling"/> 时生效。
+        /// 默认值为 50。
+        /// </summary>
+        public int PollingInterval { get; set; } = 50;
+
+        /// <summary>
+        /// 获取或设置首次扫描时是否立即通知当前值。
+        /// 仅在自定义轮询订阅模式下生效。
+        /// </summary>
+        public bool NotifyOnFirstScan { get; set; }
+    }
+
     public class OpcUaClientPLC : IPlc
     {
-        public Guid Id{ get; private set; }
+        private readonly object _pollingSubscriptionLock = new object();
+        private readonly Dictionary<string, PollingSubscriptionContext> _pollingSubscriptions = new Dictionary<string, PollingSubscriptionContext>();
+
+        public Guid Id { get; private set; }
 
         public CommunicationType CommunicationType { get; private set; }
 
         public string EndpointUrl { get; private set; }
 
-        public int Index { get; set; } 
+        public int Index { get; set; }
 
         public string Name { get; private set; }
 
@@ -33,9 +78,74 @@ namespace TeamAAS_VP.Core.PLCs
         /// </summary>
         public string NodeHeader { get; private set; }
 
+        /// <summary>
+        /// 获取或设置默认的节点订阅模式。
+        /// 在未显式传入订阅选项时,`SubscribeNodes` 将使用该模式决定采用 OPC UA 原生订阅还是客户端轮询订阅。
+        /// 默认值为 <see cref="OpcUaSubscriptionMode.Native"/>。
+        /// </summary>
+        public OpcUaSubscriptionMode DefaultSubscriptionMode { get; set; } = OpcUaSubscriptionMode.Native;
+
+        /// <summary>
+        /// 获取或设置默认的订阅轮询间隔,单位为毫秒。
+        /// 仅当默认订阅模式或实际订阅选项使用自定义轮询时生效。
+        /// 默认值为 `100`。
+        /// </summary>
+        public int DefaultSubscriptionPollingInterval { get; set; } = 50;
+
+        /// <summary>
+        /// 获取或设置在默认轮询订阅模式下,首次扫描节点时是否立即触发一次数据变更通知。
+        /// </summary>
+        public bool DefaultNotifyOnFirstScan { get; set; }
+
+        /// <summary>
+        /// PLC 连接状态变化事件。
+        /// 当 OPC UA 客户端完成连接、开始重连或重连完成时触发,第二个参数表示当前连接状态。
+        /// </summary>
         public event Action<object, bool> ConnectChangedEvent;
 
-        public OpcUaClientPLC(Guid id,int index, string name, string endpointUrl,string nodeHeader, CommunicationType communicationType)
+        /// <summary>
+        /// 轮询订阅上下文。
+        /// 用于保存某个自定义轮询订阅任务的节点集合、上次读取值、回调处理器以及取消控制信息。
+        /// </summary>
+        private class PollingSubscriptionContext
+        {
+            /// <summary>
+            /// 订阅唯一标识,用于区分不同的订阅任务。
+            /// </summary>
+            public string Key { get; set; }
+
+            /// <summary>
+            /// 当前订阅需要轮询的节点集合。
+            /// </summary>
+            public List<string> NodeIds { get; set; }
+
+            /// <summary>
+            /// 记录每个节点上一次读取到的值,用于比较是否发生变化。
+            /// </summary>
+            public Dictionary<string, object> LastValues { get; } = new Dictionary<string, object>();
+
+            /// <summary>
+            /// 节点值发生变化时执行的回调委托。
+            /// </summary>
+            public Action<(string key, string nodeId, object value)> DataChangeHandler { get; set; }
+
+            /// <summary>
+            /// 用于控制轮询任务取消的令牌源。
+            /// </summary>
+            public CancellationTokenSource CancellationTokenSource { get; set; }
+
+            /// <summary>
+            /// 轮询周期,单位为毫秒。
+            /// </summary>
+            public int PollingInterval { get; set; }
+
+            /// <summary>
+            /// 指示首次扫描时是否立即上报当前节点值。
+            /// </summary>
+            public bool NotifyOnFirstScan { get; set; }
+        }
+
+        public OpcUaClientPLC(Guid id, int index, string name, string endpointUrl, string nodeHeader, CommunicationType communicationType)
         {
             Id = id;
             Index = index;
@@ -63,9 +173,9 @@ namespace TeamAAS_VP.Core.PLCs
         public OpcUaClientPLC(PlcInfo plcInfo)
         {
             Id = plcInfo.Id;
-            Index=plcInfo.Index;
+            Index = plcInfo.Index;
             Name = plcInfo.Name;
-            NodeHeader=plcInfo.NodeHeader;
+            NodeHeader = plcInfo.NodeHeader;
             EndpointUrl = plcInfo.EndpointUrl;
             CommunicationType = plcInfo.CommunicationType;
             OpcUaClient = new OpcUaClient();
@@ -102,6 +212,7 @@ namespace TeamAAS_VP.Core.PLCs
 
         public void Dispose()
         {
+            StopAllPollingSubscriptions();
             OpcUaClient.Disconnect();
             OpcUaClient.ConnectComplete -= OpcUaClient_ConnectComplete;
             OpcUaClient.ReconnectStarting -= OpcUaClient_ReconnectStarting;
@@ -173,6 +284,25 @@ namespace TeamAAS_VP.Core.PLCs
             return result;
         }
 
+        /// <summary>
+        /// 读取单个PLC Bool布尔节点值
+        /// </summary>
+        /// <param name="nodeId">Excel里PLC变量名 Stop.XXX</param>
+        /// <returns>bool类型真实状态</returns>
+        public bool ReadBoolNode(string nodeId)
+        {
+            string readNodeId = nodeId;
+            if (!nodeId.StartsWith(NodeHeader))
+            {
+                readNodeId = NodeHeader + nodeId;
+            }
+
+            var value = OpcUaClient.ReadNode(new NodeId(readNodeId));
+            // 安全转换为布尔类型
+            return Convert.ToBoolean(value.Value);
+        }
+
+
         /// <summary>
         /// 读取单个节点的值
         /// </summary>
@@ -303,23 +433,95 @@ namespace TeamAAS_VP.Core.PLCs
         }
 
         /// <summary>
-        /// 订阅多个节点
+        /// 使用当前实例的默认订阅配置订阅多个节点。
+        /// </summary>
+        /// <param name="key">订阅唯一标识,用于区分和管理同一客户端上的不同订阅任务。</param>
+        /// <param name="nodeIds">需要订阅的节点标识集合,可传入带或不带 <see cref="NodeHeader"/> 前缀的节点名。</param>
+        /// <param name="dataChangeHandler">节点值变化时触发的回调,返回订阅键、节点标识和值。</param>
+        public void SubscribeNodes(string key, List<string> nodeIds, Action<(string key, string nodeId, object value)> dataChangeHandler)
+        {
+            SubscribeNodes(key, nodeIds, dataChangeHandler, new OpcUaSubscriptionOptions
+            {
+                Mode = DefaultSubscriptionMode,
+                PollingInterval = DefaultSubscriptionPollingInterval,
+                NotifyOnFirstScan = DefaultNotifyOnFirstScan
+            });
+        }
+
+        /// <summary>
+        /// 根据传入的OpcUaSubscriptionMode选项订阅多个节点
         /// </summary>
         /// <param name="key"></param>
         /// <param name="nodeIds"></param>
         /// <param name="dataChangeHandler"></param>
-        public void SubscribeNodes(string key, List<string> nodeIds, Action<(string key,string nodeId,object value)> dataChangeHandler)
+        /// <param name="mode"></param>
+        public void SubscribeNodes(string key, List<string> nodeIds, Action<(string key, string nodeId, object value)> dataChangeHandler, OpcUaSubscriptionMode mode)
+        {
+            var options = new OpcUaSubscriptionOptions
+            {
+                Mode = mode,
+                PollingInterval = DefaultSubscriptionPollingInterval,
+                NotifyOnFirstScan = DefaultNotifyOnFirstScan
+            };
+            SubscribeNodes(key, nodeIds, dataChangeHandler, options);
+        }
+
+        /// <summary>
+        /// 根据指定订阅选项订阅多个节点。
+        /// </summary>
+        /// <remarks>
+        /// 该方法会先停止同一 <paramref name="key"/> 对应的轮询订阅,再根据 <paramref name="options"/> 中的模式
+        /// 选择使用 OPC UA 原生订阅或客户端轮询订阅。节点列表会自动去重并忽略空白项。
+        /// </remarks>
+        /// <param name="key">订阅唯一标识,不能为空或仅包含空白字符。</param>
+        /// <param name="nodeIds">待订阅的节点集合。</param>
+        /// <param name="dataChangeHandler">节点值变化通知回调。</param>
+        /// <param name="options">订阅配置;为 <c>null</c> 时使用默认配置。</param>
+        /// <exception cref="ArgumentException"><paramref name="key"/> 为空或仅包含空白字符时抛出。</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="nodeIds"/> 或 <paramref name="dataChangeHandler"/> 为 <c>null</c> 时抛出。</exception>
+        public void SubscribeNodes(string key, List<string> nodeIds, Action<(string key, string nodeId, object value)> dataChangeHandler, OpcUaSubscriptionOptions options)
+        {
+            if (string.IsNullOrWhiteSpace(key))
+            {
+                throw new ArgumentException("订阅Key不能为空。", nameof(key));
+            }
+            if (nodeIds == null)
+            {
+                throw new ArgumentNullException(nameof(nodeIds));
+            }
+            if (dataChangeHandler == null)
+            {
+                throw new ArgumentNullException(nameof(dataChangeHandler));
+            }
+
+            var subscriptionOptions = options ?? new OpcUaSubscriptionOptions();
+            var normalizedNodeIds = nodeIds.Where(s => !string.IsNullOrWhiteSpace(s)).Distinct().ToList();
+            StopPollingSubscription(key);
+
+            if (subscriptionOptions.Mode == OpcUaSubscriptionMode.CustomPolling)
+            {
+                StartPollingSubscription(key, normalizedNodeIds, dataChangeHandler, subscriptionOptions);
+                return;
+            }
+
+            SubscribeNodesByOpc(key, normalizedNodeIds, dataChangeHandler);
+        }
+
+        /// <summary>
+        /// 使用 OPC UA 原生订阅机制订阅节点。
+        /// </summary>
+        /// <remarks>
+        /// 订阅前会自动补齐节点前缀;收到服务端推送后,会移除返回节点中的 <see cref="NodeHeader"/> 前缀,
+        /// 再将变化值通过回调透出给业务层。
+        /// </remarks>
+        /// <param name="key">订阅唯一标识。</param>
+        /// <param name="nodeIds">节点集合。</param>
+        /// <param name="dataChangeHandler">节点值变化回调。</param>
+        private void SubscribeNodesByOpc(string key, List<string> nodeIds, Action<(string key, string nodeId, object value)> dataChangeHandler)
         {
             OpcUaClient.AddSubscription(key, nodeIds.Select(s =>
             {
-                if (s.StartsWith(NodeHeader))
-                {
-                    return s;
-                }
-                else
-                {
-                    return NodeHeader + s;
-                }
+                return NormalizeNodeId(s);
             }).ToArray(), (key1, monitoredItem, args) =>
             {
                 MonitoredItemNotification notification = args.NotificationValue as MonitoredItemNotification;
@@ -328,14 +530,175 @@ namespace TeamAAS_VP.Core.PLCs
                     return;
                 }
                 string nodeId = monitoredItem.StartNodeId.ToString();
-                if (nodeId.StartsWith(NodeHeader))
-                {
-                    nodeId= nodeId.Substring(NodeHeader.Length);
-                }
+                nodeId = TrimNodeHeader(nodeId);
                 // 触发数据变化事件
                 dataChangeHandler?.Invoke((key1, nodeId, notification.Value.WrappedValue.Value));
             });
         }
+
+        /// <summary>
+        /// 启动自定义轮询订阅任务。
+        /// </summary>
+        /// <remarks>
+        /// 该方法会创建轮询上下文并登记到内部订阅字典中,然后在后台任务中执行轮询循环。
+        /// 轮询间隔最小限制为 10 毫秒,以避免过于频繁的读取请求。
+        /// </remarks>
+        /// <param name="key">订阅唯一标识。</param>
+        /// <param name="nodeIds">需要轮询的节点集合。</param>
+        /// <param name="dataChangeHandler">节点值变化回调。</param>
+        /// <param name="options">轮询订阅选项。</param>
+        private void StartPollingSubscription(string key, List<string> nodeIds, Action<(string key, string nodeId, object value)> dataChangeHandler, OpcUaSubscriptionOptions options)
+        {
+            var context = new PollingSubscriptionContext
+            {
+                Key = key,
+                NodeIds = nodeIds,
+                DataChangeHandler = dataChangeHandler,
+                CancellationTokenSource = new CancellationTokenSource(),
+                PollingInterval = Math.Max(10, options.PollingInterval),
+                NotifyOnFirstScan = options.NotifyOnFirstScan
+            };
+
+            lock (_pollingSubscriptionLock)
+            {
+                _pollingSubscriptions[key] = context;
+            }
+
+            Task.Run(() => PollingSubscriptionLoopAsync(context));
+        }
+
+        /// <summary>
+        /// 执行轮询订阅的后台循环。
+        /// </summary>
+        /// <remarks>
+        /// 当客户端已连接且存在待轮询节点时,循环会批量读取节点值,并与上次缓存值进行比较:
+        /// 首次读取时根据配置决定是否立即通知,后续仅在值发生变化时触发回调。
+        /// 当取消令牌触发或发生取消异常时,循环退出。
+        /// </remarks>
+        /// <param name="context">轮询订阅上下文。</param>
+        /// <returns>表示异步轮询操作的任务。</returns>
+        private async Task PollingSubscriptionLoopAsync(PollingSubscriptionContext context)
+        {
+            while (!context.CancellationTokenSource.IsCancellationRequested)
+            {
+                try
+                {
+                    if (IsConnected && context.NodeIds.Count > 0)
+                    {
+                        var values = await ReadNodesAsync(context.NodeIds.ToArray()).ConfigureAwait(false);
+                        foreach (var nodeId in context.NodeIds)
+                        {
+                            object currentValue;
+                            if (!values.TryGetValue(nodeId, out currentValue))
+                            {
+                                continue;
+                            }
+
+                            object lastValue;
+                            if (!context.LastValues.TryGetValue(nodeId, out lastValue))
+                            {
+                                context.LastValues[nodeId] = currentValue;
+                                if (context.NotifyOnFirstScan)
+                                {
+                                    context.DataChangeHandler?.Invoke((context.Key, nodeId, currentValue));
+                                }
+                                continue;
+                            }
+
+                            if (!Utils.IsEqual(lastValue, currentValue))
+                            {
+                                context.LastValues[nodeId] = currentValue;
+                                context.DataChangeHandler?.Invoke((context.Key, nodeId, currentValue));
+                            }
+                        }
+                    }
+                }
+                catch (OperationCanceledException)
+                {
+                    break;
+                }
+                catch (Exception)
+                {
+                }
+
+                try
+                {
+                    await Task.Delay(context.PollingInterval, context.CancellationTokenSource.Token).ConfigureAwait(false);
+                }
+                catch (OperationCanceledException)
+                {
+                    break;
+                }
+            }
+        }
+
+        /// <summary>
+        /// 停止指定键对应的轮询订阅。
+        /// </summary>
+        /// <param name="key">要停止的订阅唯一标识。</param>
+        private void StopPollingSubscription(string key)
+        {
+            PollingSubscriptionContext context = null;
+            lock (_pollingSubscriptionLock)
+            {
+                if (_pollingSubscriptions.TryGetValue(key, out context))
+                {
+                    _pollingSubscriptions.Remove(key);
+                }
+            }
+
+            context?.CancellationTokenSource.Cancel();
+            context?.CancellationTokenSource.Dispose();
+        }
+
+        /// <summary>
+        /// 停止并清理当前实例中的所有轮询订阅。
+        /// </summary>
+        private void StopAllPollingSubscriptions()
+        {
+            List<PollingSubscriptionContext> contexts;
+            lock (_pollingSubscriptionLock)
+            {
+                contexts = _pollingSubscriptions.Values.ToList();
+                _pollingSubscriptions.Clear();
+            }
+
+            foreach (var context in contexts)
+            {
+                context.CancellationTokenSource.Cancel();
+                context.CancellationTokenSource.Dispose();
+            }
+        }
+
+        /// <summary>
+        /// 规范化节点标识,确保其包含节点头前缀。
+        /// </summary>
+        /// <param name="nodeId">原始节点标识。</param>
+        /// <returns>包含 <see cref="NodeHeader"/> 前缀的完整节点标识。</returns>
+        private string NormalizeNodeId(string nodeId)
+        {
+            if (nodeId.StartsWith(NodeHeader))
+            {
+                return nodeId;
+            }
+
+            return NodeHeader + nodeId;
+        }
+
+        /// <summary>
+        /// 去除节点标识中的节点头前缀。
+        /// </summary>
+        /// <param name="nodeId">完整节点标识。</param>
+        /// <returns>移除 <see cref="NodeHeader"/> 前缀后的节点标识;若原值不包含此前缀则直接返回原值。</returns>
+        private string TrimNodeHeader(string nodeId)
+        {
+            if (nodeId.StartsWith(NodeHeader))
+            {
+                return nodeId.Substring(NodeHeader.Length);
+            }
+
+            return nodeId;
+        }
         #region OPC事件
         private void OpcUaClient_OpcStatusChange(object sender, OpcUaStatusEventArgs e)
         {

+ 1 - 1
TeamAAS-VM/Interfaces/IRemoteCommandService.cs

@@ -260,7 +260,7 @@ namespace TeamAAS_VP.Interfaces
         /// <param name="positionIndex"></param>
         /// <param name="remark"></param>
         /// <returns></returns>
-        Task<(bool IsSucceed, double X, double Y, double U)> ExecutePhotoGetSinglePoint(ProcedureModel procedure, Dictionary<string, string> InputTerminal, double[] robotCoord, CancellationToken cancellationToken, int positionIndex, string remark);
+        Task<(bool IsSucceed, double X, double Y, double U,string dis)> ExecutePhotoGetSinglePoint(ProcedureModel procedure, Dictionary<string, string> InputTerminal, double[] robotCoord, CancellationToken cancellationToken, int positionIndex, string remark);
 
         /// <summary>
         /// 执行相机拍照获取单个点位结果,并且返回图像和图形

+ 37 - 67
TeamAAS-VM/Resources/Languages/Lang.Designer.cs

@@ -19,7 +19,7 @@ namespace TeamAAS_VP.Resources.Languages {
     // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
     // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
     // (以 /str 作为命令选项),或重新生成 VS 项目。
-    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
     [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
     [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
     public class Lang {
@@ -3516,6 +3516,15 @@ namespace TeamAAS_VP.Resources.Languages {
             }
         }
         
+        /// <summary>
+        ///   查找类似 屏幕中心基准点位 的本地化字符串。
+        /// </summary>
+        public static string 屏幕中心基准点位 {
+            get {
+                return ResourceManager.GetString("屏幕中心基准点位", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   查找类似 峰度: 的本地化字符串。
         /// </summary>
@@ -6054,6 +6063,15 @@ namespace TeamAAS_VP.Resources.Languages {
             }
         }
         
+        /// <summary>
+        ///   查找类似 流线 的本地化字符串。
+        /// </summary>
+        public static string 流线 {
+            get {
+                return ResourceManager.GetString("流线", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   查找类似 测试 的本地化字符串。
         /// </summary>
@@ -6504,6 +6522,15 @@ namespace TeamAAS_VP.Resources.Languages {
             }
         }
         
+        /// <summary>
+        ///   查找类似 生产 的本地化字符串。
+        /// </summary>
+        public static string 生产 {
+            get {
+                return ResourceManager.GetString("生产", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   查找类似 生产排班 的本地化字符串。
         /// </summary>
@@ -8376,6 +8403,15 @@ namespace TeamAAS_VP.Resources.Languages {
             }
         }
         
+        /// <summary>
+        ///   查找类似 调机 的本地化字符串。
+        /// </summary>
+        public static string 调机 {
+            get {
+                return ResourceManager.GetString("调机", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   查找类似 调试 的本地化字符串。
         /// </summary>
@@ -9230,71 +9266,5 @@ namespace TeamAAS_VP.Resources.Languages {
                 return ResourceManager.GetString("高功率", resourceCulture);
             }
         }
-
-        /// <summary>
-        ///   查找类似 组装压力SPC控制图 的本地化字符串。
-        /// </summary>
-        public static string 组装压力SPC控制图
-        {
-            get
-            {
-                return ResourceManager.GetString("组装压力SPC控制图", resourceCulture);
-            }
-        }
-
-        /// <summary>
-        ///   查找类似 压力值 的本地化字符串。
-        /// </summary>
-        public static string 压力值
-        {
-            get
-            {
-                return ResourceManager.GetString("压力值", resourceCulture);
-            }
-        }
-
-        /// <summary>
-        ///   查找类似 实际压力值 的本地化字符串。
-        /// </summary>
-        public static string 实际压力值
-        {
-            get
-            {
-                return ResourceManager.GetString("实际压力值", resourceCulture);
-            }
-        }
-
-        /// <summary>
-        ///   查找类似 UCL上管制限 的本地化字符串。
-        /// </summary>
-        public static string UCL上管制限
-        {
-            get
-            {
-                return ResourceManager.GetString("UCL上管制限", resourceCulture);
-            }
-        }
-
-        /// <summary>
-        ///   查找类似 CL中心线 的本地化字符串。
-        /// </summary>
-        public static string CL中心线
-        {
-            get
-            {
-                return ResourceManager.GetString("CL中心线", resourceCulture);
-            }
-        }
-
-        /// <summary>
-        ///   查找类似 LCL下管制限 的本地化字符串。
-        /// </summary>
-        public static string LCL下管制限
-        {
-            get
-            {
-                return ResourceManager.GetString("LCL下管制限", resourceCulture);
-            }
-        }
     }
 }

+ 12 - 9
TeamAAS-VM/Resources/Languages/Lang.en.resx

@@ -2019,6 +2019,15 @@
   <data name="请选择要启用的光源通道" xml:space="preserve">
     <value>Please select a light channel to enable</value>
   </data>
+  <data name="是否控制光源通道亮灭" xml:space="preserve">
+    <value>Control light source channel on/off</value>
+  </data>
+  <data name="CameraBrand_opt" xml:space="preserve">
+    <value>OPT (OPT Camera Viewer)</value>
+  </data>
+  <data name="相机焦距评价" xml:space="preserve">
+    <value>Camera Focus Evaluation</value>
+  </data>
   <data name="螺丝供料器" xml:space="preserve">
     <value>Screw Feeder</value>
   </data>
@@ -3162,15 +3171,6 @@
   <data name="是否延迟保存图片" xml:space="preserve">
     <value>Do you want to delay saving the picture</value>
   </data>
-  <data name="是否控制光源通道亮灭" xml:space="preserve">
-    <value>Control light source channel on/off</value>
-  </data>
-  <data name="CameraBrand_opt" xml:space="preserve">
-    <value>OPT (OPT Camera Viewer)</value>
-  </data>
-  <data name="相机焦距评价" xml:space="preserve">
-    <value>Camera Focus Evaluation</value>
-  </data>
   <data name="SettingView_Language_Vietnamese" xml:space="preserve">
     <value>Vietnamese</value>
   </data>
@@ -3183,4 +3183,7 @@
   <data name="流线" xml:space="preserve">
     <value>streamline</value>
   </data>
+  <data name="屏幕中心基准点位" xml:space="preserve">
+    <value>屏幕中心基准点位</value>
+  </data>
 </root>

+ 12 - 9
TeamAAS-VM/Resources/Languages/Lang.ja-JP.resx

@@ -2019,6 +2019,15 @@
   <data name="请选择要启用的光源通道" xml:space="preserve">
     <value>有効にするライトチャンネルを選択してください</value>
   </data>
+  <data name="是否控制光源通道亮灭" xml:space="preserve">
+    <value>光源チャンネルの点灯・消灯を制御しますか</value>
+  </data>
+  <data name="CameraBrand_opt" xml:space="preserve">
+    <value>オプト(OPT Camera Viewer)</value>
+  </data>
+  <data name="相机焦距评价" xml:space="preserve">
+    <value>カメラ焦点評価</value>
+  </data>
   <data name="螺丝供料器" xml:space="preserve">
     <value>ネジ供給機</value>
   </data>
@@ -3162,15 +3171,6 @@
   <data name="是否延迟保存图片" xml:space="preserve">
     <value>画像の保存を遅延しますか</value>
   </data>
-  <data name="是否控制光源通道亮灭" xml:space="preserve">
-    <value>光源チャンネルの点灯・消灯を制御しますか</value>
-  </data>
-  <data name="CameraBrand_opt" xml:space="preserve">
-    <value>オプト(OPT Camera Viewer)</value>
-  </data>
-  <data name="相机焦距评价" xml:space="preserve">
-    <value>カメラ焦点評価</value>
-  </data>
   <data name="SettingView_Language_Vietnamese" xml:space="preserve">
     <value>ベトナム語</value>
   </data>
@@ -3183,4 +3183,7 @@
   <data name="流线" xml:space="preserve">
     <value>流線</value>
   </data>
+  <data name="屏幕中心基准点位" xml:space="preserve">
+    <value>屏幕中心基准点位</value>
+  </data>
 </root>

+ 3 - 0
TeamAAS-VM/Resources/Languages/Lang.resx

@@ -3278,4 +3278,7 @@
   <data name="流线" xml:space="preserve">
     <value>流线</value>
   </data>
+  <data name="屏幕中心基准点位" xml:space="preserve">
+    <value>屏幕中心基准点位</value>
+  </data>
 </root>

+ 3 - 0
TeamAAS-VM/Resources/Languages/Lang.vi.resx

@@ -3183,4 +3183,7 @@
   <data name="流线" xml:space="preserve">
     <value>đường dòng</value>
   </data>
+  <data name="屏幕中心基准点位" xml:space="preserve">
+    <value>屏幕中心基准点位</value>
+  </data>
 </root>

+ 3 - 0
TeamAAS-VM/Resources/Languages/Lang.zh-TW.resx

@@ -3183,4 +3183,7 @@
   <data name="流线" xml:space="preserve">
     <value>流線</value>
   </data>
+  <data name="屏幕中心基准点位" xml:space="preserve">
+    <value>屏幕中心基准点位</value>
+  </data>
 </root>

+ 138 - 124
TeamAAS-VM/Services/RemoteCommandService.cs

@@ -1470,7 +1470,7 @@ namespace TeamAAS_VP.Services
                 }
                 DateTime nowtime = DateTime.Now;
                 LogHelper.WriteLogInfo("开始采集图像");
-                Thread.Sleep(250);
+                //Thread.Sleep(250);
                 var image = camera.Grab();
                 if (image == null)
                 {
@@ -1893,93 +1893,103 @@ namespace TeamAAS_VP.Services
         /// <param name="positionIndex"></param>
         /// <param name="remark"></param>
         /// <returns></returns>
-        public async Task<(bool IsSucceed, double X, double Y, double U)> ExecutePhotoGetSinglePoint(ProcedureModel procedure, Dictionary<string, string> InputTerminal, double[] robotCoord, CancellationToken cancellationToken, int positionIndex, string remark)
+        public async Task<(bool IsSucceed, double X, double Y, double U, string dis)> ExecutePhotoGetSinglePoint(ProcedureModel procedure, Dictionary<string, string> InputTerminal, double[] robotCoord, CancellationToken cancellationToken, int positionIndex, string remark)
         {
-            DateTime nowtime = DateTime.Now;            
+            DateTime nowtime = DateTime.Now;
             await SetLightBeforePhoto(procedure);
-            try
+            return await Task.Run(async () =>
             {
-                //拍照失败时,需要多次拍照
-                for (int i = 0; i < procedure.FeederFailLimit; i++)
+                try
                 {
-                    SendTaskMessage(Lang.开始执行视觉流程.Replace("{0}", $"{i + 1}").Replace("{1}", procedure.Name), MessageLevel.Info);
-
-                    // 1. 执行相机拍照并运行视觉工具
-                    bool visionSucceed = ExecutePhoto(i + 1, procedure, InputTerminal, out CogToolBlockTerminalCollection outputCollection);
-                    if (!visionSucceed)
+                    string dist = "";
+                    //拍照失败时,需要多次拍照
+                    for (int i = 0; i < procedure.FeederFailLimit; i++)
                     {
-                        SendTaskMessage(Lang.视觉任务执行失败, MessageLevel.Error);
-                        continue;
-                    }
+                        SendTaskMessage(Lang.开始执行视觉流程.Replace("{0}", $"{i + 1}").Replace("{1}", procedure.Name), MessageLevel.Info);
 
-                    if (!outputCollection.Contains("Found"))
-                    {
-                        SendTaskMessage(Lang.未找到视觉输出结果, MessageLevel.Error);
-                        await RecordPhotoCaptureAsync(procedure, robotCoord, false, new double[] { 0, 0, 0 }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
-                        return (false, 0, 0, 0);
-                    }
-                    // 2. 获取视觉输出结果
-                    bool Found = (bool)(outputCollection["Found"].Value);
-                    if (!Found)
-                    {
-                        SendTaskMessage(Lang.拍照NG, MessageLevel.Debug);
-                        continue;
-                    }                                      
-                    SendTaskMessage(Lang.拍照OK, MessageLevel.Debug);
-                    if (procedure.CalibrationId == Guid.Empty)
-                    {
-                        //没有关联校准时,则执行检测任务
-                        await RecordPhotoCaptureAsync(procedure, robotCoord, true, new double[] { 0, 0, 0 }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
-                        return (true, 0, 0, 0);
-                    }
+                        // 1. 执行相机拍照并运行视觉工具
+                        bool visionSucceed = ExecutePhoto(i + 1, procedure, InputTerminal, out CogToolBlockTerminalCollection outputCollection);
+                        if (!visionSucceed)
+                        {
+                            SendTaskMessage(Lang.视觉任务执行失败, MessageLevel.Error);
+                            continue;
+                        }
 
-                    // 3. 获取视觉输出结果
-                    LogHelper.WriteLogInfo("开始获取视觉输出结果");
-                    //获取相机校准
-                    var calib = _calibrationService.GetCalibration(procedure.CalibrationId);
-                    string points = (string)(outputCollection["Point"].Value);
-                    string[] strpoints = points.Split(';');
-                    //先将pos按照逗号进行分隔,拿到数组
-                    var posParts = strpoints[0].Split(',');
-                    double _pixel_x = double.Parse(posParts[0]);
-                    double _pixel_y = double.Parse(posParts[1]);
-                    double _pixel_u = double.Parse(posParts[2]);
-                    if (calib != null)
-                    {
-                        //转换点位-像素转换成机器人绝对坐标
-                        var calibResult = _calibrationService.ConvertPixelToPosition((_pixel_x, _pixel_y, _pixel_u), robotCoord, calib, RobotBrand.XYZ_Platform);
-                        if (!calibResult.IsSucceed)
+                        if (!outputCollection.Contains("Found"))
                         {
-                            await RecordPhotoCaptureAsync(procedure, robotCoord, false, new double[] { _pixel_x, _pixel_y, _pixel_u }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
-                            return (false, 0, 0, 0);
+                            SendTaskMessage(Lang.未找到视觉输出结果, MessageLevel.Error);
+                            await RecordPhotoCaptureAsync(procedure, robotCoord, false, new double[] { 0, 0, 0 }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
+                            return (false, 0, 0, 0, "NA");
                         }
-                        await RecordPhotoCaptureAsync(procedure, robotCoord, true, new double[] { _pixel_x, _pixel_y, _pixel_u }, new double[] { calibResult.X, calibResult.Y, calibResult.U }, string.Empty, positionIndex, remark);
-                        return (true, calibResult.X, calibResult.Y, calibResult.U);
-                    }
-                    else
-                    {
-                        await RecordPhotoCaptureAsync(procedure, robotCoord, true, new double[] { _pixel_x, _pixel_y, _pixel_u }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
-                        return (true, _pixel_x, _pixel_y, _pixel_u);
+                        // 2. 获取视觉输出结果
+                        bool Found = (bool)(outputCollection["Found"].Value);
+                        if (!Found)
+                        {
+                            SendTaskMessage(Lang.拍照NG, MessageLevel.Debug);
+                            continue;
+                        }
+                        if (outputCollection.Contains("dis"))
+                        {
+                            dist = (string)outputCollection["dis"].Value;
+                        }
+                        SendTaskMessage(Lang.拍照OK, MessageLevel.Debug);
+                        if (procedure.CalibrationId == Guid.Empty)
+                        {
+                            //没有关联校准时,则执行检测任务
+                            await RecordPhotoCaptureAsync(procedure, robotCoord, true, new double[] { 0, 0, 0 }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
+                            return (true, 0, 0, 0, dist);
+                        }
+
+                        // 3. 获取视觉输出结果
+                        LogHelper.WriteLogInfo("开始获取视觉输出结果");
+                        //获取相机校准
+                        var calib = _calibrationService.GetCalibration(procedure.CalibrationId);
+                        string points = (string)(outputCollection["Point"].Value);
+                        string[] strpoints = points.Split(';');
+                        //先将pos按照逗号进行分隔,拿到数组
+                        var posParts = strpoints[0].Split(',');
+                        double _pixel_x = double.Parse(posParts[0]);
+                        double _pixel_y = double.Parse(posParts[1]);
+                        double _pixel_u = double.Parse(posParts[2]);
+                        if (calib != null)
+                        {
+                            //转换点位-像素转换成机器人绝对坐标
+                            var calibResult = _calibrationService.ConvertPixelToPosition((_pixel_x, _pixel_y, _pixel_u), robotCoord, calib, RobotBrand.XYZ_Platform);
+                            if (!calibResult.IsSucceed)
+                            {
+                                await RecordPhotoCaptureAsync(procedure, robotCoord, false, new double[] { _pixel_x, _pixel_y, _pixel_u }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
+                                return (false, 0, 0, 0, dist);
+                            }
+                            await RecordPhotoCaptureAsync(procedure, robotCoord, true, new double[] { _pixel_x, _pixel_y, _pixel_u }, new double[] { calibResult.X, calibResult.Y, calibResult.U }, string.Empty, positionIndex, remark);
+                            return (true, calibResult.X, calibResult.Y, calibResult.U, dist);
+                        }
+                        else
+                        {
+                            await RecordPhotoCaptureAsync(procedure, robotCoord, true, new double[] { _pixel_x, _pixel_y, _pixel_u }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
+                            return (true, _pixel_x, _pixel_y, _pixel_u, dist);
+                        }
+
                     }
+                    SendTaskMessage(Lang.多次拍照失败, MessageLevel.Alarm);
+                    await RecordPhotoCaptureAsync(procedure, robotCoord, false, new double[] { 0, 0, 0 }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
+                    return (false, 0, 0, 0, "NA");
+                }
+                catch (Exception ex)
+                {
+                    LogHelper.WriteLogError("执行相机取图并获取单点位时出错!", ex);
+                    await RecordPhotoCaptureAsync(procedure, robotCoord, false, new double[] { 0, 0, 0 }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
+                    return (false, 0, 0, 0, "NA");
+                }
+                finally
+                {
+                    await TurnOffLightAfterPhoto(procedure);
 
                 }
-                SendTaskMessage(Lang.多次拍照失败, MessageLevel.Alarm);
-                await RecordPhotoCaptureAsync(procedure, robotCoord, false, new double[] { 0, 0, 0 }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
-                return (false, 0, 0, 0);
-            }
-            catch (Exception ex)
-            {
-                LogHelper.WriteLogError("执行相机取图并获取单点位时出错!", ex);
-                await RecordPhotoCaptureAsync(procedure, robotCoord, false, new double[] { 0, 0, 0 }, new double[] { 0, 0, 0 }, string.Empty, positionIndex, remark);
-                return (false, 0, 0, 0);
-            }
-            finally
-            {
-                await TurnOffLightAfterPhoto(procedure);
+            });
 
-            }
         }
 
+
         /// <summary>
         /// 获取复检结果
         /// </summary>
@@ -1992,60 +2002,64 @@ namespace TeamAAS_VP.Services
         /// <returns></returns>
         public async Task<(bool IsSucceed, string distance)> ExecutePhotoGetDistance(ProcedureModel procedure, Dictionary<string, string> InputTerminal, double[] robotCoord, CancellationToken cancellationToken, int positionIndex, string remark)
         {
-            DateTime nowtime = DateTime.Now;
-            await SetLightBeforePhoto(procedure);
-            try
+            return await  Task.Run(async () =>
             {
-                var dis = "0,0";
-                //拍照失败时,需要多次拍照
-                for (int i = 0; i < procedure.FeederFailLimit; i++)
+                DateTime nowtime = DateTime.Now;
+                await SetLightBeforePhoto(procedure);
+                try
                 {
-                    SendTaskMessage(Lang.开始执行视觉流程.Replace("{0}", $"{i + 1}").Replace("{1}", procedure.Name), MessageLevel.Info);
-
-                    // 1. 执行相机拍照并运行视觉工具
-                    bool visionSucceed = ExecutePhoto(i + 1, procedure, InputTerminal, out CogToolBlockTerminalCollection outputCollection);
-                    if (!visionSucceed)
+                    var dis = "0,0";
+                    //拍照失败时,需要多次拍照
+                    for (int i = 0; i < procedure.FeederFailLimit; i++)
                     {
-                        SendTaskMessage(Lang.视觉任务执行失败, MessageLevel.Error);
-                        dis = (string)(outputCollection["Gap"].Value);
-                        continue;
-                    }
+                        SendTaskMessage(Lang.开始执行视觉流程.Replace("{0}", $"{i + 1}").Replace("{1}", procedure.Name), MessageLevel.Info);
 
-                    if (!outputCollection.Contains("Found"))
-                    {
-                        SendTaskMessage(Lang.未找到视觉输出结果, MessageLevel.Error);
-                        return (false, "NA");
-                    }
+                        // 1. 执行相机拍照并运行视觉工具
+                        bool visionSucceed = ExecutePhoto(i + 1, procedure, InputTerminal, out CogToolBlockTerminalCollection outputCollection);
+                        if (!visionSucceed)
+                        {
+                            SendTaskMessage(Lang.视觉任务执行失败, MessageLevel.Error);
+                            dis = (string)(outputCollection["Gap"].Value);
+                            continue;
+                        }
 
-                    // 2. 获取视觉输出结果
-                    bool Found = (bool)(outputCollection["Found"].Value);
-                    if (!Found)
-                    {
-                        SendTaskMessage(Lang.拍照NG, MessageLevel.Debug);
+                        if (!outputCollection.Contains("Found"))
+                        {
+                            SendTaskMessage(Lang.未找到视觉输出结果, MessageLevel.Error);
+                            return (false, "NA");
+                        }
+
+                        // 2. 获取视觉输出结果
+                        bool Found = (bool)(outputCollection["Found"].Value);
+                        if (!Found)
+                        {
+                            SendTaskMessage(Lang.拍照NG, MessageLevel.Debug);
+                            dis = (string)(outputCollection["Gap"].Value);
+                            continue;
+                        }
+
+                        SendTaskMessage(Lang.拍照OK, MessageLevel.Debug);
+
+                        LogHelper.WriteLogInfo("开始获取视觉输出结果");
                         dis = (string)(outputCollection["Gap"].Value);
-                        continue;
+                        return (true, dis);
                     }
+                    //dis = (string)(outputCollection["Gap"].Value);
+                    SendTaskMessage(Lang.多次拍照失败, MessageLevel.Alarm);
+                    return (false, dis);
+                }
+                catch (Exception ex)
+                {
+                    LogHelper.WriteLogError("执行相机取图并获取单点位时出错!", ex);
+                    return (false, "NA");
+                }
+                finally
+                {
+                    await TurnOffLightAfterPhoto(procedure);
 
-                    SendTaskMessage(Lang.拍照OK, MessageLevel.Debug);
-
-                    LogHelper.WriteLogInfo("开始获取视觉输出结果");
-                     dis = (string)(outputCollection["Gap"].Value);
-                    return (true, dis);
                 }
-                //dis = (string)(outputCollection["Gap"].Value);
-                SendTaskMessage(Lang.多次拍照失败, MessageLevel.Alarm);
-                return (false, dis);
-            }
-            catch (Exception ex)
-            {
-                LogHelper.WriteLogError("执行相机取图并获取单点位时出错!", ex);
-                return (false, "NA");
-            }
-            finally
-            {
-                await TurnOffLightAfterPhoto(procedure);
+            });
 
-            }
         }
 
         /// <summary>
@@ -2627,12 +2641,12 @@ namespace TeamAAS_VP.Services
                 foreach (var lightChannel in procedure.LightChannels)
                 {
                     bool isSucceed = false;
-                    isSucceed = await _lightManagerService.TurnOnGlobalChannelAsync(lightChannel.GlobalIndex);
-                    if (!isSucceed)
-                    {
-                        LogHelper.WriteLogInfo($"设置光源通道{lightChannel.GlobalIndex}打开失败!");
-                    }
-                    await Task.Delay(200);
+                    //isSucceed = await _lightManagerService.TurnOnGlobalChannelAsync(lightChannel.GlobalIndex);
+                    //if (!isSucceed)
+                    //{
+                    //    LogHelper.WriteLogInfo($"设置光源通道{lightChannel.GlobalIndex}打开失败!");
+                    //}
+                    // await Task.Delay(200);
                     isSucceed = await _lightManagerService.SetGlobalChannelBrightnessAsync(lightChannel.GlobalIndex, lightChannel.DefaultBrightness);
                     if (!isSucceed)
                     {
@@ -2641,7 +2655,7 @@ namespace TeamAAS_VP.Services
                 }
                 if (procedure.LightChannels.Count > 0)
                 {
-                    await Task.Delay(50);
+                    // await Task.Delay(50);
                 }
             }
         }

+ 2 - 2
TeamAAS-VM/Views/Home/ShowVisionRender.xaml.cs

@@ -526,7 +526,7 @@ namespace TeamAAS_VP.Views.Home
 
                         if (isCompress)
                         {
-                            ImageHelper.CompressImage(reimage, Recordedpath, 100);
+                            ImageHelper.CompressImage(reimage, Recordedpath, 20);
                         }
                         else
                         {
@@ -608,7 +608,7 @@ namespace TeamAAS_VP.Views.Home
 
                         if (isCompress)
                         {
-                            ImageHelper.CompressImage(reimage, Recordedpath, 100);
+                            ImageHelper.CompressImage(reimage, Recordedpath, 20);
                         }
                         else
                         {