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

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

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

2
marking_gui/MainWindow.xaml

@ -449,7 +449,7 @@
</StackPanel> </StackPanel>
</Viewbox> </Viewbox>
<Viewbox Grid.Row="2" Stretch="Uniform" MaxHeight="18" HorizontalAlignment="Center"> <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> </Viewbox>
</Grid> </Grid>
</Border> </Border>

124
marking_gui/MainWindow.xaml.cs

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

4
marking_gui/Manual/.obsidian/workspace.json

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

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

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

18
marking_gui/Models/MarkingResult.cs

@ -24,12 +24,30 @@ namespace marking_gui.Models
/// </summary> /// </summary>
public DateTime MarkedAt { get; set; } 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() public MarkingResult()
{ {
Success = false; Success = false;
ErrorCode = -1; ErrorCode = -1;
ErrorMessage = "Uninitialized"; ErrorMessage = "Uninitialized";
MarkedAt = DateTime.MinValue; MarkedAt = DateTime.MinValue;
DrawingNo = string.Empty;
HkmcNo = string.Empty;
QrString = string.Empty;
} }
} }
} }

1
marking_gui/PortSettingsWindow.xaml

@ -105,7 +105,6 @@
</ScrollViewer> </ScrollViewer>
<Grid Grid.Row="2" Margin="0,15,0,0"> <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"> <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="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"/> <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) private void btnCancel_Click(object sender, RoutedEventArgs e)
{ {
DialogResult = false; DialogResult = false;

16
marking_gui/QrVerificationWindow.xaml.cs

@ -206,6 +206,14 @@ namespace marking_gui
txtStatusMessage.Text = "QR 코드 검증 성공"; txtStatusMessage.Text = "QR 코드 검증 성공";
txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(22, 163, 74)); // Dark Green txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(22, 163, 74)); // Dark Green
} }
// 합격 시 즉시 팝업 자동 닫기 (요청 사항: 합격은 자동 닫힘 유지)
if (!_isClosing)
{
_isClosing = true;
DialogResult = true;
Close();
}
} }
else else
{ {
@ -238,14 +246,8 @@ namespace marking_gui
txtStatusMessage.Text = "QR 바코드 불일치"; txtStatusMessage.Text = "QR 바코드 불일치";
txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(220, 38, 38)); // Red 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; string currentData;
lock (_bufferLock) lock (_bufferLock)
{ {
// 수신 버퍼 8KB(8192자) 상한선 제한하여 메모리 누수 및 오버플로우 방지
if (_receiveBuffer.Length > 8192)
{
_receiveBuffer.Clear();
}
_receiveBuffer.Append(data); _receiveBuffer.Append(data);
currentData = _receiveBuffer.ToString(); currentData = _receiveBuffer.ToString();
} }
@ -235,6 +240,14 @@ namespace marking_gui.Services
public void Dispose() public void Dispose()
{ {
try
{
if (_communication != null)
{
_communication.DataReceived -= OnDataReceived;
}
}
catch { }
Disconnect(); Disconnect();
_reconnectTimer?.Dispose(); _reconnectTimer?.Dispose();
_lock.Dispose(); _lock.Dispose();

200
marking_gui/Services/EzdMarkingService.cs

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

24
marking_gui/Services/MarkingConfigService.cs

@ -122,6 +122,30 @@ namespace marking_gui.Services
public string DrawingNo => GetSetting("DrawingNo", string.Empty); public string DrawingNo => GetSetting("DrawingNo", string.Empty);
public string HkmcNo => GetSetting("HkmcNo", 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 public bool IsTestMode
{ {
get get

88
marking_gui/Services/SerialBarcodeScanner.cs

@ -67,12 +67,23 @@ namespace marking_gui.Services
private readonly BarcodeScannerSettings _settings; private readonly BarcodeScannerSettings _settings;
private readonly HashSet<string> _excludedPortNames; private readonly HashSet<string> _excludedPortNames;
private readonly List<PortSession> _sessions = new List<PortSession>(); private readonly List<PortSession> _sessions = new List<PortSession>();
private readonly object _sessionsLock = new object();
private Timer? _autoReconnectTimer;
private string? _confirmedPortName; private string? _confirmedPortName;
private bool _disposed; private bool _disposed;
public event EventHandler<BarcodeScanReceivedEventArgs>? ScanReceived; 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 string? ConfirmedPortName => _confirmedPortName;
public SerialBarcodeScanner(BarcodeScannerSettings settings, IEnumerable<string>? excludedPortNames = null) public SerialBarcodeScanner(BarcodeScannerSettings settings, IEnumerable<string>? excludedPortNames = null)
@ -87,6 +98,17 @@ namespace marking_gui.Services
Stop(); Stop();
lock (_sessionsLock)
{
OpenCandidatePorts();
}
// 5초 주기로 닫힌 포트 자동 재연결 모니터링 타이머 가동
_autoReconnectTimer ??= new Timer(_ => AutoReconnectCallback(), null, 5000, 5000);
}
private void OpenCandidatePorts()
{
var candidatePorts = GetCandidatePortNames(); var candidatePorts = GetCandidatePortNames();
if (candidatePorts.Length == 0) if (candidatePorts.Length == 0)
{ {
@ -96,6 +118,9 @@ namespace marking_gui.Services
foreach (var portName in candidatePorts) foreach (var portName in candidatePorts)
{ {
if (_sessions.Any(s => string.Equals(s.Port.PortName, portName, StringComparison.OrdinalIgnoreCase)))
continue;
try try
{ {
var port = CreateSerialPort(portName); var port = CreateSerialPort(portName);
@ -117,7 +142,46 @@ namespace marking_gui.Services
} }
} }
private void AutoReconnectCallback()
{
if (_disposed || !_settings.Enabled) return;
lock (_sessionsLock)
{
try
{
// 닫히거나 훼손된 포트 세션 정리
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}");
}
}
}
public void Stop() public void Stop()
{
_autoReconnectTimer?.Dispose();
_autoReconnectTimer = null;
lock (_sessionsLock)
{ {
foreach (var session in _sessions) foreach (var session in _sessions)
{ {
@ -131,6 +195,7 @@ namespace marking_gui.Services
_sessions.Clear(); _sessions.Clear();
_confirmedPortName = null; _confirmedPortName = null;
} }
}
public void ConfirmPort(string portName) public void ConfirmPort(string portName)
{ {
@ -141,6 +206,8 @@ namespace marking_gui.Services
_confirmedPortName = normalized; _confirmedPortName = normalized;
lock (_sessionsLock)
{
// 포트가 확정되면 다른 미사용 테스트 포트 세션 해제 // 포트가 확정되면 다른 미사용 테스트 포트 세션 해제
for (int i = _sessions.Count - 1; i >= 0; i--) for (int i = _sessions.Count - 1; i >= 0; i--)
{ {
@ -157,6 +224,7 @@ namespace marking_gui.Services
} }
} }
} }
}
private SerialPort CreateSerialPort(string portName) private SerialPort CreateSerialPort(string portName)
{ {
@ -212,7 +280,11 @@ namespace marking_gui.Services
{ {
if (sender is not SerialPort port) return; 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; if (session == null) return;
try try
@ -222,6 +294,11 @@ namespace marking_gui.Services
lock (session.Buffer) lock (session.Buffer)
{ {
// 수신 버퍼 4KB(4096자) 상한선 제한하여 메모리 누수 방지
if (session.Buffer.Length > 4096)
{
session.Buffer.Clear();
}
session.Buffer.Append(text); session.Buffer.Append(text);
} }
@ -248,8 +325,15 @@ namespace marking_gui.Services
bool isConfirmed = string.Equals(_confirmedPortName, session.Port.PortName, StringComparison.OrdinalIgnoreCase); bool isConfirmed = string.Equals(_confirmedPortName, session.Port.PortName, StringComparison.OrdinalIgnoreCase);
try
{
ScanReceived?.Invoke(this, new BarcodeScanReceivedEventArgs(session.Port.PortName, scanText, isConfirmed)); 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() public void Dispose()
{ {

5
marking_gui/Setting.ini

@ -9,10 +9,8 @@ EzdFolderPath=c:\EzCad
; 테스트 모드 여부 (true 시 하단 테스트 마킹, 강제 검사 버튼 노출 반대 false) ; 테스트 모드 여부 (true 시 하단 테스트 마킹, 강제 검사 버튼 노출 반대 false)
test=true test=true
; 도면품번 (EZD 도안의 도면품번 태그에 채워질 값 — DrawingNoTag에 매핑) ; 도면품번 및 HKMC품번 지정 (사용자가 설정)
DrawingNo=APTSL04A DrawingNo=APTSL04A
; HKMC 품번 (EZD 도안의 HKMC 품번 태그에 채워질 값 — HkmcNoTag에 매핑)
HkmcNo=254H1-QI080 HkmcNo=254H1-QI080
@ -36,6 +34,7 @@ DrawingNoTag=PARTNO
HkmcNoTag=HKMCPO HkmcNoTag=HKMCPO
; ================================================================= ; =================================================================
; 계측기 연동 설정 (SCPI / LAN) ; 계측기 연동 설정 (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); Limits = new MeasurementLimits(limits.MinVoltage, limits.MaxVoltage, limits.MinCurrent, limits.MaxCurrent, limits.MinResistance, limits.MaxResistance);
txtMinVoltage.Text = Limits.MinVoltage.ToString("F1"); txtMinVoltage.Text = Limits.MinVoltage.ToString("F2");
txtMaxVoltage.Text = Limits.MaxVoltage.ToString("F1"); txtMaxVoltage.Text = Limits.MaxVoltage.ToString("F2");
txtMinCurrent.Text = Limits.MinCurrent.ToString("F2"); txtMinCurrent.Text = Limits.MinCurrent.ToString("F2");
txtMaxCurrent.Text = Limits.MaxCurrent.ToString("F2"); txtMaxCurrent.Text = Limits.MaxCurrent.ToString("F2");
txtMinResistance.Text = Limits.MinResistance.ToString("F3"); txtMinResistance.Text = Limits.MinResistance.ToString("F3");

BIN
test_bukuk.ezd

Binary file not shown.
Loading…
Cancel
Save