feat(mcp): Dabit.Mcp 초기 공개 (리네임 + colors 수정 + scroll 검증)

- 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>
This commit is contained in:
insulee
2026-06-02 11:20:49 +09:00
parent 94dd74bd5c
commit 640c9a3380
109 changed files with 7146 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
using System.ComponentModel;
using Dabit.Mcp.Services;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
namespace Dabit.Mcp.Tools;
/// <summary>보드기능설정 읽기/저장 도구(디버그·포트기능·통신속도). ⚠️ 저장은 쓰기.</summary>
[McpServerToolType]
public sealed class BoardTools
{
private readonly LedControlService _led;
private readonly ILogger<BoardTools> _log;
public BoardTools(LedControlService led, ILogger<BoardTools> log)
{
_led = led;
_log = log;
}
[McpServerTool]
[Description("보드기능설정을 읽는다(디버그/BH1·J4 기능/J2·J3·BH1 통신속도 인덱스). 읽기 — 표시 변경 없음.")]
public async Task<string> ReadBoardSettings(CancellationToken ct = default)
{
var resp = await _led.SendAsciiAsync("B3", "0", ct, 5000);
if (resp is null)
return "보드설정 읽기 실패 — 연결 또는 통신 오류.";
var f = resp.Data.Trim().Split(',');
if (f.Length < 6)
return $"보드설정 응답 형식이 예상과 다릅니다: \"{resp.Data}\"";
var rs = f.Length >= 7 ? $", RS485주소={f[6]}" : "";
return $"보드설정 — debug={f[0]}, BH1기능={f[1]}, J4기능={f[2]}, J2속도={f[3]}, J3속도={f[4]}, BH1속도={f[5]}{rs}";
}
[McpServerTool]
[Description("보드기능설정을 저장한다. ⚠️ 쓰기·설정 저장. 범위: debug 0~15, 나머지 0~7. 잘못 설정하면 통신이 끊길 수 있으니 주의.")]
public async Task<string> SaveBoardSettings(
[Description("디버그 레벨 0~15")] int debug,
[Description("BH1 포트 기능 0~7")] int bh1Func,
[Description("J4 포트 기능 0~7")] int j4Func,
[Description("J2 통신속도 인덱스 0~7")] int j2Baud,
[Description("J3 통신속도 인덱스 0~7")] int j3Baud,
[Description("BH1 통신속도 인덱스 0~7")] int bh1Baud,
CancellationToken ct = default)
{
// 정본 B2: data 맨 앞 공백 1개 + 6필드. debug만 HEX 대문자, 나머지 10진수.
var hexDebug = Math.Clamp(debug, 0, 15).ToString("X");
var data = $" {hexDebug},{Math.Clamp(bh1Func, 0, 7)},{Math.Clamp(j4Func, 0, 7)},{Math.Clamp(j2Baud, 0, 7)},{Math.Clamp(j3Baud, 0, 7)},{Math.Clamp(bh1Baud, 0, 7)}";
var resp = await _led.SendAsciiAsync("B2", data, ct, 5000);
if (resp is null)
return "보드설정 저장 실패 — 연결 또는 통신 오류.";
return resp.Result == "F" ? "전광판이 거부했습니다 (Result=F)." : "보드설정 저장 완료.";
}
}

View File

