using System; using System.Windows; using System.Windows.Input; using System.Windows.Threading; using leak_test_project.Infrastructure; using leak_test_project.Models; using leak_test_project.Services; using leak_test_project.Utils; using leak_test_project.ViewModels.Core; namespace leak_test_project.ViewModels { /// /// Home 화면의 비즈니스 로직을 담당하는 ViewModel. /// 좌/우 채널 통신 관리, 측정값 표시, 판정 로직, 자동 시험 프로세스를 포함. /// public class HomeViewModel : ObservableObject, IDisposable { private SentinelC28Service _sentinelService; private SerialProvider _sentinelSerial; private readonly Dispatcher _dispatcher; // 자동 시험 프로세스 관련 private IDioBoard _dioBoard; private IIdSensorService _leftZmdi; private IIdSensorService _rightZmdi; private SerialProvider _leftZmdiSerial; private SerialProvider _rightZmdiSerial; private TestProcessService _testProcess; #region Left Channel Properties private string _leftValue = ""; public string LeftValue { get => _leftValue; set => SetProperty(ref _leftValue, value); } private string _leftJudgment = ""; public string LeftJudgment { get => _leftJudgment; set => SetProperty(ref _leftJudgment, value); } private bool _isLeftOk; public bool IsLeftOk { get => _isLeftOk; set => SetProperty(ref _isLeftOk, value); } private string _leftStatus = ""; public string LeftStatus { get => _leftStatus; set => SetProperty(ref _leftStatus, value); } private string _leftStartTime = ""; public string LeftStartTime { get => _leftStartTime; set => SetProperty(ref _leftStartTime, value); } private string _leftId = ""; public string LeftId { get => _leftId; set => SetProperty(ref _leftId, value); } private string _leftLowId = ""; public string LeftLowId { get => _leftLowId; set => SetProperty(ref _leftLowId, value); } private string _leftDate = ""; public string LeftDate { get => _leftDate; set => SetProperty(ref _leftDate, value); } private string _leftSerial_ = ""; public string LeftSerialNo { get => _leftSerial_; set => SetProperty(ref _leftSerial_, value); } private string _leftMcLine = ""; public string LeftMcLine { get => _leftMcLine; set => SetProperty(ref _leftMcLine, value); } private string _leftItem = ""; public string LeftItem { get => _leftItem; set => SetProperty(ref _leftItem, value); } private string _leftError = ""; public string LeftError { get => _leftError; set => SetProperty(ref _leftError, value); } #endregion #region Right Channel Properties private string _rightValue = ""; public string RightValue { get => _rightValue; set => SetProperty(ref _rightValue, value); } private string _rightJudgment = ""; public string RightJudgment { get => _rightJudgment; set => SetProperty(ref _rightJudgment, value); } private bool _isRightOk; public bool IsRightOk { get => _isRightOk; set => SetProperty(ref _isRightOk, value); } private string _rightStatus = ""; public string RightStatus { get => _rightStatus; set => SetProperty(ref _rightStatus, value); } private string _rightStartTime = ""; public string RightStartTime { get => _rightStartTime; set => SetProperty(ref _rightStartTime, value); } private string _rightId = ""; public string RightId { get => _rightId; set => SetProperty(ref _rightId, value); } private string _rightLowId = ""; public string RightLowId { get => _rightLowId; set => SetProperty(ref _rightLowId, value); } private string _rightDate = ""; public string RightDate { get => _rightDate; set => SetProperty(ref _rightDate, value); } private string _rightSerial_ = ""; public string RightSerialNo { get => _rightSerial_; set => SetProperty(ref _rightSerial_, value); } private string _rightMcLine = ""; public string RightMcLine { get => _rightMcLine; set => SetProperty(ref _rightMcLine, value); } private string _rightItem = ""; public string RightItem { get => _rightItem; set => SetProperty(ref _rightItem, value); } private string _rightError = ""; public string RightError { get => _rightError; set => SetProperty(ref _rightError, value); } #endregion #region Spec Properties private string _specUL = ""; public string SpecUL { get => _specUL; set => SetProperty(ref _specUL, value); } private string _specLL = ""; public string SpecLL { get => _specLL; set => SetProperty(ref _specLL, value); } #endregion public HomeViewModel(IDioBoard dioBoard) { _dioBoard = dioBoard; _dispatcher = Dispatcher.CurrentDispatcher; var config = ConfigManager.Current; UpdateSpecFromConfig(config); InitializeCommunication(config); InitializeTestProcess(config); ConfigManager.ConfigChanged += OnConfigChanged; } private void OnConfigChanged(object sender, EventArgs e) { _dispatcher.Invoke(() => { var newConfig = ConfigManager.Current; UpdateSpecFromConfig(newConfig); ApplyConfig(); }); } private void UpdateSpecFromConfig(AppConfig config) { SpecUL = config.SpecUL.ToString("F2"); SpecLL = config.SpecLL.ToString("F2"); } public void ApplyConfig() { CleanupAll(); // 통신 재시작 전 기존 오류 및 상태 메시지 초기화 LeftError = ""; RightError = ""; LeftStatus = "통신 대기 중"; RightStatus = "통신 대기 중"; var config = ConfigManager.Current; InitializeCommunication(config); InitializeTestProcess(config); } private void CleanupAll() { // 1. 시험 프로세스 정지 (스레드 종료 및 이벤트 해제) _testProcess?.Dispose(); _testProcess = null; // 2. ZMDI 시리얼 포트 및 서비스 해제 _leftZmdi?.Dispose(); _rightZmdi?.Dispose(); _leftZmdiSerial?.Dispose(); _rightZmdiSerial?.Dispose(); _leftZmdi = null; _rightZmdi = null; _leftZmdiSerial = null; _rightZmdiSerial = null; // 3. Sentinel C28 해제 _sentinelService?.Disconnect(); _sentinelSerial?.Dispose(); _sentinelService = null; _sentinelSerial = null; } private void InitializeCommunication(AppConfig config) { // Sentinel C28 (Leak Sensor) - 단일 포트 사용 _sentinelSerial = new SerialProvider(config.SensorPort, config.SensorBaudRate); _sentinelService = new SentinelC28Service(_sentinelSerial); _sentinelService.RawDataReceived += (s, data) => { System.Diagnostics.Debug.WriteLine($"[SENTINEL RAW] {data}"); }; _sentinelService.OnStreamingParsed += (s, data) => UpdateMeasurement(data); _sentinelService.OnFinalResultParsed += (s, data) => ProcessFinalResult(data); if (!_sentinelService.Connect()) { string msg = $"리크 센서 포트 연결 실패 ({config.SensorPort})"; LeftError = msg; RightError = msg; } } private void InitializeTestProcess(AppConfig config) { // DIO 보드 초기화 (MainViewModel에서 생성된 보드 사용) if (_dioBoard == null) return; // DIO 보드 에러 구독 _dioBoard.ErrorOccurred += (s, msg) => _dispatcher.Invoke(() => { LeftError = msg; RightError = msg; AppendLog(true, $"[DIO Board Error] {msg}"); }); // ID 센서 서비스 (LEFT/RIGHT) if (config.SelectedIdSensor == IdSensorType.Board4253) { _leftZmdiSerial = new SerialProvider(config.Board4253Port, config.Board4253BaudRate); var sharedService = new Board4253Service(_leftZmdiSerial) { TimeoutMs = config.Board4253Timeout }; _leftZmdi = new Board4253SensorService(sharedService, 0); _rightZmdi = new Board4253SensorService(sharedService, 1); } else { _leftZmdiSerial = new SerialProvider(config.LeftPort, config.ZmdiBaudRate); _rightZmdiSerial = new SerialProvider(config.RightPort, config.ZmdiBaudRate); _leftZmdi = new ZmdiSensorService(_leftZmdiSerial, 0); _rightZmdi = new ZmdiSensorService(_rightZmdiSerial, 1); } string sensorName = config.SelectedIdSensor == IdSensorType.Board4253 ? "4253 보드" : "ZMDI 센서"; string logPrefix = config.SelectedIdSensor == IdSensorType.Board4253 ? "[4253 Error]" : "[ZMDI Error]"; if (!_leftZmdi.Connect()) { string port = config.SelectedIdSensor == IdSensorType.Board4253 ? config.Board4253Port : config.LeftPort; string msg = $"{sensorName} 포트 연결 실패 ({port})"; LeftError = string.IsNullOrEmpty(LeftError) ? msg : $"{LeftError}\n{msg}"; } if (!_rightZmdi.Connect()) { string port = config.SelectedIdSensor == IdSensorType.Board4253 ? config.Board4253Port : config.RightPort; string msg = $"{sensorName} 포트 연결 실패 ({port})"; RightError = string.IsNullOrEmpty(RightError) ? msg : $"{RightError}\n{msg}"; } _leftZmdi.ProgressMessage += (s, msg) => _dispatcher.Invoke(() => LeftStatus = msg); _leftZmdi.ErrorMessage += (s, msg) => _dispatcher.Invoke(() => { LeftError = msg; AppendLog(true, $"{logPrefix} {msg}"); }); _rightZmdi.ProgressMessage += (s, msg) => _dispatcher.Invoke(() => RightStatus = msg); _rightZmdi.ErrorMessage += (s, msg) => _dispatcher.Invoke(() => { RightError = msg; AppendLog(false, $"{logPrefix} {msg}"); }); // 자동 시험 프로세스 _testProcess = new TestProcessService(_dioBoard, _leftZmdi, _rightZmdi, _sentinelService); _testProcess.ProgressChanged += (s, e) => _dispatcher.Invoke(() => { if (e.TestIndex == 0) LeftStatus = e.Message; else RightStatus = e.Message; }); _testProcess.ErrorOccurred += (s, e) => _dispatcher.Invoke(() => { if (e.TestIndex == 0) { LeftStatus = "오류 발생"; LeftError = e.Message; AppendLog(true, $"[ERROR] {e.Message}"); } else { RightStatus = "오류 발생"; RightError = e.Message; AppendLog(false, $"[ERROR] {e.Message}"); } }); _testProcess.ResultClearRequested += (s, testIndex) => _dispatcher.Invoke(() => { if (testIndex == 0) ClearLeftResult(); else ClearRightResult(); }); _testProcess.SensorReadComplete += (s, args) => _dispatcher.Invoke(() => { var d = args.Data; if (args.TestIndex == 0) { LeftId = d.ID; LeftLowId = d.LowID; LeftDate = d.Year > 0 ? $"{d.Year}/{d.Month}/{d.Day}" : ""; LeftSerialNo = d.Serial; LeftMcLine = d.McLine; LeftItem = d.Item; } else { RightId = d.ID; RightLowId = d.LowID; RightDate = d.Year > 0 ? $"{d.Year}/{d.Month}/{d.Day}" : ""; RightSerialNo = d.Serial; RightMcLine = d.McLine; RightItem = d.Item; } }); _testProcess.TestCompleted += (s, e) => _dispatcher.Invoke(() => { if (e.TestIndex == 0) { LeftValue = e.MeasuredValue; LeftJudgment = e.Judgment; IsLeftOk = e.Judgment == "OK"; LeftStatus = "시험 완료"; } else { RightValue = e.MeasuredValue; RightJudgment = e.Judgment; IsRightOk = e.Judgment == "OK"; RightStatus = "시험 완료"; } // SPEC 교차 검증 불일치 경고 if (e.SpecMismatch) { string side = e.TestIndex == 0 ? "LEFT" : "RIGHT"; string msg = $"SPEC 불일치 - 프로그램: {e.Judgment}, 센서: {e.SensorJudgment}"; if (e.TestIndex == 0) LeftError = msg; else RightError = msg; MessageBox.Show( "프로그램 스팩과 센서의 스팩이 서로 맞지 않습니다.", "SPEC 교차 검증 경고", MessageBoxButton.OK, MessageBoxImage.Warning); } }); _testProcess.Start(); } private void ClearLeftResult() { LeftValue = ""; LeftJudgment = ""; IsLeftOk = false; LeftStartTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); LeftId = ""; LeftLowId = ""; LeftDate = ""; LeftSerialNo = ""; LeftMcLine = ""; LeftItem = ""; LeftError = ""; } private void ClearRightResult() { RightValue = ""; RightJudgment = ""; IsRightOk = false; RightStartTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); RightId = ""; RightLowId = ""; RightDate = ""; RightSerialNo = ""; RightMcLine = ""; RightItem = ""; RightError = ""; } private void UpdateMeasurement(ParsedData data) { _dispatcher.Invoke(() => { // ChannelNo(C01=LEFT, C02=RIGHT)에 따라 데이터 라우팅 if (data.ChannelNo == "C01" || data.ChannelNo == "1") LeftValue = data.MeasuredValue.ToString("F3"); else if (data.ChannelNo == "C02" || data.ChannelNo == "2") RightValue = data.MeasuredValue.ToString("F3"); else { // 채널 정보가 없으면 양쪽 모두 갱신 LeftValue = data.MeasuredValue.ToString("F3"); RightValue = data.MeasuredValue.ToString("F3"); } }); } private void ProcessFinalResult(ParsedData data) { _dispatcher.Invoke(() => { double.TryParse(SpecUL, out double ul); double.TryParse(SpecLL, out double ll); string judgment = SentinelParser.EvaluateJudgment(data.MeasuredValue, ul, ll); bool isOk = judgment == "OK"; bool isLeft = (data.ChannelNo == "C01" || data.ChannelNo == "1"); string side = isLeft ? "LEFT" : "RIGHT"; if (isLeft) { LeftValue = data.MeasuredValue.ToString("F3"); LeftJudgment = judgment; IsLeftOk = isOk; LeftStatus = "TEST COMPLETE"; LeftStartTime = data.TestTime ?? ""; LeftDate = data.TestDate ?? ""; LeftId = data.UniqueId ?? ""; LeftLowId = data.LowID ?? ""; LeftSerialNo = data.SerialNo ?? ""; LeftMcLine = data.ChannelNo ?? ""; LeftItem = data.ProgramNo ?? ""; } else { RightValue = data.MeasuredValue.ToString("F3"); RightJudgment = judgment; IsRightOk = isOk; RightStatus = "TEST COMPLETE"; RightStartTime = data.TestTime ?? ""; RightDate = data.TestDate ?? ""; RightId = data.UniqueId ?? ""; RightLowId = data.LowID ?? ""; RightSerialNo = data.SerialNo ?? ""; RightMcLine = data.ChannelNo ?? ""; RightItem = data.ProgramNo ?? ""; } // SPEC 교차 검증 (C28 수동 수신 시) if (data.SensorJudgment != null) { string normalizedProgram = (judgment == "OK") ? "A" : "R"; string normalizedSensor = data.SensorJudgment; if (normalizedProgram != normalizedSensor) { string msg = $"SPEC 불일치 - 프로그램: {judgment}, 센서: {data.SensorJudgment}"; if (isLeft) LeftError = msg; else RightError = msg; } } // 로그 기록 (일일 CSV 파일) var inspectData = new InspectData { InspectDate = data.TestDate ?? DateTime.Now.ToString("yyyy-MM-dd"), InspectTime = data.TestTime ?? DateTime.Now.ToString("HH:mm:ss"), Channel = isLeft ? "LEFT" : "RIGHT", ProductId = data.UniqueId ?? "", MeasuredValue = data.MeasuredValue.ToString("F3"), Judgment = judgment, Mode = "양산", // 기본값 LineNo = data.ChannelNo ?? "", ProductType = data.ProgramNo ?? "", SpecUL = ul.ToString("F2"), SpecLL = ll.ToString("F2"), Retest = "N" }; FileLogger.LogInspectData(inspectData); }); } public async System.Threading.Tasks.Task TestReadIdAsync(int testIndex) { if (_testProcess != null) { await _testProcess.ExecuteSensorTestAsync(testIndex); } } private const int MaxLogLines = 500; private void AppendLog(bool isLeft, string message) { // 이 기능은 이제 Status/Error 필드로 대체되거나 파일 로그로 대체됨 // 현재는 UI에서 제거되었으므로 Debug 출력만 남김 System.Diagnostics.Debug.WriteLine($"[LOG][{(isLeft ? "LEFT" : "RIGHT")}] {message}"); _dispatcher.Invoke(() => { if (message.Contains("ERROR") || message.Contains("Error")) { if (isLeft) LeftError = message; else RightError = message; } else { if (isLeft) LeftStatus = message; else RightStatus = message; } }); } public void Dispose() { ConfigManager.ConfigChanged -= OnConfigChanged; CleanupAll(); _dioBoard?.Dispose(); } } }