Browse Source

新增设备信息管理与MES对接功能

本次提交实现了设备信息(线别、站别、机台号、MES配置)的配置与持久化,新增MES服务接口及其实现,并集成到主业务流程,实现PLC与MES的过站校验和数据上传。设置界面增加设备信息Tab,视觉精度测试报告导出时自动包含设备信息。调整相关依赖注入,完善异常处理。
孝锋 徐 7 months ago
parent
commit
ee50e9acda

+ 1 - 0
TeamAAS-VM/App.xaml.cs

@@ -121,6 +121,7 @@ namespace TeamAAS_VP
             containerRegistry.RegisterSingleton<IRemoteCommandService, RemoteCommandService>();
             containerRegistry.RegisterSingleton<IRemoteCommandService, RemoteCommandService>();
             containerRegistry.RegisterSingleton<ILightManagerService, LightManagerService>();
             containerRegistry.RegisterSingleton<ILightManagerService, LightManagerService>();
             containerRegistry.RegisterSingleton<ICameraCalibrationService, CameraCalibrationService>();
             containerRegistry.RegisterSingleton<ICameraCalibrationService, CameraCalibrationService>();
+            containerRegistry.RegisterSingleton<IMesService, MesService>();
             containerRegistry.RegisterSingleton<Management>();
             containerRegistry.RegisterSingleton<Management>();
 
 
             // Register system database service and initializer
             // Register system database service and initializer

+ 34 - 9
TeamAAS-VM/Core/Management.cs

@@ -75,6 +75,7 @@ namespace TeamAAS_VP.Core
         ICalibrationService _calibrationService;
         ICalibrationService _calibrationService;
         IRemoteCommandService _remoteCommandService;
         IRemoteCommandService _remoteCommandService;
         ISystemDatabaseService _systemDatabaseService;
         ISystemDatabaseService _systemDatabaseService;
+        IMesService _mesService;
 
 
         Timer yieldtimer;
         Timer yieldtimer;
         private DateTime StartTime = DateTime.Now;
         private DateTime StartTime = DateTime.Now;
@@ -171,7 +172,8 @@ namespace TeamAAS_VP.Core
 
 
         public Management(IRegionManager regionManager, IEventAggregator ea, IContainerProvider container, IConfigService configService,
         public Management(IRegionManager regionManager, IEventAggregator ea, IContainerProvider container, IConfigService configService,
             IRobotService robotService, ICameraService cameraService, IFeederService feederService, IPlcService plcService, IProductService productService,
             IRobotService robotService, ICameraService cameraService, IFeederService feederService, IPlcService plcService, IProductService productService,
-            ICalibrationService calibrationService, IRemoteCommandService remoteCommandService, ISystemDatabaseService systemDatabaseService, ILightManagerService lightManagerService)
+            ICalibrationService calibrationService, IRemoteCommandService remoteCommandService, ISystemDatabaseService systemDatabaseService, ILightManagerService lightManagerService, 
+            IMesService mesService)
         {
         {
             _regionManager = regionManager;
             _regionManager = regionManager;
             _eventAggregator = ea;
             _eventAggregator = ea;
@@ -189,6 +191,7 @@ namespace TeamAAS_VP.Core
             _remoteCommandService = remoteCommandService;
             _remoteCommandService = remoteCommandService;
             _systemDatabaseService = systemDatabaseService;
             _systemDatabaseService = systemDatabaseService;
             _lightManagerService = lightManagerService;
             _lightManagerService = lightManagerService;
+            _mesService = mesService;
         }
         }
 
 
         #region 初始化硬件模块
         #region 初始化硬件模块
@@ -2087,34 +2090,55 @@ namespace TeamAAS_VP.Core
                         {
                         {
                             if (tuple.value is Int16 state)
                             if (tuple.value is Int16 state)
                             {
                             {
-                                if (_LastMesCheck[i]!= state)
+                                if (_LastMesCheck[i] != state)
                                 {
                                 {
-                                    _LastMesCheck[i]= state;
+                                    _LastMesCheck[i] = state;
                                 }
                                 }
                                 else
                                 else
                                 {
                                 {
                                     return;
                                     return;
                                 }
                                 }
+                                var code = plc.ReadNode<string>(string.Format(addressConfig.In_Code.Address, i));
                                 if (state == 1)
                                 if (state == 1)
                                 {
                                 {
+                                    //这里缺少从MES获取产品配方的内容,需要补充
+
+
+
+                                    (bool isSuccess, string response) = await _mesService.StationGetAsync(code, 2);
                                     if (i == 0)
                                     if (i == 0)
                                     {
                                     {
-                                        SendTaskMessage($"收到当前站过站检查请求(当站是否生产)...", MessageLevel.Debug);
+                                        SendTaskMessage($"收到当前过站检查请求(当站是否生产)...", MessageLevel.Debug);
                                     }
                                     }
                                     else
                                     else
                                     {
                                     {
-                                        SendTaskMessage($"收到 {i} 过站检查请求(当站是否生产)...", MessageLevel.Debug);
+                                        SendTaskMessage($"收到{i}过站检查请求(当站是否生产)...", MessageLevel.Debug);
                                     }
                                     }
+                                    if (isSuccess)
+                                    {
+                                        //产品发生改变并且供料器发生了变化时,需要通知PLC进行取料更改
+                                        await plc.WriteNodeAsync(addressConfig.Out_ScrewFeederIsChanged.Address, (Int16)1); //不发生改变
 
 
-                                    //产品发生改变并且供料器发生了变化时,需要通知PLC进行取料更改
-                                    await plc.WriteNodeAsync(addressConfig.Out_ScrewFeederIsChanged.Address, (Int16)1); //不发生改变
+                                        await plc.WriteNodeAsync(string.Format(addressConfig.Out_MesStatus.Address, i), (Int16)1);
+                                    }
+                                    else
+                                    {
+                                        //产品发生改变并且供料器发生了变化时,需要通知PLC进行取料更改
+                                        await plc.WriteNodeAsync(addressConfig.Out_ScrewFeederIsChanged.Address, (Int16)1); //不发生改变
 
 
-                                    await plc.WriteNodeAsync(string.Format(addressConfig.Out_MesStatus.Address, i), (Int16)1);
+                                        await plc.WriteNodeAsync(string.Format(addressConfig.Out_MesStatus.Address, i), (Int16)0);
+                                    }
                                 }
                                 }
                                 // 上传本站数据
                                 // 上传本站数据
                                 else if (state == 2)
                                 else if (state == 2)
                                 {
                                 {
-                                    SendTaskMessage($"收到上传{i}站数据请求...", MessageLevel.Debug);
+                                    SendTaskMessage($"收到{i}上传本站数据请求...", MessageLevel.Debug);
+
+                                    //这里缺少上传MES的本站数据结果及开始和结束时间,需要补充
+
+                                    (bool isSuccess, string response) = await _mesService.SubmitGetAsync(code, true, DateTime.Now, DateTime.Now);
+                                    //假设上传成功,这里缺少上传失败的处理
+
                                     await plc.WriteNodeAsync(string.Format(addressConfig.Out_MesStatus.Address, i), (Int16)3);
                                     await plc.WriteNodeAsync(string.Format(addressConfig.Out_MesStatus.Address, i), (Int16)3);
                                 }
                                 }
                                 else
                                 else
@@ -2125,6 +2149,7 @@ namespace TeamAAS_VP.Core
                             }
                             }
                         }
                         }
                     }
                     }