@@ -0,0 +1,59 @@
using System.ComponentModel;
using Dabit.Mcp.Services;
using DibdProtocol.Protocol;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
namespace Dabit.Mcp.Tools;
/// <summary>밝기·전원 설정 도구. ⚠️ 쓰기.</summary>
[McpServerToolType]
public sealed class BrightnessPowerTools
{
private readonly LedControlService _led;
private readonly ILogger<BrightnessPowerTools> _log;
public BrightnessPowerTools(LedControlService led, ILogger<BrightnessPowerTools> log)
{
_led = led;
_log = log;
}
// 밝기 단계 0~4 → (표시%, HEX byte). 정본 SettingsViewModel 매핑 그대로(byte-identical).
private static readonly (int Pct, byte Hex)[] BrightnessMap =
{
(100, 0x64), (75, 0x48), (50, 0x32), (25, 0x19), (5, 0x05),
};
[McpServerTool]
[Description("전광판 밝기를 설정한다. ⚠️ 쓰기·설정 저장. level 0~4: 0=100%, 1=75%, 2=50%, 3=25%, 4=5%.")]
public async Task<string> SetBrightness(
[Description("밝기 단계 0~4 (0=100% 1=75% 2=50% 3=25% 4=5%)")] int level,
CancellationToken ct = default)
{
if (level < 0 || level > 4)
return "밝기 단계는 0~4여야 합니다 (0=100% 1=75% 2=50% 3=25% 4=5%).";
var (pct, hex) = BrightnessMap[level];
var resp = await _led.SendHexAsync(HexCommand.SetBrightness, new[] { hex }, ct, 5000);
var (ok, msg) = LedControlService.CheckWriteStatus(HexCommand.SetBrightness, resp);
return ok ? $"밝기 {pct}% 설정." : $"밝기 설정 실패 — {msg}";
}
[McpServerTool]
[Description("전광판 LED 전원을 켠다. ⚠️ 쓰기.")]
public async Task<string> LedPowerOn(CancellationToken ct = default)
{
var resp = await _led.SendHexAsync(HexCommand.LedPowerControl, new byte[] { 0x01 }, ct);
var (ok, msg) = LedControlService.CheckWriteStatus(HexCommand.LedPowerControl, resp);
return ok ? "LED 전원 켜짐." : $"전원 켜기 실패 — {msg}";
}
[McpServerTool]
[Description("전광판 LED 전원을 끈다. ⚠️ 쓰기 — 화면이 꺼진다.")]
public async Task<string> LedPowerOff(CancellationToken ct = default)
{
var resp = await _led.SendHexAsync(HexCommand.LedPowerControl, new byte[] { 0x00 }, ct);
var (ok, msg) = LedControlService.CheckWriteStatus(HexCommand.LedPowerControl, resp);
return ok ? "LED 전원 꺼짐." : $"전원 끄기 실패 — {msg}";
}
}

View File

@@ -0,0 +1,163 @@
using System.ComponentModel;
using System.Text;
using Dabit.Mcp.Services;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
namespace Dabit.Mcp.Tools;
/// <summary>
/// 전광판 표시를 바꾸는 도구. ⚠️ 쓰기 — 실제 장비의 표시 내용을 변경한다.
/// </summary>
[McpServerToolType]
public sealed class DisplayControlTools
{
private readonly LedControlService _led;
private readonly ILogger<DisplayControlTools> _log;
public DisplayControlTools(LedControlService led, ILogger<DisplayControlTools> log)
{
_led = led;
_log = log;
}
/// <summary>color 문자열 → ASCII /C 색상 코드(0~7). 8색 고정(정본 ascii-reference).</summary>
private static readonly Dictionary<string, string> ColorCodes = new(StringComparer.OrdinalIgnoreCase)
{
["black"] = "0", ["red"] = "1", ["green"] = "2", ["yellow"] = "3",
["blue"] = "4", ["magenta"] = "5", ["cyan"] = "6", ["white"] = "7",
};
/// <summary>
/// effect 문자열 → /E 효과 코드(2자리). 입장·퇴장에 각각 적용한다(/E{입장}{퇴장}).
/// stop(01)·scroll 4방향(06~09) 모두 실기 검증됨(2026-06-02, DIBD600T 펌웨어 V3.6.1, 육안 확인).
/// </summary>
private static readonly Dictionary<string, string> 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<string> 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<string> 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)");
}
/// <summary>effect 문자열 → /E 코드. 모르는 값은 정지(01)로.</summary>
private string ResolveEffect(string name)
{
if (EffectCodes.TryGetValue(name.Trim(), out var c))
return c;
_log.LogWarning("알 수 없는 효과 '{Effect}' → stop", name);
return "01";
}
/// <summary>본문에 ASCII 속성/프레임 제어코드가 섞이면 표시가 깨지거나 속성이 주입된다. 차단.</summary>
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;
}
/// <summary>응답을 사용자 메시지로 변환. 성공 판정은 Result=="0" allowlist.</summary>
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}\") — 실기 확인이 필요합니다.",
};
}
}

View File

