using HengshanPaymentTerminal.MessageEntity.Incoming; using HengshanPaymentTerminal.MessageEntity; using HengshanPaymentTerminal.Support; using HengshanPaymentTerminal; using System; using System.Collections.Concurrent; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Edge.Core.Processor.Dispatcher.Attributes; using Edge.Core.IndustryStandardInterface.Pump; using Edge.Core.IndustryStandardInterface.Pump.Fdc; using Edge.Core.Processor; using Edge.Core.Core.database; using Edge.Core.Domain.FccStationInfo.Output; using Edge.Core.Domain.FccNozzleInfo; using Edge.Core.Domain.FccNozzleInfo.Output; using System.Net.Sockets; using Edge.Core.Domain.FccOrderInfo; using Microsoft.EntityFrameworkCore; using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; using static Microsoft.AspNetCore.Hosting.Internal.HostingApplication; using HengshanPaymentTerminal.Mqtt.Request; using HengshanPaymentTerminal.Http; using HengshanPaymentTerminal.Http.Request; using System.Text.Json; using Newtonsoft.Json; using HengshanPaymentTerminal.Http.Response; namespace HengshanPaymentTerminal { /// /// Handler that communicates directly with the Hengshan Payment Terminal for card handling and pump handling via serial port. /// [MetaPartsDescriptor( "lang-zh-cn:恒山IC卡终端(UI板) App lang-en-us:Hengshan IC card terminal (UI Board)", "lang-zh-cn:用于与UI板通讯控制加油机" + "lang-en-us:Used for terminal communication to control pumps", new[] { "lang-zh-cn:恒山IC卡终端lang-en-us:HengshanICTerminal" })] public class HengshanPayTermHandler : IEnumerable, IDeviceHandler { #region Fields private string pumpIds; private string pumpSubAddresses; private string pumpNozzles; private string pumpSiteNozzleNos; private string nozzleLogicIds; private IContext _context; private List pumpHandlers = new List(); public Queue queue = new Queue(); public Queue commonQueue = new Queue(); private object syncObj = new object(); private ConcurrentDictionary statusDict = new ConcurrentDictionary(); public ConcurrentDictionary PumpStatusDict => statusDict; private Dictionary pumpIdSubAddressDict; public Dictionary> PumpNozzlesDict { get; private set; } public Dictionary NozzleLogicIdDict { get; private set; } public Dictionary> PumpSiteNozzleNoDict { get; private set; } public MysqlDbContext MysqlDbContext { get; private set; } public StationInfo stationInfo { get; set; } public List nozzleInfoList { get; private set; } public TcpClient? client { get; set; } private readonly ConcurrentDictionary> _tcsDictionary = new ConcurrentDictionary>(); private byte frame = 0x00; private object lockFrame = new object(); private readonly IHttpClientUtil httpClientUtil; //记录油枪状态,key-枪号,value:是否忙碌 private ConcurrentDictionary nozzleStatusDic = new ConcurrentDictionary(); #endregion #region Logger private static NLog.Logger logger = NLog.LogManager.LoadConfiguration("NLog.config").GetLogger("IPosPlusApp"); #endregion #region Constructor //private static List ResolveCtorMetaPartsConfigCompatibility(string incompatibleCtorParamsJsonStr) //{ // var jsonParams = JsonDocument.Parse(incompatibleCtorParamsJsonStr).RootElement.EnumerateArray().ToArray(); // //sample: "UITemplateVersion":"1.0" // string uiTemplateVersionRegex = @"(?<=""UITemplateVersion""\:\"").+?(?="")"; // var match = Regex.Match(jsonParams.First().GetRawText(), uiTemplateVersionRegex, RegexOptions.IgnoreCase | RegexOptions.Multiline); // if (match.Success) // { // var curVersion = match.Value; // if (curVersion == "1.0") // { // var existsAppConfigV1 = JsonSerializer.Deserialize(jsonParams.First().GetRawText(), typeof(HengshanPayTerminalHanlderGroupConfigV1)); // } // else // { // } // } // return null; //} [ParamsJsonSchemas("TermHandlerGroupCtorParamsJsonSchemas")] public HengshanPayTermHandler(HengshanPayTerminalHanlderGroupConfigV2 config) : this(config.PumpIds, string.Join(";", config.PumpSubAddresses.Select(m => $"{m.PumpId}={m.SubAddress}")), string.Join(";", config.PumpNozzleLogicIds.Select(m => $"{m.PumpId}={m.LogicIds}")), string.Join(";", config.PumpSiteNozzleNos.Select(m => $"{m.PumpId}={m.SiteNozzleNos}")), string.Join(";", config.NozzleLogicIds.Select(m => $"{m.NozzleNo}={m.LogicId}"))) //clientUtil) { } public HengshanPayTermHandler( string pumpIds, string pumpSubAddresses, string pumpNozzles, string pumpSiteNozzleNos, string nozzleLogicIds) //IHttpClientUtil clientUtil) { this.pumpIds = pumpIds; this.pumpSubAddresses = pumpSubAddresses; this.pumpNozzles = pumpNozzles; this.pumpSiteNozzleNos = pumpSiteNozzleNos; this.nozzleLogicIds = nozzleLogicIds; this.MysqlDbContext = new MysqlDbContext(); this.httpClientUtil = new HttpClientUtils(); GetInfo(); AssociatedPumpIds = GetPumpIdList(pumpIds); pumpIdSubAddressDict = InitializePumpSubAddressMapping(); PumpNozzlesDict = ParsePumpNozzlesList(pumpNozzles); PumpSiteNozzleNoDict = ParsePumpSiteNozzleNoList(pumpSiteNozzleNos); NozzleLogicIdDict = InitializeNozzleLogicIdMapping(nozzleLogicIds); InitializePumpHandlers(); } #endregion public void OnFdcServerInit(Dictionary parameters) { logger.Info("OnFdcServerInit called"); if (parameters.ContainsKey("LastPriceChange")) { // nozzle logical id:rawPrice var lastPriceChanges = parameters["LastPriceChange"] as Dictionary; foreach (var priceChange in lastPriceChanges) { } } } #region Event handler public event EventHandler OnTerminalMessageReceived; public event EventHandler OnTotalizerReceived; public event EventHandler OnFuelPriceChangeRequested; public event EventHandler OnTerminalFuelPriceDownloadRequested; public event EventHandler OnCheckCommandReceived; public event EventHandler OnLockUnlockCompleted; #endregion #region Properties public List AssociatedPumpIds { get; private set; } public IContext Context { get { return _context; } } public string PumpIdList => pumpIds; //public LockUnlockOperation LockUnlockOperationType { get; set; } = LockUnlockOperation.Undefined; #endregion #region Methods public int GetSubAddressForPump(int pumpId) { return pumpIdSubAddressDict.First(d => d.Key == pumpId).Value; } private List GetPumpIdList(string pumpIds) { var pumpIdList = new List(); if (!string.IsNullOrEmpty(pumpIds) && pumpIds.Contains(',')) //multiple pumps per serial port, Hengshan TQC pump { var arr = pumpIds.Split(','); foreach (var item in arr) { pumpIdList.Add(int.Parse(item)); } return pumpIdList; } else if (!string.IsNullOrEmpty(pumpIds) && pumpIds.Length == 1 || pumpIds.Length == 2) //only 1 pump per serial port, Hengshan pump { return new List { int.Parse(pumpIds) }; } else { throw new ArgumentException("Pump id list not specified!"); } } private Dictionary InitializePumpSubAddressMapping() { var dict = new Dictionary(); if (!string.IsNullOrEmpty(pumpSubAddresses)) { var sequence = pumpSubAddresses.Split(';') .Select(s => s.Split('=')) .Select(a => new { PumpId = int.Parse(a[0]), SubAddress = int.Parse(a[1]) }); foreach (var pair in sequence) { if (!dict.ContainsKey(pair.PumpId)) { dict.Add(pair.PumpId, pair.SubAddress); } } return dict; } else { throw new ArgumentException("Pump id and sub address mapping does not exist"); } } private Dictionary> ParsePumpNozzlesList(string pumpNozzles) { Dictionary> pumpNozzlesDict = new Dictionary>(); if (!string.IsNullOrEmpty(pumpNozzles) && pumpNozzles.Contains(';')) { var arr = pumpNozzles.Split(';'); foreach (var subMapping in arr) { var pair = new KeyValuePair(int.Parse(subMapping.Split('=')[0]), int.Parse(subMapping.Split('=')[1])); Console.WriteLine($"{pair.Key}, {pair.Value}"); if (!pumpNozzlesDict.ContainsKey(pair.Key)) { pumpNozzlesDict.Add(pair.Key, new List { pair.Value }); } else { List nozzlesForThisPump; pumpNozzlesDict.TryGetValue(pair.Key, out nozzlesForThisPump); if (nozzlesForThisPump != null && !nozzlesForThisPump.Contains(pair.Value)) { nozzlesForThisPump.Add(pair.Value); } } } } else if (!string.IsNullOrEmpty(pumpNozzles) && pumpNozzles.Count(c => c == '=') == 1) // only one pump per serial port { try { pumpNozzlesDict.Add( int.Parse(pumpNozzles.Split('=')[0]), new List { int.Parse(pumpNozzles.Split('=')[1]) }); } catch (Exception ex) { Console.WriteLine(ex); } } else { throw new ArgumentException("Wrong mapping between pump and its associated nozzles!"); } return pumpNozzlesDict; } static Dictionary> ParsePumpSiteNozzleNoList(string pumpSiteNozzleNos) { Dictionary> pumpSiteNozzleNoDict = new Dictionary>(); if (!string.IsNullOrEmpty(pumpSiteNozzleNos) && pumpSiteNozzleNos.Contains(';')) { var arr = pumpSiteNozzleNos.Split(';'); foreach (var subMapping in arr) { var pair = new KeyValuePair>( int.Parse(subMapping.Split('=')[0]), subMapping.Split('=')[1].Split(',').Select(a => int.Parse(a)).ToList()); Console.WriteLine($"{pair.Key}, {pair.Value}"); if (!pumpSiteNozzleNoDict.ContainsKey(pair.Key)) { pumpSiteNozzleNoDict.Add(pair.Key, pair.Value); } } } else if (!string.IsNullOrEmpty(pumpSiteNozzleNos) && pumpSiteNozzleNos.Count(c => c == '=') == 1) { try { string[] strArr = pumpSiteNozzleNos.Split('='); pumpSiteNozzleNoDict.Add( int.Parse(strArr[0]), new List { int.Parse(strArr[1]) }); } catch (Exception ex) { Console.WriteLine(ex); } } else { throw new ArgumentException("Wrong mapping between pump and its associated nozzles!"); } return pumpSiteNozzleNoDict; } private Dictionary InitializeNozzleLogicIdMapping(string nozzleLogicIds) { var dict = new Dictionary(); if (!string.IsNullOrEmpty(nozzleLogicIds)) { var sequence = nozzleLogicIds.Split(';') .Select(s => s.Split('=')) .Select(a => new { NozzleNo = int.Parse(a[0]), LogicId = int.Parse(a[1]) }); foreach (var pair in sequence) { if (!dict.ContainsKey(pair.NozzleNo)) { Console.WriteLine($"nozzle, logic id: {pair.NozzleNo} - {pair.LogicId}"); dict.Add(pair.NozzleNo, pair.LogicId); } } return dict; } else if (!string.IsNullOrEmpty(nozzleLogicIds) && nozzleLogicIds.Count(c => c == '=') == 1) { try { string[] sequence = nozzleLogicIds.Split('='); dict.Add(int.Parse(sequence[0]), int.Parse(sequence[1])); } catch (Exception ex) { Console.WriteLine(ex); } return dict; } else { throw new ArgumentException("Pump id and sub address mapping does not exist"); } } private void InitializePumpHandlers() { var pumpIdList = GetPumpIdList(pumpIds); foreach (var item in pumpIdList) { var nozzleList = GetNozzleListForPump(item); var siteNozzleNoList = PumpSiteNozzleNoDict[item]; HengshanPumpHandler pumpHandler = new HengshanPumpHandler(this, $"Pump_{item}", item, nozzleList, siteNozzleNoList); pumpHandler.OnFuelPriceChangeRequested += PumpHandler_OnFuelPriceChangeRequested; pumpHandlers.Add(pumpHandler); } } private List GetNozzleListForPump(int pumpId) { List nozzles; PumpNozzlesDict.TryGetValue(pumpId, out nozzles); return nozzles; } private void PumpHandler_OnFuelPriceChangeRequested(object sender, FuelPriceChangeRequestEventArgs e) { InfoLog($"Change price, Pump {e.PumpId}, Nozzle {e.NozzleId}, Price {e.Price}"); OnFuelPriceChangeRequested?.Invoke(sender, e); } IEnumerator IEnumerable.GetEnumerator() { return pumpHandlers.GetEnumerator(); } #endregion #region IHandler implementation public void Init(IContext context) { CommIdentity = context.Processor.Communicator.Identity; _context = context; } public string CommIdentity { get; private set; } public async Task Process(IContext context) { switch(context.Incoming.Message.Handle) { //心跳,带油枪状态信息 case 10: { //将油枪状态区分为空闲或非空闲,记录在内存。当状态有发生变化,发送到云端 HeartBeatMessage heartBeatMessage = (HeartBeatMessage)context.Incoming.Message; foreach(var nozzleState in heartBeatMessage.NozzleStatus) { bool isBusy = nozzleState.STATU != 0x03; if(nozzleStatusDic.TryGetValue(nozzleState.NozzleNum, out var value)) { if(isBusy == value) return; SendNozzleStatus(nozzleState,isBusy); } else { SendNozzleStatus(nozzleState,isBusy); } } break; } //订单 case 0x18: { //添加或修改数据库订单 OrderFromMachine orderFromMachine = (OrderFromMachine)context.Incoming.Message; FccOrderInfo fccOrderInfo = UpLoadOrder(orderFromMachine); logger.Info($"receive order from machine,database had change"); CreateTransaction(fccOrderInfo); break; } //普通应答 case 0x55: { CommonAnswerBack commonAnswerBack = (CommonAnswerBack)context.Incoming.Message; if (commonAnswerBack.Command == 0x63) //二维码回复 { byte[] keyBytes = { commonAnswerBack.Command, (byte)commonAnswerBack.NozzleNum }; var key = BitConverter.ToString(keyBytes).Replace("-", ""); if (_tcsDictionary.TryGetValue(key, out var value)) { value.SetResult(commonAnswerBack); } else { logger.Info($"qrcode response:can not get tcs for dictionary"); } } break; } // 授权回复 case 0x65: { AuthorizationResponse authorizationResponse = (AuthorizationResponse)context.Incoming.Message; byte[] keyBytes = { authorizationResponse.Handle, (byte)authorizationResponse.NozzleNum }; var key = BitConverter.ToString(keyBytes).Replace("-", ""); if (_tcsDictionary.TryGetValue(key, out var value)) { value.SetResult(authorizationResponse); } else { logger.Info($"authorization response:can not get tcs for dictionary"); } break; } // 取消授权回复 case 0x66: { UnAhorizationResponse unauthorizationResponse = (UnAhorizationResponse)context.Incoming.Message; byte[] keyBytes = { unauthorizationResponse.Handle, (byte)unauthorizationResponse.NozzleNum }; var key = BitConverter.ToString(keyBytes).Replace("-", ""); if (_tcsDictionary.TryGetValue(key, out var value)) { value.SetResult(unauthorizationResponse); } else { logger.Info($"unauthorization response:can not get tcs for dictionary"); } break; } } context.Outgoing.Write(context.Incoming.Message); } private void CheckStatus(CheckCmdRequest request) { if (!statusDict.ContainsKey(request.FuelingPoint.PumpNo)) { var result = statusDict.TryAdd(request.FuelingPoint.PumpNo, new PumpStateHolder { PumpNo = request.FuelingPoint.PumpNo, NozzleNo = 1, State = request, OperationType = LockUnlockOperation.None }); logger.Info($"Adding FuelingPoint {request.FuelingPoint.PumpNo} to dict"); if (!result) { statusDict.TryAdd(request.FuelingPoint.PumpNo, null); } } else { PumpStateHolder stateHolder = null; statusDict.TryGetValue(request.FuelingPoint.PumpNo, out stateHolder); if (stateHolder != null) { logger.Debug($"State holder, PumpNo: {stateHolder.PumpNo}, dispenser state: {stateHolder.State.DispenserState}, " + $"operation: {stateHolder.OperationType}"); } if (stateHolder != null && stateHolder.OperationType != LockUnlockOperation.None) { logger.Debug($"PumpNo: {request.FuelingPoint.PumpNo}, Last Dispenser State: {stateHolder.State.DispenserState}, " + $"Current Dispenser State: {request.DispenserState}"); if (stateHolder.State.DispenserState == 3 && request.DispenserState == 2) { //Pump is locked due to lock operation if (stateHolder.OperationType != LockUnlockOperation.None) { logger.Info("Locking done!"); stateHolder.State = request; //Update the state OnLockUnlockCompleted?.Invoke(this, new LockUnlockEventArgs(stateHolder.OperationType, true)); } } else if (stateHolder.State.DispenserState == 2 && request.DispenserState == 3) { //Pump is unlocked due to unlock operation if (stateHolder.OperationType != LockUnlockOperation.None) { logger.Info($"Unlocking done!"); stateHolder.State = request; //Update the state OnLockUnlockCompleted?.Invoke(this, new LockUnlockEventArgs(stateHolder.OperationType, true)); } } } else if (stateHolder != null && stateHolder.OperationType == LockUnlockOperation.None) { if (stateHolder.State.DispenserState != request.DispenserState) { logger.Warn($"Observed a pump state change, {stateHolder.State.DispenserState} -> {request.DispenserState}"); stateHolder.State = request; //Update the state. } } } } public void Write(CommonMessage cardMessage) { _context.Outgoing.Write(cardMessage); } public async Task WriteAsync(CommonMessage request, Func responseCapture, int timeout) { var resp = await _context.Outgoing.WriteAsync(request, responseCapture, timeout); return resp; } #endregion #region IEnumerable implementation public IEnumerator GetEnumerator() { return pumpHandlers.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return pumpHandlers.GetEnumerator(); } #endregion public void PendMessage(CardMessageBase message) { lock (syncObj) { queue.Enqueue(message); } } public bool TrySendNextMessage() { lock (syncObj) { if (queue.Count > 0) { DebugLog($"queue count: {queue.Count}"); var message = commonQueue.Dequeue(); Write(message); return true; } } return false; } public void StoreLatestFrameSqNo(int pumpId, byte frameSqNo) { var pump = GetPump(pumpId); if (pump != null) { pump.FrameSqNo = frameSqNo; } } public void UpdatePumpState(int pumpId, int logicId, LogicalDeviceState state) { var currentPump = GetPump(pumpId); currentPump?.FirePumpStateChange(state, Convert.ToByte(logicId)); } public void UpdateFuelingStatus(int pumpId, FdcTransaction fuelingTransaction) { var currentPump = GetPump(pumpId); currentPump?.FireFuelingStatusChange(fuelingTransaction); } private HengshanPumpHandler GetPump(int pumpId) { return pumpHandlers.FirstOrDefault(p => p.PumpId == pumpId); } public void SetRealPrice(int pumpId, int price) { var currentPump = GetPump(pumpId); var nozzle = currentPump?.Nozzles.FirstOrDefault(); if (nozzle != null) nozzle.RealPriceOnPhysicalPump = price; } #region Log methods private void InfoLog(string info) { logger.Info("PayTermHdlr " + info); } private void DebugLog(string debugMsg) { logger.Debug("PayTermHdlr " + debugMsg); } #endregion #region 二维码加油机相关方法 /// /// 获取站点信息 /// private void GetInfo() { Edge.Core.Domain.FccStationInfo.FccStationInfo? fccStationInfo = MysqlDbContext.FccStationInfos.FirstOrDefault(); if(fccStationInfo != null) stationInfo = new StationInfo(fccStationInfo); nozzleInfoList = MysqlDbContext.NozzleInfos.ToList().Select(n => new DetailsNozzleInfoOutput(n)).ToList(); } /// /// 发送二维码信息给油机 /// /// public async void SendQRCodeAsync() { string? smallProgram = stationInfo?.SmallProgram; if (smallProgram == null) { logger.Info($"can not get smallProgram link"); return; } System.Net.EndPoint? remoteEndPoint = this.client?.Client.RemoteEndPoint; if (remoteEndPoint == null) { logger.Info($"can not get client"); return; } string[] remoteAddr = remoteEndPoint.ToString().Split(":"); string ip = remoteAddr[0]; List nozzles = nozzleInfoList.FindAll(nozzle => nozzle.Ip == ip); foreach (var item in nozzles) { List list = new List(); byte[] commandAndNozzle = { 0x63, (byte)item.NozzleNum }; string qrCode = smallProgram + "/" + item.NozzleNum; byte[] qrCodeBytes = Encoding.ASCII.GetBytes(qrCode); list.AddRange(commandAndNozzle); list.Add((byte)qrCodeBytes.Length); list.AddRange(qrCodeBytes); byte[] sendBytes = content2data(list.ToArray(),null); await SendRequestToMachine("发送二维码", BitConverter.ToString(commandAndNozzle).Replace("-", ""), sendBytes); } } /// /// 发送实付金额给油机 /// /// public async void SendActuallyPaid(FccOrderInfo orderInfo) { List list = new List(); byte[] commandAndNozzle = { 0x19, (byte)orderInfo.NozzleNum }; byte[] ttcBytes = NumberToByteArrayWithPadding(orderInfo.Ttc, 4); byte[] amountPayableBytes = FormatDecimal(orderInfo.AmountPayable ?? orderInfo.Amount); list.AddRange(commandAndNozzle); //添加命令字和枪号 list.AddRange(ttcBytes); //添加流水号 list.Add(0x21); //由fcc推送实付金额表示该订单是二维码小程序支付的 list.AddRange(amountPayableBytes); //添加实付金额 //添加3位交易金额1,3位交易金额2,2位优惠规则代码,10位卡应用号,4位消息鉴别码 list.AddRange(new byte[] { 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00 }); byte[] sendBytes = content2data(list.ToArray(), null); await SendRequestToMachine("发送实付金额", BitConverter.ToString(commandAndNozzle).Replace("-", ""), sendBytes); } public async Task SendAuthorization(MqttAuthorizationRequest request) { List list = new List(); byte[] commandAndNozzle = { 0x65, (byte)request.NozzleNum }; byte[] authorizationTimeBytes = ConvertDateTimeToByteArray(request.AuthorizationTime); //将小数点后移两位,因为油机只支持两位小数点,这边传过去的3位字节转为int后取后两位为十分位和百分位 int value = (int)request.Value * 100; byte[] valueBytes = NumberToByteArrayWithPadding(value, 3); list.AddRange(commandAndNozzle); list.AddRange(authorizationTimeBytes); list.Add((byte)request.AuthorizationType); list.AddRange(valueBytes); byte[] sendBytes = content2data(list.ToArray(), null); return await SendRequestToMachine("发送授权请求", BitConverter.ToString(commandAndNozzle).Replace("-", ""), sendBytes); } public async Task SendUnAuthorizartion(MqttUnAhorizationRequest request) { List list = new List(); byte[] commandAndNozzle = { 0x66, (byte)request.NozzleNum }; byte[] authorizationTimeBytes = ConvertDateTimeToByteArray(request.AuthorizationTime); byte[] ttcBytes = NumberToByteArrayWithPadding(request.Ttc, 4); list.AddRange(commandAndNozzle); list.AddRange(authorizationTimeBytes); list.AddRange(ttcBytes); byte[] sendBytes = content2data(list.ToArray(), null); return await SendRequestToMachine("发送取消授权请求", BitConverter.ToString(commandAndNozzle).Replace("-", ""), sendBytes); } public void SetTcpClinet(TcpClient? tcpClient) { this.client = tcpClient; } /// /// 发送消息到油机,3秒的超时,重试三次 /// /// 发送的消息类型,用于日志记录 /// 发送的消息key,用于存储 TaskCompletionSource /// 实际发送消息 /// /// private async Task SendRequestToMachine(string sendTag,string sendKey, byte[] requestBytes) { int retryCount = 0; while(retryCount < 3) { var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); bool isAdd = _tcsDictionary.TryAdd(sendKey, new TaskCompletionSource()); logger.Info($"{sendTag}: add request {sendKey} to dic is {isAdd}"); client?.Client.Send(requestBytes); try { TaskCompletionSource? value; TaskCompletionSource tcs; if(_tcsDictionary.TryGetValue(sendKey, out value)) { tcs = value; } else { tcs = new TaskCompletionSource(); } CommonMessage response = await tcs.Task.WaitAsync(cts.Token); return response; } catch (OperationCanceledException) { retryCount++; logger.Info($"{sendTag}: time out,retrying... ({retryCount} / 3)"); } finally { if(retryCount >= 3) { logger.Info($"{sendTag}: is time out add retry 3 time"); _tcsDictionary.TryRemove(sendKey,out _); } } } return new ErrorMessage() { IsError = true, ErrorMessage = $"{sendTag}: can not receive response after 3 retries" }; } /// /// 添加或修改订单 /// /// 接收到油机的订单信息 /// public FccOrderInfo UpLoadOrder(OrderFromMachine order) { //接收到油机发送过来的订单信息 OrderFromMachine orderFromMachine = (OrderFromMachine)order; FccOrderInfo orderByMessage = orderFromMachine.ToComponent(); /** 根据枪号+流水号+授权时间来确定订单,因为冷启动后流水号会从头开始计算 * 后支付时直接将数据库直接插入 * 预支付时由于是云端先创建订单,发起授权响应成功后会插入数据库,响应成功时会回复授权时间,枪号,流水号 */ FccOrderInfo? fccOrderInfo = MysqlDbContext.fccOrderInfos .Where(order => order.NozzleNum == orderFromMachine.nozzleNum && order.Ttc == orderFromMachine.ttc && order.AuthorizationTime == orderFromMachine.dispenserTime) .FirstOrDefault(); if (fccOrderInfo == null) { logger.Info($"receive order from machine,find order from database is null"); MysqlDbContext.fccOrderInfos.Add(orderByMessage); MysqlDbContext.SaveChanges(); return orderByMessage; } else { logger.Info($"receive order from machine,padding data right now"); orderFromMachine.PaddingAuthorizationOrderData(fccOrderInfo); MysqlDbContext.SaveChanges(); return fccOrderInfo; } } private async void CreateTransaction(FccOrderInfo fccOrderInfo) { CreateTransaction createTransaction = new CreateTransaction(fccOrderInfo,stationInfo.SecretId); logger.Info($"create transaction,type is {createTransaction.type}"); HttpResponseMessage httpResponseMessage = await httpClientUtil.CreateTransaction(JsonConvert.SerializeObject(createTransaction)); Response? response = JsonConvert.DeserializeObject>(await httpResponseMessage.Content.ReadAsStringAsync()); logger.Info($"reveice create transaction response:{JsonConvert.SerializeObject(response)}"); // 后支付填充云端id if(response != null && createTransaction.type == 2) { FccOrderInfo? currentOrder = MysqlDbContext.fccOrderInfos .Where(order => order.NozzleNum == fccOrderInfo.NozzleNum && order.Ttc == fccOrderInfo.Ttc && order.AuthorizationTime == fccOrderInfo.AuthorizationTime) .FirstOrDefault(); if(currentOrder != null) { currentOrder.CloundOrderId = response.data; MysqlDbContext.SaveChanges(); } } } /// /// 发送油枪状态给云端 /// /// private async void SendNozzleStatus(HeartBeatNozzleState nozzleState,bool isBusy) { //保存变量 nozzleStatusDic[nozzleState.NozzleNum] = isBusy; //发送云端 SendNozzleStatu sendNozzleStatu = new SendNozzleStatu(nozzleState); logger.Info($"send nozzle state to cloud,{sendNozzleStatu.NozzleId}-{sendNozzleStatu.Status}"); HttpResponseMessage httpResponseMessage = await httpClientUtil.SendNozzleStatu(JsonConvert.SerializeObject(sendNozzleStatu)); Response? response = JsonConvert.DeserializeObject>(await httpResponseMessage.Content.ReadAsStringAsync()); logger.Info($"reveice send nozzle state response:{JsonConvert.SerializeObject(response)}"); } /// /// 传入有效数据,拼接为要发送给油机包 /// /// /// public byte[] content2data(byte[] content,byte? sendFrame) { List list = new List(); //目标地址,源地址,帧号 byte frameNo = 0x00; if(sendFrame == null) { lock (lockFrame) { if (frame == 0x3f) { frameNo = 0x00; } else { frameNo = (byte)(frame + 1); } } } else { frameNo = sendFrame.Value; } byte[] head = new byte[] { 0xFF, 0xE0, frameNo }; byte[] length = Int2BCD(content.Length); list.AddRange(head); list.AddRange(length); list.AddRange(content); byte[] crc = HengshanCRC16.ComputeChecksumToBytes(list.ToArray()); list.AddRange(crc); List addFAList = addFA(list); addFAList.Insert(0, 0xFA); return addFAList.ToArray(); } public int Bcd2Int(byte byte1, byte byte2) { // 提取第一个字节的高四位和低四位 int digit1 = (byte1 >> 4) & 0x0F; // 高四位 int digit2 = byte1 & 0x0F; // 低四位 // 提取第二个字节的高四位和低四位 int digit3 = (byte2 >> 4) & 0x0F; // 高四位 int digit4 = byte2 & 0x0F; // 低四位 // 组合成一个整数 int result = digit1 * 1000 + digit2 * 100 + digit3 * 10 + digit4; return result; } public byte[] Int2BCD(int number) { // 提取千位、百位、十位和个位 int thousands = number / 1000; int hundreds = (number / 100) % 10; int tens = (number / 10) % 10; int units = number % 10; // 将千位和百位组合成一个字节(千位在高四位,百位在低四位) byte firstByte = (byte)((thousands * 16) + hundreds); // 乘以16相当于左移4位 // 将十位和个位组合成一个字节(十位在高四位,个位在低四位) byte secondByte = (byte)((tens * 16) + units); // 返回结果数组 return new byte[] { firstByte, secondByte }; } public List addFA(List list) { List result = new List(); foreach (byte b in list) { if (b == 0xFA) { result.Add(0xFA); result.Add(0xFA); } else { result.Add(b); } } return result; } /// /// 将数值转为byte[] /// /// 数值 /// 数组长度,不够高位补0 /// /// public static byte[] NumberToByteArrayWithPadding(int value, int length) { if (length < 0) { throw new ArgumentException("Length must be non-negative."); } // 创建一个指定长度的字节数组 byte[] paddedBytes = new byte[length]; // 确保是大端序 for (int i = 0; i < length && i < 4; i++) { paddedBytes[length - 1 - i] = (byte)(value >> (i * 8)); } return paddedBytes; } public static byte[] FormatDecimal(decimal value) { // 四舍五入到两位小数 decimal roundedValue = Math.Round(value, 2, MidpointRounding.AwayFromZero); int valueInt = (int)(roundedValue * 100m); return NumberToByteArrayWithPadding(valueInt, 3); ; } /// /// 将时间转为 BCD /// /// /// public static byte[] ConvertDateTimeToByteArray(DateTime dateTime) { // 创建byte数组 byte[] result = new byte[7]; // 年份处理 int year = dateTime.Year; result[0] = (byte)((year / 1000) * 16 + (year / 100) % 10); // 千年和百年 result[1] = (byte)((year / 10) % 10 * 16 + year % 10); // 十年和个年 // 月、日、小时、分钟、秒直接转换为BCD result[2] = (byte)(dateTime.Month / 10 * 16 + dateTime.Month % 10); result[3] = (byte)(dateTime.Day / 10 * 16 + dateTime.Day % 10); result[4] = (byte)(dateTime.Hour / 10 * 16 + dateTime.Hour % 10); result[5] = (byte)(dateTime.Minute / 10 * 16 + dateTime.Minute % 10); result[6] = (byte)(dateTime.Second / 10 * 16 + dateTime.Second % 10); return result; } // CRC16 constants const ushort CRC_ORDER16 = 16; const ushort CRC_POLYNOM16 = 0x1021; const ushort CRC_CRCINIT16 = 0xFFFF; const ushort CRC_CRCXOR16 = 0x0000; const ushort CRC_MASK = 0xFFFF; const ushort CRC_HIGHEST_BIT = (ushort)(1 << (CRC_ORDER16 - 1)); const ushort TGT_CRC_DEFAULT_INIT = 0xFFFF; public static ushort Crc16(byte[] buffer, ushort length) { ushort crc_rc = TGT_CRC_DEFAULT_INIT; for (int i = 0; i < length; i++) { byte c = buffer[i]; for (ushort j = 0x80; j != 0; j >>= 1) { ushort crc_bit = (ushort)((crc_rc & CRC_HIGHEST_BIT) != 0 ? 1 : 0); crc_rc <<= 1; if ((c & j) != 0) { crc_bit = (ushort)((crc_bit == 0) ? 1 : 0); } if (crc_bit != 0) { crc_rc ^= CRC_POLYNOM16; } } } return (ushort)((crc_rc ^ CRC_CRCXOR16) & CRC_MASK); } #endregion } public class HengshanPayTerminalHanlderGroupConfigV1 { public string PumpIds { get; set; } public List PumpSubAddresses { get; set; } } public class HengshanPayTerminalHanlderGroupConfigV2 { public string PumpIds { get; set; } public List PumpSubAddresses { get; set; } public List PumpNozzleLogicIds { get; set; } public List PumpSiteNozzleNos { get; set; } public List NozzleLogicIds { get; set; } } public class PumpSubAddress { public byte PumpId { get; set; } public byte SubAddress { get; set; } } public class PumpNozzleLogicId { public byte PumpId { get; set; } public string LogicIds { get; set; } } public class PumpSiteNozzleNo { public byte PumpId { get; set; } public string SiteNozzleNos { get; set; } } public class NozzleLogicId { public byte NozzleNo { get; set; } public byte LogicId { get; set; } } }