471 lines
16 KiB
C#
471 lines
16 KiB
C#
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<EmptySpaceItem>();
|
|
|
|
// 헤더
|
|
_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<BaseLayoutItem>(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
|
|
}
|
|
}
|