+
                 }
                 }
             }
             }
             catch (Exception ex)
             catch (Exception ex)

+ 11 - 0
TeamAAS-VM/Interfaces/IConfigService.cs

@@ -466,5 +466,16 @@ namespace TeamAAS_VP.Interfaces
         /// 记录一次螺丝供料器批次更换记录(写入数据库)。
         /// 记录一次螺丝供料器批次更换记录(写入数据库)。
         /// </summary>
         /// </summary>
         Task RecordScrewFeederBatchAsync(ScrewFeederBatchRecord record);
         Task RecordScrewFeederBatchAsync(ScrewFeederBatchRecord record);
+
+        // Device info
+        /// <summary>
+        /// 获取或创建设备信息配置
+        /// </summary>
+        DeviceInfo GetDeviceInfo();
+
+        /// <summary>
+        /// 保存设备信息配置
+        /// </summary>
+        void SaveDeviceInfo(DeviceInfo deviceInfo);
     }
     }
 }
 }

+ 87 - 0
TeamAAS-VM/Interfaces/IMesService.cs

@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace TeamAAS_VP.Interfaces
+{
+    /// <summary>
+    /// MES 通信服务接口。
+    /// 提供通过配置的 URL 发起 GET 请求的抽象,用于执行过站(Station)与提交结果(Submit)操作。
+    /// 实现者应负责管理内部 HTTP 客户端或通信资源,并在 Dispose 中释放这些资源。
+    /// </summary>
+    public interface IMesService : IDisposable
+    {
+        /// <summary>
+        /// 使用配置的过站(Station)URL 发起 GET 请求,携带序列号(sn)作为查询参数进行过站。
+        /// </summary>
+        /// <param name="sn">要过站的产品序列号,不能为空或空白。</param>
+        /// <param name="cancellationToken">用于在外部请求取消时中止异步请求的 <see cref="CancellationToken"/>。默认值为 <see cref="CancellationToken.None"/>。</param>
+        /// <returns>
+        /// 一个包含请求结果的元组:
+        /// IsSuccess 表示请求并解析 MES 响应是否被认为成功(由实现定义,例如 HTTP 200 且业务状态正常)。
+        /// Response 为 MES 返回的原始响应字符串或描述错误的信息(不为 null,可为空字符串)。
+        /// </returns>
+        /// <remarks>
+        /// - 实现应从配置中读取过站 URL 并将 sn 作为查询参数附加到 URL。
+        /// - 实现应尊重 <paramref name="cancellationToken"/>,在取消时尽早抛出 <see cref="OperationCanceledException"/> 或返回失败结果。
+        /// - 若 <paramref name="sn"/> 无效,应抛出 <see cref="ArgumentNullException"/> 或 <see cref="ArgumentException"/>。
+        /// </remarks>
+        Task<(bool IsSuccess, string Response)> StationGetAsync(string sn, CancellationToken cancellationToken = default);
+
+        /// <summary>
+        /// 使用配置的过站(Station)URL 发起 GET 请求,携带序列号(sn)作为查询参数进行过站,并使用指定的超时时间(秒)。
+        /// </summary>
+        /// <param name="sn">要过站的产品序列号,不能为空或空白。</param>
+        /// <param name="timeoutInSeconds">请求超时时间,单位为秒。应为正数;实现可以将其应用到 HTTP 客户端或请求级别超时。</param>
+        /// <returns>
+        /// 一个包含请求结果的元组:
+        /// IsSuccess 表示请求并解析 MES 响应是否被认为成功。
+        /// Response 为 MES 返回的原始响应字符串或描述错误的信息。
+        /// </returns>
+        /// <remarks>
+        /// - 此重载方便在不使用 <see cref="CancellationToken"/> 的场景中指定超时时间。
+        /// - 实现应验证 <paramref name="timeoutInSeconds"/> 为合理值(例如 > 0),否则可抛出 <see cref="ArgumentOutOfRangeException"/>。
+        /// </remarks>
+        Task<(bool IsSuccess, string Response)> StationGetAsync(string sn, double timeoutInSeconds);
+
+        /// <summary>
+        /// 使用配置的提交(Submit)URL 发起 GET 请求,提交过站结果及时间信息。
+        /// </summary>
+        /// <param name="sn">待提交的产品序列号,不能为空或空白。</param>
+        /// <param name="isSuccess">表示该序列号对应的过站是否成功(业务层面的成功标志)。</param>
+        /// <param name="start_time">过程开始时间(本地时间或 UTC,取决于与 MES 的约定)。实现方应按与 MES 约定的时区/格式发送。</param>
+        /// <param name="stop_time">过程结束时间,语义同 <paramref name="start_time"/>。</param>
+        /// <param name="cancellationToken">用于在外部请求取消时中止异步请求的 <see cref="CancellationToken"/>。默认值为 <see cref="CancellationToken.None"/>。</param>
+        /// <returns>
+        /// 一个包含请求结果的元组:
+        /// IsSuccess 表示提交请求及解析 MES 响应是否被认为成功。
+        /// Response 为 MES 返回的原始响应字符串或描述错误的信息。
+        /// </returns>
+        /// <remarks>
+        /// - 实现应将时间参数按 MES 要求格式化为查询字符串或适当的请求参数。
+        /// - 若 <paramref name="start_time"/> 晚于 <paramref name="stop_time"/>,实现应记录并视情况返回失败或抛出异常。
+        /// - 实现应尊重 <paramref name="cancellationToken"/>,并在取消时尽早中止请求。
+        /// </remarks>
+        Task<(bool IsSuccess, string Response)> SubmitGetAsync(string sn, bool isSuccess, DateTime start_time, DateTime stop_time, CancellationToken cancellationToken = default);
+
+        /// <summary>
+        /// 使用配置的提交(Submit)URL 发起 GET 请求,提交过站结果及时间信息,并使用指定的超时时间(秒)。
+        /// </summary>
+        /// <param name="sn">待提交的产品序列号,不能为空或空白。</param>
+        /// <param name="isSuccess">表示该序列号对应的过站是否成功。</param>
+        /// <param name="start_time">过程开始时间。</param>
+        /// <param name="stop_time">过程结束时间。</param>
+        /// <param name="timeoutInSeconds">请求超时时间,单位为秒。应为正数。</param>
+        /// <returns>
+        /// 一个包含请求结果的元组:
+        /// IsSuccess 指示提交是否被认为成功。
+        /// Response 包含 MES 的原始响应或错误信息。
+        /// </returns>
+        /// <remarks>
+        /// - 此重载用于在不传入 <see cref="CancellationToken"/> 的场景中指定超时时间。
+        /// - 实现应确保对时间参数与布尔参数进行适当的 URL 编码或格式化。
+        /// </remarks>
+        Task<(bool IsSuccess, string Response)> SubmitGetAsync(string sn, bool isSuccess, DateTime start_time, DateTime stop_time, double timeoutInSeconds);
+    }
+}

