using System; using System.Collections.Generic; using System.Linq; using System.Threading; using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore; using Xylem.Common.Hardware.WaterMeter.Genesis.Registers; using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisConfig; using Xylem.Common.Utils.ProcessExec; using Xylem.Common.Utils.ProcessExec.EventArguments; namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisFile { /// /// Genesis meter file operations /// public class MeterFile : IProcessState { /// /// Port scan result event for message dispatcher to caller /// public event EventHandler OnProcessUpdate; //the raw data size is limited by the meter input buffer size, removed protocol length private const Int32 MaxRawDataSize = 56; private const Int32 ExtremeFileWriteTimeoutMs = 30000; private const Int32 ExtendedFileIoTimeoutMs = 5000; private const Int32 CommandWriteRetries = 2; //timeout value loaded dynamically starting with extended timeout, on retry to extreme timeout private Int32 _fileWriteTimeOutMs = ExtendedFileIoTimeoutMs; private readonly GenesisMeter _currentGenesis; public Int32 _filePointer; private const String StrNewFileReadWriteMode = "wb+"; private const String StrOpenFileReadOnlyMode = "rb"; private const String StrOpenFileReadWriteMode = "rb+"; private String _fileName; private Int32 _overallBytesCtr; /// /// Drive zero of meter. /// public const String StrMeterDrive0 = "0\\"; /// /// Drive one of meter. /// public const String StrMeterDrive1 = "1\\"; /// /// String delimiter of files from Genesis. /// public const Char StringDelimiter = '\0'; /// /// Wildcard for search all. /// public const Char WildcardAll = '*'; // public const String StrWildcardAll = "*"; /// /// Processed bytes counter /// public Int32 ProcessedBytesCtr; private Boolean _stopProcess; /// /// Increase timeout to extreme value /// /// /// - Timeout decreased to speed up stop operation. /// public void SetExtremeFileWriteTimeout() { _fileWriteTimeOutMs = ExtremeFileWriteTimeoutMs; } /// /// Set timeout back to default timeout /// /// /// - Timeout decreased to speed up stop operation. /// public void SetDefaultFileWriteTimeout() { _fileWriteTimeOutMs = ExtendedFileIoTimeoutMs; } /// /// Stop process /// /// /// - Timeout decreased to speed up stop operation. /// public Boolean StopProcess { get => _stopProcess; set { _currentGenesis?.TransmitProtocol.SetDefaultResponseTimeout(); _stopProcess = value; } } /// /// Ctor for genesis meter file operations /// /// /// /// - Initial /// /// /// - Stop process added /// public MeterFile(GenesisMeter genesisMeter) { _currentGenesis = genesisMeter; _fileName = ""; _stopProcess = false; } /// /// Process counter in percent /// /// /// - Initial /// public Double ProcessCtrPercent => 100.0 * ProcessedBytesCtr / (_overallBytesCtr > 0 ? _overallBytesCtr : 1); /// /// Writing a file to the meter /// /// file name as string /// data to store /// for partial file write procedures this is the offset to the file pointer /// true, if file can be find and stored and, if required, read back and verified /// /// - Initial /// /// /// - Early return on failed communication /// /// /// - Added overall bytes counter for single file processing information /// /// /// - Stop process added /// /// /// - CHeck if file exists /// /// /// - Extended response timeout /// /// /// - Register read/write retry 0 implemented. /// /// /// - Stop FWrite if FWrite failed, /// - FClose if FWrite failed, /// - avoid FTell if FWrite failed /// /// /// - Retries changed for and back on different position, /// - Timeout changed for and back on different position, /// - Dummy read removed. /// /// /// - Event on changed process counter. /// /// /// - Dynamic timeout load from extended to extreme timeout on retries. /// public Boolean WriteMeterFile(String fileName, Byte[] dataBytes, Int32 fileOffset = 0) { var returnValue = false; //set processing counter ProcessedBytesCtr = 0; //inform the caller of changed statue OnProcessUpdate?.Invoke(this, new ProcessExecEventArgs("", 0)); _overallBytesCtr = dataBytes.Length; //check if file is unlocked for write if (_fileName != fileName) return false; if (fileOffset == 0 && OpenMeterFile(fileName, StrNewFileReadWriteMode) || fileOffset > 0 && OpenMeterFile(fileName, StrOpenFileReadWriteMode)) { returnValue = true; //adjust file pointer if offset is required if (fileOffset > 0) { returnValue = SetFilePointerOffset(fileOffset); //read file pointer offset returnValue &= fileOffset == ReadFilePointerOffset(); } if (returnValue && !_stopProcess) { //set extended timeout for file operations to extended timeout _currentGenesis.TransmitProtocol.SetResponseTimeout(_fileWriteTimeOutMs); //set register write retries to 0 to avoid doubling of records, if wakeup message from //serial communication routine received, one retry will kicked off in the request protocol CommunicationConfig.RequestRetries = 0; //write size to 1 returnValue = _currentGenesis.WriteRegister(Register.Configexchange.FileWrite, 1, false); //write counts to data size returnValue &= _currentGenesis.WriteRegister(Register.Configexchange.FileWrite, dataBytes.Length, false); //write file pointer returnValue &= _currentGenesis.WriteRegister(Register.Configexchange.FileWrite, _filePointer); //calculate records to send based on MaxRawDataSize var chunkCounts = dataBytes.Length / MaxRawDataSize; if (0 != dataBytes.Length % MaxRawDataSize) { chunkCounts += 1; } var dataByteList = new List(); dataByteList.AddRange(dataBytes); var dataChunk = new List(); var ctr = 0; //the preceding routine needs to wait for response, else this is always true! while (returnValue && ctr < chunkCounts && !_stopProcess) { var index = ctr * MaxRawDataSize; var size = index + MaxRawDataSize > dataByteList.Count ? dataByteList.Count - index : MaxRawDataSize; //prepare data to write dataChunk.Clear(); dataChunk.AddRange(dataByteList.GetRange(index, size)); //break on failed write of this part returnValue = _currentGenesis.WriteRegister(Register.Configexchange.FileWrite, dataChunk.ToArray()); //set processed bytes for write loop ProcessedBytesCtr += size; //inform the caller of changed statue var processCtrPercent = 100.0 * ProcessedBytesCtr / (_overallBytesCtr > 0 ? _overallBytesCtr : 1); OnProcessUpdate?.Invoke(this, new ProcessExecEventArgs("", processCtrPercent)); ctr++; } //read back bytes written var writtenBytes = 0; var returnedFileOffset = 0; //the preceding routine needs to wait for response, else this is always true! if (returnValue) { writtenBytes = RegisterConverter.ConvertTo( _currentGenesis.ReadRegister(Register.Configexchange.FileWrite)); returnedFileOffset = ReadFilePointerOffset(); } //else //{ // //read DUMMY to overcome FW malfunction // _currentGenesis.ReadRegister(Register.System.MonotonicSeconds); //} //set timeout back to default value _currentGenesis.TransmitProtocol.SetDefaultResponseTimeout(); CommunicationConfig.RequestRetries = CommunicationConfig.DefaultRequestRetries; if (writtenBytes != ProcessedBytesCtr || returnedFileOffset != fileOffset + writtenBytes) returnValue = false; } }//file open returnValue &= CloseMeterFile(); //processed bytes not longer needed OnProcessUpdate?.Invoke(this, new ProcessExecEventArgs("", 0)); ProcessedBytesCtr = 0; _overallBytesCtr = 0; return returnValue; } /// /// Read file from the meter /// /// file name as string /// empty list of data /// size to read /// file content as byte array /// /// - Initial /// /// /// - Stop process added /// /// /// - Register read/write retry 0 implemented, /// - Extended communication timeout /// /// /// - Command write retry 0 implemented, /// - Extended timeout. /// /// /// - Retries changed for and back, /// - Timeout changed for and back, /// - Dummy read removed. /// /// /// - Wait after writing for response before reading! /// /// /// - Check remaining size to abort operation. /// /// /// - Event on changed process counter. /// public Boolean ReadMeterFile(String fileName, out List readBackData, Int32? readBackCount = null, string OpenFileReadOnlyModeOver = null) { if (String.IsNullOrEmpty(OpenFileReadOnlyModeOver)) { OpenFileReadOnlyModeOver = StrOpenFileReadOnlyMode; } //set processing counter ProcessedBytesCtr = 0; OnProcessUpdate?.Invoke(this, new ProcessExecEventArgs("", 0)); readBackData = new List(); var returnValue = OpenMeterFile(fileName, OpenFileReadOnlyModeOver); if (returnValue) { // optical.fseek(handle, 0, 2) //numbytes = optical.ftell(handle) //optical.fseek(handle, 0, 0) //set register write retries to 0 to avoid doubling of records CommunicationConfig.RequestRetries = 0; _currentGenesis.TransmitProtocol.SetResponseTimeout(ExtendedFileIoTimeoutMs); int fixedReadBackCount; if (readBackCount.HasValue) { fixedReadBackCount = readBackCount.Value; } else { _currentGenesis.WriteRegister("CONFIGEXCHANGE_FSeek", _filePointer, false); _currentGenesis.WriteRegister("CONFIGEXCHANGE_FSeek", 0, false); _currentGenesis.WriteRegister("CONFIGEXCHANGE_FSeek", 2, false); _currentGenesis.WriteRegister("CONFIGEXCHANGE_FTell", _filePointer, false); fixedReadBackCount = RegisterConverter.ConvertTo(_currentGenesis.ReadRegister("CONFIGEXCHANGE_FTell")) + 1; _currentGenesis.WriteRegister("CONFIGEXCHANGE_FSeek", _filePointer, false); _currentGenesis.WriteRegister("CONFIGEXCHANGE_FSeek", 0, false); _currentGenesis.WriteRegister("CONFIGEXCHANGE_FSeek", 0, false); } //write size to 1 _currentGenesis.WriteRegister(Register.Configexchange.FileRead, 1, false); //write counts to expected length _currentGenesis.WriteRegister(Register.Configexchange.FileRead, fixedReadBackCount, false); //write file pointer _currentGenesis.WriteRegister(Register.Configexchange.FileRead, _filePointer); //the preceding routine needs to wait for response, else this is always true! while (fixedReadBackCount > 0 && returnValue) { returnValue = _stopProcess; var remainingData = RegisterConverter.ConvertTo( _currentGenesis.ReadRegister(Register.Configexchange.FileRead)); fixedReadBackCount = remainingData > 0 ? fixedReadBackCount - remainingData : 0; while (remainingData > 0) { var size = remainingData > MaxRawDataSize ? MaxRawDataSize : remainingData; readBackData.AddRange(_currentGenesis.ReadRegister(Register.Configexchange.FileRead, size)); remainingData -= size; //set processed bytes for read loop ProcessedBytesCtr += size; //inform the caller of changed statue var processCtrPercent = 100.0 * ProcessedBytesCtr / (_overallBytesCtr > 0 ? _overallBytesCtr : 1); OnProcessUpdate?.Invoke(this, new ProcessExecEventArgs("", processCtrPercent)); } } //set timeout back to default value _currentGenesis.TransmitProtocol.SetDefaultResponseTimeout(); CommunicationConfig.RequestRetries = CommunicationConfig.DefaultRequestRetries; }//file open succeeded //dummy read to avoid 0x040B feedback from meter //_currentGenesis.ReadRegister(Register.Configexchange.FileRead, 1); returnValue &= CloseMeterFile(); //processed bytes not longer needed ProcessedBytesCtr = 0; OnProcessUpdate?.Invoke(this, new ProcessExecEventArgs("", 0)); //read raw data from meter return returnValue; } /// /// Read catalog of all meter files /// /// file names as string /// e.g. "1\\" for drive 1 /// e.g. "upd*" all upgrade files on drive /// /// /// - Initial. /// public Boolean ReadMeterFileCatalog(out List files, String drive, String wildcard) { files = new List(); //write the initial key var key = 0; _stopProcess = false; // if both key are identical, the last file has been listed while (!_stopProcess) { // remind entry key as this will be changed with the read back var lastKey = key; //\\logindex".format(drive) var searchPattern = wildcard + drive; _currentGenesis.WriteRegister(Register.Configexchange.Catalogue, searchPattern); _currentGenesis.WriteRegister(Register.Configexchange.Catalogue, key); // initial read var readData = ""; String backupData; do { backupData = readData; readData += RegisterConverter.ConvertTo( _currentGenesis.ReadRegister(Register.Configexchange.Catalogue)); } while (!readData.Contains(StringDelimiter) && backupData != readData); if (!string.IsNullOrEmpty(readData)) { //read new key key = RegisterConverter.ConvertTo( _currentGenesis.ReadRegister(Register.Configexchange.Catalogue)); } if (key == lastKey) break; // remove the "\0" var strMeterFile = readData.Split(StringDelimiter); // avoid identical files repetition var filePathName = drive + strMeterFile[0]; files.Add(filePathName); } return files.Count > 0; } /// /// Create an empty meter file as placeholder for space in the file system. /// /// file name as string /// the file size for the file pointer /// true, if file can be find and stored and, if required, read back and verified /// /// - Initial /// public Boolean CreateEmptyMeterFile(String fileName, Int32 fileSize = 0) { var returnValue = false; //check if file is unlocked for write if (_fileName != fileName) return false; if (OpenMeterFile(fileName, StrNewFileReadWriteMode)) { returnValue = true; //adjust file pointer if offset is required if (fileSize > 0) { // adjust file pointer to the end of file returnValue = SetFilePointerOffset(fileSize - 1); //read file pointer offset returnValue &= (fileSize - 1) == ReadFilePointerOffset(); } if (returnValue && !_stopProcess) { //set extended timeout for file operations to extended timeout _currentGenesis.TransmitProtocol.SetResponseTimeout(_fileWriteTimeOutMs); //set register write retries to 0 to avoid doubling of records, if wakeup message from //serial communication routine received, one retry will kicked off in the request protocol CommunicationConfig.RequestRetries = 0; //write size to 1 returnValue = _currentGenesis.WriteRegister(Register.Configexchange.FileWrite, 1, false); //write block of data to 1 as only one byte needed to be at the end of file returnValue &= _currentGenesis.WriteRegister(Register.Configexchange.FileWrite, 1, false); //write file pointer returnValue &= _currentGenesis.WriteRegister(Register.Configexchange.FileWrite, _filePointer); //write one byte at the end of file returnValue &= _currentGenesis.WriteRegister(Register.Configexchange.FileWrite, 0xFF); //read back bytes written var returnedFileOffset = 0; //the preceding routine needs to wait for response, else this is always true! if (returnValue) { RegisterConverter.ConvertTo( _currentGenesis.ReadRegister(Register.Configexchange.FileWrite)); returnedFileOffset = ReadFilePointerOffset(); } //else //{ // //read DUMMY to overcome FW malfunction // _currentGenesis.ReadRegister(Register.System.MonotonicSeconds); //} //set timeout back to default value _currentGenesis.TransmitProtocol.SetDefaultResponseTimeout(); CommunicationConfig.RequestRetries = CommunicationConfig.DefaultRequestRetries; if (returnedFileOffset != fileSize) returnValue = false; } }//file open returnValue &= CloseMeterFile(); return returnValue; } /// /// Meter file verification against given data /// /// file name as string /// data to store /// true: file found and verified /// /// - Initial /// public Boolean VerifyMeterFile(String fileName, Byte[] dataBytes) { if (!OpenMeterFile(fileName, StrOpenFileReadOnlyMode)) return false; //TODO THW implement //read raw data from meter return true; } /// /// Erase file from the meter /// /// file name as string /// /// /// - Initial /// /// /// - Extended timeout. /// /// /// - Removed retries to speed up erasure as it returns error code 0x040B. /// public Boolean EraseMeterFile(String fileName) { //check if file is unlocked for erase if (_fileName != fileName) return false; //check if logged on to meter and access level is as expected if (_currentGenesis == null || !_currentGenesis.IsLoggedOn) return false; //set extended timeout for file operations _currentGenesis.TransmitProtocol.SetResponseTimeout(ExtendedFileIoTimeoutMs); CommunicationConfig.RequestRetries = 0; //write "remove-command" to meter, wait for result and read back (read back returns false because of unreadable content) _currentGenesis.WriteRegister(Register.Configexchange.FileRemove, fileName, true, true); //set timeout back to default value _currentGenesis.TransmitProtocol.SetDefaultResponseTimeout(); CommunicationConfig.RequestRetries = CommunicationConfig.DefaultRequestRetries; return true; } /// /// Two stage unlock sequence for meter file write or erase access /// /// file name as string /// /// /// - Initial /// public Boolean UnlockEraseWriteMeterFile(String fileName) { //check if logged on to meter and access level is as expected if (_currentGenesis == null || !_currentGenesis.IsLoggedOn) return false; //remind file name being unlocked _fileName = fileName; return true; } /// /// FSeek implementation of genesis meter /// /// /// /// /// - Initial /// /// /// - Command write retry implemented. /// /// /// - Extended timeout. /// /// /// - Retries changed for and back, /// - Timeout changed for and back. /// private Boolean SetFilePointerOffset(Int32 filePointerOffset) { Boolean returnValue; var retryCtr = 0; do { if (retryCtr > 0) { //read DUMMY to overcome FW malfunction //_currentGenesis.ReadRegister(Register.Genesisflow.LedMode); _currentGenesis.ReadRegister(Register.System.MonotonicSeconds); Thread.Sleep(1000); } //set extended timeout for file operations _currentGenesis.TransmitProtocol.SetResponseTimeout(ExtendedFileIoTimeoutMs); CommunicationConfig.RequestRetries = 0; returnValue = _currentGenesis.WriteRegister(Register.Configexchange.SetFilePointerOffset, _filePointer, false); returnValue &= _currentGenesis.WriteRegister(Register.Configexchange.SetFilePointerOffset, filePointerOffset, false); returnValue &= _currentGenesis.WriteRegister(Register.Configexchange.SetFilePointerOffset, 0, false); //function call returns 0 if FSeek successfully set returnValue &= 0 == RegisterConverter.ConvertTo(_currentGenesis.ReadRegister( Register.Configexchange.SetFilePointerOffset)); //set timeout back to default value _currentGenesis.TransmitProtocol.SetDefaultResponseTimeout(); CommunicationConfig.RequestRetries = CommunicationConfig.DefaultRequestRetries; } while (!returnValue && retryCtr++ < CommandWriteRetries); return returnValue; } /// /// FTell implementation /// /// file pointer offset /// /// - Initial /// /// /// - Command write retry implemented. /// /// /// - Extended timeout. /// /// /// - Retries changed for and back, /// - Timeout changed for and back. /// private Int32 ReadFilePointerOffset() { Boolean returnValue; var retryCtr = 0; Int32 filePointerOffset; do { if (retryCtr > 0) { //read DUMMY to overcome FW malfunction //_currentGenesis.ReadRegister(Register.Genesisflow.LedMode); _currentGenesis.ReadRegister(Register.System.MonotonicSeconds); Thread.Sleep(1000); } //set extended timeout for file operations _currentGenesis.TransmitProtocol.SetResponseTimeout(ExtendedFileIoTimeoutMs); CommunicationConfig.RequestRetries = 0; //read file pointer offset returnValue = _currentGenesis.WriteRegister(Register.Configexchange.GetFilePointerOffset, _filePointer, false); filePointerOffset = RegisterConverter.ConvertTo(_currentGenesis.ReadRegister( Register.Configexchange.GetFilePointerOffset)); //set timeout back to default value _currentGenesis.TransmitProtocol.SetDefaultResponseTimeout(); CommunicationConfig.RequestRetries = CommunicationConfig.DefaultRequestRetries; } while (!returnValue && retryCtr++ < CommandWriteRetries); return filePointerOffset; } /// /// FOpen implementation of genesis meter /// Open a file of the meter and sets the file pointer /// /// /// /// true if filePointer higher than zero /// /// - Initial /// /// /// - Extended timeouts, /// - Read DUMMY to overcome FW malfunction. /// /// /// - Retries changed for and back, /// - Timeout changed for and back. /// public Boolean OpenMeterFile(String fileName, String accessMode) { //check if logged on to meter and access level is as expected if (_currentGenesis == null || !_currentGenesis.IsLoggedOn) return false; Boolean returnValue; var retryCtr = 0; do { if (retryCtr > 0) { //read DUMMY to overcome FW malfunction //_currentGenesis.ReadRegister(Register.Genesisflow.LedMode); _currentGenesis.ReadRegister(Register.System.MonotonicSeconds); Thread.Sleep(1000); } //set extended timeout for file operations _currentGenesis.TransmitProtocol.SetResponseTimeout(ExtendedFileIoTimeoutMs); CommunicationConfig.RequestRetries = 0; //write file name to meter with meter open command, don't wait for response _currentGenesis.WriteRegister(Register.Configexchange.FileOpen, fileName, false); //write access flag _currentGenesis.WriteRegister(Register.Configexchange.FileOpen, accessMode, false); //get the file pointer _filePointer = RegisterConverter.ConvertTo(_currentGenesis.ReadRegister( Register.Configexchange.FileOpen)); //set timeout back to default value _currentGenesis.TransmitProtocol.SetDefaultResponseTimeout(); CommunicationConfig.RequestRetries = CommunicationConfig.DefaultRequestRetries; returnValue = 0 < _filePointer; } while (!returnValue && retryCtr++ < CommandWriteRetries); return returnValue; } /// /// FClose implementation of genesis meter /// Close the _filePointer referenced file of the meter /// /// /// /// - Initial /// /// /// - Command write retry implemented. /// /// /// - Extended timeout. /// /// /// - Retries changed for and back, /// - Timeout changed for and back, /// - Command retry removed. /// /// /// - Wait after writing for response before reading! /// private Boolean CloseMeterFile() { //avoid file close if file pointer assignment failed if (_filePointer <= 0) return false; //set extended timeout for file operations _currentGenesis.TransmitProtocol.SetResponseTimeout(ExtendedFileIoTimeoutMs); CommunicationConfig.RequestRetries = 0; var returnValue = _currentGenesis.WriteRegister(Register.Configexchange.FileClose, _filePointer); //the preceding routine needs to wait for response, else this is always true! if (returnValue) { //function call returns 0 if FClose successfully executed returnValue &= 0 == RegisterConverter.ConvertTo( _currentGenesis.ReadRegister(Register.Configexchange.FileClose)); } //set timeout back to default value _currentGenesis.TransmitProtocol.SetDefaultResponseTimeout(); CommunicationConfig.RequestRetries = CommunicationConfig.DefaultRequestRetries; return returnValue; } } }