using LFP_Manager.DataStructure; using LFP_Manager.Function; using LFP_Manager.Utils; using System; using System.ComponentModel; using System.Data; using System.Data.SQLite; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Windows.Forms; namespace LFP_Manager.Threads { class csDbThread { #region DELEGATE public delegate void UpdateAlarmHistory(DataTable aAlarmHistoryData); #endregion #region VARIABLES CommConfig Config = new CommConfig(); CsDeviceData DeviceData; CsDeviceData OldDevData; DataTable CurrAlarmHistory; private Thread _dbProc; // ← 생성자에서 만들지 않음 private volatile bool _dbThreadEnd = true; private volatile bool Active = false; private readonly object _stateLock = new object(); private readonly ISynchronizeInvoke _ui; // 보통 Form 또는 Control private int mQty = 1; public event EventHandler OnPrint; public event UpdateAlarmHistory OnUpdateAlarmHistory = null; #endregion #region CONSTRUCTORS public csDbThread(CommConfig aConfig, ISynchronizeInvoke uiInvoker, CsDeviceData aModuleData) { Config = aConfig; DeviceData = aModuleData; OldDevData = new CsDeviceData(); _ui = uiInvoker; } public void Dispose() { Stop(); } public bool Start(CommConfig aConfig) { if (_dbProc != null && _dbProc.IsAlive) return false; Config = aConfig; mQty = (Config.CommType == csConstData.CommType.COMM_RS485) ? Config.ModuleQty : 1; if (_dbProc != null && _dbProc.IsAlive) return true; // 이미 동작 중 _dbThreadEnd = false; Active = true; // DB 경로 준비 (예외는 루프에서 잡히지만 시작 시도 전 준비) try { string modelname = csConstData.UART_MODEL[Config.UartModelIndex]; csDbUtils.LogDbCreate(modelname); DbCreate(modelname, DateTime.Now); // 최초 활성 진입 if (Active) { // START 정보 기록 _ = BmsAlarmDataInsert(modelname, (int)DbConstData.Alarm.START, (int)DbConstData.Flag.INFORMATION, DeviceData.ModuleData[0], DateTime.Now); } } catch (Exception ex) { RaiseOnPrint($"DB init error: {ex.Message}"); } _dbProc = new Thread(dbThreadProcess) { IsBackground = true, Name = "DbThread" }; _dbProc.Start(); return true; } public void Stop() { Active = false; _dbThreadEnd = true; var t = _dbProc; if (t != null && t.IsAlive) { for (int i = 0; i < 5 && t.IsAlive; i++) t.Join(200); // 최대 약 1초 } _dbProc = null; // ★ 종료 직전 1회 저장/알람 기록 FlushNow("Stop"); } #endregion #region SAFETY RAISE HELPER private void RaiseOnUpdateAlarmHistory(DataTable dt) { if (_ui != null && _ui.InvokeRequired) _ui.BeginInvoke(new Action(() => OnUpdateAlarmHistory?.Invoke(dt)), null); else OnUpdateAlarmHistory?.Invoke(dt); } private void RaiseOnPrint(string msg) { var handler = OnPrint; if (handler == null) return; var args = new PrintEventArgs(0, msg); if (_ui != null && _ui.InvokeRequired) { try { _ui.BeginInvoke(new Action(() => handler(this, args)), null); } catch { } } else { handler(this, args); } } #endregion #region DB THREAD private void dbThreadProcess() { int periodSec = Math.Max(1, Config.DbLogPeriod); // 0 방지 string modelName = csConstData.UART_MODEL[Config.UartModelIndex]; // 마지막 기록 시각 (UTC 권장; 로컬도 무방하지만 일관성 위해 UTC 사용) DateTime lastLogUtc = DateTime.UtcNow; try { CurrAlarmHistory = csDbUtils.GetBmsAlarmDataByDataTable(modelName, DateTime.Now, ""); RaiseOnUpdateAlarmHistory(CurrAlarmHistory); // UI 마샬링 버전 } catch (Exception ex) { csLog.SystemDbErrorLog(Config, ex.Message, DateTime.Now, Application.ExecutablePath); } while (!_dbThreadEnd) { if (Active) { DateTime nowLocal = DateTime.Now; DateTime nowUtc = DateTime.UtcNow; // “초 변경” + “주기 정합” 검사 // ★ 경과 시간 기반 주기 체크 (초 단위 주기 모두 정확) if ((nowUtc - lastLogUtc).TotalSeconds >= periodSec) { // Database Log Process try { CsDeviceData snapshot; lock (_stateLock) { snapshot = DeviceData.DeepCopy(); } int count = Math.Max(1, mQty); for (int i = 0; i < count; i++) { // RS-485는 i+1, 단일 UART는 1 snapshot.ModuleData[i].mNo = (Config.CommType == csConstData.CommType.COMM_RS485) ? (i + 1) : 1; csDbUtils.BmsLogDataInsert(modelName, ref snapshot.ModuleData[i], nowLocal, 1); } } catch (Exception ex) { RaiseOnPrint($"DB loop error: {ex.Message}"); } finally { // ★ 마지막 로그 시각 갱신 (드리프트 최소화를 원하면 lastLogUtc += TimeSpan.FromSeconds(periodSec)) lastLogUtc = nowUtc; } } try { // Alarm History Log modelName = csConstData.UART_MODEL[Config.UartModelIndex]; csDbUtils.LogDbCreate(modelName); for (int i = 0; i < mQty; i++) { if (CheckStatusAndAlarm(i, modelName, Config, DeviceData.ModuleData[i], OldDevData.ModuleData[i])) { CurrAlarmHistory = csDbUtils.GetBmsAlarmDataByDataTable(modelName, DateTime.Now, ""); RaiseOnUpdateAlarmHistory(CurrAlarmHistory); } } } catch (Exception ex) { csLog.SystemDbErrorLog(Config, ex.Message, DateTime.Now, Application.ExecutablePath); } } Thread.Sleep(100); } } #endregion #region BMS ALARM DB INSERT FUNCTION public static bool BmsAlarmDataInsert1(string modelname, CommConfig aConfig, int aAlarmCode, int aAlarmStatus, CsDeviceData.DeviceModuleData mData, DateTime dateTime) { bool resultFlag = false; string result = ""; //public static string LogDbFileNameFormat = @"\{0}\{1}_{2}_LOG.DB"; string dbFilename = string.Format(csDbUtils.LogDbFileNameFormat , String.Format("{0:yyMM}", DateTime.Now) , String.Format("{0:yyMMdd}", DateTime.Now) , modelname ); string dbFilePath = Path.GetDirectoryName(Application.ExecutablePath) + csDbUtils.LogDbFilePath + dbFilename; if (Directory.Exists(Path.GetDirectoryName(dbFilePath)) == false) { throw new Exception("No Log DB file path"); } if (File.Exists(dbFilePath) == false) { throw new Exception(String.Format("No Log DB file - BmsAlarmDataInsertByModule ({0})", dbFilename)); } // Open database string strConn = @"Data Source=" + dbFilePath; using (SQLiteConnection connection = new SQLiteConnection(strConn)) { try { connection.Open(); csDbUtils.BeginTran(connection); // Insert System data using (SQLiteCommand command = connection.CreateCommand()) { //sSQL = "insert into TrendTable ( TrendStamp, TagName, TagValue) Values ( " + IntToStr(stamp) + "," + name + "," + value + ");"; command.CommandText = String.Format("INSERT INTO TAlarmHistory ("); command.CommandText += "create_date" // 0 + ", alarm_code" // 1 + ", alarm_status" // 2 + ", alarm_cname" // 3 + ", alarm_sname" // 4 + ", module_no" // 5 + ", ct_no" // 6 + ", alarm_param_1" // 7 + ", alarm_param_2" // 8 + ", alarm_param_3" // 9 ; command.CommandText = command.CommandText + ")" + " Values ("; int total = 10; for (int i = 0; i < (total - 1); i++) command.CommandText += "?,"; command.CommandText += "?);"; SQLiteParameter[] p = new SQLiteParameter[total]; for (int i = 0; i < total; i++) { if (i == 0) { p[i] = new SQLiteParameter(DbType.DateTime); } else { p[i] = new SQLiteParameter(); } command.Parameters.Add(p[i]); } int j = 0; p[j++].Value = dateTime; // 0 create_date p[j++].Value = aAlarmCode; // 1 alarm_code p[j++].Value = aAlarmStatus; // 2 alarm_status p[j++].Value = DbConstData.GetAlarmName(aAlarmCode); // 3 alarm_cname p[j++].Value = DbConstData.GetFlagName(aAlarmStatus); // 4 alarm_sname p[j++].Value = mData.mNo; // 5 module_no switch (aAlarmCode) { case 0: // Cell Under Voltage p[j++].Value = mData.AvgData.minCellNum; // 6 ct_no p[j++].Value = mData.AvgData.minCellVoltage; // 7 alarm_param_1 p[j++].Value = 0; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 1: // Cell Over Voltage p[j++].Value = mData.AvgData.maxCellNum; // 6 ct_no p[j++].Value = mData.AvgData.maxCellVoltage; // 7 alarm_param_1 p[j++].Value = 0; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 2: // Module Under Voltage p[j++].Value = 0; // 6 ct_no p[j++].Value = mData.ValueData.voltage; // 7 alarm_param_1 p[j++].Value = 0; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 3: // Module Over Voltage p[j++].Value = 0; // 6 ct_no p[j++].Value = mData.ValueData.voltage; // 7 alarm_param_1 p[j++].Value = 0; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 4: // Charge High Temperature p[j++].Value = mData.AvgData.maxTempNum; // 6 ct_no p[j++].Value = mData.AvgData.maxTemp; // 7 alarm_param_1 p[j++].Value = 0; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 5: // Discharge High Temperature p[j++].Value = mData.AvgData.maxTempNum; // 6 ct_no p[j++].Value = mData.AvgData.maxTemp; // 7 alarm_param_1 p[j++].Value = 0; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 6: // Charge Low Temperature p[j++].Value = mData.AvgData.minTempNum; // 6 ct_no p[j++].Value = mData.AvgData.minTemp; // 7 alarm_param_1 p[j++].Value = 0; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 7: // Discharge Low Temperature p[j++].Value = mData.AvgData.minTempNum; // 6 ct_no p[j++].Value = mData.AvgData.minTemp; // 7 alarm_param_1 p[j++].Value = 0; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 8: // Charge Over Current p[j++].Value = 0; // 6 ct_no p[j++].Value = mData.ValueData.current; // 7 alarm_param_1 p[j++].Value = 0; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 9: // Discharge Over Current p[j++].Value = 0; // 6 ct_no p[j++].Value = mData.ValueData.current; // 7 alarm_param_1 p[j++].Value = 0; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 10: // Low SOC p[j++].Value = 0; // 6 ct_no p[j++].Value = mData.ValueData.SOC; // 7 alarm_param_1 p[j++].Value = 0; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 11: // Low SOH p[j++].Value = 0; // 6 ct_no p[j++].Value = mData.ValueData.SOH; // 7 alarm_param_1 p[j++].Value = 0; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 12: // Cell Voltage Difference p[j++].Value = 0; // 6 ct_no p[j++].Value = mData.AvgData.minCellVoltage; // 7 alarm_param_1 p[j++].Value = mData.AvgData.maxCellVoltage; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 13: // Short Circuit p[j++].Value = 0; // 6 ct_no p[j++].Value = mData.ValueData.voltage; // 7 alarm_param_1 p[j++].Value = mData.ValueData.current; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 14: // Module Comm Fail p[j++].Value = 0; // 6 ct_no p[j++].Value = mData.ValueData.voltage; // 7 alarm_param_1 p[j++].Value = mData.ValueData.current; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 15: // Cell Voltage Measurement Error p[j++].Value = 0; // 6 ct_no p[j++].Value = mData.AvgData.minCellVoltage; // 7 alarm_param_1 p[j++].Value = mData.AvgData.maxCellVoltage; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 16: // Temperature Measurement Error p[j++].Value = 0; // 6 ct_no p[j++].Value = mData.AvgData.minTemp; // 7 alarm_param_1 p[j++].Value = mData.AvgData.maxTemp; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 17: // Battery Cell Unbalance p[j++].Value = 0; // 6 ct_no p[j++].Value = mData.AvgData.minCellVoltage; // 7 alarm_param_1 p[j++].Value = mData.AvgData.maxCellVoltage; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 18: // Emergency Alarm p[j++].Value = 0; // 6 ct_no p[j++].Value = mData.ValueData.voltage; // 7 alarm_param_1 p[j++].Value = mData.ValueData.current; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; case 21: // STS_STAND_BY case 22: // STS_CHARING case 23: // STS_DISCHARGING case 24: // STS_FLOAT_CHARGING p[j++].Value = 0; // 6 ct_no p[j++].Value = mData.ValueData.voltage; // 7 alarm_param_1 p[j++].Value = mData.ValueData.current; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; default: // STS_UNKNOWN p[j++].Value = 0; // 6 ct_no p[j++].Value = 0; // 7 alarm_param_1 p[j++].Value = 0; // 8 alarm_param_2 p[j++].Value = 0; // 9 alarm_param_3 break; } command.ExecuteNonQuery(); csDbUtils.CommitTran(connection); resultFlag = true; } } catch (Exception e) { result = e.Message; } finally { if (connection.State == ConnectionState.Open) { connection.Close(); } if (result != "") { throw new Exception(result); } } } return resultFlag; } #endregion #region ALARM STATUS CHECK private bool CheckStatusAndAlarm(int mNo, string modelName, CommConfig aConfig, CsDeviceData.DeviceModuleData nModule, CsDeviceData.DeviceModuleData oModule) { bool eventFlag = false; if (nModule == null || oModule == null) return false; // 현재/이전 경고/보호/에러 비트 bool[] wNew = csUtils.UInt16ToBitArray((ushort)nModule.StatusData.warning); bool[] pNew = csUtils.UInt16ToBitArray((ushort)nModule.StatusData.protect); bool[] eNew = csUtils.UInt16ToBitArray((ushort)nModule.StatusData.errorCode); bool[] wOld = csUtils.UInt16ToBitArray((ushort)oModule.StatusData.warning); bool[] pOld = csUtils.UInt16ToBitArray((ushort)oModule.StatusData.protect); bool[] eOld = csUtils.UInt16ToBitArray((ushort)oModule.StatusData.errorCode); var alarmData = new THistoryAlarmData(); DateTime now = DateTime.Now; // 시스템 통신 상태 변화 기록 if (oModule.CommFail != nModule.CommFail) { eventFlag |= BmsAlarmDataInsert(modelName, (int)DbConstData.Alarm.COMM_FAIL, nModule.CommFail ? (int)DbConstData.Flag.WARNING : (int)DbConstData.Flag.RELEASE, nModule, now); oModule.CommFail = nModule.CommFail; } // 0..15 비트에 대한 경고/보호 엣지 검사 for (int i = 0; i < 16; i++) { alarmData.newFdata = pNew[i]; alarmData.oldFdata = pOld[i]; alarmData.newWdata = wNew[i]; alarmData.oldWdata = wOld[i]; switch (i) { case 0: // Over Voltage 0X0001 eventFlag |= InsertAlarmHistory(modelName, alarmData, (int)DbConstData.Alarm.OVER_VOLTAGE, nModule, now); break; case 1: // Cell Over Voltage 0x0002 eventFlag |= InsertAlarmHistory(modelName, alarmData, (int)DbConstData.Alarm.CELL_OVER_VOLTAGE, nModule, now); break; case 2: // Under Voltage 0x0004 eventFlag |= InsertAlarmHistory(modelName, alarmData, (int)DbConstData.Alarm.UNDER_VOLTAGE, nModule, now); break; case 3: // Cell Under Voltage 0x0008 eventFlag |= InsertAlarmHistory(modelName, alarmData, (int)DbConstData.Alarm.CELL_UNDER_VOLTAGE, nModule, now); break; case 4: // Charge Over Current 0x0010 eventFlag |= InsertAlarmHistory(modelName, alarmData, (int)DbConstData.Alarm.CHARGE_OVER_CURRENT, nModule, now); break; case 5: // Discharge Over Current 0x0020 eventFlag |= InsertAlarmHistory(modelName, alarmData, (int)DbConstData.Alarm.DISCHARGE_OVER_CURRENT, nModule, now); break; case 6: // Reserved 0x0040 break; case 7: // MOSFET Over Temperature 0x0080 eventFlag |= InsertAlarmHistory(modelName, alarmData, (int)DbConstData.Alarm.MOSFET_OVER_TERMPERATURE, nModule, now); break; case 8: // Charge Over Temperature 0x0100 eventFlag |= InsertAlarmHistory(modelName, alarmData, (int)DbConstData.Alarm.CHARGE_OVER_TEMPERATURE, nModule, now); break; case 9: // Discharge Over Temperature 0x0200 eventFlag |= InsertAlarmHistory(modelName, alarmData, (int)DbConstData.Alarm.DISCHARGE_OVER_TEMPERATURE, nModule, now); break; case 10: // Charge Low Temperature 0x0400 eventFlag |= InsertAlarmHistory(modelName, alarmData, (int)DbConstData.Alarm.CHARGE_LOW_TEMPERATURE, nModule, now); break; case 11: // Discharge Low Temperature 0x0800 eventFlag |= InsertAlarmHistory(modelName, alarmData, (int)DbConstData.Alarm.DISCHARGE_LOW_TEMPERATURE, nModule, now); break; case 12: // Low Capacity(SOC) 0x1000 eventFlag |= InsertAlarmHistory(modelName, alarmData, (int)DbConstData.Alarm.LOW_SOC, nModule, now); break; case 13: // Short Circuit 0x2000 eventFlag |= InsertAlarmHistory(modelName, alarmData, (int)DbConstData.Alarm.SHORT_CIRCUIT, nModule, now); break; case 14: // CB OFF 0x4000 eventFlag |= InsertAlarmHistory(modelName, alarmData, (int)DbConstData.Alarm.CB_OFF, nModule, now); break; } } // Update last alarm status oModule.StatusData.warning = nModule.StatusData.warning; oModule.StatusData.protect = nModule.StatusData.protect; // 0..15 비트에 대한 Error 엣지 검사 for (int i = 0; i < 16; i++) { alarmData.newEdata = eNew[i]; alarmData.oldEdata = eOld[i]; switch (i) { case 0: // Voltage Measurement Error 0X0001 eventFlag |= InsertErrorHistory(modelName, alarmData, (int)DbConstData.Alarm.VOLTAGE_MEASUREMENT_ERROR, nModule, now); break; case 1: // Temperature Measurement Error 0x0002 eventFlag |= InsertErrorHistory(modelName, alarmData, (int)DbConstData.Alarm.TEMPERATURE_MEASUREMENT_ERROR, nModule, now); break; case 4: // Battery Cell Unbalance 0x0010 eventFlag |= InsertErrorHistory(modelName, alarmData, (int)DbConstData.Alarm.BATTERY_CELL_UNBALANCE, nModule, now); break; case 5: // Emergency Alarm 0x0020 eventFlag |= InsertErrorHistory(modelName, alarmData, (int)DbConstData.Alarm.EMERGENCY_ALARM, nModule, now); break; default: // Reserved Error break; } } // Update last alarm status oModule.StatusData.errorCode = nModule.StatusData.errorCode; // Check Status Change if (oModule.StatusData.status != nModule.StatusData.status) { switch (nModule.StatusData.status) { case 0: // STANDBY eventFlag = BmsAlarmDataInsert(modelName, (int)DbConstData.Alarm.STS_STAND_BY, (int)DbConstData.Flag.STS_CHANGED, nModule, now); break; case 1: // CHARGING eventFlag = BmsAlarmDataInsert(modelName, (int)DbConstData.Alarm.STS_CHARGING, (int)DbConstData.Flag.STS_CHANGED, nModule, now); break; case 2: // DISCHARGING eventFlag = BmsAlarmDataInsert(modelName, (int)DbConstData.Alarm.STS_DISCHARGING, (int)DbConstData.Flag.STS_CHANGED, nModule, now); break; case 4: // PROTECT eventFlag = BmsAlarmDataInsert(modelName, (int)DbConstData.Alarm.STS_PROTECTED, (int)DbConstData.Flag.STS_CHANGED, nModule, now); break; default: // UNKNOWN eventFlag = BmsAlarmDataInsert(modelName, (int)DbConstData.Alarm.STS_UNKNOWN, (int)DbConstData.Flag.STS_CHANGED, nModule, now); break; } oModule.StatusData.status = nModule.StatusData.status; } if (eventFlag) { csDbUtils.BmsLogDataInsert(modelName, ref nModule, now, 1); } return eventFlag; } private bool InsertAlarmHistory(string modelName, THistoryAlarmData AlarmData, int alarmCode, CsDeviceData.DeviceModuleData mData, DateTime dateTime) { bool resultFlag = false; // Check protection if (AlarmData.oldFdata != AlarmData.newFdata) { if (AlarmData.newFdata) { // Faulty resultFlag = BmsAlarmDataInsert(modelName, alarmCode, (int)DbConstData.Flag.PROTECT, mData, dateTime); } else { if (AlarmData.oldWdata != AlarmData.newWdata) { if (AlarmData.newWdata) { // Warning resultFlag = BmsAlarmDataInsert(modelName, alarmCode, (int)DbConstData.Flag.WARNING, mData, dateTime); } else { // Release resultFlag = BmsAlarmDataInsert(modelName, alarmCode, (int)DbConstData.Flag.RELEASE, mData, dateTime); } } else { if (AlarmData.newWdata == false) { // Release resultFlag = BmsAlarmDataInsert(modelName, alarmCode, (int)DbConstData.Flag.RELEASE, mData, dateTime); } } } } else { if (AlarmData.oldWdata != AlarmData.newWdata) { if (AlarmData.newWdata) { // Warning resultFlag = BmsAlarmDataInsert(modelName, alarmCode, (int)DbConstData.Flag.WARNING, mData, dateTime); //IDInsert("", alarmCode, CsDbConstData.DB_ALARM.FLAG_WARNING); } else { if (AlarmData.newFdata == false) { // Release resultFlag = BmsAlarmDataInsert(modelName, alarmCode, (int)DbConstData.Flag.RELEASE, mData, dateTime); //IDInsert("", alarmCode, CsDbConstData.DB_ALARM.FLAG_RELEASE); } } } } return resultFlag; } private bool InsertErrorHistory(string modelName, THistoryAlarmData AlarmData, int alarmCode, CsDeviceData.DeviceModuleData mData, DateTime dateTime) { if (AlarmData.oldEdata == AlarmData.newEdata) return false; int status = AlarmData.newEdata ? (int)DbConstData.Flag.ERROR : (int)DbConstData.Flag.RELEASE; return BmsAlarmDataInsert(modelName, alarmCode, status, mData, dateTime); } #endregion #region BMS ALARM DB INSERT FUNCTION (C# 7.3 compatible) public static bool BmsAlarmDataInsert( string modelname, int aAlarmCode, int aAlarmStatus, CsDeviceData.DeviceModuleData mData, DateTime dateTime) { // 파일명/경로 안전 조합 string dbFilename = string.Format( csDbUtils.LogDbFileNameFormat, DateTime.Now.ToString("yyMM"), DateTime.Now.ToString("yyMMdd"), modelname); string exeDir = Path.GetDirectoryName(Application.ExecutablePath); if (string.IsNullOrEmpty(exeDir)) exeDir = AppDomain.CurrentDomain.BaseDirectory; string logPath = (csDbUtils.LogDbFilePath ?? string.Empty) .TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); string dbDir = csUtils.CombineSafe(exeDir, logPath); string dbFilePath = csUtils.CombineSafe(dbDir, dbFilename); if (!Directory.Exists(Path.GetDirectoryName(dbFilePath))) throw new Exception("No Log DB file path"); if (!File.Exists(dbFilePath)) throw new Exception(string.Format("No Log DB file - BmsAlarmDataInsertByModule ({0})", dbFilename)); // INSERT 대상 컬럼(총 10개) // create_date, alarm_code, alarm_status, alarm_cname, alarm_sname, // module_no, ct_no, alarm_param_1, alarm_param_2, alarm_param_3 const int total = 10; using (var connection = new SQLiteConnection(@"Data Source=" + dbFilePath)) { connection.Open(); // ★ 성능/동시성 PRAGMA 적용 (WAL + busy_timeout 등) — C# 7.3 호환 csDbUtils.SqlitePragmaHelper.ApplyPragmas(connection, true); // forWriter: true using (var tran = connection.BeginTransaction()) { try { using (var command = connection.CreateCommand()) { command.Transaction = tran; command.CommandText = "INSERT INTO TAlarmHistory (" + "create_date, alarm_code, alarm_status, alarm_cname, alarm_sname, " + "module_no, ct_no, alarm_param_1, alarm_param_2, alarm_param_3" + ") VALUES (" + string.Join(",", Enumerable.Repeat("?", total)) + ");"; // 파라미터 생성 var p = new SQLiteParameter[total]; for (int i = 0; i < total; i++) { p[i] = (i == 0) ? new SQLiteParameter(DbType.DateTime) : new SQLiteParameter(); command.Parameters.Add(p[i]); } // 공통 메타 int j = 0; p[j++].Value = string.Format("{0:yyyy-MM-dd HH:mm:ss}", dateTime); // 0 create_date p[j++].Value = aAlarmCode; // 1 alarm_code p[j++].Value = aAlarmStatus; // 2 alarm_status p[j++].Value = DbConstData.GetAlarmName(aAlarmCode); // 3 alarm_cname p[j++].Value = DbConstData.GetFlagName(aAlarmStatus); // 4 alarm_sname p[j++].Value = mData.mNo; // 5 module_no // 알람별 세부 파라미터 계산 int ctNo = 0; double param1 = 0, param2 = 0, param3 = 0; switch (aAlarmCode) { case 0: // Cell Under Voltage ctNo = mData.AvgData.minCellNum; param1 = mData.AvgData.minCellVoltage; param2 = 0; break; case 1: // Cell Over Voltage ctNo = mData.AvgData.maxCellNum; param1 = mData.AvgData.maxCellVoltage; param2 = 0; break; case 2: // Module Under Voltage case 3: // Module Over Voltage ctNo = 0; param1 = mData.ValueData.voltage; param2 = 0; break; case 4: // Charge High Temperature case 5: // Discharge High Temperature ctNo = mData.AvgData.maxTempNum; param1 = mData.AvgData.maxTemp; param2 = 0; break; case 6: // Charge Low Temperature case 7: // Discharge Low Temperature ctNo = mData.AvgData.minTempNum; param1 = mData.AvgData.minTemp; param2 = 0; break; case 8: // Charge Over Current case 9: // Discharge Over Current ctNo = 0; param1 = mData.ValueData.current; param2 = 0; break; case 10: // Low SOC ctNo = 0; param1 = mData.ValueData.SOC; param2 = 0; break; case 11: // Low SOH ctNo = 0; try { var sohProp = mData.ValueData.GetType().GetProperty("SOH"); if (sohProp != null) { object v = sohProp.GetValue(mData.ValueData, null); param1 = Convert.ToDouble(v); } else { param1 = mData.ValueData.SOC; } } catch { param1 = mData.ValueData.SOC; } param2 = 0; break; case 12: // Cell Voltage Difference ctNo = 0; param1 = mData.AvgData.minCellVoltage; param2 = mData.AvgData.maxCellVoltage; break; case 13: // Short Circuit ctNo = 0; param1 = mData.ValueData.voltage; param2 = mData.ValueData.current; break; case 14: // Module Comm Fail ctNo = 0; param1 = 0; param2 = 0; break; case 15: // MOSFET Over Temerature ctNo = 0; param1 = mData.ValueData.MosTemperature; param2 = 0; break; case 16: // CB Off ctNo = 0; param1 = mData.ValueData.voltage; param2 = mData.ValueData.current; break; case 17: // Voltage Measurement Error ctNo = 0; param1 = mData.AvgData.minCellVoltage; param2 = mData.AvgData.maxCellVoltage; break; case 18: // Temperature Measurement Error ctNo = 0; param1 = mData.AvgData.minTemp; param2 = mData.AvgData.maxTemp; break; case 19: // Battery Cell Unbalance ctNo = 0; param1 = mData.AvgData.minCellVoltage; param2 = mData.AvgData.maxCellVoltage; break; case 20: // Emergency Alarm ctNo = 0; param1 = mData.ValueData.voltage; param2 = mData.ValueData.current; break; // 상태 코드(스냅샷 저장) — 오타 보정: CHARING → CHARGING case 21: // STS_STAND_BY case 22: // STS_CHARGING case 23: // STS_DISCHARGING case 24: // STS_FLOAT_CHARGING case 25: // STS_PRE_CHARGING ctNo = 0; param1 = mData.ValueData.voltage; param2 = mData.ValueData.current; break; default: // STS_UNKNOWN 등 ctNo = 0; param1 = 0; param2 = 0; param3 = 0; break; } p[j++].Value = ctNo; // 6 ct_no p[j++].Value = param1; // 7 alarm_param_1 p[j++].Value = param2; // 8 alarm_param_2 p[j++].Value = param3; // 9 alarm_param_3 command.ExecuteNonQuery(); } tran.Commit(); return true; } catch { try { tran.Rollback(); } catch { /* ignore */ } throw; } finally { if (connection.State == ConnectionState.Open) connection.Close(); } } } } #endregion #region CREATE HISTORY DB TABLE public static void DbCreate(string modelName, DateTime aDateTime) { // 일자 기준 DB 파일명 (yyMM/yyMMdd) string dbFilename = string.Format( csDbUtils.LogDbFileNameFormat, aDateTime.ToString("yyMM"), aDateTime.ToString("yyMMdd"), modelName); // 실행파일 경로 + 로그 DB 상대경로 + 파일명 안전 결합 string exeDir = Path.GetDirectoryName(Application.ExecutablePath); if (string.IsNullOrEmpty(exeDir)) exeDir = AppDomain.CurrentDomain.BaseDirectory; string logPath = (csDbUtils.LogDbFilePath ?? string.Empty) .TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); string dbDir = csUtils.CombineSafe(exeDir, logPath); string dbFilePath = csUtils.CombineSafe(dbDir, dbFilename); if (Directory.Exists(System.IO.Path.GetDirectoryName(dbFilePath)) == false) Directory.CreateDirectory(System.IO.Path.GetDirectoryName(dbFilePath)); if (File.Exists(dbFilePath) == false) // Create database SQLiteConnection.CreateFile(dbFilePath); // Open database string strConn = @"Data Source=" + dbFilePath; using (var connection = new SQLiteConnection(strConn)) { connection.Open(); try { // Create table using (SQLiteCommand command = connection.CreateCommand()) { command.CommandText = DbConstData.Database.CreateTable; command.ExecuteNonQuery(); } } catch (Exception) { } finally { connection.Close(); } } } #endregion #region FLUSH ALARM HISTORY // csDbThread 클래스 내부 어딘가(예: #region DB THREAD 아래) private void FlushNow(string reason = null) { try { if (Config == null) return; // 모델/수량 재확인 var modelName = csConstData.UART_MODEL[Config.UartModelIndex]; mQty = (Config.CommType == csConstData.CommType.COMM_RS485) ? Config.ModuleQty : 1; // 스냅샷 확보 CsDeviceData snapshot; lock (_stateLock) { snapshot = DeviceData != null ? DeviceData.DeepCopy() : null; } if (snapshot == null) return; var now = DateTime.Now; var anyAlarm = false; // 모듈 값 1회 저장 int count = Math.Max(1, mQty); for (int i = 0; i < count; i++) { // RS-485는 i+1, 단일 UART는 1 snapshot.ModuleData[i].mNo = (Config.CommType == csConstData.CommType.COMM_RS485) ? (i + 1) : 1; csDbUtils.BmsLogDataInsert(modelName, ref snapshot.ModuleData[i], now, 1); } // STOP 정보 기록 anyAlarm |= BmsAlarmDataInsert(modelName, (int)DbConstData.Alarm.STOP, (int)DbConstData.Flag.INFORMATION, snapshot.ModuleData[0], now); if (anyAlarm) { CurrAlarmHistory = csDbUtils.GetBmsAlarmDataByDataTable(modelName, DateTime.Now, ""); RaiseOnUpdateAlarmHistory(CurrAlarmHistory); } if (!string.IsNullOrEmpty(reason)) RaiseOnPrint($"DB flushed ({reason})."); } catch (Exception ex) { RaiseOnPrint($"Flush error{(reason != null ? "(" + reason + ")" : "")}: {ex.Message}"); // 치명적 예외는 로깅만 하고 흐름 유지 csLog.SystemDbErrorLog(Config, ex.Message, DateTime.Now, Application.ExecutablePath); } } #endregion } }