App_HttpServer.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. using Edge.Core.Processor;
  2. using System;
  3. using Microsoft.EntityFrameworkCore;
  4. using Microsoft.Extensions.DependencyInjection;
  5. using Microsoft.Extensions.Logging;
  6. using System;
  7. using System.Collections.Generic;
  8. using System.Linq;
  9. using System.Text.Json;
  10. using Edge.Core.UniversalApi;
  11. using Edge.Core.Processor.Dispatcher.Attributes;
  12. using System.Threading.Tasks;
  13. using System.Net.Sockets;
  14. using Microsoft.Extensions.Logging.Abstractions;
  15. using System.Net;
  16. using System.Threading;
  17. using Edge.Core.Parser.BinaryParser.Util;
  18. using System.IO;
  19. using System.Text;
  20. namespace DispenserPartsInfoRetriever
  21. {
  22. /// <summary>
  23. /// 市场方面希望将我们广州团队当初在“广东中石化加油机信息在线项目”中所开发的加油机上信息参数
  24. /// </summary>
  25. [UniversalApi(Name = OnAppStateChangeEventName, EventDataType = typeof(OnAppStateChangeEventArg), Description = "When the App overall State Changed, like app starts or stops, the event will be fired")]
  26. [UniversalApi(Name = OnDispenserConnStateChangeEventName, EventDataType = typeof(OnDispenserConnStateChangeEventArg), Description = "When communication state changed for certain dispensere, like the connection between a dispenser and fcc is established or broken, the event will be fired")]
  27. [UniversalApi(Name = OnDispenserPartsAlarmRetrievedEventName, EventDataType = typeof(OnDispenserPartsAlarmRetrievedEventArg), Description = "When parts alarm info is read from dispenser message, the event will be fired")]
  28. [MetaPartsDescriptor(
  29. "油机配件信息获取器",
  30. "获取油机配件信息,它基于http协议与油机通讯,采集油机端的较底层设备配件信息,并开放API供业务端应用.日志标记为DynamicPrivate_DispenserPartsInfoRetriever"
  31. , new[] { "Pump" })]
  32. public class App_HttpServer : IAppProcessor
  33. {
  34. public const string OnAppStateChangeEventName = "OnAppStateChange";
  35. public const string OnDispenserConnStateChangeEventName = "OnDispenserConnStateChange";
  36. public const string OnDispenserPartsAlarmRetrievedEventName = "OnDispenserPartsAlarmRetrieved";
  37. public class OnAppStateChangeEventArg
  38. {
  39. public string State { get; set; }
  40. public string Reason { get; set; }
  41. }
  42. public class OnDispenserConnStateChangeEventArg
  43. {
  44. public string DispenserName { get; set; }
  45. public string Description { get; set; }
  46. public string State { get; set; }
  47. public string Reason { get; set; }
  48. }
  49. public class OnDispenserPartsAlarmRetrievedEventArg
  50. {
  51. public string DispenserName { get; set; }
  52. public string DispenserDescription { get; set; }
  53. public string AlarmName { get; set; }
  54. public string AlarmDetail { get; set; }
  55. }
  56. public class AppConfigV1
  57. {
  58. public int ServerListenPort { get; set; } = 7788;
  59. /// <summary>
  60. /// by seconds
  61. /// </summary>
  62. public int MaxPeriodNoSeeDispenserIncomingHttpRequest { get; set; } = 10;
  63. public List<DispenserInfoConfigV1> DispenserInfoConfigs { get; set; }
  64. }
  65. public class DispenserInfoConfigV1
  66. {
  67. public string IpAddress { get; set; }
  68. public string Name { get; set; }
  69. public string Description { get; set; }
  70. }
  71. private class HttpClientInfo
  72. {
  73. /// <summary>
  74. /// ip:port, like: 192.168.1.10:4567
  75. /// </summary>
  76. public string HttpClientEndPointIdentity { get; }
  77. public HttpListenerContext httpClientContext { get; }
  78. public DispenserInfoConfigV1 BoundDispenserInfoConfig { get; }
  79. public HttpClientInfo(HttpListenerContext httpClientContext, AppConfigV1 appConfig)
  80. {
  81. this.httpClientContext = httpClientContext;
  82. this.HttpClientEndPointIdentity = this.httpClientContext.Request.LocalEndPoint.ToString();
  83. this.BoundDispenserInfoConfig = appConfig.DispenserInfoConfigs.FirstOrDefault(c =>
  84. HttpClientEndPointIdentity.StartsWith(c.IpAddress))
  85. ?? new DispenserInfoConfigV1()
  86. {
  87. Name = $"未命名油机({this.HttpClientEndPointIdentity})",
  88. IpAddress = this.HttpClientEndPointIdentity,
  89. Description = "此油机未在本地WebConsole中定义和配置名称"
  90. };
  91. }
  92. public DateTime LastMessageReceivedTime { get; set; }
  93. }
  94. private System.Timers.Timer timelyCheckDispenserIfOnlineTimer = new System.Timers.Timer();
  95. /// <summary>
  96. /// the List is the message buffer, only a complete message is constructed, processing then start.
  97. /// </summary>
  98. private List<HttpClientInfo> clientInfos = new List<HttpClientInfo>();
  99. private AppConfigV1 appConfig;
  100. private IServiceProvider services;
  101. private HttpListener httpServer;
  102. private ILogger logger = NullLogger.Instance;
  103. protected CancellationTokenSource cancelServerListeningCts;
  104. public string MetaConfigName { get; set; }
  105. [ParamsJsonSchemas("appCtorParamsJsonSchema")]
  106. public App_HttpServer(AppConfigV1 appConfig, IServiceProvider services)
  107. {
  108. this.appConfig = appConfig;
  109. this.services = services;
  110. var loggerFactory = services.GetRequiredService<ILoggerFactory>();
  111. this.logger = loggerFactory.CreateLogger("DynamicPrivate_DispenserPartsInfoRetriever");
  112. }
  113. public void Init(IEnumerable<IProcessor> processors)
  114. {
  115. }
  116. public async Task<bool> Start()
  117. {
  118. this.cancelServerListeningCts = new CancellationTokenSource();
  119. this.httpServer = new HttpListener();
  120. this.httpServer.Prefixes.Add($"http://*:{this.appConfig.ServerListenPort}/");
  121. this.logger.LogInformation($"Starting HttpServer for listening for All IP on port: {this.appConfig.ServerListenPort}");
  122. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  123. try
  124. {
  125. this.httpServer.Start();
  126. }
  127. catch (Exception eee)
  128. {
  129. this.logger.LogInformation($"Failed to start HttpServer on port: {this.appConfig.ServerListenPort}, detail: {eee}");
  130. await universalApiHub.FirePersistGenericAlarm(this,
  131. new GenericAlarm()
  132. {
  133. Title = $"主程序启动失败",
  134. Severity = GenericAlarmSeverity.Error,
  135. Detail = $"主程序启动失败, 详情: {eee}",
  136. }, ga => ga.Detail);
  137. throw new InvalidOperationException($"Failed to start HttpServer on port: {this.appConfig.ServerListenPort}, detail: {eee}");
  138. }
  139. this.logger.LogInformation($" HttpServer started");
  140. await universalApiHub.FireEvent(this, OnAppStateChangeEventName, new OnAppStateChangeEventArg() { State = "Started", Reason = "app is started" });
  141. await universalApiHub.FirePersistGenericAlarm(this,
  142. new GenericAlarm()
  143. {
  144. Title = $"主程序启动成功",
  145. Severity = GenericAlarmSeverity.Information,
  146. Detail = $"主程序启动成功",
  147. }, ga => ga.Detail);
  148. _ = Task.Run(async () =>
  149. {
  150. while (true)
  151. {
  152. HttpListenerContext newHttpClientContext = null;
  153. try
  154. {
  155. newHttpClientContext = await Task.Run(() => this.httpServer.GetContextAsync(), this.cancelServerListeningCts.Token);
  156. }
  157. catch (TaskCanceledException tce)
  158. {
  159. this.logger.LogInformation("Fcc as the Http server is canceled due to CancellationTokenSource was set.");
  160. try
  161. {
  162. this.httpServer?.Stop();
  163. }
  164. catch { return; }
  165. finally { }
  166. }
  167. var newClientInfo = new HttpClientInfo(newHttpClientContext, this.appConfig) { LastMessageReceivedTime = DateTime.Now };
  168. this.logger.LogDebug($"A http client: { newClientInfo.BoundDispenserInfoConfig.Name } is connecting and sending in msg.");
  169. var existedClientInfo = this.clientInfos.FirstOrDefault(k => k.HttpClientEndPointIdentity == newClientInfo.HttpClientEndPointIdentity.ToString());
  170. if (existedClientInfo != null)
  171. { }
  172. else
  173. this.clientInfos.Add(newClientInfo);
  174. //var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  175. //await universalApiHub.FirePersistGenericAlarm(this,
  176. // new GenericAlarm()
  177. // {
  178. // Title = $"{newClientInfo.BoundDispenserInfoConfig?.Name}",
  179. // Severity = GenericAlarmSeverity.Information,
  180. // Detail = $"{newClientInfo.BoundDispenserInfoConfig?.Name} 联机成功",
  181. // }, ga => ga.Detail);
  182. //await universalApiHub.FireEvent(this, OnDispenserConnStateChangeEventName,
  183. // new OnDispenserConnStateChangeEventArg() { State = "Connected", DispenserName = $"{newHttpClientContext.Request.RemoteEndPoint.ToString()}", Reason = $"The dispenser with idenity: {newHttpClientContext.Request.RemoteEndPoint} is tcp connected to fcc" });
  184. JsonDocument jsonDocument;
  185. try
  186. {
  187. jsonDocument = await JsonDocument.ParseAsync(newClientInfo.httpClientContext.Request.InputStream);
  188. }
  189. catch (Exception eee)
  190. {
  191. this.logger.LogInformation($"The http client: {newClientInfo.HttpClientEndPointIdentity.ToString()} has fatal invalid json content that can't even pased as json, exception: {eee}");
  192. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  193. await universalApiHub.FirePersistGenericAlarm(this,
  194. new GenericAlarm()
  195. {
  196. Title = $"{newClientInfo.BoundDispenserInfoConfig?.Name}",
  197. Severity = GenericAlarmSeverity.Information,
  198. Detail = $"{newClientInfo.BoundDispenserInfoConfig?.Name} 发送至FCC的内容非法(非法的Json数据), 详情: {eee}",
  199. }, ga => ga.Detail);
  200. newHttpClientContext.Response.StatusCode = 400;
  201. newHttpClientContext.Response.OutputStream.Write(Encoding.UTF8.GetBytes("<HTML><BODY>The data send in Fcc is an illegal Json content</BODY></HTML>"));
  202. newHttpClientContext.Response.OutputStream.Close();
  203. continue;
  204. }
  205. if (logger.IsEnabled(LogLevel.Trace))
  206. {
  207. using (MemoryStream ms = new MemoryStream())
  208. {
  209. using (var writer = new Utf8JsonWriter(ms))
  210. {
  211. jsonDocument.WriteTo(writer);
  212. writer.Flush();
  213. ms.Position = 0;
  214. this.logger.LogTrace($"{ newClientInfo.BoundDispenserInfoConfig.Name } incoming msg:{new StreamReader(ms).ReadToEnd()}");
  215. }
  216. }
  217. }
  218. try
  219. {
  220. #region parse json doc
  221. var rootEle = jsonDocument.RootElement.GetProperty("dispenserRecord");
  222. //加油机基本信息
  223. var dispenserBasicInfo_property = rootEle.GetProperty("dispenserBasicInfo");
  224. //关键部件信息
  225. var keycomponentInfo_property = rootEle.GetProperty("keycomponentInfo");
  226. #region 加油机部件寿命信息
  227. var dispenserLifecycleInfo_property = rootEle.GetProperty("dispenserLifecycleInfo");
  228. //PSAM有效期
  229. var PSAMvalidityDate_array = dispenserLifecycleInfo_property.GetProperty("PSAMvalidityDate").EnumerateArray();
  230. foreach (var el in PSAMvalidityDate_array)
  231. {
  232. var nozzleNO = el.GetProperty("nozzleNO").GetString();
  233. var validityDate = el.GetProperty("validityDate").GetString();
  234. }
  235. //读卡器开关次数
  236. var ReaderLifecycleInfo_array = dispenserLifecycleInfo_property.GetProperty("ReaderLifecycleInfo").EnumerateArray();
  237. foreach (var el in ReaderLifecycleInfo_array)
  238. {
  239. var nozzleNO = el.GetProperty("nozzleNO").GetString();
  240. var valveCycle = el.GetProperty("valveCycle").GetString();
  241. }
  242. //流量计升数
  243. var meterLifecycleInfo_array = dispenserLifecycleInfo_property.GetProperty("meterLifecycleInfo").EnumerateArray();
  244. foreach (var el in meterLifecycleInfo_array)
  245. {
  246. var nozzleNO = el.GetProperty("nozzleNO").GetString();
  247. var meterSN = el.GetProperty("meterSN").GetString();
  248. var meterVolume = el.GetProperty("meterVolume").GetString();
  249. }
  250. //电磁阀开关次数
  251. var valveLifecycleInfo_array = dispenserLifecycleInfo_property.GetProperty("valveLifecycleInfo").EnumerateArray();
  252. foreach (var el in valveLifecycleInfo_array)
  253. {
  254. var nozzleNO = el.GetProperty("nozzleNO").GetString();
  255. var valveNum = el.GetProperty("valveNum").GetString();
  256. var valveCycle = el.GetProperty("valveCycle").GetString();
  257. }
  258. #endregion
  259. #region 设备定期检修记录
  260. var periodicalmaintenanceInfo_property = rootEle.GetProperty("periodicalmaintenanceInfo");
  261. var hydraulicsystemPMDate = periodicalmaintenanceInfo_property.GetProperty("hydraulicsystemPMDate").GetString();
  262. var controlsystemPMDate = periodicalmaintenanceInfo_property.GetProperty("controlsystemPMDate").GetString();
  263. var gasrecoverysystemPMDate = periodicalmaintenanceInfo_property.GetProperty("gasrecoverysystemPMDate").GetString();
  264. #endregion
  265. #region 加油机报警信息
  266. var dispenserAlarm_property = rootEle.GetProperty("dispenserAlarm");
  267. var nozzleStatus_array = dispenserAlarm_property.GetProperty("nozzleStatus").EnumerateArray();
  268. foreach (var el in nozzleStatus_array)
  269. {
  270. var nozzleNO = el.GetProperty("nozzleNO").GetString();
  271. var zerotransactionAlarm = el.GetProperty("zerotransactionAlarm").GetString();
  272. var powerAlarm = el.GetProperty("powerAlarm").GetString();
  273. var fuelingspeedAlarm = el.GetProperty("fuelingspeedAlarm").GetString();
  274. var encoderAlarm = el.GetProperty("encoderAlarm").GetString();
  275. var prestovershootAlarm = el.GetProperty("prestovershootAlarm").GetString();
  276. var lockAlarm = el.GetProperty("lockAlarm").GetString();
  277. var valveAlarm = el.GetProperty("valveAlarm").GetString();
  278. var leakageAlarm = el.GetProperty("leakageAlarm").GetString();
  279. var nozzleoffbootAlarm = el.GetProperty("nozzleoffbootAlarm").GetString();
  280. var vaporrecoveryRatioAlarm = el.GetProperty("vaporrecoveryRatioAlarm").GetString();
  281. }
  282. var taxchipAlarm_array = dispenserAlarm_property.GetProperty("taxchipAlarm").EnumerateArray();
  283. foreach (var el in taxchipAlarm_array)
  284. {
  285. var nozzleNO = el.GetProperty("nozzleNO").GetString();
  286. var taxchipAlarmCode = el.GetProperty("taxchipAlarmCode").GetString();
  287. }
  288. #endregion
  289. //智能锁信息
  290. var locker_property = rootEle.GetProperty("locker");
  291. var lockerStatus_array = locker_property.GetProperty("lockerStatus").EnumerateArray();
  292. foreach (var el in lockerStatus_array)
  293. {
  294. }
  295. #endregion
  296. }
  297. catch (Exception eeee)
  298. {
  299. this.logger.LogInformation($"The tcp client: {newClientInfo.HttpClientEndPointIdentity.ToString()} has partial error json data, exception: {eeee}");
  300. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  301. await universalApiHub.FirePersistGenericAlarm(this,
  302. new GenericAlarm()
  303. {
  304. Title = $"{newClientInfo.BoundDispenserInfoConfig?.Name}",
  305. Severity = GenericAlarmSeverity.Information,
  306. Detail = $"{newClientInfo.BoundDispenserInfoConfig?.Name} 发送至FCC的内容缺失某些字段(Json格式整体合法,但缺少某些字段或者内部细节格式出错), 详情: {eeee}",
  307. }, ga => ga.Detail);
  308. }
  309. finally
  310. {
  311. newHttpClientContext.Response.StatusCode = 200;
  312. newHttpClientContext.Response.OutputStream.Write(Encoding.UTF8.GetBytes("<HTML><BODY>I got you</BODY></HTML>"));
  313. newHttpClientContext.Response.OutputStream.Close();
  314. }
  315. }
  316. });
  317. this.timelyCheckDispenserIfOnlineTimer.Elapsed += async (s, a) =>
  318. {
  319. var connectedDispenserInfo = this.appConfig.DispenserInfoConfigs.Join(this.clientInfos, diConfig => diConfig.IpAddress, tci => tci.HttpClientEndPointIdentity.Split(':').First(), (tci, diConfig) => tci);
  320. var disconnectedDispenserInfo = this.appConfig.DispenserInfoConfigs.Except(connectedDispenserInfo);
  321. foreach (var di in disconnectedDispenserInfo)
  322. {
  323. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  324. await universalApiHub.CloseAndFirePersistGenericAlarm(this,
  325. new GenericAlarm()
  326. {
  327. Title = $"{di.Name ?? ""} 从未主动联入",
  328. Severity = GenericAlarmSeverity.Error,
  329. Detail = $"{di.Name ?? ""} ({di.Description}) 未见此油机(本地配置其IP为: {di.IpAddress})主动通过 HTTP 连入 FCC"
  330. }, ga => ga.Detail, "new one is on creating", ga => ga.Detail);
  331. await universalApiHub.FireEvent(this, OnDispenserConnStateChangeEventName,
  332. new OnDispenserConnStateChangeEventArg()
  333. {
  334. State = "Disconnected",
  335. DispenserName = $"{di.Name ?? $""}",
  336. Description = $"{di.Description ?? ""}",
  337. Reason = $"have not see the dispenser(local config its ip to: {di.IpAddress}) actively connected in for a while"
  338. });
  339. }
  340. if (!this.clientInfos.Any()) return;
  341. var longTimeNoSeeClientInfos =
  342. this.clientInfos.Where(tci => DateTime.Now.Subtract(tci.LastMessageReceivedTime).TotalSeconds >= this.appConfig.MaxPeriodNoSeeDispenserIncomingHttpRequest).ToArray();
  343. foreach (var ci in longTimeNoSeeClientInfos)
  344. {
  345. try
  346. {
  347. this.logger.LogInformation($"{ci.BoundDispenserInfoConfig?.Name} long time no see its msg(上次发入时间为:{ci.LastMessageReceivedTime})");
  348. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  349. await universalApiHub.CloseAndFirePersistGenericAlarm(this,
  350. new GenericAlarm()
  351. {
  352. Title = $"{ci.BoundDispenserInfoConfig.Name} 一段时间未见其发入消息",
  353. Severity = GenericAlarmSeverity.Error,
  354. Detail = $"{ci.BoundDispenserInfoConfig.Name} ({ci.BoundDispenserInfoConfig.Description}) 很久未见此油机(本地配置其IP为: {ci.HttpClientEndPointIdentity})发入消息"
  355. }, ga => ga.Detail, "new one is on creating", ga => ga.Detail);
  356. await universalApiHub.FireEvent(this, OnDispenserConnStateChangeEventName,
  357. new OnDispenserConnStateChangeEventArg()
  358. {
  359. State = "Disconnected",
  360. DispenserName = $"{ci.BoundDispenserInfoConfig.Name ?? $""}",
  361. Description = $"{ci.BoundDispenserInfoConfig.Description ?? ""}",
  362. Reason = $"have not see the dispenser(local config its ip to: {ci.HttpClientEndPointIdentity}) actively send in msg for a while(上次发入时间为:{ci.LastMessageReceivedTime})"
  363. });
  364. }
  365. catch { }
  366. finally
  367. {
  368. //this.clientInfos.Remove(ci);
  369. }
  370. }
  371. };
  372. this.timelyCheckDispenserIfOnlineTimer.Interval = 30000;
  373. this.timelyCheckDispenserIfOnlineTimer.Start();
  374. return true;
  375. }
  376. /// <summary>
  377. ///
  378. /// </summary>
  379. /// <param name="value">like input "01234", will return byte[2]: 0x00, byte[1]: 0x12, byte[0]: 0x34</param>
  380. /// <returns></returns>
  381. public byte[] StringToBcdHex(string value)
  382. {
  383. if (value.Length % 2 != 0) value = "0" + value;
  384. List<byte> output = new List<byte>();
  385. for (int i = value.Length - 1; i >= 0; i = i - 2)
  386. {
  387. output.Add((byte)(byte.Parse(value[i - 1].ToString()) * 16 + byte.Parse(value[i].ToString())));
  388. }
  389. output.Reverse();
  390. return output.ToArray();
  391. }
  392. public async Task<bool> Stop()
  393. {
  394. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  395. await universalApiHub.FireEvent(this, OnAppStateChangeEventName, new OnAppStateChangeEventArg() { State = "Stopped", Reason = "app is stopping" });
  396. try { this.httpServer?.Stop(); } catch { }
  397. try { this.timelyCheckDispenserIfOnlineTimer?.Stop(); } catch { }
  398. return true;
  399. }
  400. }
  401. }