- refactor: DabitChe.Mcp → Dabit.Mcp (namespace, csproj, exe 모두) - fix: display_message colors를 쉼표 구분 문자열로 변경 (ModelContextProtocol 1.3.0 배열 바인딩 버그 우회) - docs: scroll 4방향(left/right/up/down) 실기 검증 완료 (DIBD600T V3.6.1) 20 도구 (읽기 4 + 콘텐츠 2 + 설정 14), stdio 전송, Windows exe. 실기 검증: scroll/fill_screen 8색/display_message 글자별색 모두 정상. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
157 lines
6.9 KiB
C#
157 lines
6.9 KiB
C#
using Dabit.Mcp.Config;
|
|
using DibdProtocol.Protocol;
|
|
using DibdProtocol.Transport;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Dabit.Mcp.Services;
|
|
|
|
/// <summary>
|
|
/// 전광판과의 송수신을 담당하는 얇은 서비스.
|
|
/// 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).
|
|
/// </summary>
|
|
public sealed class LedControlService : IAsyncDisposable
|
|
{
|
|
private readonly TransportRegistry _registry;
|
|
private readonly McpConnectionSettings _settings;
|
|
private readonly ILogger<LedControlService> _log;
|
|
private readonly SemaphoreSlim _gate = new(1, 1);
|
|
|
|
public LedControlService(TransportRegistry registry, McpConnectionSettings settings, ILogger<LedControlService> log)
|
|
{
|
|
_registry = registry;
|
|
_settings = settings;
|
|
_log = log;
|
|
}
|
|
|
|
public McpConnectionSettings Settings => _settings;
|
|
|
|
private async Task<ITransport> 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);
|
|
|
|
/// <summary>
|
|
/// HEX 쓰기 명령 응답의 status를 판정한다(정본 MessageService.DescribeHexResponse 기준).
|
|
/// SetScreenSize(0x40)는 Data[0]=0xF1 echo가 정상, 나머지 쓰기는 Data[0]=0x00이 성공.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>HEX 명령 송신 후 응답 파싱. 연결/통신 실패는 null로 정규화한다.</summary>
|
|
public async Task<HexParsedResponse?> 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(); }
|
|
}
|
|
|
|
/// <summary>ASCII 명령 송신 후 응답 파싱. 연결/통신 실패는 null로 정규화한다.</summary>
|
|
public async Task<AsciiParsedResponse?> 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(); }
|
|
}
|
|
|
|
/// <summary>ASCII 혼합(속성 EUC-KR + 텍스트 raw bytes) 송신. 연결/통신 실패는 null.</summary>
|
|
public async Task<AsciiParsedResponse?> 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();
|
|
}
|
|
}
|