+ 57 - 0
TeamAAS-VM/Models/DeviceInfo.cs

@@ -0,0 +1,57 @@
+using Prism.Mvvm;
+using System;
+
+namespace TeamAAS_VP.Models
+{
+    /// <summary>
+    /// 设备信息配置,用于保存线别/站别/机台号以及 MES 相关配置。
+    /// </summary>
+    public class DeviceInfo : BindableBase
+    {
+        private string _line;
+        /// <summary>
+        /// 当前作业的线别
+        /// </summary>
+        public string Line { get => _line; set => SetProperty(ref _line, value); }
+
+        private string _station;
+        /// <summary>
+        /// 当前作业的站别
+        /// </summary>
+        public string Station { get => _station; set => SetProperty(ref _station, value); }
+
+        private string _fixtureId;
+        /// <summary>
+        /// 当前作业的机台编号
+        /// </summary>
+        public string FixtureId { get => _fixtureId; set => SetProperty(ref _fixtureId, value); }
+
+        private bool _enableMes;
+        /// <summary>
+        /// 是否启用 MES 通信功能
+        /// </summary>
+        public bool EnableMES { get => _enableMes; set => SetProperty(ref _enableMes, value); }
+
+        private string _mesStationUrl;
+        /// <summary>
+        /// MES 过站 URL
+        /// </summary>
+        public string MesStationUrl { get => _mesStationUrl; set => SetProperty(ref _mesStationUrl, value); }
+
+        private string _mesSubmitUrl;
+        /// <summary>
+        /// MES 提交 URL
+        /// </summary>
+        public string MesSubmitUrl { get => _mesSubmitUrl; set => SetProperty(ref _mesSubmitUrl, value); }
+
+        public DeviceInfo()
+        {
+            Line = string.Empty;
+            Station = string.Empty;
+            FixtureId = string.Empty;
+            EnableMES = false;
+            MesStationUrl = string.Empty;
+            MesSubmitUrl = string.Empty;
+        }
+    }
+}

+ 53 - 1
TeamAAS-VM/Services/ConfigService.cs

@@ -37,6 +37,7 @@ namespace TeamAAS_VP.Services
             public static readonly string BgTcpIpConfigurationPath = "..//Config//BgTcpIpConfiguration.cfg";
             public static readonly string BgTcpIpConfigurationPath = "..//Config//BgTcpIpConfiguration.cfg";
             public static readonly string BgModbusTcpConfigurationPath = "..//Config//BgModbusTcpConfiguration.cfg";
             public static readonly string BgModbusTcpConfigurationPath = "..//Config//BgModbusTcpConfiguration.cfg";
             public static readonly string SystemConfigurationPath = "..//Config//SystemConfiguration.cfg";
             public static readonly string SystemConfigurationPath = "..//Config//SystemConfiguration.cfg";
+            public static readonly string DeviceInfoConfigurationPath = "..//Config//DeviceInfo.cfg";
             // Feeder清料任务配置路径
             // Feeder清料任务配置路径
             public static readonly string FeederClearanceTasksConfigurationPath = "..//Config//FeederClearanceTasksConfiguration.cfg";
             public static readonly string FeederClearanceTasksConfigurationPath = "..//Config//FeederClearanceTasksConfiguration.cfg";
             // 光源控制器配置路径
             // 光源控制器配置路径
@@ -59,7 +60,8 @@ namespace TeamAAS_VP.Services
         private ObservableCollection<FeederClearWork> FeederClearanceTasks { get; set; }
         private ObservableCollection<FeederClearWork> FeederClearanceTasks { get; set; }
         // 光源控制器配置
         // 光源控制器配置
         private ObservableCollection<LightControllerConfig> LightControllers { get; set; }
         private ObservableCollection<LightControllerConfig> LightControllers { get; set; }
-
+        // Device info
+        private DeviceInfo _deviceInfo;
         private PlcAddressConfig PlcAddressConfig { get; set; }
         private PlcAddressConfig PlcAddressConfig { get; set; }
         // 单个电批配置
         // 单个电批配置
         private TeamAAS_VP.Models.ScrewDriver.ScrewDriverConfig _screwDriverConfig;
         private TeamAAS_VP.Models.ScrewDriver.ScrewDriverConfig _screwDriverConfig;
@@ -209,6 +211,15 @@ namespace TeamAAS_VP.Services
                     _screwDriverConfig = new TeamAAS_VP.Models.ScrewDriver.ScrewDriverConfig();
                     _screwDriverConfig = new TeamAAS_VP.Models.ScrewDriver.ScrewDriverConfig();
                     FileHelper.WriteJsonFile(_screwDriverConfig, ConfigPaths.ScrewDriverConfigurationPath);
                     FileHelper.WriteJsonFile(_screwDriverConfig, ConfigPaths.ScrewDriverConfigurationPath);
                 }
                 }
+
+                // load device info
+                if (File.Exists(ConfigPaths.DeviceInfoConfigurationPath))
+                    _deviceInfo = FileHelper.ReadJsonFile<DeviceInfo>(ConfigPaths.DeviceInfoConfigurationPath);
+                else
+                {
+                    _deviceInfo = new DeviceInfo();
+                    FileHelper.WriteJsonFile(_deviceInfo, ConfigPaths.DeviceInfoConfigurationPath);
+                }
             }
             }
         }
         }
 
 
