MesService.cs 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896
  1. using Cognex.VisionPro.QuickBuild.Implementation.Internal;
  2. using NPOI.SS.Formula.Functions;
  3. using Prism.Events;
  4. using Prism.Ioc;
  5. using System;
  6. using System.Collections.Generic;
  7. using System.Linq;
  8. using System.Net.Http;
  9. using System.Text;
  10. using System.Threading;
  11. using System.Threading.Tasks;
  12. using TeamAAS_VP.Core;
  13. using TeamAAS_VP.Enums;
  14. using TeamAAS_VP.Events;
  15. using TeamAAS_VP.Interfaces;
  16. using TeamAAS_VP.Models;
  17. using TeamAAS_VP.Resources.Languages;
  18. namespace TeamAAS_VP.Services
  19. {
  20. /// <summary>
  21. /// 基于 <see cref="HttpClient"/> 的 MES 通信服务实现。
  22. /// <para>
  23. /// 本服务通过注入的 <see cref="IConfigService"/> 获取设备配置(包含 MES 接口地址),
  24. /// 提供对站点查询(StationGetAsync)和提交(SubmitGetAsync)的 GET 请求封装并带有重试逻辑。
  25. /// </para>
  26. /// <para>
  27. /// 注意:
  28. /// - <see cref="_httpClient"/> 为可复用的单一实例,建议在服务生命周期内重用以避免端口耗尽。
  29. /// - 本类实现了显式的 <see cref="Dispose"/> 方法以释放内部 <see cref="HttpClient"/> 资源,调用后实例不可再用。
  30. /// </para>
  31. /// </summary>
  32. public class MesService : IMesService
  33. {
  34. IEventAggregator _eventAggregator;
  35. /// <summary>
  36. /// 用于执行 HTTP 请求的客户端实例。建议在服务生命周期内重用,避免频繁创建导致套接字/端口耗尽。
  37. /// </summary>
  38. private readonly HttpClient _httpClient;
  39. /// <summary>
  40. /// 配置服务,用于获取设备信息(包含 MES URL、是否启用 MES 等)。
  41. /// </summary>
  42. private readonly IConfigService _configService;
  43. /// <summary>
  44. /// 标记实例是否已释放,防止重复释放和在释放后继续使用导致未定义行为。
  45. /// </summary>
  46. private bool _disposed;
  47. /// <summary>
  48. /// 使用指定的配置服务创建 <see cref="MesService"/> 实例。
  49. /// </summary>
  50. /// <param name="configService">用于获取设备配置信息的实现,不能为空。</param>
  51. /// <exception cref="ArgumentNullException">当 <paramref name="configService"/> 为 null 时抛出。</exception>
  52. public MesService(IConfigService configService, IContainerProvider container, IEventAggregator ea)
  53. {
  54. _configService = configService ?? throw new ArgumentNullException(nameof(configService));
  55. _httpClient = new HttpClient();
  56. _disposed = false;
  57. _eventAggregator = ea;
  58. }
  59. /// <summary>
  60. /// 向配置中指定的 MES 站点 URL 发起 GET 请求(用于过站查询)。
  61. /// </summary>
  62. /// <param name="sn">要查询的序列号(SN)。</param>
  63. /// <param name="cancellationToken">可选的取消令牌,用于请求超时或取消。</param>
  64. /// <returns>
  65. /// 返回元组 (IsSuccess, Response):
  66. /// - IsSuccess: 请求是否视为成功(即 MES 返回了预期的 "SFC_OK" 且包含 "unit_process_check=OK")。
  67. /// - Response: 原始响应文本或错误信息说明(如 "MES功能未启用!"、"Missing MES station URL"、"Canceled" 等)。
  68. /// </returns>
  69. /// <remarks>
  70. /// 行为说明:
  71. /// - 从配置获取设备信息,若 MES 未启用则直接返回 IsSuccess=true 且 Response 为提示文本(不视为错误)。
  72. /// - 若未配置 MES URL 则返回 IsSuccess=false 与错误文本。
  73. /// - 使用配置的 URL(通过 <see cref="string.Format"/> 填充 sn)发起 GET 请求,最多重试 10 次。
  74. /// - 只有当响应包含 "SFC_OK" 且包含 "unit_process_check=OK" 时视为成功并立即返回。
  75. /// - 捕获 <see cref="OperationCanceledException"/> 返回 "Canceled",捕获其它异常则返回异常消息。
  76. /// - 若实例已被释放则抛出 <see cref="ObjectDisposedException"/>。
  77. /// </remarks>
  78. public async Task<(bool IsSuccess, string Response)> StationGetAsync(int deviceNo, string sn, string comp, CancellationToken cancellationToken = default)
  79. {
  80. // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。
  81. if (_disposed) throw new ObjectDisposedException(nameof(MesService));
  82. var device = _configService.GetDeviceInfo(deviceNo);
  83. if (device == null)
  84. device = _configService.GetDeviceInfo();
  85. if (device == null || !device.EnableMES)
  86. {
  87. return (true, "MES功能未启用!");
  88. }
  89. if (string.IsNullOrWhiteSpace(device.MesUrl))
  90. return (false, "Missing MES station URL");
  91. if (device.F == "PTL")
  92. {
  93. if (string.IsNullOrEmpty(device.comp))
  94. {
  95. return (false, "PTL模式下comp不能为空");
  96. }
  97. }
  98. string url = "";
  99. if (device.F == "PTL")
  100. {
  101. url = $"{device.MesUrl}?c=QUERY_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&comp={device.comp}:{comp}&p=message,sn,wo,model_num,color,ppid,stage";
  102. }
  103. else
  104. {
  105. url = $"{device.MesUrl}?c=QUERY_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&p=message,sn,wo,model_num,color,ppid,stage";
  106. }
  107. LogHelper.WriteLogMes($"【询问URL】{url}");
  108. SendTaskMessage($"【询问URL】{url}", MessageLevel.Info);
  109. (bool IsSuccess, string Response) result = (false, string.Empty);
  110. for (int i = 0; i < 3; i++)
  111. {
  112. try
  113. {
  114. using (var rsp = await _httpClient.GetAsync(url))
  115. {
  116. var text = await rsp.Content.ReadAsStringAsync();
  117. LogHelper.WriteLogMes($"【HTTP接收】{text}");
  118. SendTaskMessage($"【HTTP接收】{text}", MessageLevel.Info);
  119. // "SFC_OK" 代表和 MES 连接成功
  120. if (text.Contains("SFC_OK"))
  121. {
  122. if (text.Contains("message=OK"))
  123. {
  124. return (true, text);
  125. }
  126. }
  127. result = (false, text);
  128. }
  129. }
  130. catch (OperationCanceledException)
  131. {
  132. result = (false, "Canceled");
  133. }
  134. catch (Exception ex)
  135. {
  136. result = (false, ex.Message);
  137. }
  138. }
  139. return result;
  140. }
  141. public async Task<(bool IsSuccess, string Response)> StationGetExAsync(string sn, string comp, double timeoutInSeconds, int index)
  142. {
  143. using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutInSeconds)))
  144. {
  145. return await StationGetExAsync(sn, comp, index, cts.Token);
  146. }
  147. }
  148. /// <summary>
  149. /// 向 MES 发起站点查询请求,使用超时(秒)作为便捷重载。
  150. /// </summary>
  151. /// <param name="sn">序列号(SN)。</param>
  152. /// <param name="timeoutInSeconds">请求超时,单位为秒。</param>
  153. /// <returns>与 <see cref="StationGetAsync(string, CancellationToken)"/> 相同的返回语义。</returns>
  154. public async Task<(bool IsSuccess, string Response)> StationGetAsync(string sn, string comp, double timeoutInSeconds, int index)
  155. {
  156. using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutInSeconds)))
  157. {
  158. return await StationGetAsync(sn, comp, index, cts.Token);
  159. }
  160. }
  161. /// <summary>
  162. /// 向配置中指定的 MES 站点 URL 发起 GET 请求(用于过站查询)。
  163. /// </summary>
  164. /// <param name="sn">要查询的序列号(SN)。</param>
  165. /// <param name="cancellationToken">可选的取消令牌,用于请求超时或取消。</param>
  166. /// <returns>
  167. /// 返回元组 (IsSuccess, Response):
  168. /// - IsSuccess: 请求是否视为成功(即 MES 返回了预期的 "SFC_OK" 且包含 "unit_process_check=OK")。
  169. /// - Response: 原始响应文本或错误信息说明(如 "MES功能未启用!"、"Missing MES station URL"、"Canceled" 等)。
  170. /// </returns>
  171. /// <remarks>
  172. /// 行为说明:
  173. /// - 从配置获取设备信息,若 MES 未启用则直接返回 IsSuccess=true 且 Response 为提示文本(不视为错误)。
  174. /// - 若未配置 MES URL 则返回 IsSuccess=false 与错误文本。
  175. /// - 使用配置的 URL(通过 <see cref="string.Format"/> 填充 sn)发起 GET 请求,最多重试 10 次。
  176. /// - 只有当响应包含 "SFC_OK" 且包含 "unit_process_check=OK" 时视为成功并立即返回。
  177. /// - 捕获 <see cref="OperationCanceledException"/> 返回 "Canceled",捕获其它异常则返回异常消息。
  178. /// - 若实例已被释放则抛出 <see cref="ObjectDisposedException"/>。
  179. /// </remarks>
  180. public async Task<(bool IsSuccess, string Response)> StationGetExAsync(string sn, string comp, int index, CancellationToken cancellationToken = default)
  181. {
  182. // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。
  183. if (_disposed) throw new ObjectDisposedException(nameof(MesService));
  184. var device = _configService.GetDeviceInfo(index);
  185. if (device == null || !device.EnableMES)
  186. {
  187. return (true, "MES功能未启用!");
  188. }
  189. if (string.IsNullOrWhiteSpace(device.MesUrl))
  190. {
  191. return (false, "Missing MES station URL");
  192. }
  193. string url = "";
  194. url = $"{device.MesUrl}?c=QUERY_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&p=sn,message";
  195. LogHelper.WriteLogMes($"【询问URL】{url}");
  196. SendTaskMessage($"【询问URL】{url}", MessageLevel.Info);
  197. (bool IsSuccess, string Response) result = (false, string.Empty);
  198. for (int i = 0; i < 3; i++)
  199. {
  200. try
  201. {
  202. using (var rsp = await _httpClient.GetAsync(url))
  203. {
  204. var text = await rsp.Content.ReadAsStringAsync();
  205. LogHelper.WriteLogMes($"【HTTP接收】{text}");
  206. SendTaskMessage($"【HTTP接收】{text}", MessageLevel.Info);
  207. // "SFC_OK" 代表和 MES 连接成功
  208. if (text.Contains("SFC_OK"))
  209. {
  210. if (text.Contains("message=OK"))
  211. {
  212. return (true, text);
  213. }
  214. }
  215. result = (false, text);
  216. }
  217. }
  218. catch (OperationCanceledException)
  219. {
  220. result = (false, "Canceled");
  221. }
  222. catch (Exception ex)
  223. {
  224. result = (false, ex.Message);
  225. }
  226. }
  227. return result;
  228. }
  229. /// <summary>
  230. /// 向配置中指定的 MES 站点 URL 发起 GET 请求(用于过站查询)。
  231. /// </summary>
  232. /// <param name="sn">要查询的序列号(SN)。</param>
  233. /// <param name="cancellationToken">可选的取消令牌,用于请求超时或取消。</param>
  234. /// <returns>
  235. /// 返回元组 (IsSuccess, Response):
  236. /// - IsSuccess: 请求是否视为成功(即 MES 返回了预期的 "SFC_OK" 且包含 "unit_process_check=OK")。
  237. /// - Response: 原始响应文本或错误信息说明(如 "MES功能未启用!"、"Missing MES station URL"、"Canceled" 等)。
  238. /// </returns>
  239. /// <remarks>
  240. /// 行为说明:
  241. /// - 从配置获取设备信息,若 MES 未启用则直接返回 IsSuccess=true 且 Response 为提示文本(不视为错误)。
  242. /// - 若未配置 MES URL 则返回 IsSuccess=false 与错误文本。
  243. /// - 使用配置的 URL(通过 <see cref="string.Format"/> 填充 sn)发起 GET 请求,最多重试 10 次。
  244. /// - 只有当响应包含 "SFC_OK" 且包含 "unit_process_check=OK" 时视为成功并立即返回。
  245. /// - 捕获 <see cref="OperationCanceledException"/> 返回 "Canceled",捕获其它异常则返回异常消息。
  246. /// - 若实例已被释放则抛出 <see cref="ObjectDisposedException"/>。
  247. /// </remarks>
  248. public async Task<(bool IsSuccess, string Response)> StationGetAsync(string sn, string comp,int index, CancellationToken cancellationToken = default)
  249. {
  250. // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。
  251. if (_disposed) throw new ObjectDisposedException(nameof(MesService));
  252. var device = _configService.GetDeviceInfo(index);
  253. if (device == null || !device.EnableMES)
  254. {
  255. return (true, "MES功能未启用!");
  256. }
  257. if (string.IsNullOrWhiteSpace(device.MesUrl))
  258. {
  259. return (false, "Missing MES station URL");
  260. }
  261. if (device.F == "PTL")
  262. {
  263. if (string.IsNullOrEmpty(device.comp))
  264. {
  265. return (false, "PTL模式下comp不能为空");
  266. }
  267. }
  268. string url = "";
  269. if (device.F == "PTL")
  270. {
  271. url = $"{device.MesUrl}?c=QUERY_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&comp={device.comp}:{comp}&p=message,sn,wo,model_num,color,ppid,stage";
  272. }
  273. else
  274. {
  275. url = $"{device.MesUrl}?c=QUERY_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&p=message,sn,wo,model_num,color,ppid,stage";
  276. }
  277. LogHelper.WriteLogMes($"【询问URL】{url}");
  278. SendTaskMessage($"【询问URL】{url}", MessageLevel.Info);
  279. (bool IsSuccess, string Response) result = (false, string.Empty);
  280. for (int i = 0; i < 3; i++)
  281. {
  282. try
  283. {
  284. using (var rsp = await _httpClient.GetAsync(url))
  285. {
  286. var text = await rsp.Content.ReadAsStringAsync();
  287. LogHelper.WriteLogMes($"【HTTP接收】{text}");
  288. SendTaskMessage($"【HTTP接收】{text}", MessageLevel.Info);
  289. // "SFC_OK" 代表和 MES 连接成功
  290. if (text.Contains("SFC_OK"))
  291. {
  292. if (text.Contains("message=OK"))
  293. {
  294. return (true, text);
  295. }
  296. }
  297. result = (false, text);
  298. }
  299. }
  300. catch (OperationCanceledException)
  301. {
  302. result = (false, "Canceled");
  303. }
  304. catch (Exception ex)
  305. {
  306. result = (false, ex.Message);
  307. }
  308. }
  309. return result;
  310. }
  311. /// <summary>
  312. /// 向配置中指定的 MES 站点 URL 发起 GET 请求(用于过站查询)。
  313. /// </summary>
  314. /// <param name="sn">要查询的序列号(SN)。</param>
  315. /// <param name="cancellationToken">可选的取消令牌,用于请求超时或取消。</param>
  316. /// <returns>
  317. /// 返回元组 (IsSuccess, Response):
  318. /// - IsSuccess: 请求是否视为成功(即 MES 返回了预期的 "SFC_OK" 且包含 "unit_process_check=OK")。
  319. /// - Response: 原始响应文本或错误信息说明(如 "MES功能未启用!"、"Missing MES station URL"、"Canceled" 等)。
  320. /// </returns>
  321. /// <remarks>
  322. /// 行为说明:
  323. /// - 从配置获取设备信息,若 MES 未启用则直接返回 IsSuccess=true 且 Response 为提示文本(不视为错误)。
  324. /// - 若未配置 MES URL 则返回 IsSuccess=false 与错误文本。
  325. /// - 使用配置的 URL(通过 <see cref="string.Format"/> 填充 sn)发起 GET 请求,最多重试 10 次。
  326. /// - 只有当响应包含 "SFC_OK" 且包含 "unit_process_check=OK" 时视为成功并立即返回。
  327. /// - 捕获 <see cref="OperationCanceledException"/> 返回 "Canceled",捕获其它异常则返回异常消息。
  328. /// - 若实例已被释放则抛出 <see cref="ObjectDisposedException"/>。
  329. /// </remarks>
  330. public async Task<(bool IsSuccess, string Response)> StationGetExAsync(int deviceNo, string sn, string comp, CancellationToken cancellationToken = default)
  331. {
  332. // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。
  333. if (_disposed) throw new ObjectDisposedException(nameof(MesService));
  334. var device = _configService.GetDeviceInfo(deviceNo);
  335. if (device == null)
  336. device = _configService.GetDeviceInfo();
  337. if (device == null || !device.EnableMES)
  338. {
  339. return (true, "MES功能未启用!");
  340. }
  341. if (string.IsNullOrWhiteSpace(device.MesUrl))
  342. return (false, "Missing MES station URL");
  343. string url = "";
  344. url = $"{device.MesUrl}?c=QUERY_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&p=message,sn,wo";
  345. LogHelper.WriteLogMes($"【询问URL】{url}");
  346. SendTaskMessage($"【询问URL】{url}", MessageLevel.Info);
  347. (bool IsSuccess, string Response) result = (false, string.Empty);
  348. for (int i = 0; i < 3; i++)
  349. {
  350. try
  351. {
  352. using (var rsp = await _httpClient.GetAsync(url))
  353. {
  354. var text = await rsp.Content.ReadAsStringAsync();
  355. LogHelper.WriteLogMes($"【HTTP接收】{text}");
  356. SendTaskMessage($"【HTTP接收】{text}", MessageLevel.Info);
  357. // "SFC_OK" 代表和 MES 连接成功
  358. if (text.Contains("SFC_OK"))
  359. {
  360. if (text.Contains("message=OK"))
  361. {
  362. return (true, text);
  363. }
  364. }
  365. result = (false, text);
  366. }
  367. }
  368. catch (OperationCanceledException)
  369. {
  370. result = (false, "Canceled");
  371. }
  372. catch (Exception ex)
  373. {
  374. result = (false, ex.Message);
  375. }
  376. }
  377. return result;
  378. }
  379. /// <summary>
  380. /// 向 MES 发起站点查询请求,使用超时(秒)作为便捷重载。
  381. /// </summary>
  382. /// <param name="sn">序列号(SN)。</param>
  383. /// <param name="timeoutInSeconds">请求超时,单位为秒。</param>
  384. /// <returns>与 <see cref="StationGetAsync(string, CancellationToken)"/> 相同的返回语义。</returns>
  385. public async Task<(bool IsSuccess, string Response)> StationGetAsync(int deviceNo, string sn, string comp, double timeoutInSeconds)
  386. {
  387. using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutInSeconds)))
  388. {
  389. return await StationGetAsync(deviceNo, sn, comp, cts.Token);
  390. }
  391. }
  392. public async Task<(bool IsSuccess, string Response)> StationGetExAsync(int deviceNo, string sn, string comp, double timeoutInSeconds)
  393. {
  394. using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutInSeconds)))
  395. {
  396. return await StationGetExAsync(deviceNo, sn, comp, cts.Token);
  397. }
  398. }
  399. /// <summary>
  400. /// 向配置中指定的 MES 提交 URL 发起 GET 请求(用于提交测试结果/记录)。
  401. /// </summary>
  402. /// <param name="sn">要提交的序列号(SN)。</param>
  403. /// <param name="isSuccess">表示此次记录是否通过(true => PASS,false => FAIL)。</param>
  404. /// <param name="start_time">开始时间,提交时使用 "yyyy-MM-dd HH:mm:ss" 格式。</param>
  405. /// <param name="stop_time">结束时间,提交时使用 "yyyy-MM-dd HH:mm:ss" 格式。</param>
  406. /// <param name="cancellationToken">可选的取消令牌,用于请求超时或取消。</param>
  407. /// <returns>
  408. /// 返回元组 (IsSuccess, Response):
  409. /// - IsSuccess: 提交是否被 MES 视为成功(收到 "SFC_OK" 且响应中无额外多行信息)。
  410. /// - Response: MES 返回文本或错误说明;当 MES 返回 "SFC_OK" 但包含额外多行信息(代表 SN 数据问题)时,IsSuccess 为 false 且 Response 为该文本。
  411. /// </returns>
  412. /// <remarks>
  413. /// 行为说明:
  414. /// - 从配置获取设备信息,若 MES 未启用则直接返回 IsSuccess=true 且 Response 为提示文本(不视为错误)。
  415. /// - 若未配置 MES 提交 URL 则返回 IsSuccess=false 与错误文本。
  416. /// - 使用配置的 URL(通过 <see cref="string.Format"/> 按顺序填充 sn、PASS/FAIL、start_time、stop_time)发起 GET 请求,最多重试 10 次。
  417. /// - 在收到包含 "SFC_OK" 的响应时,如果返回文本包含多行(有附加信息)则视为提交失败并返回该文本,否则视为提交成功。
  418. /// - 捕获 <see cref="OperationCanceledException"/> 返回 "Canceled",捕获其它异常则返回异常消息。
  419. /// - 若实例已被释放则抛出 <see cref="ObjectDisposedException"/>。
  420. /// </remarks>
  421. public async Task<(bool IsSuccess, string Response)> SubmitGetAsync(int deviceNo, string sn, string comp, string torque, string pressure, string turn, bool isSuccess, DateTime start_time, DateTime stop_time, CancellationToken cancellationToken = default)
  422. {
  423. // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。
  424. if (_disposed) throw new ObjectDisposedException(nameof(MesService));
  425. var device = _configService.GetDeviceInfo(deviceNo);
  426. if (device == null)
  427. device = _configService.GetDeviceInfo();
  428. if (device == null || !device.EnableMES)
  429. {
  430. return (true, "MES功能未启用!");
  431. }
  432. if (string.IsNullOrWhiteSpace(device.MesUrl))
  433. return (false, "Missing MES submit URL");
  434. if (device.F == "PTL")
  435. {
  436. if (string.IsNullOrEmpty(device.comp))
  437. {
  438. return (false, "PTL模式下comp不能为空");
  439. }
  440. }
  441. //string res = isSuccess ? "Pass" : "Fail";
  442. string res = "Pass";
  443. string url = "";
  444. if (device.F == "PTL")
  445. {
  446. url = $"{device.MesUrl}?c=ADD_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&f=PTL&comp={device.comp}:{comp}" +
  447. $"&torque={torque}&pressure={pressure}&turn={turn}&result={res}&start_time={start_time.ToString("yyyy-MM-dd HH:mm:ss")}&stop_time={stop_time.ToString("yyyy-MM-dd HH:mm:ss")}";
  448. }
  449. else
  450. {
  451. url = $"{device.MesUrl}?c=ADD_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&f=PQC" +
  452. $"&torque={torque}&pressure={pressure}&turn={turn}&result={res}&start_time={start_time.ToString("yyyy-MM-dd HH:mm:ss")}&stop_time={stop_time.ToString("yyyy-MM-dd HH:mm:ss")}";
  453. }
  454. LogHelper.WriteLogMes($"【上传URL】{url}");
  455. SendTaskMessage($"【上传URL】{url}", MessageLevel.Info);
  456. (bool IsSuccess, string Response) result = (false, string.Empty);
  457. for (int i = 0; i < 3; i++)
  458. {
  459. try
  460. {
  461. using (var rsp = await _httpClient.GetAsync(url, cancellationToken))
  462. {
  463. var text = await rsp.Content.ReadAsStringAsync();
  464. LogHelper.WriteLogMes($"【HTTP接收】{text}");
  465. SendTaskMessage($"【HTTP接收】{text}", MessageLevel.Info);
  466. // "SFC_OK" 代表和 MES 连接成功
  467. if (text.Contains("SFC_OK"))
  468. {
  469. // 如果有附加信息(多行),代表此 SN 数据有问题,视为失败并返回该文本
  470. if (text.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries).Length > 1)
  471. {
  472. return (false, text);
  473. }
  474. return (true, text);
  475. }
  476. result = (false, text);
  477. }
  478. }
  479. catch (OperationCanceledException)
  480. {
  481. result = (false, "Canceled");
  482. }
  483. catch (Exception ex)
  484. {
  485. result = (false, ex.Message);
  486. }
  487. }
  488. return result;
  489. }
  490. /// <summary>
  491. /// 向配置中指定的 MES 提交 URL 发起 GET 请求(用于提交测试结果/记录)。
  492. /// </summary>
  493. /// <param name="sn">要提交的序列号(SN)。</param>
  494. /// <param name="isSuccess">表示此次记录是否通过(true => PASS,false => FAIL)。</param>
  495. /// <param name="start_time">开始时间,提交时使用 "yyyy-MM-dd HH:mm:ss" 格式。</param>
  496. /// <param name="stop_time">结束时间,提交时使用 "yyyy-MM-dd HH:mm:ss" 格式。</param>
  497. /// <param name="cancellationToken">可选的取消令牌,用于请求超时或取消。</param>
  498. /// <returns>
  499. /// 返回元组 (IsSuccess, Response):
  500. /// - IsSuccess: 提交是否被 MES 视为成功(收到 "SFC_OK" 且响应中无额外多行信息)。
  501. /// - Response: MES 返回文本或错误说明;当 MES 返回 "SFC_OK" 但包含额外多行信息(代表 SN 数据问题)时,IsSuccess 为 false 且 Response 为该文本。
  502. /// </returns>
  503. /// <remarks>
  504. /// 行为说明:
  505. /// - 从配置获取设备信息,若 MES 未启用则直接返回 IsSuccess=true 且 Response 为提示文本(不视为错误)。
  506. /// - 若未配置 MES 提交 URL 则返回 IsSuccess=false 与错误文本。
  507. /// - 使用配置的 URL(通过 <see cref="string.Format"/> 按顺序填充 sn、PASS/FAIL、start_time、stop_time)发起 GET 请求,最多重试 10 次。
  508. /// - 在收到包含 "SFC_OK" 的响应时,如果返回文本包含多行(有附加信息)则视为提交失败并返回该文本,否则视为提交成功。
  509. /// - 捕获 <see cref="OperationCanceledException"/> 返回 "Canceled",捕获其它异常则返回异常消息。
  510. /// - 若实例已被释放则抛出 <see cref="ObjectDisposedException"/>。
  511. /// </remarks>
  512. public async Task<(bool IsSuccess, string Response)> SubmitGetAsync(string sn, string comp, Dictionary<string, string> data, bool isSuccess, DateTime start_time, DateTime stop_time, CancellationToken cancellationToken = default)
  513. {
  514. // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。
  515. if (_disposed) throw new ObjectDisposedException(nameof(MesService));
  516. var device = _configService.GetDeviceInfo();
  517. if (device == null || !device.EnableMES)
  518. {
  519. return (true, "MES功能未启用!");
  520. }
  521. if (string.IsNullOrWhiteSpace(device.MesUrl))
  522. return (false, "Missing MES submit URL");
  523. if (device.F == "PTL")
  524. {
  525. if (string.IsNullOrEmpty(device.comp))
  526. {
  527. return (false, "PTL模式下comp不能为空");
  528. }
  529. }
  530. //string res = isSuccess ? "Pass" : "Fail";
  531. string res = "Pass";
  532. StringBuilder sb = new StringBuilder();
  533. if (device.F == "PTL")
  534. {
  535. sb.Append($"{device.MesUrl}?c=ADD_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&f=PTL&comp={device.comp}:{comp}");
  536. foreach (var item in data)
  537. {
  538. sb.Append($"&{item.Key}={item.Value}");
  539. }
  540. sb.Append($"&result={res}&start_time={start_time.ToString("yyyy-MM-dd HH:mm:ss")}&stop_time={stop_time.ToString("yyyy-MM-dd HH:mm:ss")}");
  541. }
  542. else
  543. {
  544. sb.Append($"{device.MesUrl}?c=ADD_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&f=PQC");
  545. foreach (var item in data)
  546. {
  547. sb.Append($"&{item.Key}={item.Value}");
  548. }
  549. sb.Append($"&result={res}&start_time={start_time.ToString("yyyy-MM-dd HH:mm:ss")}&stop_time={stop_time.ToString("yyyy-MM-dd HH:mm:ss")}");
  550. }
  551. string url = sb.ToString();
  552. //if (device.F == "PTL")
  553. //{
  554. // url = $"{device.MesUrl}?c=ADD_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&f=PTL&comp={device.comp}:{comp}" +
  555. // $"&torque={torque}&pressure={pressure}&turn={turn}&result={res}&start_time={start_time.ToString("yyyy-MM-dd HH:mm:ss")}&stop_time={stop_time.ToString("yyyy-MM-dd HH:mm:ss")}";
  556. //}
  557. //else
  558. //{
  559. // url = $"{device.MesUrl}?c=ADD_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&f=PQC" +
  560. // $"&torque={torque}&pressure={pressure}&turn={turn}&result={res}&start_time={start_time.ToString("yyyy-MM-dd HH:mm:ss")}&stop_time={stop_time.ToString("yyyy-MM-dd HH:mm:ss")}";
  561. //}
  562. LogHelper.WriteLogMes($"【上传URL】{url}");
  563. SendTaskMessage($"【上传URL】{url}", MessageLevel.Info);
  564. (bool IsSuccess, string Response) result = (false, string.Empty);
  565. for (int i = 0; i < 3; i++)
  566. {
  567. try
  568. {
  569. using (var rsp = await _httpClient.GetAsync(url, cancellationToken))
  570. {
  571. var text = await rsp.Content.ReadAsStringAsync();
  572. LogHelper.WriteLogMes($"【HTTP接收】{text}");
  573. SendTaskMessage($"【HTTP接收】{text}", MessageLevel.Info);
  574. // "SFC_OK" 代表和 MES 连接成功
  575. if (text.Contains("SFC_OK"))
  576. {
  577. // 如果有附加信息(多行),代表此 SN 数据有问题,视为失败并返回该文本
  578. if (text.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries).Length > 1)
  579. {
  580. return (false, text);
  581. }
  582. return (true, text);
  583. }
  584. result = (false, text);
  585. }
  586. }
  587. catch (OperationCanceledException)
  588. {
  589. result = (false, "Canceled");
  590. }
  591. catch (Exception ex)
  592. {
  593. result = (false, ex.Message);
  594. }
  595. }
  596. return result;
  597. }
  598. public async Task<(bool IsSuccess, string Response)> SubmitGetExAsync(string sn, string comp, Dictionary<string, string> data, List<string> results, DateTime start_time, DateTime stop_time, int index, CancellationToken cancellationToken = default)
  599. {
  600. // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。
  601. if (_disposed) throw new ObjectDisposedException(nameof(MesService));
  602. var device = _configService.GetDeviceInfo();
  603. if (device == null || !device.EnableMES)
  604. {
  605. return (true, "MES功能未启用!");
  606. }
  607. if (string.IsNullOrWhiteSpace(device.MesUrl))
  608. return (false, "Missing MES submit URL");
  609. if (device.F == "PTL")
  610. {
  611. if (string.IsNullOrEmpty(device.comp))
  612. {
  613. return (false, "PTL模式下comp不能为空");
  614. }
  615. }
  616. //string res = isSuccess ? "Pass" : "Fail";
  617. string res = "Pass";
  618. if (results.Count > 0)//如果有多个结果,则用逗号连接起来传给MES, MES端再解析
  619. {
  620. res = string.Join(",", results);
  621. }
  622. StringBuilder sb = new StringBuilder();
  623. if (device.F == "PTL")
  624. {
  625. sb.Append($"{device.MesUrl}?c=ADD_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&f=PTL&comp={device.comp}:{comp}");
  626. foreach (var item in data)
  627. {
  628. sb.Append($"&{item.Key}={item.Value}");
  629. }
  630. sb.Append($"&result={res}&start_time={start_time.ToString("yyyy-MM-dd HH:mm:ss")}&stop_time={stop_time.ToString("yyyy-MM-dd HH:mm:ss")}");
  631. }
  632. else
  633. {
  634. sb.Append($"{device.MesUrl}?c=ADD_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&f=PQC");
  635. foreach (var item in data)
  636. {
  637. sb.Append($"&{item.Key}={item.Value}");
  638. }
  639. sb.Append($"&result={res}&start_time={start_time.ToString("yyyy-MM-dd HH:mm:ss")}&stop_time={stop_time.ToString("yyyy-MM-dd HH:mm:ss")}");
  640. }
  641. string url = sb.ToString();
  642. LogHelper.WriteLogMes($"【上传URL】{url}");
  643. SendTaskMessage($"【上传URL】{url}", MessageLevel.Info);
  644. (bool IsSuccess, string Response) result = (false, string.Empty);
  645. for (int i = 0; i < 3; i++)
  646. {
  647. try
  648. {
  649. using (var rsp = await _httpClient.GetAsync(url, cancellationToken))
  650. {
  651. var text = await rsp.Content.ReadAsStringAsync();
  652. LogHelper.WriteLogMes($"【HTTP接收】{text}");
  653. SendTaskMessage($"【HTTP接收】{text}", MessageLevel.Info);
  654. // "SFC_OK" 代表和 MES 连接成功
  655. if (text.Contains("SFC_OK"))
  656. {
  657. // 如果有附加信息(多行),代表此 SN 数据有问题,视为失败并返回该文本
  658. if (text.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries).Length > 1)
  659. {
  660. return (false, text);
  661. }
  662. return (true, text);
  663. }
  664. result = (false, text);
  665. }
  666. }
  667. catch (OperationCanceledException)
  668. {
  669. result = (false, "Canceled");
  670. }
  671. catch (Exception ex)
  672. {
  673. result = (false, ex.Message);
  674. }
  675. }
  676. return result;
  677. }
  678. /// <summary>
  679. /// 向 MES 提交请求的超时重载,使用超时(秒)作为便捷参数。
  680. /// </summary>
  681. /// <param name="sn">序列号(SN)。</param>
  682. /// <param name="isSuccess">是否通过(PASS/FAIL)。</param>
  683. /// <param name="start_time">开始时间。</param>
  684. /// <param name="stop_time">结束时间。</param>
  685. /// <param name="timeoutInSeconds">请求超时,单位为秒。</param>
  686. /// <returns>与 <see cref="SubmitGetAsync(int deviceNo, string, bool, DateTime, DateTime, CancellationToken)"/> 相同的返回语义。</returns>
  687. public async Task<(bool IsSuccess, string Response)> SubmitGetAsync(int deviceNo, string sn, string comp, string torque, string pressure, string turn, bool isSuccess, DateTime start_time, DateTime stop_time, double timeoutInSeconds)
  688. {
  689. using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutInSeconds)))
  690. {
  691. return await SubmitGetAsync(deviceNo, sn, comp, torque, pressure, turn, isSuccess, start_time, stop_time, cts.Token);
  692. }
  693. }
  694. /// <summary>
  695. /// 获取服务器当前时间
  696. /// </summary>
  697. /// <param name="sn"></param>
  698. /// <param name="timeoutInSeconds"></param>
  699. /// <returns></returns>
  700. public async Task<(bool IsSuccess, DateTime Now)> GetServerTimeAsync(int deviceNo, string sn, double timeoutInSeconds)
  701. {
  702. // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。
  703. if (_disposed) throw new ObjectDisposedException(nameof(MesService));
  704. var device = _configService.GetDeviceInfo(deviceNo);
  705. if (device == null)
  706. device = _configService.GetDeviceInfo();
  707. if (device == null || !device.EnableMES)
  708. {
  709. return (true, DateTime.Now);
  710. }
  711. if (string.IsNullOrWhiteSpace(device.MesUrl))
  712. return (false, DateTime.Now);
  713. string url = "";
  714. url = $"{device.MesUrl}?c=QUERY_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&p=mes_server_time";
  715. LogHelper.WriteLogMes($"【询问MES Time】{url}");
  716. SendTaskMessage($"【询问MES Time】{url}", MessageLevel.Info);
  717. (bool IsSuccess, DateTime Now) result = (false, DateTime.Now);
  718. for (int i = 0; i < 3; i++)
  719. {
  720. try
  721. {
  722. using (var rsp = await _httpClient.GetAsync(url))
  723. {
  724. var text = await rsp.Content.ReadAsStringAsync();
  725. LogHelper.WriteLogMes($"【HTTP接收MES Time】{text}");
  726. SendTaskMessage($"【HTTP接收MES Time】{text}", MessageLevel.Info);
  727. // "SFC_OK" 代表和 MES 连接成功
  728. if (text.Contains("SFC_OK"))
  729. {
  730. //按照分隔符'/n'分割字符串
  731. string[] lines = text.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
  732. //通过linq查找是否包含"mes_server_time"的行
  733. var timeLine = lines.FirstOrDefault(l => l.Contains("mes_server_time"));
  734. if (timeLine != null)
  735. {
  736. // 提取时间字符串
  737. var timeString = timeLine.Split('=')[1].Trim();
  738. if (DateTime.TryParse(timeString, out DateTime serverTime))
  739. {
  740. return (true, serverTime);
  741. }
  742. }
  743. }
  744. result = (false, DateTime.Now);
  745. }
  746. }
  747. catch (OperationCanceledException)
  748. {
  749. result = (false, DateTime.Now);
  750. }
  751. catch (Exception ex)
  752. {
  753. result = (false, DateTime.Now);
  754. }
  755. }
  756. return result;
  757. }
  758. /// <summary>
  759. /// 上传log服务器
  760. /// </summary>
  761. /// <param name="sn"></param>
  762. /// <param name="name"></param>
  763. /// <param name="cancellationToken"></param>
  764. /// <returns></returns>
  765. /// <exception cref="ObjectDisposedException"></exception>
  766. public async Task<(bool IsSuccess, string Response)> SendFtpFileAsync(string sn, string name, CancellationToken cancellationToken = default)
  767. {
  768. // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。
  769. if (_disposed) throw new ObjectDisposedException(nameof(MesService));
  770. var device = _configService.GetDeviceInfo();
  771. if (device == null || !device.EnableMES)
  772. {
  773. return (true, "MES功能未启用!");
  774. }
  775. if (string.IsNullOrWhiteSpace(device.LogUrl))
  776. {
  777. return (false, "Missing MES station LogUrl");
  778. }
  779. string url = $"{device.LogUrl}?db=mysql&ApiKey={device.ApiKey}&jsondata={{\"SN\":\"{sn}\",\"LINE\":\"{device.Line}\",\"STATION\":\"{device.Station}\",\"TESTRESULT\":\"PASS\",\"FIXTUREID\":\"{device.FixtureId}\",\"TESTDATETIME\":\"{DateTime.Now.ToString()}\",\"LOGFILENAME\":\"{name}.zip\",\"STATIONTYPE\":\"AE\"}}";
  780. LogHelper.WriteLogMes($"【上传LogData的URL】{url}");
  781. SendTaskMessage($"【上传LogData的URL】{url}", MessageLevel.Info);
  782. (bool IsSuccess, string Response) result = (false, string.Empty);
  783. for (int i = 0; i < 3; i++)
  784. {
  785. try
  786. {
  787. using (var rsp = await _httpClient.GetAsync(url, cancellationToken))
  788. {
  789. var text = await rsp.Content.ReadAsStringAsync();
  790. LogHelper.WriteLogMes($"【上传LogData的HTTP接收】{text}");
  791. SendTaskMessage($"【上传LogData的HTTP接收】{text}", MessageLevel.Info);
  792. // "SFC_OK" 代表和 MES 连接成功
  793. if (text.Contains("\"Code\":1"))
  794. {
  795. return (true, text);
  796. }
  797. result = (false, text);
  798. }
  799. }
  800. catch (OperationCanceledException)
  801. {
  802. result = (false, "Canceled");
  803. }
  804. catch (Exception ex)
  805. {
  806. result = (false, ex.Message);
  807. }
  808. }
  809. return result;
  810. }
  811. /// <summary>
  812. /// 释放服务占用的托管资源(当前仅 <see cref="_httpClient"/>)。
  813. /// <para>本方法为幂等操作,重复调用不会产生异常。调用后实例不可再用于发起请求,尝试使用将抛出 <see cref="ObjectDisposedException"/>。</para>
  814. /// </summary>
  815. public void Dispose()
  816. {
  817. if (!_disposed)
  818. {
  819. _httpClient.Dispose();
  820. _disposed = true;
  821. }
  822. }
  823. public void SendTaskMessage(string msg, MessageLevel level)
  824. {
  825. App.Current.Dispatcher.Invoke(() =>
  826. {
  827. _eventAggregator.GetEvent<TaskMessageNotification>().Publish(new Models.MessageStruct() { Message = msg, level = level });
  828. });
  829. }
  830. }
  831. }