using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using LFP_Manager.DataStructure; using LFP_Manager.Utils; using LFP_Manager.Controls; namespace LFP_Manager.Function { /// /// BMS 장치와의 시리얼 통신을 위한 개선된 클래스 /// Modbus RTU 프로토콜 및 커스텀 프로토콜 지원 /// public class CsRs232CommFunction124050 { #region Constants - Modbus Function Codes public const byte READ_COIL_STATUS = 0x01; public const byte READ_HOLDING_REG = 0x03; public const byte READ_INPUT_REG = 0x04; public const byte FORCE_SINGLE_COIL = 0x05; public const byte PRESET_SINGLE_REG = 0x06; public const byte WRITE_COIL_REG = 0x0F; public const byte PRESET_MULTI_REG = 0x10; public const byte ERROR_REG = 0x90; public const byte FW_FLASH_ERASE_CMD = 0x43; public const byte FW_FLASH_WRITE_CMD = 0x31; public const byte NO_CMD = 0xFF; #endregion #region Constants - Register Addresses private static class RegisterAddress { // Basic Data public const int PACK_VOLTAGE = 0; public const int PACK_CURRENT = 1; public const int CELL_VOLTAGE_START = 2; public const int CELL_VOLTAGE_END = 17; public const int EXT1_TEMPERATURE = 18; public const int EXT2_TEMPERATURE = 19; public const int REMAINING_CAPACITY = 21; public const int MAX_CHARGE_CURRENT = 22; public const int STATE_OF_HEALTH = 23; public const int STATE_OF_CHARGE = 24; // Status Registers public const int OPERATING_STATUS = 25; public const int WARNING_STATUS = 26; public const int PROTECTION_STATUS = 27; public const int ERROR_CODE = 28; public const int CYCLE_COUNT_MSB = 29; public const int CYCLE_COUNT_LSB = 30; // Temperature Registers (Packed) public const int CELL_TEMP_START = 32; public const int CELL_TEMP_END = 35; // Device Info public const int CELL_QTY = 36; public const int DESIGNED_CAPACITY = 37; public const int CELL_BALANCE_STATUS = 38; public const int DATETIME_MSB = 45; public const int DATETIME_LSB = 46; public const int SPECIAL_ALARM = 49; // Protection Parameters public const int LOW_SOC_WARNING = 58; public const int CELL_UV_WARNING = 61; public const int CELL_UV_TRIP = 62; public const int CELL_UV_RELEASE = 63; public const int SYS_UV_WARNING = 64; public const int SYS_UV_TRIP = 65; public const int SYS_UV_RELEASE = 66; public const int CELL_OV_WARNING = 67; public const int CELL_OV_TRIP = 68; public const int CELL_OV_RELEASE = 69; public const int SYS_OV_WARNING = 70; public const int SYS_OV_TRIP = 71; public const int SYS_OV_RELEASE = 72; // Current Protection public const int CHA_OC_TIMES = 76; public const int DCH_OC_TIMES = 77; public const int CHA_OC_RELEASE_TIME = 78; public const int DCH_OC_RELEASE_TIME = 79; public const int CHA_OC_TRIP1 = 80; public const int DCH_OC_TRIP1 = 81; public const int SHORT_CIRCUIT = 82; public const int CHA_OC_TRIP2 = 83; public const int DCH_OC_TRIP2 = 84; public const int CHA_OC_DELAY1 = 85; public const int CHA_OC_DELAY2 = 86; public const int DCH_OC_DELAY1 = 87; public const int DCH_OC_DELAY2 = 88; // Temperature Protection public const int CHA_LOW_TEMP_WARNING = 90; public const int CHA_LOW_TEMP_TRIP = 91; public const int CHA_LOW_TEMP_RELEASE = 92; public const int CHA_HIGH_TEMP_WARNING = 93; public const int CHA_HIGH_TEMP_TRIP = 94; public const int CHA_HIGH_TEMP_RELEASE = 95; public const int DCH_LOW_TEMP_WARNING = 96; public const int DCH_LOW_TEMP_TRIP = 97; public const int DCH_LOW_TEMP_RELEASE = 98; public const int DCH_HIGH_TEMP_WARNING = 99; public const int DCH_HIGH_TEMP_TRIP = 100; public const int DCH_HIGH_TEMP_RELEASE = 101; // Device Information public const int MODEL_NAME_START = 105; public const int MODEL_NAME_END = 116; public const int FW_VERSION_START = 117; public const int FW_VERSION_END = 119; public const int SERIAL_NUMBER_START = 120; public const int SERIAL_NUMBER_END = 127; // Extended Cell Voltages public const int EXT_CELL_VOLTAGE_START = 138; public const int EXT_CELL_VOLTAGE_END = 160; // Manufacturing Date public const int MANU_DATE_START = 163; public const int MANU_DATE_END = 166; } #endregion #region Delegates and Events public delegate void LogEventHandler(string message, LogLevel level); public static event LogEventHandler OnLog; public enum LogLevel { Info, Warning, Error } #endregion #region Modbus Frame Construction /// /// Modbus 읽기 요청 프레임 생성 /// public static byte[] CreateReadRegisterFrame(byte deviceId, byte functionCode, ushort startAddress, ushort quantity) { if (quantity == 0 || quantity > 125) throw new ArgumentException("Invalid register quantity"); var frame = new byte[8]; frame[0] = deviceId; frame[1] = functionCode; frame[2] = (byte)(startAddress >> 8); frame[3] = (byte)(startAddress & 0xFF); frame[4] = (byte)(quantity >> 8); frame[5] = (byte)(quantity & 0xFF); var crc = csUtils.CalculateCRC(frame, 6); frame[6] = crc[0]; frame[7] = crc[1]; return frame; } /// /// Modbus 다중 레지스터 쓰기 요청 프레임 생성 /// public static byte[] CreateWriteMultipleRegistersFrame(byte deviceId, ushort startAddress, short[] values) { if (values == null || values.Length == 0 || values.Length > 123) throw new ArgumentException("Invalid values array"); var frame = new byte[9 + (values.Length * 2)]; int index = 0; frame[index++] = deviceId; frame[index++] = PRESET_MULTI_REG; frame[index++] = (byte)(startAddress >> 8); frame[index++] = (byte)(startAddress & 0xFF); frame[index++] = (byte)(values.Length >> 8); frame[index++] = (byte)(values.Length & 0xFF); frame[index++] = (byte)(values.Length * 2); foreach (var value in values) { frame[index++] = (byte)(value >> 8); frame[index++] = (byte)(value & 0xFF); } var crc = csUtils.CalculateCRC(frame, index); frame[index++] = crc[0]; frame[index++] = crc[1]; return frame; } /// /// 단일 코일 쓰기 요청 프레임 생성 /// public static byte[] CreateWriteCoilFrame(byte deviceId, ushort coilAddress, bool value) { var frame = new byte[8]; frame[0] = deviceId; frame[1] = FORCE_SINGLE_COIL; frame[2] = (byte)(coilAddress >> 8); frame[3] = (byte)(coilAddress & 0xFF); frame[4] = value ? (byte)0xFF : (byte)0x00; frame[5] = 0x00; var crc = csUtils.CalculateCRC(frame, 6); frame[6] = crc[0]; frame[7] = crc[1]; return frame; } #endregion #region Frame Validation /// /// Modbus 응답 프레임 검증 /// public static ValidationResult ValidateModbusResponse(byte[] data, int length) { if (data == null || length < 3) return new ValidationResult(false, "Insufficient data length"); try { byte functionCode = data[1]; int expectedLength = CalculateExpectedLength(functionCode, data, length); if (length < expectedLength) return new ValidationResult(false, "Incomplete frame"); var calculatedCrc = csUtils.CalculateCRC(data, length - 2); var receivedCrc = new byte[] { data[length - 2], data[length - 1] }; if (calculatedCrc[0] != receivedCrc[0] || calculatedCrc[1] != receivedCrc[1]) return new ValidationResult(false, "CRC mismatch"); return new ValidationResult(true, "Valid frame"); } catch (Exception ex) { OnLog?.Invoke($"Frame validation error: {ex.Message}", LogLevel.Error); return new ValidationResult(false, ex.Message); } } private static int CalculateExpectedLength(byte functionCode, byte[] data, int length) { switch (functionCode) { case READ_COIL_STATUS: case READ_HOLDING_REG: case READ_INPUT_REG: return length >= 3 ? data[2] + 5 : 5; case PRESET_MULTI_REG: case FORCE_SINGLE_COIL: case PRESET_SINGLE_REG: return 8; case ERROR_REG: return 5; case FW_FLASH_ERASE_CMD: case FW_FLASH_WRITE_CMD: return 5; default: return length; } } #endregion #region Data Processing /// /// Modbus 응답 데이터를 시스템 데이터로 변환 /// public static ProcessingResult ProcessModbusResponse(byte[] data, ushort startAddress, ushort length, ref DeviceSystemData systemData) { if (data == null || systemData == null) return new ProcessingResult(false, "Invalid input parameters"); try { byte functionCode = data[1]; switch (functionCode) { case READ_COIL_STATUS: return ProcessCoilResponse(data, startAddress, length, ref systemData); case READ_HOLDING_REG: return ProcessHoldingRegisterResponse(data, startAddress, length, ref systemData); case READ_INPUT_REG: return ProcessInputRegisterResponse(data, startAddress, length, ref systemData); case ERROR_REG: return ProcessErrorResponse(data, ref systemData); default: return new ProcessingResult(false, $"Unsupported function code: {functionCode:X2}"); } } catch (Exception ex) { OnLog?.Invoke($"Data processing error: {ex.Message}", LogLevel.Error); return new ProcessingResult(false, ex.Message); } } private static ProcessingResult ProcessHoldingRegisterResponse(byte[] data, ushort startAddress, ushort length, ref DeviceSystemData systemData) { int byteCount = data[2]; int registerCount = byteCount / 2; int dataIndex = 3; var processors = GetRegisterProcessors(); for (int i = 0; i < registerCount; i++) { ushort registerAddress = (ushort)(startAddress + i); short registerValue = (short)((data[dataIndex] << 8) | data[dataIndex + 1]); if (processors.TryGetValue(registerAddress, out var processor)) { processor.Invoke(registerValue, systemData); } dataIndex += 2; } // 계산된 값들 업데이트 UpdateCalculatedValues(ref systemData); return new ProcessingResult(true, "Processing completed successfully"); } private static ProcessingResult ProcessCoilResponse(byte[] data, ushort startAddress, ushort length, ref DeviceSystemData systemData) { // 코일 데이터 처리 로직 return new ProcessingResult(true, "Coil processing completed"); } private static ProcessingResult ProcessInputRegisterResponse(byte[] data, ushort startAddress, ushort length, ref DeviceSystemData systemData) { // 입력 레지스터 데이터 처리 로직 return new ProcessingResult(true, "Input register processing completed"); } private static ProcessingResult ProcessErrorResponse(byte[] data, ref DeviceSystemData systemData) { return new ProcessingResult(false, $"Device error: {data[2]:X2}"); } #endregion #region Register Processing private static Dictionary> GetRegisterProcessors() { return new Dictionary> { [RegisterAddress.PACK_VOLTAGE] = (value, data) => data.ValueData.voltageOfPack = (short)(value / 10), [RegisterAddress.PACK_CURRENT] = (value, data) => data.ValueData.current = (short)(value / 10), [RegisterAddress.REMAINING_CAPACITY] = (value, data) => data.ValueData.remainingCapacity = value, [RegisterAddress.STATE_OF_HEALTH] = (value, data) => data.ValueData.stateOfHealth = (short)(value * 10), [RegisterAddress.STATE_OF_CHARGE] = (value, data) => data.ValueData.rSOC = (short)(value * 10), [RegisterAddress.OPERATING_STATUS] = (value, data) => data.StatusData.status = value, [RegisterAddress.WARNING_STATUS] = (value, data) => data.StatusData.warning = ConvertWarningData(value), [RegisterAddress.PROTECTION_STATUS] = (value, data) => data.StatusData.protection = ConvertProtectionData(value), [RegisterAddress.CELL_QTY] = (value, data) => data.recv_cellQty = value, [RegisterAddress.DESIGNED_CAPACITY] = (value, data) => data.ValueData.designedCapacity = value, // 추가 레지스터 처리기들... }; } private static void ProcessCellVoltages(ushort address, short value, ref DeviceSystemData systemData) { if (address >= RegisterAddress.CELL_VOLTAGE_START && address <= RegisterAddress.CELL_VOLTAGE_END) { int cellIndex = address - RegisterAddress.CELL_VOLTAGE_START; if (cellIndex < systemData.ValueData.CellVoltage.Length) { systemData.ValueData.CellVoltage[cellIndex] = (ushort)value; } } else if (address >= RegisterAddress.EXT_CELL_VOLTAGE_START && address <= RegisterAddress.EXT_CELL_VOLTAGE_END) { int cellIndex = address - RegisterAddress.EXT_CELL_VOLTAGE_START + 16; if (cellIndex < systemData.ValueData.CellVoltage.Length) { systemData.ValueData.CellVoltage[cellIndex] = (ushort)value; } } } private static void ProcessCellTemperatures(ushort address, short value, ref DeviceSystemData systemData) { if (address >= RegisterAddress.CELL_TEMP_START && address <= RegisterAddress.CELL_TEMP_END) { int tempIndex = (address - RegisterAddress.CELL_TEMP_START) * 2; if (tempIndex + 1 < systemData.ValueData.CellTemperature.Length) { systemData.ValueData.CellTemperature[tempIndex] = (short)(((value >> 8) & 0xFF) * 10); systemData.ValueData.CellTemperature[tempIndex + 1] = (short)((value & 0xFF) * 10); } } } #endregion #region Data Conversion private static short ConvertWarningData(short rawData) { short result = 0; bool[] alarmBits = ConvertToBitArray(rawData); if (alarmBits[0]) result |= (1 << 2); // Pack OV if (alarmBits[1]) result |= (1 << 4); // Cell OV if (alarmBits[2]) result |= (1 << 3); // Pack UV if (alarmBits[3]) result |= (1 << 5); // Cell UV if (alarmBits[4]) result |= (1 << 6); // Charging OC if (alarmBits[5]) result |= (1 << 7); // Discharging OC if (alarmBits[8]) result |= (1 << 0); // Charging Over Temperature if (alarmBits[9]) result |= (1 << 0); // Discharging Over Temperature if (alarmBits[10]) result |= (1 << 1); // Charging Under Temperature if (alarmBits[11]) result |= (1 << 1); // Discharging Under Temperature if (alarmBits[12]) result |= (1 << 11); // SOC Low return result; } private static short ConvertProtectionData(short rawData) { short result = 0; bool[] alarmBits = ConvertToBitArray(rawData); if (alarmBits[0]) result |= (1 << 2); // Pack OV if (alarmBits[1]) result |= (1 << 4); // Cell OV if (alarmBits[2]) result |= (1 << 3); // Pack UV if (alarmBits[3]) result |= (1 << 5); // Cell UV if (alarmBits[4]) result |= (1 << 6); // Charging OC if (alarmBits[5]) result |= (1 << 7); // Discharging OC if (alarmBits[8]) result |= (1 << 0); // Charging Over Temperature if (alarmBits[9]) result |= (1 << 0); // Discharging Over Temperature if (alarmBits[10]) result |= (1 << 1); // Charging Under Temperature if (alarmBits[11]) result |= (1 << 1); // Discharging Under Temperature if (alarmBits[13]) result |= (1 << 9); // Short Circuit Protection return result; } private static bool[] ConvertToBitArray(short value) { var bits = new bool[16]; for (int i = 0; i < 16; i++) { bits[i] = ((value >> i) & 1) == 1; } return bits; } #endregion #region Calculated Values private static void UpdateCalculatedValues(ref DeviceSystemData systemData) { CalculateCellVoltageStatistics(ref systemData); CalculateTemperatureStatistics(ref systemData); csMakeDataFunction.MakeAlarm(ref systemData); } private static void CalculateCellVoltageStatistics(ref DeviceSystemData systemData) { if (systemData.cellQty <= 0) return; int max = 0, min = int.MaxValue, sum = 0; int maxIndex = 0, minIndex = 0; for (int i = 0; i < systemData.cellQty; i++) { if (i >= systemData.ValueData.CellVoltage.Length) break; int cellVoltage = systemData.ValueData.CellVoltage[i]; sum += cellVoltage; if (cellVoltage > max) { max = cellVoltage; maxIndex = i; } if (cellVoltage < min) { min = cellVoltage; minIndex = i; } } systemData.AvgData.avgCellVoltage = (short)(sum / systemData.cellQty); systemData.AvgData.maxCellVoltage = (short)max; systemData.AvgData.maxCellNum = (short)(maxIndex + 1); systemData.AvgData.minCellVoltage = (short)min; systemData.AvgData.minCellNum = (short)(minIndex + 1); systemData.AvgData.diffCellVoltage = (short)(max - min); } private static void CalculateTemperatureStatistics(ref DeviceSystemData systemData) { if (systemData.tempQty <= 0) return; int max = int.MinValue, min = int.MaxValue, sum = 0; int maxIndex = 0, minIndex = 0; for (int i = 0; i < systemData.tempQty; i++) { if (i >= systemData.ValueData.CellTemperature.Length) break; int temperature = systemData.ValueData.CellTemperature[i]; sum += temperature; if (temperature > max) { max = temperature; maxIndex = i; } if (temperature < min) { min = temperature; minIndex = i; } } systemData.AvgData.avgTemp = (short)(sum / systemData.tempQty); systemData.AvgData.maxTemp = (short)max; systemData.AvgData.maxTempNum = (short)(maxIndex + 1); systemData.AvgData.minTemp = (short)min; systemData.AvgData.minTempNum = (short)(minIndex + 1); systemData.AvgData.diffTemp = (short)(max - min); } #endregion #region Result Classes public class ValidationResult { public bool IsValid { get; } public string Message { get; } public ValidationResult(bool isValid, string message) { IsValid = isValid; Message = message; } } public class ProcessingResult { public bool Success { get; } public string Message { get; } public ProcessingResult(bool success, string message) { Success = success; Message = message; } } #endregion } }