diff --git a/docs/reports/계측기_저항_오버플로우_오류_분석_보고서.md b/docs/reports/계측기_저항_오버플로우_오류_분석_보고서.md new file mode 100644 index 0000000..273a655 --- /dev/null +++ b/docs/reports/계측기_저항_오버플로우_오류_분석_보고서.md @@ -0,0 +1,158 @@ +# [기술 보고서] 계측기 저항 측정 수치 변환 오류 분석 및 개선 방안 + +--- + +## 1. 개요 (Overview) + +| 항목 | 내용 | +| :--- | :--- | +| **보고서 제목** | 계측기 저항 측정 시 수치 변환 실패(`+9.90000000E+37`) 오류 원인 분석 및 개선안 | +| **발생 시스템** | 양산형 레이저 마킹 & 성능 검사 모니터링 시스템 (`marking_gui`) | +| **관련 장비** | Digital Multimeter (Keysight/Agilent 34465A 등) - 저항 측정 계측기 | +| **심각도** | **높음 (Major)** — 단선 제품 또는 프로브 미접촉 시 검사 프로세스 중단 및 시스템 오류 발생 | +| **작성 일자** | 2026년 9월 21일 | + +--- + +## 2. 현상 및 에러 상세 (Issue Description) + +### 2.1. 에러 메시지 +```text +[시스템 오류] +계측기 오류: [RESISTANCE] 수치 변환 실패 (값: '+9.90000000E+37'). 원본 응답: [+9.90000000E+37] +``` + +### 2.2. 발생 상황 +- 생산 라인 검사 시퀀스 실행 중, 전압 측정 후 저항 측정 단계(`MEAS:RES?`)에서 발생 +- 오류 발생 즉시 검사 시퀀스가 비정상 중단되고 시스템 오류 팝업 및 에러 배너가 출력됨 +- 검사 결과 판정(PASS/NG) 및 각인, 데이터베이스 저장이 진행되지 않고 정지됨 + +--- + +## 3. 근본 원인 분석 (Root Cause Analysis) + +본 장애는 **계측기 하드웨어 통신 규격(SCPI)**과 **애플리케이션 데이터 타입(`decimal`) 한계**의 불일치로 인해 발생한 문제입니다. + +```mermaid +flowchart TD + A["저항 측정 지그 접촉 불량 또는 제품 단선 (Open)"] --> B["계측기(DMM): Overload (OL) 감지"] + B --> C["SCPI 규격 응답 전송: '+9.90000000E+37'"] + C --> D["애플리케이션: Regex로 '+9.90000000E+37' 추출"] + D --> E{"decimal.TryParse() 시도"} + E -- "decimal 최대치(~7.9e28) 초과" --> F["Overflow / 파싱 실패 (false)"] + F --> G["InvalidOperationException 발생"] + G --> H["검사 중단 및 '시스템 오류' 팝업 표출"] +``` + +### 3.1. 계측기 하드웨어/통신 관점 (SCPI 규격 특성) +* **표준 SCPI Overload 응답:** Keysight 및 IEEE 488.2 호환 DMM 장비는 저항 측정 시 프로브가 분리되어 있거나, 제품 내부가 단선(Open Circuit)되어 저항값이 계측 가능 범위를 초과하는 경우 **`+9.90000000E+37`** ($9.9 \times 10^{37}$)을 반환하도록 규격화되어 있습니다. +* 즉, 이 값은 **통신 장애나 계측기 고장이 아니라 제품의 단선(Open) 또는 지그 미접촉(Overload) 상태**를 나타내는 정상적인 계측 응답입니다. + +### 3.2. 애플리케이션 코드 관점 (자료형 오버플로우) +* 문제 발생 코드 위치: `marking_gui/Services/EquipmentMeasurementService.cs` 내부 `ExtractFirstDecimal` 메서드 + +```csharp +// EquipmentMeasurementService.cs (Line 398~411) +private static decimal ExtractFirstDecimal(string response, string label) +{ + if (string.IsNullOrWhiteSpace(response)) + throw new InvalidOperationException($"[{label}] 계측기 응답이 비어있습니다 (응답 수신 실패)."); + + var match = Regex.Match(response, @"[-+]?\d+(?:\.\d+)?(?:[Ee][-+]?\d+)?"); + if (!match.Success) + throw new InvalidOperationException($"[{label}] 수치 파싱 실패. 원본 응답: [{response.Trim()}]"); + + // [문제 지점] decimal 타입으로 파싱 시도 + if (!decimal.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out decimal val)) + throw new InvalidOperationException($"[{label}] 수치 변환 실패 (값: '{match.Value}'). 원본 응답: [{response.Trim()}]"); + + return val; +} +``` + +* **자료형 한계 비교:** + | 자료형 | 최소값 ~ 최대값 범위 | 9.9e37 수용 여부 | + | :--- | :--- | :---: | + | **`decimal`** (현재 적용) | 약 $-7.9 \times 10^{28} \sim +7.9 \times 10^{28}$ | **불가 (Overflow)** | + | **`double`** (권장) | 약 $\pm 5.0 \times 10^{-324} \sim \pm 1.7 \times 10^{308}$ | **가능 (정상 수용)** | + +* 계측기 응답값 $9.9 \times 10^{37}$은 `decimal` 최대 표현 한계($7.9 \times 10^{28}$)를 초과하므로 `decimal.TryParse()`가 `false`를 반환하며 `InvalidOperationException`을 발생시킵니다. + +### 3.3. 비즈니스 로직 관점의 문제점 +* 제품이 단선된 불량품인 경우, 시스템은 이를 **불합격(NG)**으로 정상 판정하고 시퀀스를 마무리해야 합니다. +* 현재 구현은 불량 제품이 인입되었을 때 계측기 통신 에러로 인식하여 전체 공정이 정지되는 구조적 결함을 안고 있습니다. + +--- + +## 4. 영향도 및 리스크 평가 (Impact & Risk) + +1. **생산 라인 지연 및 정지 (Line Stoppage):** + - 접촉이 불안정하거나 단선 불량품이 유입될 때마다 작업자가 에러 창을 확인하고 수동 리셋해야 하므로 공정 지연이 발생합니다. +2. **불량품 판정 및 이력 누락:** + - 예외가 발생하여 `SaveAndFinalizeAsync`가 호출되지 못하므로, 해당 불량 시료의 검사 데이터가 데이터베이스에 남지 않습니다. + +--- + +## 5. 개선 및 해결 방안 (Action Plan) + +### 5.1. 수치 파싱 로직 개선 (`double` 기반 전환) +- `ExtractFirstDecimal`의 반환 타입 및 파싱 로직을 `decimal`에서 `double`로 변경하여 $10^{37}$ 스케일의 부동소수점 값을 에러 없이 파싱합니다. + +```csharp +private static double ExtractFirstNumber(string response, string label) +{ + if (string.IsNullOrWhiteSpace(response)) + throw new InvalidOperationException($"[{label}] 계측기 응답이 비어있습니다 (응답 수신 실패)."); + + var match = Regex.Match(response, @"[-+]?\d+(?:\.\d+)?(?:[Ee][-+]?\d+)?"); + if (!match.Success) + throw new InvalidOperationException($"[{label}] 수치 파싱 실패. 원본 응답: [{response.Trim()}]"); + + if (!double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double val)) + throw new InvalidOperationException($"[{label}] 수치 변환 실패 (값: '{match.Value}'). 원본 응답: [{response.Trim()}]"); + + return val; +} +``` + +### 5.2. Overload (OL / 단선) 상태 식별 및 비즈니스 로직 연계 +- 계측기 반환값이 $9.0 \times 10^{37}$ 이상일 경우 오버로드(OL, 단선) 상태로 감지합니다. +- **판정 연계:** + - 저항값을 규격 상한(예: 13 k$\Omega$)을 크게 상회하는 값(예: `99999.0` k$\Omega$)으로 처리하여 자연스러운 **저항 불합격(NG)** 판정을 유도합니다. +- **UI 표출 연계:** + - GUI 저항 표시창(`txtResistance`)에 지수 형태 대신 직관적인 **`O.L`** 또는 규격 상한 초과 수치를 표시합니다. + +```csharp +// 저항 측정 처리 예시 +double rawResistance = ExtractFirstNumber(rResponse, "RESISTANCE"); +double resistance; + +if (rawResistance >= 9.0e37) +{ + // 오버로드 (단선 / 오픈) 상태 감지 + resistance = 99999.0; // 상한 초과 값 부여 -> 자동 NG 판정 유도 + LoggerService.Warn("[Equipment] 저항 계측 Overload 감지 (단선/프로브 미접촉 상태)"); +} +else +{ + resistance = rawResistance / 1000.0; // Ω -> kΩ 정상 변환 +} +``` + +--- + +## 6. 검증 계획 (Verification Plan) + +| 검증 단계 | 검증 시나리오 | 예상 결과 | +| :--- | :--- | :--- | +| **1. 파서 단위 검증** | `+9.90000000E+37` 문자열을 파서에 입력 | 예외 없이 OL 값(99999.0)으로 안전하게 변환 확인 | +| **2. 오픈 상태 검증** | 지그에 제품을 장착하지 않고(프로브 개방) 검사 시작 버튼 클릭 | 시스템 오류 팝업 없이 저항 화면에 `O.L` 표시 및 최종 **`NG`** 판정 확인 | +| **3. 정상 상태 검증** | 정상 저항 규격(8.0 ~ 13.0 k$\Omega$) 제품 장착 후 검사 시작 | 기존과 동일하게 실제 측정 수치 표시 및 **`OK`** 판정 확인 | +| **4. DB 적재 검증** | 단선(NG) 발생 시 시퀀스 종료 후 DB 및 UI 상태 점검 | 시스템 멈춤 없이 다음 검사 대기 상태로 정상 복귀 확인 | + +--- + +## 7. 결론 및 건의사항 + +본 오류는 계측기의 하드웨어 고장이 아니라 **단선/오버로드 규격치 수신 시의 예외 처리 누락**에서 기인한 것입니다.\ +제안된 파싱 타입 변경 및 오버로드 핸들링 로직을 적용하면 단선 불량 발생 시에도 시스템 멈춤 없이 안정적으로 불합격 판정 및 공정을 지속할 수 있습니다. diff --git a/marking_gui/MainWindow.xaml.cs b/marking_gui/MainWindow.xaml.cs index 1d56374..e1f27c8 100644 --- a/marking_gui/MainWindow.xaml.cs +++ b/marking_gui/MainWindow.xaml.cs @@ -601,6 +601,23 @@ namespace marking_gui } }); } + + // 3. 4251 제품 ID 센서 보드 채널 OFF 및 하드웨어 리셋 + if (_sensorReader != null) + { + await System.Threading.Tasks.Task.Run(async () => + { + try + { + await _sensorReader.ResetAsync(); + LoggerService.Info("[장비 리셋] 4251 센서 보드 채널 OFF 및 하드웨어 리셋 완료"); + } + catch (Exception ex) + { + LoggerService.Warn($"[장비 리셋] 4251 센서 복구 예외: {ex.Message}"); + } + }); + } } catch (Exception ex) { @@ -859,6 +876,15 @@ namespace marking_gui { LoggerService.Warn("제품 ID(IC_SN)를 획득하지 못해 검사를 취소했습니다."); + // 센서 하드웨어 상태 리셋 보장 (다음 시료 측정 시 고착 방지) + if (_sensorReader != null) + { + _ = System.Threading.Tasks.Task.Run(async () => + { + try { await _sensorReader.ResetAsync(); } catch { } + }); + } + // IC_SN 읽기 실패 시 버튼 원복 후 종료 SetStartButtonReady(); diff --git a/marking_gui/Services/Board4251Service.cs b/marking_gui/Services/Board4251Service.cs index ea0e97c..e2abab9 100644 --- a/marking_gui/Services/Board4251Service.cs +++ b/marking_gui/Services/Board4251Service.cs @@ -33,6 +33,7 @@ namespace marking_gui.Services bool Connect(); void Disconnect(); Task ReadSensorAsync(); + Task ResetAsync(); } /// @@ -227,6 +228,14 @@ namespace marking_gui.Services if (currentData.Contains("")) { targetTcs.TrySetResult(currentData.Trim()); + return; + } + // 실패 응답(Fail/Error) 감지 시 가 없더라도 타임아웃 5초 대기 없이 즉시 완료 반환 + if (currentData.Contains("Fail", StringComparison.OrdinalIgnoreCase) || + currentData.Contains("Error", StringComparison.OrdinalIgnoreCase)) + { + targetTcs.TrySetResult(currentData.Trim()); + return; } return; } @@ -238,6 +247,33 @@ namespace marking_gui.Services } } + /// + /// 4251 보드의 채널을 안전하게 OFF(x00o)하고 시리얼 포트 버퍼 및 통신 신호를 리셋합니다. + /// + public async Task ResetBoardAsync() + { + try + { + // 보드에 OFF 커맨드 전송 시도 + await SendCommandAsync("x00o\r\n"); + } + catch { } + + try + { + // 시리얼 버퍼 플러시 및 하드웨어 DTR/RTS 리셋 펄스 인가 + _communication.ResetConnection(); + lock (_bufferLock) + { + _receiveBuffer.Clear(); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[Board4251] ResetBoardAsync 예외: {ex.Message}"); + } + } + public void Dispose() { try @@ -278,14 +314,34 @@ namespace marking_gui.Services public bool Connect() => _service.Connect(); public void Disconnect() => _service.Disconnect(); - public async Task ReadSensorAsync() + public async Task ResetAsync() { try { int channel = _sensorIndex + 1; + await _service.SendCommandAsync("x00o\r\n"); + await Task.Delay(100); + string calCmd = $"x00c_00{channel}001:owt28006727ea97c7801\r\n"; + await _service.SendCommandAsync(calCmd); + await Task.Delay(100); + await _service.ResetBoardAsync(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[Board4251SensorService ResetAsync] {ex.Message}"); + } + } + + public async Task ReadSensorAsync() + { + int channel = _sensorIndex + 1; + string rawId = null; + + try + { ProgressMessage?.Invoke(this, "4251 보드 초기화 중..."); - _ = _service.SendCommandAsync("x00o\r\n"); - await Task.Delay(350); + await _service.SendCommandAsync("x00o\r\n"); + await Task.Delay(200); ProgressMessage?.Invoke(this, "4251 보드 데이터 읽는 중..."); int extendedTimeout = 15000; @@ -316,7 +372,6 @@ namespace marking_gui.Services // ID 읽기 (최대 3회 시도) string idCmd = $"x00c_00{channel}101:ow2800326003e"; - string rawId = null; int maxAttempts = 3; for (int attempt = 1; attempt <= maxAttempts; attempt++) @@ -356,39 +411,57 @@ namespace marking_gui.Services if (attempt < maxAttempts) { ProgressMessage?.Invoke(this, $"ID 읽기 재시도 중... ({attempt}/{maxAttempts})"); - await Task.Delay(350); + await Task.Delay(200); continue; } } break; } - // Step 7: 보드 OFF/후처리 (x00o) - ProgressMessage?.Invoke(this, "4251 보드 OFF/후처리 중..."); - await _service.SendCommandAsync("x00o\r\n"); - await Task.Delay(350); - - // Step 8: 보드 CAL 기본값 (x00c_00{channel}001:owt28006727ea97c7801) - ProgressMessage?.Invoke(this, "4251 보드 CAL 기본값 설정 중..."); - string calCmd = $"x00c_00{channel}001:owt28006727ea97c7801\r\n"; - await _service.SendCommandAsync(calCmd); - await Task.Delay(350); - - var data = new SensorIdData + if (!string.IsNullOrEmpty(rawId)) { - ID = rawId, - Serial = "", - Item = "N/A" - }; + var data = new SensorIdData + { + ID = rawId, + Serial = "", + Item = "N/A" + }; - ProgressMessage?.Invoke(this, "ID 읽기 성공: " + rawId); - return data; + ProgressMessage?.Invoke(this, "ID 읽기 성공: " + rawId); + return data; + } + + return null; } catch (Exception ex) { ErrorMessage?.Invoke(this, $"4251 읽기 예외: {ex.Message}"); return null; } + finally + { + // [중요] 성공/실패/타임아웃/예외 불문하고 보드 채널 OFF 및 CAL 기본값 복원을 반드시 실행 + try + { + ProgressMessage?.Invoke(this, "4251 보드 채널 OFF/대기 복귀 중..."); + await _service.SendCommandAsync("x00o\r\n"); + await Task.Delay(150); + + string calCmd = $"x00c_00{channel}001:owt28006727ea97c7801\r\n"; + await _service.SendCommandAsync(calCmd); + await Task.Delay(150); + } + catch (Exception cleanupEx) + { + System.Diagnostics.Debug.WriteLine($"[Board4251] 후처리 리셋 중 예외: {cleanupEx.Message}"); + } + + // ID 읽기 실패 시, 하드웨어 버퍼 플러시 및 포트 리셋 수행하여 다음 시료 측정 시 고착 방지 + if (string.IsNullOrEmpty(rawId)) + { + await _service.ResetBoardAsync(); + } + } } public void Dispose() diff --git a/marking_gui/Services/CommunicationBase.cs b/marking_gui/Services/CommunicationBase.cs index d8081e2..9c2d484 100644 --- a/marking_gui/Services/CommunicationBase.cs +++ b/marking_gui/Services/CommunicationBase.cs @@ -28,6 +28,9 @@ namespace marking_gui.Services /// 수신 버퍼를 비워 잔류 데이터를 제거함 void ClearBuffer(); + /// 통신 하드웨어 및 버퍼를 강제 리셋함 (DTR/RTS 토글 및 플러시) + void ResetConnection(); + /// 하드웨어로부터 데이터를 수신했을 때 발생하는 이벤트 event System.EventHandler DataReceived; @@ -128,6 +131,30 @@ namespace marking_gui.Services } } + public void ResetConnection() + { + if (_disposed) return; + try + { + ClearBuffer(); + if (_serialPort != null && _serialPort.IsOpen) + { + // DTR/RTS 신호 토글을 통한 4251 센서 보드 하드웨어 리셋 펄스 인가 + _serialPort.DtrEnable = false; + _serialPort.RtsEnable = false; + System.Threading.Thread.Sleep(50); + _serialPort.DtrEnable = true; + _serialPort.RtsEnable = true; + ClearBuffer(); + System.Diagnostics.Debug.WriteLine($"[SerialProvider] {Name} 포트 하드웨어 리셋(DTR/RTS 토글) 완료"); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[SerialProvider ResetConnection Error] {ex.Message}"); + } + } + public void Dispose() { if (!_disposed)