using System.Globalization; using System.Text; using System.Text.RegularExpressions; using Housing.Models; namespace Housing.Services; public sealed class EquipmentMeasurementService { private readonly EquipmentMeasurementSettings _settings; public EquipmentMeasurementService(EquipmentMeasurementSettings settings) { _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; 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 SendCleanupCommandAsync(currentClient, _settings.Current, log); currentClient?.Dispose(); } } 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}"); } } 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); } 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); } private Task CreateClientAsync(string label, ScpiChannelSettings channel) { var connection = GetConnectionType(channel); return connection switch { "TCP" or "LAN" => 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 중 하나로 설정하세요.") }; } private static string GetConnectionType(ScpiChannelSettings channel) { if (!string.IsNullOrWhiteSpace(channel.Connection)) { return channel.Connection.Trim().ToUpperInvariant(); } if (!string.IsNullOrWhiteSpace(channel.ResourceName)) { return "VISA"; } if (!string.IsNullOrWhiteSpace(channel.IdnMatch)) { return "VISA"; } if (!string.IsNullOrWhiteSpace(channel.PortName)) { return "SERIAL"; } if (!string.IsNullOrWhiteSpace(channel.Host)) { return "TCP"; } return string.Empty; } private async Task CreateTcpClientAsync(string label, ScpiChannelSettings channel) { if (string.IsNullOrWhiteSpace(channel.Host)) { throw new InvalidOperationException($"Hardware.ini [Equipment] {label} Host 값을 확인하세요."); } return await TcpScpiClient.ConnectAsync(channel.Host, channel.Port, _settings.Timeout); } private async Task CreateVisaClientAsync(ScpiChannelSettings channel) { return await VisaScpiClient.OpenMatchingAsync(channel.ResourceName, channel.GetIdnMatches(), _settings.Timeout); } private static string DescribeClient(IScpiClient client, ScpiChannelSettings channel) { return client switch { SerialScpiClient serial => $"Serial {serial.PortName} @ {channel.BaudRate}", VisaScpiClient visa => $"VISA {visa.ResourceName}", TcpScpiClient => $"TCP {channel.Host}:{channel.Port}", _ => channel.Connection }; } 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); } 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); } }