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 ParseHexString(string s) { var list = new List(); 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; } }