@@ -244,6 +255,9 @@ namespace TeamAAS_VP.Services
                     FileHelper.WriteJsonFile(ScrewFeeders, ConfigPaths.ScrewFeedersConfigurationPath);
                     FileHelper.WriteJsonFile(ScrewFeeders, ConfigPaths.ScrewFeedersConfigurationPath);
                 // save plc addresses
                 // save plc addresses
                 //FileHelper.WriteJsonFile(PlcAddressConfig, "..//Config//PlcAddressConfiguration.cfg");
                 //FileHelper.WriteJsonFile(PlcAddressConfig, "..//Config//PlcAddressConfiguration.cfg");
+                // save device info
+                if (_deviceInfo != null)
+                    FileHelper.WriteJsonFile(_deviceInfo, ConfigPaths.DeviceInfoConfigurationPath);
             }
             }
         }
         }
 
 
@@ -346,6 +360,44 @@ namespace TeamAAS_VP.Services
         public bool ContainsCamera(Guid id) { lock (_sync) { return Cameras.Any(c => c.Id == id); } }
         public bool ContainsCamera(Guid id) { lock (_sync) { return Cameras.Any(c => c.Id == id); } }
         #endregion
         #endregion
 
 
+        #region DeviceInfo
+        /// <summary>
+        /// 获取设备信息配置
+        /// </summary>
+        /// <returns></returns>
+        public DeviceInfo GetDeviceInfo()
+        {
+            lock (_sync)
+            {
+                if (_deviceInfo == null)
+                {
+                    if (File.Exists(ConfigPaths.DeviceInfoConfigurationPath))
+                        _deviceInfo = FileHelper.ReadJsonFile<DeviceInfo>(ConfigPaths.DeviceInfoConfigurationPath);
+                    else
+                    {
+                        _deviceInfo = new DeviceInfo();
+                        FileHelper.WriteJsonFile(_deviceInfo, ConfigPaths.DeviceInfoConfigurationPath);
+                    }
+                }
+                return _deviceInfo;
+            }
+        }
+
+        /// <summary>
+        /// 保存设备信息
+        /// </summary>
+        /// <param name="deviceInfo"></param>
+        public void SaveDeviceInfo(DeviceInfo deviceInfo)
+        {
+            if (deviceInfo == null) return;
+            lock (_sync)
+            {
+                _deviceInfo = deviceInfo;
+                FileHelper.WriteJsonFile(_deviceInfo, ConfigPaths.DeviceInfoConfigurationPath);
+            }
+        }
+        #endregion
+
         #region Robots
         #region Robots
         /// <summary>
         /// <summary>
         /// 获取所有机器人配置的只读集合。
         /// 获取所有机器人配置的只读集合。

+ 233 - 0
TeamAAS-VM/Services/MesService.cs

