096111e983
.claude/ 제외(.gitignore 추가). 기존 초기커밋(5a96a69) 위에 신규·수정·이동분 커밋.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
301 lines
12 KiB
C#
301 lines
12 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Media;
|
|
using Microsoft.Win32;
|
|
|
|
namespace HoodSimulator
|
|
{
|
|
public partial class MainWindow : Window
|
|
{
|
|
readonly HoodProtocol _hood = new();
|
|
int _rxCount;
|
|
|
|
// 조리 종료 후 지연배기(잔여 냄새 배출) — 후드측이 담당. 배기중에는 ERV 에 계속 '켜짐' 보고,
|
|
// 종료 시점에 OFF 전송 → ERV 가 원래 모드/풍량으로 복귀. (사양 260613 9p 3.3)
|
|
readonly System.Windows.Threading.DispatcherTimer _makeupTimer =
|
|
new() { Interval = TimeSpan.FromSeconds(1) };
|
|
const int MakeupHoldSec = 10; // 지연배기 유지 시간 — 시간 불변(시뮬 10초; 사양 원래 30초)
|
|
int _makeupRemainSec;
|
|
|
|
static readonly Brush AccentCyan = (Brush)new BrushConverter().ConvertFromString("#7DCFFF")!;
|
|
static readonly Brush AccentGreen = (Brush)new BrushConverter().ConvertFromString("#9ECE6A")!;
|
|
static readonly Brush AccentRed = (Brush)new BrushConverter().ConvertFromString("#F7768E")!;
|
|
static readonly Brush CardBg = (Brush)new BrushConverter().ConvertFromString("#313147")!;
|
|
static readonly Brush TextPrimary = (Brush)new BrushConverter().ConvertFromString("#C0CAF5")!;
|
|
static readonly Brush TextSecondary = (Brush)new BrushConverter().ConvertFromString("#565F89")!;
|
|
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
_hood.LogMessage += OnLog;
|
|
_hood.ConnectionChanged += OnConnectionChanged;
|
|
_hood.MasterPacketReceived += OnMasterPacket;
|
|
_hood.ResponseSent += OnResponseSent;
|
|
_makeupTimer.Tick += MakeupTick;
|
|
|
|
RefreshPorts();
|
|
UpdateFanButtons();
|
|
Closed += (_, _) => { _makeupTimer.Stop(); _hood.Dispose(); };
|
|
}
|
|
|
|
// ========== 연결 ==========
|
|
void RefreshPorts()
|
|
{
|
|
cmbPort.Items.Clear();
|
|
foreach (var p in _hood.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 (_hood.IsConnected)
|
|
{
|
|
_hood.Disconnect();
|
|
btnConnect.Content = "연결";
|
|
}
|
|
else
|
|
{
|
|
if (cmbPort.SelectedItem == null) { MessageBox.Show("COM 포트를 선택하세요."); return; }
|
|
if (_hood.Connect(cmbPort.SelectedItem.ToString()!)) btnConnect.Content = "연결 해제";
|
|
}
|
|
}
|
|
|
|
void Start_Click(object s, RoutedEventArgs e)
|
|
{
|
|
if (!_hood.IsConnected) return;
|
|
_hood.StartResponding();
|
|
btnStart.IsEnabled = false;
|
|
btnStop.IsEnabled = true;
|
|
}
|
|
|
|
void Stop_Click(object s, RoutedEventArgs e)
|
|
{
|
|
_hood.StopResponding();
|
|
btnStart.IsEnabled = true;
|
|
btnStop.IsEnabled = false;
|
|
}
|
|
|
|
// ========== 후드 제어 ==========
|
|
void Power_Click(object s, RoutedEventArgs e)
|
|
{
|
|
if (tglPower.IsChecked == true)
|
|
{
|
|
// 켜기 : 진행중인 지연배기 취소 후 즉시 ON (풍량 1)
|
|
StopMakeupHold();
|
|
_hood.PowerOn = true;
|
|
_hood.FanStage = 1;
|
|
txtPower.Text = "ON";
|
|
txtPower.Foreground = AccentGreen;
|
|
UpdateFanButtons();
|
|
OnLog("[제어] 전원 → ON (풍량 1)");
|
|
}
|
|
else
|
|
{
|
|
// 끄기 : OFF 표시 + 옆에 지연배기 카운트다운 시작. 그동안 ERV엔 계속 '켜짐' 보고.
|
|
// 지연배기 종료 시 후드 OFF 전송 → ERV 가 원래 모드/풍량으로 복귀.
|
|
txtPower.Text = "OFF";
|
|
txtPower.Foreground = TextSecondary;
|
|
if (_hood.PowerOn && _makeupRemainSec == 0)
|
|
{
|
|
_makeupRemainSec = MakeupHoldSec;
|
|
_makeupTimer.Start();
|
|
txtMakeup.Text = $"지연배기(원래는 30초) {_makeupRemainSec}s";
|
|
OnLog($"[제어] 전원 OFF 요청 → 지연배기 {MakeupHoldSec}s 유지 (ERV엔 계속 켜짐 보고)");
|
|
}
|
|
}
|
|
}
|
|
|
|
// 지연배기 카운트다운 (1초). 0이 되면 실제 OFF 전송.
|
|
void MakeupTick(object? s, EventArgs e)
|
|
{
|
|
_makeupRemainSec--;
|
|
if (_makeupRemainSec > 0)
|
|
{
|
|
txtMakeup.Text = $"지연배기(원래는 30초) {_makeupRemainSec}s";
|
|
}
|
|
else
|
|
{
|
|
StopMakeupHold();
|
|
_hood.PowerOn = false;
|
|
_hood.FanStage = 0;
|
|
UpdateFanButtons();
|
|
OnLog("[제어] 지연배기 종료 → 후드 OFF 전송 (ERV 원래 모드/풍량 복귀)");
|
|
}
|
|
}
|
|
|
|
void StopMakeupHold()
|
|
{
|
|
_makeupTimer.Stop();
|
|
_makeupRemainSec = 0;
|
|
txtMakeup.Text = "";
|
|
}
|
|
|
|
void Fan_Click(object s, RoutedEventArgs e)
|
|
{
|
|
if (s is Button b && b.Tag is string tag && byte.TryParse(tag, out var f))
|
|
{
|
|
_hood.FanStage = f;
|
|
UpdateFanButtons();
|
|
OnLog($"[제어] 풍량 → {f}{(f == 0 ? " (꺼짐)" : "단")}");
|
|
}
|
|
}
|
|
|
|
void Light_Click(object s, RoutedEventArgs e)
|
|
{
|
|
_hood.Light = tglLight.IsChecked == true;
|
|
txtLight.Text = _hood.Light ? "ON" : "OFF";
|
|
txtLight.Foreground = _hood.Light ? AccentGreen : TextSecondary;
|
|
OnLog($"[제어] 조명 → {(_hood.Light ? "ON" : "OFF")}");
|
|
}
|
|
|
|
// 에러코드 토글 (FAN 에러=1 / 기타 에러=2). 둘 다 켜지면 FAN(1) 우선 송신.
|
|
bool _errFan, _errEtc;
|
|
void ErrorCard_Click(object s, System.Windows.Input.MouseButtonEventArgs e)
|
|
{
|
|
if (s is not Border b || b.Tag is not string tag) return;
|
|
if (tag == "1") _errFan = !_errFan;
|
|
else if (tag == "2") _errEtc = !_errEtc;
|
|
|
|
_hood.ErrorCode = _errFan ? (byte)1 : _errEtc ? (byte)2 : (byte)0;
|
|
UpdateErrorCards();
|
|
OnLog($"[제어] 에러코드 → {ErrorName(_hood.ErrorCode)} (ERROR={_hood.ErrorCode})");
|
|
|
|
// 에러 발생 시 전원 OFF / 풍량 0 / 조명 OFF (다음 응답에 반영되어 전송)
|
|
if (_hood.ErrorCode != 0)
|
|
{
|
|
StopMakeupHold(); // 진행중인 지연배기 즉시 취소
|
|
_hood.PowerOn = false;
|
|
_hood.FanStage = 0;
|
|
_hood.Light = false;
|
|
|
|
tglPower.IsChecked = false;
|
|
txtPower.Text = "OFF"; txtPower.Foreground = TextSecondary;
|
|
tglLight.IsChecked = false;
|
|
txtLight.Text = "OFF"; txtLight.Foreground = TextSecondary;
|
|
UpdateFanButtons();
|
|
OnLog("[제어] 에러 발생 → 전원 OFF / 풍량 0 / 조명 OFF");
|
|
}
|
|
}
|
|
|
|
void UpdateErrorCards()
|
|
{
|
|
UpdateErrLed(ErrCard_Fan, ErrLed_Fan, _errFan);
|
|
UpdateErrLed(ErrCard_Etc, ErrLed_Etc, _errEtc);
|
|
}
|
|
|
|
static void UpdateErrLed(Border card, System.Windows.Shapes.Ellipse led, bool on)
|
|
{
|
|
led.Fill = on ? AccentRed : (Brush)new BrushConverter().ConvertFromString("#3B3B55")!;
|
|
card.BorderBrush = on ? AccentRed : (Brush)new BrushConverter().ConvertFromString("#3B3B55")!;
|
|
card.BorderThickness = new Thickness(on ? 2 : 1);
|
|
}
|
|
|
|
static string ErrorName(byte e) => e switch { 1 => "FAN 에러", 2 => "기타 에러", _ => "정상" };
|
|
|
|
void UpdateFanButtons()
|
|
{
|
|
foreach (var child in FanPanel.Children)
|
|
{
|
|
if (child is Button btn && btn.Tag is string tag && byte.TryParse(tag, out var f))
|
|
{
|
|
bool active = f == _hood.FanStage;
|
|
btn.Background = active ? AccentCyan : CardBg;
|
|
btn.Foreground = active ? Brushes.Black : TextPrimary;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========== 마스터 패킷 수신 ==========
|
|
void OnMasterPacket(byte mode, byte fan, byte en, byte run)
|
|
{
|
|
Dispatcher.Invoke(() =>
|
|
{
|
|
_rxCount++;
|
|
txtRxCount.Text = $"수신: {_rxCount}";
|
|
txtRxMode.Text = $"{mode} ({ModeName(mode)})";
|
|
txtRxFan.Text = fan == 0 ? "0 (OFF)" : $"{fan}단";
|
|
txtRxEn.Text = en != 0 ? "Enable" : "Disable";
|
|
txtRxRun.Text = run != 0 ? "운전중" : "정지";
|
|
});
|
|
}
|
|
|
|
// ========== 응답 송신 ==========
|
|
void OnResponseSent(byte[] tx)
|
|
{
|
|
Dispatcher.Invoke(() =>
|
|
{
|
|
txtTxFan.Text = tx[3] == 0 ? "0 (OFF)" : $"{tx[3]}단";
|
|
txtTxLight.Text = tx[4] != 0 ? "ON" : "OFF";
|
|
txtTxCmd.Text = tx[6] != 0 ? "1 (켜짐)" : "0 (꺼짐)";
|
|
txtTxError.Text = $"{tx[7]} ({ErrorName(tx[7])})";
|
|
});
|
|
}
|
|
|
|
static string ModeName(byte m) => m switch
|
|
{
|
|
0 => "OFF", 1 => "환기", 2 => "자동", 3 => "공청", 4 => "바이패스", 5 => "기타", _ => "?"
|
|
};
|
|
|
|
// ========== 로그 ==========
|
|
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 = $"HoodSimLog_{DateTime.Now:yyyyMMdd_HHmmss}.txt"
|
|
};
|
|
if (dlg.ShowDialog() == true)
|
|
{
|
|
try
|
|
{
|
|
string h = $"========================================\r\n 후드 시뮬레이터 통신 로그\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(() =>
|
|
{
|
|
if (connected)
|
|
{
|
|
statusLed.Fill = AccentGreen;
|
|
txtStatus.Text = "연결됨";
|
|
btnStart.IsEnabled = true;
|
|
btnConnect.Content = "연결 해제";
|
|
}
|
|
else
|
|
{
|
|
statusLed.Fill = AccentRed;
|
|
txtStatus.Text = "미연결";
|
|
btnStart.IsEnabled = false;
|
|
btnStop.IsEnabled = false;
|
|
btnConnect.Content = "연결";
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|