using Edge.Core.Database.Models; using Edge.Core.Processor; using Edge.Core.IndustryStandardInterface.Pump; using Edge.Core.Parser.BinaryParser.Util; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; using Wayne.FDCPOSLibrary; using Wayne_Pump_Dart.MessageEntity; using Wayne_Pump_Dart.MessageEntity.Incoming; using Wayne_Pump_Dart.MessageEntity.Outgoing; namespace Wayne_Pump_Dart { public class PumpHandler : IFdcPumpController, IDisposable//, IHandler { //static ILog logger = log4net.LogManager.GetLogger("PumpHandler"); static NLog.Logger logger = NLog.LogManager.LoadConfiguration("nlog.config").GetLogger("PumpHandler"); private LogicalDeviceState lastLogicalDeviceState = LogicalDeviceState.FDC_OFFLINE; private DateTime lastLogicalDeviceStateReceivedTime; // by seconds, change this value need change the correlated deviceOfflineCountdownTimer's interval as well public const int lastLogicalDeviceStateExpiredTime = 9; private List nozzles = new List(); private System.Timers.Timer deviceOfflineCountdownTimer; /// /// will set to true once nozzle change to OUT while internal fdc state is FDC_FUELING, indicates a fule is done. /// 'true' will block pump auth request from outer /// private bool lastFillRetrievedSinceNozzleDownAtFdcFueling = true; /// /// the pump calling, fuelling message does not contain nozzle info, need track the operating nozzle /// seperatly in other message which will be kept in this variable. /// default set to 0 which is an invalid nozzle id for wayne dart pump(valid should starts from 1). /// private byte operatingNozzlePhysicalId = 0; private IContext context; private bool isOnFdcServerInitCalled = false; private int pumpId; private PumpGroupHandler parent; /// /// address used in wayne dart protocol to comm with physical pump. /// 0x4F + physical pump mother board side config address(range from 1-32) /// private byte dartPumpCommAddress; /// /// when first time connected with physical pump , in some case, the pump will not report any status actively, /// so need send a status query from FC. /// From then on, pump will actively notify FC when state changes, no need to send query anymore from FC. /// private bool initialPumpStatueEverRetrieved = false; private int amountDecimalDigits; private int volumeDecimalDigits; private int priceDecimalDigits; private int volumeTotalizerDecimalDigits; /// /// for avoid a case that FC may miss a pump status change event(pump side issue? or wire issue?), /// we timely actively request the pump state. /// //private System.Timers.Timer pollingPumpStatus; //private int pollingPumpStatusInterval = 30000; #region MyRegion public string Name => "Wayne_Pump_Dart"; public int PumpId => this.pumpId; /// /// Gets the pump physical id. /// address used in wayne dart protocol to comm with physical pump. /// 0x4F + physical pump mother board side config address(range from 1-32) /// public int PumpPhysicalId => this.dartPumpCommAddress; public IEnumerable Nozzles => this.nozzles; public int AmountDecimalDigits => this.amountDecimalDigits; public int VolumeDecimalDigits => this.volumeDecimalDigits; public int PriceDecimalDigits => this.priceDecimalDigits; public int VolumeTotalizerDecimalDigits => this.volumeDecimalDigits; public Guid Id => Guid.NewGuid(); public event EventHandler OnStateChange; public event EventHandler OnCurrentFuellingStatusChange; private async Task InternalAuthorizeAsync() { if (!this.lastFillRetrievedSinceNozzleDownAtFdcFueling) { logger.Info("Pump: " + this.pumpId + ", " + "Start Authorize pump is denied by internal"); return false; } var authorizeSucceed = false; var authroizedResponse = await this.context.Outgoing.WriteAsync( new AuthorizeRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId)), (_, testResponse) => testResponse.Adrs == this.dartPumpCommAddress && testResponse.BlockSeqNumber == _.BlockSeqNumber, 3500); if (authroizedResponse != null) { if (authroizedResponse.ControlCharacter == ControlCharacter.ACK) { logger.Info("Pump: " + this.pumpId + ", " + "AuthorizeRequest ACKed"); authorizeSucceed = true; } else { authorizeSucceed = false; logger.Info("Pump: " + this.pumpId + ", " + "AuthorizeRequest NAKed"); } } else { logger.Info("Pump: " + this.pumpId + ", " + "failed in get AuthorizeRequest ACK(timed out)"); } return authorizeSucceed; } public async Task AuthorizeAsync(byte logicalNozzleId) { if (!this.lastFillRetrievedSinceNozzleDownAtFdcFueling) { logger.Info("Pump: " + this.pumpId + ", " + "Start Authorize pump with logicalNozzle: " + logicalNozzleId + " is denied by internal"); return false; } logger.Info("Pump: " + this.pumpId + ", " + "Start Authorize pump with logicalNozzle: " + logicalNozzleId + ", first send AllowedNozzleNumbersRequest(allow all)"); // now always allow all nozzles this.context.Outgoing.Write( new AllowedNozzleNumbersRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId), this.nozzles.Select(n => n.PhysicalId).ToArray())); return await this.InternalAuthorizeAsync(); } public async Task AuthorizeWithAmountAsync(int moneyAmountWithoutDecimalPoint, byte logicalNozzleId) { if (!this.lastFillRetrievedSinceNozzleDownAtFdcFueling) { logger.Info("Pump: " + this.pumpId + ", " + "Start AuthorizeWithAmount pump with logicalNozzle: " + logicalNozzleId + " is denied by internal"); return false; } logger.Info("Pump: " + this.pumpId + ", " + "start AuthorizeWithAmount pump with logicalNozzle: " + logicalNozzleId + ", moneyAmount: " + moneyAmountWithoutDecimalPoint + ", first send AllowedNozzleNumbersRequest(allow all)"); // now always allow all nozzles this.context.Outgoing.Write( new AllowedNozzleNumbersRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId), this.nozzles.Select(n => n.PhysicalId).ToArray())); var authorizeSucceed = false; var response = await this.context.Outgoing.WriteAsync( new PresetAmountRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId), moneyAmountWithoutDecimalPoint), (request, testResponse) => testResponse.Adrs == this.dartPumpCommAddress && testResponse.BlockSeqNumber == request.BlockSeqNumber, 3500); if (response != null) { if (response.ControlCharacter == ControlCharacter.ACK) { logger.Info("Pump: " + this.pumpId + ", " + "PresetAmountRequest ACKed, will send InternalAuthorize()"); //InternalAuthorize is a blocking call, should not stop the I/O thread for comm with pump device. //ThreadPool.QueueUserWorkItem(o => //new Thread(() => //{ // if (this.InternalAuthorize()) // authorizeSucceed = true; // else // authorizeSucceed = false; //}).Start(); var _ = await this.InternalAuthorizeAsync(); if (_) authorizeSucceed = true; else authorizeSucceed = false; } else { logger.Info("Pump: " + this.pumpId + ", " + "PresetAmountRequest NAKed"); authorizeSucceed = false; } } else { logger.Info("Pump: " + this.pumpId + ", " + "PresetAmountRequest waiting ACK timed out"); authorizeSucceed = false; } return authorizeSucceed; } public async Task AuthorizeWithVolumeAsync(int volumnWithoutDecimalPoint, byte logicalNozzleId) { if (!this.lastFillRetrievedSinceNozzleDownAtFdcFueling) { logger.Info("Pump: " + this.pumpId + ", " + "Start AuthorizeWithVolumn pump with logicalNozzle: " + logicalNozzleId + " is denied by internal"); return false; } logger.Info("Pump: " + this.pumpId + ", " + "start AuthorizeWithVolumn pump with logicalNozzle: " + logicalNozzleId + ", moneyAmount: " + volumnWithoutDecimalPoint + ", first send AllowedNozzleNumbersRequest(allow all)"); // now always allow all nozzles this.context.Outgoing.Write( new AllowedNozzleNumbersRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId), this.nozzles.Select(n => n.PhysicalId).ToArray())); var authorizeSucceed = false; var response = await this.context.Outgoing.WriteAsync( new PresetVolumeRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId), volumnWithoutDecimalPoint), (request, testResponse) => testResponse.Adrs == this.dartPumpCommAddress && testResponse.BlockSeqNumber == request.BlockSeqNumber, 3500); if (response != null) { if (response.ControlCharacter == ControlCharacter.ACK) { logger.Info("Pump: " + this.pumpId + ", " + "PresetVolumeRequest ACKed, will send InternalAuthorize()"); //InternalAuthorize is a blocking call, should not stop the I/O thread for comm with pump device. //ThreadPool.QueueUserWorkItem(o => //{ // if (this.InternalAuthorize()) // authorizeSucceed = true; // else // authorizeSucceed = false; // blocker.Set(); //}); //new Thread(() => //{ // if (this.InternalAuthorize()) // authorizeSucceed = true; // else // authorizeSucceed = false; // blocker.Set(); //}).Start(); var _ = await this.InternalAuthorizeAsync(); if (_) authorizeSucceed = true; else authorizeSucceed = false; } else { logger.Info("Pump: " + this.pumpId + ", " + "PresetVolumeRequest NAKed"); authorizeSucceed = false; } } else { logger.Info("Pump: " + this.pumpId + ", " + "PresetVolumeRequest waiting ACK timed out"); authorizeSucceed = false; } return authorizeSucceed; } public Task ChangeFuelPriceAsync(int newPriceWithoutDecimalPoint, byte logicalNozzleId) { try { var nozzlesPriceListAsendingByNozzlePhysicalId = this.nozzles.OrderBy(k => k.PhysicalId).Select(s => s.ExpectingPriceOnFcSide ?? 0); logger.Info("Pump: " + this.pumpId + ", " + "Change Fuel Price for LogicalNozzle: " + logicalNozzleId + ", physicalNozzle: " + this.nozzles.First(n => n.LogicalId == logicalNozzleId).PhysicalId + " with new price(without decimal): " + newPriceWithoutDecimalPoint + "(old price list for all nozzles based on physicalId from 1 to n: " + nozzlesPriceListAsendingByNozzlePhysicalId.Select(s => s.ToString()).Aggregate((n, acc) => n + ", " + acc) + ")"); this.nozzles.First(n => n.LogicalId == logicalNozzleId).ExpectingPriceOnFcSide = newPriceWithoutDecimalPoint; return this.InternalChangeFuelPriceAsync(nozzlesPriceListAsendingByNozzlePhysicalId.ToList()); } catch (Exception exxx) { logger.Error("Pump: " + this.pumpId + ", " + "Exceptioned in ChangeFuelPrice: " + exxx); return Task.FromResult(false); } } /// /// Wayne dart price change is always targeting all nozzles. /// /// price without decimal points from nozzle 1 to N /// private async Task InternalChangeFuelPriceAsync(List newPricesFromPhysicalNozzleFirstToLast) { if (this.lastLogicalDeviceState == LogicalDeviceState.FDC_CLOSED || this.lastLogicalDeviceState == LogicalDeviceState.FDC_OFFLINE) { logger.Info("Pump: " + this.pumpId + ", " + " Pump is in state FDC_CLOSED or FDC_OFFLINE, InternalChangeFuelPrice will return false"); return false; } if (newPricesFromPhysicalNozzleFirstToLast.Count != this.nozzles.Count) throw new ArgumentException("Wayne dart pump price change must provide prices for total " + this.nozzles.Count + " nozzles, but now only pass in " + newPricesFromPhysicalNozzleFirstToLast.Count); bool changePriceSucceed = false; logger.Info("Pump: " + this.pumpId + ", " + " InternalChangeFuelPrice starting with prices: " + newPricesFromPhysicalNozzleFirstToLast.Select(p => p.ToString()).Aggregate((acc, n) => acc + ", " + n)); var priceChangedResponse = await this.context.Outgoing.WriteAsync( new PriceUpdateRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId), newPricesFromPhysicalNozzleFirstToLast) , (_, testResponse) => testResponse.Adrs == this.dartPumpCommAddress && testResponse.BlockSeqNumber == _.BlockSeqNumber, 2000); if (priceChangedResponse != null && priceChangedResponse.ControlCharacter == ControlCharacter.ACK) { changePriceSucceed = true; try { for (int i = 0; i < newPricesFromPhysicalNozzleFirstToLast.Count; i++) { this.nozzles.First(n => n.PhysicalId == i + 1).RealPriceOnPhysicalPump = newPricesFromPhysicalNozzleFirstToLast[i]; //this.nozzles.First(n => n.PhysicalId == i + 1).ExpectingPriceOnFcSide = newPricesFromPhysicalNozzleFirstToLast[i]; } logger.Info("Pump: " + this.pumpId + ", " + " InternalChangeFuelPrice done succeed"); } catch (Exception exxx) { logger.Info("InternalChangeFuelPrice partially succeed with exception:" + exxx); } } else if (priceChangedResponse != null && priceChangedResponse.ControlCharacter == ControlCharacter.NAK) { logger.Error("Pump: " + this.pumpId + ", " + "InternalChangeFuelPrice is denied (NAK) by wayne dart pump, will reset msg token to 0 for align."); //reset id to 0 to re-align, wayne dart have this check! this.parent.ResetMessageTokenToAlign(this.pumpId); } else { logger.Error("Pump: " + this.pumpId + ", " + "InternalChangeFuelPrice failed with timeout"); } // this FC handle WayneDart pump price change one nozzle by nozzle, so change prices on single FuelPoint with multiple // nozzle will interpreted as multiple price change request, by testing, too fast send multiple price change request // to a FP might be ignored by pump side though message here are all good, so hardcode sleep a while. Thread.Sleep(1000); return changePriceSucceed; } public async Task FuelingRoundUpByAmountAsync(int amount) { logger.Info("Pump: " + this.pumpId + ", " + "start FuelingRoundUpByAmount with amount: " + amount); var isSucceed = false; this.context.Outgoing.WriteAsync( new PresetAmountRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId), amount), (request, testResponse) => testResponse.Adrs == this.dartPumpCommAddress && testResponse.BlockSeqNumber == request.BlockSeqNumber, (request, response) => { if (response != null) { if (response.ControlCharacter == ControlCharacter.ACK) { logger.Debug("Pump: " + this.pumpId + ", " + "PresetAmountRequest ACKed"); isSucceed = true; } else { logger.Info("Pump: " + this.pumpId + ", " + "PresetAmountRequest NAKed"); isSucceed = false; } } else { logger.Info("Pump: " + this.pumpId + ", " + "PresetAmountRequest waiting ACK timed out"); isSucceed = false; } }, 1500); return isSucceed; } public async Task FuelingRoundUpByVolumeAsync(int volume) { return false; } public async Task QueryStatusAsync() { return this.lastLogicalDeviceState; } public async Task> QueryTotalizerAsync(byte logicalNozzleId) { var result = new System.Tuple(-1, -1); logger.Info("Pump: " + this.pumpId + ", " + "start QueryTotalizer pump with nozzle: " + logicalNozzleId); if (this.lastLogicalDeviceState == LogicalDeviceState.FDC_CLOSED || this.lastLogicalDeviceState == LogicalDeviceState.FDC_OFFLINE) { logger.Info("Pump: " + this.pumpId + ", " + " Pump is in state FDC_CLOSED or FDC_OFFLINE, totalizer will return -1, -1"); return new System.Tuple(-1, -1); } var nozzlePhysicalId = this.nozzles.FirstOrDefault(n => n.LogicalId == logicalNozzleId)?.PhysicalId; if (nozzlePhysicalId == null) { logger.Info("Pump: " + this.pumpId + ", " + " Nozzle with logicalId: " + logicalNozzleId + " does not exists, totalizer will return -1, -1"); return new System.Tuple(-1, -1); } var response = await this.context.Outgoing.WriteAsync(new RequestTotalVolumeCountersRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId), nozzlePhysicalId.Value), (request, testResponse) => testResponse.Adrs == this.dartPumpCommAddress && testResponse.ControlCharacter == ControlCharacter.DATA, 3000); if (response != null) { this.context.Outgoing.Write(new ACK(this.dartPumpCommAddress, response.BlockSeqNumber)); var totalCountersTrx = new TotalCounters_TransactionData(response.TransactionDatas.First(f => f.TransactionNumber == 0x65)); logger.Info("Pump: " + this.pumpId + ", " + "QueryTotalizer for nozzle: " + logicalNozzleId + " succeed, volume total: " + totalCountersTrx.TotalValue); result = new System.Tuple(-1, totalCountersTrx.TotalValue); } else { logger.Error("Pump: " + this.pumpId + ", " + "QueryTotalizer waiting Data timed out"); } return result; } public async Task ResumeFuellingAsync() { throw new NotImplementedException(); } public async Task SuspendFuellingAsync() { throw new NotImplementedException(); } /// /// unauthorize the authed fueling point, will trigger wayne dart pump switched to `FILLING COMPLETE` state. /// /// wayne dart no need specify nozzle id /// public async Task UnAuthorizeAsync(byte logicalNozzleId) { var unauthorizeSucceed = false; logger.Info("Pump: " + this.pumpId + ", " + "Start UnAuthorize pump with nozzle: " + logicalNozzleId); var authorizedResponse = await this.context.Outgoing.WriteAsync( new StopRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId)), (_, testResponse) => testResponse.Adrs == this.dartPumpCommAddress && testResponse.BlockSeqNumber == _.BlockSeqNumber, 3500); if (authorizedResponse != null) { if (authorizedResponse.ControlCharacter == ControlCharacter.ACK) { logger.Info("Pump: " + this.pumpId + ", " + "unAuthorizeRequest ACKed and succeed"); unauthorizeSucceed = true; } else logger.Info("Pump: " + this.pumpId + ", " + "unAuthorizeRequest NAKed (rejected?)"); } else { logger.Info("Pump: " + this.pumpId + ", " + "unAuthorizeRequest timed out"); } return unauthorizeSucceed; } public async Task LockNozzleAsync(byte logicalNozzleId) { return false; } public async Task UnlockNozzleAsync(byte logicalNozzleId) { return false; } #endregion public PumpHandler(PumpGroupHandler parent, int pumpId, int amountDecimalDigits, int volumeDecimalDigits, int priceDecimalDigits, int volumeTotalizerDecimalDigits, string pumpXmlConfiguration) { this.parent = parent; this.pumpId = pumpId; this.amountDecimalDigits = amountDecimalDigits; this.volumeDecimalDigits = volumeDecimalDigits; this.priceDecimalDigits = priceDecimalDigits; this.volumeTotalizerDecimalDigits = volumeTotalizerDecimalDigits; // sample of pumpXmlConfiguration // // // // // // // var xmlDocument = new XmlDocument(); xmlDocument.LoadXml(pumpXmlConfiguration); var physicalPumpAddressConfiguratedInPump = byte.Parse(xmlDocument.SelectSingleNode("/Pump").Attributes["physicalId"].Value); if (physicalPumpAddressConfiguratedInPump > 0x20) throw new ArgumentOutOfRangeException("Wayne dart pump only accept pump address range from 1 to 32, make sure this value is correctly configurated in physical pump mother board"); this.dartPumpCommAddress = (byte)(0x4F + physicalPumpAddressConfiguratedInPump); foreach (var nozzleElement in xmlDocument.GetElementsByTagName("Nozzle").Cast()) { var nozzlePhysicalId = byte.Parse(nozzleElement.Attributes["physicalId"].Value); var nozzleLogicalId = byte.Parse(nozzleElement.Attributes["logicalId"].Value); var nozzleRawDefaultPriceWithoutDecimal = nozzleElement.Attributes["defaultNoDecimalPointPriceIfNoHistoryPriceReadFromDb"].Value; if (nozzlePhysicalId < 1 || nozzlePhysicalId > 8) throw new ArgumentOutOfRangeException("Wayne dart pump only accept nozzle physical id range in config from 1 to 8"); this.nozzles.Add(new LogicalNozzle(pumpId, nozzlePhysicalId, nozzleLogicalId, null) { ExpectingPriceOnFcSide = int.Parse(nozzleRawDefaultPriceWithoutDecimal) }); logger.Info("Pump: " + this.pumpId + ", created a nozzle with logicalId: " + nozzleLogicalId + ", physicalId: " + nozzlePhysicalId + ", default raw price without decimal points: " + nozzleRawDefaultPriceWithoutDecimal); } this.deviceOfflineCountdownTimer = new System.Timers.Timer(3000); this.deviceOfflineCountdownTimer.Elapsed += (_, __) => { if (DateTime.Now.Subtract(this.lastLogicalDeviceStateReceivedTime).TotalSeconds >= lastLogicalDeviceStateExpiredTime) { this.initialPumpStatueEverRetrieved = false; if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_OFFLINE) { this.lastLogicalDeviceState = LogicalDeviceState.FDC_OFFLINE; logger.Info("Pump: " + this.pumpId + ", " + " State switched to FDC_OFFLINE due to long time no see pump data incoming"); var safe0 = this.OnStateChange; safe0?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_OFFLINE, null)); logger.Trace("Pump: " + this.pumpId + ", " + " OnStateChange event fired and back"); } } }; this.deviceOfflineCountdownTimer.Start(); } public void OnFdcServerInit(Dictionary parameters) { /* Wayne Dart pump will miss last price when disconnected or power off * from FC a while(pump state `PUMP NOT PROGRAMMED` indicates this happened), so here * is trying to recover the price from Fdc database, and then push to Pump*/ if (parameters.ContainsKey("LastPriceChange")) { // nozzle logical id:rawPrice var lastPriceChanges = parameters["LastPriceChange"] as Dictionary; foreach (var priceChange in lastPriceChanges) { logger.Info("Pump: " + this.pumpId + ", " + "Pump " + this.pumpId + " OnFdcServerInit, load last price change " + "on logical nozzle: " + priceChange.Key + " with price: " + priceChange.Value); this.nozzles.First(n => n.LogicalId == priceChange.Key).ExpectingPriceOnFcSide = priceChange.Value; } } /* Load Last sale trx(from db) for void the case of FC accidently disconnect from Pump in fueling, and may cause a fueling trx gone from FC control */ if (parameters.ContainsKey("LastFuelSaleTrx")) { // nozzle logical id:LastSale var lastFuelSaleTrxes = parameters["LastFuelSaleTrx"] as Dictionary; foreach (var lastFuelSaleTrx in lastFuelSaleTrxes) { logger.Info("Pump: " + this.pumpId + ", OnFdcServerInit, load last volume Totalizer " + "on logical nozzle: " + lastFuelSaleTrx.Key + " with volume value: " + lastFuelSaleTrx.Value.VolumeTotalizer); this.nozzles.First(n => n.LogicalId == lastFuelSaleTrx.Key).VolumeTotalizer = lastFuelSaleTrx.Value.VolumeTotalizer; } } this.isOnFdcServerInitCalled = true; } public void Init(IContext context) { this.context = context; this.context.Communicator.OnConnected += (a, b) => { //this.context.Outgoing.Write(new ReturnStatusRequest(this.dartPumpCommAddress, 0)); }; //this.context.Communicator.OnDisconnected += (a, b) => this.pumpStatusEverReceived = false; } public async Task Process(IContext context) { if (context.Incoming.Message.ControlCharacter == ControlCharacter.DATA) this.context.Outgoing.Write(new ACK(this.dartPumpCommAddress, context.Incoming.Message.BlockSeqNumber)); if (!isOnFdcServerInitCalled) return; if (context.Incoming.Message.ControlCharacter == ControlCharacter.NAK) { logger.Info("Pump: " + this.pumpId + ", " + " received a NAK, will set msg token to 0 for re-align"); this.parent.ResetMessageTokenToAlign(this.pumpId); } this.lastLogicalDeviceStateReceivedTime = DateTime.Now; if (this.lastLogicalDeviceState == LogicalDeviceState.FDC_OFFLINE) { logger.Info("Pump: " + this.pumpId + ", " + "Recevied an Pump Msg in FDC_OFFLINE state, " + "indicates the underlying connection is established, switch to FDC_READY"); this.lastLogicalDeviceState = LogicalDeviceState.FDC_READY; var safe = this.OnStateChange; safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_READY)); logger.Trace("Pump: " + this.pumpId + ", " + " OnStateChange event fired and back"); } if (!this.initialPumpStatueEverRetrieved) { // mute for next incoming request this.initialPumpStatueEverRetrieved = true; logger.Info("Pump: " + this.pumpId + ", " + "Never received pump status, send ReturnStatusRequest right now"); // capture the EOT response here for avoid infinite loop: FC send ReturnStatusRequest, FC received EOT, and send ReturnStatusRequest again... this.context.Outgoing.WriteAsync( new ReturnStatusRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId)), (request, testResponse) => testResponse.Adrs == this.dartPumpCommAddress && testResponse.BlockSeqNumber == request.BlockSeqNumber, (request, statusResponse) => { if (statusResponse == null) { this.initialPumpStatueEverRetrieved = false; logger.Info("Pump: " + this.pumpId + ", " + " ReturnStatusRequest timed out wait for pump EOT"); } else if (statusResponse.ControlCharacter == ControlCharacter.NAK) { logger.Info("Pump: " + this.pumpId + ", " + " ReturnStatusRequest NAKed, will set msg token to 0 to align and send again"); //reset id to 0 to re-align, iGEN board wayne dart have this check! this.parent.ResetMessageTokenToAlign(this.pumpId); this.initialPumpStatueEverRetrieved = false; } }, 1500); } if (context.Incoming.Message.ControlCharacter == ControlCharacter.DATA) { var nozzleStatusAndFillingPriceTrxData = context.Incoming.Message.TransactionDatas.LastOrDefault(d => d.TransactionNumber == 0x03); var pumpStatusTrxData = context.Incoming.Message.TransactionDatas.FirstOrDefault(d => d.TransactionNumber == 0x01); var filledVolumeAndAmountTrxData = context.Incoming.Message.TransactionDatas.FirstOrDefault(d => d.TransactionNumber == 0x02); NozzleStatusAndFillingPrice_TransactionData nozzleStatusTrx = null; PumpStatus_TransactionData pumpStatusTrx = null; FilledVolumeAndAmount_TransactionData filledVolAndAmtTrx = null; var overallStateLogStr = "Pump: " + this.pumpId + ", " + ">>>>>>>>>>===start====(internalFdcState: " + this.lastLogicalDeviceState + ")" + System.Environment.NewLine; if (nozzleStatusAndFillingPriceTrxData != null) { nozzleStatusTrx = new NozzleStatusAndFillingPrice_TransactionData(nozzleStatusAndFillingPriceTrxData); overallStateLogStr += "Nozzle with physical id: " + nozzleStatusTrx.Status.Key + " is in state: " + nozzleStatusTrx.Status.Value + ", filling price: " + nozzleStatusTrx.FillingPrice + System.Environment.NewLine; } if (pumpStatusTrxData != null) { pumpStatusTrx = new PumpStatus_TransactionData(pumpStatusTrxData); overallStateLogStr += "WayneDart State: " + pumpStatusTrx.Status + System.Environment.NewLine; } if (filledVolumeAndAmountTrxData != null) { filledVolAndAmtTrx = new FilledVolumeAndAmount_TransactionData(filledVolumeAndAmountTrxData); overallStateLogStr += "Filled vol " + filledVolAndAmtTrx.FilledVolume + ", amt " + filledVolAndAmtTrx.FilledAmount + System.Environment.NewLine; } logger.Info(overallStateLogStr + "<<<<<<<<<<===end===="); if (pumpStatusTrx != null && pumpStatusTrx.Status == PumpStatus.PUMP_NOT_PROGRAMMED) { #region PUMP_NOT_PROGRAMMED if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_ERRORSTATE) { logger.Debug("Pump: " + this.pumpId + ", " + " NEW State is PUMP_NOT_PROGRAMMED, indicates price not set yet, State switched to FDC_ERRORSTATE"); this.lastLogicalDeviceState = LogicalDeviceState.FDC_ERRORSTATE; var safe0 = this.OnStateChange; safe0?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_ERRORSTATE)); logger.Trace("Pump: " + this.pumpId + ", " + " OnStateChange event fired and back"); } logger.Debug("Pump: " + this.pumpId + ", " + " Pump status is PUMP_NOT_PROGRAMMED, will set price"); //ThreadPool.QueueUserWorkItem(o => //{ try { var nozzlesPriceListAsendingByNozzlePhysicalId = this.nozzles.OrderBy(k => k.PhysicalId).Select(s => s.ExpectingPriceOnFcSide ?? 0).ToList(); var cpResponse = await this.InternalChangeFuelPriceAsync(nozzlesPriceListAsendingByNozzlePhysicalId); if (cpResponse) logger.Info("Pump: " + this.pumpId + ", " + " Price change(reason PUMP_NOT_PROGRAMMED) succeed"); else { logger.Error("Pump: " + this.pumpId + ", " + " Price change(reason PUMP_NOT_PROGRAMMED) failed, will retry 1st time"); cpResponse = await this.InternalChangeFuelPriceAsync(nozzlesPriceListAsendingByNozzlePhysicalId); if (cpResponse) logger.Info("Pump: " + this.pumpId + ", " + " Price change(reason PUMP_NOT_PROGRAMMED) succeed in 1st retry"); else { logger.Info("Pump: " + this.pumpId + ", " + " Price change(reason PUMP_NOT_PROGRAMMED) failed, will retry 2nd time"); cpResponse = await this.InternalChangeFuelPriceAsync(nozzlesPriceListAsendingByNozzlePhysicalId); if (cpResponse) logger.Info("Pump: " + this.pumpId + ", " + " Price change(reason PUMP_NOT_PROGRAMMED) succeed in 2nd retry"); else { logger.Info("Pump: " + this.pumpId + ", " + " Price change(reason PUMP_NOT_PROGRAMMED) failed, will retry 3rd time"); cpResponse = await this.InternalChangeFuelPriceAsync(nozzlesPriceListAsendingByNozzlePhysicalId); if (cpResponse) logger.Info("Pump: " + this.pumpId + ", " + " Price change(reason PUMP_NOT_PROGRAMMED) succeed in 3rd retry"); else logger.Error("Pump: " + this.pumpId + ", " + " Price change(reason PUMP_NOT_PROGRAMMED) failed again in 3rd retry, will stop"); } } this.context.Outgoing.Write( new ResetRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId))); } } catch (Exception exx) { logger.Error("Pump: " + this.pumpId + ", " + " Price change(reason PUMP_NOT_PROGRAMMED) exceptioned: " + exx); } //} //); #endregion } else if (pumpStatusTrx != null && pumpStatusTrx.Status == PumpStatus.SWITCHED_OFF) { logger.Debug("Pump: " + this.pumpId + ", " + " NEW State is SWITCHED_OFF, will send RESET"); this.context.Outgoing.WriteAsync( new ResetRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId)), (request, testResponse) => testResponse.Adrs == this.dartPumpCommAddress && testResponse.BlockSeqNumber == request.BlockSeqNumber && testResponse.ControlCharacter == ControlCharacter.ACK, (request, response) => { if (response != null) { logger.Debug("Pump: " + this.pumpId + ", " + "RESET request acked."); } else logger.Error("Pump: " + this.pumpId + ", " + "RESET request failed(timed out) to ack."); }, 3500); } if (pumpStatusTrx != null && pumpStatusTrx.Status == PumpStatus.FILLING_COMPLETED) { byte nozzlePhysicalIdForTrxDoneOn = 0; #region determine target nozzle if (nozzleStatusTrx != null) { if (nozzleStatusTrx.Status.Value == NozzleStatusAndFillingPrice_TransactionData.NozzleStatus.IN) { if (nozzleStatusTrx.Status.Key == 0) { //logger.Debug("Pump: " + this.pumpId + ", " + " more like a China domestic wayne dart since FILLING_COMPLETED contains nozzleStatus but nozzle physicalId is 0"); // nozzle number ==0 indicates all nozzles were put back(In) nozzlePhysicalIdForTrxDoneOn = this.operatingNozzlePhysicalId; } else nozzlePhysicalIdForTrxDoneOn = nozzleStatusTrx.Status.Key; } else if (nozzleStatusTrx.Status.Value == NozzleStatusAndFillingPrice_TransactionData.NozzleStatus.OUT) { // here happens in a case that shutdown the FC at a fueling for seconds, // and the pump will stop fueling by its design, and then start FC without put back nozzle, // then the nozzle will reported as OUT with FILLING_COMPLETED // for this case, FC still can recover the last fill since nozzle id is confirmed nozzlePhysicalIdForTrxDoneOn = nozzleStatusTrx.Status.Key; } } else { // igem wayne dart won't report nozzle number when entered FILLING_COMPLETED. //logger.Debug("Pump: " + this.pumpId + ", " + " more like a iGEM wayne dart since FILLING_COMPLETED does not contain nozzleStatus at all"); nozzlePhysicalIdForTrxDoneOn = this.operatingNozzlePhysicalId; } if (nozzlePhysicalIdForTrxDoneOn == 0) { logger.Info("Pump: " + this.pumpId + ", " + " targetNozzlePhysicalId is 0, will not query last fill"); return; } if (this.nozzles.FirstOrDefault(n => n.PhysicalId == nozzlePhysicalIdForTrxDoneOn) == null) { /* I do see a case that pump side report a nozzle physical id 15, possible wire issue?? here try to recover*/ logger.Info("Pump: " + this.pumpId + ", " + " targetNozzlePhysicalId: " + nozzlePhysicalIdForTrxDoneOn + " is NOT bound to any physical nozzle, will use last operating nozzle physical Id: " + this.operatingNozzlePhysicalId); nozzlePhysicalIdForTrxDoneOn = this.operatingNozzlePhysicalId; } #endregion try { LoopReadLastFill(nozzlePhysicalIdForTrxDoneOn); } catch (Exception exxx) { logger.Info("Pump: " + this.pumpId + ", " + " Read Last Fill exceptioned: " + exxx); this.lastFillRetrievedSinceNozzleDownAtFdcFueling = true; } } #region Trigger Fdc state change event if (pumpStatusTrx != null && pumpStatusTrx.Status == PumpStatus.AUTHORIZED) { // only WayneDart Authorized state received in FDC_CALLING is valid and expected. if (this.lastLogicalDeviceState == LogicalDeviceState.FDC_CALLING) { logger.Debug("Pump: " + this.pumpId + ", " + " State switched to FDC_AUTHORISED"); this.lastLogicalDeviceState = LogicalDeviceState.FDC_AUTHORISED; var safe1 = this.OnStateChange; safe1?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_AUTHORISED)); logger.Trace("Pump: " + this.pumpId + ", " + " OnStateChange event fired and back"); } else { logger.Info("Pump: " + this.pumpId + ", " + " Unexpected WayneDart state: AUTHORIZED, will stop the pump"); this.UnAuthorizePumpAndSwithToFdcReady(); } } if (pumpStatusTrx != null && pumpStatusTrx.Status == PumpStatus.FILLING) { if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_FUELLING) { logger.Debug("Pump: " + this.pumpId + ", " + " State switched to FDC_FUELLING"); this.lastLogicalDeviceState = LogicalDeviceState.FDC_FUELLING; var safe2 = this.OnStateChange; safe2?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_FUELLING, this.nozzles.FirstOrDefault(n => n.PhysicalId == this.operatingNozzlePhysicalId))); logger.Trace("Pump: " + this.pumpId + ", " + " OnStateChange event fired and back"); } } if (filledVolAndAmtTrx != null) { if (this.lastLogicalDeviceState == LogicalDeviceState.FDC_AUTHORISED) { if (nozzleStatusTrx != null && (nozzleStatusTrx.Status.Key == 0 || nozzleStatusTrx.Status.Value == NozzleStatusAndFillingPrice_TransactionData.NozzleStatus.IN)) { /* we do see a case: * >>>>>>>>>>===start====(internalFdcState: FDC_AUTHORISED) * Nozzle with physical id: 0 is in state: IN, filling price: 666 * Filled vol 4, amt 26 * <<<<<<<<<<===end==== * this is abnormal that why nozzle is IN but still have a running fuel, this caused by leaking control missed * in domestic WayneDart pump. * ignore this small running fuel, it will NOT report filling_complete later, HengShanIC reader rely on a flow of fueling+filling_Complete * finally, FC will unAuth this pump. */ logger.Info("Pump: " + this.pumpId + ", " + " Received a nozzle down with filling info in FDC_AUTHORISED state, " + "treat as fuel leak control malfunctioning in physical pump side case, will NOT switch to FDC_FUELLING"); } else { logger.Info("Pump: " + this.pumpId + ", " + " State switched to FDC_FUELLING from FDC_AUTHORISED"); this.lastLogicalDeviceState = LogicalDeviceState.FDC_FUELLING; var safe2 = this.OnStateChange; safe2?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_FUELLING, this.nozzles.FirstOrDefault(n => n.PhysicalId == this.operatingNozzlePhysicalId))); logger.Trace("Pump: " + this.pumpId + ", " + " OnStateChange event fired and back"); } } if (this.lastLogicalDeviceState == LogicalDeviceState.FDC_FUELLING) { logger.Debug("Pump: " + this.pumpId + ", " + "Filled volume " + filledVolAndAmtTrx.FilledVolume + ", amount " + filledVolAndAmtTrx.FilledAmount + " in state: FDC_FUELLING, will fire fdcEvent for logicalNozzle: " + this.nozzles.First(n => n.PhysicalId == this.operatingNozzlePhysicalId).LogicalId + ", physicalNozzle: " + this.operatingNozzlePhysicalId); var safe = this.OnCurrentFuellingStatusChange; safe?.Invoke(this, new FdcTransactionDoneEventArg(new FdcTransaction() { Nozzle = this.nozzles.First(n => n.PhysicalId == this.operatingNozzlePhysicalId), Amount = filledVolAndAmtTrx.FilledAmount, Volumn = filledVolAndAmtTrx.FilledVolume, Price = this.nozzles.First(n => n.PhysicalId == this.operatingNozzlePhysicalId).RealPriceOnPhysicalPump ?? 0, Finished = false, })); logger.Trace("Pump: " + this.pumpId + ", " + " OnCurrentFuellingStatusChange event fired and back"); } else logger.Info("Pump: " + this.pumpId + ", " + "Filled volume " + filledVolAndAmtTrx.FilledVolume + ", amount " + filledVolAndAmtTrx.FilledAmount + " received in internal Fdc state: " + this.lastLogicalDeviceState + ", will do nothing"); } #endregion if (nozzleStatusTrx != null) { #region Nozzle status and filling price var targetNozzle = this.nozzles.FirstOrDefault(n => n.PhysicalId == nozzleStatusTrx.Status.Key); if (targetNozzle != null) targetNozzle.RealPriceOnPhysicalPump = nozzleStatusTrx.FillingPrice; // nozzle number ==0 indicates all nozzles were put back(In) if (nozzleStatusTrx.Status.Key == 0 || nozzleStatusTrx.Status.Value == NozzleStatusAndFillingPrice_TransactionData.NozzleStatus.IN) { if (this.lastLogicalDeviceState == LogicalDeviceState.FDC_AUTHORISED) { /* here we just unauthorize the pump once nozzle down and pump get authorized previously, * some customers may want keep authorized state, but leave for future to implement */ logger.Debug("Pump: " + this.pumpId + ", " + " PhysicalNozzle: " + nozzleStatusTrx.Status.Key + " is put back, " + "will `Stop`(Unauthorize) the pump"); this.UnAuthorizePumpAndSwithToFdcReady(); } else { // Nozzle-IN in FDC state FDC_FUELLING if (this.lastLogicalDeviceState == LogicalDeviceState.FDC_FUELLING) { // if nozzle IN incoming with a FILLING_COMPLETED, then no need to fire extra read last fill process, // other parts of code will handle // here is trying to handle the case of FILLING_COMPLETED missed read in FC. if (pumpStatusTrx == null || (pumpStatusTrx != null && pumpStatusTrx.Status != PumpStatus.FILLING_COMPLETED)) { // will block pump auth from now by this flag set. this.lastFillRetrievedSinceNozzleDownAtFdcFueling = false; /* there's a case that phsycial pump missed to send Filling_complete to FC, cause miss to read last fill here bring in an extra read. */ // filling complete typically incoming in 1 or 2 seconds, and read last fill and read totalizer // will cost another 2 seconds. // here, if filling complete have not incoming for 6 seconds, then extra read will happens // I do see cases of `filling complete not incoming into FC` in field, may the issue in wire or physical pump?? this.retryReadLastFillTimer = new System.Timers.Timer(6000); this.retryReadLastFillTimer.Elapsed += (_, __) => { byte physicalNozzlePlacedBack = (nozzleStatusTrx.Status.Key == 0 ? this.operatingNozzlePhysicalId : nozzleStatusTrx.Status.Key); if (this.nozzles.FirstOrDefault(n => n.PhysicalId == physicalNozzlePlacedBack) == null) { /* I do see a case that pump report a nozzle with physical id 15 here! try to recover*/ logger.Info("Pump: " + this.pumpId + ", " + " targetNozzlePhysicalId: " + physicalNozzlePlacedBack + " is NOT bound to any physical nozzle, will use last operating nozzle physical Id: " + this.operatingNozzlePhysicalId); physicalNozzlePlacedBack = this.operatingNozzlePhysicalId; } logger.Info("Pump: " + this.pumpId + ", physicalNozzle: " + physicalNozzlePlacedBack + ", extra retrieving last fill is kicked off"); LoopReadLastFill(physicalNozzlePlacedBack); }; retryReadLastFillTimer.Start(); } } if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_READY) { logger.Debug("Pump: " + this.pumpId + ", " + " State switched to FDC_READY due to nozzle IN."); this.lastLogicalDeviceState = LogicalDeviceState.FDC_READY; var safe3 = this.OnStateChange; safe3?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_READY)); logger.Trace("Pump: " + this.pumpId + ", " + " OnStateChange event fired and back"); return; } } } else { /* specific nozzle out */ if ((this.lastLogicalDeviceState == LogicalDeviceState.FDC_AUTHORISED || (pumpStatusTrx != null && pumpStatusTrx.Status == PumpStatus.AUTHORIZED)) && nozzleStatusTrx.Status.Value == NozzleStatusAndFillingPrice_TransactionData.NozzleStatus.OUT) { if (this.operatingNozzlePhysicalId != nozzleStatusTrx.Status.Key) { //I do see cases in lab, fast switch(put down and put on another) nozzle by initial calling nozzle get authorized, //but Pump side didn't send in down Nozzle's In message. // we don't allow this case logger.Info("Pump: " + this.pumpId + ", " + " Detect nozzle switch(from physicalId: " + this.operatingNozzlePhysicalId + " to " + nozzleStatusTrx.Status.Key + ") in WayneDart.AUTHORIZED or LogicalDeviceState.FDC_AUTHORISED state, illegal, will stop the pump"); // how switched a nozzle in WayneDart in AUTHORIZED state?? this.UnAuthorizePumpAndSwithToFdcReady(); } // no need to calling anymore since it already in Authorized, this happens for WayneDart PumpSim software } else if ((this.lastLogicalDeviceState == LogicalDeviceState.FDC_FUELLING || this.lastLogicalDeviceState == LogicalDeviceState.FDC_AUTHORISED) && nozzleStatusTrx.Status.Value == NozzleStatusAndFillingPrice_TransactionData.NozzleStatus.OUT) { // we do see this case in domestic wayne dart pump, not sure how it happens. } else if (nozzleStatusTrx.Status.Value == NozzleStatusAndFillingPrice_TransactionData.NozzleStatus.OUT) { /* nozzle out */ if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_CALLING) { // need make sure previous trx had been retrieved by FC, otherwise should not send RequestRequest which will // clear previous trx info in physical pump side. if (!this.lastFillRetrievedSinceNozzleDownAtFdcFueling) { logger.Info("Pump: " + this.pumpId + ", " + " will ignore this nozzle out due to last fill still in retrieving"); return; } this.operatingNozzlePhysicalId = nozzleStatusTrx.Status.Key; logger.Debug("Pump: " + this.pumpId + ", " + " send RESET to clear previous trx info, prepare for new fuel"); this.context.Outgoing.WriteAsync( new ResetRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId)), (request, testResponse) => testResponse.Adrs == this.dartPumpCommAddress && testResponse.BlockSeqNumber == request.BlockSeqNumber, (request, ackResponse) => { if (ackResponse == null) { logger.Error("Pump: " + this.pumpId + ", " + " RESET wait for ACK timedout, will not fire calling state"); } else { logger.Debug("Pump: " + this.pumpId + ", " + " State switched to FDC_CALLING due to RESET ACKed or NAKed(physicalNozzle: " + nozzleStatusTrx.Status.Key + ")"); // insert a Poll for poll and receive wayne dart pump entered RESET state msg, otherwise // after the Fdc_Calling event fired below, outer will send auth request // directly and then pump will report 2 states in one msg response after auth: RESET + AUTHORIZED, //this has no problem, only for better understanding and logging. this.context.Outgoing.Write(new Poll(this.dartPumpCommAddress, 0)); //this.context.Outgoing.Write(new Poll(this.dartPumpCommAddress, 0)); //this.context.Outgoing.Write(new Poll(this.dartPumpCommAddress, 0)); //this.context.Outgoing.Write(new Poll(this.dartPumpCommAddress, 0)); this.lastLogicalDeviceState = LogicalDeviceState.FDC_CALLING; var safe2 = this.OnStateChange; safe2?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg( LogicalDeviceState.FDC_CALLING, this.nozzles.First(n => n.PhysicalId == nozzleStatusTrx.Status.Key))); logger.Trace("Pump: " + this.pumpId + ", " + " OnStateChange event fired and back"); } }, 2000); } if (pumpStatusTrx != null && pumpStatusTrx.Status == PumpStatus.FILLING) { if (filledVolAndAmtTrx != null && this.lastLogicalDeviceState == LogicalDeviceState.FDC_FUELLING) { logger.Debug("Pump: " + this.pumpId + ", " + "Filled volume " + filledVolAndAmtTrx.FilledVolume + ", amount " + filledVolAndAmtTrx.FilledAmount + " in state: FDC_FUELLING, will fire fdcEvent1"); var safe = this.OnCurrentFuellingStatusChange; safe?.Invoke(this, new FdcTransactionDoneEventArg(new FdcTransaction() { Nozzle = this.nozzles.First(n => n.PhysicalId == this.operatingNozzlePhysicalId), Amount = filledVolAndAmtTrx.FilledAmount, Volumn = filledVolAndAmtTrx.FilledVolume, Price = this.nozzles.First(n => n.PhysicalId == this.operatingNozzlePhysicalId).RealPriceOnPhysicalPump ?? 0, Finished = false, })); logger.Trace("Pump: " + this.pumpId + ", " + " OnCurrentFuellingStatusChange event fired and back"); } } } } #endregion } } } private void UnAuthorizePumpAndSwithToFdcReady() { this.context.Outgoing.WriteAsync( new StopRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId)), (request, testResponse) => testResponse.Adrs == this.dartPumpCommAddress && testResponse.BlockSeqNumber == request.BlockSeqNumber, (request, response) => { if (response != null) { if (response.ControlCharacter == ControlCharacter.ACK) { if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_READY) { logger.Debug("Pump: " + this.pumpId + ", " + " State switched to FDC_READY due to StopRequest acked."); this.lastLogicalDeviceState = LogicalDeviceState.FDC_READY; var safe3 = this.OnStateChange; safe3?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_READY)); logger.Trace("Pump: " + this.pumpId + ", " + " OnStateChange event fired and back"); return; } } else logger.Error("Pump: " + this.pumpId + ", " + "StopRequest NAKed."); } else logger.Error("Pump: " + this.pumpId + ", " + "StopRequest request timed out for ack."); if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_READY) { logger.Debug("Pump: " + this.pumpId + ", " + " State switched to FDC_READY though StopRequest failed"); this.lastLogicalDeviceState = LogicalDeviceState.FDC_READY; var safe2 = this.OnStateChange; safe2?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_READY)); logger.Trace("Pump: " + this.pumpId + ", " + " OnStateChange event fired and back"); } }, 1500); } private System.Timers.Timer retryReadLastFillTimer; private int onReadingLastFill = 0; /// /// may get called via 2 route: /// 1. received a Filling_Complete from pump. /// 2. by a delay timer that fired by nozzle IN from FDC_FUELING state. /// /// private void LoopReadLastFill(byte nozzlePhysicalIdForTrxDoneOn) { if (0 == Interlocked.CompareExchange(ref this.onReadingLastFill, 1, 0)) { this.retryReadLastFillTimer?.Stop(); this.retryReadLastFillTimer?.Dispose(); // disable auth since last fill has not been read yet, this happens in real site with bad wire connection condition. //this.lastFillRetrieved = false; this.ReadLastFillAndUpdateLocalTotalizerAndFireFdcTrxDoneEvent(nozzlePhysicalIdForTrxDoneOn, (t) => { if (t != null) { this.lastFillRetrievedSinceNozzleDownAtFdcFueling = true; this.onReadingLastFill = 0; return; } logger.Info("Pump: " + this.pumpId + ", " + " Read Last Fill failed on physicalNozzle: " + nozzlePhysicalIdForTrxDoneOn + ", will retry 1st time"); this.ReadLastFillAndUpdateLocalTotalizerAndFireFdcTrxDoneEvent(nozzlePhysicalIdForTrxDoneOn, (tt) => { if (tt != null) { this.lastFillRetrievedSinceNozzleDownAtFdcFueling = true; this.onReadingLastFill = 0; return; } logger.Info("Pump: " + this.pumpId + ", " + " Read Last Fill failed on physicalNozzle: " + nozzlePhysicalIdForTrxDoneOn + ", will retry 2nd time"); this.ReadLastFillAndUpdateLocalTotalizerAndFireFdcTrxDoneEvent(nozzlePhysicalIdForTrxDoneOn, (ttt) => { if (ttt != null) { this.lastFillRetrievedSinceNozzleDownAtFdcFueling = true; this.onReadingLastFill = 0; return; } logger.Info("Pump: " + this.pumpId + ", " + " Read Last Fill failed on physicalNozzle: " + nozzlePhysicalIdForTrxDoneOn + ", will retry 3rd time"); this.ReadLastFillAndUpdateLocalTotalizerAndFireFdcTrxDoneEvent(nozzlePhysicalIdForTrxDoneOn, (tttt) => { if (tttt != null) { this.lastFillRetrievedSinceNozzleDownAtFdcFueling = true; this.onReadingLastFill = 0; return; } logger.Error("Pump: " + this.pumpId + ", " + " Read Last Fill failed on physicalNozzle: " + nozzlePhysicalIdForTrxDoneOn + ", will NOT retry anymore (total read 4 times)"); // Have no way but release it to allow continue pump auth and fueling, But a fuel sale has been lost! this.onReadingLastFill = 0; this.lastFillRetrievedSinceNozzleDownAtFdcFueling = true; }); }); }); }); } } /// /// /// /// /// FdcTransaction is null when retrieved failed, like time out. otherwise, an object will return private void ReadLastFillAndUpdateLocalTotalizerAndFireFdcTrxDoneEvent(byte targetNozzlePhysicalId, Action callback) { try { #region ReturnFillingInfomrationRequest var targetNozzle = this.nozzles.First(n => n.PhysicalId == targetNozzlePhysicalId); logger.Debug("Pump: " + this.pumpId + ", " + " filling is finished, will query last fill info for physicalNozzle: " + targetNozzlePhysicalId + ", logicalNozzle: " + targetNozzle.LogicalId); this.context.Outgoing.WriteAsync( new ReturnFillingInfomrationRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId)), (request, testResponse) => testResponse.Adrs == this.dartPumpCommAddress && testResponse.ControlCharacter == ControlCharacter.DATA, (request, returnFillingInfoResponse) => { try { #region ReturnFillingInfomration Response if (returnFillingInfoResponse == null || (returnFillingInfoResponse != null && returnFillingInfoResponse.TransactionDatas.FirstOrDefault(f => f.TransactionNumber == 0x02) == null)) { logger.Info("Pump: " + this.pumpId + ", " + " retrieve last fill info timed out or inner FilledVolumeAndAmount trxData is null"); callback?.Invoke(null); } else { try { #region RequestTotalVolumeCountersRequest this.context.Outgoing.Write(new ACK(this.dartPumpCommAddress, returnFillingInfoResponse.BlockSeqNumber)); /*note, the response would contain last sale vol and money, besides, all nozzle state, and pump state will be returned as well, we only interested on sale vol and money which trx number is 0x02 */ FilledVolumeAndAmount_TransactionData lastFinishedTrx = null; try { lastFinishedTrx = new FilledVolumeAndAmount_TransactionData( returnFillingInfoResponse.TransactionDatas.FirstOrDefault(f => f.TransactionNumber == 0x02)); } catch (Exception exx) { var safeLogStr = "exceptioned in catch: "; try { safeLogStr = returnFillingInfoResponse?.ToLogString() ?? ""; } catch (Exception iiexx) { safeLogStr += iiexx.ToString(); } logger.Error($"Pump: " + this.pumpId + ", " + $"Parse FilledVolumeAndAmount_TransactionData(from: {safeLogStr}) exceptioned: {exx}"); } if (lastFinishedTrx != null && lastFinishedTrx.FilledVolume != 0) { #region concrete Non-Zero trx read logger.Info("Pump: " + this.pumpId + ", " + " Last fill retrieved, volume " + lastFinishedTrx.FilledVolume + ", amount " + lastFinishedTrx.FilledAmount + ", done in logical nozzle: " + targetNozzle.LogicalId + ", physical nozzle: " + targetNozzlePhysicalId + ", will read vol totalizer for compare"); this.context.Outgoing.WriteAsync( new RequestTotalVolumeCountersRequest(this.dartPumpCommAddress, this.parent.GetNewMessageToken(this.pumpId) , targetNozzle.PhysicalId), (___, testResponse) => testResponse.Adrs == this.dartPumpCommAddress && testResponse.ControlCharacter == ControlCharacter.DATA, (_____, volTotalResponse) => { FdcTransaction lastFillTrx = null; try { TotalCounters_TransactionData justReadVolumeTotalizerTrx = null; if (volTotalResponse != null) { this.context.Outgoing.Write(new ACK(this.dartPumpCommAddress, volTotalResponse.BlockSeqNumber)); if (volTotalResponse.TransactionDatas.FirstOrDefault(f => f.TransactionNumber == 0x65) != null) justReadVolumeTotalizerTrx = new TotalCounters_TransactionData( volTotalResponse.TransactionDatas.FirstOrDefault(f => f.TransactionNumber == 0x65)); } //any of null below will quit the last fill check, no worry about the trx miss, the caller will start retry if (volTotalResponse == null || justReadVolumeTotalizerTrx == null) { logger.Info("Pump: " + this.pumpId + ", " + " logical nozzle: " + targetNozzle.LogicalId + ", physicalNozzle: " + targetNozzlePhysicalId + ", read VolTotalizer failed"); callback?.Invoke(null); return; } logger.Info("Pump: " + this.pumpId + ", " + " logical nozzle: " + targetNozzle.LogicalId + ", physicalNozzle: " + targetNozzlePhysicalId + ", Vol totalizer read value: " + ((justReadVolumeTotalizerTrx?.TotalValue.ToString()) ?? "null")); if (targetNozzle.VolumeTotalizer.HasValue) { /* compare last backup totalizer with just read value, if equals, should not rasie up new trx*/ var lastVolumeTotalizerValue = targetNozzle.VolumeTotalizer.Value; logger.Info("Pump: " + this.pumpId + ", " + "logical nozzle: " + targetNozzle.LogicalId + ", last backup volume totalizer value: " + lastVolumeTotalizerValue + ", while now just read volume totalizer value: " + justReadVolumeTotalizerTrx.TotalValue + ", they're " + (lastVolumeTotalizerValue == justReadVolumeTotalizerTrx.TotalValue ? "Equal(no new trx)" : "Not Equal with diff: " + (justReadVolumeTotalizerTrx.TotalValue - lastVolumeTotalizerValue) + " (new trx detected and created with vol from LastFill data element: " + lastFinishedTrx.FilledVolume + ")")); targetNozzle.VolumeTotalizer = justReadVolumeTotalizerTrx.TotalValue; if (lastVolumeTotalizerValue == justReadVolumeTotalizerTrx.TotalValue) { // no new trx callback?.Invoke(new FdcTransaction()); return; } } if (!targetNozzle.VolumeTotalizer.HasValue) targetNozzle.VolumeTotalizer = justReadVolumeTotalizerTrx.TotalValue; // at least within 65 years, exception will not throw here int newTrxSeqNumber = (int)(DateTime.Now.Subtract(new DateTime(2018, 5, 25)).TotalSeconds); logger.Info("Pump: " + this.pumpId + ", " + "logical nozzle: " + targetNozzle.LogicalId + ", physicalNozzle: " + targetNozzlePhysicalId + ", trx done with amt: " + lastFinishedTrx.FilledAmount + ", vol: " + lastFinishedTrx.FilledVolume + ", seqNo.: " + newTrxSeqNumber); lastFillTrx = new FdcTransaction() { Nozzle = this.nozzles.First(n => n.PhysicalId == targetNozzlePhysicalId), Amount = lastFinishedTrx.FilledAmount, Volumn = lastFinishedTrx.FilledVolume, Price = this.nozzles.First(n => n.PhysicalId == targetNozzlePhysicalId).RealPriceOnPhysicalPump ?? 0, SequenceNumberGeneratedOnPhysicalPump = newTrxSeqNumber, VolumeTotalizer = justReadVolumeTotalizerTrx?.TotalValue ?? 0, Finished = true, }; callback?.Invoke(lastFillTrx); } catch { callback?.Invoke(null); } var safe6 = this.OnCurrentFuellingStatusChange; safe6?.Invoke(this, new FdcTransactionDoneEventArg(lastFillTrx)); logger.Trace("Pump: " + this.pumpId + ", " + " OnCurrentFuellingStatusChange event fired and back"); }, 3000); #endregion } else { logger.Info("Pump: " + this.pumpId + ", " + " Last filled info is null or volume is 0, will ignore"); // zero amount trx callback?.Invoke(new FdcTransaction()); } #endregion } catch (Exception exxxx) { callback?.Invoke(null); } } #endregion } catch { callback?.Invoke(null); } }, 2500); #endregion } catch { callback?.Invoke(null); } } public void Dispose() { this.deviceOfflineCountdownTimer.Stop(); this.deviceOfflineCountdownTimer.Dispose(); this.retryReadLastFillTimer?.Stop(); this.retryReadLastFillTimer?.Dispose(); } } }