using DevExpress.Utils; using DevExpress.XtraEditors.Repository; using DevExpress.XtraGrid; using DevExpress.XtraGrid.Columns; using DevExpress.XtraGrid.Views.Grid; using LFP_Manager.DataStructure; using LFP_Manager.Function; using LFP_Manager.Utils; using System; using System.Data; using System.Windows.Forms; namespace LFP_Manager.Forms { public partial class fmxHistory : DevExpress.XtraEditors.XtraForm { #region VARIABLES private int SystemId = 0; private CommConfig Config; private byte[] rData = null; private ushort curr_addr = 0; private int recv_data_len = 0; private int hist_no = 0; private bool stopReq = true; private int cell_qty = 16; private int temp_qty = 4; private int req_timeoutCount = 0; private DateTime ReqDateTime; private DataTable BmsHistoryTable = null; public event SendDataUartEvent OnSendUartData = null; #endregion #region CONTRUCTORS public fmxHistory(CommConfig aConfig, int systemId) { InitializeComponent(); Config = aConfig; switch (Config.UartModelIndex) { case csConstData.MODEL_INDEX.LFPM_124050D: recv_data_len = 112; cell_qty = 39; temp_qty = 8; break; default: recv_data_len = 58; cell_qty = 16; temp_qty = 4; break; } ucHistroy1.SetCommCofig(Config); SystemId = systemId; CreateHistoryColumn(ref BmsHistoryTable); BuildHistoryGridColumns(gcBmsHistory, gvBmsHistory, BmsHistoryTable); gvBmsHistory.Appearance.HeaderPanel.TextOptions.HAlignment = HorzAlignment.Center; gvBmsHistory.Appearance.HeaderPanel.TextOptions.VAlignment = VertAlignment.Center; switch (Config.CommType) { case csConstData.CommType.COMM_UART: pgBmsHistory.PageEnabled = true; //tabHistory.TabPages.Remove(pgBmsHistory); break; case csConstData.CommType.COMM_RS485: pgBmsHistory.PageEnabled = false; tabHistory.TabPages.Remove(pgBmsHistory); break; case csConstData.CommType.COMM_SNMP: pgBmsHistory.PageEnabled = false; tabHistory.TabPages.Remove(pgBmsHistory); break; default: break; } } #endregion #region INITIALIZE COMPONENT private void CreateHistoryColumn(ref DataTable hDT) { hDT = new DataTable(); hDT.Columns.Add("no", typeof(int)); hDT.Columns.Add("datetime", typeof(DateTime)); hDT.Columns.Add("status", typeof(int)); hDT.Columns.Add("warning", typeof(string)); hDT.Columns.Add("protect", typeof(string)); hDT.Columns.Add("error", typeof(string)); hDT.Columns.Add("voltage", typeof(decimal)); hDT.Columns.Add("current", typeof(decimal)); hDT.Columns.Add("SOC", typeof(decimal)); hDT.Columns.Add("pcb_t", typeof(decimal)); hDT.Columns.Add("amb_t", typeof(decimal)); for (int i = 0; i < temp_qty; i++) { hDT.Columns.Add(string.Format("temp_{0:00}", i + 1), typeof(decimal)); } for (int i = 0; i < cell_qty; i++) { hDT.Columns.Add(string.Format("cell_v_{0:00}", i + 1), typeof(decimal)); } gcBmsHistory.DataSource = hDT; } private void BuildHistoryGridColumns(GridControl grid, GridView view, DataTable dt) { grid.BeginUpdate(); view.BeginUpdate(); try { // 1️⃣ DataSource 바인딩 grid.DataSource = dt; // 2️⃣ 기존 컬럼 제거 view.Columns.Clear(); // 3️⃣ RepositoryItems 준비 RepositoryItemTextEdit repoText = new RepositoryItemTextEdit { ReadOnly = true }; RepositoryItemSpinEdit repoInt = new RepositoryItemSpinEdit { IsFloatValue = false, Mask = { EditMask = "N0", UseMaskAsDisplayFormat = true } }; RepositoryItemSpinEdit repoF1 = new RepositoryItemSpinEdit { IsFloatValue = true, Mask = { EditMask = "F1", UseMaskAsDisplayFormat = true } }; RepositoryItemSpinEdit repoF2 = new RepositoryItemSpinEdit { IsFloatValue = true, Mask = { EditMask = "F2", UseMaskAsDisplayFormat = true } }; RepositoryItemSpinEdit repoF3 = new RepositoryItemSpinEdit { IsFloatValue = true, Mask = { EditMask = "F3", UseMaskAsDisplayFormat = true } }; RepositoryItemDateEdit repoDate = new RepositoryItemDateEdit { Mask = { EditMask = "yyyy-MM-dd HH:mm:ss", UseMaskAsDisplayFormat = true } }; grid.RepositoryItems.AddRange(new RepositoryItem[] { repoText, repoInt, repoF1, repoF2, repoF3, repoDate }); int visibleIndex = 0; // 4️⃣ DataTable 컬럼 기준 GridColumn 생성 foreach (DataColumn dc in dt.Columns) { GridColumn col = new GridColumn { FieldName = dc.ColumnName, Name = "gc_" + dc.ColumnName, Caption = GetHistoryColumnCaption(dc.ColumnName), // ⭐ 헤더 변경 Visible = true, VisibleIndex = visibleIndex++ }; string name = dc.ColumnName.ToLower(); // ───── 타입/이름 기준 포맷 규칙 ───── if (name == "no") { col.ColumnEdit = repoInt; col.DisplayFormat.FormatType = FormatType.Numeric; col.DisplayFormat.FormatString = "N0"; col.AppearanceCell.TextOptions.HAlignment = HorzAlignment.Center; } else if (name == "datetime") { col.ColumnEdit = repoDate; col.DisplayFormat.FormatType = FormatType.DateTime; col.DisplayFormat.FormatString = "yyyy-MM-dd HH:mm:ss"; } else if (name == "status") { col.ColumnEdit = repoText; // ✅ status는 표시가 문자열이므로 가운데 정렬 강제 col.AppearanceCell.Options.UseTextOptions = true; // ⭐⭐⭐ 핵심 ⭐⭐⭐ col.AppearanceCell.TextOptions.HAlignment = HorzAlignment.Center; // (선택) 숫자 표시 포맷/에디터 굳이 필요 없으면 빼도 됨 // col.ColumnEdit = repoInt; } else if (name == "voltage" || name == "current" || name == "soc") { col.ColumnEdit = repoF2; col.DisplayFormat.FormatType = FormatType.Numeric; col.DisplayFormat.FormatString = "0.00"; } else if (name == "pcb_t" || name == "amb_t" || name.StartsWith("temp_")) { col.ColumnEdit = repoF1; col.DisplayFormat.FormatType = FormatType.Numeric; col.DisplayFormat.FormatString = "0.0"; } else if (name.StartsWith("cell_v_")) { col.ColumnEdit = repoF3; col.DisplayFormat.FormatType = FormatType.Numeric; col.DisplayFormat.FormatString = "0.000"; } else { // warning / protect / error (string) col.ColumnEdit = repoText; col.AppearanceCell.Options.UseTextOptions = true; // ⭐⭐⭐ 핵심 ⭐⭐⭐ col.AppearanceCell.TextOptions.HAlignment = HorzAlignment.Center; } view.Columns.Add(col); } // 5️⃣ 전체 헤더 가운데 정렬 (요청하신 설정) view.Appearance.HeaderPanel.TextOptions.HAlignment = DevExpress.Utils.HorzAlignment.Center; view.Appearance.HeaderPanel.TextOptions.VAlignment = DevExpress.Utils.VertAlignment.Center; // 6️⃣ Grid 기본 옵션 view.OptionsView.ColumnAutoWidth = false; view.OptionsView.ShowGroupPanel = false; view.OptionsBehavior.Editable = false; // History Grid → 읽기 전용 권장 } finally { view.EndUpdate(); grid.EndUpdate(); } } private string GetHistoryColumnCaption(string fieldName) { string name = fieldName.ToLower(); if (name == "no") return "No"; if (name == "datetime") return "Date / Time"; if (name == "status") return "Status"; if (name == "warning") return "Warning Flags"; if (name == "protect") return "Protect Flags"; if (name == "error") return "Error Flags"; if (name == "voltage") return "Pack Voltage (V)"; if (name == "current") return "Pack Current (A)"; if (name == "soc") return "SOC (%)"; if (name == "pcb_t") return "PCB Temp (°C)"; if (name == "amb_t") return "Ambient Temp (°C)"; if (name.StartsWith("temp_")) { int idx = int.Parse(name.Substring(5)); return $"Temp {idx} (°C)"; } if (name.StartsWith("cell_v_")) { int idx = int.Parse(name.Substring(7)); return $"Cell {idx} Voltage (V)"; } // 기본: 컬럼명 그대로 (Fallback) return fieldName; } #endregion #region PUBLIC FUNCTIONS public void RecvUartData(byte[] data) { if (InvokeRequired) { BeginInvoke(new Action(() => RecvUartData(data))); return; } rData = data; LcGrResult.Text = $"Result ({rData.Length})"; if ((rData != null) && (rData.Length == recv_data_len)) { try { req_timeoutCount = 0; DataRow hRow = BmsHistoryTable.NewRow(); short[] shorts = csUtils.ByteArrTo2ShortSafe(data); int pos = 0; int yy, MM, dd, HH, mm, ss; Int32 iDateTime = (rData[3 + 2] << 24) | (rData[3 + 3] << 16) | (rData[3 + 0] << 8) | (rData[3 + 1] << 0); //Int32 iDateTime = (Int32)((shorts[pos + 1] << 16) | (shorts[pos + 0] << 0)); yy = (iDateTime >> 26) & 0x003F; MM = (iDateTime >> 22) & 0x000F; dd = (iDateTime >> 17) & 0x001F; HH = (iDateTime >> 12) & 0x001F; mm = (iDateTime >> 6) & 0x003F; ss = (iDateTime >> 0) & 0x003F; hRow["no"] = hist_no; string stDateTime = string.Format("{0}-{1:00}-{2:00} {3:00}:{4:00}:{5:00}" , yy , MM , dd , HH , mm , ss ); DateTime aDateTime; if (MM < 1 || MM > 12 || dd < 1 || dd > 31 || HH > 23 || mm > 59 || ss > 59) aDateTime = DateTime.MinValue; // 또는 DateTime.MinValue 넣기 else aDateTime = Convert.ToDateTime(stDateTime); hRow["datetime"] = aDateTime; pos = 2; hRow["status"] = shorts[pos++]; hRow["warning"] = string.Format("{0:X4}", shorts[pos++]); hRow["protect"] = string.Format("{0:X4}", shorts[pos++]); hRow["error"] = string.Format("{0:X4}", shorts[pos++]); hRow["voltage"] = shorts[pos++] / 100m; hRow["current"] = shorts[pos++] / 100m; hRow["SOC"] = shorts[pos++] / 1m; hRow["pcb_t"] = (byte)(shorts[pos] >> 8) / 1m; hRow["amb_t"] = (byte)(shorts[pos] >> 0) / 1m; pos++; // Temperature for (int i = 0; i < (temp_qty / 2); i++) { hRow[string.Format("temp_{0:00}", (i * 2) + 1)] = Convert.ToDouble((byte)(shorts[pos] >> 8)); hRow[string.Format("temp_{0:00}", (i * 2) + 2)] = Convert.ToDouble((byte)(shorts[pos++] >> 0)); } // Cell Voltage for (int i = 0; i < cell_qty; i++) { hRow[string.Format("cell_v_{0:00}", i + 1)] = Convert.ToDecimal(shorts[pos++]) / 1000m; } //hRow["SOH"] = shorts[pos++] / 1m; BmsHistoryTable.Rows.Add(hRow); } catch (Exception e) { MessageBox.Show(e.Message); } } hist_no++; curr_addr += 0x20; RequestHistoryData(curr_addr); } #endregion #region BUTTON EVENT private void btnClose_Click(object sender, EventArgs e) { Close(); } private void btnBmsHistoryReq_Click(object sender, EventArgs e) { if (stopReq) { BmsHistoryTable.Rows.Clear(); stopReq = false; hist_no = 1; curr_addr = 0x0200; TmrCheckReq.Enabled = true; RequestHistoryData(curr_addr); btnBmsHistoryReq.Text = "STOP"; } else { StopAndFinish("User Stop Request"); } } private void RequestHistoryData(ushort haddr) { byte[] crc; ushort addr = haddr; // Normal mode --> Bootloader mode byte[] sdata = new byte[8]; sdata[0] = (byte)SystemId; // Dev Address sdata[1] = 0x03; // Function Code sdata[2] = (byte)(addr >> 8); // Address H sdata[3] = (byte)(addr >> 0); // Address L sdata[4] = 0x00; // Number of Register H sdata[5] = 0x35; // Number of Register L crc = csSerialCommFunction.GetCRC(sdata, 6); sdata[6] = crc[1]; // CRCH sdata[7] = crc[0]; // CRCL if (stopReq == false) { OnSendUartData?.Invoke(addr, sdata, false, 1); ReqDateTime = DateTime.Now; } } private void StopAndFinish(string reason = null) { stopReq = true; TmrCheckReq.Enabled = false; btnBmsHistoryReq.Text = "Request"; req_timeoutCount = 0; gvBmsHistory.BestFitColumns(); gvBmsHistory.ClearSelection(); if (!string.IsNullOrEmpty(reason)) MessageBox.Show($"Finished: {reason}"); else MessageBox.Show("Finished BMS History Read"); } private void btnExportExcel_Click(object sender, EventArgs e) { SaveFileDialog sDialog = new SaveFileDialog(); sDialog.Title = "Select save file"; sDialog.DefaultExt = "xlsx"; sDialog.Filter = "Excel files (*.xlsx)|*.xlsx|All files (*.*)|*.*"; if (sDialog.ShowDialog() == DialogResult.OK) { string filename = sDialog.FileName; try { //csExcelExport.ExportToExcelExt(BmsHistoryTable, filename); DataTable[] export = new DataTable[1]; export[0] = BmsHistoryTable; csExcelFunction.ExportDgvWithClosedXml(filename, export); MessageBox.Show("Complete Export Excel File", "Result", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show(ex.Message, "Exception", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } #endregion #region GRID DISPLAY private void gvBmsHistory_CustomColumnDisplayText(object sender, DevExpress.XtraGrid.Views.Base.CustomColumnDisplayTextEventArgs e) { if (e.Column.FieldName == "status") { int status = Convert.ToInt32(e.Value); switch (status) { case 0: e.DisplayText = "Standby"; break; case 1: e.DisplayText = "Charging"; break; case 2: e.DisplayText = "Discharging"; break; case 4: e.DisplayText = "Protect"; break; case 8: e.DisplayText = "Charging-Lmt"; break; case 0x32: e.DisplayText = "Power-On"; break; default: e.DisplayText = string.Format("Unknown({0:X4})", status); break; } } } #endregion #region TIMER EVENT private void TmrCheckReq_Tick(object sender, EventArgs e) { DateTime nDateTime = ReqDateTime.AddSeconds(1); if (DateTime.Now - ReqDateTime > TimeSpan.FromSeconds(1)) { req_timeoutCount++; if (req_timeoutCount >= 3) { StopAndFinish("Read Finished"); } } } #endregion #region FORM EVENT private void fmxHistory_Load(object sender, EventArgs e) { //SetGridColumnFormat(); } #endregion } }