Browse Source

NG 결과 Db 저장 및 카운트

master
seuuung 3 months ago
parent
commit
233c28ff25
  1. BIN
      .vs/marking_gui.slnx/v18/.suo
  2. 12
      .vs/marking_gui.slnx/v18/DocumentLayout.backup.json
  3. 12
      .vs/marking_gui.slnx/v18/DocumentLayout.json
  4. 2
      marking_gui/MainWindow.xaml
  5. 128
      marking_gui/MainWindow.xaml.cs
  6. 4
      marking_gui/Manual/.obsidian/workspace.json
  7. 14
      marking_gui/Manual/07_시험_프로세스.md
  8. 30
      marking_gui/Manual/Manual_Merged.md
  9. 52
      marking_gui/Manual/Manual_for_pdf.html
  10. 18
      marking_gui/Models/MarkingResult.cs
  11. 1
      marking_gui/PortSettingsWindow.xaml
  12. 50
      marking_gui/PortSettingsWindow.xaml.cs
  13. 16
      marking_gui/QrVerificationWindow.xaml.cs
  14. 13
      marking_gui/Services/Board4251Service.cs
  15. 390
      marking_gui/Services/EzdMarkingService.cs
  16. 24
      marking_gui/Services/MarkingConfigService.cs
  17. 122
      marking_gui/Services/SerialBarcodeScanner.cs
  18. 5
      marking_gui/Setting.ini
  19. 4
      marking_gui/SettingsWindow.xaml.cs
  20. BIN
      test_bukuk.ezd

BIN
.vs/marking_gui.slnx/v18/.suo

Binary file not shown.

12
.vs/marking_gui.slnx/v18/DocumentLayout.backup.json

