From a2a5cdeeb69f0370d4e4cbcd52ea6dcbcf942343 Mon Sep 17 00:00:00 2001 From: gudae Date: Tue, 9 Jun 2026 13:34:09 +0900 Subject: [PATCH] add DB --- .vs/Housing/DesignTimeBuild/.dtbcache.v2 | Bin 0 -> 122439 bytes Hardware.ini | 28 +++-- MainWindow.xaml | 5 +- MainWindow.xaml.cs | 112 ++++++++++++++---- Models/HousingAssemblyRecord.cs | 8 +- Services/BoardHardwareSettings.cs | 27 ++++- Services/BoardTestService.cs | 36 +++++- Services/EquipmentMeasurementService.cs | 144 ++++++++++++++++++++--- Services/EquipmentMeasurementSettings.cs | 15 +++ Services/HousingAssemblyRepository.cs | 74 ++++++++---- Services/SerialBoardClient.cs | 19 ++- Services/SerialScpiClient.cs | 96 +++++++++++++-- 12 files changed, 478 insertions(+), 86 deletions(-) create mode 100644 .vs/Housing/DesignTimeBuild/.dtbcache.v2 diff --git a/.vs/Housing/DesignTimeBuild/.dtbcache.v2 b/.vs/Housing/DesignTimeBuild/.dtbcache.v2 new file mode 100644 index 0000000000000000000000000000000000000000..1e212145c4cd59c351eff0ea1b604b17e6ce7b6e GIT binary patch literal 122439 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 +DtrEnable=false +RtsEnable=false [Equipment] ; 목표: 장비 화면에 표시되는 현재 측정값을 읽습니다. @@ -15,20 +18,31 @@ EndToken= ; 예: USB0::0x2A8D::0x1301::MY12345678::INSTR Timeout=5000 SettleMilliseconds=300 +PreBoardCommand=VOLT 5, (@2);OUTP ON, (@2) +PreBoardDelayMilliseconds=2000 +LogoutCommand=OUTP OFF, (@1);OUTP OFF, (@2) ; 34465A DMM -VoltageConnection=Visa +VoltageConnection=Tcp VoltageResource= VoltageIdnMatch=34460,34461,34465,34470,3446,3447 +VoltageHost=192.168.0.3 +VoltagePort=5025 VoltageSetupCommand= VoltageReadCommand=MEAS:VOLT:DC? -; E36233A Power Supply -CurrentConnection=Visa +; E36200 Series Power Supply +CurrentConnection=Tcp CurrentResource= CurrentIdnMatch=E362 -CurrentSetupCommand= -CurrentReadCommand=MEAS:CURR? CH1 +CurrentHost= +CurrentPort=5025 +CurrentPortName= +CurrentBaudRate=9600 +CurrentTerminator=LF +CurrentSetupCommand=OUTP ON, (@1) +CurrentCleanupCommand=OUTP OFF, (@1) +CurrentReadCommand=MEAS:CURR? (@1) ; COM 포트로 잡히는 장비 예시: ; VoltageConnection=Serial diff --git a/MainWindow.xaml b/MainWindow.xaml index 259e479..13bda62 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -528,8 +528,9 @@ BorderBrush="{StaticResource HmiWarningBrush}" FontSize="22" FontWeight="SemiBold" - HorizontalContentAlignment="Left" - VerticalContentAlignment="Top" + HorizontalContentAlignment="Center" + VerticalContentAlignment="Center" + TextAlignment="Center" TextWrapping="Wrap" AcceptsReturn="True" VerticalScrollBarVisibility="Auto"/> diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 8654787..719f764 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -109,16 +109,22 @@ public partial class MainWindow : Window var boardSettings = BoardHardwareSettings.Load(GetHardwareIniPath()); var equipmentSettings = EquipmentMeasurementSettings.Load(GetHardwareIniPath()); var service = new BoardTestService(boardSettings, equipmentSettings); - var measurement = await service.RunAsync(); + string? dbHeaderSaveError = null; + var measurement = await service.RunAsync(async icSn => + { + IcSnOutputTextBox.Text = icSn; + dbHeaderSaveError = await SaveHeaderStatusAsync(icSn); + }); IcSnOutputTextBox.Text = measurement.IcSn; VOutputTextBox.Text = FormatDecimal(measurement.Voltage); AOutputTextBox.Text = FormatDecimal(measurement.Current); var resultCode = _inspectionSettings.IsPass(measurement.Voltage, measurement.Current) ? "OK" : "NG"; - ResultOutputTextBox.Text = $"{resultCode}\r\n{measurement.RawLog}"; + ResultOutputTextBox.Text = resultCode; - await SaveCurrentResultAsync(resultCode); + var dbMeasurementSaveError = await SaveMeasurementStatusAsync(resultCode); + ResultOutputTextBox.Text = $"{resultCode}\r\n{BuildDbSaveMessage(dbHeaderSaveError, dbMeasurementSaveError)}"; } catch (Exception ex) { @@ -132,8 +138,11 @@ public partial class MainWindow : Window } } - private void LogoutButton_Click(object sender, RoutedEventArgs e) + private async void LogoutButton_Click(object sender, RoutedEventArgs e) { + LogoutButton.IsEnabled = false; + await RunEquipmentLogoutCommandAsync(); + ResetScreen(); _loginResult = new StartupLoginWindowResult(); OperatorTextBlock.Text = "-"; @@ -145,6 +154,20 @@ public partial class MainWindow : Window } } + 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 @@ -166,9 +189,11 @@ public partial class MainWindow : Window private void ApplyLoginResult() { OperatorTextBlock.Text = string.IsNullOrWhiteSpace(_loginResult.Operator) ? "-" : _loginResult.Operator; - LogoutButton.Visibility = string.IsNullOrWhiteSpace(_loginResult.Operator) - ? Visibility.Collapsed - : Visibility.Visible; + var isLoggedIn = !string.IsNullOrWhiteSpace(_loginResult.Operator); + LogoutButton.Visibility = isLoggedIn + ? Visibility.Visible + : Visibility.Collapsed; + LogoutButton.IsEnabled = isLoggedIn; } private void ResetScreen() @@ -201,33 +226,58 @@ public partial class MainWindow : Window }), DispatcherPriority.Input); } - private async Task SaveCurrentResultAsync(string resultCode) + private async Task SaveHeaderStatusAsync(string icSn) { try { var repository = new HousingAssemblyRepository(DatabaseSettings.Load(GetDatabaseIniPath())); - var record = CreateCurrentRecord(resultCode); - await repository.InsertAsync(record); + var record = CreateHeaderRecord(icSn); + await repository.InsertHeaderAsync(record); - ResultOutputTextBox.Text += "\r\nDB 저장 완료"; + return null; } catch (Exception ex) { - ResultOutputTextBox.Text += $"\r\nDB 저장 실패: {ex.Message}"; + return ex.Message; } } - private HousingAssemblyRecord CreateCurrentRecord(string resultCode) + private async Task SaveMeasurementStatusAsync(string resultCode) { - var barcode = BarcodeTextBox.Text.Trim(); - var icSn = IcSnOutputTextBox.Text.Trim(); + try + { + var repository = new HousingAssemblyRepository(DatabaseSettings.Load(GetDatabaseIniPath())); + var record = CreateMeasurementRecord(resultCode); + await repository.UpdateMeasurementAsync(record); - if (string.IsNullOrWhiteSpace(icSn)) + return null; + } + catch (Exception ex) { - icSn = barcode; - IcSnOutputTextBox.Text = icSn; + return ex.Message; + } + } + + private static string BuildDbSaveMessage(params string?[] errors) + { + var failureMessages = new List(); + foreach (var error in errors) + { + if (!string.IsNullOrWhiteSpace(error)) + { + failureMessages.Add(error); + } } + return failureMessages.Count == 0 + ? "DB 저장 완료" + : $"DB 저장 실패 ({string.Join("; ", failureMessages)})"; + } + + private HousingAssemblyRecord CreateHeaderRecord(string icSn) + { + var barcode = BarcodeTextBox.Text.Trim(); + return new HousingAssemblyRecord { IcSn = icSn, @@ -241,10 +291,32 @@ public partial class MainWindow : Window ProductionDate = DateTime.Now, Line = _loginResult.LineNo, LotNo = _loginResult.LotNo, - JigNo = _loginResult.JigNo, + 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, PtVol1 = ParseDecimal(VOutputTextBox.Text), PtCurrent1 = ParseDecimal(AOutputTextBox.Text), - Result = resultCode + Result = resultCode, + Spare1 = null, + Spare2 = null, + Spare3 = null, + Spare4 = null }; } diff --git a/Models/HousingAssemblyRecord.cs b/Models/HousingAssemblyRecord.cs index a14036e..01951cf 100644 --- a/Models/HousingAssemblyRecord.cs +++ b/Models/HousingAssemblyRecord.cs @@ -19,8 +19,8 @@ public sealed class HousingAssemblyRecord public decimal PtVol1 { get; set; } public decimal PtCurrent1 { get; set; } public string Result { get; set; } = string.Empty; - public string Spare1 { get; set; } = string.Empty; - public string Spare2 { get; set; } = string.Empty; - public string Spare3 { get; set; } = string.Empty; - public string Spare4 { get; set; } = string.Empty; + public string? Spare1 { get; set; } + public string? Spare2 { get; set; } + public string? Spare3 { get; set; } + public string? Spare4 { get; set; } } diff --git a/Services/BoardHardwareSettings.cs b/Services/BoardHardwareSettings.cs index 2a2e69e..651e94c 100644 --- a/Services/BoardHardwareSettings.cs +++ b/Services/BoardHardwareSettings.cs @@ -7,10 +7,13 @@ public sealed class BoardHardwareSettings public string PortName { get; set; } = "Auto"; public int BaudRate { get; set; } = 115200; public int ReadTimeout { get; set; } = 5000; + public string PreConnectCommand { get; set; } = "x00o"; public string ConnectCommand { get; set; } = "x00c_001101:owt28006727ea97c7801"; public string ReadIdCommand { get; set; } = "x00c_001101:ow2800326003e"; - public string CalDefaultCommand { get; set; } = "x00c_001001"; + public string CalDefaultCommand { get; set; } = "x00c_001001:owt28006727ea97c7801"; public string EndToken { get; set; } = ""; + public bool DtrEnable { get; set; } + public bool RtsEnable { get; set; } public static BoardHardwareSettings Load(string filePath) { @@ -25,10 +28,13 @@ public sealed class BoardHardwareSettings PortName = GetString(values, "PortName", "Auto"), BaudRate = GetInt(values, "BaudRate", 115200), ReadTimeout = GetInt(values, "ReadTimeout", 5000), + PreConnectCommand = GetString(values, "PreConnectCommand", "x00o"), ConnectCommand = GetString(values, "ConnectCommand", "x00c_001101:owt28006727ea97c7801"), ReadIdCommand = GetString(values, "ReadIdCommand", "x00c_001101:ow2800326003e"), - CalDefaultCommand = GetString(values, "CalDefaultCommand", "x00c_001001"), - EndToken = GetString(values, "EndToken", "") + CalDefaultCommand = GetString(values, "CalDefaultCommand", "x00c_001001:owt28006727ea97c7801"), + EndToken = GetString(values, "EndToken", ""), + DtrEnable = GetBool(values, "DtrEnable", false), + RtsEnable = GetBool(values, "RtsEnable", false) }; } @@ -45,4 +51,19 @@ public sealed class BoardHardwareSettings ? result : defaultValue; } + + private static bool GetBool(Dictionary values, string key, bool defaultValue) + { + if (!values.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value)) + { + return defaultValue; + } + + return value.Trim().ToUpperInvariant() switch + { + "1" or "TRUE" or "YES" or "ON" => true, + "0" or "FALSE" or "NO" or "OFF" => false, + _ => defaultValue + }; + } } diff --git a/Services/BoardTestService.cs b/Services/BoardTestService.cs index 2518fd2..fb2195f 100644 --- a/Services/BoardTestService.cs +++ b/Services/BoardTestService.cs @@ -15,12 +15,23 @@ public sealed class BoardTestService _equipmentSettings = equipmentSettings; } - public async Task RunAsync() + public async Task RunAsync(Func? onIcSnReadAsync = null) { - using var boardClient = new SerialBoardClient(_boardSettings); var log = new StringBuilder(); + var equipmentService = new EquipmentMeasurementService(_equipmentSettings); + + log.Append(await equipmentService.RunPreBoardCommandAsync()); + if (_equipmentSettings.PreBoardDelayMilliseconds > 0) + { + log.AppendLine($"> CURRENT PRE-BOARD DELAY: {_equipmentSettings.PreBoardDelayMilliseconds} ms"); + await Task.Delay(_equipmentSettings.PreBoardDelayMilliseconds); + } + + using var boardClient = new SerialBoardClient(_boardSettings); log.AppendLine($"> BOARD PORT: {boardClient.PortName} @ {_boardSettings.BaudRate}"); + await SendOnlyAndLogAsync(boardClient, "PRE_CONNECT", _boardSettings.PreConnectCommand, log); + var connectResponse = await SendAndLogAsync(boardClient, "CONNECT", _boardSettings.ConnectCommand, _boardSettings.ReadTimeout, true, log); if (!ContainsSuccess(connectResponse)) { @@ -34,9 +45,13 @@ public sealed class BoardTestService throw new InvalidOperationException("IC_SN 읽기 실패"); } + if (onIcSnReadAsync is not null) + { + await onIcSnReadAsync(icSn); + } + await SendAndLogAsync(boardClient, "CAL_DEFAULT", _boardSettings.CalDefaultCommand, _boardSettings.ReadTimeout, false, log); - var equipmentService = new EquipmentMeasurementService(_equipmentSettings); var equipmentResult = await equipmentService.ReadAsync(); log.Append(equipmentResult.RawLog); @@ -63,6 +78,21 @@ public sealed class BoardTestService return response; } + private static async Task SendOnlyAndLogAsync( + SerialBoardClient client, + string label, + string command, + StringBuilder log) + { + if (string.IsNullOrWhiteSpace(command)) + { + return; + } + + log.AppendLine($"> BOARD {label}: {command}"); + await client.SendOnlyAsync(command); + } + private static bool ContainsSuccess(string response) { return response diff --git a/Services/EquipmentMeasurementService.cs b/Services/EquipmentMeasurementService.cs index c993eff..341aff7 100644 --- a/Services/EquipmentMeasurementService.cs +++ b/Services/EquipmentMeasurementService.cs @@ -14,40 +14,131 @@ public sealed class EquipmentMeasurementService _settings = settings; } + 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(); + } + + 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(); + } + public async Task ReadAsync() { var log = new StringBuilder(); + IScpiClient? currentClient = null; - if (_settings.SettleMilliseconds > 0) + try + { + 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); + } + + if (_settings.SettleMilliseconds > 0) + { + await Task.Delay(_settings.SettleMilliseconds); + } + + var voltage = await ReadChannelAsync("VOLTAGE", _settings.Voltage, log); + var current = currentClient is null + ? await ReadChannelAsync("CURRENT", _settings.Current, log) + : await ReadChannelWithClientAsync("CURRENT", _settings.Current, currentClient, log, skipSetupCommand: true); + + return new BoardMeasurementResult + { + Voltage = voltage, + Current = current, + RawLog = log.ToString() + }; + } + finally { - await Task.Delay(_settings.SettleMilliseconds); + await SendCleanupCommandAsync(currentClient, _settings.Current, log); + currentClient?.Dispose(); } + } - var voltage = await ReadChannelAsync("VOLTAGE", _settings.Voltage, log); - var current = await ReadChannelAsync("CURRENT", _settings.Current, log); + private static async Task SendCleanupCommandAsync(IScpiClient? client, ScpiChannelSettings channel, StringBuilder log) + { + if (client is null || string.IsNullOrWhiteSpace(channel.CleanupCommand)) + { + return; + } - return new BoardMeasurementResult + try { - Voltage = voltage, - Current = current, - RawLog = log.ToString() - }; + log.AppendLine($"> CURRENT CLEANUP: {channel.CleanupCommand}"); + await SendCommandListAsync(client, channel.CleanupCommand); + } + catch (Exception ex) + { + log.AppendLine($"> CURRENT CLEANUP FAILED: {ex.Message}"); + } } - private async Task ReadChannelAsync(string label, ScpiChannelSettings channel, StringBuilder log) + 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); - log.AppendLine($"> {label} CONNECT: {DescribeClient(client, channel)}"); + return await ReadChannelWithClientAsync(label, channel, client, log, skipSetupCommand); + } + + 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 값을 확인하세요."); + } - if (!string.IsNullOrWhiteSpace(channel.SetupCommand)) + log.AppendLine($"> {label} CONNECT: {DescribeClient(client, channel)}"); + if (!skipSetupCommand && !string.IsNullOrWhiteSpace(channel.SetupCommand)) { log.AppendLine($"> {label} SETUP: {channel.SetupCommand}"); - await client.SendAsync(channel.SetupCommand); + await SendCommandListAsync(client, channel.SetupCommand); } log.AppendLine($"> {label} READ: {channel.ReadCommand}"); @@ -63,7 +154,7 @@ public sealed class EquipmentMeasurementService return connection switch { "TCP" or "LAN" => CreateTcpClientAsync(label, channel), - "SERIAL" or "COM" => Task.FromResult(new SerialScpiClient(channel.PortName, channel.BaudRate, _settings.Timeout, channel.GetIdnMatches())), + "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 중 하나로 설정하세요.") }; @@ -135,4 +226,29 @@ public sealed class EquipmentMeasurementService return decimal.Parse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture); } + + private static async Task SendCommandListAsync(IScpiClient client, string commands) + { + foreach (var command in SplitCommands(commands)) + { + await client.SendAsync(command); + await Task.Delay(100); + } + } + + private static IEnumerable SplitCommands(string commands) + { + return commands + .Split(';') + .Select(command => command.Trim()) + .Where(command => !string.IsNullOrWhiteSpace(command)); + } + + private static bool IsDisabled(ScpiChannelSettings channel) + { + var connection = channel.Connection.Trim(); + return string.Equals(connection, "None", StringComparison.OrdinalIgnoreCase) || + string.Equals(connection, "Disabled", StringComparison.OrdinalIgnoreCase) || + string.Equals(connection, "Off", StringComparison.OrdinalIgnoreCase); + } } diff --git a/Services/EquipmentMeasurementSettings.cs b/Services/EquipmentMeasurementSettings.cs index e0100f3..aecc886 100644 --- a/Services/EquipmentMeasurementSettings.cs +++ b/Services/EquipmentMeasurementSettings.cs @@ -6,6 +6,9 @@ public sealed class EquipmentMeasurementSettings { public int Timeout { get; set; } = 5000; public int SettleMilliseconds { get; set; } = 300; + public string PreBoardCommand { get; set; } = string.Empty; + public int PreBoardDelayMilliseconds { get; set; } = 0; + public string LogoutCommand { get; set; } = string.Empty; public ScpiChannelSettings Voltage { get; set; } = new(); public ScpiChannelSettings Current { get; set; } = new(); @@ -21,6 +24,9 @@ public sealed class EquipmentMeasurementSettings { Timeout = GetInt(values, "Timeout", 5000), SettleMilliseconds = GetInt(values, "SettleMilliseconds", 300), + PreBoardCommand = GetString(values, "PreBoardCommand", string.Empty), + PreBoardDelayMilliseconds = GetInt(values, "PreBoardDelayMilliseconds", 0), + LogoutCommand = GetString(values, "LogoutCommand", string.Empty), Voltage = LoadChannel(values, "Voltage", "MEAS:VOLT:DC?", "34460,34461,34465,34470,3446,3447"), Current = LoadChannel(values, "Current", "MEAS:CURR? CH1", "E362") }; @@ -41,7 +47,9 @@ public sealed class EquipmentMeasurementSettings 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) }; } @@ -70,7 +78,9 @@ public sealed class ScpiChannelSettings public int Port { get; set; } = 5025; public string PortName { get; set; } = string.Empty; public int BaudRate { get; set; } = 115200; + public string Terminator { get; set; } = string.Empty; public string SetupCommand { get; set; } = string.Empty; + public string CleanupCommand { get; set; } = string.Empty; public string ReadCommand { get; set; } = string.Empty; public string[] GetIdnMatches() @@ -81,4 +91,9 @@ public sealed class ScpiChannelSettings .Where(value => !string.IsNullOrWhiteSpace(value)) .ToArray(); } + + public string GetTerminator() + { + return string.IsNullOrWhiteSpace(Terminator) ? "\n" : Terminator; + } } diff --git a/Services/HousingAssemblyRepository.cs b/Services/HousingAssemblyRepository.cs index 575c773..e13e1a4 100644 --- a/Services/HousingAssemblyRepository.cs +++ b/Services/HousingAssemblyRepository.cs @@ -13,7 +13,7 @@ public sealed class HousingAssemblyRepository _settings = settings; } - public async Task InsertAsync(HousingAssemblyRecord record) + public async Task InsertHeaderAsync(HousingAssemblyRecord record) { using var connection = new SqlConnection(CreateConnectionString()); using var command = connection.CreateCommand(); @@ -32,14 +32,7 @@ INSERT INTO dbo.Housing_Assembly [Production_Date], [Line], [Lot_No], - [Jig_No], - [PT_Vol_1], - [PT_Current_1], - [Result], - [Spare_1], - [Spare_2], - [Spare_3], - [Spare_4] + [Jig_No] ) VALUES ( @@ -54,14 +47,7 @@ VALUES @Production_Date, @Line, @Lot_No, - @Jig_No, - @PT_Vol_1, - @PT_Current_1, - @Result, - @Spare_1, - @Spare_2, - @Spare_3, - @Spare_4 + @Jig_No );"; AddNVarChar(command, "@IC_SN", 50, record.IcSn); @@ -76,16 +62,52 @@ VALUES AddNVarChar(command, "@Line", 5, record.Line); AddNVarChar(command, "@Lot_No", 8, record.LotNo); AddNVarChar(command, "@Jig_No", 10, record.JigNo); + + await connection.OpenAsync(); + await command.ExecuteNonQueryAsync(); + } + + public async Task UpdateMeasurementAsync(HousingAssemblyRecord record) + { + using var connection = new SqlConnection(CreateConnectionString()); + using var command = connection.CreateCommand(); + + command.CommandText = @" +UPDATE dbo.Housing_Assembly +SET + [PT_Vol_1] = @PT_Vol_1, + [PT_Current_1] = @PT_Current_1, + [Result] = @Result, + [Spare_1] = @Spare_1, + [Spare_2] = @Spare_2, + [Spare_3] = @Spare_3, + [Spare_4] = @Spare_4 +WHERE + [IC_SN] = @IC_SN + AND [PCB_Barcode] = @PCB_Barcode;"; + + AddNVarChar(command, "@IC_SN", 50, record.IcSn); + AddNVarChar(command, "@PCB_Barcode", 50, record.PcbBarcode); AddDecimal(command, "@PT_Vol_1", record.PtVol1); AddDecimal(command, "@PT_Current_1", record.PtCurrent1); AddNVarChar(command, "@Result", 5, record.Result); - AddNVarChar(command, "@Spare_1", 30, record.Spare1); - AddNVarChar(command, "@Spare_2", 30, record.Spare2); - AddNVarChar(command, "@Spare_3", 30, record.Spare3); - AddNVarChar(command, "@Spare_4", 30, record.Spare4); + AddNullableNVarChar(command, "@Spare_1", 30, record.Spare1); + AddNullableNVarChar(command, "@Spare_2", 30, record.Spare2); + AddNullableNVarChar(command, "@Spare_3", 30, record.Spare3); + AddNullableNVarChar(command, "@Spare_4", 30, record.Spare4); await connection.OpenAsync(); - await command.ExecuteNonQueryAsync(); + var affectedRows = await command.ExecuteNonQueryAsync(); + if (affectedRows == 0) + { + throw new InvalidOperationException("DB measurement update target was not found."); + } + } + + public async Task InsertAsync(HousingAssemblyRecord record) + { + await InsertHeaderAsync(record); + await UpdateMeasurementAsync(record); } private string CreateConnectionString() @@ -116,6 +138,14 @@ VALUES command.Parameters.Add(new SqlParameter(name, SqlDbType.NVarChar, size) { Value = value }); } + private static void AddNullableNVarChar(SqlCommand command, string name, int size, string? value) + { + command.Parameters.Add(new SqlParameter(name, SqlDbType.NVarChar, size) + { + Value = string.IsNullOrWhiteSpace(value) ? DBNull.Value : value + }); + } + private static void AddDecimal(SqlCommand command, string name, decimal value) { var parameter = new SqlParameter(name, SqlDbType.Decimal) diff --git a/Services/SerialBoardClient.cs b/Services/SerialBoardClient.cs index 582105f..d08f44d 100644 --- a/Services/SerialBoardClient.cs +++ b/Services/SerialBoardClient.cs @@ -34,6 +34,21 @@ public sealed class SerialBoardClient : IDisposable return Task.Run(() => SendCommand(command, timeoutMilliseconds, waitForEndToken)); } + public Task SendOnlyAsync(string command) + { + return Task.Run(() => + { + if (string.IsNullOrWhiteSpace(command)) + { + return; + } + + Open(); + _serialPort.DiscardInBuffer(); + _serialPort.Write(command.Trim() + "\r\n"); + }); + } + private static SerialPort CreateSerialPort(string portName, BoardHardwareSettings settings) { return new SerialPort(portName, settings.BaudRate) @@ -42,8 +57,8 @@ public sealed class SerialBoardClient : IDisposable NewLine = "\r\n", ReadTimeout = settings.ReadTimeout, WriteTimeout = settings.ReadTimeout, - DtrEnable = true, - RtsEnable = true + DtrEnable = settings.DtrEnable, + RtsEnable = settings.RtsEnable }; } diff --git a/Services/SerialScpiClient.cs b/Services/SerialScpiClient.cs index 244ee0b..9797f0e 100644 --- a/Services/SerialScpiClient.cs +++ b/Services/SerialScpiClient.cs @@ -7,13 +7,20 @@ namespace Housing.Services; public sealed class SerialScpiClient : IScpiClient { private readonly SerialPort _serialPort; + private readonly string _commandTerminator; public string PortName => _serialPort.PortName; - public SerialScpiClient(string portName, int baudRate, int timeoutMilliseconds, IEnumerable? idnMatches = null) + public SerialScpiClient( + string portName, + int baudRate, + int timeoutMilliseconds, + IEnumerable? idnMatches = null, + string commandTerminator = "\n") { var resolvedPortName = ResolvePortName(portName, baudRate, timeoutMilliseconds, idnMatches); + _commandTerminator = NormalizeTerminator(commandTerminator); _serialPort = CreateSerialPort(resolvedPortName, baudRate, timeoutMilliseconds); _serialPort.Open(); _serialPort.DiscardInBuffer(); @@ -22,7 +29,7 @@ public sealed class SerialScpiClient : IScpiClient public Task SendAsync(string command) { - return Task.Run(() => _serialPort.Write(command.Trim() + "\n")); + return Task.Run(() => _serialPort.Write(command.Trim() + _commandTerminator)); } public Task QueryAsync(string command) @@ -30,11 +37,51 @@ public sealed class SerialScpiClient : IScpiClient return Task.Run(() => { _serialPort.DiscardInBuffer(); - _serialPort.Write(command.Trim() + "\n"); - return _serialPort.ReadLine().Trim(); + _serialPort.Write(command.Trim() + _commandTerminator); + return ReadResponse(command); }); } + private string ReadResponse(string command) + { + var response = new List(); + + while (true) + { + try + { + var value = _serialPort.ReadByte(); + if (value < 0) + { + break; + } + + if (value is '\r' or '\n') + { + if (response.Count > 0) + { + break; + } + + continue; + } + + response.Add((byte)value); + } + catch (TimeoutException) + { + if (response.Count > 0) + { + break; + } + + throw new TimeoutException($"{_serialPort.PortName} did not respond to {command}."); + } + } + + return Encoding.ASCII.GetString(response.ToArray()).Trim(); + } + private static SerialPort CreateSerialPort(string portName, int baudRate, int timeoutMilliseconds) { return new SerialPort(portName, baudRate) @@ -62,11 +109,11 @@ public sealed class SerialScpiClient : IScpiClient if (portNames.Length == 0) { - throw new InvalidOperationException("사용 가능한 장비 COM 포트를 찾을 수 없습니다."); + throw new InvalidOperationException("No available SCPI COM port."); } var tokens = NormalizeIdnMatches(idnMatches); - if (tokens.Length == 0 && portNames.Length == 1) + if (portNames.Length == 1) { return portNames[0]; } @@ -79,7 +126,7 @@ public sealed class SerialScpiClient : IScpiClient } } - throw new InvalidOperationException($"SCPI 장비 자동 포트 검색 실패. 확인한 포트: {string.Join(", ", portNames)}"); + throw new InvalidOperationException($"SCPI device auto port search failed. Checked ports: {string.Join(", ", portNames)}"); } private static bool IsAutoPort(string portName) @@ -113,9 +160,26 @@ public sealed class SerialScpiClient : IScpiClient probe.Open(); probe.DiscardInBuffer(); probe.DiscardOutBuffer(); - probe.Write("*IDN?\n"); - var idn = probe.ReadLine().Trim(); + probe.Write("*IDN?\r\n"); + + var response = new List(); + while (true) + { + var value = probe.ReadByte(); + if (value < 0 || value is '\r' or '\n') + { + if (response.Count > 0) + { + break; + } + + continue; + } + + response.Add((byte)value); + } + var idn = Encoding.ASCII.GetString(response.ToArray()).Trim(); return idnMatches.Length == 0 || idnMatches.Any(token => idn.Contains(token, StringComparison.OrdinalIgnoreCase)); } @@ -125,6 +189,20 @@ public sealed class SerialScpiClient : IScpiClient } } + private static string NormalizeTerminator(string value) + { + return value.Trim().ToUpperInvariant() switch + { + "CRLF" => "\r\n", + "CR" => "\r", + "LF" => "\n", + "\\R\\N" => "\r\n", + "\\R" => "\r", + "\\N" => "\n", + _ => string.IsNullOrEmpty(value) ? "\n" : value + }; + } + public void Dispose() { _serialPort.Dispose();