@@ -0,0 +1,72 @@
using System.ComponentModel;
using System.Text;
using System.Text.RegularExpressions;
using Dabit.Mcp.Services;
using DibdProtocol.Protocol;
using ModelContextProtocol.Server;
namespace Dabit.Mcp.Tools;
/// <summary>
/// 전광판 상태를 읽는 도구 모음. 모두 읽기 전용 — 표시 내용을 바꾸지 않는다.
/// </summary>
[McpServerToolType]
public sealed class DisplayStatusTools
{
private readonly LedControlService _led;
public DisplayStatusTools(LedControlService led)
{
_led = led;
}
[McpServerTool]
[Description("연결된 LED 전광판의 펌웨어 정보(모델/버전/현재 화면 크기 등)를 읽는다. 표시 내용은 변경하지 않는다.")]
public async Task<string> GetFirmwareInfo(CancellationToken ct = default)
{
var resp = await _led.SendHexAsync(HexCommand.ReadBoardInfo, [0xF1], ct);
if (resp is null || resp.Data.Length == 0)
return "펌웨어 정보를 읽지 못했습니다 (응답 없음 또는 연결 실패).";
// 응답 data[0] == 0xF1 (요청 마커 에코)이면 건너뛴다 (FirmwareService 선례)
var start = (resp.Data.Length > 1 && resp.Data[0] == 0xF1) ? 1 : 0;
var text = Encoding.ASCII.GetString(resp.Data, start, resp.Data.Length - start).TrimEnd('\0', ' ');
return string.IsNullOrWhiteSpace(text) ? "펌웨어 정보가 비어 있습니다." : text;
}
[McpServerTool]
[Description("전광판의 현재 화면 크기(가로 x 세로 픽셀)를 읽는다. 펌웨어 정보 응답에서 추출한다.")]
public async Task<string> GetScreenSize(CancellationToken ct = default)
{
var info = await GetFirmwareInfo(ct);
// 펌웨어 문자열에서 "96x320" 같은 패턴 추출 (실기 검증 후 정밀화)
var m = Regex.Match(info, @"(\d+)\s*[xX]\s*(\d{2,4})");
return m.Success
? $"화면 크기: 가로 {m.Groups[1].Value} x 세로 {m.Groups[2].Value} 픽셀"
: $"화면 크기를 펌웨어 응답에서 찾지 못했습니다. 원문: {info}";
}
[McpServerTool]
[Description("전광판 컨트롤러의 현재 시각(RTC)을 읽는다.")]
public async Task<string> GetTime(CancellationToken ct = default)
{
var resp = await _led.SendHexAsync(HexCommand.ReadTime, [0x00], ct);
if (resp is null || resp.Data.Length == 0)
return "시간을 읽지 못했습니다 (응답 없음 또는 연결 실패).";
// 응답 7바이트 BCD: [yy, MM, dd, dow, HH, mm, ss] (sync_controller_time과 동일 레이아웃)
var d = resp.Data;
if (d.Length < 7)
return $"시간 응답이 예상보다 짧습니다 (raw {Convert.ToHexString(d)}).";
static int FromBcd(byte b) => (b >> 4) * 10 + (b & 0x0F);
var year = 2000 + FromBcd(d[0]);
return $"컨트롤러 시각: {year}-{FromBcd(d[1]):D2}-{FromBcd(d[2]):D2} {FromBcd(d[4]):D2}:{FromBcd(d[5]):D2}:{FromBcd(d[6]):D2}";
}
[McpServerTool]
[Description("현재 MCP 서버에 설정된 전광판 연결 정보를 반환한다. 실제 통신은 하지 않는다(연결 점검·스모크용).")]
public string GetConnectionStatus()
{
return $"전광판 연결 설정 — {_led.Settings.Describe()}";
}
}

View File