@ -22,12 +22,16 @@
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 3,
"SelectedChildIndex": 4,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
},
{
"$type": "Document",
"DocumentIndex": 1,
@ -37,8 +41,7 @@
"ToolTip": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\QrVerificationWindow.xaml",
"RelativeToolTip": "marking_gui\\QrVerificationWindow.xaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
"WhenOpened": "2026-07-29T04:12:09.69Z",
"EditorCaption": ""
"WhenOpened": "2026-07-29T04:12:09.69Z"
},
{
"$type": "Document",
@ -49,8 +52,7 @@
"ToolTip": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\SettingsWindow.xaml",
"RelativeToolTip": "marking_gui\\SettingsWindow.xaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
"WhenOpened": "2026-07-29T04:12:06.926Z",
"EditorCaption": ""
"WhenOpened": "2026-07-29T04:12:06.926Z"
},
{
"$type": "Document",

12
.vs/marking_gui.slnx/v18/DocumentLayout.json

@ -22,12 +22,16 @@
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 3,
"SelectedChildIndex": 4,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
},
{
"$type": "Document",
"DocumentIndex": 1,
@ -37,8 +41,7 @@
"ToolTip": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\QrVerificationWindow.xaml",
"RelativeToolTip": "marking_gui\\QrVerificationWindow.xaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
"WhenOpened": "2026-07-29T04:12:09.69Z",
"EditorCaption": ""
"WhenOpened": "2026-07-29T04:12:09.69Z"
},
{
"$type": "Document",
@ -49,8 +52,7 @@
"ToolTip": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\SettingsWindow.xaml",
"RelativeToolTip": "marking_gui\\SettingsWindow.xaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
"WhenOpened": "2026-07-29T04:12:06.926Z",
"EditorCaption": ""
"WhenOpened": "2026-07-29T04:12:06.926Z"
},
{
"$type": "Document",

2
marking_gui/MainWindow.xaml

@ -449,7 +449,7 @@
</StackPanel>
</Viewbox>
<Viewbox Grid.Row="2" Stretch="Uniform" MaxHeight="18" HorizontalAlignment="Center">
<TextBlock x:Name="txtVoltageRangeGuide" Text="기준범위: 0.0V ~ 0.5V" FontSize="14" Foreground="#9CA3AF" HorizontalAlignment="Center"/>
<TextBlock x:Name="txtVoltageRangeGuide" Text="기준범위: 0.00V ~ 0.50V" FontSize="14" Foreground="#9CA3AF" HorizontalAlignment="Center"/>
</Viewbox>
</Grid>
</Border>

128
marking_gui/MainWindow.xaml.cs

@ -355,7 +355,7 @@ namespace marking_gui
private void UpdateRangeGuides()
{
txtVoltageRangeGuide.Text = string.Format("기준범위: {0:F1}V ~ {1:F1}V", _limits.MinVoltage, _limits.MaxVoltage);
txtVoltageRangeGuide.Text = string.Format("기준범위: {0:F2}V ~ {1:F2}V", _limits.MinVoltage, _limits.MaxVoltage);
txtCurrentRangeGuide.Text = string.Format("기준범위: {0:F2}mA ~ {1:F2}mA", _limits.MinCurrent, _limits.MaxCurrent);
txtResistanceRangeGuide.Text = string.Format("기준범위: {0:F3}kΩ ~ {1:F3}kΩ", _limits.MinResistance, _limits.MaxResistance);
}
@ -1363,9 +1363,6 @@ namespace marking_gui
string lotNo = string.IsNullOrWhiteSpace(_loginInfo.LotNo) ? "00" : _loginInfo.LotNo.Trim();
string markedSerial = string.Format("{0}{1:D4}", lotNo, seq);
string drawingNo = _markingConfig.DrawingNo;
string hkmcNo = _markingConfig.HkmcNo;
string qrString = string.Format("{0};{1};{2};{3};", serial, drawingNo, hkmcNo, markedSerial);
// 마킹 스캔 요청 데이터 빌드
var req = new MarkingRequest
@ -1374,12 +1371,12 @@ namespace marking_gui
IcSerial = serial,
LotNo = lotNo,
ProductNo = _loginInfo.Model,
DrawingNo = drawingNo,
HkmcNo = hkmcNo,
DrawingNo = string.Empty,
HkmcNo = string.Empty,
EzdFileName = _currentEzdFile,
LineCode = _loginInfo.LineNo,
ShiftCode = "A", // 기본 1조
QrString = qrString
QrString = string.Empty
};
MarkingResult markingRes;
@ -1387,12 +1384,19 @@ namespace marking_gui
{
// 오프라인 모드 시뮬레이션
await System.Threading.Tasks.Task.Delay(500);
string offDrawingNo = _markingConfig.GetDrawingNoForEzd(_currentEzdFile);
string offHkmcNo = _markingConfig.GetHkmcNoForEzd(_currentEzdFile);
string offQr = string.Format("{0};{1};{2};{3};", serial, offDrawingNo, offHkmcNo, markedSerial);
markingRes = new MarkingResult
{
Success = true,
ErrorCode = 0,
ErrorMessage = "오프라인 모드 각인 완료 모사",
MarkedAt = DateTime.Now
MarkedAt = DateTime.Now,
DrawingNo = offDrawingNo,
HkmcNo = offHkmcNo,
QrString = offQr
};
}
else
@ -1403,13 +1407,17 @@ namespace marking_gui
// UI 스레드로 안전하게 컴백
await Dispatcher.InvokeAsync(async () =>
{
string ezdDrawingNo = markingRes.DrawingNo;
string ezdHkmcNo = markingRes.HkmcNo;
string ezdQrString = markingRes.QrString;
if (txtMarkedSerial != null)
{
txtMarkedSerial.Text = markedSerial;
}
if (txtMarkingQr != null)
{
txtMarkingQr.Text = qrString;
txtMarkingQr.Text = string.IsNullOrWhiteSpace(ezdQrString) ? "---" : ezdQrString;
}
bool finalMarkingPass = markingRes.Success;
@ -1418,10 +1426,10 @@ namespace marking_gui
{
if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 완료 (QR 검증 대기)";
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber
LoggerService.Info($"[마킹 성공] S/N: {serial}, 각인번호: {markedSerial} -> QR 검증 팝업 오픈");
LoggerService.Info($"[마킹 성공] S/N: {serial}, 각인번호: {markedSerial}, 도면품번: {ezdDrawingNo}, HKMC품번: {ezdHkmcNo} -> QR 검증 팝업 오픈");
// DB 저장 전 레이저 각인 QR 코드 스캔 검증 팝업 오픈 (시리얼 스캐너 서비스 인계)
var qrVerifyWin = new QrVerificationWindow(qrString, _barcodeScanner)
var qrVerifyWin = new QrVerificationWindow(ezdQrString, _barcodeScanner)
{
Owner = this
};
@ -1439,8 +1447,8 @@ namespace marking_gui
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green
LoggerService.Info($"[QR 검증 성공] S/N: {serial}, 각인번호: {markedSerial} -> DB 저장 진행");
// QR 검증 통과 시 최종 DB 저장 및 완료 처리
await SaveAndFinalizeAsync(serial, finalVoltage, finalCurrent, finalResistance, true, "OK", markedSerial, bypassPrevSteps);
// QR 검증 통과 시 최종 DB 저장 및 완료 처리 (EZD 추출 실시간 품번 포함)
await SaveAndFinalizeAsync(serial, finalVoltage, finalCurrent, finalResistance, true, "OK", markedSerial, ezdDrawingNo, ezdHkmcNo, bypassPrevSteps);
}
else
{
@ -1448,8 +1456,8 @@ namespace marking_gui
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
LoggerService.Warn($"[QR 검증 실패] S/N: {serial}, 각인번호: {markedSerial} -> DB NG 저장 진행");
// 마킹 성공 후 QR 검증 불일치/취소 시 NG로 DB 저장
await SaveAndFinalizeAsync(serial, finalVoltage, finalCurrent, finalResistance, false, "NG", markedSerial, bypassPrevSteps);
// 마킹 성공 후 QR 검증 불일치/취소 시 NG로 DB 저장 (EZD 추출 실시간 품번 포함)
await SaveAndFinalizeAsync(serial, finalVoltage, finalCurrent, finalResistance, false, "NG", markedSerial, ezdDrawingNo, ezdHkmcNo, bypassPrevSteps);
}
}
else
@ -1526,8 +1534,12 @@ namespace marking_gui
}
else
{
// 불합격인 경우 즉시 DB 저장 및 복구
await SaveAndFinalizeAsync(serial, finalVoltage, finalCurrent, finalResistance, isFinalPass, "NG", "", bypassPrevSteps);
// 불합격인 경우에도 Lot No 및 예정 각인번호/품번 정보를 포함하여 DB 저장 및 복구
string markedSerial = GetCurrentMarkedSerial();
string drawingNo = _markingConfig.GetDrawingNoForEzd(_currentEzdFile);
string hkmcNo = _markingConfig.GetHkmcNoForEzd(_currentEzdFile);
await SaveAndFinalizeAsync(serial, finalVoltage, finalCurrent, finalResistance, isFinalPass, "NG", markedSerial, drawingNo, hkmcNo, bypassPrevSteps: bypassPrevSteps);
}
}
}
@ -1586,7 +1598,38 @@ namespace marking_gui
}
}
private async Task SaveAndFinalizeAsync(string serial, double voltage, double current, double resistance, bool isFinalPass, string result, string markedSerial, bool bypassPrevSteps = false)
private string GetCurrentMarkedSerial()
{
int seq = 1;
if (txtNextSequence != null)
{
if (Dispatcher.CheckAccess())
{
string rawText = txtNextSequence.Text;
string digitsOnly = System.Text.RegularExpressions.Regex.Replace(rawText ?? "", @"[^\d]", "");
if (int.TryParse(digitsOnly, out int parsedSeq) && parsedSeq > 0)
{
seq = parsedSeq;
}
}
else
{
Dispatcher.Invoke(() =>
{
string rawText = txtNextSequence.Text;
string digitsOnly = System.Text.RegularExpressions.Regex.Replace(rawText ?? "", @"[^\d]", "");
if (int.TryParse(digitsOnly, out int parsedSeq) && parsedSeq > 0)
{
seq = parsedSeq;
}
});
}
}
string lotNo = string.IsNullOrWhiteSpace(_loginInfo.LotNo) ? "00" : _loginInfo.LotNo.Trim();
return string.Format("{0}{1:D4}", lotNo, seq);
}
private async Task SaveAndFinalizeAsync(string serial, double voltage, double current, double resistance, bool isFinalPass, string result, string markedSerial, string drawingNo = "", string hkmcNo = "", bool bypassPrevSteps = false)
{
// 종료 시간 기록 및 UI 갱신
_testEndTime = DateTime.Now;
@ -1608,6 +1651,9 @@ namespace marking_gui
_currentProduct.PcbBarcode = serial;
}
string finalDrawingNo = string.IsNullOrWhiteSpace(drawingNo) ? _markingConfig.DrawingNo : drawingNo;
string finalHkmcNo = string.IsNullOrWhiteSpace(hkmcNo) ? _markingConfig.HkmcNo : hkmcNo;
try
{
// 데이터베이스 저장 (비동기 스레드 격리로 UI 프리징 방지)
@ -1631,8 +1677,8 @@ namespace marking_gui
_testEndTime,
markedSerial,
pcbBarcodeToSave, // 강제검사 시 시리얼 번호 사용
_markingConfig.DrawingNo,
_markingConfig.HkmcNo);
finalDrawingNo,
finalHkmcNo);
});
if (dbResult)
@ -1745,11 +1791,18 @@ namespace marking_gui
{
borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed;
}
}
// 컨트롤 원상 복구 (재검사를 위해)
txtSerial.IsEnabled = true;
SetStartButtonReady();
_dbWasOffline = true;
SetStartButtonConnecting("⚠️ DB 연결 필요");
}
finally
{
txtSerial.IsEnabled = true;
if (!_dbWasOffline)
{
SetStartButtonReady();
}
}
}
// 화면 렌더링 완료 시 최초 로그인 팝업 구동
@ -1835,6 +1888,11 @@ namespace marking_gui
{
// DB 재연결 감시 루프 중단
_dbWatcherCts?.Cancel();
_dbWatcherCts?.Dispose();
// 윈도우 타이머 정지 (WPF DispatcherTimer GC 메모리 누수 방지)
_clockTimer?.Stop();
_clockTimer = null;
// LMC 마킹 기계 무선/시리얼 연결 핸들 안전 해제 (재시작 시 LMC 장치 락 방지)
_markingService?.Shutdown();
@ -1882,7 +1940,7 @@ namespace marking_gui
if (!_isInspectionRunning)
{
Dispatcher.Invoke(() =>
await Dispatcher.InvokeAsync(() =>
{
SetStartButtonConnecting("⚠️ DB 연결 필요");
@ -1906,7 +1964,7 @@ namespace marking_gui
if (!_isInspectionRunning)
{
Dispatcher.Invoke(() =>
await Dispatcher.InvokeAsync(() =>
{
if (txtSystemStatus != null)
{
@ -1921,11 +1979,6 @@ namespace marking_gui
await Dispatcher.InvokeAsync(() => RefreshNextSequence());
}
}
else if (isNowConnected && !_dbWasOffline && !_isInspectionRunning)
{
// DB 연결 유지 상태에서도 DB 레코드 직접 변경/삭제 시 각인 순번 실시간 반영 (조용히 UI 갱신)
await Dispatcher.InvokeAsync(() => RefreshNextSequenceSilently());
}
}
catch (Exception ex)
{
@ -2101,7 +2154,18 @@ namespace marking_gui
{
_markingConfig.SaveLastSelectedEzdFile(fullPath);
}
LoggerService.Info($"EZD 도안 파일 선택 완료: {fileName} (절대경로: {fullPath})");
string drawingNo = string.Empty;
string hkmcNo = string.Empty;
if (_markingService is EzdMarkingService ezdService)
{
drawingNo = ezdService.GetTextFromEzd(_markingConfig.DrawingNoTag, fullPath);
hkmcNo = ezdService.GetTextFromEzd(_markingConfig.HkmcNoTag, fullPath);
}
if (string.IsNullOrWhiteSpace(drawingNo)) drawingNo = _markingConfig.GetDrawingNoForEzd(fullPath);
if (string.IsNullOrWhiteSpace(hkmcNo)) hkmcNo = _markingConfig.GetHkmcNoForEzd(fullPath);
LoggerService.Info($"EZD 도안 파일 선택 완료: {fileName} (절대경로: {fullPath}, 도면품번: {drawingNo}, HKMC품번: {hkmcNo})");
}
private void InitializeBarcodeScanner()

4
marking_gui/Manual/.obsidian/workspace.json

@ -41,7 +41,9 @@
"type": "file-explorer",
"state": {
"sortOrder": "alphabetical",
"autoReveal": false
"autoReveal": false,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-folder-closed",
"title": "파일 탐색기"

14
marking_gui/Manual/07_시험_프로세스.md

@ -67,8 +67,8 @@ graph TD
* 이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, `NG` 내역으로 DB에 기록됩니다.
2. **모든 항목이 합격인 경우 (OK)**:
* 모든 수치가 기준치 내에 들어오면 최종 합격(**`OK`**) 판정이 내려집니다.
* 합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.
* 마킹 완료 후 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증합니다.
* 합격 판정과 동시에 선택된 EZD 도안 파일(.ezd)에서 도면품번 및 HKMC품번을 실시간으로 추출하여 각인 및 QR 코드가 자동으로 조합되어 레이저 마킹이 실행됩니다.
* 마킹 완료 후 EZD 파싱 품번 기반의 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증하고, 검증 완료 시 해당 품번 데이터로 DB 저장을 완료합니다.
---
@ -76,12 +76,14 @@ graph TD
레이저 마킹이 완료되면 검증을 위해 **QR 검증 팝업 창**이 화면에 활성화되며 바코드 스캐너 또는 가상 키보드로 각인된 QR 코드를 읽어 들입니다.
1. **QR 스캔 검증 성공 (즉시 자동 닫힘)**
1. **QR 스캔 검증 성공 (즉시 자동 닫힘 및 DB OK 적재)**
- 스캔된 QR 코드가 예상 QR 코드와 정확히 일치하면 팝업 창이 지연 없이 **즉시 자동으로 닫히며** DB에 `OK` 상태로 최종 저장을 완료하고 대기 상태로 복구됩니다.
2. **QR 스캔 검증 실패 (NG 판정 및 설명 표출)**
- 스캔된 QR 코드가 불일치하거나 검증을 취소하는 경우, 작업자가 [확인] 버튼을 누르면 팝업 창이 닫힙니다.
- 팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 **`OK`**에서 **`NG`** (빨간색 테두리 및 배경)로 즉시 전환됩니다.
2. **QR 스캔 검증 실패 (팝업 유지 및 연속 재스캔 지원)**
- 스캔된 QR 코드가 불일치(대조 실패)할 경우, 팝업 창이 **자동으로 닫히지 않고 열린 상태를 유지**하며 "QR 바코드 불일치" 경고 상태를 표출합니다.
- 팝업이 열려 있는 동안 바코드/QR 스캐너 또는 키보드 입력을 통해 올바른 QR 코드로 **연속 재스캔**할 수 있으며, 재스캔에서 대조 성공 시 자동으로 통과(닫힘) 및 DB `OK` 저장 처리됩니다.
- 대조 불일치 상태에서 작업자가 **[확인]** 또는 **[취소]** (ESC 키) 버튼을 누르면 팝업 창이 닫히며, 판정 결과 **`NG`**로 데이터베이스 저장을 완료합니다.
- 팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 **`OK`**에서 **`NG`** (빨간색 테두리 및 배경)로 전환됩니다.
- 공정 진행 상태 및 시스템 상태 바에 `"마킹 완료 / 각인 QR 검증 NG"`, `"상태: 검사 불합격 (각인 QR 검증 실패)"`, `"각인 QR 검증 실패 (DB NG 저장 완료)"` 등 구체적인 원인 설명이 표시되며, DB에 최종 `NG` 결과로 기록됩니다.

30
marking_gui/Manual/Manual_Merged.md

@ -658,7 +658,7 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
* **세부 정보 그리드**:
- **검사 시간 정보**: 전체 시퀀스 기동 시작 시간 및 종료 시간 표출
- **일련번호 정보**: `IC S/N`, **`Marking QR :`** (마킹 생성 QR 값), **`Scan QR :`** (스캐너 실측 QR 값), `각인 번호` 4개 항목을 수직 배열로 표출
- **최종 판정결과**: 최종 **PASS/OK** (초록색) / **FAIL/NG** (빨간색) 대형 배지 표출 (성능 합격 후 마킹 QR 검증 실패 시에도 **NG**로 자동 반영됨)
- **최종 판정결과**: 최종 **`OK`** (초록색) / **`NG`** (빨간색) 대형 배지 표출 (성능 합격 후 마킹 QR 검증 실패 시에도 **`NG`**로 자동 반영됨)
- **시스템 상태**: 통신 및 디바이스 가동 상태 또는 에러/검증 실패 사유 메시지 배너 표출
<img src="./Pasted image 20260728100413.png" alt="Pasted image 20260728100413.png" class="img-screenshot" />
---
@ -840,8 +840,8 @@ graph TD
H --> I[12~14단계: 전압 / 저항 / 전류 실측]
I --> J[15단계: 테스트 주전원 안전 차단]
J --> K{최종 규격 비교 판정}
K -- 1개 이상 이탈 FAIL --> L[대형 FAIL 표시 & 마킹 제외 & DB 불합격 적재]
K -- 모두 통과 PASS --> M[대형 PASS 표시 & DB 합격 적재 & LMC1 레이저 QR 각인]
K -- 1개 이상 이탈 NG --> L[대형 NG 표시 & 마킹 제외 & DB NG 적재]
K -- 모두 통과 OK --> M[대형 OK 표시 & DB OK 적재 & LMC1 레이저 QR 각인]
L --> Z
M --> Z
```
@ -880,13 +880,13 @@ graph TD
계측이 완료되면 측정된 전압(V), 저항(kΩ), 전류(mA) 값을 설정한 기준 규격값(SPEC)과 비교하여 최종 판정을 내립니다.
1. **기준 규격을 벗어난 경우 (FAIL)**:
* 전압, 전류, 저항 중 하나라도 기준을 벗어나면 최종 불합격(FAIL) 판정이 내려집니다.
* 이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, 불합격 내역으로 DB에 기록됩니다.
2. **모든 항목이 합격인 경우 (PASS)**:
* 모든 수치가 기준치 내에 들어오면 최종 합격(PASS) 판정이 내려집니다.
* 합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.
* 마킹 완료 후 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증합니다.
1. **기준 규격을 벗어난 경우 (NG)**:
* 전압, 전류, 저항 중 하나라도 기준을 벗어나면 최종 불합격(**`NG`**) 판정이 내려집니다.
* 이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, `NG` 내역으로 DB에 기록됩니다.
2. **모든 항목이 합격인 경우 (OK)**:
* 모든 수치가 기준치 내에 들어오면 최종 합격(**`OK`**) 판정이 내려집니다.
* 합격 판정과 동시에 선택된 EZD 도안 파일(.ezd)에서 도면품번 및 HKMC품번을 실시간으로 추출하여 각인 및 QR 코드가 자동으로 조합되어 레이저 마킹이 실행됩니다.
* 마킹 완료 후 EZD 파싱 품번 기반의 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증하고, 검증 완료 시 해당 품번 데이터로 DB 저장을 완료합니다.
---
@ -894,12 +894,14 @@ graph TD
레이저 마킹이 완료되면 검증을 위해 **QR 검증 팝업 창**이 화면에 활성화되며 바코드 스캐너 또는 가상 키보드로 각인된 QR 코드를 읽어 들입니다.
1. **QR 스캔 검증 성공 (즉시 자동 닫힘)**
1. **QR 스캔 검증 성공 (즉시 자동 닫힘 및 DB OK 적재)**
- 스캔된 QR 코드가 예상 QR 코드와 정확히 일치하면 팝업 창이 지연 없이 **즉시 자동으로 닫히며** DB에 `OK` 상태로 최종 저장을 완료하고 대기 상태로 복구됩니다.
2. **QR 스캔 검증 실패 (NG 판정 및 설명 표출)**
- 스캔된 QR 코드가 불일치하거나 검증을 취소하는 경우, 작업자가 [확인] 버튼을 누르면 팝업 창이 닫힙니다.
- 팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 **`OK`**에서 **`NG`** (빨간색 테두리 및 배경)로 즉시 전환됩니다.
2. **QR 스캔 검증 실패 (팝업 유지 및 연속 재스캔 지원)**
- 스캔된 QR 코드가 불일치(대조 실패)할 경우, 팝업 창이 **자동으로 닫히지 않고 열린 상태를 유지**하며 "QR 바코드 불일치" 경고 상태를 표출합니다.
- 팝업이 열려 있는 동안 바코드/QR 스캐너 또는 키보드 입력을 통해 올바른 QR 코드로 **연속 재스캔**할 수 있으며, 재스캔에서 대조 성공 시 자동으로 통과(닫힘) 및 DB `OK` 저장 처리됩니다.
- 대조 불일치 상태에서 작업자가 **[확인]** 또는 **[취소]** (ESC 키) 버튼을 누르면 팝업 창이 닫히며, 판정 결과 **`NG`**로 데이터베이스 저장을 완료합니다.
- 팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 **`OK`**에서 **`NG`** (빨간색 테두리 및 배경)로 전환됩니다.
- 공정 진행 상태 및 시스템 상태 바에 `"마킹 완료 / 각인 QR 검증 NG"`, `"상태: 검사 불합격 (각인 QR 검증 실패)"`, `"각인 QR 검증 실패 (DB NG 저장 완료)"` 등 구체적인 원인 설명이 표시되며, DB에 최종 `NG` 결과로 기록됩니다.

52
marking_gui/Manual/Manual_for_pdf.html

@ -405,9 +405,9 @@ a:hover {
<li><strong>프로그램 실행 및 로그인</strong></li>
<li>바탕화면의 <code>Marking System</code> (<code>marking_gui</code>) 프로그램을 실행한 뒤, 작업자 계정 정보를 입력하여 로그인합니다. (데이터베이스 점검 중인 경우 설정 파일을 통해 오프라인 모드로의 진입이 가능합니다.)</li>
<li><strong>통신 상태 확인 및 채널 설정</strong></li>
<li>우측 상단의 <code>통신 설정</code> 메뉴에서 센서가 올바른 포트와 채널에 연결되었는지 확인합니다.</li>
<li>화면 <strong>하단 바</strong>의 <code>포트 설정</code> 버튼을 클릭하여 센서가 올바른 포트와 채널에 연결되었는지 확인합니다.</li>
<li><strong>검사 규격값 검증</strong></li>
<li>우측 상단의 <code>시험 설정</code> 메뉴를 열고, 전압/전류/저항 검사 기준치와 각인 템플릿 도면 파일의 경로가 올바른지 점검합니다.</li>
<li>화면 <strong>하단 바</strong>의 <code>파라미터 설정</code> 버튼을 클릭하여 전압/전류/저항 검사 기준치와 각인 템플릿 도면 파일의 경로가 올바른지 점검합니다.</li>
<li><strong>성능 검사 및 마킹 시작</strong></li>
<li>안착 지그에 제품을 바르게 장착한 뒤 메인 화면 하단의 <code>검사 시작 (START)</code> 버튼을 눌러 계측 및 마킹을 수행합니다.</li>
</ol>
@ -640,7 +640,7 @@ a:hover {
<h4>1) Setting.ini (주요 변경 가능 설정)</h4>
<p>현장 네트워크 IP, 도안 파일 경로 및 검사 규격(임계치) 변경 시 수정하는 주요 설정 항목입니다.</p>
<pre><code class="language-ini">[Settings]
EzdFolderPath=c:\EzCad\EasyCfg ; EZD 도안 파일 기본 탐색/저장 폴더 경로
EzdFolderPath=c:\EzCad ; EZD 도안 파일 기본 탐색/저장 폴더 경로
test=false ; 테스트 마킹 버튼 및 개발 모드 노출 여부 (true/false)
[Equipment]
@ -738,9 +738,9 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
<img src="./Pasted image 20260728100354.png" alt="Pasted image 20260728100354.png" class="img-screenshot" /></p>
<h3>3.2.3 [카드 3] 측정 데이터 수치 카드 (우측 상단)</h3>
<p>실제 계측기를 통해 수집된 3대 전기 파라미터 수치와 설정된 규격 범위를 표시하는 3열 구조의 카드입니다.
* <strong>측정 전압 (Voltage)</strong>: DMM 계측 장비가 측정한 실측 전압 수치 (<strong>V</strong> 단위, 기본 기준범위: <code>0.0V ~ 0.5V</code>)
* <strong>측정 전류 (Current)</strong>: Power Supply 전원공급 장치가 측정한 소모 전류 수치 (<strong>mA</strong> 단위, 기본 기준범위: <code>8.00mA ~ 10.00mA</code>)
* <strong>측정 저항 (Resistance)</strong>: DMM 측정 장비가 측정한 전기적 저항 수치 (<strong>kΩ</strong> 단위, 기본 기준범위: <code>0.400kΩ ~ 0.800kΩ</code>)
* <strong>측정 전압 (Voltage)</strong>: DMM 계측 장비가 측정한 실측 전압 수치 (<strong>V</strong> 단위, 기본 기준범위: <code>1.00V ~ 6.00V</code>)
* <strong>측정 전류 (Current)</strong>: Power Supply 전원공급 장치가 측정한 소모 전류 수치 (<strong>mA</strong> 단위, 기본 기준범위: <code>5.00mA ~ 10.00mA</code>)
* <strong>측정 저항 (Resistance)</strong>: DMM 측정 장비가 측정한 전기적 저항 수치 (<strong>kΩ</strong> 단위, 기본 기준범위: <code>8.000kΩ ~ 13.000kΩ</code>)
* <em>알림: 각 수치 카드 하단에는 설정 파일(<code>Setting.ini</code>)에서 지정한 임계 규격 범위가 표시되며, 실측값이 범위 내에 들어오면 카드 테두리가 </em><em>초록색(PASS)</em><em>으로, 범위를 벗어나면 </em><em>빨간색(FAIL)</em><em>으로 자동 전환됩니다.</em>
<img src="./Pasted image 20260728100404.png" alt="Pasted image 20260728100404.png" class="img-screenshot" /></p>
<h3>3.2.4 [카드 4] 최종 판정 및 종합 상태 영역 (우측 하단)</h3>
@ -749,7 +749,7 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
* <strong>세부 정보 그리드</strong>:
- <strong>검사 시간 정보</strong>: 전체 시퀀스 기동 시작 시간 및 종료 시간 표출
- <strong>일련번호 정보</strong>: <code>IC S/N</code>, <strong><code>Marking QR :</code></strong> (마킹 생성 QR 값), <strong><code>Scan QR :</code></strong> (스캐너 실측 QR 값), <code>각인 번호</code> 4개 항목을 수직 배열로 표출
- <strong>최종 판정결과</strong>: 최종 <strong>PASS/OK</strong> (초록색) / <strong>FAIL/NG</strong> (빨간색) 대형 배지 표출 (성능 합격 후 마킹 QR 검증 실패 시에도 <strong>NG</strong>로 자동 반영됨)
- <strong>최종 판정결과</strong>: 최종 <strong><code>OK</code></strong> (초록색) / <strong><code>NG</code></strong> (빨간색) 대형 배지 표출 (성능 합격 후 마킹 QR 검증 실패 시에도 <strong><code>NG</code></strong>로 자동 반영됨)
- <strong>시스템 상태</strong>: 통신 및 디바이스 가동 상태 또는 에러/검증 실패 사유 메시지 배너 표출
<img src="./Pasted image 20260728100413.png" alt="Pasted image 20260728100413.png" class="img-screenshot" /></p>
<hr />
@ -761,7 +761,7 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
<li><strong>포트 설정</strong>: 센서 통신 보드의 시리얼 COM 포트 및 채널 설정 창을 호출합니다.</li>
<li><strong>강제 검사 (스킵)</strong>: 이전 공정 이력이 없어도 검증을 건너뛰고 계측 및 마킹을 수행합니다.</li>
<li><strong>테스트 마킹</strong>: 계측 과정 없이 레이저 각인 장비 동작을 테스트합니다.</li>
<li><em>참고: <code>강제 검사 (스킵)</code> 및 <code>테스트 마킹</code> 버튼은 설정 파일(<code>Setting.ini</code>)에서 <code>TestMode=true</code>로 활성화된 경우에만 화면에 노출됩니다.</em>
<li><em>참고: <code>강제 검사 (스킵)</code> 버튼은 설정 파일(<code>Setting.ini</code>)에서 <code>test=true</code>로 활성화된 경우에만 화면에 노출됩니다.</em>
<img src="./Pasted image 20260728100445.png" alt="Pasted image 20260728100445.png" class="img-screenshot" /></li>
</ul>
</li>
@ -924,8 +924,8 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
H --&gt; I[12~14단계: 전압 / 저항 / 전류 실측]
I --&gt; J[15단계: 테스트 주전원 안전 차단]
J --&gt; K{최종 규격 비교 판정}
K -- 1개 이상 이탈 FAIL --&gt; L[대형 FAIL 표시 &amp; 마킹 제외 &amp; DB 불합격 적재]
K -- 모두 통과 PASS --&gt; M[대형 PASS 표시 &amp; DB 합격 적재 &amp; LMC1 레이저 QR 각인]
K -- 1개 이상 이탈 NG --&gt; L[대형 NG 표시 &amp; 마킹 제외 &amp; DB NG 적재]
K -- 모두 통과 OK --&gt; M[대형 OK 표시 &amp; DB OK 적재 &amp; LMC1 레이저 QR 각인]
L --&gt; Z
M --&gt; Z
</code></pre>
@ -961,15 +961,15 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
<h2>7.3 최종 판정 및 마킹 실행 조건</h2>
<p>계측이 완료되면 측정된 전압(V), 저항(kΩ), 전류(mA) 값을 설정한 기준 규격값(SPEC)과 비교하여 최종 판정을 내립니다.</p>
<ol>
<li><strong>기준 규격을 벗어난 경우 (FAIL)</strong>:<ul>
<li>전압, 전류, 저항 중 하나라도 기준을 벗어나면 최종 불합격(FAIL) 판정이 내려집니다.</li>
<li>이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, 불합격 내역으로 DB에 기록됩니다.</li>
<li><strong>기준 규격을 벗어난 경우 (NG)</strong>:<ul>
<li>전압, 전류, 저항 중 하나라도 기준을 벗어나면 최종 불합격(<strong><code>NG</code></strong>) 판정이 내려집니다.</li>
<li>이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, <code>NG</code> 내역으로 DB에 기록됩니다.</li>
</ul>
</li>
<li><strong>모든 항목이 합격인 경우 (PASS)</strong>:<ul>
<li>모든 수치가 기준치 내에 들어오면 최종 합격(PASS) 판정이 내려집니다.</li>
<li>합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.</li>
<li>마킹 완료 후 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증합니다.</li>
<li><strong>모든 항목이 합격인 경우 (OK)</strong>:<ul>
<li>모든 수치가 기준치 내에 들어오면 최종 합격(<strong><code>OK</code></strong>) 판정이 내려집니다.</li>
<li>합격 판정과 동시에 선택된 EZD 도안 파일(.ezd)에서 도면품번 및 HKMC품번을 실시간으로 추출하여 각인 및 QR 코드가 자동으로 조합되어 레이저 마킹이 실행됩니다.</li>
<li>마킹 완료 후 EZD 파싱 품번 기반의 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증하고, 검증 완료 시 해당 품번 데이터로 DB 저장을 완료합니다.</li>
</ul>
</li>
</ol>
@ -977,15 +977,17 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
<h2>7.4 레이저 각인 QR 검증 팝업 및 결과 연동</h2>
<p>레이저 마킹이 완료되면 검증을 위해 <strong>QR 검증 팝업 창</strong>이 화면에 활성화되며 바코드 스캐너 또는 가상 키보드로 각인된 QR 코드를 읽어 들입니다.</p>
<ol>
<li><strong>QR 스캔 검증 성공 (즉시 자동 닫힘)</strong></li>
<li><strong>QR 스캔 검증 성공 (즉시 자동 닫힘 및 DB OK 적재)</strong></li>
<li>
<p>스캔된 QR 코드가 예상 QR 코드와 정확히 일치하면 팝업 창이 지연 없이 <strong>즉시 자동으로 닫히며</strong> DB에 <code>OK</code> 상태로 최종 저장을 완료하고 대기 상태로 복구됩니다.</p>
</li>
<li>
<p><strong>QR 스캔 검증 실패 (NG 판정 및 설명 표출)</strong></p>
<p><strong>QR 스캔 검증 실패 (팝업 유지 및 연속 재스캔 지원)</strong></p>
</li>
<li>스캔된 QR 코드가 불일치하거나 검증을 취소하는 경우, 작업자가 [확인] 버튼을 누르면 팝업 창이 닫힙니다.</li>
<li>팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 <strong><code>OK</code></strong>에서 <strong><code>NG</code></strong> (빨간색 테두리 및 배경)로 즉시 전환됩니다.</li>
<li>스캔된 QR 코드가 불일치(대조 실패)할 경우, 팝업 창이 <strong>자동으로 닫히지 않고 열린 상태를 유지</strong>하며 "QR 바코드 불일치" 경고 상태를 표출합니다.</li>
<li>팝업이 열려 있는 동안 바코드/QR 스캐너 또는 키보드 입력을 통해 올바른 QR 코드로 <strong>연속 재스캔</strong>할 수 있으며, 재스캔에서 대조 성공 시 자동으로 통과(닫힘) 및 DB <code>OK</code> 저장 처리됩니다.</li>
<li>대조 불일치 상태에서 작업자가 <strong>[확인]</strong> 또는 <strong>[취소]</strong> (ESC 키) 버튼을 누르면 팝업 창이 닫히며, 판정 결과 <strong><code>NG</code></strong>로 데이터베이스 저장을 완료합니다.</li>
<li>팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 <strong><code>OK</code></strong>에서 <strong><code>NG</code></strong> (빨간색 테두리 및 배경)로 전환됩니다.</li>
<li>공정 진행 상태 및 시스템 상태 바에 <code>"마킹 완료 / 각인 QR 검증 NG"</code>, <code>"상태: 검사 불합격 (각인 QR 검증 실패)"</code>, <code>"각인 QR 검증 실패 (DB NG 저장 완료)"</code> 등 구체적인 원인 설명이 표시되며, DB에 최종 <code>NG</code> 결과로 기록됩니다.</li>
</ol>
<div class="page-break"></div>
@ -996,7 +998,7 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
<p>본 제어 프로그램은 설비 가동 중에 발생하는 계측기와의 통신 상태, 측정값 정보 및 각종 예외 오류 상황을 로그 파일로 기록합니다.</p>
<h3>8.1.1 로그 파일 보관 방법</h3>
<ul>
<li><strong>보관 폴더</strong>: 프로그램 실행 파일 경로의 하위 <strong><code>Logs</code></strong> 폴더에 저장됩니다.</li>
<li><strong>보관 폴더</strong>: 프로그램 설치 경로(<code>C:\marking\</code>) 하위 <strong><code>logs/</code></strong> 폴더에 저장됩니다. (예: <code>C:\marking\logs\2026-08-05.log</code>)</li>
<li><strong>파일명 규칙</strong>: 날짜별로 파일이 자동으로 생성되어 보관됩니다.</li>
<li><strong>로그 중요도 분류</strong>:<ul>
<li><strong>정보 (INFO)</strong>: 정상적인 검사 프로세스 시작, 계측기 연결 성공, 레이저 인쇄 성공 및 데이터베이스 저장 완료 기록.</li>
@ -1070,7 +1072,7 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
<ul>
<li><strong>현상</strong>: 성능 검사는 합격(PASS)했으나, 레이저 장비로의 데이터 전송 단계에서 경고 에러가 발생함.</li>
<li><strong>원인 및 해결</strong>:<ol>
<li><strong>도면 파일 점검</strong>: <code>시험 설정</code> 화면에서 세팅해 놓은 작업 도면 파일이 실제 해당 경로에 존재하는지 점검하십시오.</li>
<li><strong>도면 파일 점검</strong>: 메인 화면의 <code>파일 선택</code> 버튼을 통해 선택한 작업 도면 파일(.ezd)이 실제 해당 경로에 존재하는지 점검하십시오.</li>
<li><strong>마킹기 전원/연결 점검</strong>: 레이저 마킹기의 전원이 켜져 있는지 확인하고, 제어 PC와 마킹기 간의 연결 케이블 상태를 확인하십시오.</li>
</ol>
</li>
@ -1092,8 +1094,8 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
<ul>
<li><strong>현상</strong>: 로그인 화면에서 [로그인] 버튼 클릭 시 "DB 연결 상태 또는 ID/PW를 확인해 주십시오." 경고 팝업이 발생하며 로그인되지 않음.</li>
<li><strong>원인 및 해결</strong>:<ol>
<li><strong>네트워크/DB 서버 점검</strong>: 중앙 MSSQL 서버와의 통신이 단선되었거나 서버 전원이 꺼진 경우 타임아웃(1초)에 의해 경고가 표출됩니다. <code>Database.ini</code> 파일의 IP 설정 및 데이터베이스 서버 연결 상태를 점검하십시오.</li>
<li><strong>계정 정보 확인</strong>: 입력한 사용자 ID 및 비밀번호가 DB 계정 테이블 및 <code>LoginOptions.json</code> 계정 목록에 올바르게 등록되어 있는지 재확인하십시오.</li>
<li><strong>네트워크/DB 서버 점검</strong>: 중앙 MSSQL 서버와의 통신이 단선되었거나 서버 전원이 꺼진 경우 타임아웃(3초)에 의해 경고가 표출됩니다. <code>Database.ini</code> 파일의 IP 설정 및 데이터베이스 서버 연결 상태를 점검하십시오.</li>
<li><strong>계정 정보 확인</strong>: 입력한 사용자 ID 및 비밀번호가 MSSQL <code>Users</code> 테이블에 활성 상태(<code>IsActive=1</code>)로 등록되어 있는지 확인하십시오. (DB 접속 실패 시에도 <code>LoginOptions.json</code> 파일의 <code>loginAccounts</code> 배열에 등록된 계정으로 1차 인증을 진행할 수 있으므로, 필요시 해당 파일의 계정 설정도 함께 점검하십시오.)</li>
</ol>
</li>
</ul>

18
marking_gui/Models/MarkingResult.cs

@ -24,12 +24,30 @@ namespace marking_gui.Models
/// </summary>
public DateTime MarkedAt { get; set; }
/// <summary>
/// EZD 도안에서 파싱하여 사용된 도면품번
/// </summary>
public string DrawingNo { get; set; } = string.Empty;
/// <summary>
/// EZD 도안에서 파싱하여 사용된 HKMC품번
/// </summary>
public string HkmcNo { get; set; } = string.Empty;
/// <summary>
/// EZD 도안 품번 기반으로 최종 조합된 마킹/검증용 QR 코드 문자열
/// </summary>
public string QrString { get; set; } = string.Empty;
public MarkingResult()
{
Success = false;
ErrorCode = -1;
ErrorMessage = "Uninitialized";
MarkedAt = DateTime.MinValue;
DrawingNo = string.Empty;
HkmcNo = string.Empty;
QrString = string.Empty;
}
}
}

1
marking_gui/PortSettingsWindow.xaml

@ -105,7 +105,6 @@
</ScrollViewer>
<Grid Grid.Row="2" Margin="0,15,0,0">
<Button x:Name="btnScanTest" Content="📷 스캔 테스트" Style="{StaticResource DialogButtonStyle}" Background="#4F46E5" Width="120" Height="32" Click="btnScanTest_Click" HorizontalAlignment="Left"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="btnSave" Content="저장" Style="{StaticResource DialogButtonStyle}" Width="70" Height="32" Click="btnSave_Click" Margin="0,0,10,0"/>
<Button x:Name="btnCancel" Content="취소" Style="{StaticResource CancelButtonStyle}" Width="70" Height="32" Click="btnCancel_Click"/>

50
marking_gui/PortSettingsWindow.xaml.cs

@ -198,56 +198,6 @@ namespace marking_gui
}
}
private void btnScanTest_Click(object sender, RoutedEventArgs e)
{
string scannerPort = cbScannerPort?.SelectedItem?.ToString() ?? "Auto";
int scannerBaud = 115200;
if (cbScannerBaudRate?.SelectedItem is ComboBoxItem selectedBaudItem && int.TryParse(selectedBaudItem.Content?.ToString(), out int parsedBaud))
{
scannerBaud = parsedBaud;
}
var tempSettings = new BarcodeScannerSettings
{
Enabled = true,
PortName = scannerPort,
BaudRate = scannerBaud
};
SerialBarcodeScanner tempScanner = null;
try
{
try
{
tempScanner = new SerialBarcodeScanner(tempSettings);
}
catch { }
string testSampleQr = "TEST_PORT_SCAN;DRAWING_01;HKMC_99;2607299999;";
var qrTestWin = new QrVerificationWindow(testSampleQr, tempScanner)
{
Owner = this,
Title = $"📷 [스캐너 포트 {scannerPort} / {scannerBaud} Baud] 연동 테스트"
};
bool? testResult = qrTestWin.ShowDialog();
if (testResult == true)
{
MessageBox.Show($"스캐너 (포트: {scannerPort}) 통신 및 QR 대조 검증 성공!\n\n현재 선택한 포트로 스캐너가 정상 작동합니다.", "스캔 테스트 성공", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
MessageBox.Show($"스캔 테스트가 중단되었거나 취소되었습니다.", "스캔 테스트 종료", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
finally
{
try { tempScanner?.Stop(); } catch { }
try { tempScanner?.Dispose(); } catch { }
}
}
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;

16
marking_gui/QrVerificationWindow.xaml.cs

@ -206,6 +206,14 @@ namespace marking_gui
txtStatusMessage.Text = "QR 코드 검증 성공";
txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(22, 163, 74)); // Dark Green
}
// 합격 시 즉시 팝업 자동 닫기 (요청 사항: 합격은 자동 닫힘 유지)
if (!_isClosing)
{
_isClosing = true;
DialogResult = true;
Close();
}
}
else
{
@ -238,14 +246,8 @@ namespace marking_gui
txtStatusMessage.Text = "QR 바코드 불일치";
txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(220, 38, 38)); // Red
}
}
// 합격이든 불합격이든 스캔 완료 즉시 팝업 닫기
if (!_isClosing)
{
_isClosing = true;
DialogResult = _isPassed;
Close();
// 불합격(대조 실패) 시 팝업을 자동으로 닫지 않고 유지하여 계속 스캔 데이터 수신
}
}

13
marking_gui/Services/Board4251Service.cs

@ -209,6 +209,11 @@ namespace marking_gui.Services
string currentData;
lock (_bufferLock)
{
// 수신 버퍼 8KB(8192자) 상한선 제한하여 메모리 누수 및 오버플로우 방지
if (_receiveBuffer.Length > 8192)
{
_receiveBuffer.Clear();
}
_receiveBuffer.Append(data);
currentData = _receiveBuffer.ToString();
}
@ -235,6 +240,14 @@ namespace marking_gui.Services
public void Dispose()
{
try
{
if (_communication != null)
{
_communication.DataReceived -= OnDataReceived;
}
}
catch { }
Disconnect();
_reconnectTimer?.Dispose();
_lock.Dispose();

390
marking_gui/Services/EzdMarkingService.cs

@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using marking_gui.Models;
namespace marking_gui.Services
@ -38,15 +39,9 @@ namespace marking_gui.Services
[DllImport("MarkEzdWrapper.dll", EntryPoint = "ChangeTextByName", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
private static extern int ChangeTextByNameW(IntPtr hHandle, string strTextName, string strTextNew);
[DllImport("MarkEzdWrapper.dll", EntryPoint = "ChangeTextByName", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern int ChangeTextByNameA(IntPtr hHandle, string strTextName, string strTextNew);
[DllImport("MarkEzdWrapper.dll", EntryPoint = "GetTextByName", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
private static extern int GetTextByNameW(IntPtr hHandle, string strTextName, System.Text.StringBuilder strText);
[DllImport("MarkEzdWrapper.dll", EntryPoint = "GetTextByName", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern int GetTextByNameA(IntPtr hHandle, string strTextName, System.Text.StringBuilder strText);
[DllImport("MarkEzdWrapper.dll", EntryPoint = "Mark", CallingConvention = CallingConvention.Cdecl)]
private static extern int Mark(IntPtr hHandle);
@ -61,6 +56,7 @@ namespace marking_gui.Services
private IntPtr _lastWindowHandle = IntPtr.Zero;
private string _lastConfigPath = string.Empty;
private string _lastLoadedEzdPath = string.Empty;
public bool Initialize(string configPath, IntPtr windowHandle)
{
@ -68,92 +64,74 @@ namespace marking_gui.Services
{
try
{
_lastConfigPath = configPath;
if (windowHandle != IntPtr.Zero) _lastWindowHandle = windowHandle;
_lastConfigPath = configPath;
if (windowHandle != IntPtr.Zero) _lastWindowHandle = windowHandle;
_lastInitErrorMessage = string.Empty;
_config = new MarkingConfigService();
_ezdFolderPath = _config.EzdFolderPath;
if (string.IsNullOrEmpty(_ezdFolderPath))
{
_lastInitErrorMessage = "Setting.ini 내 EzdFolderPath 경로가 비어 있습니다.";
_isInitialized = false;
return false;
}
_lastInitErrorMessage = string.Empty;
_config = new MarkingConfigService();
_ezdFolderPath = _config.EzdFolderPath;
// EZCAD.CFG 파일 유무 검증 및 상위/하위 경로 자동 탐색
string cfgFile = Path.Combine(_ezdFolderPath, "EZCAD.CFG");
if (!File.Exists(cfgFile))
{
if (File.Exists(@"c:\EzCad\EZCAD.CFG"))
if (string.IsNullOrEmpty(_ezdFolderPath))
{
_ezdFolderPath = @"c:\EzCad";
_lastInitErrorMessage = "Setting.ini 내 EzdFolderPath 경로가 비어 있습니다.";
_isInitialized = false;
return false;
}
else if (File.Exists(@"c:\EzCad\EasyCfg\EZCAD.CFG"))
// EZCAD.CFG 파일 유무 검증 및 상위/하위 경로 자동 탐색
string cfgFile = Path.Combine(_ezdFolderPath, "EZCAD.CFG");
if (!File.Exists(cfgFile))
{
_ezdFolderPath = @"c:\EzCad\EasyCfg";
if (File.Exists(@"c:\EzCad\EZCAD.CFG"))
{
_ezdFolderPath = @"c:\EzCad";
}
else if (File.Exists(@"c:\EzCad\EasyCfg\EZCAD.CFG"))
{
_ezdFolderPath = @"c:\EzCad\EasyCfg";
}
}
}
if (!Directory.Exists(_ezdFolderPath))
{
_lastInitErrorMessage = string.Format("EzCad 폴더가 존재하지 않습니다: {0} (Setting.ini 설정 확인 필요)", _ezdFolderPath);
_isInitialized = false;
return false;
}
// 기존 핸들이 열려 있다면 정리
if (_markingHandle != IntPtr.Zero)
{
try { Close(_markingHandle); } catch { }
_markingHandle = IntPtr.Zero;
}
if (!Directory.Exists(_ezdFolderPath))
{
_lastInitErrorMessage = string.Format("마킹 라이브러리 폴더가 존재하지 않습니다: {0}", _ezdFolderPath);
_isInitialized = false;
return false;
}
System.Diagnostics.Debug.WriteLine("[EzdMarking] 마킹 기계 LMC 초기화 시도. EzCadPath: " + _ezdFolderPath + ", WindowHandle: " + windowHandle);
// 1. InitMarkEzd() 호출로 디바이스 전역 제어 핸들 획득
_markingHandle = InitMarkEzd();
System.Diagnostics.Debug.WriteLine(string.Format("[EzdMarking] InitMarkEzd() 완료. 생성된 핸들 포인터: 0x{0:X}", _markingHandle.ToInt64()));
if (_markingHandle == IntPtr.Zero)
{
_markingHandle = InitMarkEzd();
}
if (_markingHandle == IntPtr.Zero)
{
_isInitialized = false;
_lastInitErrorMessage = "InitMarkEzd() 실행 결과 생성된 핸들 포인터가 Zero(Null)입니다. 마킹 보드 및 드라이버 연결 상태를 점검하세요.";
return false;
}
if (_markingHandle == IntPtr.Zero)
{
_lastInitErrorMessage = "InitMarkEzd() 생성 실패 (IntPtr.Zero)";
_isInitialized = false;
return false;
}
// 2. 획득된 핸들을 이용해 Init() 장치 매핑 수행
int result = Init(_markingHandle, _ezdFolderPath, windowHandle);
if (result == 0) // 0 이 성공 코드
{
_isInitialized = true;
System.Diagnostics.Debug.WriteLine("[EzdMarking] 실서버 LMC 컨트롤 보드 장치 연결 및 초기화 성공.");
}
else
{
_isInitialized = false;
_lastInitErrorMessage = string.Format("LMC 컨트롤 카드 통신 실패 (Init 에러코드: {0})", result);
System.Diagnostics.Debug.WriteLine("[EzdMarking] LMC 장치 연결 실패. 에러 코드: " + result);
try { Close(_markingHandle); } catch { }
_markingHandle = IntPtr.Zero;
int result = Init(_markingHandle, _ezdFolderPath, windowHandle);
if (result == 0)
{
_isInitialized = true;
_lastLoadedEzdPath = string.Empty;
System.Diagnostics.Debug.WriteLine("[EzdMarking] 마킹기 연동 성공");
return true;
}
else
{
_lastInitErrorMessage = string.Format("Init() 실패 코드: {0} (경로: {1})", result, _ezdFolderPath);
try { Close(_markingHandle); } catch { }
_markingHandle = IntPtr.Zero;
_isInitialized = false;
return false;
}
}
return _isInitialized;
}
catch (DllNotFoundException ex)
{
_isInitialized = false;
_lastInitErrorMessage = "MarkEzdWrapper.dll 또는 하위 C++ DLL 로드 실패 (x86/32비트 아키텍처 타겟팅 컴파일 여부 확인 필요)";
System.Diagnostics.Debug.WriteLine("[EzdMarking] MarkEzdWrapper.dll 또는 하위 DLL(Lmc1.dll 등)을 로드할 수 없습니다. 실행 폴더에 복사했는지 확인하세요. 상세: " + ex.Message);
return false;
}
catch (Exception ex)
{
_lastInitErrorMessage = "초기화 도중 예외: " + ex.Message;
_isInitialized = false;
_lastInitErrorMessage = "마킹 컨트롤 보드 연동 예외 발생: " + ex.Message;
System.Diagnostics.Debug.WriteLine("[EzdMarking] 초기화 예외 발생: " + ex.Message);
return false;
}
}
@ -163,167 +141,177 @@ namespace marking_gui.Services
[System.Security.SecurityCritical]
public MarkingResult RequestMarking(MarkingRequest request)
{
if (!_isInitialized || _markingHandle == IntPtr.Zero)
{
// 각인 요청 시 미초기화 상태이면 1회 자동 재시도(Retry) 수행
System.Diagnostics.Debug.WriteLine("[EzdMarking] 디바이스 미초기화 상태 감지. 재연결 시도...");
Initialize(_lastConfigPath, _lastWindowHandle);
}
if (!_isInitialized || _markingHandle == IntPtr.Zero)
{
return new MarkingResult
{
Success = false,
ErrorCode = -10,
ErrorMessage = "LMC 마킹 디바이스가 연결 및 초기화되지 않았습니다. 상세 원인: " + _lastInitErrorMessage,
MarkedAt = DateTime.Now
};
}
try
lock (_handleLock)
{
// 1. 도안 파일 경로 확인
string ezdFile = request.EzdFileName;
string ezdPath = ezdFile;
if (!File.Exists(ezdPath))
if (!_isInitialized || _markingHandle == IntPtr.Zero)
{
ezdPath = Path.Combine(_ezdFolderPath, ezdFile);
System.Diagnostics.Debug.WriteLine("[EzdMarking] 디바이스 미초기화 상태 감지. 재연결 시도...");
Initialize(_lastConfigPath, _lastWindowHandle);
}
if (!File.Exists(ezdPath))
if (!_isInitialized || _markingHandle == IntPtr.Zero)
{
ezdPath = Path.Combine(_ezdFolderPath, "EasyCfg", ezdFile);
}
if (!File.Exists(ezdPath))
{
// 최종 확인 실패 시 에러
return new MarkingResult
{
Success = false,
ErrorCode = -11,
ErrorMessage = string.Format("도안 파일(.ezd)이 존재하지 않습니다: {0}", ezdFile),
ErrorCode = -10,
ErrorMessage = "LMC 마킹 디바이스가 연결 및 초기화되지 않았습니다. 상세 원인: " + _lastInitErrorMessage,
MarkedAt = DateTime.Now
};
}
System.Diagnostics.Debug.WriteLine("[EzdMarking] 도안 파일 로드 시작: " + ezdPath);
string absoluteEzdPath = Path.GetFullPath(ezdPath);
int loadResult = LoadEzdFile(_markingHandle, absoluteEzdPath); // 획득한 핸들 전달
if (loadResult != 0)
try
{
return new MarkingResult
string ezdFile = request.EzdFileName;
string ezdPath = ezdFile;
if (!File.Exists(ezdPath)) ezdPath = Path.Combine(_ezdFolderPath, ezdFile);
if (!File.Exists(ezdPath)) ezdPath = Path.Combine(_ezdFolderPath, "EasyCfg", ezdFile);
if (!File.Exists(ezdPath))
{
Success = false,
ErrorCode = loadResult,
ErrorMessage = string.Format("도안 파일(.ezd) 로드 실패. (경로: {0}, LoadEzdFile 에러코드: {1})", absoluteEzdPath, loadResult),
MarkedAt = DateTime.Now
};
}
return new MarkingResult
{
Success = false,
ErrorCode = -11,
ErrorMessage = string.Format("도안 파일(.ezd)이 존재하지 않습니다: {0}", ezdFile),
MarkedAt = DateTime.Now
};
}
// 1-1. 도안 파일(.ezd) 내부의 고정 텍스트 오브젝트(도면품번, HKMC 품번)를 직접 읽어오기
string ezdDrawingNo = GetTextFromEzd(_config.DrawingNoTag);
string ezdHkmcNo = GetTextFromEzd(_config.HkmcNoTag);
string absoluteEzdPath = Path.GetFullPath(ezdPath);
if (!string.IsNullOrWhiteSpace(ezdDrawingNo))
{
request.DrawingNo = ezdDrawingNo;
}
if (!string.IsNullOrWhiteSpace(ezdHkmcNo))
{
request.HkmcNo = ezdHkmcNo;
}
if (!string.Equals(_lastLoadedEzdPath, absoluteEzdPath, StringComparison.OrdinalIgnoreCase))
{
System.Diagnostics.Debug.WriteLine("[EzdMarking] 도안 파일 신규 로드: " + absoluteEzdPath);
int loadResult = LoadEzdFile(_markingHandle, absoluteEzdPath);
if (loadResult != 0)
{
_lastLoadedEzdPath = string.Empty;
return new MarkingResult
{
Success = false,
ErrorCode = loadResult,
ErrorMessage = string.Format("도안 파일(.ezd) 로드 실패. (경로: {0}, LoadEzdFile 에러코드: {1})", absoluteEzdPath, loadResult),
MarkedAt = DateTime.Now
};
}
_lastLoadedEzdPath = absoluteEzdPath;
}
// EZD 고정값 기반으로 QR 코드 데이터 문자열 최종 재조합 ([IC S/N];[도면품번];[HKMC품번];[제품 S/N];)
string icSerial = string.IsNullOrWhiteSpace(request.IcSerial) ? request.Serial : request.IcSerial;
request.QrString = string.Format("{0};{1};{2};{3};", icSerial, request.DrawingNo, request.HkmcNo, request.Serial);
string ezdDrawingNo = GetTextFromEzd(_config.DrawingNoTag, absoluteEzdPath);
string ezdHkmcNo = GetTextFromEzd(_config.HkmcNoTag, absoluteEzdPath);
// 2. 도안 내 지정된 5개 변수 태그만 치환 (SERIAL, YYMMDD, DATAMATRIX, PARTNO, HKMCPO)
string seqOnly = (!string.IsNullOrEmpty(request.Serial) && request.Serial.Length >= 4)
? request.Serial.Substring(request.Serial.Length - 4)
: request.Serial;
if (string.IsNullOrWhiteSpace(ezdDrawingNo) && _config != null)
ezdDrawingNo = _config.GetDrawingNoForEzd(absoluteEzdPath);
if (string.IsNullOrWhiteSpace(ezdHkmcNo) && _config != null)
ezdHkmcNo = _config.GetHkmcNoForEzd(absoluteEzdPath);
ChangeTextInEzd(_config.SerialTag, seqOnly); // 1. SERIAL (4자리 순번)
ChangeTextInEzd(_config.LotNoTag, request.LotNo); // 2. YYMMDD (LotNo)
ChangeTextInEzd(_config.QrCodeTag, request.QrString); // 3. DATAMATRIX (QR코드)
ChangeTextInEzd(_config.DrawingNoTag, request.DrawingNo); // 4. PARTNO (도면품번)
ChangeTextInEzd(_config.HkmcNoTag, request.HkmcNo); // 5. HKMCPO (HKMC품번)
if (!string.IsNullOrWhiteSpace(ezdDrawingNo)) request.DrawingNo = ezdDrawingNo;
if (!string.IsNullOrWhiteSpace(ezdHkmcNo)) request.HkmcNo = ezdHkmcNo;
System.Diagnostics.Debug.WriteLine(string.Format(
"[EzdMarking] 도안 텍스트 치환 완료. Serial:{0}, LotNo:{1}, Product:{2}, Drawing:{3}, Hkmc:{4}, Qr:{5}",
request.Serial, request.LotNo, request.ProductNo, request.DrawingNo, request.HkmcNo, request.QrString));
string icSerial = string.IsNullOrWhiteSpace(request.IcSerial) ? request.Serial : request.IcSerial;
request.QrString = string.Format("{0};{1};{2};{3};", icSerial, request.DrawingNo, request.HkmcNo, request.Serial);
// 3. 레이저 마킹 시작 (획득한 핸들 전달)
System.Diagnostics.Debug.WriteLine("[EzdMarking] 레이저 마킹 기계 동작 시작...");
int markResult = Mark(_markingHandle);
string seqOnly = (!string.IsNullOrEmpty(request.Serial) && request.Serial.Length >= 4)
? request.Serial.Substring(request.Serial.Length - 4)
: request.Serial;
if (markResult == 0)
{
System.Diagnostics.Debug.WriteLine("[EzdMarking] 마킹 기계 동작 완료 (성공)");
return new MarkingResult
ChangeTextInEzd(_config.SerialTag, seqOnly);
ChangeTextInEzd(_config.LotNoTag, request.LotNo);
ChangeTextInEzd(_config.QrCodeTag, request.QrString);
if (!string.IsNullOrWhiteSpace(request.DrawingNo)) ChangeTextInEzd(_config.DrawingNoTag, request.DrawingNo);
if (!string.IsNullOrWhiteSpace(request.HkmcNo)) ChangeTextInEzd(_config.HkmcNoTag, request.HkmcNo);
System.Diagnostics.Debug.WriteLine("[EzdMarking] 레이저 마킹 기계 동작 시작 (30초 타임아웃 제한)...");
IntPtr handleToMark = _markingHandle;
var markingTask = Task.Run(() => Mark(handleToMark));
var timeoutTask = Task.Delay(30000);
var completedTask = Task.WhenAny(markingTask, timeoutTask).Result;
if (completedTask == timeoutTask)
{
Success = true,
ErrorCode = 0,
ErrorMessage = "마킹 완료",
MarkedAt = DateTime.Now
};
}
else
{
System.Diagnostics.Debug.WriteLine("[EzdMarking] 마킹 기계 동작 에러 발생: " + markResult);
return new MarkingResult
_isInitialized = false;
_markingHandle = IntPtr.Zero;
_lastLoadedEzdPath = string.Empty;
return new MarkingResult { Success = false, ErrorCode = -98, ErrorMessage = "레이저 각인 하드웨어 응답 타임아웃 (30초)", MarkedAt = DateTime.Now };
}
int markResult = markingTask.Result;
if (markResult == 0)
{
Success = false,
ErrorCode = markResult,
ErrorMessage = string.Format("레이저 각인 하드웨어 동작 에러 (에러코드: {0})", markResult),
MarkedAt = DateTime.Now
};
return new MarkingResult
{
Success = true,
ErrorCode = 0,
ErrorMessage = "마킹 완료",
MarkedAt = DateTime.Now,
DrawingNo = request.DrawingNo,
HkmcNo = request.HkmcNo,
QrString = request.QrString
};
}
else
{
_isInitialized = false;
_markingHandle = IntPtr.Zero;
_lastLoadedEzdPath = string.Empty;
return new MarkingResult { Success = false, ErrorCode = markResult, ErrorMessage = string.Format("레이저 각인 하드웨어 동작 에러 (에러코드: {0})", markResult), MarkedAt = DateTime.Now };
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("[EzdMarking] RequestMarking 실행 중 예외 발생: " + ex.Message);
return new MarkingResult
catch (Exception ex)
{
Success = false,
ErrorCode = -99,
ErrorMessage = "마킹 처리 중 예외 발생: " + ex.Message,
MarkedAt = DateTime.Now
};
_isInitialized = false;
_markingHandle = IntPtr.Zero;
_lastLoadedEzdPath = string.Empty;
return new MarkingResult { Success = false, ErrorCode = -99, ErrorMessage = "마킹 처리 중 예외 발생: " + ex.Message, MarkedAt = DateTime.Now };
}
}
}
private bool _disposed = false;
private string GetTextFromEzd(string tagName)
public string GetTextFromEzd(string tagName, string ezdFilePath = null)
{
if (string.IsNullOrWhiteSpace(tagName) || _markingHandle == IntPtr.Zero)
return string.Empty;
if (string.IsNullOrWhiteSpace(tagName)) return string.Empty;
try
// 1단계: P/Invoke GetTextByNameW 호출 (DLL 지원 시 도안 텍스트 읽기)
if (_markingHandle != IntPtr.Zero)
{
// 1차: Unicode (UTF-16) 시도
var sbW = new System.Text.StringBuilder(512);
int resW = GetTextByNameW(_markingHandle, tagName, sbW);
if (resW == 0 && sbW.Length > 0)
try
{
return sbW.ToString().Trim();
var sbW = new System.Text.StringBuilder(512);
int resW = GetTextByNameW(_markingHandle, tagName, sbW);
if (resW == 0 && sbW.Length > 0)
{
string val = sbW.ToString().Trim();
if (!string.IsNullOrWhiteSpace(val)) return val;
}
}
// 2차: Ansi (CP949/EUC-KR) 시도 (C++ Wrapper가 ANSI인 경우 대비)
var sbA = new System.Text.StringBuilder(512);
int resA = GetTextByNameA(_markingHandle, tagName, sbA);
if (resA == 0 && sbA.Length > 0)
catch (EntryPointNotFoundException)
{
System.Diagnostics.Debug.WriteLine($"[EzdMarking] GetTextByName({tagName}) DLL 내 엔트리포인트 없음.");
}
catch (Exception ex)
{
return sbA.ToString().Trim();
System.Diagnostics.Debug.WriteLine($"[EzdMarking] GetTextByName({tagName}) 읽기 예외: {ex.Message}");
}
}
catch (Exception ex)
// 2단계: Setting.ini / MarkingConfigService 사용자 명시 설정값 참조
if (_config != null)
{
System.Diagnostics.Debug.WriteLine($"[EzdMarking] GetTextByName({tagName}) 읽기 예외: {ex.Message}");
if (tagName.Equals(_config.DrawingNoTag, StringComparison.OrdinalIgnoreCase))
{
return _config.GetDrawingNoForEzd(ezdFilePath);
}
if (tagName.Equals(_config.HkmcNoTag, StringComparison.OrdinalIgnoreCase))
{
return _config.GetHkmcNoForEzd(ezdFilePath);
}
}
return string.Empty;
}
@ -332,11 +320,7 @@ namespace marking_gui.Services
if (string.IsNullOrWhiteSpace(tagName) || _markingHandle == IntPtr.Zero) return;
try
{
int resW = ChangeTextByNameW(_markingHandle, tagName, textNew);
if (resW != 0)
{
ChangeTextByNameA(_markingHandle, tagName, textNew);
}
ChangeTextByNameW(_markingHandle, tagName, textNew);
}
catch
{

24
marking_gui/Services/MarkingConfigService.cs

@ -122,6 +122,30 @@ namespace marking_gui.Services
public string DrawingNo => GetSetting("DrawingNo", string.Empty);
public string HkmcNo => GetSetting("HkmcNo", string.Empty);
public string GetDrawingNoForEzd(string ezdFileName)
{
if (!string.IsNullOrWhiteSpace(ezdFileName))
{
string fileNameOnly = System.IO.Path.GetFileName(ezdFileName);
string key = "DrawingNo_" + fileNameOnly;
string val = GetSetting(key, string.Empty);
if (!string.IsNullOrWhiteSpace(val)) return val;
}
return DrawingNo;
}
public string GetHkmcNoForEzd(string ezdFileName)
{
if (!string.IsNullOrWhiteSpace(ezdFileName))
{
string fileNameOnly = System.IO.Path.GetFileName(ezdFileName);
string key = "HkmcNo_" + fileNameOnly;
string val = GetSetting(key, string.Empty);
if (!string.IsNullOrWhiteSpace(val)) return val;
}
return HkmcNo;
}
public bool IsTestMode
{
get

122
marking_gui/Services/SerialBarcodeScanner.cs

@ -67,12 +67,23 @@ namespace marking_gui.Services
private readonly BarcodeScannerSettings _settings;
private readonly HashSet<string> _excludedPortNames;
private readonly List<PortSession> _sessions = new List<PortSession>();
private readonly object _sessionsLock = new object();
private Timer? _autoReconnectTimer;
private string? _confirmedPortName;
private bool _disposed;
public event EventHandler<BarcodeScanReceivedEventArgs>? ScanReceived;
public bool IsRunning => _sessions.Count > 0;
public bool IsRunning
{
get
{
lock (_sessionsLock)
{
return _sessions.Any(s => s.Port.IsOpen);
}
}
}
public string? ConfirmedPortName => _confirmedPortName;
public SerialBarcodeScanner(BarcodeScannerSettings settings, IEnumerable<string>? excludedPortNames = null)
@ -87,6 +98,17 @@ namespace marking_gui.Services
Stop();
lock (_sessionsLock)
{
OpenCandidatePorts();
}
// 5초 주기로 닫힌 포트 자동 재연결 모니터링 타이머 가동
_autoReconnectTimer ??= new Timer(_ => AutoReconnectCallback(), null, 5000, 5000);
}
private void OpenCandidatePorts()
{
var candidatePorts = GetCandidatePortNames();
if (candidatePorts.Length == 0)
{
@ -96,6 +118,9 @@ namespace marking_gui.Services
foreach (var portName in candidatePorts)
{
if (_sessions.Any(s => string.Equals(s.Port.PortName, portName, StringComparison.OrdinalIgnoreCase)))
continue;
try
{
var port = CreateSerialPort(portName);
@ -117,19 +142,59 @@ namespace marking_gui.Services
}
}
public void Stop()
private void AutoReconnectCallback()
{
foreach (var session in _sessions)
if (_disposed || !_settings.Enabled) return;
lock (_sessionsLock)
{
try
{
session.Port.DataReceived -= SerialPort_DataReceived;
session.Dispose();
// 닫히거나 훼손된 포트 세션 정리
for (int i = _sessions.Count - 1; i >= 0; i--)
{
var session = _sessions[i];
if (!session.Port.IsOpen)
{
try
{
session.Port.DataReceived -= SerialPort_DataReceived;
session.Dispose();
}
catch { }
_sessions.RemoveAt(i);
}
}
// 닫힌 포트가 발견되었을 때만 후보 포트 재오픈 시도
OpenCandidatePorts();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[SerialBarcodeScanner] AutoReconnect 예외: {ex.Message}");
}
catch { }
}
_sessions.Clear();
_confirmedPortName = null;
}
public void Stop()
{
_autoReconnectTimer?.Dispose();
_autoReconnectTimer = null;
lock (_sessionsLock)
{
foreach (var session in _sessions)
{
try
{
session.Port.DataReceived -= SerialPort_DataReceived;
session.Dispose();
}
catch { }
}
_sessions.Clear();
_confirmedPortName = null;
}
}
public void ConfirmPort(string portName)
@ -141,19 +206,22 @@ namespace marking_gui.Services
_confirmedPortName = normalized;
// 포트가 확정되면 다른 미사용 테스트 포트 세션 해제
for (int i = _sessions.Count - 1; i >= 0; i--)
lock (_sessionsLock)
{
var session = _sessions[i];
if (!string.Equals(session.Port.PortName, normalized, StringComparison.OrdinalIgnoreCase))
// 포트가 확정되면 다른 미사용 테스트 포트 세션 해제
for (int i = _sessions.Count - 1; i >= 0; i--)
{
try
var session = _sessions[i];
if (!string.Equals(session.Port.PortName, normalized, StringComparison.OrdinalIgnoreCase))
{
session.Port.DataReceived -= SerialPort_DataReceived;
session.Dispose();
try
{
session.Port.DataReceived -= SerialPort_DataReceived;
session.Dispose();
}
catch { }
_sessions.RemoveAt(i);
}
catch { }
_sessions.RemoveAt(i);
}
}
}
@ -212,7 +280,11 @@ namespace marking_gui.Services
{
if (sender is not SerialPort port) return;
var session = _sessions.FirstOrDefault(s => ReferenceEquals(s.Port, port));
PortSession? session = null;
lock (_sessionsLock)
{
session = _sessions.FirstOrDefault(s => ReferenceEquals(s.Port, port));
}
if (session == null) return;
try
@ -222,6 +294,11 @@ namespace marking_gui.Services
lock (session.Buffer)
{
// 수신 버퍼 4KB(4096자) 상한선 제한하여 메모리 누수 방지
if (session.Buffer.Length > 4096)
{
session.Buffer.Clear();
}
session.Buffer.Append(text);
}
@ -248,7 +325,14 @@ namespace marking_gui.Services
bool isConfirmed = string.Equals(_confirmedPortName, session.Port.PortName, StringComparison.OrdinalIgnoreCase);
ScanReceived?.Invoke(this, new BarcodeScanReceivedEventArgs(session.Port.PortName, scanText, isConfirmed));
try
{
ScanReceived?.Invoke(this, new BarcodeScanReceivedEventArgs(session.Port.PortName, scanText, isConfirmed));
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[ScanReceived Event Error] {ex.Message}");
}
}
public void Dispose()

5
marking_gui/Setting.ini

@ -9,10 +9,8 @@ EzdFolderPath=c:\EzCad
; 테스트 모드 여부 (true 시 하단 테스트 마킹, 강제 검사 버튼 노출 반대 false)
test=true
; 도면품번 (EZD 도안의 도면품번 태그에 채워질 값 — DrawingNoTag에 매핑)
; 도면품번 및 HKMC품번 지정 (사용자가 설정)
DrawingNo=APTSL04A
; HKMC 품번 (EZD 도안의 HKMC 품번 태그에 채워질 값 — HkmcNoTag에 매핑)
HkmcNo=254H1-QI080
@ -36,6 +34,7 @@ DrawingNoTag=PARTNO
HkmcNoTag=HKMCPO
; =================================================================
; 계측기 연동 설정 (SCPI / LAN)
; =================================================================

4
marking_gui/SettingsWindow.xaml.cs

@ -17,8 +17,8 @@ namespace marking_gui
Limits = new MeasurementLimits(limits.MinVoltage, limits.MaxVoltage, limits.MinCurrent, limits.MaxCurrent, limits.MinResistance, limits.MaxResistance);
txtMinVoltage.Text = Limits.MinVoltage.ToString("F1");
txtMaxVoltage.Text = Limits.MaxVoltage.ToString("F1");
txtMinVoltage.Text = Limits.MinVoltage.ToString("F2");
txtMaxVoltage.Text = Limits.MaxVoltage.ToString("F2");
txtMinCurrent.Text = Limits.MinCurrent.ToString("F2");
txtMaxCurrent.Text = Limits.MaxCurrent.ToString("F2");
txtMinResistance.Text = Limits.MinResistance.ToString("F3");

BIN
test_bukuk.ezd

Binary file not shown.
Loading…
Cancel
Save