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 ReadAsync() { var log = new StringBuilder(); if (_settings.SettleMilliseconds > 0) { await Task.Delay(_settings.SettleMilliseconds); } var voltage = await ReadChannelAsync("VOLTAGE", _settings.Voltage, log); var current = await ReadChannelAsync("CURRENT", _settings.Current, log); return new BoardMeasurementResult { Voltage = voltage, Current = current, RawLog = log.ToString() }; } private async Task ReadChannelAsync(string label, ScpiChannelSettings channel, StringBuilder log) { 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)}"); if (!string.IsNullOrWhiteSpace(channel.SetupCommand)) { log.AppendLine($"> {label} SETUP: {channel.SetupCommand}"); await client.SendAsync(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())), "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); } }