using Dabit.Mcp.Config; using DibdProtocol.Protocol; using DibdProtocol.Transport; using Microsoft.Extensions.Logging; namespace Dabit.Mcp.Services; /// /// 전광판과의 송수신을 담당하는 얇은 서비스. /// DabitChe.AutoSend.Services.MessageService를 축약했다(WPF 의존성 제거). /// PoC 폐기성 코드 — Phase 2에서 DibdProtocol.Transport.Rpc.DibdRpcClient(96 method)로 대체 예정. /// /// 동시성: MCP 서버는 요청을 직렬화하지 않으므로 _gate(SemaphoreSlim)로 명령을 한 번에 하나씩 처리한다. /// 연결: KeepOpen=true로 프로세스 수명 동안 포트를 유지하고 DisposeAsync에서 1회만 닫는다. /// timeout: FlashWrite류 명령(시간/보드/화면크기/페이지 설정 등)은 응답이 느리므로 호출자가 /// timeoutMsOverride로 5초 등을 지정한다(기본은 설정값 ResponseLatencyMs). /// public sealed class LedControlService : IAsyncDisposable { private readonly TransportRegistry _registry; private readonly McpConnectionSettings _settings; private readonly ILogger _log; private readonly SemaphoreSlim _gate = new(1, 1); public LedControlService(TransportRegistry registry, McpConnectionSettings settings, ILogger log) { _registry = registry; _settings = settings; _log = log; } public McpConnectionSettings Settings => _settings; private async Task EnsureConnectedAsync(CancellationToken ct) { _registry.CurrentType = _settings.ConnectType; var transport = _registry.GetCurrent(); if (transport.IsConnected) return transport; var info = new ConnectionInfo { PortName = _settings.PortName, BaudRate = _settings.BaudRate, Host = _settings.Host, Port = _settings.Port, ResponseLatencyMs = _settings.ResponseLatencyMs, KeepOpen = true, }; _log.LogDebug("연결 시도: {Conn}", _settings.Describe()); await transport.ConnectAsync(info, ct); return transport; } private TimeSpan Timeout(int? overrideMs) => TimeSpan.FromMilliseconds(overrideMs ?? _settings.ResponseLatencyMs); /// /// HEX 쓰기 명령 응답의 status를 판정한다(정본 MessageService.DescribeHexResponse 기준). /// SetScreenSize(0x40)는 Data[0]=0xF1 echo가 정상, 나머지 쓰기는 Data[0]=0x00이 성공. /// public static (bool Ok, string Message) CheckWriteStatus(HexCommand cmd, HexParsedResponse? resp) { if (resp is null) return (false, "응답 없음 — 연결 또는 통신 오류."); if (resp.Data.Length == 0) return (false, "빈 응답."); var b = resp.Data[0]; var ok = cmd == HexCommand.SetScreenSize ? b == 0xF1 : b == 0x00; if (ok) return (true, "성공"); var reason = b switch { 0x10 => "명령 없음(No Command)", 0x20 => "기능 없음(No Function)", 0x40 => "데이터 오류(Data Error)", 0x80 => "알 수 없는 오류", _ => $"상태 0x{b:X2}", }; return (false, reason); } /// HEX 명령 송신 후 응답 파싱. 연결/통신 실패는 null로 정규화한다. public async Task SendHexAsync(HexCommand cmd, byte[] data, CancellationToken ct = default, int? timeoutMsOverride = null) { await _gate.WaitAsync(ct); try { var transport = await EnsureConnectedAsync(ct); var packet = PacketBuilder.BuildHex(cmd, data); _log.LogInformation("TX HEX cmd=0x{Cmd:X2} ({Len}B)", (byte)cmd, data.Length); var response = await transport.SendAndReceiveAsync(packet, Timeout(timeoutMsOverride), ct); var parsed = PacketParser.ParseHex(response); _log.LogInformation("RX HEX {Result}", parsed is null ? "파싱 실패" : $"cmd=0x{(byte)parsed.Cmd:X2} {parsed.Data.Length}B"); return parsed; } catch (Exception ex) when (ex is not OperationCanceledException) { _log.LogError(ex, "HEX 전송 실패 cmd=0x{Cmd:X2}", (byte)cmd); return null; } finally { _gate.Release(); } } /// ASCII 명령 송신 후 응답 파싱. 연결/통신 실패는 null로 정규화한다. public async Task SendAsciiAsync(string msgType, string data, CancellationToken ct = default, int? timeoutMsOverride = null) { await _gate.WaitAsync(ct); try { var transport = await EnsureConnectedAsync(ct); var packet = PacketBuilder.BuildAscii(msgType, data); _log.LogInformation("TX ASCII msgType={MsgType} data=\"{Data}\"", msgType, data); var response = await transport.SendAndReceiveAsync(packet, Timeout(timeoutMsOverride), ct); var parsed = PacketParser.ParseAscii(response); _log.LogInformation("RX ASCII {Result}", parsed is null ? "파싱 실패" : $"result={parsed.Result} data=\"{parsed.Data}\""); return parsed; } catch (Exception ex) when (ex is not OperationCanceledException) { _log.LogError(ex, "ASCII 전송 실패 msgType={MsgType}", msgType); return null; } finally { _gate.Release(); } } /// ASCII 혼합(속성 EUC-KR + 텍스트 raw bytes) 송신. 연결/통신 실패는 null. public async Task SendAsciiMixedAsync(string msgType, string attributes, byte[] textBytes, CancellationToken ct = default, int? timeoutMsOverride = null) { await _gate.WaitAsync(ct); try { var transport = await EnsureConnectedAsync(ct); var packet = PacketBuilder.BuildAsciiMixed(msgType, attributes, textBytes); _log.LogInformation("TX ASCII-mixed msgType={MsgType} attrs=\"{Attrs}\" text={Len}B", msgType, attributes, textBytes.Length); var response = await transport.SendAndReceiveAsync(packet, Timeout(timeoutMsOverride), ct); var parsed = PacketParser.ParseAscii(response); _log.LogInformation("RX ASCII {Result}", parsed is null ? "파싱 실패" : $"result={parsed.Result} data=\"{parsed.Data}\""); return parsed; } catch (Exception ex) when (ex is not OperationCanceledException) { _log.LogError(ex, "ASCII-mixed 전송 실패 msgType={MsgType}", msgType); return null; } finally { _gate.Release(); } } public async ValueTask DisposeAsync() { try { var transport = _registry.GetCurrent(); if (transport.IsConnected) await transport.DisconnectAsync(); } catch { /* 종료 시 닫기 실패 무시 */ } _gate.Dispose(); } }