@@ -0,0 +1,233 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using TeamAAS_VP.Interfaces;
+using TeamAAS_VP.Models;
+
+namespace TeamAAS_VP.Services
+{
+    /// <summary>
+    /// 基于 <see cref="HttpClient"/> 的 MES 通信服务实现。
+    /// <para>
+    /// 本服务通过注入的 <see cref="IConfigService"/> 获取设备配置(包含 MES 接口地址),
+    /// 提供对站点查询(StationGetAsync)和提交(SubmitGetAsync)的 GET 请求封装并带有重试逻辑。
+    /// </para>
+    /// <para>
+    /// 注意:
+    /// - <see cref="_httpClient"/> 为可复用的单一实例,建议在服务生命周期内重用以避免端口耗尽。
+    /// - 本类实现了显式的 <see cref="Dispose"/> 方法以释放内部 <see cref="HttpClient"/> 资源,调用后实例不可再用。
+    /// </para>
+    /// </summary>
+    public class MesService : IMesService
+    {
+        /// <summary>
+        /// 用于执行 HTTP 请求的客户端实例。建议在服务生命周期内重用,避免频繁创建导致套接字/端口耗尽。
+        /// </summary>
+        private readonly HttpClient _httpClient;
+
+        /// <summary>
+        /// 配置服务,用于获取设备信息(包含 MES URL、是否启用 MES 等)。
+        /// </summary>
+        private readonly IConfigService _configService;
+
+        /// <summary>
+        /// 标记实例是否已释放,防止重复释放和在释放后继续使用导致未定义行为。
+        /// </summary>
+        private bool _disposed;
+
+        /// <summary>
+        /// 使用指定的配置服务创建 <see cref="MesService"/> 实例。
+        /// </summary>
+        /// <param name="configService">用于获取设备配置信息的实现,不能为空。</param>
+        /// <exception cref="ArgumentNullException">当 <paramref name="configService"/> 为 null 时抛出。</exception>
+        public MesService(IConfigService configService)
+        {
+            _configService = configService ?? throw new ArgumentNullException(nameof(configService));
+            _httpClient = new HttpClient();
+            _disposed = false;
+        }
+
+
+        /// <summary>
+        /// 向配置中指定的 MES 站点 URL 发起 GET 请求(用于过站查询)。
+        /// </summary>
+        /// <param name="sn">要查询的序列号(SN)。</param>
+        /// <param name="cancellationToken">可选的取消令牌,用于请求超时或取消。</param>
+        /// <returns>
+        /// 返回元组 (IsSuccess, Response):
+        /// - IsSuccess: 请求是否视为成功(即 MES 返回了预期的 "SFC_OK" 且包含 "unit_process_check=OK")。
+        /// - Response: 原始响应文本或错误信息说明(如 "MES功能未启用!"、"Missing MES station URL"、"Canceled" 等)。
+        /// </returns>
+        /// <remarks>
+        /// 行为说明:
+        /// - 从配置获取设备信息,若 MES 未启用则直接返回 IsSuccess=true 且 Response 为提示文本(不视为错误)。
+        /// - 若未配置 MES URL 则返回 IsSuccess=false 与错误文本。
+        /// - 使用配置的 URL(通过 <see cref="string.Format"/> 填充 sn)发起 GET 请求,最多重试 10 次。
+        /// - 只有当响应包含 "SFC_OK" 且包含 "unit_process_check=OK" 时视为成功并立即返回。
+        /// - 捕获 <see cref="OperationCanceledException"/> 返回 "Canceled",捕获其它异常则返回异常消息。
+        /// - 若实例已被释放则抛出 <see cref="ObjectDisposedException"/>。
+        /// </remarks>
+        public async Task<(bool IsSuccess, string Response)> StationGetAsync(string sn, CancellationToken cancellationToken = default)
+        {
+            // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。
+            if (_disposed) throw new ObjectDisposedException(nameof(MesService));
+
+            var device = _configService.GetDeviceInfo();
+            if (device == null || !device.EnableMES)
+            {
+                return (true, "MES功能未启用!");
+            }
+            if (string.IsNullOrWhiteSpace(device.MesStationUrl))
+                return (false, "Missing MES station URL");
+
+            var url = string.Format(device.MesStationUrl, sn);
+
+            (bool IsSuccess, string Response) result = (false, string.Empty);
+            for (int i = 0; i < 5; i++)
+            {
+                try
+                {
+                    using (var rsp = await _httpClient.GetAsync(url, cancellationToken))
+                    {
+                        var text = await rsp.Content.ReadAsStringAsync();
+                        // "SFC_OK" 代表和 MES 连接成功
+                        if (text.Contains("SFC_OK"))
+                        {
+                            if (text.Contains("unit_process_check=OK"))
+                            {
+                                return (true, text);
+                            }
+                        }
+                        result = (false, text);
+                    }
+                }
+                catch (OperationCanceledException)
+                {
+                    result = (false, "Canceled");
+                }
+                catch (Exception ex)
+                {
+                    result = (false, ex.Message);
+                }
+            }
+            return result;
+        }
+
+        /// <summary>
+        /// 向 MES 发起站点查询请求,使用超时(秒)作为便捷重载。
+        /// </summary>
+        /// <param name="sn">序列号(SN)。</param>
+        /// <param name="timeoutInSeconds">请求超时,单位为秒。</param>
+        /// <returns>与 <see cref="StationGetAsync(string, CancellationToken)"/> 相同的返回语义。</returns>
+        public async Task<(bool IsSuccess, string Response)> StationGetAsync(string sn, double timeoutInSeconds)
+        {
+            using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutInSeconds)))
+            {
+                return await StationGetAsync(sn, cts.Token);
+            }
+        }
+
+        /// <summary>
+        /// 向配置中指定的 MES 提交 URL 发起 GET 请求(用于提交测试结果/记录)。
+        /// </summary>
+        /// <param name="sn">要提交的序列号(SN)。</param>
+        /// <param name="isSuccess">表示此次记录是否通过(true => PASS,false => FAIL)。</param>
+        /// <param name="start_time">开始时间,提交时使用 "yyyy-MM-dd HH:mm:ss" 格式。</param>
+        /// <param name="stop_time">结束时间,提交时使用 "yyyy-MM-dd HH:mm:ss" 格式。</param>
+        /// <param name="cancellationToken">可选的取消令牌,用于请求超时或取消。</param>
+        /// <returns>
+        /// 返回元组 (IsSuccess, Response):
+        /// - IsSuccess: 提交是否被 MES 视为成功(收到 "SFC_OK" 且响应中无额外多行信息)。
+        /// - Response: MES 返回文本或错误说明;当 MES 返回 "SFC_OK" 但包含额外多行信息(代表 SN 数据问题)时,IsSuccess 为 false 且 Response 为该文本。
+        /// </returns>
+        /// <remarks>
+        /// 行为说明:
+        /// - 从配置获取设备信息,若 MES 未启用则直接返回 IsSuccess=true 且 Response 为提示文本(不视为错误)。
+        /// - 若未配置 MES 提交 URL 则返回 IsSuccess=false 与错误文本。
+        /// - 使用配置的 URL(通过 <see cref="string.Format"/> 按顺序填充 sn、PASS/FAIL、start_time、stop_time)发起 GET 请求,最多重试 10 次。
+        /// - 在收到包含 "SFC_OK" 的响应时,如果返回文本包含多行(有附加信息)则视为提交失败并返回该文本,否则视为提交成功。
+        /// - 捕获 <see cref="OperationCanceledException"/> 返回 "Canceled",捕获其它异常则返回异常消息。
+        /// - 若实例已被释放则抛出 <see cref="ObjectDisposedException"/>。
+        /// </remarks>
+        public async Task<(bool IsSuccess, string Response)> SubmitGetAsync(string sn, bool isSuccess, DateTime start_time, DateTime stop_time, CancellationToken cancellationToken = default)
+        {
+            // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。
+            if (_disposed) throw new ObjectDisposedException(nameof(MesService));
+
+            var device = _configService.GetDeviceInfo();
+            if (device == null || !device.EnableMES)
+            {
+                return (true, "MES功能未启用!");
+            }
+            if (string.IsNullOrWhiteSpace(device.MesStationUrl))
+                return (false, "Missing MES submit URL");
+
+            var url = string.Format(device.MesStationUrl, sn, isSuccess ? "PASS" : "FAIL", start_time.ToString("yyyy-MM-dd HH:mm:ss"), stop_time.ToString("yyyy-MM-dd HH:mm:ss"));
+
+            (bool IsSuccess, string Response) result = (false, string.Empty);
+            for (int i = 0; i < 5; i++)
+            {
+                try
+                {
+                    using (var rsp = await _httpClient.GetAsync(url, cancellationToken))
+                    {
+                        var text = await rsp.Content.ReadAsStringAsync();
+                        // "SFC_OK" 代表和 MES 连接成功
+                        if (text.Contains("SFC_OK"))
+                        {
+                            // 如果有附加信息(多行),代表此 SN 数据有问题,视为失败并返回该文本
+                            if (text.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries).Length > 1)
+                            {
+                                return (false, text);
+                            }
+                            return (true, text);
+                        }
+                        result = (false, text);
+                    }
+                }
+                catch (OperationCanceledException)
+                {
+                    result = (false, "Canceled");
+                }
+                catch (Exception ex)
+                {
+                    result = (false, ex.Message);
+                }
+            }
+            return result;
+        }
+
+        /// <summary>
+        /// 向 MES 提交请求的超时重载,使用超时(秒)作为便捷参数。
+        /// </summary>
+        /// <param name="sn">序列号(SN)。</param>
+        /// <param name="isSuccess">是否通过(PASS/FAIL)。</param>
+        /// <param name="start_time">开始时间。</param>
+        /// <param name="stop_time">结束时间。</param>
+        /// <param name="timeoutInSeconds">请求超时,单位为秒。</param>
+        /// <returns>与 <see cref="SubmitGetAsync(string, bool, DateTime, DateTime, CancellationToken)"/> 相同的返回语义。</returns>
+        public async Task<(bool IsSuccess, string Response)> SubmitGetAsync(string sn, bool isSuccess, DateTime start_time, DateTime stop_time, double timeoutInSeconds)
+        {
+            using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutInSeconds)))
+            {
+                return await SubmitGetAsync(sn, isSuccess, start_time, stop_time, cts.Token);
+            }
+        }
+
+        /// <summary>
+        /// 释放服务占用的托管资源(当前仅 <see cref="_httpClient"/>)。
+        /// <para>本方法为幂等操作,重复调用不会产生异常。调用后实例不可再用于发起请求,尝试使用将抛出 <see cref="ObjectDisposedException"/>。</para>
+        /// </summary>
+        public void Dispose()
+        {
+            if (!_disposed)
+            {
+                _httpClient.Dispose();
+                _disposed = true;
+            }
+        }
+    }
+}

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

