chore: HERV 통합 저장소 재초기화 커밋
손상된 .git 히스토리(missing tree)로 재초기화 후 작업트리 전체 커밋. .claude/ 만 제외(로컬 에이전트 설정). 구 저장소 백업(.git_corrupt_backup/) 포함. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
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초)
|
||||
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 표시 + 옆에 메이크업 유지(1분) 카운트다운 시작. 그동안 ERV엔 계속 '켜짐' 보고.
|
||||
// 유지 종료 시 후드 OFF 전송 → ERV 가 원래 모드/풍량으로 복귀.
|
||||
txtPower.Text = "OFF";
|
||||
txtPower.Foreground = TextSecondary;
|
||||
if (_hood.PowerOn && _makeupRemainSec == 0)
|
||||
{
|
||||
_makeupRemainSec = MakeupHoldSec;
|
||||
_makeupTimer.Start();
|
||||
txtMakeup.Text = $"메이크업 {_makeupRemainSec}s";
|
||||
OnLog($"[제어] 전원 OFF 요청 → 메이크업 에어 {MakeupHoldSec}s 유지 (ERV엔 계속 켜짐 보고)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 메이크업 유지 카운트다운 (1초). 0이 되면 실제 OFF 전송.
|
||||
void MakeupTick(object? s, EventArgs e)
|
||||
{
|
||||
_makeupRemainSec--;
|
||||
if (_makeupRemainSec > 0)
|
||||
{
|
||||
txtMakeup.Text = $"메이크업 {_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 = "연결";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user