using System.ComponentModel;
using System.Text;
using Dabit.Mcp.Services;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
namespace Dabit.Mcp.Tools;
///
/// 전광판 표시를 바꾸는 도구. ⚠️ 쓰기 — 실제 장비의 표시 내용을 변경한다.
///
[McpServerToolType]
public sealed class DisplayControlTools
{
private readonly LedControlService _led;
private readonly ILogger _log;
public DisplayControlTools(LedControlService led, ILogger log)
{
_led = led;
_log = log;
}
/// color 문자열 → ASCII /C 색상 코드(0~7). 8색 고정(정본 ascii-reference).
private static readonly Dictionary ColorCodes = new(StringComparer.OrdinalIgnoreCase)
{
["black"] = "0", ["red"] = "1", ["green"] = "2", ["yellow"] = "3",
["blue"] = "4", ["magenta"] = "5", ["cyan"] = "6", ["white"] = "7",
};
///
/// effect 문자열 → /E 효과 코드(2자리). 입장·퇴장에 각각 적용한다(/E{입장}{퇴장}).
/// stop(01)·scroll 4방향(06~09) 모두 실기 검증됨(2026-06-02, DIBD600T 펌웨어 V3.6.1, 육안 확인).
///
private static readonly Dictionary EffectCodes = new(StringComparer.OrdinalIgnoreCase)
{
["stop"] = "01", // 정지 — 검증
["scroll_left"] = "06", // 좌이동(오른쪽→왼쪽) — 검증
["scroll_right"] = "07", // 우이동(왼쪽→오른쪽) — 검증(2026-06-02)
["scroll_up"] = "08", // 상향(아래→위) — 검증(2026-06-02)
["scroll_down"] = "09", // 하향(위→아래) — 검증(2026-06-02)
};
private const int MaxTextLength = 100;
[McpServerTool]
[Description("전광판에 단색 텍스트 한 줄을 즉시 표시한다(실시간 메시지, 정지). " +
"⚠️ 쓰기 동작 — 현재 표시 중인 실시간 메시지를 즉시 덮어쓴다. " +
"지원 색상 8종: red, green, yellow, blue, magenta, cyan, white, black (그 외는 노랑으로 근사). " +
"글자별 색이나 이동 효과가 필요하면 display_message를 쓴다.")]
public async Task DisplayText(
[Description("표시할 텍스트(한글 가능, 최대 100자). 예: \"환영합니다\"")] string text,
[Description("문자 색상. red/green/yellow/blue/magenta/cyan/white/black 중 하나. 기본 yellow")] string color = "yellow",
CancellationToken ct = default)
{
if (string.IsNullOrEmpty(text))
return "표시할 텍스트가 비어 있습니다.";
if (!ValidateText(text, out var err))
return err;
if (!ColorCodes.TryGetValue(color.Trim(), out var code))
{
_log.LogWarning("지원하지 않는 색상 '{Color}' → 노랑(yellow)으로 근사", color);
code = "3";
}
// 실시간 메시지(msgType "0") + 색상/정지(즉시)/속도유지/EUC-KR 16x16. (실기 검증된 형식)
var data = $"/C{code}/E0101/S2002/F0003{text}";
_log.LogInformation("display_text: \"{Text}\" color={Color}(/C{Code})", text, color, code);
return Format(await _led.SendAsciiAsync("0", data, ct), $"\"{text}\" ({color})");
}
[McpServerTool]
[Description("전광판에 텍스트를 글자별 색상·입장/퇴장 이동효과·속도와 함께 표시한다(실시간 메시지). " +
"⚠️ 쓰기 동작 — 현재 표시 중인 실시간 메시지를 즉시 덮어쓴다. " +
"colors: 글자별 색을 쉼표로 구분(text 글자 수와 1:1, 예 \"red,green,yellow,blue\", 생략 시 전체 노랑). " +
"effect=입장 효과, exit_effect=퇴장 효과: stop/scroll_left/scroll_right/scroll_up/scroll_down. " +
"연속으로 흐르게 하려면 입장·퇴장을 같은 scroll로 준다(예: 둘 다 scroll_left). " +
"speed 0~99(작을수록 빠름, 0=최고속). hold_sec 유지 시간(초). " +
"scroll 4방향(left/right/up/down) 모두 실기 검증 완료(2026-06-02, DIBD600T V3.6.1).")]
public async Task DisplayMessage(
[Description("표시할 텍스트(한글 가능). 예: \"가나다라\"")] string text,
[Description("글자별 색을 쉼표로 구분한 문자열, text 글자와 1:1 대응. 예: \"red,green,yellow,blue\". 생략 시 전체 노랑")] string? colors = null,
[Description("입장 효과: stop/scroll_left/scroll_right/scroll_up/scroll_down. 기본 stop")] string effect = "stop",
[Description("퇴장 효과: 위와 동일 목록. 입장과 같은 scroll을 주면 멈춤 없이 연속으로 흐른다. 기본 stop")] string exitEffect = "stop",
[Description("이동 속도 0~99(작을수록 빠름, 0=최고속). 기본 20")] int speed = 20,
[Description("유지 시간(초), 0=대기 없음. 기본 1")] int holdSec = 1,
CancellationToken ct = default)
{
if (string.IsNullOrEmpty(text))
return "표시할 텍스트가 비어 있습니다.";
if (!ValidateText(text, out var err))
return err;
var enter = ResolveEffect(effect);
var exit = ResolveEffect(exitEffect);
var sp = Math.Clamp(speed, 0, 99);
var hold2 = Math.Clamp(holdSec * 2, 0, 99); // 유지시간은 0.5초 단위(1초=02)
// colors는 쉼표 구분 문자열("red,green,..."). ModelContextProtocol 1.3.0이 string[]? 파라미터의
// JSON 스키마를 깨뜨려(type 누락) 배열 인자 바인딩 시 호출 자체가 실패하므로, 문자열로 받아 직접 분리한다.
var colorArr = string.IsNullOrWhiteSpace(colors)
? null
: colors.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
// 속성(입장/퇴장 효과·속도·폰트) 먼저, 그 뒤에 글자별 색을 인라인으로 붙인다.
var sb = new StringBuilder($"/E{enter}{exit}/S{sp:D2}{hold2:D2}/F0003");
for (int i = 0; i < text.Length; i++)
{
var code = "3"; // 기본 노랑
var want = colorArr is not null && i < colorArr.Length ? colorArr[i] : null;
if (want is not null && ColorCodes.TryGetValue(want.Trim(), out var cc))
code = cc;
else if (want is not null)
_log.LogWarning("지원하지 않는 색상 '{Color}'(글자 {Idx}) → 노랑", want, i);
sb.Append($"/C{code}{text[i]}");
}
var data = sb.ToString();
_log.LogInformation("display_message: 입장={Enter} 퇴장={Exit} speed={Speed} hold={Hold}s data=\"{Data}\"", effect, exitEffect, sp, holdSec, data);
return Format(await _led.SendAsciiAsync("0", data, ct), $"\"{text}\" (입장 {effect}, 퇴장 {exitEffect}, 속도 {sp}, 유지 {holdSec}s)");
}
/// effect 문자열 → /E 코드. 모르는 값은 정지(01)로.
private string ResolveEffect(string name)
{
if (EffectCodes.TryGetValue(name.Trim(), out var c))
return c;
_log.LogWarning("알 수 없는 효과 '{Effect}' → stop", name);
return "01";
}
/// 본문에 ASCII 속성/프레임 제어코드가 섞이면 표시가 깨지거나 속성이 주입된다. 차단.
private bool ValidateText(string text, out string error)
{
if (text.Contains('/') || text.Contains("![") || text.Contains("!]"))
{
error = "텍스트에 제어 문자('/', '![', '!]')는 사용할 수 없습니다 — 속성 코드로 해석되어 표시가 깨집니다.";
return false;
}
if (text.Length > MaxTextLength)
{
error = $"텍스트가 너무 깁니다(최대 {MaxTextLength}자, 입력 {text.Length}자).";
return false;
}
error = "";
return true;
}
/// 응답을 사용자 메시지로 변환. 성공 판정은 Result=="0" allowlist.
private static string Format(DibdProtocol.Protocol.AsciiParsedResponse? resp, string what)
{
if (resp is null)
return "전송 실패 — 연결 또는 통신 오류 (전광판 응답 없음).";
return resp.Result switch
{
"0" => $"표시 완료: {what}.",
"F" => "전광판이 명령을 거부했습니다 (Result=F).",
_ => $"전송했으나 응답 형식이 예상과 다릅니다 (Result=\"{resp.Result}\") — 실기 확인이 필요합니다.",
};
}
}