29 changed files with 1629 additions and 906 deletions
Binary file not shown.
@ -0,0 +1,88 @@ |
|||
<Window x:Class="marking_gui.QrVerificationWindow" |
|||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
Title="마킹 QR 바코드 검증" Height="520" Width="680" |
|||
WindowStartupLocation="CenterOwner" ResizeMode="NoResize" |
|||
Background="#F8FAFC" FontFamily="Segoe UI, Malgun Gothic" Loaded="Window_Loaded" |
|||
PreviewKeyDown="Window_PreviewKeyDown"> |
|||
|
|||
<Grid Margin="24"> |
|||
<Grid.RowDefinitions> |
|||
<RowDefinition Height="Auto"/> |
|||
<RowDefinition Height="*"/> |
|||
<RowDefinition Height="Auto"/> |
|||
<RowDefinition Height="Auto"/> |
|||
</Grid.RowDefinitions> |
|||
|
|||
<!-- 헤더 영역 --> |
|||
<Border Grid.Row="0" Background="#FFFFFF" CornerRadius="8" Padding="16" Margin="0,0,0,16" BorderBrush="#E2E8F0" BorderThickness="1"> |
|||
<StackPanel> |
|||
<TextBlock Text="레이저 각인 QR 바코드 검증" FontSize="18" FontWeight="Bold" Foreground="#0F172A"/> |
|||
<TextBlock Text="제품 표면에 각인된 QR 코드를 스캐너로 스캔하십시오." FontSize="13" Foreground="#64748B" Margin="0,4,0,0"/> |
|||
</StackPanel> |
|||
</Border> |
|||
|
|||
<!-- 데이터 비교 정보 영역 (상단: 예정값, 하단: 스캔값) --> |
|||
<Grid Grid.Row="1"> |
|||
<Grid.RowDefinitions> |
|||
<RowDefinition Height="*"/> |
|||
<RowDefinition Height="*"/> |
|||
</Grid.RowDefinitions> |
|||
|
|||
<!-- 1. DB 저장 예정 QR 값 --> |
|||
<Border Grid.Row="0" x:Name="borderExpectedCard" Background="#FFFFFF" CornerRadius="8" Padding="16" Margin="0,0,0,12" BorderBrush="#3B82F6" BorderThickness="1.5"> |
|||
<Grid> |
|||
<Grid.RowDefinitions> |
|||
<RowDefinition Height="Auto"/> |
|||
<RowDefinition Height="*"/> |
|||
</Grid.RowDefinitions> |
|||
<DockPanel Grid.Row="0"> |
|||
<TextBlock Text="[DB 저장 예정 QR 값]" FontSize="13" FontWeight="Bold" Foreground="#2563EB"/> |
|||
<TextBlock Text="(정답 데이터)" FontSize="11" Foreground="#94A3B8" Margin="8,0,0,0" VerticalAlignment="Center"/> |
|||
</DockPanel> |
|||
<TextBox Grid.Row="1" x:Name="txtExpectedQr" IsReadOnly="True" BorderThickness="0" Background="Transparent" |
|||
FontSize="14" FontWeight="Bold" Foreground="#0F172A" VerticalAlignment="Center" TextWrapping="Wrap"/> |
|||
</Grid> |
|||
</Border> |
|||
|
|||
<!-- 2. 스캐너로 스캔된 QR 값 --> |
|||
<Border Grid.Row="1" x:Name="borderScannedCard" Background="#FFFFFF" CornerRadius="8" Padding="16" Margin="0,0,0,0" BorderBrush="#CBD5E1" BorderThickness="1.5"> |
|||
<Grid> |
|||
<Grid.RowDefinitions> |
|||
<RowDefinition Height="Auto"/> |
|||
<RowDefinition Height="*"/> |
|||
</Grid.RowDefinitions> |
|||
<DockPanel Grid.Row="0"> |
|||
<TextBlock Text="[스캐너 스캔 입력 값]" FontSize="13" FontWeight="Bold" Foreground="#475569"/> |
|||
<TextBlock x:Name="txtScanStatusSubText" Text="(스캔 대기 중)" FontSize="11" Foreground="#94A3B8" Margin="8,0,0,0" VerticalAlignment="Center"/> |
|||
</DockPanel> |
|||
<TextBox Grid.Row="1" x:Name="txtScannedQr" IsReadOnly="True" BorderThickness="0" Background="Transparent" |
|||
FontSize="14" FontWeight="Bold" Foreground="#64748B" VerticalAlignment="Center" TextWrapping="Wrap" Text="스캔 대기 중..."/> |
|||
</Grid> |
|||
</Border> |
|||
</Grid> |
|||
|
|||
<!-- 상태 텍스트 배너 --> |
|||
<Border Grid.Row="2" x:Name="borderStatusBanner" Background="#F1F5F9" CornerRadius="6" Padding="14" Margin="0,16,0,16"> |
|||
<TextBlock x:Name="txtStatusMessage" Text="스캔 대기 중" FontSize="16" FontWeight="Bold" Foreground="#334155" HorizontalAlignment="Center"/> |
|||
</Border> |
|||
|
|||
<!-- 하단 컨트롤 버튼 (다시스캔 삭제, 취소 및 확인 2개만 깔끔히 배치) --> |
|||
<Grid Grid.Row="3"> |
|||
<Grid.ColumnDefinitions> |
|||
<ColumnDefinition Width="*"/> |
|||
<ColumnDefinition Width="Auto"/> |
|||
<ColumnDefinition Width="12"/> |
|||
<ColumnDefinition Width="Auto"/> |
|||
</Grid.ColumnDefinitions> |
|||
|
|||
<Button Grid.Column="1" x:Name="btnCancel" Content="취소 (닫기)" Width="130" Height="42" |
|||
Background="#94A3B8" Foreground="#FFFFFF" FontSize="14" FontWeight="Bold" |
|||
BorderThickness="0" Cursor="Hand" Click="btnCancel_Click"/> |
|||
|
|||
<Button Grid.Column="3" x:Name="btnConfirm" Content="확인" Width="140" Height="42" |
|||
Background="#3B82F6" Foreground="#FFFFFF" FontSize="14" FontWeight="Bold" |
|||
BorderThickness="0" Cursor="Hand" Click="btnConfirm_Click"/> |
|||
</Grid> |
|||
</Grid> |
|||
</Window> |
|||
@ -0,0 +1,284 @@ |
|||
using System; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using System.Windows; |
|||
using System.Windows.Input; |
|||
using System.Windows.Media; |
|||
using marking_gui.Services; |
|||
|
|||
namespace marking_gui |
|||
{ |
|||
public partial class QrVerificationWindow : Window |
|||
{ |
|||
private readonly string _expectedQr; |
|||
private readonly SerialBarcodeScanner? _serialScanner; |
|||
private bool _isPassed = false; |
|||
private bool _hasScanned = false; |
|||
private bool _isClosing = false; |
|||
private readonly StringBuilder _keyScanBuffer = new StringBuilder(); |
|||
|
|||
public string ScannedQr { get; private set; } = string.Empty; |
|||
|
|||
public QrVerificationWindow(string expectedQr, SerialBarcodeScanner? serialScanner = null) |
|||
{ |
|||
InitializeComponent(); |
|||
_expectedQr = expectedQr ?? string.Empty; |
|||
_serialScanner = serialScanner; |
|||
} |
|||
|
|||
private void Window_Loaded(object sender, RoutedEventArgs e) |
|||
{ |
|||
if (txtExpectedQr != null) |
|||
{ |
|||
txtExpectedQr.Text = _expectedQr; |
|||
} |
|||
|
|||
if (_serialScanner != null) |
|||
{ |
|||
_serialScanner.ScanReceived += SerialScanner_ScanReceived; |
|||
} |
|||
|
|||
ResetToWaitingState(); |
|||
} |
|||
|
|||
private void Window_Unloaded(object sender, RoutedEventArgs e) |
|||
{ |
|||
if (_serialScanner != null) |
|||
{ |
|||
_serialScanner.ScanReceived -= SerialScanner_ScanReceived; |
|||
} |
|||
} |
|||
|
|||
private void SerialScanner_ScanReceived(object? sender, BarcodeScanReceivedEventArgs e) |
|||
{ |
|||
Dispatcher.BeginInvoke(new Action(async () => |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(e.Text)) |
|||
{ |
|||
_serialScanner?.ConfirmPort(e.PortName); |
|||
await ProcessScanResultAsync(e.Text.Trim()); |
|||
} |
|||
}), System.Windows.Threading.DispatcherPriority.Input); |
|||
} |
|||
|
|||
private void ResetToWaitingState() |
|||
{ |
|||
_hasScanned = false; |
|||
_isPassed = false; |
|||
_isClosing = false; |
|||
_keyScanBuffer.Clear(); |
|||
|
|||
if (txtExpectedQr != null) |
|||
{ |
|||
txtExpectedQr.Text = _expectedQr; |
|||
} |
|||
|
|||
if (txtScannedQr != null) |
|||
{ |
|||
txtScannedQr.Text = "스캔 대기 중..."; |
|||
txtScannedQr.Foreground = new SolidColorBrush(Color.FromRgb(148, 163, 184)); // Gray
|
|||
} |
|||
|
|||
if (txtScanStatusSubText != null) |
|||
{ |
|||
txtScanStatusSubText.Text = "(스캔 대기 중)"; |
|||
txtScanStatusSubText.Foreground = new SolidColorBrush(Color.FromRgb(148, 163, 184)); |
|||
} |
|||
|
|||
if (borderExpectedCard != null) |
|||
{ |
|||
borderExpectedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
|
|||
borderExpectedCard.BorderThickness = new Thickness(1.5); |
|||
} |
|||
|
|||
if (borderScannedCard != null) |
|||
{ |
|||
borderScannedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(203, 213, 225)); // Gray
|
|||
borderScannedCard.BorderThickness = new Thickness(1.5); |
|||
} |
|||
|
|||
if (borderStatusBanner != null) |
|||
{ |
|||
borderStatusBanner.Background = new SolidColorBrush(Color.FromRgb(241, 245, 249)); // Light Gray
|
|||
} |
|||
|
|||
if (txtStatusMessage != null) |
|||
{ |
|||
txtStatusMessage.Text = "스캔 대기 중"; |
|||
txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(51, 65, 85)); |
|||
} |
|||
} |
|||
|
|||
private async void Window_PreviewKeyDown(object sender, KeyEventArgs e) |
|||
{ |
|||
// ESC 키로 창 닫기
|
|||
if (e.Key == Key.Escape) |
|||
{ |
|||
btnCancel_Click(sender, e); |
|||
return; |
|||
} |
|||
|
|||
// Enter 키 수신 시 입력된 스캔 버퍼 전송
|
|||
if (e.Key == Key.Enter || e.Key == Key.Return) |
|||
{ |
|||
string scannedText = _keyScanBuffer.ToString().Trim(); |
|||
_keyScanBuffer.Clear(); |
|||
|
|||
if (!string.IsNullOrEmpty(scannedText)) |
|||
{ |
|||
await ProcessScanResultAsync(scannedText); |
|||
} |
|||
return; |
|||
} |
|||
|
|||
// 일반 문자 키 수집 (스캐너 가상 키보드 수신)
|
|||
char keyChar = KeyToChar(e.Key); |
|||
if (keyChar != '\0') |
|||
{ |
|||
_keyScanBuffer.Append(keyChar); |
|||
} |
|||
} |
|||
|
|||
private char KeyToChar(Key key) |
|||
{ |
|||
if (key >= Key.D0 && key <= Key.D9) return (char)('0' + (key - Key.D0)); |
|||
if (key >= Key.NumPad0 && key <= Key.NumPad9) return (char)('0' + (key - Key.NumPad0)); |
|||
if (key >= Key.A && key <= Key.Z) return (char)('A' + (key - Key.A)); |
|||
if (key == Key.OemSemicolon) return ';'; |
|||
if (key == Key.OemMinus) return '-'; |
|||
return '\0'; |
|||
} |
|||
|
|||
private async Task ProcessScanResultAsync(string scannedText) |
|||
{ |
|||
if (_isClosing) return; |
|||
|
|||
_hasScanned = true; |
|||
ScannedQr = scannedText ?? string.Empty; |
|||
|
|||
if (txtScannedQr != null) |
|||
{ |
|||
txtScannedQr.Text = scannedText; |
|||
} |
|||
|
|||
// nnn;nnnn;nnnn;nnnn; (세미콜론 구분 포맷 정밀 비교)
|
|||
string normExpected = NormalizeQrFormat(_expectedQr); |
|||
string normScanned = NormalizeQrFormat(scannedText); |
|||
|
|||
bool isMatch = string.Equals(normScanned, normExpected, StringComparison.OrdinalIgnoreCase); |
|||
|
|||
if (isMatch) |
|||
{ |
|||
_isPassed = true; |
|||
|
|||
if (txtScannedQr != null) |
|||
{ |
|||
txtScannedQr.Foreground = new SolidColorBrush(Color.FromRgb(22, 163, 74)); // Green
|
|||
} |
|||
|
|||
if (txtScanStatusSubText != null) |
|||
{ |
|||
txtScanStatusSubText.Text = "(스캔 완료 - 일치)"; |
|||
txtScanStatusSubText.Foreground = new SolidColorBrush(Color.FromRgb(22, 163, 74)); |
|||
} |
|||
|
|||
if (borderExpectedCard != null) |
|||
{ |
|||
borderExpectedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(34, 197, 94)); // Green
|
|||
} |
|||
|
|||
if (borderScannedCard != null) |
|||
{ |
|||
borderScannedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(34, 197, 94)); // Green
|
|||
borderScannedCard.BorderThickness = new Thickness(2); |
|||
} |
|||
|
|||
if (borderStatusBanner != null) |
|||
{ |
|||
borderStatusBanner.Background = new SolidColorBrush(Color.FromRgb(240, 253, 244)); // Light Green
|
|||
} |
|||
|
|||
if (txtStatusMessage != null) |
|||
{ |
|||
txtStatusMessage.Text = "QR 코드 검증 성공"; |
|||
txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(22, 163, 74)); // Dark Green
|
|||
} |
|||
|
|||
// 스캔값이 합격이면 딜레이 없이 즉시 팝업 닫기
|
|||
if (!_isClosing) |
|||
{ |
|||
_isClosing = true; |
|||
DialogResult = true; |
|||
Close(); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
_isPassed = false; |
|||
|
|||
if (txtScannedQr != null) |
|||
{ |
|||
txtScannedQr.Foreground = new SolidColorBrush(Color.FromRgb(220, 38, 38)); // Red
|
|||
} |
|||
|
|||
if (txtScanStatusSubText != null) |
|||
{ |
|||
txtScanStatusSubText.Text = "(스캔 완료 - 불일치)"; |
|||
txtScanStatusSubText.Foreground = new SolidColorBrush(Color.FromRgb(220, 38, 38)); |
|||
} |
|||
|
|||
if (borderScannedCard != null) |
|||
{ |
|||
borderScannedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
|
|||
borderScannedCard.BorderThickness = new Thickness(2); |
|||
} |
|||
|
|||
if (borderStatusBanner != null) |
|||
{ |
|||
borderStatusBanner.Background = new SolidColorBrush(Color.FromRgb(254, 242, 242)); // Light Red
|
|||
} |
|||
|
|||
if (txtStatusMessage != null) |
|||
{ |
|||
txtStatusMessage.Text = "QR 바코드 불일치"; |
|||
txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(220, 38, 38)); // Red
|
|||
} |
|||
} |
|||
} |
|||
|
|||
private static string NormalizeQrFormat(string input) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(input)) return string.Empty; |
|||
|
|||
string trimmed = input.Trim(); |
|||
if (!trimmed.EndsWith(";")) |
|||
{ |
|||
trimmed += ";"; |
|||
} |
|||
return trimmed; |
|||
} |
|||
|
|||
private void btnCancel_Click(object sender, RoutedEventArgs e) |
|||
{ |
|||
if (_isClosing) return; |
|||
_isClosing = true; |
|||
DialogResult = false; |
|||
Close(); |
|||
} |
|||
|
|||
private void btnConfirm_Click(object sender, RoutedEventArgs e) |
|||
{ |
|||
if (_isClosing) return; |
|||
|
|||
if (!_hasScanned) |
|||
{ |
|||
MessageBox.Show("QR 코드를 스캔한 후 확인 버튼을 클릭하십시오.", "스캔 대기", MessageBoxButton.OK, MessageBoxImage.Information); |
|||
return; |
|||
} |
|||
|
|||
_isClosing = true; |
|||
DialogResult = _isPassed; |
|||
Close(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,163 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
|
|||
namespace marking_gui.Services |
|||
{ |
|||
public sealed class BarcodeScannerSettings |
|||
{ |
|||
public bool Enabled { get; set; } = true; |
|||
public string PortName { get; set; } = "Auto"; |
|||
public int BaudRate { get; set; } = 9600; |
|||
public int ReadTimeout { get; set; } = 500; |
|||
public int IdleCommitMilliseconds { get; set; } = 250; |
|||
public bool DtrEnable { get; set; } = true; |
|||
public bool RtsEnable { get; set; } = true; |
|||
|
|||
public static bool IsAutoPort(string? portName) |
|||
{ |
|||
return string.IsNullOrWhiteSpace(portName) || |
|||
string.Equals(portName.Trim(), "Auto", StringComparison.OrdinalIgnoreCase); |
|||
} |
|||
|
|||
public static BarcodeScannerSettings Load(string iniPath) |
|||
{ |
|||
var settings = new BarcodeScannerSettings(); |
|||
|
|||
if (!File.Exists(iniPath)) |
|||
{ |
|||
return settings; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
var lines = File.ReadAllLines(iniPath); |
|||
bool isScannerSection = false; |
|||
|
|||
foreach (var line in lines) |
|||
{ |
|||
var trimmed = line.Trim(); |
|||
if (string.IsNullOrWhiteSpace(trimmed) || trimmed.StartsWith(";") || trimmed.StartsWith("#")) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (trimmed.StartsWith("[") && trimmed.EndsWith("]")) |
|||
{ |
|||
string sectionName = trimmed.Substring(1, trimmed.Length - 2).Trim(); |
|||
isScannerSection = string.Equals(sectionName, "BarcodeScanner", StringComparison.OrdinalIgnoreCase); |
|||
continue; |
|||
} |
|||
|
|||
if (!isScannerSection) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var parts = trimmed.Split(new[] { '=' }, 2); |
|||
if (parts.Length != 2) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
string key = parts[0].Trim(); |
|||
string val = parts[1].Trim(); |
|||
|
|||
if (string.Equals(key, "Enabled", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
if (bool.TryParse(val, out bool enabled)) settings.Enabled = enabled; |
|||
} |
|||
else if (string.Equals(key, "PortName", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
settings.PortName = val; |
|||
} |
|||
else if (string.Equals(key, "BaudRate", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
if (int.TryParse(val, out int baudRate)) settings.BaudRate = baudRate; |
|||
} |
|||
else if (string.Equals(key, "ReadTimeout", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
if (int.TryParse(val, out int timeout)) settings.ReadTimeout = timeout; |
|||
} |
|||
else if (string.Equals(key, "IdleCommitMilliseconds", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
if (int.TryParse(val, out int idleMs)) settings.IdleCommitMilliseconds = idleMs; |
|||
} |
|||
else if (string.Equals(key, "DtrEnable", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
if (bool.TryParse(val, out bool dtr)) settings.DtrEnable = dtr; |
|||
} |
|||
else if (string.Equals(key, "RtsEnable", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
if (bool.TryParse(val, out bool rts)) settings.RtsEnable = rts; |
|||
} |
|||
} |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
System.Diagnostics.Debug.WriteLine("[BarcodeScannerSettings Load Error] " + ex.Message); |
|||
} |
|||
|
|||
return settings; |
|||
} |
|||
|
|||
public void Save(string iniPath) |
|||
{ |
|||
try |
|||
{ |
|||
var lines = File.Exists(iniPath) ? new List<string>(File.ReadAllLines(iniPath)) : new List<string>(); |
|||
int sectionIndex = -1; |
|||
int nextSectionIndex = lines.Count; |
|||
|
|||
for (int i = 0; i < lines.Count; i++) |
|||
{ |
|||
string trimmed = lines[i].Trim(); |
|||
if (trimmed.StartsWith("[") && trimmed.EndsWith("]")) |
|||
{ |
|||
string sec = trimmed.Substring(1, trimmed.Length - 2).Trim(); |
|||
if (string.Equals(sec, "BarcodeScanner", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
sectionIndex = i; |
|||
} |
|||
else if (sectionIndex >= 0 && nextSectionIndex == lines.Count) |
|||
{ |
|||
nextSectionIndex = i; |
|||
} |
|||
} |
|||
} |
|||
|
|||
var newSectionLines = new List<string> |
|||
{ |
|||
"[BarcodeScanner]", |
|||
$"Enabled={Enabled.ToString().ToLower()}", |
|||
$"PortName={PortName}", |
|||
$"BaudRate={BaudRate}", |
|||
$"ReadTimeout={ReadTimeout}", |
|||
$"IdleCommitMilliseconds={IdleCommitMilliseconds}", |
|||
$"DtrEnable={DtrEnable.ToString().ToLower()}", |
|||
$"RtsEnable={RtsEnable.ToString().ToLower()}" |
|||
}; |
|||
|
|||
if (sectionIndex >= 0) |
|||
{ |
|||
lines.RemoveRange(sectionIndex, nextSectionIndex - sectionIndex); |
|||
lines.InsertRange(sectionIndex, newSectionLines); |
|||
} |
|||
else |
|||
{ |
|||
if (lines.Count > 0 && !string.IsNullOrWhiteSpace(lines[lines.Count - 1])) |
|||
{ |
|||
lines.Add(""); |
|||
} |
|||
lines.AddRange(newSectionLines); |
|||
} |
|||
|
|||
File.WriteAllLines(iniPath, lines); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
System.Diagnostics.Debug.WriteLine("[BarcodeScannerSettings Save Error] " + ex.Message); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,263 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO.Ports; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace marking_gui.Services |
|||
{ |
|||
public class BarcodeScanReceivedEventArgs : EventArgs |
|||
{ |
|||
public string PortName { get; } |
|||
public string Text { get; } |
|||
public bool IsConfirmedPort { get; } |
|||
|
|||
public BarcodeScanReceivedEventArgs(string portName, string text, bool isConfirmedPort) |
|||
{ |
|||
PortName = portName; |
|||
Text = text; |
|||
IsConfirmedPort = isConfirmedPort; |
|||
} |
|||
} |
|||
|
|||
public sealed class SerialBarcodeScanner : IDisposable |
|||
{ |
|||
private sealed class PortSession : IDisposable |
|||
{ |
|||
public SerialPort Port { get; } |
|||
public StringBuilder Buffer { get; } = new StringBuilder(); |
|||
public Timer? Timer { get; set; } |
|||
private readonly Action<PortSession> _onIdleCommit; |
|||
|
|||
public PortSession(SerialPort port, Action<PortSession> onIdleCommit) |
|||
{ |
|||
Port = port; |
|||
_onIdleCommit = onIdleCommit; |
|||
} |
|||
|
|||
public void ResetIdleTimer(int milliseconds) |
|||
{ |
|||
if (Timer == null) |
|||
{ |
|||
Timer = new Timer(_ => _onIdleCommit(this), null, milliseconds, Timeout.Infinite); |
|||
} |
|||
else |
|||
{ |
|||
Timer.Change(milliseconds, Timeout.Infinite); |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
Timer?.Dispose(); |
|||
try |
|||
{ |
|||
if (Port.IsOpen) |
|||
{ |
|||
Port.Close(); |
|||
} |
|||
Port.Dispose(); |
|||
} |
|||
catch { } |
|||
} |
|||
} |
|||
|
|||
private readonly BarcodeScannerSettings _settings; |
|||
private readonly HashSet<string> _excludedPortNames; |
|||
private readonly List<PortSession> _sessions = new List<PortSession>(); |
|||
private string? _confirmedPortName; |
|||
private bool _disposed; |
|||
|
|||
public event EventHandler<BarcodeScanReceivedEventArgs>? ScanReceived; |
|||
|
|||
public bool IsRunning => _sessions.Count > 0; |
|||
public string? ConfirmedPortName => _confirmedPortName; |
|||
|
|||
public SerialBarcodeScanner(BarcodeScannerSettings settings, IEnumerable<string>? excludedPortNames = null) |
|||
{ |
|||
_settings = settings ?? new BarcodeScannerSettings(); |
|||
_excludedPortNames = new HashSet<string>(excludedPortNames ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase); |
|||
} |
|||
|
|||
public void Start() |
|||
{ |
|||
if (!_settings.Enabled) return; |
|||
|
|||
Stop(); |
|||
|
|||
var candidatePorts = GetCandidatePortNames(); |
|||
if (candidatePorts.Length == 0) |
|||
{ |
|||
System.Diagnostics.Debug.WriteLine("[SerialBarcodeScanner] 사용 가능한 COM 포트가 없습니다."); |
|||
return; |
|||
} |
|||
|
|||
foreach (var portName in candidatePorts) |
|||
{ |
|||
try |
|||
{ |
|||
var port = CreateSerialPort(portName); |
|||
var session = new PortSession(port, CommitBufferedScan); |
|||
|
|||
port.DataReceived += SerialPort_DataReceived; |
|||
_sessions.Add(session); |
|||
|
|||
port.Open(); |
|||
port.DiscardInBuffer(); |
|||
port.DiscardOutBuffer(); |
|||
|
|||
System.Diagnostics.Debug.WriteLine($"[SerialBarcodeScanner] 시리얼 스캐너 포트 오픈 성공: {portName}"); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
System.Diagnostics.Debug.WriteLine($"[SerialBarcodeScanner] 포트 '{portName}' 오픈 실패: {ex.Message}"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void Stop() |
|||
{ |
|||
foreach (var session in _sessions) |
|||
{ |
|||
try |
|||
{ |
|||
session.Port.DataReceived -= SerialPort_DataReceived; |
|||
session.Dispose(); |
|||
} |
|||
catch { } |
|||
} |
|||
_sessions.Clear(); |
|||
_confirmedPortName = null; |
|||
} |
|||
|
|||
public void ConfirmPort(string portName) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(portName)) return; |
|||
|
|||
string normalized = NormalizePortName(portName); |
|||
if (string.Equals(_confirmedPortName, normalized, StringComparison.OrdinalIgnoreCase)) return; |
|||
|
|||
_confirmedPortName = normalized; |
|||
|
|||
// 포트가 확정되면 다른 미사용 테스트 포트 세션 해제
|
|||
for (int i = _sessions.Count - 1; i >= 0; i--) |
|||
{ |
|||
var session = _sessions[i]; |
|||
if (!string.Equals(session.Port.PortName, normalized, StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
try |
|||
{ |
|||
session.Port.DataReceived -= SerialPort_DataReceived; |
|||
session.Dispose(); |
|||
} |
|||
catch { } |
|||
_sessions.RemoveAt(i); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private SerialPort CreateSerialPort(string portName) |
|||
{ |
|||
return new SerialPort(portName, _settings.BaudRate) |
|||
{ |
|||
Encoding = Encoding.ASCII, |
|||
NewLine = "\r\n", |
|||
ReadTimeout = _settings.ReadTimeout, |
|||
WriteTimeout = _settings.ReadTimeout, |
|||
DtrEnable = _settings.DtrEnable, |
|||
RtsEnable = _settings.RtsEnable |
|||
}; |
|||
} |
|||
|
|||
private string[] GetCandidatePortNames() |
|||
{ |
|||
if (!BarcodeScannerSettings.IsAutoPort(_settings.PortName)) |
|||
{ |
|||
return new[] { NormalizePortName(_settings.PortName) }; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
return SerialPort.GetPortNames() |
|||
.Select(NormalizePortName) |
|||
.Where(p => !_excludedPortNames.Contains(p)) |
|||
.OrderBy(GetPortNumber) |
|||
.ToArray(); |
|||
} |
|||
catch |
|||
{ |
|||
return Array.Empty<string>(); |
|||
} |
|||
} |
|||
|
|||
private static string NormalizePortName(string portName) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(portName)) return string.Empty; |
|||
return portName.Trim().ToUpper(); |
|||
} |
|||
|
|||
private static int GetPortNumber(string portName) |
|||
{ |
|||
if (portName.StartsWith("COM", StringComparison.OrdinalIgnoreCase) && |
|||
int.TryParse(portName.Substring(3), out int num)) |
|||
{ |
|||
return num; |
|||
} |
|||
return int.MaxValue; |
|||
} |
|||
|
|||
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) |
|||
{ |
|||
if (sender is not SerialPort port) return; |
|||
|
|||
var session = _sessions.FirstOrDefault(s => ReferenceEquals(s.Port, port)); |
|||
if (session == null) return; |
|||
|
|||
try |
|||
{ |
|||
string text = port.ReadExisting(); |
|||
if (string.IsNullOrEmpty(text)) return; |
|||
|
|||
lock (session.Buffer) |
|||
{ |
|||
session.Buffer.Append(text); |
|||
} |
|||
|
|||
session.ResetIdleTimer(_settings.IdleCommitMilliseconds); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
System.Diagnostics.Debug.WriteLine($"[SerialPort_DataReceived Error] {port.PortName}: {ex.Message}"); |
|||
} |
|||
} |
|||
|
|||
private void CommitBufferedScan(PortSession session) |
|||
{ |
|||
string scanText; |
|||
lock (session.Buffer) |
|||
{ |
|||
scanText = session.Buffer.ToString(); |
|||
session.Buffer.Clear(); |
|||
} |
|||
|
|||
if (string.IsNullOrWhiteSpace(scanText)) return; |
|||
|
|||
scanText = scanText.Trim(); |
|||
|
|||
bool isConfirmed = string.Equals(_confirmedPortName, session.Port.PortName, StringComparison.OrdinalIgnoreCase); |
|||
|
|||
ScanReceived?.Invoke(this, new BarcodeScanReceivedEventArgs(session.Port.PortName, scanText, isConfirmed)); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (!_disposed) |
|||
{ |
|||
Stop(); |
|||
_disposed = true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue