562 lines
22 KiB
C#
562 lines
22 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// BMS 장치와의 시리얼 통신을 위한 개선된 클래스
|
|
/// Modbus RTU 프로토콜 및 커스텀 프로토콜 지원
|
|
/// </summary>
|
|
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
|
|
/// <summary>
|
|
/// Modbus 읽기 요청 프레임 생성
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Modbus 다중 레지스터 쓰기 요청 프레임 생성
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 단일 코일 쓰기 요청 프레임 생성
|
|
/// </summary>
|
|
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
|
|
/// <summary>
|
|
/// Modbus 응답 프레임 검증
|
|
/// </summary>
|
|
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
|
|
/// <summary>
|
|
/// Modbus 응답 데이터를 시스템 데이터로 변환
|
|
/// </summary>
|
|
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<ushort, Action<short, DeviceSystemData>> GetRegisterProcessors()
|
|
{
|
|
return new Dictionary<ushort, Action<short, DeviceSystemData>>
|
|
{
|
|
[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
|
|
}
|
|
} |