using Edge.Core.Processor; using System; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using Edge.Core.UniversalApi; using Edge.Core.Processor.Dispatcher.Attributes; using System.Threading.Tasks; using System.Net.Sockets; using Microsoft.Extensions.Logging.Abstractions; using System.Net; using System.Threading; using Edge.Core.Parser.BinaryParser.Util; using System.IO; using System.Text; namespace DispenserPartsInfoRetriever { /// /// 市场方面希望将我们广州团队当初在“广东中石化加油机信息在线项目”中所开发的加油机上信息参数 /// [UniversalApi(Name = OnAppStateChangeEventName, EventDataType = typeof(OnAppStateChangeEventArg), Description = "When the App overall State Changed, like app starts or stops, the event will be fired")] [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")] [UniversalApi(Name = OnDispenserPartsAlarmRetrievedEventName, EventDataType = typeof(OnDispenserPartsAlarmRetrievedEventArg), Description = "When parts alarm info is read from dispenser message, the event will be fired")] [MetaPartsDescriptor( "油机配件信息获取器", "获取油机配件信息,它基于http协议与油机通讯,采集油机端的较底层设备配件信息,并开放API供业务端应用.日志标记为DynamicPrivate_DispenserPartsInfoRetriever" , new[] { "Pump" })] public class App_HttpServer : IAppProcessor { public const string OnAppStateChangeEventName = "OnAppStateChange"; public const string OnDispenserConnStateChangeEventName = "OnDispenserConnStateChange"; public const string OnDispenserPartsAlarmRetrievedEventName = "OnDispenserPartsAlarmRetrieved"; public class OnAppStateChangeEventArg { public string State { get; set; } public string Reason { get; set; } } public class OnDispenserConnStateChangeEventArg { public string DispenserName { get; set; } public string Description { get; set; } public string State { get; set; } public string Reason { get; set; } } public class OnDispenserPartsAlarmRetrievedEventArg { public string DispenserName { get; set; } public string DispenserDescription { get; set; } public string AlarmName { get; set; } public string AlarmDetail { get; set; } } public class AppConfigV1 { public int ServerListenPort { get; set; } = 7788; /// /// by seconds /// public int MaxPeriodNoSeeDispenserIncomingHttpRequest { get; set; } = 10; public List DispenserInfoConfigs { get; set; } } public class DispenserInfoConfigV1 { public string IpAddress { get; set; } public string Name { get; set; } public string Description { get; set; } } private class HttpClientInfo { /// /// ip:port, like: 192.168.1.10:4567 /// public string HttpClientEndPointIdentity { get; } public HttpListenerContext httpClientContext { get; } public DispenserInfoConfigV1 BoundDispenserInfoConfig { get; } public HttpClientInfo(HttpListenerContext httpClientContext, AppConfigV1 appConfig) { this.httpClientContext = httpClientContext; this.HttpClientEndPointIdentity = this.httpClientContext.Request.LocalEndPoint.ToString(); this.BoundDispenserInfoConfig = appConfig.DispenserInfoConfigs.FirstOrDefault(c => HttpClientEndPointIdentity.StartsWith(c.IpAddress)) ?? new DispenserInfoConfigV1() { Name = $"未命名油机({this.HttpClientEndPointIdentity})", IpAddress = this.HttpClientEndPointIdentity, Description = "此油机未在本地WebConsole中定义和配置名称" }; } public DateTime LastMessageReceivedTime { get; set; } } private System.Timers.Timer timelyCheckDispenserIfOnlineTimer = new System.Timers.Timer(); /// /// the List is the message buffer, only a complete message is constructed, processing then start. /// private List clientInfos = new List(); private AppConfigV1 appConfig; private IServiceProvider services; private HttpListener httpServer; private ILogger logger = NullLogger.Instance; protected CancellationTokenSource cancelServerListeningCts; public string MetaConfigName { get; set; } [ParamsJsonSchemas("appCtorParamsJsonSchema")] public App_HttpServer(AppConfigV1 appConfig, IServiceProvider services) { this.appConfig = appConfig; this.services = services; var loggerFactory = services.GetRequiredService(); this.logger = loggerFactory.CreateLogger("DynamicPrivate_DispenserPartsInfoRetriever"); } public void Init(IEnumerable processors) { } public async Task Start() { this.cancelServerListeningCts = new CancellationTokenSource(); this.httpServer = new HttpListener(); this.httpServer.Prefixes.Add($"http://*:{this.appConfig.ServerListenPort}/"); this.logger.LogInformation($"Starting HttpServer for listening for All IP on port: {this.appConfig.ServerListenPort}"); var universalApiHub = this.services.GetRequiredService(); try { this.httpServer.Start(); } catch (Exception eee) { this.logger.LogInformation($"Failed to start HttpServer on port: {this.appConfig.ServerListenPort}, detail: {eee}"); await universalApiHub.FirePersistGenericAlarm(this, new GenericAlarm() { Title = $"主程序启动失败", Severity = GenericAlarmSeverity.Error, Detail = $"主程序启动失败, 详情: {eee}", }, ga => ga.Detail); throw new InvalidOperationException($"Failed to start HttpServer on port: {this.appConfig.ServerListenPort}, detail: {eee}"); } this.logger.LogInformation($" HttpServer started"); await universalApiHub.FireEvent(this, OnAppStateChangeEventName, new OnAppStateChangeEventArg() { State = "Started", Reason = "app is started" }); await universalApiHub.FirePersistGenericAlarm(this, new GenericAlarm() { Title = $"主程序启动成功", Severity = GenericAlarmSeverity.Information, Detail = $"主程序启动成功", }, ga => ga.Detail); _ = Task.Run(async () => { while (true) { HttpListenerContext newHttpClientContext = null; try { newHttpClientContext = await Task.Run(() => this.httpServer.GetContextAsync(), this.cancelServerListeningCts.Token); } catch (TaskCanceledException tce) { this.logger.LogInformation("Fcc as the Http server is canceled due to CancellationTokenSource was set."); try { this.httpServer?.Stop(); } catch { return; } finally { } } var newClientInfo = new HttpClientInfo(newHttpClientContext, this.appConfig) { LastMessageReceivedTime = DateTime.Now }; this.logger.LogDebug($"A http client: { newClientInfo.BoundDispenserInfoConfig.Name } is connecting and sending in msg."); var existedClientInfo = this.clientInfos.FirstOrDefault(k => k.HttpClientEndPointIdentity == newClientInfo.HttpClientEndPointIdentity.ToString()); if (existedClientInfo != null) { } else this.clientInfos.Add(newClientInfo); //var universalApiHub = this.services.GetRequiredService(); //await universalApiHub.FirePersistGenericAlarm(this, // new GenericAlarm() // { // Title = $"{newClientInfo.BoundDispenserInfoConfig?.Name}", // Severity = GenericAlarmSeverity.Information, // Detail = $"{newClientInfo.BoundDispenserInfoConfig?.Name} 联机成功", // }, ga => ga.Detail); //await universalApiHub.FireEvent(this, OnDispenserConnStateChangeEventName, // new OnDispenserConnStateChangeEventArg() { State = "Connected", DispenserName = $"{newHttpClientContext.Request.RemoteEndPoint.ToString()}", Reason = $"The dispenser with idenity: {newHttpClientContext.Request.RemoteEndPoint} is tcp connected to fcc" }); JsonDocument jsonDocument; try { jsonDocument = await JsonDocument.ParseAsync(newClientInfo.httpClientContext.Request.InputStream); } catch (Exception eee) { this.logger.LogInformation($"The http client: {newClientInfo.HttpClientEndPointIdentity.ToString()} has fatal invalid json content that can't even pased as json, exception: {eee}"); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.FirePersistGenericAlarm(this, new GenericAlarm() { Title = $"{newClientInfo.BoundDispenserInfoConfig?.Name}", Severity = GenericAlarmSeverity.Information, Detail = $"{newClientInfo.BoundDispenserInfoConfig?.Name} 发送至FCC的内容非法(非法的Json数据), 详情: {eee}", }, ga => ga.Detail); newHttpClientContext.Response.StatusCode = 400; newHttpClientContext.Response.OutputStream.Write(Encoding.UTF8.GetBytes("The data send in Fcc is an illegal Json content")); newHttpClientContext.Response.OutputStream.Close(); continue; } if (logger.IsEnabled(LogLevel.Trace)) { using (MemoryStream ms = new MemoryStream()) { using (var writer = new Utf8JsonWriter(ms)) { jsonDocument.WriteTo(writer); writer.Flush(); ms.Position = 0; this.logger.LogTrace($"{ newClientInfo.BoundDispenserInfoConfig.Name } incoming msg:{new StreamReader(ms).ReadToEnd()}"); } } } try { #region parse json doc var rootEle = jsonDocument.RootElement.GetProperty("dispenserRecord"); //加油机基本信息 var dispenserBasicInfo_property = rootEle.GetProperty("dispenserBasicInfo"); //关键部件信息 var keycomponentInfo_property = rootEle.GetProperty("keycomponentInfo"); #region 加油机部件寿命信息 var dispenserLifecycleInfo_property = rootEle.GetProperty("dispenserLifecycleInfo"); //PSAM有效期 var PSAMvalidityDate_array = dispenserLifecycleInfo_property.GetProperty("PSAMvalidityDate").EnumerateArray(); foreach (var el in PSAMvalidityDate_array) { var nozzleNO = el.GetProperty("nozzleNO").GetString(); var validityDate = el.GetProperty("validityDate").GetString(); } //读卡器开关次数 var ReaderLifecycleInfo_array = dispenserLifecycleInfo_property.GetProperty("ReaderLifecycleInfo").EnumerateArray(); foreach (var el in ReaderLifecycleInfo_array) { var nozzleNO = el.GetProperty("nozzleNO").GetString(); var valveCycle = el.GetProperty("valveCycle").GetString(); } //流量计升数 var meterLifecycleInfo_array = dispenserLifecycleInfo_property.GetProperty("meterLifecycleInfo").EnumerateArray(); foreach (var el in meterLifecycleInfo_array) { var nozzleNO = el.GetProperty("nozzleNO").GetString(); var meterSN = el.GetProperty("meterSN").GetString(); var meterVolume = el.GetProperty("meterVolume").GetString(); } //电磁阀开关次数 var valveLifecycleInfo_array = dispenserLifecycleInfo_property.GetProperty("valveLifecycleInfo").EnumerateArray(); foreach (var el in valveLifecycleInfo_array) { var nozzleNO = el.GetProperty("nozzleNO").GetString(); var valveNum = el.GetProperty("valveNum").GetString(); var valveCycle = el.GetProperty("valveCycle").GetString(); } #endregion #region 设备定期检修记录 var periodicalmaintenanceInfo_property = rootEle.GetProperty("periodicalmaintenanceInfo"); var hydraulicsystemPMDate = periodicalmaintenanceInfo_property.GetProperty("hydraulicsystemPMDate").GetString(); var controlsystemPMDate = periodicalmaintenanceInfo_property.GetProperty("controlsystemPMDate").GetString(); var gasrecoverysystemPMDate = periodicalmaintenanceInfo_property.GetProperty("gasrecoverysystemPMDate").GetString(); #endregion #region 加油机报警信息 var dispenserAlarm_property = rootEle.GetProperty("dispenserAlarm"); var nozzleStatus_array = dispenserAlarm_property.GetProperty("nozzleStatus").EnumerateArray(); foreach (var el in nozzleStatus_array) { var nozzleNO = el.GetProperty("nozzleNO").GetString(); var zerotransactionAlarm = el.GetProperty("zerotransactionAlarm").GetString(); var powerAlarm = el.GetProperty("powerAlarm").GetString(); var fuelingspeedAlarm = el.GetProperty("fuelingspeedAlarm").GetString(); var encoderAlarm = el.GetProperty("encoderAlarm").GetString(); var prestovershootAlarm = el.GetProperty("prestovershootAlarm").GetString(); var lockAlarm = el.GetProperty("lockAlarm").GetString(); var valveAlarm = el.GetProperty("valveAlarm").GetString(); var leakageAlarm = el.GetProperty("leakageAlarm").GetString(); var nozzleoffbootAlarm = el.GetProperty("nozzleoffbootAlarm").GetString(); var vaporrecoveryRatioAlarm = el.GetProperty("vaporrecoveryRatioAlarm").GetString(); } var taxchipAlarm_array = dispenserAlarm_property.GetProperty("taxchipAlarm").EnumerateArray(); foreach (var el in taxchipAlarm_array) { var nozzleNO = el.GetProperty("nozzleNO").GetString(); var taxchipAlarmCode = el.GetProperty("taxchipAlarmCode").GetString(); } #endregion //智能锁信息 var locker_property = rootEle.GetProperty("locker"); var lockerStatus_array = locker_property.GetProperty("lockerStatus").EnumerateArray(); foreach (var el in lockerStatus_array) { } #endregion } catch (Exception eeee) { this.logger.LogInformation($"The tcp client: {newClientInfo.HttpClientEndPointIdentity.ToString()} has partial error json data, exception: {eeee}"); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.FirePersistGenericAlarm(this, new GenericAlarm() { Title = $"{newClientInfo.BoundDispenserInfoConfig?.Name}", Severity = GenericAlarmSeverity.Information, Detail = $"{newClientInfo.BoundDispenserInfoConfig?.Name} 发送至FCC的内容缺失某些字段(Json格式整体合法,但缺少某些字段或者内部细节格式出错), 详情: {eeee}", }, ga => ga.Detail); } finally { newHttpClientContext.Response.StatusCode = 200; newHttpClientContext.Response.OutputStream.Write(Encoding.UTF8.GetBytes("I got you")); newHttpClientContext.Response.OutputStream.Close(); } } }); this.timelyCheckDispenserIfOnlineTimer.Elapsed += async (s, a) => { var connectedDispenserInfo = this.appConfig.DispenserInfoConfigs.Join(this.clientInfos, diConfig => diConfig.IpAddress, tci => tci.HttpClientEndPointIdentity.Split(':').First(), (tci, diConfig) => tci); var disconnectedDispenserInfo = this.appConfig.DispenserInfoConfigs.Except(connectedDispenserInfo); foreach (var di in disconnectedDispenserInfo) { var universalApiHub = this.services.GetRequiredService(); await universalApiHub.CloseAndFirePersistGenericAlarm(this, new GenericAlarm() { Title = $"{di.Name ?? ""} 从未主动联入", Severity = GenericAlarmSeverity.Error, Detail = $"{di.Name ?? ""} ({di.Description}) 未见此油机(本地配置其IP为: {di.IpAddress})主动通过 HTTP 连入 FCC" }, ga => ga.Detail, "new one is on creating", ga => ga.Detail); await universalApiHub.FireEvent(this, OnDispenserConnStateChangeEventName, new OnDispenserConnStateChangeEventArg() { State = "Disconnected", DispenserName = $"{di.Name ?? $""}", Description = $"{di.Description ?? ""}", Reason = $"have not see the dispenser(local config its ip to: {di.IpAddress}) actively connected in for a while" }); } if (!this.clientInfos.Any()) return; var longTimeNoSeeClientInfos = this.clientInfos.Where(tci => DateTime.Now.Subtract(tci.LastMessageReceivedTime).TotalSeconds >= this.appConfig.MaxPeriodNoSeeDispenserIncomingHttpRequest).ToArray(); foreach (var ci in longTimeNoSeeClientInfos) { try { this.logger.LogInformation($"{ci.BoundDispenserInfoConfig?.Name} long time no see its msg(上次发入时间为:{ci.LastMessageReceivedTime})"); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.CloseAndFirePersistGenericAlarm(this, new GenericAlarm() { Title = $"{ci.BoundDispenserInfoConfig.Name} 一段时间未见其发入消息", Severity = GenericAlarmSeverity.Error, Detail = $"{ci.BoundDispenserInfoConfig.Name} ({ci.BoundDispenserInfoConfig.Description}) 很久未见此油机(本地配置其IP为: {ci.HttpClientEndPointIdentity})发入消息" }, ga => ga.Detail, "new one is on creating", ga => ga.Detail); await universalApiHub.FireEvent(this, OnDispenserConnStateChangeEventName, new OnDispenserConnStateChangeEventArg() { State = "Disconnected", DispenserName = $"{ci.BoundDispenserInfoConfig.Name ?? $""}", Description = $"{ci.BoundDispenserInfoConfig.Description ?? ""}", Reason = $"have not see the dispenser(local config its ip to: {ci.HttpClientEndPointIdentity}) actively send in msg for a while(上次发入时间为:{ci.LastMessageReceivedTime})" }); } catch { } finally { //this.clientInfos.Remove(ci); } } }; this.timelyCheckDispenserIfOnlineTimer.Interval = 30000; this.timelyCheckDispenserIfOnlineTimer.Start(); return true; } /// /// /// /// like input "01234", will return byte[2]: 0x00, byte[1]: 0x12, byte[0]: 0x34 /// public byte[] StringToBcdHex(string value) { if (value.Length % 2 != 0) value = "0" + value; List output = new List(); for (int i = value.Length - 1; i >= 0; i = i - 2) { output.Add((byte)(byte.Parse(value[i - 1].ToString()) * 16 + byte.Parse(value[i].ToString()))); } output.Reverse(); return output.ToArray(); } public async Task Stop() { var universalApiHub = this.services.GetRequiredService(); await universalApiHub.FireEvent(this, OnAppStateChangeEventName, new OnAppStateChangeEventArg() { State = "Stopped", Reason = "app is stopping" }); try { this.httpServer?.Stop(); } catch { } try { this.timelyCheckDispenserIfOnlineTimer?.Stop(); } catch { } return true; } } }