using Edge.Core.Database.Models; using Edge.Core.Processor; using Edge.Core.IndustryStandardInterface.Pump; using Edge.Core.UniversalApi; using LanTian_Pump_664_Or_886.MessageEntity; using LanTian_Pump_664_Or_886.MessageEntity.Incoming; using LanTian_Pump_664_Or_886.MessageEntity.Outgoing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using NLog.Fluent; using Edge.Core.Parser.BinaryParser.Util; using Stateless; 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 static LanTian_Pump_664_Or_886.PumpGroupParameter; namespace LanTian_Pump_664_Or_886 { public class StatePumpHandler : IFdcPumpController { private IServiceProvider services; static ILogger logger = NullLogger.Instance; 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 = 6; private List nozzles = new List(); private System.Timers.Timer deviceOfflineCountdownTimer; private IContext context; private bool isOnFdcServerInitCalled = false; private int pumpId; /// /// hardware address value of this pump, must be set in physical dispenser side. /// private byte address; private PumpModelEnum pumpModel; private PumpAuthorizeModeEnum pumpAuthorizeMode; protected enum Trigger { //AnyPumpMsgReceived, AnyPumpMsgHaveNotReceivedForWhile, NozzleLifted_And_开机, NozzleLifted_And_停机, NozzleReplaced_And_开机, NozzleReplaced_And_停机, NozzleFuelNumbersIsRunning, //PumpAuthorizedByFC, } StateMachine stateless = new StateMachine(LogicalDeviceState.FDC_OFFLINE); private int amountDecimalDigits; private int volumeDecimalDigits; private int priceDecimalDigits; private int volumeTotalizerDecimalDigits; private StateMachine.TriggerWithParameters nozzleFuelNumbersIsRunningTrigger; /// /// 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 public string Name => "LanTian_Pump_664_Or_886"; public int PumpId => this.pumpId; 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 int PumpPhysicalId => this.address; public event EventHandler OnStateChange; public event EventHandler OnCurrentFuellingStatusChange; public async Task QueryStatusAsync() { return this.lastLogicalDeviceState; } public async Task LockNozzleAsync(byte logicalNozzleId) { return false; } public async Task UnlockNozzleAsync(byte logicalNozzleId) { return false; } #endregion public StatePumpHandler(int pumpId, byte address, PumpModelEnum pumpModel, PumpAuthorizeModeEnum pumpAuthorizeMode, int amountDecimalDigits, int volumeDecimalDigits, int priceDecimalDigits, int volumeTotalizerDecimalDigits, IServiceProvider services) { this.services = services; var loggerFactory = services.GetRequiredService(); logger = loggerFactory.CreateLogger("PumpHandler"); try { this.pumpId = pumpId; this.address = address; this.pumpModel = pumpModel; this.pumpAuthorizeMode = pumpAuthorizeMode; this.amountDecimalDigits = amountDecimalDigits; this.volumeDecimalDigits = volumeDecimalDigits; this.priceDecimalDigits = priceDecimalDigits; this.volumeTotalizerDecimalDigits = volumeTotalizerDecimalDigits; this.pumpId = pumpId; // real nozzle Id put hardcode 1 here since HengShan_pump_nonIC only have 1 nozzle per pump. // price is not dectected yet, put Null. this.nozzles.Add(new LogicalNozzle(pumpId, this.address, 1, null)); this.deviceOfflineCountdownTimer = new System.Timers.Timer(2000); this.deviceOfflineCountdownTimer.Elapsed += async (_, __) => { if (DateTime.Now.Subtract(this.lastLogicalDeviceStateReceivedTime).TotalSeconds >= lastLogicalDeviceStateExpiredTime) await this.stateless.FireAsync(Trigger.AnyPumpMsgHaveNotReceivedForWhile); }; this.deviceOfflineCountdownTimer.Start(); this.stateless.OnTransitioned(async (transition) => { if (transition.Destination != transition.Source) { this.lastLogicalDeviceState = transition.Destination; logger.LogInformation("Pump: " + this.pumpId + ", " + " State switched from: " + transition.Source + " to " + transition.Destination); this.OnStateChange?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(transition.Destination, this.nozzles.FirstOrDefault())); } if (transition.Source == LogicalDeviceState.FDC_OFFLINE) { //always send a query to try to align latest price when switched to Online. this.context.Outgoing.Write(new ReadPriceRequest() { Adrs = this.address }); } }); this.stateless.Configure(LogicalDeviceState.FDC_OFFLINE) .OnEntryAsync(async () => { }) .OnEntryAsync(async () => { }) //.Ignore(Trigger.AnyPumpMsgReceived) .Ignore(Trigger.AnyPumpMsgHaveNotReceivedForWhile) .PermitIf(Trigger.NozzleLifted_And_停机, LogicalDeviceState.FDC_CALLING, () => true) .PermitIf(Trigger.NozzleLifted_And_开机, LogicalDeviceState.FDC_AUTHORISED, () => true) .PermitIf(Trigger.NozzleReplaced_And_停机, LogicalDeviceState.FDC_READY, () => true) .PermitIf(Trigger.NozzleReplaced_And_开机, LogicalDeviceState.FDC_AUTHORISED, () => true); //once a fule sale is done, a retrieving process is triggered, before the process done, new pump Calling will be blocked. bool isOnRetrievingLastFuelSale = false; this.stateless.Configure(LogicalDeviceState.FDC_READY) .OnEntryFromAsync(Trigger.NozzleReplaced_And_停机, async (transition) => { if (transition.Source == LogicalDeviceState.FDC_FUELLING) { isOnRetrievingLastFuelSale = true; ReadFuelDataResponse finalTrxDataReading; try { logger.LogInformation("Pump: " + this.pumpId + ", A fueling just done, Retrieving last fuel sale trx..."); finalTrxDataReading = await this.context.Outgoing.WriteAsync(new ReadFuelDataRequest() { Adrs = this.address }, (_, testResponse) => testResponse is ReadFuelDataResponse rp && rp.Adrs == this.address, 3000) as ReadFuelDataResponse; if (finalTrxDataReading == null) { finalTrxDataReading = await this.context.Outgoing.WriteAsync(new ReadFuelDataRequest() { Adrs = this.address }, (_, testResponse) => testResponse is ReadFuelDataResponse rp && rp.Adrs == this.address, 2000) as ReadFuelDataResponse; if (finalTrxDataReading == null) { logger.LogWarning("Pump: " + this.pumpId + ", fuel sale trx is missing for final read(auto tried one more time and still failed), trx will lost"); return; } } } finally { isOnRetrievingLastFuelSale = false; } // at least within 65 years, exception will not throw here int newTrxSeqNumber = (int)(DateTime.Now.Subtract(new DateTime(2020, 6, 5)).TotalSeconds); logger.LogDebug("Pump: " + this.pumpId + ", Retrieved fuel sale trx, amt: " + finalTrxDataReading.Amount + ", vol: " + finalTrxDataReading.Volume + ", seqNo.: " + newTrxSeqNumber); var newTrx = new FdcTransaction() { // 只有一把枪 Nozzle = this.nozzles.First(), Amount = finalTrxDataReading.Amount, Volumn = finalTrxDataReading.Volume, Price = this.nozzles.First().RealPriceOnPhysicalPump ?? -1, SequenceNumberGeneratedOnPhysicalPump = newTrxSeqNumber, Finished = true, }; this.OnCurrentFuellingStatusChange?.Invoke(this, new FdcTransactionDoneEventArg(newTrx)); } }) .OnEntryAsync(async () => { }) //.Ignore(Trigger.AnyPumpMsgReceived) .Ignore(Trigger.NozzleReplaced_And_停机) .Ignore(Trigger.NozzleFuelNumbersIsRunning) .PermitIf(Trigger.NozzleLifted_And_停机, LogicalDeviceState.FDC_CALLING, () => !isOnRetrievingLastFuelSale) .PermitIf(Trigger.NozzleLifted_And_开机, LogicalDeviceState.FDC_AUTHORISED, () => true) .PermitIf(Trigger.AnyPumpMsgHaveNotReceivedForWhile, LogicalDeviceState.FDC_OFFLINE, () => true); this.stateless.Configure(LogicalDeviceState.FDC_CALLING) .OnEntryAsync(async () => { }) .OnEntryAsync(async () => { }) //.Ignore(Trigger.AnyPumpMsgReceived) .Ignore(Trigger.NozzleLifted_And_停机) .PermitIf(Trigger.NozzleReplaced_And_停机, LogicalDeviceState.FDC_READY, () => true) .PermitIf(Trigger.NozzleLifted_And_开机, LogicalDeviceState.FDC_AUTHORISED, () => true) .PermitIf(Trigger.AnyPumpMsgHaveNotReceivedForWhile, LogicalDeviceState.FDC_OFFLINE, () => true); this.nozzleFuelNumbersIsRunningTrigger = this.stateless.SetTriggerParameters(Trigger.NozzleFuelNumbersIsRunning); this.stateless.Configure(LogicalDeviceState.FDC_AUTHORISED) .OnEntryAsync(() => Task.Run(() => this.context.Outgoing.Write(new ReadFuelDataRequest() { Adrs = this.address })) ) .OnExitAsync(async () => { }) //.Ignore(Trigger.AnyPumpMsgReceived) .Ignore(Trigger.NozzleLifted_And_开机) .PermitIf(Trigger.NozzleFuelNumbersIsRunning, LogicalDeviceState.FDC_FUELLING, () => true) .PermitIf(Trigger.AnyPumpMsgHaveNotReceivedForWhile, LogicalDeviceState.FDC_OFFLINE, () => true); this.stateless.Configure(LogicalDeviceState.FDC_FUELLING) .OnEntryFromAsync(this.nozzleFuelNumbersIsRunningTrigger, async (arg) => { this.OnCurrentFuellingStatusChange?.Invoke(this, new FdcTransactionDoneEventArg(arg)); this.context.Outgoing.Write(new ReadFuelDataRequest() { Adrs = this.address }); //for detecting nozzle placed back. this.context.Outgoing.Write(new ReadPumpStateRequest() { Adrs = this.address }); }) .OnEntryAsync(async () => { }) .OnExitAsync(async () => { }) .PermitReentry(Trigger.NozzleFuelNumbersIsRunning) //.Ignore(Trigger.AnyPumpMsgReceived) .Ignore(Trigger.NozzleLifted_And_开机) .PermitIf(Trigger.NozzleReplaced_And_停机, LogicalDeviceState.FDC_READY, () => true) .PermitIf(Trigger.NozzleReplaced_And_开机, LogicalDeviceState.FDC_AUTHORISED, () => true) .PermitIf(Trigger.AnyPumpMsgHaveNotReceivedForWhile, LogicalDeviceState.FDC_OFFLINE, () => true); } catch (Exception exxx) { logger.LogError("Constructing LanTian_Pump_664_Or_886.StatePumpHanlder exceptioned: " + exxx); } } public void OnFdcServerInit(Dictionary parameters) { if (parameters.ContainsKey("LastPriceChange")) { // nozzle logical id:rawPrice var lastPriceChanges = parameters["LastPriceChange"] as Dictionary; foreach (var priceChange in lastPriceChanges) { logger.LogInformation("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.LogInformation("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; var timeWindowWithActivePollingOutgoing = this.context.Outgoing as TimeWindowWithActivePollingOutgoing; timeWindowWithActivePollingOutgoing.PollingMsgProducer = () => new ReadPumpStateRequest(); } public async Task Process(IContext context) { try { if (!isOnFdcServerInitCalled) return; this.lastLogicalDeviceStateReceivedTime = DateTime.Now; //await this.stateless.FireAsync(Trigger.AnyPumpMsgReceived); if (context.Incoming.Message is ReadPumpStateResponse readPumpStateResponse) { #region AcquireControl if pump authorize mode is FC_Authorize if (readPumpStateResponse.ControlState == ReadPumpStateResponse.ControlStateEnum.自控 && this.pumpAuthorizeMode == PumpAuthorizeModeEnum.FC_Authorize) { var acquireControlResponse = await this.context.Outgoing.WriteAsync( new AcquireControlRequest() { Adrs = this.address }, (request, response) => response is AcquireControlResponse rp && rp.Adrs == this.address, 3000).ConfigureAwait(false) as AcquireControlResponse; if (acquireControlResponse == null) { logger.LogInformation("Pump: " + this.pumpId + ", " + " AcquireControlResponse first time timedout, will send 2nd time..."); acquireControlResponse = await this.context.Outgoing.WriteAsync( new AcquireControlRequest() { Adrs = this.address }, (request, response) => response is AcquireControlResponse rp && rp.Adrs == this.address, 3000).ConfigureAwait(false) as AcquireControlResponse; if (acquireControlResponse == null) logger.LogInformation("Pump: " + this.pumpId + ", " + " AcquireControlResponse 2nd time timedout, will ignore this error, then the pump may have wrong authorize mode"); } } #endregion if (readPumpStateResponse.NozzleState == ReadPumpStateResponse.NozzleStateEnum.提枪) { if (readPumpStateResponse.StartOrStopState == ReadPumpStateResponse.StartOrStopStateEnum.开机) await this.stateless.FireAsync(Trigger.NozzleLifted_And_开机); else await this.stateless.FireAsync(Trigger.NozzleLifted_And_停机); } else { if (readPumpStateResponse.StartOrStopState == ReadPumpStateResponse.StartOrStopStateEnum.开机) await this.stateless.FireAsync(Trigger.NozzleReplaced_And_开机); else await this.stateless.FireAsync(Trigger.NozzleReplaced_And_停机); } } else if (context.Incoming.Message is ReadFuelDataResponse readFuelDataResponse) { //await this.stateless.FireAsync(Trigger.NozzleFuelNumbersIsRunning); await this.stateless.FireAsync(this.nozzleFuelNumbersIsRunningTrigger, new FdcTransaction() { // 只有一把枪 Nozzle = this.nozzles.First(), Amount = readFuelDataResponse.Amount, Volumn = readFuelDataResponse.Volume, Price = this.nozzles.First().RealPriceOnPhysicalPump ?? -1, Finished = false, }); } else if (context.Incoming.Message is ReadPriceResponse readPriceResponse) { if (!this.nozzles.First().RealPriceOnPhysicalPump.HasValue || this.nozzles.First().RealPriceOnPhysicalPump.Value != readPriceResponse.Price) logger.LogInformation("Pump " + this.pumpId + ", detected the pump side price changed from: " + (this.nozzles.First().RealPriceOnPhysicalPump ?? -1) + " to: " + readPriceResponse.Price + ", will updating to local"); this.nozzles.First().RealPriceOnPhysicalPump = readPriceResponse.Price; } } catch (Exception eee) { logger.LogInformation("Process(...) exceptioned: " + eee); } } /// /// no decimal point value. /// /// MoneyTotalizer:VolumnTotalizer public async Task> QueryTotalizerAsync(byte logicalNozzleId) { var readTotalizerResponse = await this.context.Outgoing.WriteAsync(new ReadTotalizerRequest() { Adrs = this.address }, (_, testResponse) => testResponse is ReadTotalizerResponse rp && rp.Adrs == this.address, 3000) as ReadTotalizerResponse; if (readTotalizerResponse == null) return new System.Tuple(-1, -1); return new System.Tuple((int)readTotalizerResponse.AmountTotalizer, (int)readTotalizerResponse.VolumeTotalizer); } public async Task SuspendFuellingAsync() { logger.LogDebug("Pump " + this.pumpId + ", SuspendFuellingAsync..."); var suspendFuelResponse = await this.context.Outgoing.WriteAsync(new SuspendFuelRequest() { Adrs = this.address }, (_, testResponse) => testResponse is SuspendFuelResponse rp && rp.Adrs == this.address, 3000) as SuspendFuelResponse; var result = suspendFuelResponse != null; logger.LogDebug("Pump " + this.pumpId + ", SuspendFuellingAsync result: " + result); return result; } public async Task ResumeFuellingAsync() { logger.LogDebug("Pump " + this.pumpId + ", ResumeFuellingAsync..."); var resumeFuelResponse = await this.context.Outgoing.WriteAsync(new ResumeFuelRequest() { Adrs = this.address }, (_, testResponse) => testResponse is ResumeFuelResponse rp && rp.Adrs == this.address, 3000) as ResumeFuelResponse; var result = resumeFuelResponse != null; logger.LogDebug("Pump " + this.pumpId + ", ResumeFuellingAsync result: " + result); return result; } public async Task ChangeFuelPriceAsync(int newPriceWithoutDecimalPoint, byte logicalNozzleId) { logger.LogInformation("Pump: " + this.pumpId + ", " + " ChangeFuelPriceAsync, new price: " + newPriceWithoutDecimalPoint); var readPriceResponse = await this.context.Outgoing.WriteAsync(new ReadPriceRequest() { Adrs = this.address }, (request, response) => response is ReadPriceResponse rp && rp.Adrs == this.address, 3000).ConfigureAwait(false) as ReadPriceResponse; if (readPriceResponse == null) { logger.LogInformation("Pump: " + this.pumpId + ", " + " Reading pump side price timedout, will ignore this error and won't send a ChangePriceRequest here but may need a manual Price change if find price discrepancy"); return false; } this.nozzles.First().RealPriceOnPhysicalPump = readPriceResponse.Price; if (readPriceResponse.Price != newPriceWithoutDecimalPoint) { //logger.LogInformation("Pump: " + this.pumpId + ", " // + " Read the pump side price(without decimal points): " + readPriceResponse.Price + " which NOT Equals with the price as FC expect, will start a change price internally..."); var changePriceResponse = await this.context.Outgoing.WriteAsync( new ChangePriceRequest(newPriceWithoutDecimalPoint, this.pumpModel == PumpModelEnum.Model_664 ? (byte)2 : (byte)3) { Adrs = this.address }, (request, response) => response is ChangePriceResponse rp && rp.Adrs == this.address, 3000).ConfigureAwait(false) as ChangePriceResponse; if (changePriceResponse == null || !changePriceResponse.Succeed) { logger.LogInformation("Pump: " + this.pumpId + ", " + " change price timedout or failed, may need a manual price change later if find price discrepancy"); return false; } var doubleConfirm_readPriceResponse = await this.context.Outgoing.WriteAsync(new ReadPriceRequest() { Adrs = this.address }, (request, response) => response is ReadPriceResponse rp && rp.Adrs == this.address, 3000).ConfigureAwait(false) as ReadPriceResponse; if (doubleConfirm_readPriceResponse == null) { logger.LogInformation("Pump: " + this.pumpId + ", " + " Double confirm change price timedout, so FC can't sure if the new price applied, or a manual Price change is needed if find price discrepancy"); return false; } else if (doubleConfirm_readPriceResponse.Price == newPriceWithoutDecimalPoint) { this.nozzles.First().RealPriceOnPhysicalPump = doubleConfirm_readPriceResponse.Price; logger.LogInformation("Pump: " + this.pumpId + ", " + " Double confirm succeed for new FC price(without decimal points): " + newPriceWithoutDecimalPoint + " have been applied to pump."); return true; } else { logger.LogInformation("Pump: " + this.pumpId + ", " + " Double confirm failed for new FC price(without decimal points): " + newPriceWithoutDecimalPoint + " failed applying to pump as pump side still report its price(without decimal point): " + doubleConfirm_readPriceResponse.Price + ", a manual Change Price is needed"); return false; } } else { logger.LogInformation("Pump: " + this.pumpId + ", " + " Read the pump side price(without decimal points): " + readPriceResponse.Price + ", no need to align with FC(FC has no expecting price)"); return true; } } public async Task AuthorizeAsync(byte logicalNozzleId) { logger.LogDebug("Pump " + this.pumpId + ", AuthorizeAsync..."); var openResponse = await this.context.Outgoing.WriteAsync(new OpenRequest() { Adrs = this.address }, (_, testResponse) => testResponse is OpenResponse rp && rp.Adrs == this.address, 3000) as OpenResponse; if (openResponse == null) { logger.LogInformation("Pump " + this.pumpId + ", AuthorizeAsync failed due to timedout"); return false; } logger.LogInformation("Pump " + this.pumpId + ", AuthorizeAsync result: " + openResponse.Succeed); return openResponse.Succeed; } public async Task UnAuthorizeAsync(byte logicalNozzleId) { return false; } public async Task AuthorizeWithAmountAsync(int moneyAmountWithoutDecimalPoint, byte logicalNozzleId) { logger.LogDebug("Pump " + this.pumpId + ", AuthorizeWithAmountAsync(amt: " + moneyAmountWithoutDecimalPoint + ")..."); var presetAmountResponse = await this.context.Outgoing.WriteAsync( new PresetAmountRequest(moneyAmountWithoutDecimalPoint, this.pumpModel == PumpModelEnum.Model_664 ? (byte)3 : (byte)4) { Adrs = this.address }, (_, testResponse) => testResponse is PresetAmountResponse rp && rp.Adrs == this.address, 3000) as PresetAmountResponse; if (presetAmountResponse == null) { logger.LogInformation("Pump " + this.pumpId + ", AuthorizeWithAmountAsync failed due to PresetAmountRequest timedout"); return false; } logger.LogInformation("Pump " + this.pumpId + ", AuthorizeWithAmountAsync PresetAmountRequest succeed, will AuthorizePump..."); return await this.AuthorizeAsync(logicalNozzleId); } public async Task AuthorizeWithVolumeAsync(int volumnWithoutDecimalPoint, byte logicalNozzleId) { logger.LogDebug("Pump " + this.pumpId + ", AuthorizeWithVolumeAsync(amt: " + volumnWithoutDecimalPoint + ")..."); var presetVolumeResponse = await this.context.Outgoing.WriteAsync( new PresetVolumeRequest(volumnWithoutDecimalPoint, this.pumpModel == PumpModelEnum.Model_664 ? (byte)3 : (byte)4) { Adrs = this.address }, (_, testResponse) => testResponse is PresetVolumeResponse rp && rp.Adrs == this.address, 3000) as PresetVolumeResponse; if (presetVolumeResponse == null) { logger.LogInformation("Pump " + this.pumpId + ", AuthorizeWithVolumeAsync failed due to PresetVolumeRequest timedout"); return false; } logger.LogInformation("Pump " + this.pumpId + ", AuthorizeWithVolumeAsync PresetVolumeRequest succeed, will AuthorizePump..."); return await this.AuthorizeAsync(logicalNozzleId); } public Task FuelingRoundUpByAmountAsync(int amount) { return Task.FromResult(false); } public Task FuelingRoundUpByVolumeAsync(int volume) { return Task.FromResult(false); } } }