Browse Source

4251 센서 보드 복구 처리 및 오류 분석 보고서 추가

master
seuuung 1 week ago
parent
commit
a0f3bdf571
  1. 158
      docs/reports/계측기_저항_오버플로우_오류_분석_보고서.md
  2. 26
      marking_gui/MainWindow.xaml.cs
  3. 105
      marking_gui/Services/Board4251Service.cs
  4. 27
      marking_gui/Services/CommunicationBase.cs

158
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. 결론 및 건의사항
본 오류는 계측기의 하드웨어 고장이 아니라 **단선/오버로드 규격치 수신 시의 예외 처리 누락**에서 기인한 것입니다.\
제안된 파싱 타입 변경 및 오버로드 핸들링 로직을 적용하면 단선 불량 발생 시에도 시스템 멈춤 없이 안정적으로 불합격 판정 및 공정을 지속할 수 있습니다.

26
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();

105
marking_gui/Services/Board4251Service.cs

@ -33,6 +33,7 @@ namespace marking_gui.Services
bool Connect();
void Disconnect();
Task<SensorIdData> ReadSensorAsync();
Task ResetAsync();
}
/// <summary>
@ -227,6 +228,14 @@ namespace marking_gui.Services
if (currentData.Contains("<end>"))
{
targetTcs.TrySetResult(currentData.Trim());
return;
}
// 실패 응답(Fail/Error) 감지 시 <end>가 없더라도 타임아웃 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
}
}
/// <summary>
/// 4251 보드의 채널을 안전하게 OFF(x00o)하고 시리얼 포트 버퍼 및 통신 신호를 리셋합니다.
/// </summary>
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<SensorIdData> 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<SensorIdData> 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,24 +411,15 @@ 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);
if (!string.IsNullOrEmpty(rawId))
{
var data = new SensorIdData
{
ID = rawId,
@ -384,11 +430,38 @@ namespace marking_gui.Services
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()

27
marking_gui/Services/CommunicationBase.cs

@ -28,6 +28,9 @@ namespace marking_gui.Services
/// <summary> 수신 버퍼를 비워 잔류 데이터를 제거함 </summary>
void ClearBuffer();
/// <summary> 통신 하드웨어 및 버퍼를 강제 리셋함 (DTR/RTS 토글 및 플러시) </summary>
void ResetConnection();
/// <summary> 하드웨어로부터 데이터를 수신했을 때 발생하는 이벤트 </summary>
event System.EventHandler<string> 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)

Loading…
Cancel
Save