|
|
|
|
using System.IO.Ports;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
|
|
|
|
|
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<string>? idnMatches = null,
|
|
|
|
|
string commandTerminator = "\n")
|
|
|
|
|
{
|
|
|
|
|
var resolvedPortName = ResolvePortName(portName, baudRate, timeoutMilliseconds, idnMatches);
|
|
|
|
|
|
|
|
|
|
_commandTerminator = NormalizeTerminator(commandTerminator);
|
|
|
|
|
_serialPort = CreateSerialPort(resolvedPortName, baudRate, timeoutMilliseconds);
|
|
|
|
|
_serialPort.Open();
|
|
|
|
|
_serialPort.DiscardInBuffer();
|
|
|
|
|
_serialPort.DiscardOutBuffer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task SendAsync(string command)
|
|
|
|
|
{
|
|
|
|
|
return Task.Run(() => _serialPort.Write(command.Trim() + _commandTerminator));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task<string> QueryAsync(string command)
|
|
|
|
|
{
|
|
|
|
|
return Task.Run(() =>
|
|
|
|
|
{
|
|
|
|
|
_serialPort.DiscardInBuffer();
|
|
|
|
|
_serialPort.Write(command.Trim() + _commandTerminator);
|
|
|
|
|
return ReadResponse(command);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("No available SCPI COM port.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var tokens = NormalizeIdnMatches(idnMatches);
|
|
|
|
|
if (portNames.Length == 1)
|
|
|
|
|
{
|
|
|
|
|
return portNames[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var candidate in portNames)
|
|
|
|
|
{
|
|
|
|
|
if (MatchesIdn(candidate, baudRate, timeoutMilliseconds, tokens))
|
|
|
|
|
{
|
|
|
|
|
return candidate;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new InvalidOperationException($"SCPI device auto port search failed. Checked ports: {string.Join(", ", portNames)}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var idn = Encoding.ASCII.GetString(response.ToArray()).Trim();
|
|
|
|
|
return idnMatches.Length == 0 ||
|
|
|
|
|
idnMatches.Any(token => idn.Contains(token, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|