diff --git a/.vs/Housing/DesignTimeBuild/.dtbcache.v2 b/.vs/Housing/DesignTimeBuild/.dtbcache.v2 new file mode 100644 index 0000000..1e21214 Binary files /dev/null and b/.vs/Housing/DesignTimeBuild/.dtbcache.v2 differ diff --git a/Hardware.ini b/Hardware.ini index f79dff4..e0f0927 100644 --- a/Hardware.ini +++ b/Hardware.ini @@ -1,11 +1,14 @@ [Board] -PortName=Auto +PortName=COM5 BaudRate=115200 ReadTimeout=5000 +PreConnectCommand=x00o ConnectCommand=x00c_001101:owt28006727ea97c7801 ReadIdCommand=x00c_001101:ow2800326003e -CalDefaultCommand=x00c_001001 +CalDefaultCommand=x00c_001001:owt28006727ea97c7801 EndToken= +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();