using System.IO.Ports; using System.Text; using System.Text.RegularExpressions; namespace Housing.Services; public sealed class SerialBoardClient : IDisposable { private readonly BoardHardwareSettings _settings; private readonly SerialPort _serialPort; public string PortName => _serialPort.PortName; public SerialBoardClient(BoardHardwareSettings settings) { _settings = settings; var portName = ResolvePortName(settings); _serialPort = CreateSerialPort(portName, _settings); } public void Open() { if (!_serialPort.IsOpen) { _serialPort.Open(); _serialPort.DiscardInBuffer(); _serialPort.DiscardOutBuffer(); } } public Task SendCommandAsync(string command, int timeoutMilliseconds, bool waitForEndToken) { return Task.Run(() => SendCommand(command, timeoutMilliseconds, waitForEndToken)); } private static SerialPort CreateSerialPort(string portName, BoardHardwareSettings settings) { return new SerialPort(portName, settings.BaudRate) { Encoding = Encoding.ASCII, NewLine = "\r\n", ReadTimeout = settings.ReadTimeout, WriteTimeout = settings.ReadTimeout, DtrEnable = true, RtsEnable = true }; } private static string ResolvePortName(BoardHardwareSettings settings) { if (!IsAutoPort(settings.PortName)) { return settings.PortName.Trim(); } var portNames = GetOrderedPortNames(); if (portNames.Length == 0) { throw new InvalidOperationException("사용 가능한 COM 포트를 찾을 수 없습니다."); } foreach (var portName in portNames) { if (CanConnectBoard(portName, settings)) { return portName; } } throw new InvalidOperationException($"보드 자동 포트 검색 실패. 확인한 포트: {string.Join(", ", portNames)}"); } private static bool IsAutoPort(string portName) { return string.IsNullOrWhiteSpace(portName) || string.Equals(portName.Trim(), "Auto", StringComparison.OrdinalIgnoreCase); } private static string[] GetOrderedPortNames() { return SerialPort.GetPortNames() .OrderBy(GetPortNumber) .ThenBy(portName => portName, StringComparer.OrdinalIgnoreCase) .ToArray(); } 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 bool CanConnectBoard(string portName, BoardHardwareSettings settings) { try { using var probe = CreateSerialPort(portName, settings); probe.Open(); probe.DiscardInBuffer(); probe.DiscardOutBuffer(); if (string.IsNullOrWhiteSpace(settings.ConnectCommand)) { return true; } probe.Write(settings.ConnectCommand.Trim() + "\r\n"); var startedAt = DateTime.UtcNow; while ((DateTime.UtcNow - startedAt).TotalMilliseconds < settings.ReadTimeout) { try { var line = probe.ReadLine(); if (IsSuccessLine(line)) { return true; } if (!string.IsNullOrWhiteSpace(settings.EndToken) && line.Contains(settings.EndToken, StringComparison.OrdinalIgnoreCase)) { return false; } } catch (TimeoutException) { return false; } } } catch { return false; } return false; } private static bool IsSuccessLine(string line) { return string.Equals(line.Trim(), "Success", StringComparison.OrdinalIgnoreCase); } private string SendCommand(string command, int timeoutMilliseconds, bool waitForEndToken) { if (string.IsNullOrWhiteSpace(command)) { return string.Empty; } Open(); _serialPort.ReadTimeout = Math.Max(500, timeoutMilliseconds); _serialPort.DiscardInBuffer(); _serialPort.Write(command.Trim() + "\r\n"); var startedAt = DateTime.UtcNow; var response = new StringBuilder(); while ((DateTime.UtcNow - startedAt).TotalMilliseconds < timeoutMilliseconds) { try { var line = _serialPort.ReadLine(); response.AppendLine(line); if (!waitForEndToken) { break; } if (line.Contains(_settings.EndToken, StringComparison.OrdinalIgnoreCase)) { break; } } catch (TimeoutException) { break; } } return response.ToString(); } public void Dispose() { _serialPort.Dispose(); } }