@@ -535,8 +535,11 @@
     <Compile Include="Core\RectangleCenterCalculator.cs" />
     <Compile Include="Core\RectangleCenterCalculator.cs" />
     <Compile Include="Core\StabilityAnalyzer.cs" />
     <Compile Include="Core\StabilityAnalyzer.cs" />
     <Compile Include="Events\MainTabSwitchNotification.cs" />
     <Compile Include="Events\MainTabSwitchNotification.cs" />
+    <Compile Include="Interfaces\IMesService.cs" />
+    <Compile Include="Models\DeviceInfo.cs" />
     <Compile Include="Models\Feeder\ScrewFeederInfo.cs" />
     <Compile Include="Models\Feeder\ScrewFeederInfo.cs" />
     <Compile Include="Models\ScrewFeederBatchRecord.cs" />
     <Compile Include="Models\ScrewFeederBatchRecord.cs" />
+    <Compile Include="Services\MesService.cs" />
     <Compile Include="ViewModels\Calibration\CalibrationDistortionViewModel.cs" />
     <Compile Include="ViewModels\Calibration\CalibrationDistortionViewModel.cs" />
     <Compile Include="ViewModels\Calibration\CalibIndependentCameraViewModel.cs" />
     <Compile Include="ViewModels\Calibration\CalibIndependentCameraViewModel.cs" />
     <Compile Include="ViewModels\DebugMod\PlcRobotManualStepViewModel.cs" />
     <Compile Include="ViewModels\DebugMod\PlcRobotManualStepViewModel.cs" />

+ 24 - 2
TeamAAS-VM/ViewModels/Product/VisionDynamicAccuracyAnalyzerViewModel.cs

@@ -38,6 +38,7 @@ namespace TeamAAS_VP.ViewModels.Product
         ICameraService _cameraService;
         ICameraService _cameraService;
         ISystemDatabaseService _systemDatabaseService;
         ISystemDatabaseService _systemDatabaseService;
         ICalibrationService _calibrationService;
         ICalibrationService _calibrationService;
+        IConfigService _configService;
 
 
         //测试过程中控制取消的CTS
         //测试过程中控制取消的CTS
         CancellationTokenSource _cts;
         CancellationTokenSource _cts;
@@ -304,7 +305,8 @@ namespace TeamAAS_VP.ViewModels.Product
         #endregion
         #endregion
 
 
         public VisionDynamicAccuracyAnalyzerViewModel(IRegionManager regionManager, IEventAggregator ea, IContainerProvider container, IDialogService dialogService,
         public VisionDynamicAccuracyAnalyzerViewModel(IRegionManager regionManager, IEventAggregator ea, IContainerProvider container, IDialogService dialogService,
-            IFeederService feederService, IRobotService robotService, ICameraService cameraService, ISystemDatabaseService systemDatabaseService, ICalibrationService calibrationService)
+            IFeederService feederService, IRobotService robotService, ICameraService cameraService, ISystemDatabaseService systemDatabaseService, ICalibrationService calibrationService,
+            IConfigService configService)
         {
         {
             _regionManager = regionManager;
             _regionManager = regionManager;
             _eventAggregator = ea;
             _eventAggregator = ea;
@@ -316,6 +318,7 @@ namespace TeamAAS_VP.ViewModels.Product
             _systemDatabaseService = systemDatabaseService;
             _systemDatabaseService = systemDatabaseService;
             MessageQueue = new SnackbarMessageQueue(TimeSpan.FromSeconds(1));
             MessageQueue = new SnackbarMessageQueue(TimeSpan.FromSeconds(1));
             _calibrationService = calibrationService;
             _calibrationService = calibrationService;
+            _configService = configService;
         }
         }
 
 
         #region 方法
         #region 方法
@@ -651,6 +654,15 @@ namespace TeamAAS_VP.ViewModels.Product
                         metaTable.AddCell(cellVal);
                         metaTable.AddCell(cellVal);
                     }
                     }
 
 
