You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

291 lines
9.9 KiB

2 months ago
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;
}
2 months ago
public async Task<string> 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<string> 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();
}
2 months ago
public async Task<BoardMeasurementResult> ReadAsync()
{
var log = new StringBuilder();
2 months ago
IScpiClient? currentClient = null;
2 months ago
2 months ago
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
2 months ago
{
2 months ago
await SendCleanupCommandAsync(currentClient, _settings.Current, log);
currentClient?.Dispose();
2 months ago
}
2 months ago
}
2 months ago
2 months ago
private static async Task SendCleanupCommandAsync(IScpiClient? client, ScpiChannelSettings channel, StringBuilder log)
{
if (client is null || string.IsNullOrWhiteSpace(channel.CleanupCommand))
{
return;
}
2 months ago
2 months ago
try
2 months ago
{
2 months ago
log.AppendLine($"> CURRENT CLEANUP: {channel.CleanupCommand}");
await SendCommandListAsync(client, channel.CleanupCommand);
}
catch (Exception ex)
{
log.AppendLine($"> CURRENT CLEANUP FAILED: {ex.Message}");
}
2 months ago
}
2 months ago
private async Task<decimal> ReadChannelAsync(
string label,
ScpiChannelSettings channel,
StringBuilder log,
bool skipSetupCommand = false)
2 months ago
{
2 months ago
if (IsDisabled(channel))
{
log.AppendLine($"> {label} SKIP: disabled in Hardware.ini");
return 0m;
}
2 months ago
if (string.IsNullOrWhiteSpace(channel.ReadCommand))
{
throw new InvalidOperationException($"Hardware.ini [Equipment] {label} ReadCommand 값을 확인하세요.");
}
using var client = await CreateClientAsync(label, channel);
2 months ago
return await ReadChannelWithClientAsync(label, channel, client, log, skipSetupCommand);
}
private static async Task<decimal> 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 값을 확인하세요.");
}
2 months ago
2 months ago
log.AppendLine($"> {label} CONNECT: {DescribeClient(client, channel)}");
if (!skipSetupCommand && !string.IsNullOrWhiteSpace(channel.SetupCommand))
2 months ago
{
log.AppendLine($"> {label} SETUP: {channel.SetupCommand}");
2 months ago
await SendCommandListAsync(client, channel.SetupCommand);
2 months ago
}
log.AppendLine($"> {label} READ: {channel.ReadCommand}");
var response = await client.QueryAsync(channel.ReadCommand);
log.AppendLine(response);
return ExtractFirstDecimal(response, label);
}
private Task<IScpiClient> CreateClientAsync(string label, ScpiChannelSettings channel)
{
var connection = GetConnectionType(channel);
return connection switch
{
"TCP" or "LAN" or "ETHERNET" => CreateTcpClientAsync(label, channel),
2 months ago
"SERIAL" or "COM" => Task.FromResult<IScpiClient>(new SerialScpiClient(channel.PortName, channel.BaudRate, _settings.Timeout, channel.GetIdnMatches(), channel.GetTerminator())),
2 months ago
"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.Host))
2 months ago
{
return "TCP";
2 months ago
}
if (!string.IsNullOrWhiteSpace(channel.ResourceName))
2 months ago
{
return "VISA";
}
if (!string.IsNullOrWhiteSpace(channel.PortName))
{
return "SERIAL";
}
if (!string.IsNullOrWhiteSpace(channel.IdnMatch))
2 months ago
{
return "VISA";
2 months ago
}
return string.Empty;
}
private async Task<IScpiClient> CreateTcpClientAsync(string label, ScpiChannelSettings channel)
{
if (string.IsNullOrWhiteSpace(channel.Host))
{
throw new InvalidOperationException($"Hardware.ini [Equipment] {label} Host 값을 확인하세요.");
}
var client = await TcpScpiClient.ConnectAsync(channel.Host, channel.Port, _settings.Timeout);
try
{
await ValidateTcpIdnAsync(label, channel, client);
return client;
}
catch
{
client.Dispose();
throw;
}
}
private static async Task ValidateTcpIdnAsync(string label, ScpiChannelSettings channel, IScpiClient client)
{
var idnMatches = channel.GetIdnMatches();
if (idnMatches.Length == 0)
{
return;
}
var idn = await client.QueryAsync("*IDN?");
var isMatch = idnMatches.Any(token => idn.Contains(token, StringComparison.OrdinalIgnoreCase));
if (!isMatch)
{
throw new InvalidOperationException($"{label} LAN IDN mismatch. Expected: {string.Join(", ", idnMatches)} / Actual: {idn}");
}
2 months ago
}
private async Task<IScpiClient> 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);
}
2 months ago
private static async Task SendCommandListAsync(IScpiClient client, string commands)
{
foreach (var command in SplitCommands(commands))
{
2 weeks ago
if (IsProtectedPowerOffCommand(command))
{
continue;
}
2 months ago
await client.SendAsync(command);
await Task.Delay(100);
}
}
private static IEnumerable<string> SplitCommands(string commands)
{
return commands
.Split(';')
.Select(command => command.Trim())
.Where(command => !string.IsNullOrWhiteSpace(command));
}
2 weeks ago
private static bool IsProtectedPowerOffCommand(string command)
{
return Regex.IsMatch(command, @"^\s*OUTP(?:UT)?\s+OFF\s*,?\s*\(@2\)\s*$", RegexOptions.IgnoreCase);
}
2 months ago
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);
}
2 months ago
}