You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1068 lines
34 KiB

using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using Housing.Login;
using Housing.Models;
using Housing.Services;
namespace Housing;
public partial class MainWindow : Window
{
private const string BarcodeRequiredMessage = "스캐너로 PCB_Barcode를 스캔해주세요.";
private const string BoardReadyMessage = "스캔 완료. 시료 투입 후 Housing 장비 시작 버튼을 눌러주세요.";
private const string BarcodeInvalidMessage = "스캔 형식이 올바르지 않습니다. PCBA S/N 바코드를 스캔해주세요.";
private const string DefaultOutputValue = "0.00";
private const int ScannerInputMinimumLength = 4;
private const double ResultStatusFontSize = 22;
private const double InspectionResultFontSize = 96;
private const decimal MilliampsPerAmp = 1000m;
private static readonly TimeSpan ScannerInputMaxGap = TimeSpan.FromMilliseconds(80);
private static readonly TimeSpan ScannerInputIdleCommitDelay = TimeSpan.FromMilliseconds(250);
private readonly StringBuilder _scannerInputBuffer = new();
private readonly DispatcherTimer _scannerInputCommitTimer;
private SerialBarcodeScanner? _barcodeScanner;
private decimal? _lastMeasuredVoltage;
private DateTime _lastScannerInputAt = DateTime.MinValue;
private StartupLoginWindowResult _loginResult = new();
private InspectionJudgementSettings _inspectionSettings;
private bool _isStartupLoginShown;
private bool _isBoardTestRunning;
private bool _suppressBarcodeStatusUpdate;
private enum ProcessStep
{
Barcode,
Start,
Board,
IcSn,
Measure,
Db,
Result
}
public MainWindow()
{
_inspectionSettings = InspectionSettingsStore.Load(GetInspectionSettingsIniPath());
InitializeComponent();
_scannerInputCommitTimer = new DispatcherTimer { Interval = ScannerInputIdleCommitDelay };
_scannerInputCommitTimer.Tick += ScannerInputCommitTimer_Tick;
CommandManager.AddPreviewExecutedHandler(BarcodeTextBox, BarcodeTextBox_PreviewExecuted);
AttachJudgementCriteriaEnterFocus();
ApplyInspectionSettingsToTextBoxes();
ApplyLoginResult();
ResetScreen();
}
protected override void OnContentRendered(EventArgs e)
{
base.OnContentRendered(e);
if (_isStartupLoginShown)
{
return;
}
_isStartupLoginShown = true;
if (!ShowLoginPopup())
{
Close();
}
}
protected override void OnClosed(EventArgs e)
{
_barcodeScanner?.Dispose();
_scannerInputCommitTimer.Stop();
CommandManager.RemovePreviewExecutedHandler(BarcodeTextBox, BarcodeTextBox_PreviewExecuted);
base.OnClosed(e);
}
protected override void OnActivated(EventArgs e)
{
base.OnActivated(e);
KeepBarcodeFocusIfEmpty();
}
private void BarcodeTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (_suppressBarcodeStatusUpdate)
{
return;
}
if (string.IsNullOrWhiteSpace(BarcodeTextBox.Text))
{
IcSnOutputTextBox.Clear();
SetProcessStatus("WAITING BARCODE", "HmiAccentBrush");
ShowResultStatusMessage(BarcodeRequiredMessage);
KeepBarcodeFocusIfEmpty();
return;
}
SetProcessStatus("BARCODE SCANNED", "HmiRunBrush");
ShowResultStatusMessage(BoardReadyMessage);
}
private void BarcodeTextBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
if (IsBarcodeFocusExceptionTarget(e.NewFocus))
{
return;
}
KeepBarcodeFocusIfEmpty();
}
private void BarcodeTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
e.Handled = true;
AppendScannerText(e.Text);
}
private async void BarcodeTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter || e.Key == Key.Return || e.Key == Key.Tab)
{
e.Handled = true;
await CompleteScannerInputAsync(runBoardSequence: true);
return;
}
if (e.Key == Key.Back || e.Key == Key.Delete || e.Key == Key.Escape ||
Keyboard.Modifiers.HasFlag(ModifierKeys.Control) ||
Keyboard.Modifiers.HasFlag(ModifierKeys.Alt))
{
e.Handled = true;
ClearPendingScannerInput();
return;
}
}
private void BarcodeTextBox_PreviewExecuted(object sender, ExecutedRoutedEventArgs e)
{
e.Handled = true;
}
private void ScannerInputCommitTimer_Tick(object? sender, EventArgs e)
{
_scannerInputCommitTimer.Stop();
_ = CompleteScannerInputAsync(runBoardSequence: true);
}
private void AppendScannerText(string text)
{
var now = DateTime.UtcNow;
if (_scannerInputBuffer.Length > 0 && now - _lastScannerInputAt > ScannerInputMaxGap)
{
_scannerInputBuffer.Clear();
}
_lastScannerInputAt = now;
_scannerInputBuffer.Append(text);
_scannerInputCommitTimer.Stop();
_scannerInputCommitTimer.Start();
}
private async Task CompleteScannerInputAsync(bool runBoardSequence)
{
_scannerInputCommitTimer.Stop();
var scannerText = _scannerInputBuffer.ToString().Trim();
_scannerInputBuffer.Clear();
await ProcessScannerTextAsync(scannerText, runBoardSequence, showInvalidInput: true);
}
private async Task<bool> ProcessScannerTextAsync(string scannerText, bool runBoardSequence, bool showInvalidInput)
{
scannerText = scannerText.Trim();
var hasPendingScannerInput = scannerText.Length > 0;
var acceptedScannerInput = TryExtractPcbBarcode(scannerText, out var scannedBarcode);
if (acceptedScannerInput)
{
BarcodeTextBox.Text = scannedBarcode;
BarcodeTextBox.CaretIndex = BarcodeTextBox.Text.Length;
}
else if (showInvalidInput && hasPendingScannerInput && scannerText.Length >= ScannerInputMinimumLength)
{
BarcodeTextBox.Clear();
SetProcessStatus("SCAN ERROR", "HmiDangerBrush");
ShowResultStatusMessage(BarcodeInvalidMessage);
FocusBarcodeInput();
}
if (runBoardSequence)
{
if (!hasPendingScannerInput || acceptedScannerInput)
{
await RunBoardSequenceAsync();
}
return acceptedScannerInput;
}
FocusBarcodeInput();
return acceptedScannerInput;
}
private void StartBarcodeScanner()
{
if (_barcodeScanner is not null)
{
return;
}
SerialBarcodeScanner? barcodeScanner = null;
try
{
var hardwareIniPath = GetHardwareIniPath();
var settings = BarcodeScannerSettings.Load(hardwareIniPath);
if (!settings.Enabled)
{
return;
}
barcodeScanner = new SerialBarcodeScanner(settings, GetReservedBarcodeScannerPortNames(hardwareIniPath));
barcodeScanner.ScanReceived += BarcodeScanner_ScanReceived;
barcodeScanner.Start();
_barcodeScanner = barcodeScanner;
}
catch (Exception ex)
{
barcodeScanner?.Dispose();
_barcodeScanner?.Dispose();
_barcodeScanner = null;
SetProcessStatus("SCANNER ERROR", "HmiDangerBrush");
ShowResultStatusMessage($"{BarcodeRequiredMessage}\r\nBarcode scanner COM: {ex.Message}");
}
}
private void BarcodeScanner_ScanReceived(object? sender, BarcodeScanReceivedEventArgs e)
{
Dispatcher.BeginInvoke(new Action(async () =>
{
try
{
var hasBarcodeFormat = TryExtractPcbBarcode(e.Text, out _);
if (hasBarcodeFormat)
{
_barcodeScanner?.ConfirmPort(e.PortName);
}
await ProcessScannerTextAsync(e.Text, runBoardSequence: true, showInvalidInput: e.IsConfirmedPort);
}
catch (Exception ex)
{
SetProcessStatus("SCAN ERROR", "HmiDangerBrush");
ShowResultStatusMessage($"Barcode scanner input failed: {ex.Message}");
}
}), DispatcherPriority.Input);
}
private static bool TryExtractPcbBarcode(string scannerText, out string pcbBarcode)
{
pcbBarcode = string.Empty;
if (string.IsNullOrWhiteSpace(scannerText))
{
return false;
}
var pcbaSerialMatch = Regex.Match(
scannerText,
@"PCBA\s*S\/N\s*:\s*(?<serial>[A-Za-z0-9-]{4,50})",
RegexOptions.IgnoreCase);
if (pcbaSerialMatch.Success)
{
pcbBarcode = pcbaSerialMatch.Groups["serial"].Value.Trim();
return true;
}
var plainSerial = scannerText.Trim();
if (IsPlainPcbBarcode(plainSerial))
{
pcbBarcode = plainSerial;
return true;
}
var fields = scannerText
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(part => part.Split(':', 2, StringSplitOptions.TrimEntries))
.Where(parts => parts.Length == 2)
.ToDictionary(parts => parts[0], parts => parts[1], StringComparer.OrdinalIgnoreCase);
if (!fields.ContainsKey("MODEL") ||
!fields.TryGetValue("SERIAL", out var serial) ||
!fields.ContainsKey("OPTION") ||
string.IsNullOrWhiteSpace(serial))
{
return false;
}
pcbBarcode = serial.Trim();
return pcbBarcode.Length <= 50;
}
private static bool IsPlainPcbBarcode(string value)
{
return Regex.IsMatch(value, @"^[A-Za-z0-9][A-Za-z0-9-]{3,49}$") &&
Regex.IsMatch(value, @"\d");
}
private void ClearPendingScannerInput()
{
_scannerInputCommitTimer.Stop();
_scannerInputBuffer.Clear();
}
public async Task OnEquipmentStartButtonPressedAsync()
{
await RunBoardSequenceAsync();
}
private async Task RunBoardSequenceAsync()
{
if (_isBoardTestRunning)
{
return;
}
if (string.IsNullOrWhiteSpace(BarcodeTextBox.Text))
{
SetProcessStatus("WAITING BARCODE", "HmiAccentBrush");
ShowResultStatusMessage(BarcodeRequiredMessage);
FocusBarcodeInput();
return;
}
if (!TryApplyInspectionSettingsFromTextBoxes())
{
SetProcessStatus("RANGE SETTING ERROR", "HmiDangerBrush");
return;
}
try
{
_isBoardTestRunning = true;
SetInputEnabled(false);
SetProcessStatus("INSPECTION START", "HmiAccentBrush");
var boardSettings = BoardHardwareSettings.Load(GetHardwareIniPath());
var equipmentSettings = EquipmentMeasurementSettings.Load(GetHardwareIniPath());
var startSignalSettings = StartSignalSettings.Load(GetHardwareIniPath());
var startSignalReceivedAt = await WaitForEquipmentStartSignalAsync(startSignalSettings);
var measurementNotBeforeUtc = startSignalReceivedAt?.AddMilliseconds(startSignalSettings.PostSignalDelayMilliseconds);
ShowResultStatusMessage(
"보드 테스트 시작\r\n" +
"1. 시료 연결 확인\r\n" +
"2. IC_SN 읽기\r\n" +
"3. CAL DEFAULT 조건 설정\r\n" +
"4. 시작 신호 기준 지연 후 계측 장비에서 V/A 읽기");
var service = new BoardTestService(boardSettings, equipmentSettings);
SetProcessStatus("BOARD TEST", "HmiWarningBrush");
var measurement = await service.RunAsync(
icSn =>
{
IcSnOutputTextBox.Text = icSn;
SetProcessStatus("IC_SN READ", "HmiAccentBrush");
return Task.CompletedTask;
},
measurementNotBeforeUtc);
SetProcessStatus("MEASUREMENT DONE", "HmiAccentBrush");
IcSnOutputTextBox.Text = measurement.IcSn;
_lastMeasuredVoltage = measurement.Voltage;
VOutputTextBox.Text = FormatDecimal(measurement.Voltage);
AOutputTextBox.Text = FormatCurrent(measurement.Current);
var voltagePass = IsVoltagePass(measurement.Voltage);
var currentPass = IsCurrentPass(measurement.Current);
var inspectionPass = voltagePass && currentPass;
ApplyMeasurementOutputState(VOutputTextBox, voltagePass);
ApplyMeasurementOutputState(AOutputTextBox, currentPass);
ShowInspectionResult(inspectionPass);
var resultCode = inspectionPass ? "OK" : "NG";
SetProcessStatus("DB SAVING", "HmiWarningBrush");
var dbMeasurementSaveError = await SaveInspectionResultAsync(measurement, resultCode);
if (!string.IsNullOrWhiteSpace(dbMeasurementSaveError))
{
SetProcessStatus("DB SAVE ERROR", "HmiDangerBrush");
MessageBox.Show(this, $"DB 저장 실패: {dbMeasurementSaveError}", "DB", MessageBoxButton.OK, MessageBoxImage.Warning);
}
else
{
SetProcessStatus(inspectionPass ? "COMPLETE OK" : "COMPLETE NG", inspectionPass ? "HmiRunBrush" : "HmiDangerBrush");
}
if (!inspectionPass)
{
ShowInspectionFailurePopup(measurement.Voltage, measurement.Current, voltagePass, currentPass);
}
ClearTraceabilityForNextScan();
}
catch (Exception ex)
{
var shutdownError = await TryRunEquipmentLogoutCommandAsync();
SetProcessStatus("ERROR", "HmiDangerBrush");
var errorMessage = $"보드 테스트 실패: {ex.Message}";
if (!string.IsNullOrWhiteSpace(shutdownError))
{
errorMessage += $"\r\nPower supply OFF failed: {shutdownError}";
}
ShowResultStatusMessage(errorMessage);
}
finally
{
_isBoardTestRunning = false;
SetInputEnabled(true);
FocusBarcodeInput();
}
}
private async Task<DateTime?> WaitForEquipmentStartSignalAsync(StartSignalSettings settings)
{
if (!settings.Enabled)
{
return null;
}
SetProcessStatus("WAIT START SIGNAL", "HmiWarningBrush");
ShowResultStatusMessage(
"장비 시작 신호 대기\r\n" +
$"{StartSignalWatcher.Describe(settings)}\r\n" +
"시료를 넣고 Housing 장비 시작 버튼을 눌러주세요.");
await StartSignalWatcher.WaitForStartAsync(settings);
var receivedAt = DateTime.UtcNow;
if (settings.PostSignalDelayMilliseconds <= 0)
{
SetProcessStatus("START SIGNAL", "HmiRunBrush");
ShowResultStatusMessage("장비 시작 신호 수신 완료");
return receivedAt;
}
SetProcessStatus("START DELAY", "HmiWarningBrush");
ShowResultStatusMessage(
"장비 시작 신호 수신 완료\r\n" +
$"전압 측정은 신호 수신 후 {settings.PostSignalDelayMilliseconds / 1000.0:0.###}초 이후 시작합니다.");
return receivedAt;
}
private async void LogoutButton_Click(object sender, RoutedEventArgs e)
{
LogoutButton.IsEnabled = false;
await RunEquipmentLogoutCommandAsync();
ResetScreen();
_loginResult = new StartupLoginWindowResult();
OperatorTextBlock.Text = "-";
LogoutButton.Visibility = Visibility.Collapsed;
if (!ShowLoginPopup())
{
Close();
}
}
private async Task RunEquipmentLogoutCommandAsync()
{
var error = await TryRunEquipmentLogoutCommandAsync();
if (!string.IsNullOrWhiteSpace(error))
{
MessageBox.Show(this, $"파워서플라이 logout 전원 OFF 실패: {error}", "Logout", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
private async Task<string?> TryRunEquipmentLogoutCommandAsync()
{
try
{
var equipmentSettings = EquipmentMeasurementSettings.Load(GetHardwareIniPath());
var equipmentService = new EquipmentMeasurementService(equipmentSettings);
await equipmentService.RunLogoutCommandAsync();
return null;
}
catch (Exception ex)
{
return ex.Message;
}
}
private bool ShowLoginPopup()
{
var loginWindow = new StartupLoginWindow
{
Owner = this
};
if (loginWindow.ShowDialog() != true)
{
return false;
}
_loginResult = loginWindow.Result;
ApplyLoginResult();
ResetScreen();
StartBarcodeScanner();
return true;
}
private void ApplyLoginResult()
{
OperatorTextBlock.Text = string.IsNullOrWhiteSpace(_loginResult.Operator) ? "-" : _loginResult.Operator;
var isLoggedIn = !string.IsNullOrWhiteSpace(_loginResult.Operator);
LogoutButton.Visibility = isLoggedIn
? Visibility.Visible
: Visibility.Collapsed;
LogoutButton.IsEnabled = isLoggedIn;
}
private void ResetScreen()
{
ClearTraceabilityForNextScan();
_lastMeasuredVoltage = null;
VOutputTextBox.Text = DefaultOutputValue;
AOutputTextBox.Text = FormatCurrent(0m);
ClearOutputState(VOutputTextBox);
ClearOutputState(AOutputTextBox);
SetProcessStatus("WAITING BARCODE", "HmiAccentBrush");
ShowResultStatusMessage(BarcodeRequiredMessage);
FocusBarcodeInput();
}
private void SetInputEnabled(bool isEnabled)
{
BarcodeTextBox.IsEnabled = isEnabled;
VMinTextBox.IsEnabled = isEnabled;
VMaxTextBox.IsEnabled = isEnabled;
AMinTextBox.IsEnabled = isEnabled;
AMaxTextBox.IsEnabled = isEnabled;
LogoutButton.IsEnabled = isEnabled;
if (isEnabled)
{
KeepBarcodeFocusIfEmpty();
}
}
private void AttachJudgementCriteriaEnterFocus()
{
foreach (var textBox in GetJudgementCriteriaTextBoxes())
{
textBox.PreviewKeyDown += JudgementCriteriaTextBox_PreviewKeyDown;
}
}
private void JudgementCriteriaTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter && e.Key != Key.Return)
{
return;
}
e.Handled = true;
FocusBarcodeInput(force: true);
}
private void KeepBarcodeFocusIfEmpty()
{
if (!IsActive ||
!BarcodeTextBox.IsEnabled ||
!BarcodeTextBox.IsVisible ||
IsBarcodeFocusExceptionActive() ||
!string.IsNullOrWhiteSpace(BarcodeTextBox.Text))
{
return;
}
FocusBarcodeInput();
}
private void FocusBarcodeInput(bool force = false)
{
Dispatcher.BeginInvoke(new Action(() =>
{
if (!BarcodeTextBox.IsEnabled ||
!BarcodeTextBox.IsVisible ||
(!force && IsBarcodeFocusExceptionActive()))
{
return;
}
BarcodeTextBox.Focus();
Keyboard.Focus(BarcodeTextBox);
BarcodeTextBox.SelectAll();
}), DispatcherPriority.Input);
}
private bool IsBarcodeFocusExceptionTarget(IInputElement? focusTarget)
{
return focusTarget is DependencyObject dependencyObject &&
GetBarcodeFocusExceptionControls()
.Any(control => IsVisualOrLogicalChildOf(dependencyObject, control));
}
private bool IsBarcodeFocusExceptionActive()
{
return GetBarcodeFocusExceptionControls()
.Any(control => control.IsKeyboardFocusWithin);
}
private IEnumerable<Control> GetBarcodeFocusExceptionControls()
{
yield return LogoutButton;
foreach (var textBox in GetJudgementCriteriaTextBoxes())
{
yield return textBox;
}
}
private IEnumerable<TextBox> GetJudgementCriteriaTextBoxes()
{
yield return VMinTextBox;
yield return VMaxTextBox;
yield return AMinTextBox;
yield return AMaxTextBox;
}
private static bool IsVisualOrLogicalChildOf(DependencyObject dependencyObject, DependencyObject parent)
{
for (var current = dependencyObject; current is not null;)
{
if (ReferenceEquals(current, parent))
{
return true;
}
current = VisualTreeHelper.GetParent(current) ?? LogicalTreeHelper.GetParent(current);
}
return false;
}
private void SetProcessStatus(string status, string brushResourceKey)
{
var brush = (Brush)FindResource(brushResourceKey);
CurrentProcessTextBlock.Text = status;
CurrentProcessTextBlock.Foreground = brush;
HighlightProcessStep(GetProcessStep(status), brush);
}
private ProcessStep GetProcessStep(string status)
{
return status switch
{
"WAIT START SIGNAL" or
"START SIGNAL" or
"START DELAY" or
"INSPECTION START" => ProcessStep.Start,
"BOARD TEST" or
"RANGE SETTING ERROR" => ProcessStep.Board,
"IC_SN READ" => ProcessStep.IcSn,
"MEASUREMENT DONE" => ProcessStep.Measure,
"DB SAVING" or
"DB SAVE ERROR" => ProcessStep.Db,
"COMPLETE OK" or
"COMPLETE NG" or
"ERROR" => ProcessStep.Result,
_ => ProcessStep.Barcode
};
}
private void HighlightProcessStep(ProcessStep activeStep, Brush activeBrush)
{
var inactiveBrush = (Brush)FindResource("ProcessStepInactiveBrush");
var activeBackground = (Brush)FindResource("ProcessStepActiveBackgroundBrush");
var inactiveBackground = (Brush)new BrushConverter().ConvertFromString("#0B161F")!;
var activeTextBrush = (Brush)FindResource("HmiTextBrush");
var inactiveTextBrush = (Brush)FindResource("HmiMutedTextBrush");
foreach (var (step, border) in GetProcessStepBorders())
{
var isActive = step == activeStep;
border.BorderBrush = isActive ? activeBrush : inactiveBrush;
border.BorderThickness = isActive ? new Thickness(2) : new Thickness(1);
border.Background = isActive ? activeBackground : inactiveBackground;
if (border.Child is TextBlock textBlock)
{
textBlock.Foreground = isActive ? activeTextBrush : inactiveTextBrush;
}
}
}
private IEnumerable<(ProcessStep Step, Border Border)> GetProcessStepBorders()
{
yield return (ProcessStep.Barcode, ProcessStepBarcodeBorder);
yield return (ProcessStep.Start, ProcessStepStartBorder);
yield return (ProcessStep.Board, ProcessStepBoardBorder);
yield return (ProcessStep.IcSn, ProcessStepIcSnBorder);
yield return (ProcessStep.Measure, ProcessStepMeasureBorder);
yield return (ProcessStep.Db, ProcessStepDbBorder);
yield return (ProcessStep.Result, ProcessStepResultBorder);
}
private void ShowResultStatusMessage(string message)
{
ResultOutputTextBox.FontSize = ResultStatusFontSize;
ResultOutputTextBox.FontWeight = FontWeights.SemiBold;
ResultOutputTextBox.TextWrapping = TextWrapping.Wrap;
ClearOutputState(ResultOutputTextBox);
ResultOutputTextBox.Text = message;
}
private void ShowInspectionResult(bool isPass)
{
ResultOutputTextBox.FontSize = InspectionResultFontSize;
ResultOutputTextBox.FontWeight = FontWeights.Black;
ResultOutputTextBox.TextWrapping = TextWrapping.NoWrap;
ResultOutputTextBox.Text = isPass ? "OK" : "NG";
ApplyMeasurementOutputState(ResultOutputTextBox, isPass);
}
private void ApplyMeasurementOutputState(TextBox textBox, bool isPass)
{
var brush = GetJudgementBrush(isPass);
textBox.Foreground = brush;
textBox.BorderBrush = brush;
}
private void ClearOutputState(TextBox textBox)
{
textBox.ClearValue(Control.ForegroundProperty);
textBox.ClearValue(Control.BorderBrushProperty);
}
private Brush GetJudgementBrush(bool isPass)
{
return (Brush)FindResource(isPass ? "HmiRunBrush" : "HmiDangerBrush");
}
private bool IsVoltagePass(decimal voltage)
{
return voltage >= _inspectionSettings.VMin &&
voltage <= _inspectionSettings.VMax;
}
private bool IsCurrentPass(decimal current)
{
return current >= _inspectionSettings.AMin &&
current <= _inspectionSettings.AMax;
}
private void ShowInspectionFailurePopup(
decimal voltage,
decimal current,
bool voltagePass,
bool currentPass)
{
var messages = new List<string>();
if (!voltagePass)
{
messages.Add(
$"PT_VOL_1: {FormatDecimal(voltage)} V (기준 {FormatDecimal(_inspectionSettings.VMin)} ~ {FormatDecimal(_inspectionSettings.VMax)} V)");
}
if (!currentPass)
{
messages.Add(
$"PT_CURRENT_1: {FormatCurrent(current)} mA (기준 {FormatCurrentLimit(_inspectionSettings.AMin)} ~ {FormatCurrentLimit(_inspectionSettings.AMax)} mA)");
}
var detail = messages.Count == 0
? "알 수 없는 판정 오류입니다."
: string.Join("\r\n", messages);
MessageBox.Show(
this,
$"NG 판정입니다.\r\n\r\n{detail}",
"Inspection NG",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
private void ClearTraceabilityForNextScan()
{
_suppressBarcodeStatusUpdate = true;
try
{
BarcodeTextBox.Clear();
IcSnOutputTextBox.Clear();
}
finally
{
_suppressBarcodeStatusUpdate = false;
}
ClearPendingScannerInput();
}
private async Task<string?> SaveInspectionResultAsync(BoardMeasurementResult measurement, string resultCode)
{
var databaseIniPath = GetDatabaseIniPath();
DatabaseSettings? databaseSettings = null;
try
{
databaseSettings = DatabaseSettings.Load(databaseIniPath);
var repository = new HousingAssemblyRepository(databaseSettings);
var record = CreateInspectionRecord(measurement, resultCode);
await repository.UpsertInspectionAsync(record);
return null;
}
catch (Exception ex)
{
var target = databaseSettings is null
? databaseIniPath
: $"{databaseSettings.Ip} / {databaseSettings.Database} (설정: {databaseIniPath})";
return $"{ex.Message}\r\nDB target: {target}";
}
}
private HousingAssemblyRecord CreateHeaderRecord(string icSn)
{
var barcode = BarcodeTextBox.Text.Trim();
return new HousingAssemblyRecord
{
IcSn = icSn,
PcbBarcode = barcode,
Maker = _loginResult.Maker,
Model = _loginResult.Model,
Variant1 = _loginResult.Variant1,
Variant2 = _loginResult.Variant2,
Operator = _loginResult.Operator,
ProductionDate = GetProductionDateSecond(),
Line = _loginResult.LineNo,
LotNo = _loginResult.LotNo,
JigNo = _loginResult.JigNo
};
}
private HousingAssemblyRecord CreateInspectionRecord(BoardMeasurementResult measurement, string resultCode)
{
var barcode = BarcodeTextBox.Text.Trim();
var icSn = string.IsNullOrWhiteSpace(measurement.IcSn)
? IcSnOutputTextBox.Text.Trim()
: measurement.IcSn.Trim();
if (string.IsNullOrWhiteSpace(icSn))
{
icSn = barcode;
IcSnOutputTextBox.Text = icSn;
}
return new HousingAssemblyRecord
{
IcSn = icSn,
PcbBarcode = barcode,
Maker = _loginResult.Maker,
Model = _loginResult.Model,
Variant1 = _loginResult.Variant1,
Variant2 = _loginResult.Variant2,
Operator = _loginResult.Operator,
ProductionDate = GetProductionDateSecond(),
Line = _loginResult.LineNo,
LotNo = _loginResult.LotNo,
JigNo = _loginResult.JigNo,
PtVol1 = measurement.Voltage,
PtCurrent1 = AmpsToMilliamps(measurement.Current),
Result = resultCode
};
}
private static DateTime GetProductionDateSecond()
{
var now = DateTime.Now;
return new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second);
}
private void ApplyInspectionSettingsToTextBoxes()
{
VMinTextBox.Text = FormatDecimal(_inspectionSettings.VMin);
VMaxTextBox.Text = FormatDecimal(_inspectionSettings.VMax);
AMinTextBox.Text = FormatCurrentLimit(_inspectionSettings.AMin);
AMaxTextBox.Text = FormatCurrentLimit(_inspectionSettings.AMax);
}
private bool TryApplyInspectionSettingsFromTextBoxes()
{
if (!TryReadRangeValue(VMinTextBox, "V 최소값", out var vMin) ||
!TryReadRangeValue(VMaxTextBox, "V 최대값", out var vMax) ||
!TryReadRangeValue(AMinTextBox, "mA 최소값", out var aMinMilliamp) ||
!TryReadRangeValue(AMaxTextBox, "mA 최대값", out var aMaxMilliamp))
{
return false;
}
var aMin = MilliampsToAmps(aMinMilliamp);
var aMax = MilliampsToAmps(aMaxMilliamp);
if (vMin > vMax)
{
MessageBox.Show(this, "V 최소값은 최대값보다 클 수 없습니다.", "판정 범위", MessageBoxButton.OK, MessageBoxImage.Warning);
VMinTextBox.Focus();
return false;
}
if (aMin > aMax)
{
MessageBox.Show(this, "A 최소값은 최대값보다 클 수 없습니다.", "판정 범위", MessageBoxButton.OK, MessageBoxImage.Warning);
AMinTextBox.Focus();
return false;
}
_inspectionSettings = new InspectionJudgementSettings
{
VMin = vMin,
VMax = vMax,
AMin = aMin,
AMax = aMax
};
InspectionSettingsStore.Save(GetInspectionSettingsIniPath(), _inspectionSettings);
return true;
}
private bool TryReadRangeValue(TextBox textBox, string fieldName, out decimal value)
{
if (decimal.TryParse(textBox.Text, NumberStyles.Number, CultureInfo.InvariantCulture, out value))
{
return true;
}
if (decimal.TryParse(textBox.Text, out value))
{
return true;
}
MessageBox.Show(this, $"{fieldName}은 숫자로 입력하세요.", "판정 범위", MessageBoxButton.OK, MessageBoxImage.Warning);
textBox.Focus();
return false;
}
private static decimal ParseDecimal(string value)
{
return decimal.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var result)
? result
: 0m;
}
private static string FormatDecimal(decimal value)
{
return value.ToString("0.00", CultureInfo.InvariantCulture);
}
private static string FormatCurrent(decimal value)
{
return AmpsToMilliamps(value).ToString("0.00", CultureInfo.InvariantCulture);
}
private static string FormatCurrentLimit(decimal value)
{
return AmpsToMilliamps(value).ToString("0.00", CultureInfo.InvariantCulture);
}
private static decimal AmpsToMilliamps(decimal amps)
{
return amps * MilliampsPerAmp;
}
private static decimal MilliampsToAmps(decimal milliamps)
{
return milliamps / MilliampsPerAmp;
}
private static IReadOnlyCollection<string> GetReservedBarcodeScannerPortNames(string hardwareIniPath)
{
var reservedPortNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var boardSettings = BoardHardwareSettings.Load(hardwareIniPath);
AddReservedPortName(reservedPortNames, boardSettings.PortName);
var equipmentSettings = EquipmentMeasurementSettings.Load(hardwareIniPath);
AddReservedScpiChannelPortName(reservedPortNames, equipmentSettings.Voltage);
AddReservedScpiChannelPortName(reservedPortNames, equipmentSettings.Current);
return reservedPortNames;
}
private static void AddReservedScpiChannelPortName(ISet<string> reservedPortNames, ScpiChannelSettings channel)
{
if (string.Equals(channel.Connection, "Serial", StringComparison.OrdinalIgnoreCase) ||
string.Equals(channel.Connection, "COM", StringComparison.OrdinalIgnoreCase))
{
AddReservedPortName(reservedPortNames, channel.PortName);
}
}
private static void AddReservedPortName(ISet<string> reservedPortNames, string portName)
{
if (BarcodeScannerSettings.IsAutoPort(portName))
{
return;
}
reservedPortNames.Add(portName.Trim());
}
private static string GetDatabaseIniPath()
{
return Path.Combine(AppContext.BaseDirectory, "Database.ini");
}
private static string GetHardwareIniPath()
{
return Path.Combine(AppContext.BaseDirectory, "Hardware.ini");
}
private static string GetInspectionSettingsIniPath()
{
return GetWritableSettingsPath("InspectionSettings.ini");
}
private static string GetWritableSettingsPath(string fileName)
{
var settingsDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"AMO Housing");
Directory.CreateDirectory(settingsDirectory);
var writablePath = Path.Combine(settingsDirectory, fileName);
if (!File.Exists(writablePath))
{
var bundledPath = Path.Combine(AppContext.BaseDirectory, fileName);
if (File.Exists(bundledPath))
{
File.Copy(bundledPath, writablePath);
}
}
return writablePath;
}
}