feat: 06-17 신규 작업본 반영 (개발사양서/기능검토/승인원/Source 등 추가)
.claude/ 제외(.gitignore 추가). 기존 초기커밋(5a96a69) 위에 신규·수정·이동분 커밋.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
using System.IO.Ports;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace CvnetPacketProgram;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private SerialPort? _port;
|
||||
private readonly FrameParser _parser = new();
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitUi();
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 초기화
|
||||
// ====================================================================
|
||||
private void InitUi()
|
||||
{
|
||||
foreach (var b in new[] { 9600, 19200, 38400, 57600, 115200, 4800, 2400 })
|
||||
CmbBaud.Items.Add(b);
|
||||
CmbBaud.SelectedIndex = 0; // 9600 기본
|
||||
|
||||
// 송신은 마스터(월패드) 측 요청만 — 응답(0x91/0xD1)은 ERV가 보내며 RX 로그에서 디코딩한다.
|
||||
CmbCmd.Items.Add(new CmdItem(Cvnet.CmdStatusQuery, "상태 조회 (0x11)"));
|
||||
CmbCmd.Items.Add(new CmdItem(Cvnet.CmdCtrlReq, "상세 제어 요구 (0x51)"));
|
||||
|
||||
foreach (var m in Cvnet.Modes) CmbMode.Items.Add(new ByteItem(m.val, $"0x{m.val:X2} {m.name}"));
|
||||
foreach (var fobj in Cvnet.Fans) CmbFan.Items.Add(new ByteItem(fobj.val, $"0x{fobj.val:X2} {fobj.name}"));
|
||||
CmbMode.SelectedIndex = 2; // 수동 일반
|
||||
CmbFan.SelectedIndex = 1; // 약
|
||||
|
||||
RefreshPorts();
|
||||
CmbCmd.SelectedIndex = 1; // 상세 제어 요구 기본
|
||||
}
|
||||
|
||||
private void RefreshPorts()
|
||||
{
|
||||
string? cur = CmbPort.SelectedItem as string;
|
||||
CmbPort.Items.Clear();
|
||||
foreach (var p in SerialPort.GetPortNames().OrderBy(NaturalKey))
|
||||
CmbPort.Items.Add(p);
|
||||
if (cur != null && CmbPort.Items.Contains(cur)) CmbPort.SelectedItem = cur;
|
||||
else if (CmbPort.Items.Count > 0) CmbPort.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private static int NaturalKey(string s)
|
||||
=> int.TryParse(new string(s.Where(char.IsDigit).ToArray()), out int n) ? n : 0;
|
||||
|
||||
private void OnRefreshPorts(object sender, RoutedEventArgs e) => RefreshPorts();
|
||||
|
||||
// ====================================================================
|
||||
// 연결
|
||||
// ====================================================================
|
||||
private void OnToggleOpen(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_port is { IsOpen: true }) { ClosePort(); return; }
|
||||
|
||||
if (CmbPort.SelectedItem is not string portName)
|
||||
{
|
||||
MessageBox.Show("COM 포트를 선택하세요.", "포트", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
_port = new SerialPort(portName, (int)CmbBaud.SelectedItem!, Parity.None, 8, StopBits.One)
|
||||
{
|
||||
ReadTimeout = 500,
|
||||
WriteTimeout = 500,
|
||||
};
|
||||
_port.DataReceived += OnDataReceived;
|
||||
_parser.Clear();
|
||||
_port.Open();
|
||||
SetConnUi(true, portName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_port = null;
|
||||
MessageBox.Show($"포트 열기 실패:\n{ex.Message}", "오류", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClosePort()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_port != null)
|
||||
{
|
||||
_port.DataReceived -= OnDataReceived;
|
||||
if (_port.IsOpen) _port.Close();
|
||||
_port.Dispose();
|
||||
}
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
_port = null;
|
||||
SetConnUi(false, null);
|
||||
}
|
||||
|
||||
private void SetConnUi(bool open, string? port)
|
||||
{
|
||||
LedConn.Fill = open
|
||||
? System.Windows.Media.Brushes.LimeGreen
|
||||
: new System.Windows.Media.SolidColorBrush(
|
||||
System.Windows.Media.Color.FromRgb(0xC0, 0x39, 0x2B));
|
||||
TxtConn.Text = open ? $"{port} 연결됨" : "닫힘";
|
||||
BtnOpen.Content = open ? "닫기" : "열기";
|
||||
CmbPort.IsEnabled = !open;
|
||||
CmbBaud.IsEnabled = !open;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 수신
|
||||
// ====================================================================
|
||||
private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sp = _port;
|
||||
if (sp is not { IsOpen: true }) return;
|
||||
int n = sp.BytesToRead;
|
||||
if (n <= 0) return;
|
||||
var buf = new byte[n];
|
||||
int read = sp.Read(buf, 0, n);
|
||||
_parser.Append(buf, read);
|
||||
|
||||
var frames = _parser.Extract();
|
||||
if (frames.Count == 0) return;
|
||||
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
foreach (var f in frames)
|
||||
AppendLog(TxtRxLog, ChkRxAutoScroll, "RX", f, Cvnet.Decode(f));
|
||||
});
|
||||
}
|
||||
catch { /* 수신 중 포트 닫힘 등 무시 */ }
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Cmd 변경 → 입력 패널 토글
|
||||
// ====================================================================
|
||||
private void OnCmdChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (CmbCmd.SelectedItem is not CmdItem ci) return;
|
||||
bool isQuery = ci.Cmd == Cvnet.CmdStatusQuery;
|
||||
bool isCtrl = ci.Cmd == Cvnet.CmdCtrlReq;
|
||||
|
||||
if (GrpFields != null) GrpFields.Visibility = isQuery ? Visibility.Collapsed : Visibility.Visible;
|
||||
if (PnlCtrlFlags != null) PnlCtrlFlags.Visibility = isCtrl ? Visibility.Visible : Visibility.Collapsed;
|
||||
OnBuild(sender, e);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 빌드 / 전송
|
||||
// ====================================================================
|
||||
private byte[]? BuildPacket()
|
||||
{
|
||||
if (CmbCmd.SelectedItem is not CmdItem ci) return null;
|
||||
byte subId = ParseHexByte(TxtSubId.Text, 0x01);
|
||||
|
||||
byte mode = (CmbMode.SelectedItem as ByteItem)?.Val ?? 0x00;
|
||||
byte fan = (CmbFan.SelectedItem as ByteItem)?.Val ?? 0x00;
|
||||
byte reserve = ParseDecByte(TxtReserve.Text, 0);
|
||||
|
||||
switch (ci.Cmd)
|
||||
{
|
||||
case Cvnet.CmdStatusQuery:
|
||||
return Cvnet.BuildStatusQuery(subId);
|
||||
|
||||
case Cvnet.CmdCtrlReq:
|
||||
return Cvnet.BuildCtrlReq(
|
||||
subId,
|
||||
mode, ChkModeFlag.IsChecked == true, ChkBasic.IsChecked == true, ChkRange.IsChecked == true,
|
||||
fan, ChkFanFlag.IsChecked == true, ChkFilterReset.IsChecked == true,
|
||||
reserve, ChkRsvFlag.IsChecked == true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void OnBuild(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var pkt = BuildPacket();
|
||||
if (pkt != null) TxtPreview.Text = Cvnet.Hex(pkt);
|
||||
}
|
||||
|
||||
private void OnSendBuilt(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var pkt = BuildPacket();
|
||||
if (pkt == null) return;
|
||||
TxtPreview.Text = Cvnet.Hex(pkt);
|
||||
SendBytes(pkt);
|
||||
}
|
||||
|
||||
private void OnSendRaw(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var bytes = ParseHexString(TxtRawHex.Text);
|
||||
if (bytes.Count == 0)
|
||||
{
|
||||
MessageBox.Show("유효한 HEX 바이트가 없습니다.", "HEX", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
if (ChkAutoSum.IsChecked == true)
|
||||
bytes = Cvnet.Finalize(bytes).ToList();
|
||||
SendBytes(bytes.ToArray());
|
||||
}
|
||||
|
||||
private void SendBytes(byte[] pkt)
|
||||
{
|
||||
if (_port is not { IsOpen: true })
|
||||
{
|
||||
// 포트 미연결이어도 로그에는 남겨 패킷을 확인할 수 있게 한다.
|
||||
AppendLog(TxtTxLog, ChkTxAutoScroll, "TX(미연결)", pkt, Cvnet.Decode(pkt));
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
_port.Write(pkt, 0, pkt.Length);
|
||||
AppendLog(TxtTxLog, ChkTxAutoScroll, "TX", pkt, Cvnet.Decode(pkt));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"전송 실패:\n{ex.Message}", "오류", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 로그
|
||||
// ====================================================================
|
||||
private static void AppendLog(TextBox box, CheckBox autoScroll, string tag, byte[] frame, string decode)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var sb = new StringBuilder();
|
||||
sb.Append('[').Append(now.ToString("HH:mm:ss.fff")).Append("] ")
|
||||
.Append(tag).Append(" (").Append(frame.Length).Append("B)\n");
|
||||
sb.Append(" HEX: ").Append(Cvnet.Hex(frame)).Append('\n');
|
||||
sb.Append(IndentLines(decode)).Append("\n\n");
|
||||
|
||||
box.AppendText(sb.ToString());
|
||||
if (autoScroll.IsChecked == true) box.ScrollToEnd();
|
||||
}
|
||||
|
||||
private static string IndentLines(string s)
|
||||
{
|
||||
var lines = s.Replace("\r", "").Split('\n');
|
||||
return string.Join('\n', lines.Select(l => " " + l));
|
||||
}
|
||||
|
||||
private void OnClearTx(object sender, RoutedEventArgs e) => TxtTxLog.Clear();
|
||||
private void OnClearRx(object sender, RoutedEventArgs e) => TxtRxLog.Clear();
|
||||
|
||||
// ====================================================================
|
||||
// 파서 유틸
|
||||
// ====================================================================
|
||||
private static byte ParseHexByte(string s, byte def)
|
||||
=> byte.TryParse(s?.Trim().Replace("0x", "", StringComparison.OrdinalIgnoreCase),
|
||||
System.Globalization.NumberStyles.HexNumber, null, out var v) ? v : def;
|
||||
|
||||
private static byte ParseDecByte(string s, byte def)
|
||||
=> byte.TryParse(s?.Trim(), out var v) ? v : def;
|
||||
|
||||
private static List<byte> ParseHexString(string s)
|
||||
{
|
||||
var list = new List<byte>();
|
||||
if (string.IsNullOrWhiteSpace(s)) return list;
|
||||
var tokens = s.Replace("0x", " ", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(",", " ").Replace("\r", " ").Replace("\n", " ").Replace("\t", " ")
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var t in tokens)
|
||||
if (byte.TryParse(t, System.Globalization.NumberStyles.HexNumber, null, out var v))
|
||||
list.Add(v);
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
ClosePort();
|
||||
base.OnClosed(e);
|
||||
}
|
||||
|
||||
// 콤보 항목 ----------------------------------------------------------
|
||||
private sealed record CmdItem(byte Cmd, string Text) { public override string ToString() => Text; }
|
||||
private sealed record ByteItem(byte Val, string Text) { public override string ToString() => Text; }
|
||||
}
|
||||
Reference in New Issue
Block a user