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); 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); } private void BarcodeTextBox_TextChanged(object sender, TextChangedEventArgs e) { if (_suppressBarcodeStatusUpdate) { return; } if (string.IsNullOrWhiteSpace(BarcodeTextBox.Text)) { IcSnOutputTextBox.Clear(); SetProcessStatus("WAITING BARCODE", "HmiAccentBrush"); ShowResultStatusMessage(BarcodeRequiredMessage); return; } SetProcessStatus("BARCODE SCANNED", "HmiRunBrush"); ShowResultStatusMessage(BoardReadyMessage); } 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 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*(?[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 SaveMeasurementStatusAsync(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) { SetProcessStatus("ERROR", "HmiDangerBrush"); ShowResultStatusMessage($"보드 테스트 실패: {ex.Message}"); } finally { _isBoardTestRunning = false; SetInputEnabled(true); FocusBarcodeInput(); } } private async Task 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() { try { var equipmentSettings = EquipmentMeasurementSettings.Load(GetHardwareIniPath()); var equipmentService = new EquipmentMeasurementService(equipmentSettings); await equipmentService.RunLogoutCommandAsync(); } catch (Exception ex) { MessageBox.Show(this, $"파워서플라이 logout 전원 OFF 실패: {ex.Message}", "Logout", MessageBoxButton.OK, MessageBoxImage.Warning); } } 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; } private void FocusBarcodeInput() { Dispatcher.BeginInvoke(new Action(() => { BarcodeTextBox.Focus(); Keyboard.Focus(BarcodeTextBox); BarcodeTextBox.SelectAll(); }), DispatcherPriority.Input); } 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(); 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 SaveMeasurementStatusAsync(string resultCode) { try { var repository = new HousingAssemblyRepository(DatabaseSettings.Load(GetDatabaseIniPath())); var record = CreateMeasurementRecord(resultCode); await repository.UpsertInspectionAsync(record); return null; } catch (Exception ex) { return ex.Message; } } 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 CreateMeasurementRecord(string resultCode) { var barcode = BarcodeTextBox.Text.Trim(); var icSn = IcSnOutputTextBox.Text.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 = _lastMeasuredVoltage ?? ParseDecimal(VOutputTextBox.Text), PtCurrent1 = ParseDecimal(AOutputTextBox.Text), 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.000", 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 GetReservedBarcodeScannerPortNames(string hardwareIniPath) { var reservedPortNames = new HashSet(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 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 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; } }