@@ -0,0 +1,62 @@
using System.ComponentModel;
using Dabit.Mcp.Services;
using DibdProtocol.Protocol;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
namespace Dabit.Mcp.Tools;
/// <summary>페이지 메시지 개수·초기화·섹션 효과 도구. ⚠️ 쓰기.</summary>
[McpServerToolType]
public sealed class PageTools
{
private readonly LedControlService _led;
private readonly ILogger<PageTools> _log;
public PageTools(LedControlService led, ILogger<PageTools> log)
{
_led = led;
_log = log;
}
[McpServerTool]
[Description("페이지 메시지 개수를 설정한다. ⚠️ 쓰기·설정 저장. count 1~10.")]
public async Task<string> SetPageCount(
[Description("페이지 수 1~10")] int count,
CancellationToken ct = default)
{
if (count < 1 || count > 10)
return "페이지 수는 1~10이어야 합니다.";
var resp = await _led.SendAsciiAsync("60", count.ToString("D2"), ct, 5000);
return Format(resp, $"페이지 개수 {count}개 설정.");
}
[McpServerTool]
[Description("페이지 메시지를 초기화(삭제)한다. ⚠️ 쓰기. page: 0=전체 삭제, 1~10=개별 페이지.")]
public async Task<string> ResetPageMessage(
[Description("0=전체 삭제, 1~10=개별 페이지")] int page,
CancellationToken ct = default)
{
if (page < 0 || page > 10)
return "page는 0(전체)~10이어야 합니다.";
// 정본: 전체→"99", 개별 N(1~10)→(N-1) zero-pad 2자리
var data = page == 0 ? "99" : (page - 1).ToString("D2");
var resp = await _led.SendAsciiAsync("61", data, ct, 5000);
return Format(resp, page == 0 ? "전체 페이지 초기화." : $"{page}번 페이지 초기화.");
}
[McpServerTool]
[Description("섹션별 표출 효과 모드를 설정한다. ⚠️ 쓰기. mode: 0=동시표출, 1=개별표출.")]
public async Task<string> SetSectionEffect(
[Description("0=동시표출, 1=개별표출")] int mode,
CancellationToken ct = default)
{
// 정본: N=동시표출, Y=개별표출
var data = mode == 0 ? "N" : "Y";
var resp = await _led.SendAsciiAsync("62", data, ct, 5000);
return Format(resp, mode == 0 ? "섹션 동시표출 설정." : "섹션 개별표출 설정.");
}
private static string Format(DibdProtocol.Protocol.AsciiParsedResponse? r, string ok)
=> r is null ? "전송 실패 — 연결 또는 통신 오류." : (r.Result == "F" ? "전광판이 거부했습니다 (Result=F)." : ok);
}

View File

@@ -0,0 +1,76 @@
using System.ComponentModel;
using Dabit.Mcp.Services;
using DibdProtocol.Protocol;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
namespace Dabit.Mcp.Tools;
/// <summary>화면 크기·채우기·배경이미지 설정 도구. ⚠️ 쓰기.</summary>
[McpServerToolType]
public sealed class ScreenTools
{
private readonly LedControlService _led;
private readonly ILogger<ScreenTools> _log;
public ScreenTools(LedControlService led, ILogger<ScreenTools> log)
{
_led = led;
_log = log;
}
// 색 인덱스 0~7 → RGB332 colorByte (정본 SettingsViewModel, R[2:0]G[5:3]B[7:6])
private static readonly byte[] FillColors = { 0x00, 0x07, 0x38, 0x3F, 0xC0, 0xC7, 0xF8, 0xFF };
private static readonly string[] ColorNames = { "검정", "빨강", "초록", "노랑", "파랑", "분홍", "청록", "하양" };
// bpp 입력(0/1/2) → payload byte (정본: 0→0x02, 2→0x08, 그 외→0x03=3bpp 8색 기본)
private static byte BppByte(int bpp) => bpp switch { 0 => 0x02, 2 => 0x08, _ => 0x03 };
[McpServerTool]
[Description("전광판 화면 크기(단/열)를 설정한다. ⚠️ 쓰기·설정 저장. layout은 모듈 배열(0~5).")]
public async Task<string> SetScreenSize(
[Description("단(행) 수 1~16")] int rows,
[Description("열 수 1~32")] int cols,
[Description("색심도 0/1/2 (기본 1=3bpp 8색)")] int bpp = 1,
[Description("배열 0~5 (기본 0)")] int layout = 0,
CancellationToken ct = default)
{
byte[] payload = { BppByte(bpp), (byte)Math.Clamp(rows, 1, 16), (byte)Math.Clamp(cols, 1, 32), (byte)Math.Clamp(layout, 0, 5), 0x00, 0xF1 };
var resp = await _led.SendHexAsync(HexCommand.SetScreenSize, payload, ct, 5000);
var (ok, msg) = LedControlService.CheckWriteStatus(HexCommand.SetScreenSize, resp);
if (!ok) return $"화면 크기 설정 실패 — {msg}";
// 응답 [0xF1, rows, cols] — 실제 적용된 값
return resp!.Data.Length >= 3
? $"화면 크기 설정됨: {resp.Data[1]}단 x {resp.Data[2]}열."
: "화면 크기 설정됨.";
}
[McpServerTool]
[Description("전광판 전체를 단색으로 채운다. ⚠️ 쓰기 — 현재 화면을 덮어쓴다. color 0~7: 0검정 1빨강 2초록 3노랑 4파랑 5분홍 6청록 7하양.")]
public async Task<string> FillScreen(
[Description("색 인덱스 0~7 (0검정 1빨강 2초록 3노랑 4파랑 5분홍 6청록 7하양)")] int color,
[Description("색심도 0/1/2 (기본 1)")] int bpp = 1,
CancellationToken ct = default)
{
if (color < 0 || color > 7)
return "색 인덱스는 0~7이어야 합니다 (0검정 1빨강 2초록 3노랑 4파랑 5분홍 6청록 7하양).";
byte[] payload = { BppByte(bpp), FillColors[color], 0x00, 0x00, 0x00 };
var resp = await _led.SendHexAsync(HexCommand.FillScreen, payload, ct, 5000);
var (ok, msg) = LedControlService.CheckWriteStatus(HexCommand.FillScreen, resp);
return ok ? $"화면 채움: {ColorNames[color]}." : $"화면 채우기 실패 — {msg}";
}
[McpServerTool]
[Description("저장된 배경이미지를 불러와 표시한다. ⚠️ 쓰기. num 0~255 (0=사용 안 함).")]
public async Task<string> LoadBackgroundImage(
[Description("이미지 번호 0~255 (0=사용 안 함)")] int num,
CancellationToken ct = default)
{
if (num < 0 || num > 255)
return "이미지 번호는 0~255여야 합니다.";
var resp = await _led.SendHexAsync(HexCommand.LoadBackgroundImage, new[] { (byte)num }, ct, 5000);
var (ok, msg) = LedControlService.CheckWriteStatus(HexCommand.LoadBackgroundImage, resp);
if (!ok) return $"배경이미지 로드 실패 — {msg}";
return num == 0 ? "배경이미지 사용 안 함으로 설정." : $"배경이미지 {num}번 로드.";
}
}

