5a96a696b1
- 펌웨어(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>
263 lines
12 KiB
C#
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; }
|
|
});
|
|
}
|
|
}
|
|
}
|