using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using Xylem.Common.Utils.Logging; using NLog; using Xylem.Common.Hardware.Interfaces.Ports.PortCore.EventArguments; using Xylem.Common.Hardware.Interfaces.Protocols.ProtocolCore; using Xylem.Common.Hardware.Interfaces.Protocols.ProtocolCore.EventArguments; using Xylem.Common.Hardware.WaterMeter.Genesis.DataPackages.EventArguments; using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisConfig; using Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.RequestProtocol.Consts; using Xylem.Common.Hardware.WaterMeter.Genesis.Registers; namespace Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.RequestProtocol { /// /// a bidirectional protocol support read and write genesis meter registers /// needed for /// public class RequestProtocol : BaseProtocol { //All read and write data sets (payload size) are organized in 4 byte chunks //Format of RequestProtocol read data //Request CMD | Register LSB | Register MSB | 0x00 | 0x00 | 0x00 | 0x00 //Response CMD | ErrCode Reason | ErrCode Base | DATA0 | DATA1 | DATA2 | DATA3 //Format of RequestProtocol write data //Request CMD | Register LSB | Register MSB | DATA0 | DATA1 | DATA2 | DATA3 //Response CMD | ErrCode Reason | ErrCode Base | UNDEF0 | UNDEF1 | UNDEF2 | UNDEF3 //Format of RequestProtocol multiple read data //Request CMD | Register LSB | Register MSB | RequestChunks LSB | RequestChunks MSB //Response CMD | ErrCode Reason | ErrCode Base | ErrPosition LSB | ErrPosition MSB | //DATA0 | DATA1 | DATA2 | DATA3.... (repeat for number of chunks) //Format of RequestProtocol multiple read data //Request CMD | Register LSB | Register MSB | WriteChunks LSB | WriteChunks MSB | //DATA0 | DATA1 | DATA2 | DATA3.... (repeat for number of chunks) //Response CMD | ErrCode Reason | ErrCode Base | ErrPosition LSB | ErrPosition MSB //Indexes of the request protocol read/write data //private const Int32 RequestCommandIndex = 0; //private const Int32 RequestRegisterIndex = 1; //private const Int32 RequestDataIndex = 3; //Indexes for response protocol read/write data private const Int32 ResponseCommandIndex = 0; private const Int32 ResponseErrorCodeIndex = 1; private const Int32 ResponseDataIndex = 3; //Indexes of the request protocol multiple read/write data //private const Int32 RequestChunksIndex = 3; //private const Int32 RequestMultiDataIndex = 5; //Indexes of the response protocol multiple read/write data private const Int32 ResponseMultiErrorPosition = 3; private const Int32 ResponseMultiDataIndex = 5; //Indexes of error code private const Int32 ErrorCodeReasonIndex = 0; private const Int32 ErrorCodeBaseIndex = 1; //Multiple response header size is 1 byte command 2 bytes error code and 2 bytes error position private const Int32 ResponseMultiHeaderSize = 5; //fill byte private const Byte FillByte = 0x00; private readonly ILogger _logger; // wakeup from register (water meter) to adapter private static readonly Byte[] WakeupMessage = { 0x00, 0xFF, 0xFF }; /// /// FIFO of records to be send next /// private readonly ConcurrentQueue _recordsSendFifo = new ConcurrentQueue(); /// /// This is the actual record in the send loop /// private RequestRecord _recordInProcess; /// /// Last communication time for session refresh /// public DateTimeOffset? LastCommTime; /// public override event EventHandler OnRecordReadyToSend; /// public override event EventHandler OnRecordIsDecoded; /// /// Occurs when a meter Response for write password is good /// public event EventHandler OnAuthorizationGrant; /// /// Event after a register entries changed /// public event EventHandler OnMeterRegisterUpdated; /// /// User adjustable additional retry timeout. This is 0 ms for standard operation. /// public Int32 AdditionalRetryTimeoutMs; private Int32 _maxTimeOutMs; private readonly String _ident; /// public RequestProtocol(String ident) : base(ident) { _ident = ident; _logger = NLogHelper.CreateOrGetMultiLogger(ident, "", "RequestProtocol", "MeterBase", "MeterBase"); } /// /// Process all records needed to be sent, this routine has to be called /// to kick-off the communication of all RequestRecords saved to the send /// FIFO /// /// /// - Initial /// /// /// - Logging of raw RequestProtocol at time of sending, /// - Logging of retries. /// /// /// - Added timeout from transmit protocol. /// /// /// - Added reorder FIFO to bring login at first position /// /// /// - Hide data in logging for e.g. passwords /// /// /// - Retry counter reset if authorization required and this record will be put back into FIFO, /// - Retry delay corrected for all errors. /// /// /// - Dynamic retry delay: ResponseTimeoutMs * Retry counter. /// /// /// - Skip retries on specific error mask to speed up communication on functional errors /// or informational feed backs (e.g FW not installed 0x0004) /// /// /// /// - User adjustable additional retry timeout. /// /// /// - Response timeout message output, /// - Initialize acknowledge code before communication to NoResponse. /// /// /// - Response timeout deviated from system time. /// /// /// - Avoid enqueue of _recordInProcess if retry counter is 0. /// /// /// - Additional DEBUG information included about FIFO and loops. /// /// /// - Command not assigned response for recordInProcess == null. /// /// /// - Initial request acknowledge state changed from NotDecoded to NoResponse. /// public RequestAcknowledgeState ProcessRecordList() { _logger.Trace($"{_ident} Entry to ProcessRecordList, FIFO contains ({_recordsSendFifo.Count}) records"); //The inter record send delay has to be hold before trying to communicate again const Int32 interRecordSendDelayMs = CommunicationConfig.InterRecordSendDelayMs; //remind counter for FIFO and therefore execution loops var loopCounter = 0; //check if FIFO is empty and get the actual record to process out of it while (_recordsSendFifo.TryDequeue(out var internalRecord)) { // Starting with record number 1 loopCounter++; _logger.Trace($"{_ident} Record({loopCounter}) - Dequeued from FIFO"); //pointer to new record _recordInProcess = internalRecord; if (_recordInProcess == null) return RequestAcknowledgeState.CommandNotAssigned; do { //mark record as answer outstanding _recordInProcess.Acknowledge = RequestAcknowledgeState.NoResponse; _logger.Trace($"{_ident} Record({loopCounter}) - Processing"); //log retries if (_recordInProcess.RetryCtr > 0) { _logger.Debug($"{_ident} Retry({_recordInProcess.RetryCtr})"); _logger.Trace($"{_ident} Record({loopCounter}) - Retry({_recordInProcess.RetryCtr})"); } //log request protocol content _logger.Debug(_recordInProcess.HideDataInLog ? $"{_ident} SentData(*****)" : $"{_ident} SentData({BitConverter.ToString(_recordInProcess.RequestProtocolData.ToArray())})"); //remind time for keep-session-active test to deny automatic logout of meter LastCommTime = DateTimeOffset.UtcNow; //set timeout for one communication trial adding the transmit protocol specific timeout and //increase the timeout with each retry _maxTimeOutMs = _recordInProcess.ResponseTimeoutMs + AdditionalRetryTimeoutMs + CommunicationConfig.ResponseTimeoutMs * (_recordInProcess.RetryCtr + 1); //start initial communication or retry OnRecordReadyToSend?.Invoke(this, new ListBytePortDataEventArgs(_recordInProcess.EncodedRequestData)); //time reminder of request record var requestTimeUtc = DateTimeOffset.UtcNow; Int32 actualResponseWaitTimeMs; //wait for communication acknowledge or until timeout, this also handles the inter record send delay do { //minimum delay is the inter record send delay Thread.Sleep(interRecordSendDelayMs); var actualTimeUtc = DateTimeOffset.UtcNow; //actual time difference from request to now var timeSpan = actualTimeUtc - requestTimeUtc; //avoid total milliseconds below zero at time overflow if (timeSpan.TotalMilliseconds < 0) { requestTimeUtc = DateTimeOffset.UtcNow; } actualResponseWaitTimeMs = (Int32)timeSpan.TotalMilliseconds; } while (_recordInProcess.Acknowledge != RequestAcknowledgeState.Ok && _recordInProcess.SkipRetryErrorCode != _recordInProcess.ResponseErrorCode && _maxTimeOutMs > actualResponseWaitTimeMs); //if timeout value reaches zero, the response hasn't been received or the delay until //next communication needed to be hold if (_maxTimeOutMs <= actualResponseWaitTimeMs) { _logger.Error(_recordInProcess.Acknowledge == RequestAcknowledgeState.NoResponse ? $"{_ident} Response timeout({actualResponseWaitTimeMs}ms)" : $"{_ident} Communication delay({actualResponseWaitTimeMs}ms)"); } //skip loop to handle re-authorization if (_recordInProcess.Acknowledge != RequestAcknowledgeState.AuthorizationRequired) continue; //reset retry counter for this record, it will be dispatched after re-authorization _recordInProcess.RetryCtr = 0; if (CommunicationConfig.RequestRetries > 0) { //add the recordInProcess to the top of the FIFO _recordsSendFifo.Enqueue(_recordInProcess); } //call login return _recordInProcess.Acknowledge; } while (_recordInProcess.Acknowledge != RequestAcknowledgeState.Ok && _recordInProcess.RetryCtr++ < CommunicationConfig.RequestRetries && _recordInProcess.SkipRetryErrorCode != _recordInProcess.ResponseErrorCode); } return _recordInProcess.Acknowledge; } /// /// Assemble record with transmit protocol and put it to record-send-FIFO, /// backup the ready-to-send, which is the dataEncodedWithTransmitProtocol, /// to the RequestRecord object for sending including the retry capability. /// /// Data package with all details like CRC, etc /// Command identifier /// which is intend /// hiding data in log file to avoid spying of passwords /// error mask to skip retries /// the assembled record for the send FIFO /// /// - Initial /// /// /// - Logging of raw data (RequestProtocol) moved to ProcessRecordList. /// /// /// - Added timeout from transmit protocol. /// /// /// - Hide data in logging for e.g. passwords /// /// /// - Skip retries on specific error mask to speed up communication on functional errors /// or informational feed backs (e.g FW not installed 0x0004) /// /// /// /// - Initial skip retry error code set to 0x0004 (e.g FW not installed 0x0004). /// /// /// - Hide data in log forwarded to DecodeDateForPhysicalLayer to hide passwords in log files. /// private RequestRecord AddRecordToSendFifo(Byte cmd, Byte[] payload, RegisterDefinition register = null, Boolean hideDataInLog = false, UInt16 skipRetryErrorCode = CommunicationConfig.SkipRetryErrorCode) { var requestProtocolData = new List { cmd }; requestProtocolData.AddRange(payload); //encode request protocol data with transmit protocol var transmit = GetTransmitProtocol(); var dataEncodedWithTransmitProtocol = transmit.DecodeDataForPhysicalLayer(_ident, cmd, payload, hideDataInLog); //remind port specific response timeout var transmitPortSettings = transmit.GetTransmitPortSettings(); var recordForSendFifo = new RequestRecord(cmd, requestProtocolData, dataEncodedWithTransmitProtocol, transmitPortSettings.ResponseTimeoutMs, register, hideDataInLog, skipRetryErrorCode); _recordsSendFifo.Enqueue(recordForSendFifo); return recordForSendFifo; } /// /// Record dispatcher to send FIFO of UI1236 command /// /// /// - Initial /// /// string for logging of slot /// Data package with all details like CRC, etc /// which is intend /// hiding data in log file to avoid spying of passwords /// error mask to skip retries /// the assembled record for the send FIFO // ReSharper disable once InconsistentNaming UI1236 is a naming forced by the caller public RequestRecord AddRecordToSendFifoUI1236(String ident, Byte[] payload, RegisterDefinition register = null, Boolean hideDataInLog = false, UInt16 skipRetryErrorCode = CommunicationConfig.SkipRetryErrorCode) { const Byte maxValue = byte.MaxValue; var requestProtocolData = new List { maxValue }; requestProtocolData.AddRange(payload); var transmitProtocol = GetTransmitProtocol(); var encodedRequestData = transmitProtocol.DecodeDataForPhysicalLayerUI1236(ident, payload, hideDataInLog); var transmitPortSettings = transmitProtocol.GetTransmitPortSettings(); var sendFifoUi1236 = new RequestRecord(maxValue, requestProtocolData, encodedRequestData, transmitPortSettings.ResponseTimeoutMs, register, hideDataInLog, skipRetryErrorCode); _recordsSendFifo.Enqueue(sendFifoUi1236); return sendFifoUi1236; } /// /// Called after successful response. /// Decode and check record, handle Errors and dispatch result. /// Invokes if somebody is listening /// /// /// - Initial /// /// /// - First part reworked to extract the response protocol information /// /// /// - Hide data in logging for e.g. passwords /// /// /// - Error code extraction changed for /// /// /// - Wakeup-message will avoid further timeout (during FW update this is in the range /// of 15000ms). /// - Allow one additional retry on decoding error, which is often the wakeup-message. /// /// /// - Send time reminder for timeout time calculation as output in log-file, /// - OnRecordIsDecoded?.Invoke moved before return to assure that the Acknowledge status is set. /// /// /// - Avoid activation of wakeup retry if record is meanwhile acknowledged. /// /// /// - Wakeup message handling changed, /// - Multiple replies on wakeup message allowed. /// /// /// - On wakeup message 5 retires are allowed to avoid an infinite loop. /// /// /// - On wakeup message exit this routine. /// /// /// - Early exit on _recordInProcess == null, /// - Wakeup message retries from 5 to 2, /// - Removed error base from error code decision as error base is only the AppId /// /// /// - HideDataInLog. /// /// /// - Ignore wakeup if message already acknowledged (avoid to set /// "_recordInProcess.Acknowledge = RequestAcknowledgeState.NotDecoded"), /// - Avoid to overwrite "_recordInProcess.Acknowledge = RequestAcknowledgeState.Acknowledge" with /// "RequestAcknowledgeState.WakeupMessage". /// protected override void DecodeRecord(IPortDataEventArgs data) { if (_recordInProcess == null) return; //the request response record covers the entire request protocol var responseRecord = GetTransmitProtocol().DecodeDataForLogicLayer(_ident, (List)data.GetData(), _recordInProcess.HideDataInLog); //on protocol decoding failure if (responseRecord.Count == 0) { _recordInProcess.Acknowledge = RequestAcknowledgeState.DecodingError; _logger.Debug($"{_ident} Message decoding error."); //reset timeout to a normal value if timeout is extremely high but response received if (_maxTimeOutMs > CommunicationConfig.BusyTimeoutMs) _maxTimeOutMs = CommunicationConfig.BusyTimeoutMs; return; } if (responseRecord.Count == WakeupMessage.Length) { // check for wakeup message var wakeUp = true; for (var i = 0; i < WakeupMessage.Length; i++) { if (responseRecord[i] != WakeupMessage[i]) wakeUp = false; } if (wakeUp) { //avoid wakeup retry on acknowledged record if (RequestAcknowledgeState.Ok != _recordInProcess.Acknowledge) _recordInProcess.Acknowledge = RequestAcknowledgeState.WakeupMessage; //initiate a single retry on wakeup message response if (_recordInProcess.WakeupMessageRetryCtr < 2) { _recordInProcess.WakeupMessageRetryCtr++; _logger.Debug( $"{_ident} Wakeup message({_recordInProcess.WakeupMessageRetryCtr}) received"); } } else { //if it is not identified as valid wakeup message it is something unknown _recordInProcess.Acknowledge = RequestAcknowledgeState.DecodingError; _logger.Debug($"{_ident} Message decoding error."); } // reset retry counter to get all required retries _recordInProcess.RetryCtr = -1; //reset timeout to a normal value if timeout is extremely high but response received if (_maxTimeOutMs > CommunicationConfig.BusyTimeoutMs) _maxTimeOutMs = CommunicationConfig.BusyTimeoutMs; return; } //log request protocol content _logger.Debug(_recordInProcess.HideDataInLog ? $"{_ident} DecodedRecord(*****)" : $"{_ident} DecodedRecord({BitConverter.ToString(responseRecord.ToArray())})"); //extract information command var replyCmd = responseRecord[ResponseCommandIndex]; //extract position of error for multiple access if (replyCmd == Commands.MultipleReadDataReply || replyCmd == Commands.MultipleWriteDataReply) { _recordInProcess.ResponseErrorPosition = (UInt16)((responseRecord[ResponseMultiErrorPosition] & 0x00FF) | (UInt16)((responseRecord[ResponseMultiErrorPosition + 1] << 8) & 0xFF00)); } //extract the error codes from response record to _record in process _recordInProcess.ResponseErrorBase = responseRecord[ResponseErrorCodeIndex + ErrorCodeBaseIndex]; _recordInProcess.ResponseErrorReason = responseRecord[ResponseErrorCodeIndex + ErrorCodeReasonIndex]; //combine error base and error reason _recordInProcess.ResponseErrorCode = (UInt16)(_recordInProcess.ResponseErrorReason | (_recordInProcess.ResponseErrorBase << 8)); _recordInProcess.ResponsePayload = new List(); switch (replyCmd) { case Commands.MultipleReadDataReply: _recordInProcess.ResponsePayload.AddRange( responseRecord.GetRange(ResponseMultiDataIndex, responseRecord.Count - ResponseMultiHeaderSize)); break; case Commands.ReadDataReply: _recordInProcess.ResponsePayload.AddRange(responseRecord.GetRange(ResponseDataIndex, RegisterDefinition.ChunkSize)); break; } //Deny access if reply does not match if (_recordInProcess.ResponseCommand != replyCmd) { _logger.Fatal($"{_ident} Wrong command received({replyCmd:X2})," + $" expected command({_recordInProcess.ResponseCommand:X2})"); _recordInProcess.Acknowledge = RequestAcknowledgeState.CommandError; OnRecordIsDecoded?.Invoke(this, new RequestResponseDataEventArgs { RequestResponseData = responseRecord }); return; } //examine error code, if base (AppId) is not 0 the reason 0 may be an error!!!! if (/*_recordInProcess.ResponseErrorBase != 0 ||*/ _recordInProcess.ResponseErrorReason != 0) { _logger.Warn($"{_ident} Error code received(0x{_recordInProcess.ResponseErrorBase:X2}" + $"{_recordInProcess.ResponseErrorReason:X2})"); _recordInProcess.Acknowledge = RequestAcknowledgeState.MeterError; CheckErrorCode(); OnRecordIsDecoded?.Invoke(this, new RequestResponseDataEventArgs { RequestResponseData = responseRecord }); return; } //register password acknowledge if (_recordInProcess.Register.GetIdent() == Register.Configexchange.Password) { OnAuthorizationGrant?.Invoke(null, null); } //dispatch data if (_recordInProcess.ResponsePayload != null) { OnMeterRegisterUpdated?.Invoke(this, new RegisterUpdatedEventArgs(_recordInProcess.Register, _recordInProcess.ResponsePayload.ToArray())); } _recordInProcess.Acknowledge = RequestAcknowledgeState.Ok; OnRecordIsDecoded?.Invoke(this, new RequestResponseDataEventArgs { RequestResponseData = responseRecord }); } /// /// Method to send basic Commands to Meter. Creates an integer of 4 bytes for the payload. /// Supported Commands are: ,, /// , and . /// Calculate CRCs and Command length and check if command is valid. /// Use to push data to Port/Meter. /// /// /// Supported ,, /// , and . /// /// Register to Read or Write, null for /// /// Data to push into the . /// must be null on Read Commands ( ,) /// and not null on WriteData ( and ) /// /// /// hiding data in log file to avoid spying of passwords /// mask to skip reties on error /// an new command just send to port/meter /// /// - Initial /// /// /// - Reworked to zero pad payload with chunks of 4 bytes, the caller needn't take care of the size /// /// /// - Corrected payload content in request protocol /// /// /// - Hide data in logging for e.g. passwords /// /// /// - Skip retries on specific error mask to speed up communication on functional errors /// or informational feed backs (e.g FW not installed 0x0004) /// /// /// /// - Multiple read for string split to simple reads. /// /// /// - Default set. /// /// /// - MultipleReadData based on data size. /// /// /// - Exit multiple read date if retries exceeded. /// /// /// - Exit multiple read date if retries exceeded increased to . /// /// /// - Rewound to version from 06.09.2023 14:44:33 before Commit 8c9d7704. /// /// /// - Removed useless too short comments as every string is going to be delimited by a 0 and usually won't match /// to a 4 byte chunk. /// /// /// - Removed redundant 'return cRecord'. /// public RequestRecord CommandToMeter(Byte command, RegisterDefinition meterRegister = null, Byte[] payload = null, Int32? expectedLength = null, Boolean hideDataInLog = false, UInt16 skipRetryErrorCode = CommunicationConfig.SkipRetryErrorCode) { if (command == Commands.MultipleReadData && meterRegister != null && meterRegister.DataType == typeof(String)) { var completeResponse = new List(); while (true) { var cRecord = CommandToMeter(Commands.ReadData, meterRegister, payload, expectedLength, hideDataInLog, skipRetryErrorCode); var rest = ProcessRecordList(); if (rest == RequestAcknowledgeState.Ok && cRecord?.ResponsePayload != null) { completeResponse.AddRange(cRecord.ResponsePayload); if (!cRecord.ResponsePayload.Contains(0)) continue; cRecord.ResponsePayload = completeResponse; OnMeterRegisterUpdated?.Invoke(this, new RegisterUpdatedEventArgs(meterRegister, completeResponse.ToArray())); } return cRecord; } } //the minimum payload is one 4 byte chunk var chunkSize = RegisterDefinition.ChunkSize; if (payload == null) { payload = new Byte[chunkSize]; for (var i = 0; i < chunkSize; i++) { payload[i] = FillByte; } } //real payload based on 4 byte chunks var requestPayload = new List(); requestPayload.AddRange(payload); //fill-byte padding if (payload.Length % chunkSize != 0) { for (var i = 0; i < chunkSize - payload.Length % chunkSize; i++) { requestPayload.Add(FillByte); } } //request protocol excluding the command var requestProtocol = new List(); if (meterRegister != null) { requestProtocol.Add(meterRegister.RegisterAddress[1]); requestProtocol.Add(meterRegister.RegisterAddress[0]); if (command == Commands.MultipleWriteData) { //set counter to inform meter writes are expected //the payload size is already padded to 4 byte chunks var numberOfChunks = (UInt16)(requestPayload.Count / chunkSize); requestProtocol.Add((Byte)(numberOfChunks & 0xFF)); requestProtocol.Add((Byte)((numberOfChunks >> 8) & 0xFF)); } if (command == Commands.MultipleReadData) { //TODO THW create expected size out of meterRegister.DataType if (!expectedLength.HasValue) { if ((meterRegister.DataType == typeof(UInt64) || meterRegister.DataType == typeof(Int64))) { expectedLength = 8; } else { expectedLength = 24; } } var numberOfChunks = (UInt16)(expectedLength.Value / chunkSize); if (expectedLength % chunkSize != 0) numberOfChunks++; requestProtocol.Add((Byte)(numberOfChunks & 0xFF)); requestProtocol.Add((Byte)((numberOfChunks >> 8) & 0xFF)); } //add always the payload to the request protocol requestProtocol.AddRange(requestPayload); } return AddRecordToSendFifo(command, requestProtocol.ToArray(), meterRegister, hideDataInLog, skipRetryErrorCode); } /// /// Analyzes the error code /// private void CheckErrorCode() { if (_recordInProcess.ResponseErrorBase == (Byte)ConfigExErrors.Base) { if (_recordInProcess.ResponseErrorReason == (Byte)ConfigExErrors.AuthenticationFail || _recordInProcess.ResponseErrorReason == (Byte)ConfigExErrors.AccessDenied || _recordInProcess.ResponseErrorReason == (Byte)ConfigExErrors.LockedOut) { _recordInProcess.Acknowledge = RequestAcknowledgeState.AuthorizationRequired; } } } /// /// Reorders the record list so that first entry is the defined topRegister /// /// public void ReorderRecordList(String topRegister) { _recordsSendFifo.Enqueue(_recordInProcess); //reorder FIFO to bring login at first position for (var i = 0; i < _recordsSendFifo.Count; i++) { _recordsSendFifo.TryPeek(out var tmpRecord); if (tmpRecord == null || tmpRecord.Register.GetIdent() == topRegister) { break; } _recordsSendFifo.TryDequeue(out tmpRecord); _recordsSendFifo.Enqueue(tmpRecord); } } /// /// Checks the active register to access /// /// /// public Boolean ContainsRegisterIdent(String registerName) { return _recordsSendFifo.Any(a => string.Equals(a.Register.GetIdent(), registerName, StringComparison.CurrentCultureIgnoreCase)); } } }