diff --git a/.vs/Housing/DesignTimeBuild/.dtbcache.v2 b/.vs/Housing/DesignTimeBuild/.dtbcache.v2 index 2e12dbe..74d5672 100644 Binary files a/.vs/Housing/DesignTimeBuild/.dtbcache.v2 and b/.vs/Housing/DesignTimeBuild/.dtbcache.v2 differ diff --git a/Database.ini b/Database.ini index 2a8763d..28e333a 100644 --- a/Database.ini +++ b/Database.ini @@ -3,9 +3,9 @@ IP=192.168.10.10,1433 Database=NE1aW_PT_Sensor DbId=su_user DbPw=amotech821 -Encrypt=True +Encrypt=False TrustServerCertificate=True -Timeout=5 +Timeout=15 [Login] Procedure=dbo.CheckOperator diff --git a/EquipmentSeparated/DMM.ini b/EquipmentSeparated/DMM.ini new file mode 100644 index 0000000..1c63e8b --- /dev/null +++ b/EquipmentSeparated/DMM.ini @@ -0,0 +1,27 @@ +[Equipment] +Timeout=5000 +SettleMilliseconds=300 + +[DMM] +Model=Keysight 34465A +Measurement=Voltage +Connection=LAN +Resource= +IdnMatch=34465A +Host=192.168.10.13 +Port=5025 +PortName= +BaudRate=115200 +Terminator=LF +SetupCommand= +CleanupCommand= +ReadCommand=MEAS:VOLT:DC? + +[OriginalHardwareIniKeys] +Connection=VoltageConnection +Resource=VoltageResource +IdnMatch=VoltageIdnMatch +Host=VoltageHost +Port=VoltagePort +SetupCommand=VoltageSetupCommand +ReadCommand=VoltageReadCommand diff --git a/EquipmentSeparated/DMM_Code.md b/EquipmentSeparated/DMM_Code.md new file mode 100644 index 0000000..41ef10d --- /dev/null +++ b/EquipmentSeparated/DMM_Code.md @@ -0,0 +1,155 @@ +# DMM Related Code + +This file is a reference-only extraction. It is not compiled by the Housing app. + +## Source Files + +- `Services/EquipmentMeasurementSettings.cs` +- `Services/EquipmentMeasurementService.cs` +- `Services/IScpiClient.cs` +- `Services/TcpScpiClient.cs` +- `Services/SerialScpiClient.cs` +- `Services/VisaScpiClient.cs` + +## Hardware.ini Keys + +```ini +[Equipment] +Timeout=5000 +SettleMilliseconds=300 + +; 34465A DMM +VoltageConnection=LAN +VoltageResource= +VoltageIdnMatch=34465A +VoltageHost=192.168.10.13 +VoltagePort=5025 +VoltageSetupCommand= +VoltageReadCommand=MEAS:VOLT:DC? +``` + +## Settings Load Code + +```csharp +Voltage = LoadChannel( + values, + "Voltage", + "MEAS:VOLT:DC?", + "34460,34461,34465,34470,3446,3447"); +``` + +```csharp +private static ScpiChannelSettings LoadChannel( + Dictionary values, + string prefix, + string defaultReadCommand, + string defaultIdnMatch) +{ + return new ScpiChannelSettings + { + Connection = GetString(values, prefix + "Connection", string.Empty), + ResourceName = GetString(values, prefix + "Resource", string.Empty), + IdnMatch = GetString(values, prefix + "IdnMatch", defaultIdnMatch), + Host = GetString(values, prefix + "Host", string.Empty), + Port = GetInt(values, prefix + "Port", 5025), + PortName = GetString(values, prefix + "PortName", string.Empty), + BaudRate = GetInt(values, prefix + "BaudRate", 115200), + Terminator = GetString(values, prefix + "Terminator", string.Empty), + SetupCommand = GetString(values, prefix + "SetupCommand", string.Empty), + CleanupCommand = GetString(values, prefix + "CleanupCommand", string.Empty), + ReadCommand = GetString(values, prefix + "ReadCommand", defaultReadCommand) + }; +} +``` + +## Voltage Read Flow + +```csharp +var voltage = await ReadChannelAsync("VOLTAGE", _settings.Voltage, log); +``` + +```csharp +private async Task ReadChannelAsync( + string label, + ScpiChannelSettings channel, + StringBuilder log, + bool skipSetupCommand = false) +{ + if (IsDisabled(channel)) + { + log.AppendLine($"> {label} SKIP: disabled in Hardware.ini"); + return 0m; + } + + if (string.IsNullOrWhiteSpace(channel.ReadCommand)) + { + throw new InvalidOperationException($"Hardware.ini [Equipment] {label} ReadCommand 값을 확인하세요."); + } + + using var client = await CreateClientAsync(label, channel); + return await ReadChannelWithClientAsync(label, channel, client, log, skipSetupCommand); +} +``` + +```csharp +private static async Task ReadChannelWithClientAsync( + string label, + ScpiChannelSettings channel, + IScpiClient client, + StringBuilder log, + bool skipSetupCommand = false) +{ + if (string.IsNullOrWhiteSpace(channel.ReadCommand)) + { + throw new InvalidOperationException($"Hardware.ini [Equipment] {label} ReadCommand 값을 확인하세요."); + } + + log.AppendLine($"> {label} CONNECT: {DescribeClient(client, channel)}"); + if (!skipSetupCommand && !string.IsNullOrWhiteSpace(channel.SetupCommand)) + { + log.AppendLine($"> {label} SETUP: {channel.SetupCommand}"); + await SendCommandListAsync(client, channel.SetupCommand); + } + + log.AppendLine($"> {label} READ: {channel.ReadCommand}"); + var response = await client.QueryAsync(channel.ReadCommand); + log.AppendLine(response); + + return ExtractFirstDecimal(response, label); +} +``` + +## Shared SCPI Connection Code + +```csharp +private Task CreateClientAsync(string label, ScpiChannelSettings channel) +{ + var connection = GetConnectionType(channel); + return connection switch + { + "TCP" or "LAN" or "ETHERNET" => CreateTcpClientAsync(label, channel), + "SERIAL" or "COM" => Task.FromResult( + new SerialScpiClient( + channel.PortName, + channel.BaudRate, + _settings.Timeout, + channel.GetIdnMatches(), + channel.GetTerminator())), + "VISA" or "USB" => CreateVisaClientAsync(channel), + _ => throw new InvalidOperationException($"{label} 장비 연결 방식은 Visa, Serial, Tcp 중 하나로 설정하세요.") + }; +} +``` + +```csharp +private static decimal ExtractFirstDecimal(string response, string label) +{ + var match = Regex.Match(response, @"[-+]?\d+(?:\.\d+)?(?:[Ee][-+]?\d+)?"); + if (!match.Success) + { + throw new InvalidOperationException($"{label} 측정값 파싱 실패"); + } + + return decimal.Parse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture); +} +``` diff --git a/EquipmentSeparated/Power Supply.ini b/EquipmentSeparated/Power Supply.ini new file mode 100644 index 0000000..1af2e17 --- /dev/null +++ b/EquipmentSeparated/Power Supply.ini @@ -0,0 +1,37 @@ +[Equipment] +Timeout=5000 +SettleMilliseconds=300 + +[PowerSupply] +Model=Keysight E36233A +Measurement=Current +PreBoardCommand=VOLT 5, (@2);OUTP ON, (@2) +PreBoardDelayMilliseconds=2000 +LogoutCommand=OUTP OFF, (@1) +Connection=LAN +Resource= +IdnMatch=E36233A +Host=192.168.10.12 +Port=5025 +PortName= +BaudRate=9600 +Terminator=LF +SetupCommand=OUTP ON, (@1) +CleanupCommand=OUTP OFF, (@1) +ReadCommand=MEAS:CURR? (@1) + +[OriginalHardwareIniKeys] +PreBoardCommand=PreBoardCommand +PreBoardDelayMilliseconds=PreBoardDelayMilliseconds +LogoutCommand=LogoutCommand +Connection=CurrentConnection +Resource=CurrentResource +IdnMatch=CurrentIdnMatch +Host=CurrentHost +Port=CurrentPort +PortName=CurrentPortName +BaudRate=CurrentBaudRate +Terminator=CurrentTerminator +SetupCommand=CurrentSetupCommand +CleanupCommand=CurrentCleanupCommand +ReadCommand=CurrentReadCommand diff --git a/EquipmentSeparated/Power Supply_Code.md b/EquipmentSeparated/Power Supply_Code.md new file mode 100644 index 0000000..5fb9a38 --- /dev/null +++ b/EquipmentSeparated/Power Supply_Code.md @@ -0,0 +1,144 @@ +# Power Supply Related Code + +This file is a reference-only extraction. It is not compiled by the Housing app. + +## Source Files + +- `Services/EquipmentMeasurementSettings.cs` +- `Services/EquipmentMeasurementService.cs` +- `Services/IScpiClient.cs` +- `Services/TcpScpiClient.cs` +- `Services/SerialScpiClient.cs` +- `Services/VisaScpiClient.cs` + +## Hardware.ini Keys + +```ini +[Equipment] +Timeout=5000 +SettleMilliseconds=300 +PreBoardCommand=VOLT 5, (@2);OUTP ON, (@2) +PreBoardDelayMilliseconds=2000 +LogoutCommand=OUTP OFF, (@1) + +; E36233A Power Supply +CurrentConnection=LAN +CurrentResource= +CurrentIdnMatch=E36233A +CurrentHost=192.168.10.12 +CurrentPort=5025 +CurrentPortName= +CurrentBaudRate=9600 +CurrentTerminator=LF +CurrentSetupCommand=OUTP ON, (@1) +CurrentCleanupCommand=OUTP OFF, (@1) +CurrentReadCommand=MEAS:CURR? (@1) +``` + +## Settings Load Code + +```csharp +PreBoardCommand = GetString(values, "PreBoardCommand", string.Empty); +PreBoardDelayMilliseconds = GetInt(values, "PreBoardDelayMilliseconds", 0); +LogoutCommand = GetString(values, "LogoutCommand", string.Empty); +Current = LoadChannel(values, "Current", "MEAS:CURR? CH1", "E362"); +``` + +## Pre-Board Output Control + +```csharp +public async Task RunPreBoardCommandAsync() +{ + var log = new StringBuilder(); + if (IsDisabled(_settings.Current) || string.IsNullOrWhiteSpace(_settings.PreBoardCommand)) + { + return string.Empty; + } + + using var client = await CreateClientAsync("CURRENT", _settings.Current); + log.AppendLine($"> CURRENT PRE-BOARD CONNECT: {DescribeClient(client, _settings.Current)}"); + log.AppendLine($"> CURRENT PRE-BOARD SETUP: {_settings.PreBoardCommand}"); + await SendCommandListAsync(client, _settings.PreBoardCommand); + return log.ToString(); +} +``` + +## Current Read Flow + +```csharp +if (!IsDisabled(_settings.Current) && !string.IsNullOrWhiteSpace(_settings.Current.SetupCommand)) +{ + currentClient = await CreateClientAsync("CURRENT", _settings.Current); + log.AppendLine($"> CURRENT PRE-READ CONNECT: {DescribeClient(currentClient, _settings.Current)}"); + log.AppendLine($"> CURRENT PRE-READ SETUP: {_settings.Current.SetupCommand}"); + await SendCommandListAsync(currentClient, _settings.Current.SetupCommand); +} + +var current = currentClient is null + ? await ReadChannelAsync("CURRENT", _settings.Current, log) + : await ReadChannelWithClientAsync("CURRENT", _settings.Current, currentClient, log, skipSetupCommand: true); +``` + +## Cleanup And Logout + +```csharp +private static async Task SendCleanupCommandAsync(IScpiClient? client, ScpiChannelSettings channel, StringBuilder log) +{ + if (client is null || string.IsNullOrWhiteSpace(channel.CleanupCommand)) + { + return; + } + + try + { + log.AppendLine($"> CURRENT CLEANUP: {channel.CleanupCommand}"); + await SendCommandListAsync(client, channel.CleanupCommand); + } + catch (Exception ex) + { + log.AppendLine($"> CURRENT CLEANUP FAILED: {ex.Message}"); + } +} +``` + +```csharp +public async Task RunLogoutCommandAsync() +{ + var log = new StringBuilder(); + if (IsDisabled(_settings.Current) || string.IsNullOrWhiteSpace(_settings.LogoutCommand)) + { + return string.Empty; + } + + using var client = await CreateClientAsync("CURRENT", _settings.Current); + log.AppendLine($"> CURRENT LOGOUT CONNECT: {DescribeClient(client, _settings.Current)}"); + log.AppendLine($"> CURRENT LOGOUT: {_settings.LogoutCommand}"); + await SendCommandListAsync(client, _settings.LogoutCommand); + return log.ToString(); +} +``` + +## Protected Power-Off Command Guard + +```csharp +private static async Task SendCommandListAsync(IScpiClient client, string commands) +{ + foreach (var command in SplitCommands(commands)) + { + if (IsProtectedPowerOffCommand(command)) + { + continue; + } + + await client.SendAsync(command); + await Task.Delay(100); + } +} +``` + +```csharp +private static bool IsProtectedPowerOffCommand(string command) +{ + return Regex.IsMatch(command, @"^\s*OUTP(?:UT)?\s+OFF\s*,?\s*\(@2\)\s*$", RegexOptions.IgnoreCase); +} +``` diff --git a/Hardware.ini b/Hardware.ini index 2108712..c88a8db 100644 --- a/Hardware.ini +++ b/Hardware.ini @@ -39,7 +39,7 @@ Timeout=5000 SettleMilliseconds=300 PreBoardCommand=VOLT 5, (@2);OUTP ON, (@2) PreBoardDelayMilliseconds=2000 -LogoutCommand=OUTP OFF, (@1);OUTP OFF, (@2) +LogoutCommand=OUTP OFF, (@1) ; 34465A DMM VoltageConnection=LAN diff --git a/Housing.csproj b/Housing.csproj index 8f4dc2c..21a0bcf 100644 --- a/Housing.csproj +++ b/Housing.csproj @@ -12,6 +12,9 @@ + + + diff --git a/Housing.sln b/Housing.sln index 26e7385..84d2680 100644 --- a/Housing.sln +++ b/Housing.sln @@ -11,6 +11,8 @@ Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "Setup0", "Setup0\Setup0.vdp EndProject Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "Setup", "Setup\Setup.vdproj", "{DBE9999D-346E-CA63-6964-F3B7FC1D42B4}" EndProject +Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "Setup100", "Setup100\Setup100.vdproj", "{935631E6-D737-F794-5DC8-B9B7DCD816ED}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +53,12 @@ Global {DBE9999D-346E-CA63-6964-F3B7FC1D42B4}.Release|Any CPU.ActiveCfg = Release {DBE9999D-346E-CA63-6964-F3B7FC1D42B4}.Release|x64.ActiveCfg = Release {DBE9999D-346E-CA63-6964-F3B7FC1D42B4}.Release|x86.ActiveCfg = Release + {935631E6-D737-F794-5DC8-B9B7DCD816ED}.Debug|Any CPU.ActiveCfg = Debug + {935631E6-D737-F794-5DC8-B9B7DCD816ED}.Debug|x64.ActiveCfg = Debug + {935631E6-D737-F794-5DC8-B9B7DCD816ED}.Debug|x86.ActiveCfg = Debug + {935631E6-D737-F794-5DC8-B9B7DCD816ED}.Release|Any CPU.ActiveCfg = Release + {935631E6-D737-F794-5DC8-B9B7DCD816ED}.Release|x64.ActiveCfg = Release + {935631E6-D737-F794-5DC8-B9B7DCD816ED}.Release|x86.ActiveCfg = Release EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Housing_UserManual/Housing_User_Manual_CodeAligned_v1.0.docx b/Housing_UserManual/Housing_User_Manual_CodeAligned_v1.0.docx new file mode 100644 index 0000000..c774c7d Binary files /dev/null and b/Housing_UserManual/Housing_User_Manual_CodeAligned_v1.0.docx differ diff --git a/Login/StartupLoginWindow.xaml.cs b/Login/StartupLoginWindow.xaml.cs index 5a2ad35..9503458 100644 --- a/Login/StartupLoginWindow.xaml.cs +++ b/Login/StartupLoginWindow.xaml.cs @@ -446,23 +446,17 @@ public partial class StartupLoginWindow : Window private static string GetLoginOptionsPath() { - var directory = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "Housing"); - var filePath = Path.Combine(directory, "LoginOptions.json"); - if (File.Exists(filePath)) - { - return filePath; - } - - Directory.CreateDirectory(directory); - var bundledPath = Path.Combine(AppContext.BaseDirectory, "LoginOptions.json"); if (File.Exists(bundledPath)) { - File.Copy(bundledPath, filePath); + return bundledPath; } - else + + var directory = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Housing"); + var filePath = Path.Combine(directory, "LoginOptions.json"); + if (!File.Exists(filePath)) { LoginSelectionOptions.CreateDefaultFile(filePath); } diff --git a/MainWindow.xaml b/MainWindow.xaml index dea9236..8ec8a78 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -486,6 +486,7 @@ PreviewKeyDown="BarcodeTextBox_PreviewKeyDown" PreviewTextInput="BarcodeTextBox_PreviewTextInput" InputMethod.IsInputMethodEnabled="False" + LostKeyboardFocus="BarcodeTextBox_LostKeyboardFocus" TextChanged="BarcodeTextBox_TextChanged"/> diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 3551af3..61c6fec 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -56,6 +56,7 @@ public partial class MainWindow : Window _scannerInputCommitTimer = new DispatcherTimer { Interval = ScannerInputIdleCommitDelay }; _scannerInputCommitTimer.Tick += ScannerInputCommitTimer_Tick; CommandManager.AddPreviewExecutedHandler(BarcodeTextBox, BarcodeTextBox_PreviewExecuted); + AttachJudgementCriteriaEnterFocus(); ApplyInspectionSettingsToTextBoxes(); ApplyLoginResult(); @@ -87,6 +88,12 @@ public partial class MainWindow : Window base.OnClosed(e); } + protected override void OnActivated(EventArgs e) + { + base.OnActivated(e); + KeepBarcodeFocusIfEmpty(); + } + private void BarcodeTextBox_TextChanged(object sender, TextChangedEventArgs e) { if (_suppressBarcodeStatusUpdate) @@ -99,6 +106,7 @@ public partial class MainWindow : Window IcSnOutputTextBox.Clear(); SetProcessStatus("WAITING BARCODE", "HmiAccentBrush"); ShowResultStatusMessage(BarcodeRequiredMessage); + KeepBarcodeFocusIfEmpty(); return; } @@ -106,6 +114,16 @@ public partial class MainWindow : Window 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; @@ -380,7 +398,7 @@ public partial class MainWindow : Window var resultCode = inspectionPass ? "OK" : "NG"; SetProcessStatus("DB SAVING", "HmiWarningBrush"); - var dbMeasurementSaveError = await SaveMeasurementStatusAsync(resultCode); + var dbMeasurementSaveError = await SaveInspectionResultAsync(measurement, resultCode); if (!string.IsNullOrWhiteSpace(dbMeasurementSaveError)) { SetProcessStatus("DB SAVE ERROR", "HmiDangerBrush"); @@ -400,8 +418,16 @@ public partial class MainWindow : Window } catch (Exception ex) { + var shutdownError = await TryRunEquipmentLogoutCommandAsync(); + SetProcessStatus("ERROR", "HmiDangerBrush"); - ShowResultStatusMessage($"보드 테스트 실패: {ex.Message}"); + var errorMessage = $"보드 테스트 실패: {ex.Message}"; + if (!string.IsNullOrWhiteSpace(shutdownError)) + { + errorMessage += $"\r\nPower supply OFF failed: {shutdownError}"; + } + + ShowResultStatusMessage(errorMessage); } finally { @@ -458,16 +484,26 @@ public partial class MainWindow : Window } 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 TryRunEquipmentLogoutCommandAsync() { try { var equipmentSettings = EquipmentMeasurementSettings.Load(GetHardwareIniPath()); var equipmentService = new EquipmentMeasurementService(equipmentSettings); await equipmentService.RunLogoutCommandAsync(); + return null; } catch (Exception ex) { - MessageBox.Show(this, $"파워서플라이 logout 전원 OFF 실패: {ex.Message}", "Logout", MessageBoxButton.OK, MessageBoxImage.Warning); + return ex.Message; } } @@ -521,18 +557,108 @@ public partial class MainWindow : Window 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 FocusBarcodeInput() + 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 GetBarcodeFocusExceptionControls() + { + yield return LogoutButton; + foreach (var textBox in GetJudgementCriteriaTextBoxes()) + { + yield return textBox; + } + } + + private IEnumerable 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); @@ -696,19 +822,27 @@ public partial class MainWindow : Window ClearPendingScannerInput(); } - private async Task SaveMeasurementStatusAsync(string resultCode) + private async Task SaveInspectionResultAsync(BoardMeasurementResult measurement, string resultCode) { + var databaseIniPath = GetDatabaseIniPath(); + DatabaseSettings? databaseSettings = null; + try { - var repository = new HousingAssemblyRepository(DatabaseSettings.Load(GetDatabaseIniPath())); - var record = CreateMeasurementRecord(resultCode); + databaseSettings = DatabaseSettings.Load(databaseIniPath); + var repository = new HousingAssemblyRepository(databaseSettings); + var record = CreateInspectionRecord(measurement, resultCode); await repository.UpsertInspectionAsync(record); return null; } catch (Exception ex) { - return ex.Message; + var target = databaseSettings is null + ? databaseIniPath + : $"{databaseSettings.Ip} / {databaseSettings.Database} (설정: {databaseIniPath})"; + + return $"{ex.Message}\r\nDB target: {target}"; } } @@ -732,10 +866,12 @@ public partial class MainWindow : Window }; } - private HousingAssemblyRecord CreateMeasurementRecord(string resultCode) + private HousingAssemblyRecord CreateInspectionRecord(BoardMeasurementResult measurement, string resultCode) { var barcode = BarcodeTextBox.Text.Trim(); - var icSn = IcSnOutputTextBox.Text.Trim(); + var icSn = string.IsNullOrWhiteSpace(measurement.IcSn) + ? IcSnOutputTextBox.Text.Trim() + : measurement.IcSn.Trim(); if (string.IsNullOrWhiteSpace(icSn)) { @@ -756,8 +892,8 @@ public partial class MainWindow : Window Line = _loginResult.LineNo, LotNo = _loginResult.LotNo, JigNo = _loginResult.JigNo, - PtVol1 = _lastMeasuredVoltage ?? ParseDecimal(VOutputTextBox.Text), - PtCurrent1 = ParseDecimal(AOutputTextBox.Text), + PtVol1 = measurement.Voltage, + PtCurrent1 = AmpsToMilliamps(measurement.Current), Result = resultCode }; } @@ -844,7 +980,7 @@ public partial class MainWindow : Window private static string FormatCurrent(decimal value) { - return AmpsToMilliamps(value).ToString("0.000", CultureInfo.InvariantCulture); + return AmpsToMilliamps(value).ToString("0.00", CultureInfo.InvariantCulture); } private static string FormatCurrentLimit(decimal value) diff --git a/Services/BoardTestService.cs b/Services/BoardTestService.cs index c2b1721..1a0baf2 100644 --- a/Services/BoardTestService.cs +++ b/Services/BoardTestService.cs @@ -32,6 +32,9 @@ public sealed class BoardTestService using var boardClient = new SerialBoardClient(_boardSettings); log.AppendLine($"> BOARD PORT: {boardClient.PortName} @ {_boardSettings.BaudRate}"); + var postReadIdCommandSent = false; + try + { await SendOnlyAndLogAsync(boardClient, "PRE_CONNECT", _boardSettings.PreConnectCommand, log); var connectResponse = await SendAndLogAsync(boardClient, "CONNECT", _boardSettings.ConnectCommand, _boardSettings.ReadTimeout, true, log); @@ -48,6 +51,7 @@ public sealed class BoardTestService } await SendOnlyAndLogAsync(boardClient, "POST_READ_ID", _boardSettings.PostReadIdCommand, log); + postReadIdCommandSent = true; if (onIcSnReadAsync is not null) { @@ -76,6 +80,31 @@ public sealed class BoardTestService Current = equipmentResult.Current, RawLog = log.ToString() }; + } + finally + { + if (!postReadIdCommandSent) + { + await SendFinalBoardOffAsync(boardClient, log); + } + } + } + + private async Task SendFinalBoardOffAsync(SerialBoardClient boardClient, StringBuilder log) + { + if (string.IsNullOrWhiteSpace(_boardSettings.PostReadIdCommand)) + { + return; + } + + try + { + await SendOnlyAndLogAsync(boardClient, "FINAL_OFF", _boardSettings.PostReadIdCommand, log); + } + catch (Exception ex) + { + log.AppendLine($"> BOARD FINAL_OFF FAILED: {ex.Message}"); + } } private static async Task SendAndLogAsync( diff --git a/Services/EquipmentMeasurementService.cs b/Services/EquipmentMeasurementService.cs index 2aba356..69f8cfa 100644 --- a/Services/EquipmentMeasurementService.cs +++ b/Services/EquipmentMeasurementService.cs @@ -257,6 +257,11 @@ public sealed class EquipmentMeasurementService { foreach (var command in SplitCommands(commands)) { + if (IsProtectedPowerOffCommand(command)) + { + continue; + } + await client.SendAsync(command); await Task.Delay(100); } @@ -270,6 +275,11 @@ public sealed class EquipmentMeasurementService .Where(command => !string.IsNullOrWhiteSpace(command)); } + private static bool IsProtectedPowerOffCommand(string command) + { + return Regex.IsMatch(command, @"^\s*OUTP(?:UT)?\s+OFF\s*,?\s*\(@2\)\s*$", RegexOptions.IgnoreCase); + } + private static bool IsDisabled(ScpiChannelSettings channel) { var connection = channel.Connection.Trim(); diff --git a/Services/HousingAssemblyRepository.cs b/Services/HousingAssemblyRepository.cs index 5b9cea8..7b6497b 100644 --- a/Services/HousingAssemblyRepository.cs +++ b/Services/HousingAssemblyRepository.cs @@ -16,7 +16,7 @@ public sealed class HousingAssemblyRepository public async Task InsertHeaderAsync(HousingAssemblyRecord record) { using var connection = new SqlConnection(CreateConnectionString()); - using var command = connection.CreateCommand(); + using var command = CreateCommand(connection); command.CommandText = @" INSERT INTO dbo.Housing_Assembly @@ -56,7 +56,7 @@ VALUES AddNullableNVarChar(command, "@Variant_2", 10, record.Variant2); AddNVarChar(command, "@Operator", 10, record.Operator); command.Parameters.Add(new SqlParameter("@Production_Date", SqlDbType.DateTime2) { Value = record.ProductionDate }); - AddNVarChar(command, "@Line", 5, record.Line); + AddNVarChar(command, "@Line", 10, record.Line); AddNVarChar(command, "@Lot_No", 8, record.LotNo); AddNVarChar(command, "@Jig_No", 10, record.JigNo); @@ -67,7 +67,7 @@ VALUES public async Task UpsertHeaderAsync(HousingAssemblyRecord record) { using var connection = new SqlConnection(CreateConnectionString()); - using var command = connection.CreateCommand(); + using var command = CreateCommand(connection); command.CommandText = @" SET XACT_ABORT ON; @@ -131,7 +131,7 @@ COMMIT TRANSACTION;"; AddNullableNVarChar(command, "@Variant_2", 10, record.Variant2); AddNVarChar(command, "@Operator", 10, record.Operator); command.Parameters.Add(new SqlParameter("@Production_Date", SqlDbType.DateTime2) { Value = record.ProductionDate }); - AddNVarChar(command, "@Line", 5, record.Line); + AddNVarChar(command, "@Line", 10, record.Line); AddNVarChar(command, "@Lot_No", 8, record.LotNo); AddNVarChar(command, "@Jig_No", 10, record.JigNo); @@ -142,7 +142,7 @@ COMMIT TRANSACTION;"; public async Task UpdateMeasurementAsync(HousingAssemblyRecord record) { using var connection = new SqlConnection(CreateConnectionString()); - using var command = connection.CreateCommand(); + using var command = CreateCommand(connection); command.CommandText = @" UPDATE dbo.Housing_Assembly @@ -154,8 +154,8 @@ WHERE [IC_SN] = @IC_SN;"; AddNVarChar(command, "@IC_SN", 50, record.IcSn); - AddDecimal(command, "@PT_Vol_1", record.PtVol1); - AddDecimal(command, "@PT_Current_1", record.PtCurrent1); + AddDecimal(command, "@PT_Vol_1", record.PtVol1, 4, 2); + AddDecimal(command, "@PT_Current_1", record.PtCurrent1, 4, 2); AddNVarChar(command, "@Result", 5, record.Result); await connection.OpenAsync(); @@ -174,7 +174,7 @@ WHERE } using var connection = new SqlConnection(CreateConnectionString()); - using var command = connection.CreateCommand(); + using var command = CreateCommand(connection); command.CommandText = @" SET XACT_ABORT ON; @@ -247,11 +247,11 @@ COMMIT TRANSACTION;"; AddNullableNVarChar(command, "@Variant_2", 10, record.Variant2); AddNVarChar(command, "@Operator", 10, record.Operator); command.Parameters.Add(new SqlParameter("@Production_Date", SqlDbType.DateTime2) { Value = record.ProductionDate }); - AddNVarChar(command, "@Line", 5, record.Line); + AddNVarChar(command, "@Line", 10, record.Line); AddNVarChar(command, "@Lot_No", 8, record.LotNo); AddNVarChar(command, "@Jig_No", 10, record.JigNo); - AddDecimal(command, "@PT_Vol_1", record.PtVol1); - AddDecimal(command, "@PT_Current_1", record.PtCurrent1); + AddDecimal(command, "@PT_Vol_1", record.PtVol1, 4, 2); + AddDecimal(command, "@PT_Current_1", record.PtCurrent1, 4, 2); AddNVarChar(command, "@Result", 5, record.Result); await connection.OpenAsync(); @@ -263,6 +263,17 @@ COMMIT TRANSACTION;"; await UpsertInspectionAsync(record); } + public async Task WarmUpAsync() + { + using var connection = new SqlConnection(CreateConnectionString()); + using var command = CreateCommand(connection); + + command.CommandText = "SELECT 1;"; + + await connection.OpenAsync(); + await command.ExecuteScalarAsync(); + } + private string CreateConnectionString() { if (string.IsNullOrWhiteSpace(_settings.Ip) || @@ -286,6 +297,13 @@ COMMIT TRANSACTION;"; return builder.ConnectionString; } + private SqlCommand CreateCommand(SqlConnection connection) + { + var command = connection.CreateCommand(); + command.CommandTimeout = Math.Max(1, _settings.Timeout); + return command; + } + private static void AddNVarChar(SqlCommand command, string name, int size, string value) { command.Parameters.Add(new SqlParameter(name, SqlDbType.NVarChar, size) { Value = value }); @@ -299,12 +317,12 @@ COMMIT TRANSACTION;"; }); } - private static void AddDecimal(SqlCommand command, string name, decimal value) + private static void AddDecimal(SqlCommand command, string name, decimal value, byte precision, byte scale) { var parameter = new SqlParameter(name, SqlDbType.Decimal) { - Precision = 3, - Scale = 2, + Precision = precision, + Scale = scale, Value = value }; diff --git a/Services/SerialBoardClient.cs b/Services/SerialBoardClient.cs index d08f44d..64a0369 100644 --- a/Services/SerialBoardClient.cs +++ b/Services/SerialBoardClient.cs @@ -6,6 +6,8 @@ namespace Housing.Services; public sealed class SerialBoardClient : IDisposable { + private const int SendOnlyDrainTimeoutMilliseconds = 1000; + private const int SendOnlyDrainIdleMilliseconds = 100; private readonly BoardHardwareSettings _settings; private readonly SerialPort _serialPort; @@ -46,6 +48,7 @@ public sealed class SerialBoardClient : IDisposable Open(); _serialPort.DiscardInBuffer(); _serialPort.Write(command.Trim() + "\r\n"); + DrainInputBuffer(SendOnlyDrainTimeoutMilliseconds, SendOnlyDrainIdleMilliseconds); }); } @@ -201,6 +204,39 @@ public sealed class SerialBoardClient : IDisposable return response.ToString(); } + private void DrainInputBuffer(int timeoutMilliseconds, int idleMilliseconds) + { + var originalReadTimeout = _serialPort.ReadTimeout; + var startedAt = DateTime.UtcNow; + + try + { + _serialPort.ReadTimeout = Math.Max(50, idleMilliseconds); + + while ((DateTime.UtcNow - startedAt).TotalMilliseconds < timeoutMilliseconds) + { + try + { + var line = _serialPort.ReadLine(); + if (!string.IsNullOrWhiteSpace(_settings.EndToken) && + line.Contains(_settings.EndToken, StringComparison.OrdinalIgnoreCase)) + { + break; + } + } + catch (TimeoutException) + { + break; + } + } + } + finally + { + _serialPort.ReadTimeout = originalReadTimeout; + _serialPort.DiscardInBuffer(); + } + } + public void Dispose() { _serialPort.Dispose(); diff --git a/Setup (2).zip b/Setup (2).zip new file mode 100644 index 0000000..2d95dbd Binary files /dev/null and b/Setup (2).zip differ diff --git a/Setup (3).zip b/Setup (3).zip new file mode 100644 index 0000000..26b82a7 Binary files /dev/null and b/Setup (3).zip differ diff --git a/Setup (4).zip b/Setup (4).zip new file mode 100644 index 0000000..1629108 Binary files /dev/null and b/Setup (4).zip differ diff --git a/Setup (5).zip b/Setup (5).zip new file mode 100644 index 0000000..604acac Binary files /dev/null and b/Setup (5).zip differ diff --git a/Setup (6).zip b/Setup (6).zip new file mode 100644 index 0000000..cba5666 Binary files /dev/null and b/Setup (6).zip differ diff --git a/Setup (7).zip b/Setup (7).zip new file mode 100644 index 0000000..079c226 Binary files /dev/null and b/Setup (7).zip differ diff --git a/Setup (8).zip b/Setup (8).zip new file mode 100644 index 0000000..28c709e Binary files /dev/null and b/Setup (8).zip differ diff --git a/Setup (9).zip b/Setup (9).zip new file mode 100644 index 0000000..4b1936c Binary files /dev/null and b/Setup (9).zip differ diff --git a/Setup/Release (3).zip b/Setup.zip similarity index 77% rename from Setup/Release (3).zip rename to Setup.zip index 243a742..419ccd9 100644 Binary files a/Setup/Release (3).zip and b/Setup.zip differ diff --git a/Setup/Release (2).zip b/Setup/Release (2).zip index 31dd2a3..62419d6 100644 Binary files a/Setup/Release (2).zip and b/Setup/Release (2).zip differ diff --git a/Setup/Release.zip b/Setup/Release.zip index 8ea0e79..3303bfe 100644 Binary files a/Setup/Release.zip and b/Setup/Release.zip differ diff --git a/Setup/Release/Setup.msi b/Setup/Release/Setup.msi index a4a30a7..f18214e 100644 Binary files a/Setup/Release/Setup.msi and b/Setup/Release/Setup.msi differ diff --git a/Setup100/Release/Setup100.msi b/Setup100/Release/Setup100.msi new file mode 100644 index 0000000..438bddd Binary files /dev/null and b/Setup100/Release/Setup100.msi differ diff --git a/Setup100/Release/setup.exe b/Setup100/Release/setup.exe new file mode 100644 index 0000000..cc73c5c Binary files /dev/null and b/Setup100/Release/setup.exe differ diff --git a/Setup100/Setup100.vdproj b/Setup100/Setup100.vdproj new file mode 100644 index 0000000..fb715dd --- /dev/null +++ b/Setup100/Setup100.vdproj @@ -0,0 +1,733 @@ +"DeployProject" +{ +"VSVersion" = "3:800" +"ProjectType" = "8:{978C614F-708E-4E1A-B201-565925725DBA}" +"IsWebType" = "8:FALSE" +"ProjectName" = "8:Setup100" +"LanguageId" = "3:1042" +"CodePage" = "3:949" +"UILanguageId" = "3:1042" +"SccProjectName" = "8:" +"SccLocalPath" = "8:" +"SccAuxPath" = "8:" +"SccProvider" = "8:" + "Hierarchy" + { + "Entry" + { + "MsmKey" = "8:_579C5BF19AF141B38F44C957EADB21FC" + "OwnerKey" = "8:_UNDEFINED" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_8883E9101F6441DEB01A93D9ED8184BB" + "OwnerKey" = "8:_UNDEFINED" + "MsmSig" = "8:_UNDEFINED" + } + } + "Configurations" + { + "Debug" + { + "DisplayName" = "8:Debug" + "IsDebugOnly" = "11:TRUE" + "IsReleaseOnly" = "11:FALSE" + "OutputFilename" = "8:Debug\\Setup100.msi" + "PackageFilesAs" = "3:2" + "PackageFileSize" = "3:-2147483648" + "CabType" = "3:1" + "Compression" = "3:2" + "SignOutput" = "11:FALSE" + "CertificateFile" = "8:" + "PrivateKeyFile" = "8:" + "TimeStampServer" = "8:" + "InstallerBootstrapper" = "3:2" + } + "Release" + { + "DisplayName" = "8:Release" + "IsDebugOnly" = "11:FALSE" + "IsReleaseOnly" = "11:TRUE" + "OutputFilename" = "8:Release\\Setup100.msi" + "PackageFilesAs" = "3:2" + "PackageFileSize" = "3:-2147483648" + "CabType" = "3:1" + "Compression" = "3:2" + "SignOutput" = "11:FALSE" + "CertificateFile" = "8:" + "PrivateKeyFile" = "8:" + "TimeStampServer" = "8:" + "InstallerBootstrapper" = "3:2" + } + } + "Deployable" + { + "CustomAction" + { + } + "DefaultFeature" + { + "Name" = "8:DefaultFeature" + "Title" = "8:" + "Description" = "8:" + } + "ExternalPersistence" + { + "LaunchCondition" + { + "{A06ECF26-33A3-4562-8140-9B0E340D4F24}:_340FFED54D9141C0B594E0BF67568967" + { + "Name" = "8:.NET Core" + "Message" = "8:[VSDNETCOREMSG]" + "AllowLaterVersions" = "11:FALSE" + "InstallUrl" = "8:https://dotnet.microsoft.com/download/dotnet-core/[NetCoreVerMajorDotMinor]" + "IsNETCore" = "11:TRUE" + "Architecture" = "2:0" + "Runtime" = "2:0" + } + } + } + "File" + { + "{1FB2D0AE-D3B9-43D4-B9DD-F88EC61E35DE}:_579C5BF19AF141B38F44C957EADB21FC" + { + "SourcePath" = "8:..\\Resources\\Housing_voltage_current_icon_msi_noborder.ico" + "TargetName" = "8:Housing_voltage_current_icon_msi_noborder.ico" + "Tag" = "8:" + "Folder" = "8:_25EEF778496E404083BDF310C935E38F" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Vital" = "11:TRUE" + "ReadOnly" = "11:FALSE" + "Hidden" = "11:FALSE" + "System" = "11:FALSE" + "Permanent" = "11:FALSE" + "SharedLegacy" = "11:FALSE" + "PackageAs" = "3:1" + "Register" = "3:1" + "Exclude" = "11:FALSE" + "IsDependency" = "11:FALSE" + "IsolateTo" = "8:" + } + } + "FileType" + { + } + "Folder" + { + "{3C67513D-01DD-4637-8A68-80971EB9504F}:_25EEF778496E404083BDF310C935E38F" + { + "DefaultLocation" = "8:[ProgramFiles64Folder][Manufacturer]\\[ProductName]" + "Name" = "8:#1925" + "AlwaysCreate" = "11:FALSE" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Property" = "8:TARGETDIR" + "Folders" + { + } + } + "{1525181F-901A-416C-8A58-119130FE478E}:_66AFF44728114DBBA9BCE34144FAAB41" + { + "Name" = "8:#1916" + "AlwaysCreate" = "11:FALSE" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Property" = "8:DesktopFolder" + "Folders" + { + } + } + "{1525181F-901A-416C-8A58-119130FE478E}:_BEE777F673764227A190AF65ACD21AC2" + { + "Name" = "8:#1919" + "AlwaysCreate" = "11:FALSE" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Property" = "8:ProgramMenuFolder" + "Folders" + { + } + } + } + "LaunchCondition" + { + } + "Locator" + { + } + "MsiBootstrapper" + { + "LangId" = "3:1042" + "RequiresElevation" = "11:FALSE" + } + "Product" + { + "Name" = "8:Microsoft Visual Studio" + "ProductName" = "8:Setup100" + "ProductCode" = "8:{42EE0882-2178-4689-8512-0C0FC32A0125}" + "PackageCode" = "8:{6CF458C9-AF29-44D4-943F-464C3DC7E545}" + "UpgradeCode" = "8:{610D4218-DD05-4EAB-BAE2-1A802D29ADD4}" + "AspNetVersion" = "8:" + "RestartWWWService" = "11:FALSE" + "RemovePreviousVersions" = "11:FALSE" + "DetectNewerInstalledVersion" = "11:TRUE" + "InstallAllUsers" = "11:FALSE" + "ProductVersion" = "8:1.0.0" + "Manufacturer" = "8:temp" + "ARPHELPTELEPHONE" = "8:" + "ARPHELPLINK" = "8:" + "Title" = "8:Setup100" + "Subject" = "8:" + "ARPCONTACT" = "8:temp" + "Keywords" = "8:" + "ARPCOMMENTS" = "8:" + "ARPURLINFOABOUT" = "8:" + "ARPPRODUCTICON" = "8:" + "ARPIconIndex" = "3:0" + "SearchPath" = "8:" + "UseSystemSearchPath" = "11:TRUE" + "TargetPlatform" = "3:1" + "PreBuildEvent" = "8:" + "PostBuildEvent" = "8:" + "RunPostBuildEvent" = "3:0" + } + "Registry" + { + "HKLM" + { + "Keys" + { + "{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_39EF283C652B4AC69FFE46D72F8A9F1F" + { + "Name" = "8:Software" + "Condition" = "8:" + "AlwaysCreate" = "11:FALSE" + "DeleteAtUninstall" = "11:FALSE" + "Transitive" = "11:FALSE" + "Keys" + { + "{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_9526C11DBE504CBB8BC54ACE4A67A452" + { + "Name" = "8:[Manufacturer]" + "Condition" = "8:" + "AlwaysCreate" = "11:FALSE" + "DeleteAtUninstall" = "11:FALSE" + "Transitive" = "11:FALSE" + "Keys" + { + } + "Values" + { + } + } + } + "Values" + { + } + } + } + } + "HKCU" + { + "Keys" + { + "{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_9B0F05EAC02645F99F3EA4153E6985B4" + { + "Name" = "8:Software" + "Condition" = "8:" + "AlwaysCreate" = "11:FALSE" + "DeleteAtUninstall" = "11:FALSE" + "Transitive" = "11:FALSE" + "Keys" + { + "{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_0EE8870A6A344A6EA9C888D961766653" + { + "Name" = "8:[Manufacturer]" + "Condition" = "8:" + "AlwaysCreate" = "11:FALSE" + "DeleteAtUninstall" = "11:FALSE" + "Transitive" = "11:FALSE" + "Keys" + { + } + "Values" + { + } + } + } + "Values" + { + } + } + } + } + "HKCR" + { + "Keys" + { + } + } + "HKU" + { + "Keys" + { + } + } + "HKPU" + { + "Keys" + { + } + } + } + "Sequences" + { + } + "Shortcut" + { + "{970C0BB2-C7D0-45D7-ABFA-7EC378858BC0}:_0869784F4F8841ACA5963F59D7B16198" + { + "Name" = "8:Housing" + "Arguments" = "8:" + "Description" = "8:" + "ShowCmd" = "3:1" + "IconIndex" = "3:0" + "Transitive" = "11:FALSE" + "Target" = "8:_8883E9101F6441DEB01A93D9ED8184BB" + "Folder" = "8:_66AFF44728114DBBA9BCE34144FAAB41" + "WorkingFolder" = "8:_25EEF778496E404083BDF310C935E38F" + "Icon" = "8:_579C5BF19AF141B38F44C957EADB21FC" + "Feature" = "8:" + } + } + "UserInterface" + { + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_04AB189F79C141D4B656C539A572FAEF" + { + "Name" = "8:#1900" + "Sequence" = "3:2" + "Attributes" = "3:1" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_02368588C083450E83F88B39FF14CFA1" + { + "Sequence" = "3:100" + "DisplayName" = "8:환영" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdAdminWelcomeDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "CopyrightWarning" + { + "Name" = "8:CopyrightWarning" + "DisplayName" = "8:#1002" + "Description" = "8:#1102" + "Type" = "3:3" + "ContextData" = "8:" + "Attributes" = "3:0" + "Setting" = "3:1" + "Value" = "8:#1202" + "DefaultValue" = "8:#1202" + "UsePlugInResources" = "11:TRUE" + } + "Welcome" + { + "Name" = "8:Welcome" + "DisplayName" = "8:#1003" + "Description" = "8:#1103" + "Type" = "3:3" + "ContextData" = "8:" + "Attributes" = "3:0" + "Setting" = "3:1" + "Value" = "8:#1203" + "DefaultValue" = "8:#1203" + "UsePlugInResources" = "11:TRUE" + } + } + } + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_6B2319BBA43F4B0E98499B17431AABC7" + { + "Sequence" = "3:300" + "DisplayName" = "8:설치 확인" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdAdminConfirmDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_8199646E4ADF4CA497CB35DD5D9B4EA3" + { + "Sequence" = "3:200" + "DisplayName" = "8:설치 폴더" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdAdminFolderDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_48A28929DD8F42FA8754A057491B7190" + { + "Name" = "8:#1902" + "Sequence" = "3:2" + "Attributes" = "3:3" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_1958E6FF4D7644948E621DB2C391ABB5" + { + "Sequence" = "3:100" + "DisplayName" = "8:마침" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdAdminFinishedDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + "{2479F3F5-0309-486D-8047-8187E2CE5BA0}:_48FD9E8D7F534713B0B26FD39C225534" + { + "UseDynamicProperties" = "11:FALSE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdUserInterface.wim" + } + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_5722F25259EC4038B82754A82CD8314C" + { + "Name" = "8:#1900" + "Sequence" = "3:1" + "Attributes" = "3:1" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_7AD188584ACC4527B8ADF78D4B8C1E94" + { + "Sequence" = "3:300" + "DisplayName" = "8:설치 확인" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdConfirmDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_8F418472D442448A82EF5B715330C4AD" + { + "Sequence" = "3:100" + "DisplayName" = "8:환영" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdWelcomeDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "CopyrightWarning" + { + "Name" = "8:CopyrightWarning" + "DisplayName" = "8:#1002" + "Description" = "8:#1102" + "Type" = "3:3" + "ContextData" = "8:" + "Attributes" = "3:0" + "Setting" = "3:1" + "Value" = "8:#1202" + "DefaultValue" = "8:#1202" + "UsePlugInResources" = "11:TRUE" + } + "Welcome" + { + "Name" = "8:Welcome" + "DisplayName" = "8:#1003" + "Description" = "8:#1103" + "Type" = "3:3" + "ContextData" = "8:" + "Attributes" = "3:0" + "Setting" = "3:1" + "Value" = "8:#1203" + "DefaultValue" = "8:#1203" + "UsePlugInResources" = "11:TRUE" + } + } + } + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_A70CDD4D26FE416E85635A6477E83C99" + { + "Sequence" = "3:200" + "DisplayName" = "8:설치 폴더" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdFolderDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "InstallAllUsersVisible" + { + "Name" = "8:InstallAllUsersVisible" + "DisplayName" = "8:#1059" + "Description" = "8:#1159" + "Type" = "3:5" + "ContextData" = "8:1;True=1;False=0" + "Attributes" = "3:0" + "Setting" = "3:0" + "Value" = "3:1" + "DefaultValue" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_61DB10700E264A71B89788C6597725A2" + { + "Name" = "8:#1902" + "Sequence" = "3:1" + "Attributes" = "3:3" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_849D9D5D0F5A481A8AEDAE597AE94504" + { + "Sequence" = "3:100" + "DisplayName" = "8:마침" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdFinishedDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "UpdateText" + { + "Name" = "8:UpdateText" + "DisplayName" = "8:#1058" + "Description" = "8:#1158" + "Type" = "3:15" + "ContextData" = "8:" + "Attributes" = "3:0" + "Setting" = "3:1" + "Value" = "8:#1258" + "DefaultValue" = "8:#1258" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + "{2479F3F5-0309-486D-8047-8187E2CE5BA0}:_66D5A4849DA74893B5DE7BC91285BF39" + { + "UseDynamicProperties" = "11:FALSE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdBasicDialogs.wim" + } + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_9ECC9BE8279449D08E3879378D28F5F4" + { + "Name" = "8:#1901" + "Sequence" = "3:1" + "Attributes" = "3:2" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_11605F0AA4EF461C9ED97277B3198620" + { + "Sequence" = "3:100" + "DisplayName" = "8:진행률" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdProgressDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "ShowProgress" + { + "Name" = "8:ShowProgress" + "DisplayName" = "8:#1009" + "Description" = "8:#1109" + "Type" = "3:5" + "ContextData" = "8:1;True=1;False=0" + "Attributes" = "3:0" + "Setting" = "3:0" + "Value" = "3:1" + "DefaultValue" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_A39EAE152FD34A3D8E60FA00E71EE82E" + { + "Name" = "8:#1901" + "Sequence" = "3:2" + "Attributes" = "3:2" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_96B06613F8184F7CB033D968D7ACAB56" + { + "Sequence" = "3:100" + "DisplayName" = "8:진행률" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdAdminProgressDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "ShowProgress" + { + "Name" = "8:ShowProgress" + "DisplayName" = "8:#1009" + "Description" = "8:#1109" + "Type" = "3:5" + "ContextData" = "8:1;True=1;False=0" + "Attributes" = "3:0" + "Setting" = "3:0" + "Value" = "3:1" + "DefaultValue" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + } + "MergeModule" + { + } + "ProjectOutput" + { + "{5259A561-127C-4D43-A0A1-72F10C7B3BF8}:_8883E9101F6441DEB01A93D9ED8184BB" + { + "SourcePath" = "8:..\\obj\\Release\\net9.0-windows\\apphost.exe" + "TargetName" = "8:" + "Tag" = "8:" + "Folder" = "8:_25EEF778496E404083BDF310C935E38F" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Vital" = "11:TRUE" + "ReadOnly" = "11:FALSE" + "Hidden" = "11:FALSE" + "System" = "11:FALSE" + "Permanent" = "11:FALSE" + "SharedLegacy" = "11:FALSE" + "PackageAs" = "3:1" + "Register" = "3:1" + "Exclude" = "11:FALSE" + "IsDependency" = "11:FALSE" + "IsolateTo" = "8:" + "ProjectOutputGroupRegister" = "3:1" + "OutputConfiguration" = "8:" + "OutputGroupCanonicalName" = "8:PublishItemsOutputGroup" + "OutputProjectGuid" = "8:{7364A567-F115-4ACE-9207-C6FDEEF0A394}" + "ShowKeyOutput" = "11:TRUE" + "ExcludeFilters" + { + } + } + } + } +} diff --git a/housing-result-ng.png b/housing-result-ng.png new file mode 100644 index 0000000..8d44a28 Binary files /dev/null and b/housing-result-ng.png differ diff --git a/housing-result-ok.png b/housing-result-ok.png new file mode 100644 index 0000000..30d8775 Binary files /dev/null and b/housing-result-ok.png differ diff --git a/init.sql b/init.sql index 2990866..80483f7 100644 --- a/init.sql +++ b/init.sql @@ -187,3 +187,33 @@ BEGIN COMMIT TRANSACTION; END; GO + +IF OBJECT_ID(N'dbo.Housing_Assembly', N'U') IS NOT NULL + AND COL_LENGTH(N'dbo.Housing_Assembly', N'IC_SN') IS NOT NULL + AND NOT EXISTS + ( + SELECT 1 + FROM sys.indexes + WHERE object_id = OBJECT_ID(N'dbo.Housing_Assembly', N'U') + AND name = N'UX_Housing_Assembly_IC_SN' + ) +BEGIN + IF EXISTS + ( + SELECT 1 + FROM dbo.Housing_Assembly + WHERE [IC_SN] IS NOT NULL + AND [IC_SN] <> N'' + GROUP BY [IC_SN] + HAVING COUNT(*) > 1 + ) + BEGIN + THROW 51001, 'Cannot create UX_Housing_Assembly_IC_SN because duplicate IC_SN values exist.', 1; + END; + + CREATE UNIQUE INDEX UX_Housing_Assembly_IC_SN + ON dbo.Housing_Assembly ([IC_SN]) + WHERE [IC_SN] IS NOT NULL + AND [IC_SN] <> N''; +END; +GO diff --git a/publish/setup-win-x64/Database.ini b/publish/setup-win-x64/Database.ini index 8f32133..bf82481 100644 --- a/publish/setup-win-x64/Database.ini +++ b/publish/setup-win-x64/Database.ini @@ -1,11 +1,11 @@ [Database] -IP=127.0.0.1 -Database=Housing -DbId=sa -DbPw=your_password -Encrypt=True +IP=192.168.10.10,1433 +Database=NE1aW_PT_Sensor +DbId=su_user +DbPw=amotech821 +Encrypt=False TrustServerCertificate=True -Timeout=5 +Timeout=15 [Login] Procedure=dbo.CheckOperator diff --git a/publish/setup-win-x64/Hardware.ini b/publish/setup-win-x64/Hardware.ini index cafa5ef..a089d98 100644 --- a/publish/setup-win-x64/Hardware.ini +++ b/publish/setup-win-x64/Hardware.ini @@ -39,7 +39,7 @@ Timeout=5000 SettleMilliseconds=300 PreBoardCommand=VOLT 5, (@2);OUTP ON, (@2) PreBoardDelayMilliseconds=2000 -LogoutCommand=OUTP OFF, (@1);OUTP OFF, (@2) +LogoutCommand=OUTP OFF, (@1) ; 34465A DMM VoltageConnection=LAN diff --git a/publish/setup-win-x64/init.sql b/publish/setup-win-x64/init.sql index 2990866..80483f7 100644 --- a/publish/setup-win-x64/init.sql +++ b/publish/setup-win-x64/init.sql @@ -187,3 +187,33 @@ BEGIN COMMIT TRANSACTION; END; GO + +IF OBJECT_ID(N'dbo.Housing_Assembly', N'U') IS NOT NULL + AND COL_LENGTH(N'dbo.Housing_Assembly', N'IC_SN') IS NOT NULL + AND NOT EXISTS + ( + SELECT 1 + FROM sys.indexes + WHERE object_id = OBJECT_ID(N'dbo.Housing_Assembly', N'U') + AND name = N'UX_Housing_Assembly_IC_SN' + ) +BEGIN + IF EXISTS + ( + SELECT 1 + FROM dbo.Housing_Assembly + WHERE [IC_SN] IS NOT NULL + AND [IC_SN] <> N'' + GROUP BY [IC_SN] + HAVING COUNT(*) > 1 + ) + BEGIN + THROW 51001, 'Cannot create UX_Housing_Assembly_IC_SN because duplicate IC_SN values exist.', 1; + END; + + CREATE UNIQUE INDEX UX_Housing_Assembly_IC_SN + ON dbo.Housing_Assembly ([IC_SN]) + WHERE [IC_SN] IS NOT NULL + AND [IC_SN] <> N''; +END; +GO diff --git a/tools/ManualScreenCapture/ManualScreenCapture.csproj b/tools/ManualScreenCapture/ManualScreenCapture.csproj new file mode 100644 index 0000000..b35acd7 --- /dev/null +++ b/tools/ManualScreenCapture/ManualScreenCapture.csproj @@ -0,0 +1,12 @@ + + + Exe + net9.0-windows + true + enable + enable + + + + + diff --git a/tools/ManualScreenCapture/Program.cs b/tools/ManualScreenCapture/Program.cs new file mode 100644 index 0000000..df805a8 --- /dev/null +++ b/tools/ManualScreenCapture/Program.cs @@ -0,0 +1,216 @@ +using System.Reflection; +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Housing; +using Housing.Login; + +internal static class Program +{ + private const int MainWidth = 1200; + private const int MainHeight = 800; + private const string ExampleBarcode = "A251001AM46110901T00001-01"; + private const string ExampleIcSn = "025949A8492CFEAC"; + + [STAThread] + private static void Main() + { + var outputDirectory = Path.GetFullPath( + Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "manual-screens")); + Directory.CreateDirectory(outputDirectory); + + var app = new App(); + app.InitializeComponent(); + + CaptureLogin(Path.Combine(outputDirectory, "01-login.png")); + CaptureMainScreens(outputDirectory); + + Console.WriteLine(outputDirectory); + } + + private static void CaptureLogin(string path) + { + var window = new StartupLoginWindow + { + WindowState = WindowState.Normal, + Width = 760, + Height = 760 + }; + + SelectFirst(window, "MakerComboBox"); + SelectFirst(window, "ModelComboBox"); + SelectFirst(window, "Variant1ComboBox"); + SelectFirst(window, "Variant2ComboBox"); + SelectFirst(window, "LineNoComboBox"); + SelectFirst(window, "JigNoComboBox"); + SetText(window, "OperatorTextBox", "OP1001"); + SetPassword(window, "PasswordBox", "manual"); + SetText(window, "LotNoTextBox", "LOT26001"); + + RenderWindowContent(window, path, 760, 760); + window.Close(); + } + + private static void CaptureMainScreens(string outputDirectory) + { + CaptureMain(Path.Combine(outputDirectory, "02-waiting-barcode.png"), window => + { + PrepareMain(window); + SetText(window, "BarcodeTextBox", string.Empty); + SetText(window, "IcSnOutputTextBox", string.Empty); + Invoke(window, "SetProcessStatus", "WAITING BARCODE", "HmiAccentBrush"); + Invoke(window, "ShowResultStatusMessage", "스캐너로 PCB_Barcode를 스캔해주세요."); + }); + + CaptureMain(Path.Combine(outputDirectory, "03-wait-start-signal.png"), window => + { + PrepareMain(window); + SetText(window, "BarcodeTextBox", ExampleBarcode); + Invoke(window, "SetProcessStatus", "WAIT START SIGNAL", "HmiWarningBrush"); + Invoke( + window, + "ShowResultStatusMessage", + "장비 시작 신호 대기\nNI-6501 Dev1/port0/line0\n시료를 넣고 Housing 장비 시작 버튼을 눌러주세요."); + }); + + CaptureMain(Path.Combine(outputDirectory, "04-board-test.png"), window => + { + PrepareMain(window); + SetText(window, "BarcodeTextBox", ExampleBarcode); + SetText(window, "IcSnOutputTextBox", ExampleIcSn); + Invoke(window, "SetProcessStatus", "BOARD TEST", "HmiWarningBrush"); + Invoke( + window, + "ShowResultStatusMessage", + "보드 테스트 시작\n1. 시료 연결 확인\n2. IC_SN 읽기\n3. CAL DEFAULT 조건 설정\n4. 시작 신호 기준 지연 후 계측 장비에서 V/A 읽기"); + }); + + CaptureMain(Path.Combine(outputDirectory, "05-result-ok.png"), window => + { + PrepareMain(window); + SetText(window, "BarcodeTextBox", ExampleBarcode); + SetText(window, "IcSnOutputTextBox", ExampleIcSn); + SetText(window, "VOutputTextBox", "5.02"); + SetText(window, "AOutputTextBox", "118.40"); + Invoke(window, "ApplyMeasurementOutputState", Find(window, "VOutputTextBox"), true); + Invoke(window, "ApplyMeasurementOutputState", Find(window, "AOutputTextBox"), true); + Invoke(window, "ShowInspectionResult", true); + Invoke(window, "SetProcessStatus", "COMPLETE OK", "HmiRunBrush"); + }); + + CaptureMain(Path.Combine(outputDirectory, "06-result-ng.png"), window => + { + PrepareMain(window); + SetText(window, "BarcodeTextBox", ExampleBarcode); + SetText(window, "IcSnOutputTextBox", ExampleIcSn); + SetText(window, "VOutputTextBox", "5.84"); + SetText(window, "AOutputTextBox", "122.10"); + Invoke(window, "ApplyMeasurementOutputState", Find(window, "VOutputTextBox"), false); + Invoke(window, "ApplyMeasurementOutputState", Find(window, "AOutputTextBox"), true); + Invoke(window, "ShowInspectionResult", false); + Invoke(window, "SetProcessStatus", "COMPLETE NG", "HmiDangerBrush"); + }); + + CaptureMain(Path.Combine(outputDirectory, "07-db-save-error.png"), window => + { + PrepareMain(window); + SetText(window, "BarcodeTextBox", ExampleBarcode); + SetText(window, "IcSnOutputTextBox", ExampleIcSn); + SetText(window, "VOutputTextBox", "5.01"); + SetText(window, "AOutputTextBox", "117.80"); + Invoke(window, "ApplyMeasurementOutputState", Find(window, "VOutputTextBox"), true); + Invoke(window, "ApplyMeasurementOutputState", Find(window, "AOutputTextBox"), true); + Invoke(window, "ShowInspectionResult", true); + Invoke(window, "SetProcessStatus", "DB SAVE ERROR", "HmiDangerBrush"); + }); + } + + private static void CaptureMain(string path, Action arrange) + { + var window = new MainWindow + { + WindowState = WindowState.Normal, + Width = MainWidth, + Height = MainHeight + }; + + arrange(window); + RenderWindowContent(window, path, MainWidth, MainHeight); + window.Close(); + } + + private static void PrepareMain(MainWindow window) + { + SetTextBlock(window, "OperatorTextBlock", "OP1001"); + Find