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? 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 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(); 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? 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? idnMatches) { return idnMatches? .SelectMany(value => value.Split(',', ';')) .Select(value => value.Trim()) .Where(value => !string.IsNullOrWhiteSpace(value)) .ToArray() ?? Array.Empty(); } 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(); 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(); } }