View File

@@ -0,0 +1,69 @@
using System.ComponentModel;
using Dabit.Mcp.Services;
using DibdProtocol.Protocol;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
namespace Dabit.Mcp.Tools;
/// <summary>
/// 표출신호(HUB75 패턴) 설정 도구. ⚠️ 쓰기 — F16 영역(70+ 모델 의존).
/// 대표 신호만 이름으로 지원하고, 그 외는 send_custom_signal로 raw 코드 전송.
/// </summary>
[McpServerToolType]
public sealed class SignalTools
{
private readonly LedControlService _led;
private readonly ILogger<SignalTools> _log;
public SignalTools(LedControlService led, ILogger<SignalTools> log)
{
_led = led;
_log = log;
}
// 대표 표출신호 → HexData 4바이트 (정본 DisplaySignalData 일부). 전체 70+는 send_custom_signal로.
private static readonly Dictionary<string, byte[]> SignalHexData = new(StringComparer.OrdinalIgnoreCase)
{
["16D-P16D1S11"] = new byte[] { 0x00, 0x10, 0x01, 0x11 },
["16D-P16D1S10-1"] = new byte[] { 0x00, 0x10, 0x01, 0x01 },
["08D-P32D1S11"] = new byte[] { 0x00, 0x08, 0x01, 0x11 },
["08D-P64D1S21"] = new byte[] { 0x00, 0x08, 0x01, 0x21 },
["04D-P32D2S11"] = new byte[] { 0x00, 0x04, 0x02, 0x11 },
["04D-P64D1S11"] = new byte[] { 0x00, 0x04, 0x01, 0x11 },
["32D-P16D1S11"] = new byte[] { 0x00, 0x20, 0x01, 0x11 },
};
[McpServerTool]
[Description("전광판 표출신호(HUB75 패턴)를 이름으로 설정한다. ⚠️ 쓰기 — 잘못 설정하면 화면이 깨진다. " +
"지원: 16D-P16D1S11, 16D-P16D1S10-1, 08D-P32D1S11, 08D-P64D1S21, 04D-P32D2S11, 04D-P64D1S11, 32D-P16D1S11. " +
"목록에 없으면 send_custom_signal을 쓴다. (08D-P64D1S21은 138 IC 스캔 고정)")]
public async Task<string> SetDisplaySignal(
[Description("신호 이름. 예: \"16D-P16D1S11\"")] string signalName,
[Description("색 순서 0~6 (기본 0)")] int colorOrder = 0,
CancellationToken ct = default)
{
if (!SignalHexData.TryGetValue(signalName.Trim(), out var hd))
return $"지원하지 않는 신호 '{signalName}'. 지원 목록: {string.Join(", ", SignalHexData.Keys)}. 또는 send_custom_signal 사용.";
var colorHex = (byte)(Math.Clamp(colorOrder, 0, 6) + 1); // 정본: idx+1
byte[] payload = { hd[0], hd[1], hd[2], hd[3], colorHex, 0x01 }; // scanHex 기본 0x01(138 IC)
var resp = await _led.SendHexAsync(HexCommand.SetHub75Pattern, payload, ct, 5000);
var (ok, msg) = LedControlService.CheckWriteStatus(HexCommand.SetHub75Pattern, resp);
return ok ? $"표출신호 '{signalName}' 설정 (color={colorOrder})." : $"표출신호 설정 실패 — {msg}";
}
[McpServerTool]
[Description("사용자 지정 표출신호 코드를 직접 전송한다(고급). ⚠️ 쓰기 — 코드 형식을 정확히 알 때만 사용. 잘못된 값은 화면을 깨뜨릴 수 있다.")]
public async Task<string> SendCustomSignal(
[Description("신호 코드 문자열. 예: \"8121\"")] string code,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(code) || code.Contains('/') || code.Contains("![") || code.Contains("!]"))
return "유효한 신호 코드가 아닙니다 ('/', '![', '!]' 불가).";
// 정본: ASCII msgType "44", data 맨 앞 공백 1개 필수
var resp = await _led.SendAsciiAsync("44", " " + code.Trim(), ct, 5000);
if (resp is null) return "전송 실패 — 연결 또는 통신 오류.";
return resp.Result == "F" ? "전광판이 거부했습니다 (Result=F)." : $"사용자 표출신호 '{code}' 전송.";
}
}