+                    // include device info (line, station, fixture id) but exclude MES flags/urls
+                    var device = _configService?.GetDeviceInfo();
+                    if (device != null)
+                    {
+                        AddMeta("线别", device.Line ?? string.Empty);
+                        AddMeta("站别", device.Station ?? string.Empty);
+                        AddMeta("机台号", device.FixtureId ?? string.Empty);
+                    }
+
                     AddMeta("导出时间", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
                     AddMeta("导出时间", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
                     AddMeta("测试次数(目标)", RepeatCount.ToString(System.Globalization.CultureInfo.InvariantCulture));
                     AddMeta("测试次数(目标)", RepeatCount.ToString(System.Globalization.CultureInfo.InvariantCulture));
                     AddMeta("当前进度", $"{CurrentProgress} / {RepeatCount}");
                     AddMeta("当前进度", $"{CurrentProgress} / {RepeatCount}");
@@ -817,7 +829,7 @@ namespace TeamAAS_VP.ViewModels.Product
                 string path = dlg.FileName;
                 string path = dlg.FileName;
                 var sb = new System.Text.StringBuilder();
                 var sb = new System.Text.StringBuilder();
 
 
-                // CSV 字段转义
+                // 辅助:CSV 字段转义
                 Func<string, string> EscapeCsv = (s) =>
                 Func<string, string> EscapeCsv = (s) =>
                 {
                 {
                     if (s == null) return "";
                     if (s == null) return "";
@@ -825,6 +837,16 @@ namespace TeamAAS_VP.ViewModels.Product
                     string esc = s.Replace("\"", "\"\"");
                     string esc = s.Replace("\"", "\"\"");
                     return mustQuote ? $"\"{esc}\"" : esc;
                     return mustQuote ? $"\"{esc}\"" : esc;
                 };
                 };
+                // include device info at top of CSV
+                var device = _configService?.GetDeviceInfo() ?? _configService?.GetDeviceInfo();
+                if (device != null)
+                {
+                    sb.AppendLine("设备信息");
+                    sb.AppendLine($"线别,{EscapeCsv(device.Line)}");
+                    sb.AppendLine($"站别,{EscapeCsv(device.Station)}");
+                    sb.AppendLine($"机台号,{EscapeCsv(device.FixtureId)}");
+                    sb.AppendLine();
+                }
 
 
                 // 写入表头
                 // 写入表头
                 var headers = new[]
                 var headers = new[]

+ 26 - 3
TeamAAS-VM/ViewModels/Product/VisionStaticAccuracyAnalyzerViewModel.cs

@@ -42,6 +42,7 @@ namespace TeamAAS_VP.ViewModels.Product
         ICameraService _cameraService;
         ICameraService _cameraService;
         ISystemDatabaseService _systemDatabaseService;
         ISystemDatabaseService _systemDatabaseService;
         ICalibrationService _calibrationService;
         ICalibrationService _calibrationService;
+        IConfigService _configService;
 
 
         //测试过程中控制取消的CTS
         //测试过程中控制取消的CTS
         CancellationTokenSource _cts;
         CancellationTokenSource _cts;
@@ -252,7 +253,8 @@ namespace TeamAAS_VP.ViewModels.Product
         #endregion
         #endregion
 
 
         public VisionStaticAccuracyAnalyzerViewModel(IRegionManager regionManager, IEventAggregator ea, IContainerProvider container, IDialogService dialogService,
         public VisionStaticAccuracyAnalyzerViewModel(IRegionManager regionManager, IEventAggregator ea, IContainerProvider container, IDialogService dialogService,
-            IFeederService feederService, IRobotService robotService, ICameraService cameraService, ISystemDatabaseService systemDatabaseService, ICalibrationService calibrationService)
+            IFeederService feederService, IRobotService robotService, ICameraService cameraService, ISystemDatabaseService systemDatabaseService, ICalibrationService calibrationService,
+            IConfigService configService)
         {
         {
             _regionManager = regionManager;
             _regionManager = regionManager;
             _eventAggregator = ea;
             _eventAggregator = ea;
@@ -264,6 +266,7 @@ namespace TeamAAS_VP.ViewModels.Product
             _systemDatabaseService = systemDatabaseService;
             _systemDatabaseService = systemDatabaseService;
             MessageQueue = new SnackbarMessageQueue(TimeSpan.FromSeconds(1));
             MessageQueue = new SnackbarMessageQueue(TimeSpan.FromSeconds(1));
             _calibrationService = calibrationService;
             _calibrationService = calibrationService;
+            _configService = configService;
         }
         }
 
 
         #region 方法
         #region 方法
@@ -474,7 +477,7 @@ namespace TeamAAS_VP.ViewModels.Product
                         titleFont = iTextSharp.text.FontFactory.GetFont(iTextSharp.text.FontFactory.HELVETICA, 16, iTextSharp.text.Font.BOLD, iTextSharp.text.BaseColor.BLACK);
                         titleFont = iTextSharp.text.FontFactory.GetFont(iTextSharp.text.FontFactory.HELVETICA, 16, iTextSharp.text.Font.BOLD, iTextSharp.text.BaseColor.BLACK);
                         headerFont = iTextSharp.text.FontFactory.GetFont(iTextSharp.text.FontFactory.HELVETICA, 10, iTextSharp.text.Font.BOLD, iTextSharp.text.BaseColor.BLACK);
                         headerFont = iTextSharp.text.FontFactory.GetFont(iTextSharp.text.FontFactory.HELVETICA, 10, iTextSharp.text.Font.BOLD, iTextSharp.text.BaseColor.BLACK);
                         normalFont = iTextSharp.text.FontFactory.GetFont(iTextSharp.text.FontFactory.HELVETICA, 10, iTextSharp.text.Font.NORMAL, iTextSharp.text.BaseColor.BLACK);
                         normalFont = iTextSharp.text.FontFactory.GetFont(iTextSharp.text.FontFactory.HELVETICA, 10, iTextSharp.text.Font.NORMAL, iTextSharp.text.BaseColor.BLACK);
-                        
+
                     }
                     }
 
 
 
 
@@ -498,6 +501,15 @@ namespace TeamAAS_VP.ViewModels.Product
                         metaTable.AddCell(cellVal);
                         metaTable.AddCell(cellVal);
                     }
                     }
 
 
+                    // include device info (line, station, fixture id) but exclude MES flags/urls
+                    var device = _configService?.GetDeviceInfo() ?? _configService?.GetDeviceInfo();
+                    if (device != null)
+                    {
+                        AddMeta("线别", device.Line ?? string.Empty);
+                        AddMeta("站别", device.Station ?? string.Empty);
+                        AddMeta("机台号", device.FixtureId ?? string.Empty);
+                    }
+
                     AddMeta("导出时间", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
                     AddMeta("导出时间", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
                     AddMeta("测试次数(目标)", RepeatCount.ToString());
                     AddMeta("测试次数(目标)", RepeatCount.ToString());
                     AddMeta("当前进度", $"{CurrentProgress} / {RepeatCount}");
                     AddMeta("当前进度", $"{CurrentProgress} / {RepeatCount}");
@@ -666,6 +678,17 @@ namespace TeamAAS_VP.ViewModels.Product
                     return mustQuote ? $"\"{esc}\"" : esc;
                     return mustQuote ? $"\"{esc}\"" : esc;
                 };
                 };
 
 
+                // include device info at top of CSV
+                var device = _configService?.GetDeviceInfo();
+                if (device != null)
+                {
+                    sb.AppendLine("设备信息");
+                    sb.AppendLine($"线别,{EscapeCsv(device.Line)}");
+                    sb.AppendLine($"站别,{EscapeCsv(device.Station)}");
+                    sb.AppendLine($"机台号,{EscapeCsv(device.FixtureId)}");
+                    sb.AppendLine();
+                }
+
                 // 1. 写入表头
                 // 1. 写入表头
                 for (int c = 0; c < TestResult.Columns.Count; c++)
                 for (int c = 0; c < TestResult.Columns.Count; c++)
                 {
                 {
@@ -845,7 +868,7 @@ namespace TeamAAS_VP.ViewModels.Product
                     if (image == null)
                     if (image == null)
                     {
                     {
 
 
-                        SendTaskMessage("图像采集失败");
+                        SendTaskMessage(Lang.图像采集失败);
                         outputCollection = null;
                         outputCollection = null;
                         return (false, result.ToArray());
                         return (false, result.ToArray());
                     }
                     }

+ 35 - 1
TeamAAS-VM/ViewModels/SettingViewModel.cs

@@ -326,6 +326,12 @@ namespace TeamAAS_VP.ViewModels
             set { SetProperty(ref _SelectLightController, value); }
             set { SetProperty(ref _SelectLightController, value); }
         }
         }
 
 
+        private DeviceInfo _DeviceInfo;
+        public DeviceInfo DeviceInfo
+        {
+            get { return _DeviceInfo; }
+            set { SetProperty(ref _DeviceInfo, value); }
+        }
         #endregion
         #endregion
 
 
         #region 命令
         #region 命令
