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();
}
}