using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Configuration; using System.IO.Ports; using System.Linq; using System.Text; using System.Threading; using log4net; using Timer = System.Timers.Timer; using Common; using System.Collections; using HengShan_Pump_NonIC.MessageEntity; using DeviceProcessor; using Wayne.FDCPOSLibrary; using Common.Config; using System.Xml; namespace HengShan_Pump_NonIC { public class PumpHandler : IFdcPumpController, IHandler { private object syncObject = new object(); private System.Timers.Timer polling; //static ILog fdcLogger = log4net.LogManager.GetLogger("FdcServer"); static ILog logger = log4net.LogManager.GetLogger("PumpHandler"); protected IContext context; public event EventHandler OnStateChange; /// /// fired on fueling process is on going, the fuel amount should keep changing. /// public event EventHandler OnCurrentFuellingStatusChange; protected LogicalDeviceState lastLogicalDeviceState = LogicalDeviceState.FDC_CLOSED; protected DateTime lastLogicalDeviceStateReceivedTime; // by seconds public const int lastLogicalDeviceStateExpiredTime = 6; private AutoResetEvent waitResponseBlocker = new AutoResetEvent(false); private int waitResponseBlockerTime = 11 * 1000; private StartResponse lastStartResponse; private SetFuelPriceResponse lastSetFuelPriceResponse; private GetAccumulateResponse lastGetAccumulateResponse; private AuthPumpWithAmountResponse lastAuthPumpWithAmountResponse; private AuthPumpWithGallonResponse lastAuthPumpWithGallonResponse; private AuthPumpWithKiloResponse lastAuthPumpWithKiloResponse; private RoundUpByAmountResponse lastRoundUpByAmountResponse; private Guid uniqueId = Guid.NewGuid(); private int pumpId = -1; protected int pollingInterval = 600; protected List nozzles = new List(); /// /// 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)); } /// /// 恒山非IC油机 /// /// /// public PumpHandler(int pumpId, string nozzlesXmlConfiguration) { 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, 1, 1, null)); } private void ResetAllCachedResponse() { this.lastStartResponse = null; this.lastSetFuelPriceResponse = null; this.lastGetAccumulateResponse = null; this.lastAuthPumpWithAmountResponse = null; this.lastAuthPumpWithGallonResponse = null; this.lastAuthPumpWithKiloResponse = null; this.lastRoundUpByAmountResponse = null; } protected bool isSafeForSend = false; public void Init(IContext context) { this.context = context; this.polling = new Timer(this.pollingInterval); this.polling.Elapsed += (_, __) => { lock (this.syncObject) { this.isSafeForSend = false; context.Outgoing.Write(new GetNozzleStatusRequest()); } }; this.polling.Start(); } public virtual void Process(IContext context) { this.context = context; this.ResetAllCachedResponse(); if (context.Incoming.Message is GetNozzleStatusResponse getNozzleStatusResponse) { this.isSafeForSend = true; this.lastLogicalDeviceStateReceivedTime = DateTime.Now; // put the price by reading the real price. this.nozzles.First().RealPriceOnPhysicalPump = getNozzleStatusResponse.单价; var latestStatus = getNozzleStatusResponse.GetPumpStatus(); if (latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.油枪打开) && latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.加油结束) && latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.不允许加油) && latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.电机关) && !latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.加油过程)) { logger.Debug("非加油状态下的提枪"); /* 非加油状态下的提枪 */ if (this.lastLogicalDeviceState == LogicalDeviceState.FDC_AUTHORISED) { //说明是未提枪时就已经auth成功了,所以还是保持发送 FDC_AUTHORISED 的状态给FdcClient。 logger.Debug(" 未提枪时就已经auth成功了"); } else { if (this.previousUnfinishedFuelingNozzleStatus != null) { // no data loss since getNozzleStatusResponse will still carry last trx data until an auth. logger.Info("Detected a fast put back and pull out nozzle case(action time < polling time cause nozzle place back not detected)," + "\r\n will fire the trx done event for earlier trx(no data loss)->\r\n " + "seqNo: " + getNozzleStatusResponse.流水号 + ", amount: " + getNozzleStatusResponse.加油金额 + ", volume: " + getNozzleStatusResponse.加油金额 + ", price: " + getNozzleStatusResponse.单价); logger.Info(" State switched to FDC_READY(simulate)"); this.lastLogicalDeviceState = LogicalDeviceState.FDC_READY; this.FireOnStateChangeEvent(LogicalDeviceState.FDC_READY); this.FireOnCurrentFuellingStatusChangeEvent(new FdcTransaction() { // 恒山油机只有一把枪 Nozzle = this.nozzles.First(), Amount = getNozzleStatusResponse.加油金额, Volumn = getNozzleStatusResponse.加油量, Price = getNozzleStatusResponse.单价, SequenceNumberGeneratedOnPhysicalPump = getNozzleStatusResponse.流水号, Finished = true, }); } if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_CALLING) { logger.Debug(" State switched to FDC_CALLING"); //直接提枪了 this.lastLogicalDeviceState = LogicalDeviceState.FDC_CALLING; var safe = this.OnStateChange; safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_CALLING, this.nozzles.First())); } } } else if (latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.允许加油) && latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.油枪打开) && (latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.加油过程) || latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.电机打开))) { //status code: B1 logger.Debug("正处于加油状态下"); /* 正处于加油状态下 */ this.previousUnfinishedFuelingNozzleStatus = getNozzleStatusResponse; if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_FUELLING) { logger.Debug(" State switched to FDC_FUELLING"); this.lastLogicalDeviceState = LogicalDeviceState.FDC_FUELLING; var safe0 = this.OnStateChange; safe0?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_FUELLING, this.nozzles.First())); } //fire fuelling progress. var safe1 = this.OnCurrentFuellingStatusChange; safe1?.Invoke(this, new FdcTransactionDoneEventArg(new FdcTransaction() { // 恒山油机只有一把枪 Nozzle = this.nozzles.First(), Amount = getNozzleStatusResponse.加油金额, Volumn = getNozzleStatusResponse.加油量, Price = getNozzleStatusResponse.单价, SequenceNumberGeneratedOnPhysicalPump = getNozzleStatusResponse.流水号, Finished = false, })); } else if (latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.加油结束)) { /* 油机首次上电也会进入此处,并不断发送上一次的加油记录,这种情况下的交易记录并无法判断其之前是否已经发送至系统(并存入数据库), 所以继续往系统里送入,由系统判断重复情况。 而其它正常加油过程中的交易记录仅在油机状态变化时才送入系统。*/ // status code: 40 logger.Debug("收到状态: 加油结束"); this.previousUnfinishedFuelingNozzleStatus = null; if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_READY) { logger.Debug(" State switched to FDC_READY"); lastLogicalDeviceState = LogicalDeviceState.FDC_READY; var safe0 = this.OnStateChange; safe0?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_READY, null)); if (getNozzleStatusResponse.加油量 != 0 || getNozzleStatusResponse.加油金额 != 0) { var safe1 = this.OnCurrentFuellingStatusChange; safe1?.Invoke(this, new FdcTransactionDoneEventArg(new FdcTransaction() { // 恒山油机 一个加油点只有一把枪 Nozzle = this.nozzles.First(), Amount = getNozzleStatusResponse.加油金额, Volumn = getNozzleStatusResponse.加油量, Price = getNozzleStatusResponse.单价, SequenceNumberGeneratedOnPhysicalPump = getNozzleStatusResponse.流水号, Finished = true, })); } } } } else if (context.Incoming.Message is GetAccumulateResponse) { logger.Info(" incoming GetAccumulateResponse, will Set() a signal"); var _ = context.Incoming.Message as GetAccumulateResponse; this.lastGetAccumulateResponse = _; this.waitResponseBlocker.Set(); } else if (context.Incoming.Message is SetFuelPriceResponse) { logger.Info(" incoming SetFuelPriceResponse, will Set() a signal"); var _ = context.Incoming.Message as SetFuelPriceResponse; this.lastSetFuelPriceResponse = _; this.waitResponseBlocker.Set(); } else if (context.Incoming.Message is StartResponse) { logger.Info(" incoming StartResponse, will Set() a signal"); var _ = context.Incoming.Message as StartResponse; this.lastStartResponse = _; this.waitResponseBlocker.Set(); //if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_AUTHORISED) //{ // // switch state // this.lastLogicalDeviceState = LogicalDeviceState.FDC_AUTHORISED; // var safe = this.OnStateChange; // safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_AUTHORISED)); //} } else if (context.Incoming.Message is AuthPumpWithAmountResponse) { logger.Info(" incoming AuthPumpWithAmountResponse, will Set() a signal"); var _ = context.Incoming.Message as AuthPumpWithAmountResponse; this.lastAuthPumpWithAmountResponse = _; this.waitResponseBlocker.Set(); } else if (context.Incoming.Message is AuthPumpWithGallonResponse) { logger.Info(" incoming AuthPumpWithGallonResponse, will Set() a signal"); var _ = context.Incoming.Message as AuthPumpWithGallonResponse; this.lastAuthPumpWithGallonResponse = _; this.waitResponseBlocker.Set(); } else if (context.Incoming.Message is AuthPumpWithKiloResponse) { logger.Info(" incoming AuthPumpWithKiloResponse, will Set() a signal"); var _ = context.Incoming.Message as AuthPumpWithKiloResponse; this.lastAuthPumpWithKiloResponse = _; this.waitResponseBlocker.Set(); } else if (context.Incoming.Message is RoundUpByAmountResponse) { logger.Info(" incoming RoundUpByAmountResponse, will Set() a signal"); var _ = context.Incoming.Message as RoundUpByAmountResponse; this.lastRoundUpByAmountResponse = _; this.waitResponseBlocker.Set(); } } public void Dispose() { this.polling.Stop(); } 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 => 2; public int VolumeDecimalDigits => 2; public int PriceDecimalDigits => 2; public int VolumeTotalizerDecimalDigits => 2; public virtual LogicalDeviceState QueryStatus() { // if last state is expired, we return a OFFLINE here to FdcClient. if (DateTime.Now.Subtract(this.lastLogicalDeviceStateReceivedTime).TotalSeconds > lastLogicalDeviceStateExpiredTime) { if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_OFFLINE) { this.lastLogicalDeviceState = LogicalDeviceState.FDC_OFFLINE; logger.Info(" State switched to FDC_OFFLINE due to cached state expired"); var safe0 = this.OnStateChange; safe0?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_OFFLINE, null)); } return LogicalDeviceState.FDC_OFFLINE; } return this.lastLogicalDeviceState; } private const int safeWaitMaxWaitTimes = 10; protected bool SafeWait() { int waitedTimes = 0; while (!this.isSafeForSend) { waitedTimes++; if (waitedTimes >= safeWaitMaxWaitTimes) { logger.Info("safe wait failed!"); return false; } Thread.Sleep(300); } return true; } /// /// /// /// MoneyTotalizer:VolumnTotalizer public Tuple QueryTotalizer(byte logicalNozzleId) { lock (this.syncObject) { if (this.lastLogicalDeviceState == LogicalDeviceState.FDC_CLOSED || this.lastLogicalDeviceState == LogicalDeviceState.FDC_OFFLINE) return new Tuple(-1, -1); if (!this.SafeWait()) return new Tuple(-2, -2); this.context.Outgoing.Write(new GetAccumulateRequest()); logger.Info(" QueryTotalizer waitOne"); var got = this.waitResponseBlocker.WaitOne(this.waitResponseBlockerTime); if (got) { if (this.lastGetAccumulateResponse == null) { logger.Info("lastGetAccumulateResponse is Null"); return new Tuple(-1, -1); } else return new Tuple(this.lastGetAccumulateResponse.金额累计, this.lastGetAccumulateResponse.升累计); } logger.Info("QueryTotalizer timed out"); return new Tuple(-1, -1); } } public virtual bool ChangeFuelPrice(int newPriceWithoutDecimalPoint, byte logicalNozzleId) { //fdcLogger.Info(this.GetType().Name + "with id: " + this.pumpId + " is trying to change fuel price to: " + newPrice); /* this type of pump only have 1 nozzle, so logicalNozzleId is meaningless */ lock (this.syncObject) { if (this.lastLogicalDeviceState == LogicalDeviceState.FDC_CLOSED || this.lastLogicalDeviceState == LogicalDeviceState.FDC_OFFLINE) return false; if (!this.SafeWait()) return false; this.context.Outgoing.Write(new SetFuelPriceRequest() { FuelPrice = newPriceWithoutDecimalPoint }); logger.Info(" ChangeFuelPrice waitOne"); var got = this.waitResponseBlocker.WaitOne(this.waitResponseBlockerTime); if (got) { if (this.lastSetFuelPriceResponse == null) { logger.Info("lastSetFuelPriceResponse is Null"); return false; } else if (this.lastSetFuelPriceResponse.EnumResult != NonICMessageTemplateBase.Result.成功) { logger.Info("lastSetFuelPriceResponse is 失败"); return false; } return true; } return false; } } /// /// /// /// useless for this type of pump, it always one pump one nozzle /// public virtual bool Authorize(byte logicalNozzleId) { lock (this.syncObject) { if (!this.SafeWait()) return false; this.context.Outgoing.Write(new StartRequest()); logger.Info(" Authorize waitOne"); var got = this.waitResponseBlocker.WaitOne(this.waitResponseBlockerTime); if (got) { if (this.lastStartResponse == null) { logger.Info("lastStartResponse is Null"); return false; } else if (this.lastStartResponse.EnumResult != NonICMessageTemplateBase.Result.成功) { logger.Info("lastStartResponse is 失败"); return false; } else { if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_AUTHORISED) { this.lastLogicalDeviceState = LogicalDeviceState.FDC_AUTHORISED; var safe = this.OnStateChange; safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_AUTHORISED, this.nozzles.First())); } return true; } } return false; } } /// /// /// /// /// useless for this type of pump, it always one pump one nozzle /// public virtual bool AuthorizeWithAmount(int moneyAmountWithoutDecimalPoint, byte logicalNozzleId) { lock (this.syncObject) { if (!this.SafeWait()) return false; this.context.Outgoing.Write(new AuthPumpWithAmountRequest() { Amount = moneyAmountWithoutDecimalPoint }); logger.Info(" Authorize(amount) waitOne"); var got = this.waitResponseBlocker.WaitOne(this.waitResponseBlockerTime); if (got) { if (this.lastAuthPumpWithAmountResponse == null) { logger.Info("lastAuthPumpWithAmountResponse is Null"); return false; } else if (this.lastAuthPumpWithAmountResponse.EnumResult != NonICMessageTemplateBase.Result.成功) { logger.Info("lastAuthPumpWithAmountResponse is 失败"); return false; } else { if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_AUTHORISED) { lastLogicalDeviceState = LogicalDeviceState.FDC_AUTHORISED; var safe = this.OnStateChange; safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_AUTHORISED, this.nozzles.First())); } Thread.Sleep(500); return this.Authorize(logicalNozzleId); }; } else return false; } } /// /// /// /// /// useless for this type of pump, it always one pump one nozzle /// public virtual bool AuthorizeWithVolumn(int volumnWithoutDecimalPoint, byte logicalNozzleId) { lock (this.syncObject) { if (!this.SafeWait()) return false; this.context.Outgoing.Write(new AuthPumpWithGallonRequest() { Gallon = volumnWithoutDecimalPoint }); logger.Info(" Authorize(vol) waitOne"); var got = this.waitResponseBlocker.WaitOne(this.waitResponseBlockerTime); if (got) { if (this.lastAuthPumpWithGallonResponse == null) { logger.Info("lastAuthPumpWithGallonResponse is Null"); return false; } else if (this.lastAuthPumpWithGallonResponse.EnumResult != NonICMessageTemplateBase.Result.成功) { logger.Info("lastAuthPumpWithGallonResponse is 失败"); return false; } else { if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_AUTHORISED) { lastLogicalDeviceState = LogicalDeviceState.FDC_AUTHORISED; var safe = this.OnStateChange; safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_AUTHORISED, this.nozzles.First())); } Thread.Sleep(500); return this.Authorize(logicalNozzleId); } } return false; } } public virtual bool FuelingRoundUpByAmount() { lock (this.syncObject) { if (!this.SafeWait()) return false; this.context.Outgoing.Write(new RoundUpByAmountRequest()); logger.Info(" FuelingRoundUpByAmount(vol) waitOne"); var got = this.waitResponseBlocker.WaitOne(this.waitResponseBlockerTime); if (got) { if (this.lastRoundUpByAmountResponse == null) { logger.Info("lastRoundUpByAmountResponse is Null"); return false; } else if (this.lastRoundUpByAmountResponse.EnumResult != NonICMessageTemplateBase.Result.成功) { logger.Info("lastRoundUpByAmountResponse is 失败"); return false; } return true; } return false; } } #region not implemented public bool TerminateFuelling() { throw new NotImplementedException(); } public bool SuspendFuelling() { throw new NotImplementedException(); } public bool ResumeFuelling() { throw new NotImplementedException(); } public bool FuelingRoundUpByVolumn() { throw new NotImplementedException(); } public void OnFdcServerInit(Dictionary parameters) { //throw new NotImplementedException(); } #endregion } }