Files
HECO2/Simulator/RJ2-232_RoomConSimulator/MainWindow.xaml.cs
T
jeon 5a96a696b1 chore: HERV 통합 저장소 초기 커밋
- 펌웨어(program), C# 대시보드(TestProgram), 시뮬레이터(Simulator),
  프로토콜/문서(Protocol, doc) 전체를 단일 저장소로 통합
- program 폴더의 별도 git 저장소를 제거하고 통합 저장소에 흡수
- 빌드 산출물(program/build, bin/obj, *.o/.elf/.bin/.hex 등) .gitignore 처리
- 사내 Synology NAS Git 원격 연결 예정

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:44:23 +09:00

263 lines
12 KiB
C#

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; }
});
}
}
}