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; 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; } #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}"))) { } public HengshanPayTermHandler( string pumpIds, string pumpSubAddresses, string pumpNozzles, string pumpSiteNozzleNos, string nozzleLogicIds) { this.pumpIds = pumpIds; this.pumpSubAddresses = pumpSubAddresses; this.pumpNozzles = pumpNozzles; this.pumpSiteNozzleNos = pumpSiteNozzleNos; this.nozzleLogicIds = nozzleLogicIds; this.MysqlDbContext = new MysqlDbContext(); 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 0x18: //添加或修改数据库订单 OrderFromMachine message = (OrderFromMachine)context.Incoming.Message; int row = UpLoadOrder(message); logger.Info($"receive order from machine,database had ${row} count change"); 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 void SendQRCode() { 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()); this.client?.Client.Send(sendBytes); } } /// /// 发送实付金额给油机 /// /// public 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); 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()); this.client?.Client.Send(sendBytes); } public void SetTcpClinet(TcpClient? tcpClient) { this.client = tcpClient; } /// /// 添加或修改订单 /// /// 接收到油机的订单信息 /// public int UpLoadOrder(OrderFromMachine order) { FccOrderInfo orderByMessage = order.ToComponent(); FccOrderInfo? fccOrderInfo = MysqlDbContext.fccOrderInfos.FirstOrDefault(fccOrder => fccOrder.NozzleNum == order.nozzleNum && fccOrder.Ttc == order.ttc); if (fccOrderInfo == null) { logger.Info($"receive order from machine,find order from database is null"); MysqlDbContext.fccOrderInfos.Add(orderByMessage); } else { logger.Info($"receive order from machine,padding data right now"); order.PaddingAuthorizationOrderData(fccOrderInfo); } return MysqlDbContext.SaveChanges(); } /// /// 传入有效数据,拼接为要发送给油机包 /// /// /// public byte[] content2data(byte[] content) { List list = new List(); //目标地址,源地址,帧号 byte[] head = new byte[] { 0xFF, 0xE0, 0x01 }; 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); ; } // 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; } } }