using System; using System.Collections.Generic; using System.Linq; using HengShan_Pump_NonIC_Plus.MessageEntity; using Edge.Core.Processor; using Edge.Core.IndustryStandardInterface.Pump; using Wayne.FDCPOSLibrary; using Edge.Core.Database.Models; using System.Threading.Tasks; using Edge.Core.Parser.BinaryParser.MessageEntity; using static HengShan_Pump_NonIC_Plus.MessageEntity.GetNozzleStatusResponse; using static HengShan_Pump_NonIC_Plus.MessageEntity.NonICMessageTemplateResponseBase; using System.Xml; using Microsoft.Extensions.Logging; namespace HengShan_Pump_NonIC_Plus { public class PumpHandler : IFdcPumpController, IDisposable { public event EventHandler OnStateChange; /// /// fired on fueling process is on going, the fuel amount should keep changing. /// public event EventHandler OnCurrentFuellingStatusChange; protected IContext context; private ILogger logger = null; /// /// 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 PumpStatus lastLogicalDeviceState = PumpStatus.未运行; /// /// Indicator for OnFdcServiceInit function called, the Process() will be called eariler that this function, /// protected bool isOnFdcServerInitCalled = false; private Guid uniqueId = Guid.NewGuid(); private PumpGroupHandler parent; private int pumpId = -1; protected List nozzles = new List(); private byte liftNozzleId = 0; private int amountDecimalDigits; private int volumeDecimalDigits; private int priceDecimalDigits; private int volumeTotalizerDecimalDigits; private int previousPolledHandlerIndex = 0; /// /// this type of pump state change is detected by FC actively polling, then state is always delay reported, so there's a corner case that in a fueling process, /// the attendants put back and pull out nozzle very quickly and it happened exactly in the middle of a polling, meanwhile, /// an auth request was done to auth the pump again(most likely the autoAuthCallingPump set with True), /// then the pump state returned from physical pump will still be read as a fueling state, but actually the /// 2nd fueling process is started, so detect the pump state is not enough, need detect if the fueling seq number reset to null(if place back nozzle detected) or not. /// protected GetNozzleStatusResponse previousUnfinishedFuelingNozzleStatus; public IEnumerable Nozzles => this.nozzles; protected void FireOnStateChangeEvent(LogicalDeviceState state) { var safe = this.OnStateChange; safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(state, this.nozzles.First())); } protected void FireOnCurrentFuellingStatusChangeEvent(FdcTransaction trx) { var safe = this.OnCurrentFuellingStatusChange; safe?.Invoke(this, new FdcTransactionDoneEventArg(trx)); } public PumpHandler(PumpGroupHandler parent, int pumpId, int amountDecimalDigits, int volumeDecimalDigits, int priceDecimalDigits, int volumeTotalizerDecimalDigits, string pumpXmlConfiguration, ILogger logger) { this.parent = parent; this.pumpId = pumpId; this.amountDecimalDigits = amountDecimalDigits; this.volumeDecimalDigits = volumeDecimalDigits; this.priceDecimalDigits = priceDecimalDigits; this.volumeTotalizerDecimalDigits = volumeTotalizerDecimalDigits; this.logger = logger; // 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("HSC+ pump only accept pump address range from 1 to 32, make sure this value is correctly configurated in physical pump mother board"); 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("HSC+ 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.LogInformation("Pump: " + this.pumpId + ", created a nozzle with logicalId: " + nozzleLogicalId + ", physicalId: " + nozzlePhysicalId + ", default raw price without decimal points: " + nozzleRawDefaultPriceWithoutDecimal); } } public NonICMessageTemplateBase GetRequest() { if (this.liftNozzleId != 0) return new GetNozzleStatusRequest(this.liftNozzleId); if (this.nozzles.Count <= previousPolledHandlerIndex) previousPolledHandlerIndex = 0; var target = this.nozzles[previousPolledHandlerIndex++]; return new GetNozzleStatusRequest(target.PhysicalId); } public void Init(IContext context) { this.context = context; this.context.Incoming.OnLongTimeNoSeeMessage += (_, __) => { if (this.lastLogicalDeviceState != PumpStatus.未运行) { this.lastLogicalDeviceState = PumpStatus.未运行; logger.LogInformation("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)); logger.LogTrace("Pump: " + this.pumpId + ", " + " OnStateChange event fired and back"); } }; this.context.Incoming.LongTimeNoSeeMessageTimeout = 3000; } public async Task Process(IContext context) { if (!isOnFdcServerInitCalled) return; this.context = context; if (context.Incoming.Message is GetNozzleStatusResponse getNozzleStatusResponse) { var latestStatus = (PumpStatus)getNozzleStatusResponse.Status; var safe = this.OnStateChange; string prefix = "Pump: " + this.pumpId + ", " + "Nozzle: " + getNozzleStatusResponse.Nozzle + ", "; if (this.lastLogicalDeviceState == PumpStatus.未运行 && latestStatus != PumpStatus.未运行) { logger.LogInformation(prefix + "Recevied an Pump Msg in FDC_OFFLINE state, " + "indicates the underlying connection is established, switch to FDC_READY"); if (latestStatus == PumpStatus.空闲态) this.lastLogicalDeviceState = latestStatus; safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_READY)); logger.LogTrace(prefix + " OnStateChange event fired and back"); } // put the price by reading the real price. if (0 != getNozzleStatusResponse.Nozzle) this.nozzles.First(n => n.PhysicalId == getNozzleStatusResponse.Nozzle).RealPriceOnPhysicalPump = getNozzleStatusResponse.单价; if (latestStatus == PumpStatus.空闲态) { //在加油结束后,交易信息跟随加油状态主动上报给后台 //if (this.lastLogicalDeviceState == PumpStatus.正在加油 || this.lastLogicalDeviceState == PumpStatus.暂停加油 || // latestStatus == PumpStatus.暂停开始 || latestStatus == PumpStatus.暂停加油) //{ //} this.liftNozzleId = 0; this.lastLogicalDeviceState = PumpStatus.空闲态; safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_READY, this.nozzles.First(n => n.PhysicalId == getNozzleStatusResponse.Nozzle))); } else if (latestStatus == PumpStatus.提枪) { this.liftNozzleId = getNozzleStatusResponse.Nozzle; logger.LogDebug(prefix + "收到状态: " + latestStatus.ToString() + ", switch to FDC_CALLING"); this.lastLogicalDeviceState = PumpStatus.提枪; safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_CALLING, this.nozzles.First(n => n.PhysicalId == liftNozzleId))); } else if (latestStatus == PumpStatus.授权) { this.liftNozzleId = getNozzleStatusResponse.Nozzle; logger.LogDebug(prefix + "收到状态: " + latestStatus.ToString() + ", switch to FDC_AUTHORISED"); lastLogicalDeviceState = PumpStatus.授权; safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_AUTHORISED, this.nozzles.First(n => n.PhysicalId == liftNozzleId))); } else if (latestStatus == PumpStatus.开始加油 || latestStatus == PumpStatus.正在加油) { this.liftNozzleId = getNozzleStatusResponse.Nozzle; logger.LogDebug(prefix + "收到状态: " + latestStatus.ToString() + ", switch to FDC_FUELLING"); lastLogicalDeviceState = latestStatus; safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_FUELLING, this.nozzles.First(n => n.PhysicalId == liftNozzleId))); } else if (latestStatus == PumpStatus.暂停开始 || latestStatus == PumpStatus.暂停加油) { this.liftNozzleId = getNozzleStatusResponse.Nozzle; logger.LogDebug(prefix + "收到状态: " + latestStatus.ToString() + ", switch to FDC_SUSPENDED_FUELLING"); lastLogicalDeviceState = latestStatus; safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_SUSPENDED_FUELLING, this.nozzles.First(n => n.PhysicalId == liftNozzleId))); } else if (latestStatus == PumpStatus.未运行 || latestStatus == PumpStatus.关闭) { this.lastLogicalDeviceState = latestStatus; } else { this.lastLogicalDeviceState = PumpStatus.未运行; logger.LogDebug(prefix + "收到未知状态: " + getNozzleStatusResponse.ToLogString() + ", \r\n switch to FDC_ERRORSTATE"); } } else if (context.Incoming.Message is ActivePushTransactionResponse trx) { logger.LogDebug($"Pump: {this.pumpId}, Nozzle: {trx.Nozzle}, {trx.ToLogString()}"); byte targetNozzlePhysicalId = trx.Nozzle; var lastFillTrx = new FdcTransaction() { Nozzle = this.nozzles.First(n => n.PhysicalId == targetNozzlePhysicalId), Amount = trx.加油金额, Volumn = trx.加油量, Price = this.nozzles.First(n => n.PhysicalId == targetNozzlePhysicalId).RealPriceOnPhysicalPump ?? 0, SequenceNumberGeneratedOnPhysicalPump = trx.SequenceNo, VolumeTotalizer = (int)trx.升累计, Finished = true, }; FireOnCurrentFuellingStatusChangeEvent(lastFillTrx); byte result = (byte)EnumResult.成功; await this.context.Outgoing.WriteAsync(new AckActivePushTransactionRequest(targetNozzlePhysicalId) { HandleResult = result }, null, 1); } else { logger.LogDebug("Pump: " + this.pumpId + ", " + "收到: " + context.Incoming.Message.ToLogString()); } } public virtual async Task QueryStatusAsync() { switch (this.lastLogicalDeviceState) { case PumpStatus.空闲态: return LogicalDeviceState.FDC_OFFLINE; default: return LogicalDeviceState.FDC_OFFLINE; } } public string Name => this.GetType().FullName; public Guid Id => this.uniqueId; /// /// Gets the Identification of the pump for the system. Is the logical number of the pump /// public int PumpId => this.pumpId; /// /// this pump have no way to share same comport since this HengShan protocol content does not contains /// any id info, so always static 0 here. /// 地址面地址 /// public int PumpPhysicalId => 0; public int AmountDecimalDigits => this.amountDecimalDigits; public int VolumeDecimalDigits => this.volumeDecimalDigits; public int PriceDecimalDigits => this.priceDecimalDigits; public int VolumeTotalizerDecimalDigits => this.volumeTotalizerDecimalDigits; /// /// /// /// MoneyTotalizer:VolumnTotalizer public async Task> QueryTotalizerAsync(byte logicalNozzleId) { var result = new Tuple(-1, -1); logger.LogInformation("Pump: " + this.pumpId + ", " + " Start QueryTotalizer for logicalNozzle: " + logicalNozzleId); if (this.lastLogicalDeviceState == PumpStatus.未运行) { logger.LogInformation("Pump: " + this.pumpId + ", " + " Pump is in state FDC_CLOSED or FDC_OFFLINE, will return -1, -1"); return result; } byte nozzleId = this.nozzles.First(n => n.LogicalId == logicalNozzleId).PhysicalId; var response = await this.context.Outgoing.WriteAsync(new GetAccumulateRequest(nozzleId), (request, testResponse) => testResponse is GetAccumulateResponse, 3000); if (response == null) { logger.LogInformation("Pump: " + this.pumpId + ", " + "QueryTotalizer timed out"); return result; } else { var accumResponse = response as GetAccumulateResponse; logger.LogDebug($"Pump: {this.pumpId}, {accumResponse.ToLogString()}"); result = new Tuple((int)accumResponse.金额累计, (int)accumResponse.升累计); } return result; } public virtual async Task ChangeFuelPriceAsync(int newPriceWithoutDecimalPoint, byte logicalNozzleId) { logger.LogInformation("Pump: " + this.pumpId + ", " + " Start ChangeFuelPrice for logicalNozzle: " + logicalNozzleId + " with new price(without decimalPoints): " + newPriceWithoutDecimalPoint); if (this.lastLogicalDeviceState == PumpStatus.未运行) { logger.LogInformation("Pump: " + this.pumpId + ", " + " Pump is in state FDC_CLOSED or FDC_OFFLINE, ChangeFuelPrice will return false"); return false; } byte nozzleId = this.nozzles.First(n => n.LogicalId == logicalNozzleId).PhysicalId; var response = await this.context.Outgoing.WriteAsync(new SetFuelPriceRequest(nozzleId) { FuelPrice = newPriceWithoutDecimalPoint }, (request, testResponse) => testResponse is SetFuelPriceResponse, 3000); if (response == null) { logger.LogInformation("Pump: " + this.pumpId + ", " + "ChangeFuelPrice timed out"); return false; } else { var priceChangeResponse = response as SetFuelPriceResponse; if (priceChangeResponse.Result != EnumResult.成功) { logger.LogInformation("Pump: " + this.pumpId + ", " + "ChangeFuelPriceResponse is NOT Result.成功"); return false; } else { logger.LogInformation("Pump: " + this.pumpId + ", " + "ChangeFuelPriceResponse succeed"); return true; } } } public virtual async Task ChangePumpClockAsync(DateTime datetime, byte logicalNozzleId) { logger.LogInformation("Pump: " + this.pumpId + ", " + " Start ChangePumpClockAsync"); if (this.lastLogicalDeviceState == PumpStatus.未运行) { logger.LogInformation("Pump: " + this.pumpId + ", " + " Pump is in state FDC_CLOSED or FDC_OFFLINE, ChangePumpClockAsync will return false"); return false; } var response = await this.context.Outgoing.WriteAsync(new SetClockRequest(logicalNozzleId, datetime), (request, testResponse) => testResponse is SetClockResponse, 3000); if (response == null) { logger.LogInformation("Pump: " + this.pumpId + ", " + "ChangePumpClock timed out"); return false; } else { var setClockResponse = response as SetClockResponse; if (setClockResponse.Result != EnumResult.成功) { logger.LogInformation("Pump: " + this.pumpId + ", " + "SetClockResponse is NOT Result.成功"); return false; } else { logger.LogInformation("Pump: " + this.pumpId + ", " + "SetClockResponse succeed"); return true; } } } /// /// /// /// useless for this type of pump, it always one pump one nozzle /// public virtual async Task AuthorizeAsync(byte logicalNozzleId) { logger.LogDebug("Pump: " + this.pumpId + ", " + "Start Authorize for logicalNozzle: " + this.liftNozzleId); var response = await this.context.Outgoing.WriteAsync(new StartRequest(this.liftNozzleId), (request, testResponse) => testResponse is StartResponse, 3000); if (response == null) { logger.LogInformation("Pump: " + this.pumpId + ", " + "Authorize timed out"); return false; } else { var startResponse = response as StartResponse; string prefix = "Pump: " + this.pumpId + ", " + "Nozzle: " + startResponse.Nozzle + ", "; if (startResponse.Result != EnumResult.成功) { logger.LogInformation(prefix + "Authorize (StartResponse) is NOT Result.成功"); return false; } else { logger.LogDebug(prefix + "Authorize (StartResponse) succeed"); return true; } } } /// /// /// /// /// useless for this type of pump, it always one pump one nozzle /// public virtual async Task AuthorizeWithAmountAsync(int moneyAmountWithoutDecimalPoint, byte logicalNozzleId) { //return await AuthorizeWithVolumeAsync(moneyAmountWithoutDecimalPoint, logicalNozzleId); logger.LogDebug("Pump: " + this.pumpId + ", " + "Start AuthorizeWithAmount for logicalNozzle: " + this.liftNozzleId + " with money(without decimalPoint): " + moneyAmountWithoutDecimalPoint); var response = await this.context.Outgoing.WriteAsync(new AuthPumpWithAmountRequest(this.liftNozzleId) { Amount = moneyAmountWithoutDecimalPoint }, (request, testResponse) => testResponse is AuthPumpWithAmountResponse, 3000); if (response == null) { logger.LogInformation("Pump: " + this.pumpId + ", " + "AuthorizeWithAmount timed out"); return false; } else { var presetResponse = response as AuthPumpWithAmountResponse; if (presetResponse.Result != EnumResult.成功) { logger.LogInformation("Pump: " + this.pumpId + ", " + "AuthPumpWithAmountResponse is NOT Result.成功"); return false; } else { logger.LogDebug("Pump: " + this.pumpId + ", " + "Authorize (StartResponse) succeed"); return true; } } } /// /// /// /// /// useless for this type of pump, it always one pump one nozzle /// public virtual async Task AuthorizeWithVolumeAsync(int volumnWithoutDecimalPoint, byte logicalNozzleId) { logger.LogDebug("Pump: " + this.pumpId + ", " + "Start AuthorizeWithVolumn for logicalNozzle: " + this.liftNozzleId + " with vol(without decimalPoint): " + volumnWithoutDecimalPoint); var response = await this.context.Outgoing.WriteAsync(new AuthPumpWithGallonRequest(this.liftNozzleId) { Gallon = volumnWithoutDecimalPoint }, (request, testResponse) => testResponse is AuthPumpWithGallonResponse, 3000); if (response == null) { logger.LogInformation("Pump: " + this.pumpId + ", " + "AuthPumpWithGallonRequest timed out"); return false; } else { var presetResponse = (AuthPumpWithGallonResponse)response; if (presetResponse.Result != EnumResult.成功) { logger.LogInformation("Pump: " + this.pumpId + ", " + "AuthorizeWithVolumnResponse is NOT Result.成功"); return false; } else { logger.LogDebug("Pump: " + this.pumpId + ", " + "Authorize (StartResponse) succeed"); return true; } } } public virtual async Task GetTransactionAsync(int sequenceNo, byte logicalNozzleId) { logger.LogInformation("Pump: " + this.pumpId + ", " + "Start Get transaction for sequenceNo: " + sequenceNo); var response = await this.context.Outgoing.WriteAsync(new GetTransactionRequest(logicalNozzleId) { SequenceNo = sequenceNo }, (request, testResponse) => (testResponse is GetTransactionResponse || testResponse is GetTransactionFailureResponse), 3000); if (response == null) { logger.LogInformation("Pump: " + this.pumpId + ", " + "GetTransactionRequest timed out"); return false; } else { if (response is GetTransactionFailureResponse) { logger.LogInformation("Pump: " + this.pumpId + ", " + "不存在该流水"); return false; } else { var trxResponse = response as GetTransactionResponse; logger.LogInformation($"Pump: {this.pumpId}, {trxResponse.ToLogString()}"); return true; } } } public virtual async Task GetVersionAsync(byte logicalNozzleId) { logger.LogInformation("Pump: " + this.pumpId + ", " + "Start Get version"); var response = await this.context.Outgoing.WriteAsync(new GetVersionRequest(logicalNozzleId), (request, testResponse) => (testResponse is GetVersionResponse), 3000); if (response == null) { logger.LogInformation("Pump: " + this.pumpId + ", " + "GetVersionRequest timed out"); return false; } else { var versionResponse = response as GetVersionResponse; logger.LogInformation($"Pump: {this.pumpId}, {versionResponse.ToLogString()}"); return true; } } public virtual async Task ErrorPromptAsync(string errorMessage, byte logicalNozzleId) { logger.LogInformation("Pump: " + this.pumpId + ", " + "Start Error message prompt."); var response = await this.context.Outgoing.WriteAsync(new ErrorPromptRequest(logicalNozzleId, errorMessage), (request, testResponse) => testResponse is ErrorPromptResponse, 3000); if (response == null) { logger.LogInformation("Pump: " + this.pumpId + ", " + "Error message prompt timed out"); return false; } else { var errorResponse = response as ErrorPromptResponse; if (errorResponse.Result != EnumResult.成功) { logger.LogInformation("Pump: " + this.pumpId + ", " + "ErrorPromptResponse is NOT Result.成功"); return false; } else { logger.LogInformation("Pump: " + this.pumpId + ", " + "Error message prompt succeed"); return true; } } } public virtual async Task CancelRationAsync(byte logicalNozzleId) { logger.LogInformation("Pump: " + this.pumpId + ", " + "Start Cancel ration."); var response = await this.context.Outgoing.WriteAsync(new CancelRationRequest(logicalNozzleId), (request, testResponse) => testResponse is CancelRationResponse, 3000); if (response == null) { logger.LogInformation("Pump: " + this.pumpId + ", " + "Cancel ration timed out"); return false; } else { var rationResponse = response as CancelRationResponse; if (rationResponse.Result != EnumResult.成功) { logger.LogInformation("Pump: " + this.pumpId + ", " + "CancelRationResponse is NOT Result.成功"); return false; } else { logger.LogInformation("Pump: " + this.pumpId + ", " + "Cancel ration succeed"); return true; } } } public virtual async Task FuelingRoundUpByAmountAsync(int amount) { logger.LogInformation("Pump: " + this.pumpId + ", " + " Start FuelingRoundUpByAmount, amount: " + amount + " will be ignored due to hardware limit"); return await Task.FromResult(false); } #region not implemented public async Task UnAuthorizeAsync(byte logicalNozzleId) { throw new NotImplementedException(); } public async Task SuspendFuellingAsync() { throw new NotImplementedException(); } public async Task ResumeFuellingAsync() { throw new NotImplementedException(); } public async Task FuelingRoundUpByVolumeAsync(int volume) { throw new NotImplementedException(); } #endregion /// /// protected Dictionary logicalNozzleIdToLastFuelSaleTrxMapping = new Dictionary(); public void OnFdcServerInit(Dictionary parameters) { if (parameters.ContainsKey("LastPriceChange")) { } /* Load Last sale(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.LogInformation("Pump: " + this.pumpId + ", OnFdcServerInit, load last fuel sale " + "on logical nozzle: " + lastFuelSaleTrx.Key + " with value: " + lastFuelSaleTrx.Value); this.logicalNozzleIdToLastFuelSaleTrxMapping.Remove(lastFuelSaleTrx.Key); this.logicalNozzleIdToLastFuelSaleTrxMapping.Add(lastFuelSaleTrx.Key, lastFuelSaleTrx.Value); } } this.isOnFdcServerInitCalled = true; } public async Task LockNozzleAsync(byte logicalNozzleId) { return false; } public async Task UnlockNozzleAsync(byte logicalNozzleId) { return false; } public void Dispose() { //this.retryReadLastFillTimer?.Stop(); //this.retryReadLastFillTimer?.Dispose(); } } }