@@ -414,7 +420,8 @@ namespace TeamAAS_VP.ViewModels
         public DelegateCommand SaveScrewFeedersCommand =>
         public DelegateCommand SaveScrewFeedersCommand =>
             _SaveScrewFeedersCommand ?? (_SaveScrewFeedersCommand = new DelegateCommand(ExecuteSaveScrewFeedersCommand));
             _SaveScrewFeedersCommand ?? (_SaveScrewFeedersCommand = new DelegateCommand(ExecuteSaveScrewFeedersCommand));
 
 
-       
+        private DelegateCommand _SaveDeviceInfoCommand;
+        public DelegateCommand SaveDeviceInfoCommand => _SaveDeviceInfoCommand ?? (_SaveDeviceInfoCommand = new DelegateCommand(ExecuteSaveDeviceInfoCommand));
         #endregion
         #endregion
 
 
         #region 事件
         #region 事件
@@ -1305,6 +1312,12 @@ namespace TeamAAS_VP.ViewModels
                 BgModbus = _configService.GetBgModbusTcp();
                 BgModbus = _configService.GetBgModbusTcp();
             }
             }
 
 
+            if (DeviceInfo == null)
+            {
+                // load device info
+                DeviceInfo = _configService.GetDeviceInfo();
+            }
+
             CurrentLanguage = _configService.GetSystemConfiguration().CurrentLanguage;
             CurrentLanguage = _configService.GetSystemConfiguration().CurrentLanguage;
 
 
             // load system parameters into UI
             // load system parameters into UI
@@ -1517,5 +1530,26 @@ namespace TeamAAS_VP.ViewModels
             }
             }
         }
         }
         #endregion
         #endregion
+
+        #region 设备信息
+        /// <summary>
+        /// 保存设备信息
+        /// </summary>
+        void ExecuteSaveDeviceInfoCommand()
+        {
+            try
+            {
+                if (DeviceInfo != null)
+                {
+                    _configService.SaveDeviceInfo(DeviceInfo);
+                    _eventAggregator.GetEvent<SnackbarMessageNotification>().Publish(new MessageParameter() { Msg = Lang.完成, Duration = 1 });
+                }
+            }
+            catch (Exception ex)
+            {
+                _eventAggregator.GetEvent<SnackbarMessageNotification>().Publish(new MessageParameter() { Msg = ex.Message, Duration = 1 });
+            }
+        }
+        #endregion
     }
     }
 }
 }

+ 101 - 0
TeamAAS-VM/Views/SettingView.xaml

@@ -213,6 +213,107 @@
                 </Grid>
                 </Grid>
             </TabItem>
             </TabItem>
 
 
+            <!--设备信息-->
+            <TabItem>
+                <TabItem.Header>
+                    <StackPanel HorizontalAlignment="Stretch">
+                        <materialDesign:PackIcon Width="24"
+                                                 Height="24"
+                                                 HorizontalAlignment="Center"
+                                                 Kind="RouterNetwork" />
+                        <TextBlock Style="{StaticResource TabHeaderTextBlockStyle}"
+                                   FontSize="{DynamicResource Font.Size.Body2}"
+                                   Text="设备信息"
+                                   HorizontalAlignment="Stretch" />
+                    </StackPanel>
+                </TabItem.Header>
+
+                <Grid Margin="16">
+                    <Grid.RowDefinitions>
+                        <RowDefinition Height="Auto" />
+                        <RowDefinition Height="Auto" />
+                        <RowDefinition Height="Auto" />
+                        <RowDefinition Height="Auto" />
+                        <RowDefinition Height="Auto" />
+                        <RowDefinition Height="Auto" />
+                        <RowDefinition Height="Auto" />
+                    </Grid.RowDefinitions>
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="auto" />
+                        <ColumnDefinition Width="*" />
+                    </Grid.ColumnDefinitions>
+
+                    <TextBlock Grid.Row="0"
+                               Grid.Column="0"
+                               Text="线别:"
+                               HorizontalAlignment="Right"
+                               VerticalAlignment="Center" />
+                    <TextBox Grid.Row="0"
+                             Grid.Column="1"
+                             Text="{Binding DeviceInfo.Line, Mode=TwoWay}"
+                             Margin="6" />
+
+                    <TextBlock Grid.Row="1"
+                               Grid.Column="0"
+                               Text="站别:"
+                               HorizontalAlignment="Right"
+                               VerticalAlignment="Center" />
+                    <TextBox Grid.Row="1"
+                             Grid.Column="1"
+                             Text="{Binding DeviceInfo.Station, Mode=TwoWay}"
+                             Margin="6" />
+
+                    <TextBlock Grid.Row="2"
+                               Grid.Column="0"
+                               Text="机台号:"
+                               HorizontalAlignment="Right"
+                               VerticalAlignment="Center" />
+                    <TextBox Grid.Row="2"
+                             Grid.Column="1"
+                             Text="{Binding DeviceInfo.FixtureId, Mode=TwoWay}"
+                             Margin="6" />
+
+                    <TextBlock Grid.Row="3"
+                               Grid.Column="0"
+                               Text="启用 MES:"
+                               HorizontalAlignment="Right"
+                               VerticalAlignment="Center" />
+                    <CheckBox Grid.Row="3"
+                              Grid.Column="1"
+                              IsChecked="{Binding DeviceInfo.EnableMES, Mode=TwoWay}"
+                              Margin="6" />
+
+                    <TextBlock Grid.Row="4"
+                               Grid.Column="0"
+                               Text="MES 过站 URL:"
+                               HorizontalAlignment="Right"
+                               VerticalAlignment="Center" />
+                    <TextBox Grid.Row="4"
+                             Grid.Column="1"
+                             Text="{Binding DeviceInfo.MesStationUrl, Mode=TwoWay}"
+                             Margin="6" />
+
+                    <TextBlock Grid.Row="5"
+                               Grid.Column="0"
+                               Text="MES 提交 URL:"
+                               HorizontalAlignment="Right"
+                               VerticalAlignment="Center" />
+                    <TextBox Grid.Row="5"
+                             Grid.Column="1"
+                             Text="{Binding DeviceInfo.MesSubmitUrl, Mode=TwoWay}"
+                             Margin="6" />
+
+                    <StackPanel Orientation="Horizontal"
+                                HorizontalAlignment="Right"
+                                Grid.Row="6"
+                                Grid.ColumnSpan="2"
+                                Margin="6">
+                        <Button Style="{StaticResource MaterialDesignRaisedButton}"
+                                Command="{Binding SaveDeviceInfoCommand}">保存</Button>
+                    </StackPanel>
+                </Grid>
+            </TabItem>
+
             <!--机器人-->
             <!--机器人-->
             <TabItem>
             <TabItem>
                 <TabItem.Header>
                 <TabItem.Header>