using AutoMapper; using Edge.Core.Database; using Edge.Core.Processor; using Edge.Core.IndustryStandardInterface.Pump; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Wayne_VaporRecoveryDataCollectorBoard.MessageEntity; using Wayne_VaporRecoveryDataCollectorBoard.MessageEntity.Incoming; using Wayne_VaporRecoveryDataCollectorBoard.MessageEntity.Outgoing; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; using Timer = System.Timers.Timer; using Edge.Core.Processor.Dispatcher.Attributes; using Edge.Core.Processor.Communicator; using Edge.Core.UniversalApi; using System.Threading; namespace Wayne_VaporRecoveryDataCollectorBoard { [MetaPartsRequired(typeof(HalfDuplexActivePollingDeviceProcessor<,>))] [MetaPartsRequired(typeof(ComPortCommunicator<>))] [MetaPartsRequired(typeof(TcpClientCommunicator<>))] //[UniversalApi(Name = GenericAlarm.UniversalApiEventName, EventDataType = typeof(GenericAlarm[]), Description = "Fire GenericAlarms to AlarmBar for attracting users.")] [MetaPartsDescriptor( "lang-zh-cn:稳牌气液比数据收集板lang-zh-tw:穩牌氣液比數據收集板lang-en-us:稳牌气液比数据收集板", "lang-zh-cn:用于驱动 稳牌气液比数据收集板,日志名称 DynamicPrivate_VaporRecoveryDataCollectorBoard" + "lang-zh-tw:用於驅動 穩牌氣液比數據收集板" + "lang-en-us:Used for driven Wayne Vapor and Liquid collector board, log file name: DynamicPrivate_VaporRecoveryDataCollectorBoard", new[] { "lang-zh-cn:在线监测lang-zh-tw:在線監測lang-en-us:OnlineWatch" })] /// /// one comm channel may have multiple devices connected. /// public class GroupHandler : TestableActivePollingDeviceHandler, IEnumerable, IDisposable { //private DateTime lastDeviceMessageReceivedTime; // by seconds, change this value need change the correlated deviceOfflineCountdownTimer's interval as well //public const int maxSecondsDeviceMuteWillTreatAsOffline = 30; //private Timer deviceOfflineCountdownTimer; private IServiceProvider services; public event EventHandler OnBoardStateChange; /// /// notify only the nozzle fuelling state change. /// public event EventHandler OnBoardNozzleStateChange; /// /// either a trx done flow data, or a serials of real time flow data. /// public event EventHandler OnDataRecieved; //public string HardwareIdentity { get; set; } public IContext Context { get { return this.context; } } static ILogger logger = NullLogger.Instance; private int singleBoardOfflineTimeThresholdByMs = 15000; private Timer singleBoardOfflineCheckTimer; public List Boards { get; private set; } = new List(); /// /// the device has an internal time, if that time lagging too much from Fc, then FC will send a Write Time. /// this value controls as the threshold. /// private const int maxTimeGapFromFcAndDeviceBySeconds = 60 * 5; //private XmlConfiguration configuration; private IContext context; //public byte DeviceAddress { get; set; } public DeviceConfigV1 DeviceConfig { get; private set; } public DataCollectorBoardTypeEnum? CurrentDataCollectorType { get; set; } //public int DeviceId { get; } public class DeviceConfigV1 { public IEnumerable BoardConfigs { get; set; } } public class BoardConfigV1 { /// /// each board has a serial number of global unique, like the mac address. /// public int SerialNumber { get; set; } /// /// fcc will based on serial number to set a physical address to board dynamically. /// public byte SetPhysicalAddress { get; set; } public IEnumerable NozzleConfigs { get; set; } } public class BoardInitParameterConfigV1 { public byte 最大未变化次数 { get; set; } public int 开始加油阀值 { get; set; } public int 停止加油阀值 { get; set; } public int 最小加油量 { get; set; } public int 加油脉冲当量 { get; set; } public int 油气脉冲当量 { get; set; } /// /// 控制板将依据此值在加油过程中调节气泵回气量,以求尽可能靠近此比值 /// public double 气液比值 { get; set; } public string ToLogString() { return "最大未变化次数: " + this.最大未变化次数 + ", 开始加油阀值: " + this.开始加油阀值 + ", 停止加油阀值: " + this.停止加油阀值 + ", 最小加油量: " + this.最小加油量 + ", 加油脉冲当量: " + this.加油脉冲当量 + ", 油气脉冲当量: " + this.油气脉冲当量 + ", 气液比值: " + this.气液比值; } } public class BoardNozzleConfigV1 { public int OnBoardNozzleNumber { get; set; } /// /// each vr watched nozzle is a physical pump nozzle, and each nozzle must have a site level nozzle id(in China), /// here is the binding info, this is the key info for draw UI for user. /// public int SiteLevelNozzleId { get; set; } /// /// specified by config in device handler, has the priority compare to external input values. /// public BoardInitParameterConfigV1 ManualInitParameter { get; set; } /// /// specified by config from external(like an app), has the low priority compare to internal manual values. /// public BoardInitParameterConfigV1 ExternalInitParameter { get; set; } } protected void FireOnBoardStateChangeEvent(Board board) { this.OnBoardStateChange?.Invoke(this, new BoardStateChangeEventArgs(board)); } protected void FireOnDataReceivedEvent(DataCollectorMessageBase data) { this.OnDataRecieved?.Invoke(this, new DataRecievedEventArgs(data)); } protected void FireOnBoardNozzleStateChangeEvent(BoardNozzle nozzle) { this.OnBoardNozzleStateChange?.Invoke(this, new BoardNozzleStateChangeEventArgs(nozzle)); } [ParamsJsonSchemas("DeviceGroupHandlerCtorParamsJsonSchemas")] public GroupHandler(DeviceConfigV1 deviceConfig, IServiceProvider services) { if (deviceConfig.BoardConfigs.Select(bc => bc.SetPhysicalAddress).GroupBy(g => g).Any(g => g.Count() >= 2)) throw new ArgumentException("2 or more board configs have the same PhysicalAddress defined, while it should be unique per one comm channel"); if (deviceConfig.BoardConfigs.Select(bc => bc.SerialNumber).GroupBy(g => g).Any(g => g.Count() >= 2)) throw new ArgumentException("2 or more board configs have the same SerialNumber defined, while it should be unique per one comm channel"); this.services = services; this.DeviceConfig = deviceConfig; var loggerFactory = services.GetRequiredService(); logger = loggerFactory.CreateLogger("DynamicPrivate_VaporRecoveryDataCollectorBoard"); deviceConfig.BoardConfigs.ToList().ForEach(bc => { var board = new Board(bc.SerialNumber, bc.SetPhysicalAddress, BoardStateEnum.UnInit, bc.NozzleConfigs.Select(nc => new BoardNozzle() { NozzleNumberOnBoard = (byte)nc.OnBoardNozzleNumber, SiteLevelNozzleId = nc.SiteLevelNozzleId, NozzleState = NozzleStateEnum.Unknown }).ToList()); board.LastIncomingMessageReceivedTime = DateTime.Now; this.Boards.Add(board); }); //DeviceId = deviceId; //HardwareIdentity = config.SerialNumber; // sample // // // <最大未变化次数>5 // <开始加油阀值>16777255 // <停止加油阀值>268435495 // <最小加油量>268435495 // <加油脉冲当量>268435456 // <油气脉冲当量>1677721600 // <气液比值>2013265924 // // // <最大未变化次数>5 // <开始加油阀值>16777255 // <停止加油阀值>268435495 // <最小加油量>268435495 // <加油脉冲当量>268435456 // <油气脉冲当量>1677721600 // <气液比值>2013265924 // // //this.DeviceAddress = (byte)bindingDataCollectorAddress; //var xmlSerializer = new XmlSerializer(typeof(XmlConfiguration)); //this.configuration = // xmlSerializer.Deserialize(new StringReader(xmlConfig)) as XmlConfiguration; } public async override void Init(IContext context) { base.Init(context); this.context = context; // may have many boards in underlying communicator(share one channel), so here dynamic set offline time but with a safe value for avoid too sensitive. this.singleBoardOfflineTimeThresholdByMs = 2200 * this.Boards.Count >= 6000 ? 2200 * this.Boards.Count : 6000; //accuracy is 1000ms this.singleBoardOfflineCheckTimer = new Timer(1000); this.singleBoardOfflineCheckTimer.Elapsed += async (a, b) => { var offlineBoards = this.Boards.Where(b => DateTime.Now.Subtract(b.LastIncomingMessageReceivedTime).TotalMilliseconds >= this.singleBoardOfflineTimeThresholdByMs); foreach (var offBoard in offlineBoards) { if (offBoard.State == BoardStateEnum.UnInit || offBoard.State == BoardStateEnum.Initializing) continue; logger.LogInformation($"Board with serialNumber: {offBoard.SerialNumber}, PhyAddr: {offBoard.BoardPhysicalAddress} is offline({this.singleBoardOfflineTimeThresholdByMs}ms no see data), turn state to UnInit..."); offBoard.State = BoardStateEnum.UnInit; this.FireOnBoardStateChangeEvent(offBoard); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.FirePersistGenericAlarmIfNotExists(this.context.Processor, new GenericAlarm() { Title = $"lang-en-us:V/R board disconnectedlang-zh-cn:气液比采集板通讯断开lang-zh-tw:氣液比數據采集板通訊斷開", Category = $"lang-zh-cn:气液比采集板lang-zh-tw:氣液比采集板", Detail = $"lang-zh-cn:断开采集板的SN是:{offBoard.SerialNumber},上面配置的全站枪号是: " + $"{string.Join(", ", offBoard.Nozzles.Select(n => n.SiteLevelNozzleId.ToString()))}" + $"lang-zh-tw:斷開采集板的SN是:{offBoard.SerialNumber},上面配置的全站槍號是: " + $"{string.Join(", ", offBoard.Nozzles.Select(n => n.SiteLevelNozzleId.ToString()))}", Severity = GenericAlarmSeverity.Warning }, ga => ga.Detail, ga => ga.Detail); } }; this.singleBoardOfflineCheckTimer.Start(); this.context.Incoming.OnLongTimeNoSeeMessage += async (_, __) => { logger.LogInformation($"Boards group(serialNumbers are: {string.Join(" ,", this.Boards.Select(b => b.SerialNumber.ToString()))}, PhyAddr are: {string.Join(" ,", this.Boards.Select(b => b.BoardPhysicalAddress.ToString()))}) has lone time no see incoming message from underlying comm, switching all boards' state to UnInit."); this.Boards.ForEach(s => { if (s.State != BoardStateEnum.UnInit) { s.State = BoardStateEnum.UnInit; this.FireOnBoardStateChangeEvent(s); } }); var disconnectedBoardPhyAddressesStr = this.DeviceConfig.BoardConfigs?.Select(b => b.SetPhysicalAddress.ToString()).Aggregate("", (acc, n) => acc + ", " + n); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.FirePersistGenericAlarmIfNotExists(context.Processor, new GenericAlarm() { Title = $"lang-en-us:Multiple V/R board disconnectedlang-zh-cn:多块气液比数据采集板通讯断开lang-zh-tw:多塊氣液比數據采集板通訊斷開", Category = $"lang-zh-cn:气液比采集板lang-zh-tw:氣液比采集板", Detail = $"lang-zh-cn:断开采集板的物理地址分别是:{disconnectedBoardPhyAddressesStr}" + $"lang-zh-tw:斷開采集板的物理地址分別是:{disconnectedBoardPhyAddressesStr}", Severity = GenericAlarmSeverity.Warning }, ga => ga.Detail, ga => ga.Detail); }; this.context.Incoming.LongTimeNoSeeMessageTimeout = 18000; var timeWindowWithActivePollingOutgoing = this.context.Outgoing as TimeWindowWithActivePollingOutgoing; int prePolledBoardIndex = 0; timeWindowWithActivePollingOutgoing.PollingMsgProducer = () => { if (prePolledBoardIndex == this.Boards.Count) prePolledBoardIndex = 0; var targetBoard = this.Boards[prePolledBoardIndex]; prePolledBoardIndex++; switch (targetBoard.State) { case BoardStateEnum.Initialized: { return new READ_WORKING_TYPE_Request() { Address = targetBoard.BoardPhysicalAddress }; } case BoardStateEnum.UnInit: { //logger.LogInformation($"Board with SerialNumber: {targetBoard.SerialNumber }, PhysicalAddress: {targetBoard.BoardPhysicalAddress} is turning state from UnInit to Initializing..."); targetBoard.State = BoardStateEnum.Initializing; this.FireOnBoardStateChangeEvent(targetBoard); Task.Run(() => { try { this.InitializeDevice(targetBoard.SerialNumber, targetBoard.BoardPhysicalAddress).ContinueWith(async (pt) => { if (pt.Result) { targetBoard.State = BoardStateEnum.Initialized; var universalApiHub = this.services.GetRequiredService(); await universalApiHub.ClosePersistGenericAlarms(this.context.Processor, $"断开", "conn recovered"); } else { targetBoard.State = BoardStateEnum.UnInit; } this.FireOnBoardStateChangeEvent(targetBoard); }); } catch (Exception exxx) { logger.LogError($"InitializeDevice Board with SerialNumber: {targetBoard.SerialNumber} will set to PhyAddr: {targetBoard.BoardPhysicalAddress} exceptioned with: {exxx}"); } }); return null; } case BoardStateEnum.Initializing: { return null; } }; return null; }; context.Communicator.OnConnected += async (_, __) => { //await Task.Delay(5000).ContinueWith((t) => this.InitializeDevice()); }; } /// /// send params and re-sync time from device. /// , /// private async Task InitializeDevice(int boardSerialNumber, byte setBoardPhysicalAddress) { logger.LogInformation($"InitializeDevice ==========> Board with SerialNumber: {boardSerialNumber} will set to PhyAddr: {setBoardPhysicalAddress}..."); var setAddressResponse = await context.Outgoing.WriteAsync( new SET_ADDRESS_Request(boardSerialNumber, setBoardPhysicalAddress), (_, testResponse) => testResponse is SET_ADDRESS_Response r && r.Address == 0 && r.DeviceSerialNumber == boardSerialNumber && r.NewDeviceAddress == setBoardPhysicalAddress, 1000) as SET_ADDRESS_Response; if (setAddressResponse == null) { logger.LogError($" Board with SerialNumber: { boardSerialNumber} timedout to set to PhyAddr: { setBoardPhysicalAddress}"); return false; } var b = this.Boards?.FirstOrDefault(b => b.BoardPhysicalAddress == setAddressResponse.NewDeviceAddress); if (b == null) { logger.LogInformation($" B PhyAddr: {setAddressResponse.NewDeviceAddress} does not defined via local configs"); return false; } //logger.LogInformation($" Board with SerialNumber: {boardSerialNumber}, " + // $"Will read board side Params for its total {this.DeviceConfig.BoardConfigs.FirstOrDefault(bc => bc.SerialNumber == boardSerialNumber)?.NozzleConfigs?.Count() ?? 0} nozzles"); foreach (var nozzleNo in this.DeviceConfig.BoardConfigs.FirstOrDefault(bc => bc.SerialNumber == boardSerialNumber)?.NozzleConfigs?.Select(nc => (byte)nc.OnBoardNozzleNumber)) { //var readParaResponse = await context.Outgoing.WriteAsync( // new READ_PARA_Request(nozzleNo) { Address = setBoardPhysicalAddress }, // (request, response) => response is READ_PARA_Response r && r.Address == setBoardPhysicalAddress, 5000) as READ_PARA_Response; //if (readParaResponse == null) //{ // logger.LogInformation(" B PhyAddr: " + setBoardPhysicalAddress + ", board side Parameter read for nozzle " + // nozzleNo + // " timedout, will skip this nozzle and continue..."); // continue; //} //logger.LogInformation(" B PhyAddr: " + setBoardPhysicalAddress + ", board side Parameter was read for nozzle " + readParaResponse.NozzleNumber // + ", detail: " + readParaResponse.ToLogString()); var nozzleInitParameter = this.DeviceConfig.BoardConfigs.FirstOrDefault(bc => bc.SerialNumber == boardSerialNumber) ?.NozzleConfigs?.FirstOrDefault(nc => nc.OnBoardNozzleNumber == nozzleNo)?.ManualInitParameter; if (nozzleInitParameter == null) { logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, Will try use ExternalInitParameter to Write Para to board for nozzle: {nozzleNo}"); nozzleInitParameter = this.DeviceConfig.BoardConfigs.FirstOrDefault(bc => bc.SerialNumber == boardSerialNumber) ?.NozzleConfigs?.FirstOrDefault(nc => nc.OnBoardNozzleNumber == nozzleNo)?.ExternalInitParameter; } else logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, Will use ManualInitParameter to Write para to board for nozzle: {nozzleNo}"); if (nozzleInitParameter != null) { //logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, Will set new PARA to board for nozzle: {nozzleNo}"); var writeParamsResponse = await context.Outgoing.WriteAsync( new WRITE_PARA_Request(nozzleNo) { Address = setBoardPhysicalAddress, 最大未变化次数 = nozzleInitParameter.最大未变化次数, 开始加油阀值 = nozzleInitParameter.开始加油阀值, 停止加油阀值 = nozzleInitParameter.停止加油阀值, 最小加油量 = nozzleInitParameter.最小加油量, 加油脉冲当量 = nozzleInitParameter.加油脉冲当量, 油气脉冲当量 = nozzleInitParameter.油气脉冲当量, 气液比值 = nozzleInitParameter.气液比值 }, (_, test) => test is WRITE_PARA_Response r && r.Address == setBoardPhysicalAddress, 2000); if (writeParamsResponse != null) logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, new Parameter was set successfully to nozzle {nozzleNo}"); else logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, new Parameter was failed set to nozzle {nozzleNo}"); } else logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, Skip set new PARA to board for nozzle: {nozzleNo} as Manual and External InitParmater are both null"); } b.LastIncomingMessageReceivedTime = DateTime.Now; logger.LogInformation($" Board with SerialNumber: { boardSerialNumber} has successfully set to PhyAddr: { setBoardPhysicalAddress}"); var syncTimeResult = await this.ReadAndWriteTimeIfMaxTimeGapReached(setBoardPhysicalAddress); if (syncTimeResult) { logger.LogInformation("B PhyAddr: " + setBoardPhysicalAddress + ", Sync time succeed, and init succeed!"); return true; } else { logger.LogInformation("B PhyAddr: " + setBoardPhysicalAddress + ", Sync time failed, init failed"); return false; } } [UniversalApi(Description = "获取VR board油枪的初始化参数值")] public async virtual Task ReadBoardNozzleInitParameters(byte boardPhysicalAddress, byte onBoardNozzleNumber) { logger.LogInformation($"B PhyAddr: {boardPhysicalAddress }, Will ReadBoardNozzleInitParameters for nozzle { onBoardNozzleNumber}"); var targetBoard = this.Boards.FirstOrDefault(b => b.BoardPhysicalAddress == boardPhysicalAddress && b.Nozzles.Any(n => n.NozzleNumberOnBoard == onBoardNozzleNumber)); if (targetBoard == null) { logger.LogInformation($" B PhyAddr: {boardPhysicalAddress }, parameters were read failed for nozzle {onBoardNozzleNumber} due to target board or nozzel does not exists"); return null; } var response = await this.context.Outgoing.WriteAsync( new READ_PARA_Request(onBoardNozzleNumber) { Address = boardPhysicalAddress }, (_, test) => test is READ_PARA_Response r && r.Address == boardPhysicalAddress, 2000); if (response is READ_PARA_Response readParameterResponse && readParameterResponse != null) { logger.LogDebug($" B PhyAddr: {boardPhysicalAddress }, parameters were read successfully for nozzle {onBoardNozzleNumber}: {readParameterResponse.ToLogString()}"); return readParameterResponse; } else { logger.LogInformation($" B PhyAddr: {boardPhysicalAddress }, parameters were read failed for nozzle {onBoardNozzleNumber}"); return null; } } [UniversalApi(Description = "写入VR board油枪的初始化参数值")] public async virtual Task WriteBoardNozzleInitParameters(byte boardPhysicalAddress, byte onBoardNozzleNumber, BoardInitParameterConfigV1 parametersConfig) { logger.LogInformation($"B PhyAddr: {boardPhysicalAddress }, Will WriteBoardNozzleInitParameters for nozzle { onBoardNozzleNumber}: {parametersConfig.ToLogString()}"); var targetBoard = this.Boards.FirstOrDefault(b => b.BoardPhysicalAddress == boardPhysicalAddress && b.Nozzles.Any(n => n.NozzleNumberOnBoard == onBoardNozzleNumber)); if (targetBoard == null) { logger.LogInformation($" B PhyAddr: {boardPhysicalAddress }, write Parameters failed for nozzle {onBoardNozzleNumber} due to target board or nozzel does not exists"); return false; } var response = await context.Outgoing.WriteAsync( new WRITE_PARA_Request(onBoardNozzleNumber) { Address = boardPhysicalAddress, 最大未变化次数 = parametersConfig.最大未变化次数, 开始加油阀值 = parametersConfig.开始加油阀值, 停止加油阀值 = parametersConfig.停止加油阀值, 最小加油量 = parametersConfig.最小加油量, 加油脉冲当量 = parametersConfig.加油脉冲当量, 油气脉冲当量 = parametersConfig.油气脉冲当量, 气液比值 = parametersConfig.气液比值 }, (_, test) => test is WRITE_PARA_Response r && r.Address == boardPhysicalAddress, 2000); if (response is WRITE_PARA_Response writeParameterResponse && writeParameterResponse != null) { logger.LogInformation($" B PhyAddr: {boardPhysicalAddress }, write Parameters successfully for nozzle {onBoardNozzleNumber}"); return true; } else { logger.LogInformation($" B PhyAddr: {boardPhysicalAddress }, write Parameters failed for nozzle {onBoardNozzleNumber}"); return false; } } private async Task ReadAndWriteTimeIfMaxTimeGapReached(byte boardPhysicalAddress) { logger.LogInformation(">>>>>B PhyAddr: " + boardPhysicalAddress + ", trying sync time with FCC=========="); var readTimeResponse = await context.Outgoing.WriteAsync( new READ_TIME_Request() { Address = boardPhysicalAddress }, (request, response) => response is READ_TIME_Response r && r.Address == boardPhysicalAddress, 5000) as READ_TIME_Response; if (readTimeResponse == null) { logger.LogInformation("B PhyAddr: " + boardPhysicalAddress + ", Time was read failed from DataCollector, init failed."); return false; } //this.lastDeviceMessageReceivedTime = DateTime.Now; logger.LogDebug("B PhyAddr: " + boardPhysicalAddress + ", Time was read from DataCollector board which is: " + readTimeResponse.CurrentTime.ToString("yyyy-MM-dd HH:mm:ss")); if (Math.Abs(readTimeResponse.CurrentTime.Subtract(DateTime.Now).TotalSeconds) >= maxTimeGapFromFcAndDeviceBySeconds) { logger.LogDebug("B PhyAddr: " + boardPhysicalAddress + ", there's " + maxTimeGapFromFcAndDeviceBySeconds + " seconds time gap detected between DataCollector and local FC, will start time sync..."); var writeTimeResponse = await context.Outgoing.WriteAsync( new WRITE_TIME_Request(DateTime.Now) { Address = boardPhysicalAddress }, (request0, response0) => response0 is WRITE_TIME_Response r && r.Address == boardPhysicalAddress, 5000); if (writeTimeResponse != null) { this.Boards.First(b => b.BoardPhysicalAddress == writeTimeResponse.Address).LastIncomingMessageReceivedTime = DateTime.Now; //this.lastDeviceMessageReceivedTime = DateTime.Now; logger.LogDebug("B PhyAddr: " + boardPhysicalAddress + ", Sync time succeed!"); return true; } else { logger.LogInformation("B PhyAddr: " + boardPhysicalAddress + ", Sync time failed!"); return false; } } else { logger.LogDebug("B PhyAddr: " + boardPhysicalAddress + ", No need sync time."); } return true; } private int onReadAndClearHistoryDataFromBoardGuard = 0; public async override Task Process(IContext context) { if (!this.DeviceConfig.BoardConfigs.Any(bc => bc.SetPhysicalAddress == context.Incoming.Message.Address)) throw new ArgumentException($"Incoming message from board with PhyAddr: {context.Incoming.Message.Address} does not has definition in this device handler config, may the FC side config wrong here?"); this.Boards.First(b => b.BoardPhysicalAddress == context.Incoming.Message.Address).LastIncomingMessageReceivedTime = DateTime.Now; switch (context.Incoming.Message) { case READ_WORKING_AND_TYPE_Response realTimeStateAndDataResponse: if (realTimeStateAndDataResponse.NozzleStates?.Any() ?? false) { foreach (var newNozzleState in realTimeStateAndDataResponse.NozzleStates) { var nozzle = this.Boards.FirstOrDefault(b => b.BoardPhysicalAddress == realTimeStateAndDataResponse.Address) ?.Nozzles.FirstOrDefault(n => n.NozzleNumberOnBoard == newNozzleState.Key); if (nozzle == null) { logger.LogError($"Incoming a device msg from Board(on comm: {context.Communicator.Identity ?? ""}, with PhysicalAddress: {realTimeStateAndDataResponse.Address}) for board nozzle(with index:{newNozzleState.Key}) say its state changed to: {newNozzleState.Value}, but no correlated nozzle in FCC, may the config missing in FCC??"); } else { if (nozzle.NozzleState != newNozzleState.Value) { logger.LogDebug($"B PhyAddr: { context.Incoming.Message.Address}, Nozzle: {nozzle.NozzleNumberOnBoard} is swtching state from: {nozzle.NozzleState} to: {newNozzleState.Value}"); nozzle.NozzleState = newNozzleState.Value; this.FireOnBoardNozzleStateChangeEvent(nozzle); } } } this.FireOnDataReceivedEvent(realTimeStateAndDataResponse); } this.CurrentDataCollectorType = realTimeStateAndDataResponse.DataCollectorType; var withHistoryDataNozzles = realTimeStateAndDataResponse.NozzleHistoryDataStates.Where(h => h.Value == NozzleHistoryDataStateEnum.HasData); foreach (var n in withHistoryDataNozzles) { //context.Outgoing.Write(new READ_HISTORY_NUM_Request(n.Key) { Address = realTimeStateAndDataResponse.Address }); if (0 == Interlocked.CompareExchange(ref this.onReadAndClearHistoryDataFromBoardGuard, 1, 0)) { var nozzleNumber = n.Key; var targetBoard = this.Boards.FirstOrDefault(b => b.BoardPhysicalAddress == context.Incoming.Message.Address); try { if (logger.IsEnabled(LogLevel.Debug)) logger.LogDebug($"B PhyAddr: { targetBoard.BoardPhysicalAddress}, Nozzle: {nozzleNumber} has data, will try ReadAndClear..."); var result = await ReadAndClearHistoryDataFromBoard(context, targetBoard, nozzleNumber); if (logger.IsEnabled(LogLevel.Debug)) logger.LogDebug($"B PhyAddr: { targetBoard.BoardPhysicalAddress}, Nozzle: {nozzleNumber} ReadAndClearHistoryData overall result is: {result}"); } finally { logger.LogDebug($"B PhyAddr: { targetBoard?.BoardPhysicalAddress ?? -1}, Nozzle: {nozzleNumber} released guard"); this.onReadAndClearHistoryDataFromBoardGuard = 0; } } else { if (logger.IsEnabled(LogLevel.Debug)) logger.LogDebug($"B PhyAddr: { context.Incoming.Message.Address}, Nozzle: {n.Key} skip this round of ReadAndClear as prev still running, will try later..."); break; } } //this.OnDataRecieved?.Invoke(this, new DataRecievedEventArgs(realTimeStateAndDataResponse)); break; //case READ_HISTORY_NUM_Response historyNumResponse: // context.Outgoing.Write( // new READ_ONE_HISTORY_DATA_Request(historyNumResponse.NozzleHistoryUnreadRecordCount.Key) { Address = historyNumResponse.Address }); // break; //case READ_ONE_HISTORY_DATA_Response historyDataResponse: // logger.LogDebug("B PhyAddr: " + historyDataResponse.Address + ", Read a history data from nozzle " + historyDataResponse.NozzleNumber // + ", LiquidVolume " + historyDataResponse.LiquidVolume // + ", AirVolume " + historyDataResponse.AirVolume // + " which trx started from " + historyDataResponse.FuellingStartTime.ToString("yyyy-MM-dd HH:mm:ss") // + " to " + historyDataResponse.FuellingEndTime.ToString("yyyy-MM-dd HH:mm:ss")); // var clearResult = await ClearHistory(context, historyDataResponse); // if (clearResult) this.FireOnDataReceivedEvent(historyDataResponse); // break; } } private async Task ReadAndClearHistoryDataFromBoard(IContext context, Board board, byte onBoardNozzleNumber) { var readHistoryNumResponse = await context.Outgoing.WriteAsync( new READ_HISTORY_NUM_Request(onBoardNozzleNumber) { Address = board.BoardPhysicalAddress }, (r, testResponse) => testResponse is READ_HISTORY_NUM_Response rp && rp.Address == board.BoardPhysicalAddress && rp.NozzleHistoryUnreadRecordCount.Key == onBoardNozzleNumber, 1500) as READ_HISTORY_NUM_Response; if (readHistoryNumResponse == null) { if (logger.IsEnabled(LogLevel.Debug)) logger.LogDebug($"B PhyAddr: {board.BoardPhysicalAddress}, Nozzle: {onBoardNozzleNumber} timedout for READ_HISTORY_NUM_Request"); return false; } if (readHistoryNumResponse.NozzleHistoryUnreadRecordCount.Value < 1) { if (logger.IsEnabled(LogLevel.Debug)) logger.LogDebug($"B PhyAddr: {board.BoardPhysicalAddress}, Nozzle: {onBoardNozzleNumber} indicates no un-read data?!"); return false; } int retryingTimes = 0; retryReadData: var operationResponse = await context.Outgoing.WriteAsync( new READ_ONE_HISTORY_DATA_Request(onBoardNozzleNumber) { Address = board.BoardPhysicalAddress }, (r, testResponse) => (testResponse is READ_ONE_HISTORY_DATA_Response rp && rp.Address == board.BoardPhysicalAddress && rp.NozzleNumber == onBoardNozzleNumber) || (testResponse is COLLECTOR_BUSY_Response bRp && bRp.Address == board.BoardPhysicalAddress), 1500); if (operationResponse == null) { if (logger.IsEnabled(LogLevel.Debug)) logger.LogDebug($"B PhyAddr: {board.BoardPhysicalAddress}, Nozzle: {onBoardNozzleNumber} timedout for READ_ONE_HISTORY_DATA_Request"); return false; } else if (operationResponse is COLLECTOR_BUSY_Response) { //0 indicates disabled for retry. int maxRetryTimes = 0; retryingTimes++; if (retryingTimes > maxRetryTimes) { logger.LogInformation($"B PhyAddr: {board.BoardPhysicalAddress}, Nozzle: {onBoardNozzleNumber} report busy and deny for READ_ONE_HISTORY_DATA_Request"); return false; } if (logger.IsEnabled(LogLevel.Debug)) logger.LogDebug($"B PhyAddr: {board.BoardPhysicalAddress}, Nozzle: {onBoardNozzleNumber} Retrying for READ_ONE_HISTORY_DATA_Request..."); goto retryReadData; } var historyDataResponse = operationResponse as READ_ONE_HISTORY_DATA_Response; logger.LogDebug($"B PhyAddr: { historyDataResponse.Address }, Nozzle {historyDataResponse.NozzleNumber} Read a history data" + $", LiquidVolume: {historyDataResponse.LiquidVolume}, AirVolume: { historyDataResponse.AirVolume}" + $", trx started from { historyDataResponse.FuellingStartTime.ToString("yyyy-MM-dd HH:mm:ss")}" + $" to { historyDataResponse.FuellingEndTime.ToString("yyyy-MM-dd HH:mm:ss")}, will clear..."); var clearHistoryResponse = await context.Outgoing.WriteAsync( new CLEAR_ONE_HISTORY_Request(historyDataResponse.NozzleNumber) { Address = historyDataResponse.Address }, (request, response) => response is CLEAR_ONE_HISTORY_Response r && r.Address == historyDataResponse.Address, 1500) as CLEAR_ONE_HISTORY_Response; if (clearHistoryResponse != null) { if (logger.IsEnabled(LogLevel.Debug)) logger.LogDebug($"B PhyAddr: {historyDataResponse.Address}, Nozzle: {historyDataResponse.NozzleNumber} One history has been cleared"); this.FireOnDataReceivedEvent(historyDataResponse); return true; } else { logger.LogInformation($"B PhyAddr: { historyDataResponse.Address }, Nozzle: {historyDataResponse.NozzleNumber} Previous clear One history request failed"); return false; } } [UniversalApi(Description = "Get state for all boards under this comm channel")] public Task> GetBoardStates() { return Task.FromResult(this.Boards); } [UniversalApi(Description = "Send a query right now to all boards under this comm channel, and return the datas, if read timedout for certain board, the specific data will not be returned.")] public async Task> ReadRealTimeDatas() { List results = new List(); var addresses = this.DeviceConfig.BoardConfigs.Select(bc => bc.SetPhysicalAddress).ToList(); foreach (var adrs in addresses) { var rsp = await this.context.Outgoing.WriteAsync( new READ_WORKING_TYPE_Request() { Address = adrs }, (request, testingResponse) => testingResponse is READ_WORKING_AND_TYPE_Response p && p.Address == adrs, 4000) as READ_WORKING_AND_TYPE_Response; if (rsp != null) results.Add(rsp); } return results; } public IEnumerator GetEnumerator() { return this.Boards.GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return this.Boards.GetEnumerator(); } public virtual void Dispose() { this.singleBoardOfflineCheckTimer?.Dispose(); foreach (var d in this.OnBoardStateChange?.GetInvocationList() ?? new Delegate[] { }) { this.OnBoardStateChange -= d as EventHandler; } foreach (var d in this.OnBoardNozzleStateChange?.GetInvocationList() ?? new Delegate[] { }) { this.OnBoardNozzleStateChange -= d as EventHandler; } foreach (var d in this.OnDataRecieved?.GetInvocationList() ?? new Delegate[] { }) { this.OnDataRecieved -= d as EventHandler; } } } public enum BoardStateEnum { /// /// first time eastalized the connection with FC, or disconnected from a established connection. /// UnInit, /// /// processing and not ready for other command. /// Initializing, Initialized, } public class BoardNozzle { public byte NozzleNumberOnBoard { get; set; } /// /// each vr watching nozzle is a physical pump nozzle, here is the bind info, will be used to draw UI. /// public int SiteLevelNozzleId { get; set; } public NozzleStateEnum NozzleState { get; set; } } public class Board { public int SerialNumber { get; } public byte BoardPhysicalAddress { get; } public IEnumerable Nozzles { get; } public BoardStateEnum State { get; set; } /// /// used for track the last received message which for caculate the offline state of this device. /// public DateTime LastIncomingMessageReceivedTime { get; set; } internal Board(int serialNumber, byte boardPhysicalAddress, BoardStateEnum state, IEnumerable nozzles) { this.SerialNumber = serialNumber; this.BoardPhysicalAddress = boardPhysicalAddress; this.State = state; this.Nozzles = nozzles; } } }