using System; using System.Collections.Generic; using System.Data; using System.Drawing; using System.Linq; using System.Threading; using System.Windows.Forms; using DevExpress.XtraCharts; using DevExpress.XtraLayout; using LFP_Manager.DataStructure; namespace LFP_Manager.Controls { public delegate void CommandEvent(int sId, int cmd, int index, int flag, ref CsDeviceData.DeviceModuleData.DeviceParamData aParam, ref CsDeviceData.DeviceModuleData.DeviceCalibration aCalib); public partial class ucMainStatus : DevExpress.XtraEditors.XtraUserControl { #region CONSTANTS / ENUMS private const int HeaderHeight = 30; private const int RowHeight = 60; private const int BorderPadding = 2; private static class Colors { public static readonly Color Normal = Color.Green; public static readonly Color Warning = Color.Orange; public static readonly Color Fault = Color.Red; public static readonly Color Offline = Color.Red; public static readonly Color Charging = Color.Blue; public static readonly Color Discharging = Color.Magenta; public static readonly Color Default = Color.Black; } private enum OperatingStatus { Standby = 0, Charging = 1, Discharging = 2, Floating = 3 } private enum BatteryStatus { Normal = 0, Warning = 1, Fault = 2, WarmingUp = 3, FaultAntiTheftComm = 4, FaultAntiTheftGyro = 5 } #endregion #region FIELDS private CommConfig _config; private CsDeviceData _deviceData; private static CsDeviceData.DeviceModuleData dummy; private ucModuleMainHeader _moduleMainHeader; private ucModuleMainB[] _moduleMainRows; private LayoutControlItem[] _lcitemModuleMain; private EmptySpaceItem[] _emptySpaceItems; private int _moduleQty; private double _totalVoltage; private double _totalCurrent; private double _totalTemperature; private double _totalSoc; private double _totalSoh; private bool _active; public event CommandEvent OnCommand; #endregion #region CONSTRUCTORS public ucMainStatus() { InitializeComponent(); _moduleQty = csConstData.SystemInfo.MAX_MODULE_SIZE; // 크기 변경 시 레이아웃 재배치 this.Resize += (_, __) => { if (_moduleMainRows != null && _moduleMainRows.Length > 0) RepositionLayout(); }; // 🔧 리소스/이벤트 정리: Dispose 이벤트로 대체 this.Disposed += UcMainStatus_Disposed; } #endregion #region PUBLIC API public void UpdateMainConfig(CommConfig config, CsDeviceData devData) { if (config == null || devData == null) return; _config = config; _deviceData = devData; _moduleQty = Math.Max(0, Math.Min(_config.ModuleQty, csConstData.SystemInfo.MAX_MODULE_SIZE)); LoadModuleMain(_moduleQty); lcgbModuleMain.Invalidate(); } public void Start(CommConfig config, CsDeviceData devData) { if (config == null || devData == null) return; _config = config; _deviceData = devData; _moduleQty = Math.Max(0, Math.Min(_config.ModuleQty, csConstData.SystemInfo.MAX_MODULE_SIZE)); _active = true; tmrDisplay.Enabled = true; } public void Stop() { _active = false; tmrDisplay.Enabled = false; } public void UpdateData(CsDeviceData devData) { if (!_active || devData == null) return; _deviceData = devData; // 합계 값 계산 (0으로 나눔 방지, 스케일 고정) _totalVoltage = _deviceData.TotalData?.ValueData != null ? _deviceData.TotalData.ValueData.TotalVoltage / 10.0 : 0.0; _totalCurrent = _deviceData.TotalData?.ValueData != null ? _deviceData.TotalData.ValueData.TotalCurrent / 10.0 : 0.0; _totalSoc = _deviceData.TotalData?.ValueData != null ? _deviceData.TotalData.ValueData.TotalSOC / 10.0 : 0.0; _totalSoh = _deviceData.TotalData?.ValueData != null ? _deviceData.TotalData.ValueData.TotalSOH / 10.0 : 0.0; _totalTemperature = _deviceData.TotalData?.ValueData != null ? _deviceData.TotalData.ValueData.TotalTemp / 10.0 : 0.0; // 모듈 행 갱신 (범위/Null 안전) if (_moduleMainRows != null && _deviceData.ModuleData != null) { var count = Math.Min(Math.Min(_moduleMainRows.Length, _deviceData.ModuleData.Length), _moduleQty); for (var i = 0; i < count; i++) { _moduleMainRows[i]?.UpdateData(ref _deviceData.ModuleData[i]); } } } private void UcMainStatus_Disposed(object sender, EventArgs e) { try { // 타이머 정지 if (tmrDisplay != null) tmrDisplay.Enabled = false; // 이벤트 구독 해제 if (_moduleMainRows != null) { foreach (var row in _moduleMainRows) { if (row != null) row.OnCommand -= SetCmdEvent; } } } catch { /* swallow on dispose */ } } #endregion #region INTERNAL LAYOUT private void LoadModuleMain(int mQty) { if (lcModuleMain == null || lcgbModuleMain == null) return; // 기존 컨트롤/아이템 제거 (성능 최적화) lcModuleMain.SuspendLayout(); lcgbModuleMain.BeginUpdate(); try { lcModuleMain.Controls.Clear(); lcgbModuleMain.Items.Clear(); _moduleMainHeader = new ucModuleMainHeader(); _moduleMainRows = new ucModuleMainB[mQty]; _lcitemModuleMain = new LayoutControlItem[mQty + 1]; var emptySize = csConstData.SystemInfo.MAX_MODULE_SIZE - mQty; _emptySpaceItems = emptySize > 0 ? new EmptySpaceItem[emptySize] : Array.Empty(); // 헤더 _lcitemModuleMain[0] = new LayoutControlItem { Control = _moduleMainHeader, Name = "lcitemModuleMainHeader", Size = new Size(Math.Max(0, lcgbModuleMain.Width - 2 * BorderPadding), HeaderHeight), Location = new Point(BorderPadding, BorderPadding), TextVisible = false, Padding = new DevExpress.XtraLayout.Utils.Padding(0) }; // 본문 행 var tWidth = Math.Max(0, lcgbModuleMain.Width - 2 * BorderPadding); var y = BorderPadding + HeaderHeight; var emptyIdx = 0; for (var i = 0; i < csConstData.SystemInfo.MAX_MODULE_SIZE; i++) { if (i < mQty) { // 기존 구독 제거(안전) if (_moduleMainRows[i] != null) _moduleMainRows[i].OnCommand -= SetCmdEvent; var moduleDataRef = _deviceData != null && _deviceData.ModuleData != null && _deviceData.ModuleData.Length > i ? ref _deviceData.ModuleData[i] : ref GetDummyModuleRef(); _moduleMainRows[i] = new ucModuleMainB(_config, i + 1, ref moduleDataRef); _moduleMainRows[i].OnCommand += SetCmdEvent; _lcitemModuleMain[i + 1] = new LayoutControlItem { Control = _moduleMainRows[i], Name = $"lcitemModuleMain_{i + 1}", Size = new Size(tWidth, RowHeight), Location = new Point(BorderPadding, y), TextVisible = false, Padding = new DevExpress.XtraLayout.Utils.Padding(0) }; y += RowHeight; } else if (_emptySpaceItems.Length > 0 && emptyIdx < _emptySpaceItems.Length) { _emptySpaceItems[emptyIdx] = new EmptySpaceItem { Size = new Size(tWidth, RowHeight), Location = new Point(BorderPadding, y) }; y += RowHeight; emptyIdx++; } } // 컨트롤 추가 lcModuleMain.Controls.Add(_moduleMainHeader); for (var i = 0; i < mQty; i++) { if (_moduleMainRows[i] != null) lcModuleMain.Controls.Add(_moduleMainRows[i]); } // 레이아웃 아이템 구성 var items = new List(csConstData.SystemInfo.MAX_MODULE_SIZE + 1); items.Add(_lcitemModuleMain[0]); for (var i = 1; i < _lcitemModuleMain.Length; i++) { if (_lcitemModuleMain[i] != null) items.Add(_lcitemModuleMain[i]); } if (_emptySpaceItems.Length > 0) items.AddRange(_emptySpaceItems); lcgbModuleMain.Items.AddRange(items.ToArray()); } finally { lcgbModuleMain.EndUpdate(); lcModuleMain.ResumeLayout(); lcgbModuleMain.Invalidate(); } } // 크기 변경 시 아이템 사이즈/좌표만 갱신 (재생성 X) private void RepositionLayout() { if (_lcitemModuleMain == null || _lcitemModuleMain.Length == 0 || lcgbModuleMain == null) return; lcgbModuleMain.BeginUpdate(); try { var tWidth = Math.Max(0, lcgbModuleMain.Width - 2 * BorderPadding); // 헤더 if (_lcitemModuleMain[0] != null) { _lcitemModuleMain[0].Size = new Size(tWidth, HeaderHeight); _lcitemModuleMain[0].Location = new Point(BorderPadding, BorderPadding); } // 행 var y = BorderPadding + HeaderHeight; for (var i = 1; i < _lcitemModuleMain.Length; i++) { var item = _lcitemModuleMain[i]; if (item == null) continue; item.Size = new Size(tWidth, RowHeight); item.Location = new Point(BorderPadding, y); y += RowHeight; } // 빈공간 if (_emptySpaceItems != null && _emptySpaceItems.Length > 0) { foreach (var emp in _emptySpaceItems) { if (emp == null) continue; emp.Size = new Size(tWidth, RowHeight); emp.Location = new Point(BorderPadding, y); y += RowHeight; } } } finally { lcgbModuleMain.EndUpdate(); lcgbModuleMain.Invalidate(); } } // 모듈 데이터가 아직 없을 때 참조를 위한 더미(구조 동일) private ref CsDeviceData.DeviceModuleData GetDummyModuleRef() { // static 필드로 유지해도 무방 return ref dummy; } #endregion #region EVENT BRIDGE private void SetCmdEvent( int sId, int cmd, int index, int flag, ref CsDeviceData.DeviceModuleData.DeviceParamData aParam, ref CsDeviceData.DeviceModuleData.DeviceCalibration aCalib) { var handler = OnCommand; // 레이스 방지 handler?.Invoke(sId, cmd, index, flag, ref aParam, ref aCalib); } #endregion #region TIMER EVENT private void tmrDisplay_Tick(object sender, EventArgs e) { DisplayTotalValue(); DisplayStatusAndAlarm(); } #endregion #region DISPLAY FUNCTION private void DisplayTotalValue() { if (!_active || _deviceData?.TotalData == null) return; var fw = _deviceData.TotalData.IdentData?.FwVerStr ?? "N/A"; var modules = _config != null ? _config.ModuleQty : _moduleQty; rtGraph.Text = $"Real Time Graph - V{fw} - {modules} Modules"; dgTotalVoltage.Text = $"{_totalVoltage:0.0}"; dgTotalCurrent.Text = $"{_totalCurrent:0.0}"; dgTotalTemp.Text = $"{_totalTemperature:0.0}"; dgTotalSOC.Text = $"{_totalSoc:0.0}"; dgTotalSOH.Text = $"{_totalSoh:0.0}"; chartVI?.Invalidate(); } private void DisplayStatusAndAlarm() { if (_deviceData?.TotalData == null) return; if (_deviceData.TotalData.CommFail) { lbStatus.Text = "OFF-LINE"; lbStatus.ForeColor = Colors.Offline; lbAlarm.Text = "OFF-LINE"; lbAlarm.ForeColor = Colors.Offline; return; } // Operating Status var st = (OperatingStatus)(_deviceData.TotalData.StatusData?.status ?? -1); switch (st) { case OperatingStatus.Standby: lbStatus.Text = "STANDBY"; lbStatus.ForeColor = Colors.Default; break; case OperatingStatus.Charging: lbStatus.Text = "CHARGING"; lbStatus.ForeColor = Colors.Charging; break; case OperatingStatus.Discharging: lbStatus.Text = "DISCHARGING"; lbStatus.ForeColor = Colors.Discharging; break; case OperatingStatus.Floating: lbStatus.Text = "FLOATING"; lbStatus.ForeColor = Colors.Default; break; default: lbStatus.Text = "UNKNOWN"; lbStatus.ForeColor = Colors.Default; break; } // Battery Status var bs = (BatteryStatus)(_deviceData.TotalData.StatusData?.batteryStatus ?? -1); switch (bs) { case BatteryStatus.Normal: lbAlarm.Text = "NORMAL"; lbAlarm.ForeColor = Colors.Normal; break; case BatteryStatus.Warning: lbAlarm.Text = "WARNING"; lbAlarm.ForeColor = Colors.Warning; break; case BatteryStatus.Fault: lbAlarm.Text = "FAULT"; lbAlarm.ForeColor = Colors.Fault; break; case BatteryStatus.WarmingUp: lbAlarm.Text = "WARMING UP"; lbAlarm.ForeColor = Colors.Warning; break; case BatteryStatus.FaultAntiTheftComm: lbAlarm.Text = "FAULT (Anti-Theft Comm.)"; lbAlarm.ForeColor = Colors.Fault; break; case BatteryStatus.FaultAntiTheftGyro: lbAlarm.Text = "FAULT (Anti-Theft Gyro)"; lbAlarm.ForeColor = Colors.Fault; break; default: lbAlarm.Text = $"UNKNOWN ({_deviceData.TotalData.StatusData?.batteryStatus})"; lbAlarm.ForeColor = Colors.Fault; break; } } #endregion } }