using System; using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; using System.Windows.Threading; using Microsoft.Win32; namespace RJ2RoomConSimulator { public partial class MainWindow : Window { readonly RoomConProtocol _proto = new(); readonly DispatcherTimer _pollTimer; int _rxCount; bool _suppressReserve; VspWindow? _vspWin; static readonly Brush AccentCyan = Br("#7DCFFF"); static readonly Brush AccentGreen = Br("#9ECE6A"); static readonly Brush AccentRed = Br("#F7768E"); static readonly Brush AccentYellow = Br("#E0AF68"); static readonly Brush CardBg = Br("#313147"); static readonly Brush TextPrimary = Br("#C0CAF5"); static readonly Brush TextSecondary = Br("#565F89"); static readonly Brush LedOff = Br("#3B3B55"); static Brush Br(string h) => (Brush)new BrushConverter().ConvertFromString(h)!; static void SetChip(System.Windows.Shapes.Ellipse led, bool on, Brush onColor) => led.Fill = on ? onColor : LedOff; public MainWindow() { InitializeComponent(); _proto.Log += OnLog; _proto.ConnectionChanged += OnConnectionChanged; _proto.ResponseReceived += OnResponse; _pollTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) }; _pollTimer.Tick += (_, _) => { if (_proto.IsConnected) _proto.SendNormal(); }; RefreshPorts(); UpdateModeButtons(); UpdateFanButtons(); Closed += (_, _) => _proto.Dispose(); } // ================= 연결 ================= void RefreshPorts() { cmbPort.Items.Clear(); foreach (var p in _proto.GetAvailablePorts()) cmbPort.Items.Add(p); if (cmbPort.Items.Count > 0) cmbPort.SelectedIndex = 0; } void RefreshPorts_Click(object s, RoutedEventArgs e) => RefreshPorts(); void Connect_Click(object s, RoutedEventArgs e) { if (_proto.IsConnected) { _proto.Disconnect(); btnConnect.Content = "연결"; } else { if (cmbPort.SelectedItem == null) { MessageBox.Show("COM 포트를 선택하세요."); return; } if (_proto.Connect(cmbPort.SelectedItem.ToString()!)) btnConnect.Content = "연결 해제"; } } void Start_Click(object s, RoutedEventArgs e) { if (!_proto.IsConnected) return; _pollTimer.Start(); btnStart.IsEnabled = false; btnStop.IsEnabled = true; OnLog("[폴링 시작] 상태 조회(NORMAL) 주기 송신"); } void Stop_Click(object s, RoutedEventArgs e) { _pollTimer.Stop(); btnStart.IsEnabled = _proto.IsConnected; btnStop.IsEnabled = false; OnLog("[폴링 중지]"); } // ================= 룸콘 제어 ================= void Power_Click(object s, RoutedEventArgs e) { _proto.PowerOn = tglPower.IsChecked == true; if (_proto.PowerOn) { _proto.RunMode = 0; _proto.FanMode = 1; } // ON → 환기 1단 else _proto.FanMode = 0; // OFF → 풍량 0 txtPower.Text = _proto.PowerOn ? "ON" : "OFF"; txtPower.Foreground = _proto.PowerOn ? AccentGreen : TextSecondary; UpdateModeButtons(); UpdateFanButtons(); _proto.SendEvent(); OnLog($"[제어] 전원 → {(_proto.PowerOn ? "ON (환기 1단)" : "OFF")}"); } void Mode_Click(object s, RoutedEventArgs e) { if (s is not Button b || b.Tag is not string tag || !byte.TryParse(tag, out var mode)) return; _proto.PowerOn = true; _proto.RunMode = mode; // 운전모드 전환 시 풍량 1단 (자동 제외). 바이패스는 1단 고정, 공청/환기도 전환 기본 1단. // (ERV/펌웨어는 룸컨이 보낸 fan을 그대로 따르므로 마스터인 룸컨이 1단을 보내야 동기화됨) if (mode != 1) _proto.FanMode = 1; // 모드 전환 시 예약 해제 ClearReserve(); tglPower.IsChecked = true; txtPower.Text = "ON"; txtPower.Foreground = AccentGreen; UpdateModeButtons(); UpdateFanButtons(); _proto.SendEvent(); OnLog($"[제어] 모드 → {ModeName(mode)}"); } void Fan_Click(object s, RoutedEventArgs e) { if (s is not Button b || b.Tag is not string tag || !byte.TryParse(tag, out var sp)) return; if (_proto.RunMode == 1) { OnLog("자동모드에서는 풍량 변경 불가"); return; } // 자동 if (_proto.RunMode == 2 && sp > 1) { OnLog("바이패스는 1단 고정"); return; } // 바이패스 _proto.FanMode = sp; _proto.PowerOn = sp > 0 || _proto.PowerOn; if (sp > 0) { tglPower.IsChecked = true; txtPower.Text = "ON"; txtPower.Foreground = AccentGreen; } UpdateFanButtons(); _proto.SendEvent(); OnLog($"[제어] 풍량 → {sp}단"); } void Reserve_Changed(object s, SelectionChangedEventArgs e) { if (_suppressReserve || !IsLoaded || ReserveCombo.SelectedIndex < 0) return; _proto.ReserveHours = ReserveCombo.SelectedIndex; // 0=해제 _proto.SendEvent(); OnLog(_proto.ReserveHours == 0 ? "[제어] 예약 해제" : $"[제어] {_proto.ReserveHours}시간 후 꺼짐 예약"); } void ClearReserve() { _proto.ReserveHours = 0; if (ReserveCombo.SelectedIndex != 0) { _suppressReserve = true; ReserveCombo.SelectedIndex = 0; _suppressReserve = false; } } void ApplyId_Click(object s, RoutedEventArgs e) { if (byte.TryParse(txtDeviceId.Text, out var id)) { _proto.DeviceId = id; OnLog($"[설정] ID → {id}"); } else OnLog("ID는 0~255 숫자만 가능"); } void OpenVsp_Click(object s, RoutedEventArgs e) { if (_vspWin == null) { _vspWin = new VspWindow(_proto) { Owner = this }; _vspWin.Closed += (_, _) => _vspWin = null; _vspWin.Show(); } else _vspWin.Activate(); } // ================= 버튼 강조 ================= void UpdateModeButtons() { foreach (var child in ModePanel.Children) if (child is Button btn && btn.Tag is string t && byte.TryParse(t, out var m)) { bool active = _proto.PowerOn && m == _proto.RunMode; btn.Background = active ? AccentCyan : CardBg; btn.Foreground = active ? Brushes.Black : TextPrimary; } } void UpdateFanButtons() { foreach (var child in FanPanel.Children) if (child is Button btn && btn.Tag is string t && byte.TryParse(t, out var sp)) { bool active = sp == _proto.FanMode; btn.Background = active ? AccentCyan : CardBg; btn.Foreground = active ? Brushes.Black : TextPrimary; // 자동:전 단 비활성 / 바이패스:2~4 비활성 btn.IsEnabled = !(_proto.RunMode == 1) && !(_proto.RunMode == 2 && sp > 1); } } // ================= ERV 응답 ================= void OnResponse(byte cmd) { Dispatcher.Invoke(() => { _rxCount++; txtRxCount.Text = $"수신: {_rxCount}"; txtErvMode.Text = $"{_proto.ErvRunMode} ({ModeName(_proto.ErvRunMode)})"; txtErvFan.Text = _proto.ErvFanMode == 0 ? "0 (OFF)" : $"{_proto.ErvFanMode}단"; txtErvErr.Text = $"0x{_proto.ErvError:X2}"; txtErvErr.Foreground = _proto.ErvError != 0 ? AccentRed : TextPrimary; // 알람/에러 비트 디코드 (ERV ErrorCode 비트맵) byte ec = _proto.ErvError; SetChip(AlFClean, (ec & 0x01) != 0, AccentYellow); // 필터 청소 SetChip(AlFChange, (ec & 0x02) != 0, AccentYellow); // 필터 교환 SetChip(ErE02, (ec & 0x08) != 0, AccentRed); // 온도센서 SetChip(ErE09, (ec & 0x80) != 0, AccentRed); // 급기팬 SetChip(ErE10, (ec & 0x20) != 0, AccentRed); // 배기팬 SetChip(ErCold,(ec & 0x10) != 0, AccentCyan); // 장비보호 SetChip(ErE07, (ec & 0x40) != 0, AccentRed); // 내부통신 txtErvIn.Text = _proto.ErvInTemp == 100 ? "센서없음" : $"{_proto.ErvInTemp}℃"; txtErvOut.Text = _proto.ErvOutTemp == 100 ? "센서없음" : $"{_proto.ErvOutTemp}℃"; // 후드 연동 표시 (ERV HOOD_INFO 0x0A 수신값) : 후드 ON 오면 연동 ON, ERV OFF면 OFF SetChip(HoodLinkLed, _proto.HoodLinked, AccentGreen); txtHoodLink.Text = _proto.HoodLinked ? "ON" : "OFF"; txtHoodLink.Foreground = _proto.HoodLinked ? AccentGreen : TextSecondary; // 자동모드: 풍량은 ERV가 자동 결정 → 룸콘 표시를 ERV 보고값으로 동기화 if (_proto.RunMode == 1 && _proto.FanMode != _proto.ErvFanMode) { _proto.FanMode = _proto.ErvFanMode; UpdateFanButtons(); } }); } static string ModeName(int m) => m switch { 0 => "환기", 1 => "자동", 2 => "바이패스", 3 => "공기청정", 4 => "팬테스트", 10 => "OFF", _ => $"?{m}" }; // ================= 로그 ================= void OnLog(string msg) { Dispatcher.Invoke(() => { txtLog.AppendText(msg + Environment.NewLine); if (txtLog.LineCount > 500) { var lines = txtLog.Text.Split(Environment.NewLine); txtLog.Text = string.Join(Environment.NewLine, lines[^300..]); } txtLog.ScrollToEnd(); }); } void ClearLog_Click(object s, RoutedEventArgs e) => txtLog.Clear(); void SaveLog_Click(object s, RoutedEventArgs e) { var dlg = new SaveFileDialog { Filter = "텍스트 파일 (*.txt)|*.txt", FileName = $"RoomConSimLog_{DateTime.Now:yyyyMMdd_HHmmss}.txt" }; if (dlg.ShowDialog() == true) { try { string h = $"========================================\r\n RJ2-232 룸콘 시뮬레이터 통신 로그\r\n 저장 일시: {DateTime.Now:yyyy-MM-dd HH:mm:ss}\r\n========================================\r\n\r\n"; File.WriteAllText(dlg.FileName, h + txtLog.Text); MessageBox.Show($"저장 완료: {dlg.FileName}"); } catch (Exception ex) { MessageBox.Show($"저장 실패: {ex.Message}"); } } } void OnConnectionChanged(bool connected) { Dispatcher.Invoke(() => { statusLed.Fill = connected ? AccentGreen : AccentRed; txtStatus.Text = connected ? "연결됨" : "미연결"; btnStart.IsEnabled = connected; btnConnect.Content = connected ? "연결 해제" : "연결"; if (!connected) { _pollTimer.Stop(); btnStop.IsEnabled = false; } }); } } }