View File

@@ -0,0 +1,50 @@
using System.ComponentModel;
using Dabit.Mcp.Services;
using DibdProtocol.Protocol;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
namespace Dabit.Mcp.Tools;
/// <summary>컨트롤러 시각 동기화 도구. ⚠️ 쓰기.</summary>
[McpServerToolType]
public sealed class TimeTools
{
private readonly LedControlService _led;
private readonly ILogger<TimeTools> _log;
public TimeTools(LedControlService led, ILogger<TimeTools> log)
{
_led = led;
_log = log;
}
// 정본 SettingsViewModel: ToBcd(v) = (v/10*16)+(v%10). 26 → 0x26.
private static byte ToBcd(int v) => (byte)(v / 10 * 16 + v % 10);
private static string KoreanDow(DayOfWeek d) => d switch
{
DayOfWeek.Sunday => "일", DayOfWeek.Monday => "월", DayOfWeek.Tuesday => "화",
DayOfWeek.Wednesday => "수", DayOfWeek.Thursday => "목", DayOfWeek.Friday => "금",
_ => "토",
};
[McpServerTool]
[Description("컨트롤러의 시각(RTC)을 현재 PC 시간으로 동기화한다. ⚠️ 쓰기·설정 저장.")]
public async Task<string> SyncControllerTime(CancellationToken ct = default)
{
var now = DateTime.Now;
// 정본 0x47: [yy(BCD), MM(BCD), dd(BCD), dow(raw 0=일~6=토), HH(BCD), mm(BCD), ss(BCD)]
byte[] payload =
{
ToBcd(now.Year % 100), ToBcd(now.Month), ToBcd(now.Day),
(byte)(int)now.DayOfWeek,
ToBcd(now.Hour), ToBcd(now.Minute), ToBcd(now.Second),
};
var resp = await _led.SendHexAsync(HexCommand.SyncTime, payload, ct, 5000);
var (ok, msg) = LedControlService.CheckWriteStatus(HexCommand.SyncTime, resp);
return ok
? $"시간 동기화 완료: {now:yyyy-MM-dd HH:mm:ss} ({KoreanDow(now.DayOfWeek)})."
: $"시간 동기화 실패 — {msg}";
}
}