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.

211 lines
5.9 KiB

2 months ago
using System.IO.Ports;
using System.Text;
using System.Text.RegularExpressions;
namespace Housing.Services;
public sealed class SerialScpiClient : IScpiClient
{
private readonly SerialPort _serialPort;
2 months ago
private readonly string _commandTerminator;
2 months ago
public string PortName => _serialPort.PortName;
2 months ago
public SerialScpiClient(
string portName,
int baudRate,
int timeoutMilliseconds,
IEnumerable<string>? idnMatches = null,
string commandTerminator = "\n")
2 months ago
{
var resolvedPortName = ResolvePortName(portName, baudRate, timeoutMilliseconds, idnMatches);
2 months ago
_commandTerminator = NormalizeTerminator(commandTerminator);
2 months ago
_serialPort = CreateSerialPort(resolvedPortName, baudRate, timeoutMilliseconds);
_serialPort.Open();
_serialPort.DiscardInBuffer();
_serialPort.DiscardOutBuffer();
}
public Task SendAsync(string command)
{
2 months ago
return Task.Run(() => _serialPort.Write(command.Trim() + _commandTerminator));
2 months ago
}
public Task<string> QueryAsync(string command)
{
return Task.Run(() =>
{
_serialPort.DiscardInBuffer();
2 months ago
_serialPort.Write(command.Trim() + _commandTerminator);
return ReadResponse(command);
2 months ago
});
}
2 months ago
private string ReadResponse(string command)
{
var response = new List<byte>();
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();
}
2 months ago
private static SerialPort CreateSerialPort(string portName, int baudRate, int timeoutMilliseconds)
{
return new SerialPort(portName, baudRate)
{
Encoding = Encoding.ASCII,
NewLine = "\n",
ReadTimeout = timeoutMilliseconds,
WriteTimeout = timeoutMilliseconds,
DtrEnable = true,
RtsEnable = true
};
}
private static string ResolvePortName(string portName, int baudRate, int timeoutMilliseconds, IEnumerable<string>? idnMatches)
{
if (!IsAutoPort(portName))
{
return portName.Trim();
}
var portNames = SerialPort.GetPortNames()
.OrderBy(GetPortNumber)
.ThenBy(candidate => candidate, StringComparer.OrdinalIgnoreCase)
.ToArray();
if (portNames.Length == 0)
{
2 months ago
throw new InvalidOperationException("No available SCPI COM port.");
2 months ago
}
var tokens = NormalizeIdnMatches(idnMatches);
2 months ago
if (portNames.Length == 1)
2 months ago
{
return portNames[0];
}
foreach (var candidate in portNames)
{
if (MatchesIdn(candidate, baudRate, timeoutMilliseconds, tokens))
{
return candidate;
}
}
2 months ago
throw new InvalidOperationException($"SCPI device auto port search failed. Checked ports: {string.Join(", ", portNames)}");
2 months ago
}
private static bool IsAutoPort(string portName)
{
return string.IsNullOrWhiteSpace(portName) ||
string.Equals(portName.Trim(), "Auto", StringComparison.OrdinalIgnoreCase);
}
private static int GetPortNumber(string portName)
{
var match = Regex.Match(portName, @"\d+");
return match.Success && int.TryParse(match.Value, out var number)
? number
: int.MaxValue;
}
private static string[] NormalizeIdnMatches(IEnumerable<string>? idnMatches)
{
return idnMatches?
.SelectMany(value => value.Split(',', ';'))
.Select(value => value.Trim())
.Where(value => !string.IsNullOrWhiteSpace(value))
.ToArray() ?? Array.Empty<string>();
}
private static bool MatchesIdn(string portName, int baudRate, int timeoutMilliseconds, string[] idnMatches)
{
try
{
using var probe = CreateSerialPort(portName, baudRate, timeoutMilliseconds);
probe.Open();
probe.DiscardInBuffer();
probe.DiscardOutBuffer();
2 months ago
probe.Write("*IDN?\r\n");
var response = new List<byte>();
while (true)
{
var value = probe.ReadByte();
if (value < 0 || value is '\r' or '\n')
{
if (response.Count > 0)
{
break;
}
continue;
}
response.Add((byte)value);
}
2 months ago
2 months ago
var idn = Encoding.ASCII.GetString(response.ToArray()).Trim();
2 months ago
return idnMatches.Length == 0 ||
idnMatches.Any(token => idn.Contains(token, StringComparison.OrdinalIgnoreCase));
}
catch
{
return false;
}
}
2 months ago
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
};
}
2 months ago
public void Dispose()
{
_serialPort.Dispose();
}
}