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:
jeon
2026-06-16 09:29:03 +09:00
commit a502322188
630 changed files with 65126 additions and 0 deletions
@@ -0,0 +1,289 @@
using System;
using System.Windows.Threading;
namespace ERVSimulator.Model
{
// 펌웨어 [My_system.c] Air_Quality_damper_process() 포팅 (260520 사양)
// - 실별 4종센서 → 0~4 Level (모드별 임계 + 하강 히스테리시스)
// - 부하총점(Score) → 풍량단수, P_max/dP → 댐퍼(대기/집중/분산)
// - 1초 주기. RunMode==Auto && PowerOn 일 때만 댐퍼/풍량 구동.
public class AutoLogic
{
readonly ErvState _state;
readonly DamperSequencer _seq;
readonly DispatcherTimer _timer;
public event Action<string>? Log;
// 센서별 이전 단계(히스테리시스 데드존 유지) [room 1..4]
readonly int[] _prevCo2 = new int[5];
readonly int[] _prevPm25 = new int[5];
readonly int[] _prevPm10 = new int[5];
readonly int[] _prevVoc = new int[5];
// ---- 쾌적조리(후드연동) 메이크업 에어 상태 (사양서 260613 9p) ----
// 쾌적조리는 운전모드가 아닌 후드연동 토글. 토글 ON + 후드 가동중일 때만 메이크업 에어 발동.
// 조리 종료 후 잔여 배출(메이크업 유지)은 후드측이 담당 → ERV는 후드 OFF 신호 받으면 즉시 원래 상태 복귀.
bool _makeup; // 메이크업 에어(강제 연동) 동작중
byte _makeupFan; // 후드 단수 추종 결과 풍량
// 시나리오모드(안심회복/스마트수면/쾌적조리) 해제 시 진입 직전 풍량 복귀용 (운전모드는 시뮬에서 유지됨)
bool _prevScenario;
byte _scenarioSavedFan;
// 스마트수면 : 실별 CO2 히스테리시스 댐퍼 개폐 상태 (사양서 8p, >=1000 OPEN / <=800 CLOSE)
bool _prevSmartSleep;
readonly bool[] _sleepOpen = new bool[5]; // [room 1..4] true=OPEN
public AutoLogic(ErvState state, DamperSequencer seq)
{
_state = state; _seq = seq;
_timer = new DispatcherTimer(DispatcherPriority.Normal) { Interval = TimeSpan.FromSeconds(1) };
_timer.Tick += (_, _) => Process();
_timer.Start();
}
// 센서값 -> 0~4 단계. 하강 시 (임계-데드밴드) 이하라야 내려감. 데드존이면 이전 유지.
static int SensorLevel(int v, ushort[] t, ushort db, int prev)
{
int lv = prev;
if (v <= t[0] - db) lv = 0;
else if (v > t[0] && v <= t[1] - db) lv = 1;
else if (v > t[1] && v <= t[2] - db) lv = 2;
else if (v > t[2] && v <= t[3] - db) lv = 3;
else if (v > t[3]) lv = 4;
return lv;
}
static int ScoreToStage(int s)
{
if (s == 0) return 0;
if (s <= 4) return 1;
if (s <= 8) return 2;
if (s <= 12) return 3;
return 4;
}
// Level(0좋음~4매우나쁨) -> 공기질코드(1매우나쁨~4좋음)
static int AirqCode(int level) => level switch { 0 => 4, 1 => 3, 2 => 2, _ => 1 };
// 쾌적조리(후드연동) 메이크업 에어 상태 갱신. 반환=이번 틱 메이크업 에어 적용 여부.
// 매트릭스(9p 3.1) : 쾌적조리 OFF → 연동없음 / ON+후드꺼짐 → 대기(본래 설정) / ON+후드켜짐 → 메이크업 에어(강제 연동)
// 잔여 배출(메이크업 유지)은 후드측이 담당 → 후드 OFF 신호 받으면 ERV는 즉시 원래 상태 복귀(3.3).
bool UpdateCooking()
{
bool cookEnabled = _state.PowerOn && _state.HoodEnable; // 쾌적조리 토글(전원 ON 전제)
bool hoodOn = cookEnabled && _state.HoodCmd && _state.HoodFan > 0; // 후드 가동중(전원+풍량)
if (hoodOn)
{
if (!_makeup)
{
_makeup = true;
Log?.Invoke("[쾌적조리] 메이크업 에어 진입 — 자동/수동 일시정지");
}
_makeupFan = (byte)Math.Min((int)_state.HoodFan, 4); // 단수 추종(3.2) : 1→1,2→2,3→3,4→4,5→4
return true;
}
// 후드 정지 또는 쾌적조리 해제 → 즉시 원래 상태 복귀(메이크업 유지는 후드측이 담당)
if (_makeup) EndMakeup(cookEnabled ? "후드 OFF — 원래 상태 복귀" : "쾌적조리 해제");
return false;
}
void EndMakeup(string why)
{
_makeup = false;
// 진입 직전 풍량 복귀는 Process()의 시나리오 해제 처리에서 일괄 수행.
Log?.Invoke($"[쾌적조리] 메이크업 종료 ({why})");
}
void Process()
{
int p = _state.HystPreset;
// ---- 실별 Level 산출 (항상 - 표시용) ----
for (int r = 1; r <= 4; r++)
{
var room = _state.GetRoom(r);
int lc = SensorLevel(room.Co2, _state.Co2Thr[p], _state.Co2Db[p], _prevCo2[r]);
int l25 = SensorLevel(room.Pm25, _state.Pm25Thr[p], _state.Pm25Db[p], _prevPm25[r]);
int l10 = SensorLevel(room.Pm10, _state.Pm10Thr[p], _state.Pm10Db[p], _prevPm10[r]);
int lv = SensorLevel(room.Voc, _state.VocThr[p], _state.VocDb[p], _prevVoc[r]);
_prevCo2[r] = lc; _prevPm25[r] = l25; _prevPm10[r] = l10; _prevVoc[r] = lv;
int level = Math.Max(Math.Max(lc, l25), Math.Max(l10, lv));
room.Level = level;
room.AirQuality = AirqCode(level);
}
// ---- 부하총점 / P_max / dP (260428 v.Final : dP = 정렬 내림차순[0]-[1], 동점 포함) ----
// 최고단계 실이 2개 이상 동점이면 P_2nd=P_max → dP=0 → 분산. 한 실만 확실히(2↑) 나쁠 때만 집중.
int score = 0;
int[] levels = new int[4];
for (int r = 1; r <= 4; r++) { levels[r - 1] = _state.GetRoom(r).Level; score += levels[r - 1]; }
Array.Sort(levels); // 오름차순
int pmax = levels[3]; // 최고 단계
int p2nd = levels[2]; // 두번째로 높은 단계(동점 포함)
int dP = pmax - p2nd;
_state.LoadScore = score;
_state.PMax = pmax;
_state.DP = dP;
// ---- 쾌적조리(후드연동) 메이크업 에어 상태 갱신 → 연동운전중(HoodStatus) 소유 ----
bool makeupEffective = UpdateCooking();
_state.HoodStatus = makeupEffective; // 후드 폴 응답 '연동운전중'(롤백 유지 포함)
// ---- 시나리오모드 해제 → 진입 직전 운전모드로 동작 복귀 ----
// 시뮬은 시나리오 중에도 RunMode 를 유지(오버레이)하므로 운전모드는 자동 복귀.
// 시나리오가 덮어쓴 풍량만 진입 직전 값으로 되돌린다(비자동 한정, 자동은 재계산).
bool scenarioActive = _state.RecoveryMode || _state.SmartSleep || makeupEffective;
if (scenarioActive && !_prevScenario)
_scenarioSavedFan = _state.SetFanMode; // 진입 직전 풍량 저장
else if (!scenarioActive && _prevScenario && _state.PowerOn && _state.RunMode != RunMode.Auto)
_state.FanMode = _state.SetFanMode = _scenarioSavedFan; // 해제 → 이전 풍량 복귀
_prevScenario = scenarioActive;
// ---- 댐퍼/풍량 구동 (펌웨어 Air_Quality_damper_process 와 동일) ----
// 대시보드 수동 댐퍼/LED 제어는 환기·공청·바이패스(비자동·시나리오모드 아님)에서만 유지.
// 그 외(자동·시나리오모드·전원OFF)에서는 수동 플래그 해제 → 자동 제어 복귀.
// 쾌적조리 '대기 상태'(토글 ON·후드 꺼짐)는 본래 설정대로 가동 → subActive 아님.
bool subActive = _state.RecoveryMode || _state.SmartSleep || makeupEffective;
bool manualAllowed = _state.PowerOn && _state.RunMode != RunMode.Auto && !subActive;
// 댐퍼 수동 : 환기/공청/바이패스(비자동·비시나리오)에서만 유지, 그 외 해제 → 자동 제어 복귀.
// LED 수동 : 모든 운전모드·댐퍼 변경에도 유지(사용자 요청). 전원 OFF 시에만 해제 → 자동 추종(소등) 복귀.
for (int r = 1; r <= 4; r++)
{
var rm = _state.GetRoom(r);
if (!manualAllowed) rm.DamperManual = false;
if (!_state.PowerOn) rm.LedManual = false;
}
bool damperChanged = false;
void SetDamper(DiffuserRoom rm, int sa, int ra)
{
if (rm.MemorySA != sa || rm.MemoryRA != ra) damperChanged = true;
rm.MemorySA = sa; rm.MemoryRA = ra;
}
bool fanChanged = false;
string logTag;
void SetFan(byte st) { if (_state.FanMode != st) { _state.FanMode = _state.SetFanMode = st; fanChanged = true; } }
if (!_state.PowerOn)
{
// 전원 OFF : 전 실 즉시 닫힘 (18초 슬롯 시퀀스 대기 없이 Current 직접 0)
_state.AutoConcentrate = false;
for (int r = 1; r <= 4; r++)
{
var rm = _state.GetRoom(r);
SetDamper(rm, 0, 0);
rm.CurrentSA = 0; rm.CurrentRA = 0;
}
logTag = "전원OFF 전실 닫힘";
}
else if (makeupEffective) // 쾌적조리 메이크업 에어 : 전실 급기(SA) 100% 개방, 배기(RA) 닫힘, 후드 단수 추종
{
_state.AutoConcentrate = false;
for (int r = 1; r <= 4; r++) SetDamper(_state.GetRoom(r), 110, 0);
SetFan(_makeupFan);
logTag = $"쾌적조리 메이크업에어(전실 급기) {_makeupFan}단";
}
else if (_state.RecoveryMode) // 안심회복 : 침실1 음압 (급기X 배기O), 나머지 급기O 배기X, 2단
{
_state.AutoConcentrate = false;
SetDamper(_state.GetRoom(1), 110, 0); // 거실 급기
SetDamper(_state.GetRoom(2), 0, 110); // 침실1 배기(음압)
SetDamper(_state.GetRoom(3), 110, 0); // 침실2 급기
SetDamper(_state.GetRoom(4), 110, 0); // 침실3 급기
SetFan(2);
logTag = "안심회복(침실1 음압) 2단";
}
else if (_state.SmartSleep) // 스마트수면 : 1단 고정, 실별 CO2 기준 댐퍼 개폐 (사양서 8p)
{
_state.AutoConcentrate = false;
// 진입 초기상태 : 거실 CLOSE, 침실1~3 OPEN (이후 CO2 히스테리시스의 데드존 시드)
if (!_prevSmartSleep)
{
_sleepOpen[1] = false;
_sleepOpen[2] = _sleepOpen[3] = _sleepOpen[4] = true;
}
// CO2 센서 기준 : 해당 실 CO2 >= 1000 OPEN, <= 800 CLOSE, 그 사이(데드존)는 현재 상태 유지
for (int r = 1; r <= 4; r++)
{
int co2 = _state.GetRoom(r).Co2;
if (co2 >= 1000) _sleepOpen[r] = true;
else if (co2 <= 800) _sleepOpen[r] = false;
int ang = _sleepOpen[r] ? 110 : 0;
SetDamper(_state.GetRoom(r), ang, ang);
}
SetFan(1);
logTag = "스마트수면 CO2기준 실별개폐 1단";
}
else if (_state.RunMode != RunMode.Auto)
{
// 환기/공청/바이패스 : 각실 SA/RA 개방. 단, 대시보드 수동 댐퍼(DamperManual) 실은 그 위치 유지.
_state.AutoConcentrate = false;
for (int r = 1; r <= 4; r++)
{
var rm = _state.GetRoom(r);
if (rm.DamperManual) continue; // 대시보드 수동 댐퍼 - 자동 개방 덮어쓰기 안 함
SetDamper(rm, 110, 110);
}
logTag = $"{_state.RunMode} 각실 SA/RA 개방";
}
else
{
// 자동 : 대기 / 집중 / 분산
if (pmax == 0)
{
_state.AutoConcentrate = false;
for (int r = 1; r <= 4; r++) SetDamper(_state.GetRoom(r), 0, 0);
}
else if (dP >= 2)
{
_state.AutoConcentrate = true;
for (int r = 1; r <= 4; r++)
{
var rm = _state.GetRoom(r);
int ang = rm.Level == pmax ? 110 : 0;
SetDamper(rm, ang, ang);
}
}
else
{
// 분산 (260428 v.Final) : 1단계 이상 실만 개방, 0단계(좋음) 실은 닫음
_state.AutoConcentrate = false;
for (int r = 1; r <= 4; r++)
{
var rm = _state.GetRoom(r);
int ang = rm.Level >= 1 ? 110 : 0;
SetDamper(rm, ang, ang);
}
}
// 최종 풍량 : 부하총점 매핑
byte stage = (byte)ScoreToStage(score);
if (_state.FanMode != stage) { _state.FanMode = _state.SetFanMode = stage; fanChanged = true; }
logTag = $"자동 {(pmax == 0 ? "" : dP >= 2 ? "" : "")} Score={score} dP={dP} → {_state.FanMode}단";
}
// ---- LED : 댐퍼(SA/RA 중 하나라도 개방) 추종. 닫히면 0. 수동 조작(LedManual)은 예외 ----
for (int r = 1; r <= 4; r++)
{
var rm = _state.GetRoom(r);
if (rm.LedManual) continue;
int want = (_state.PowerOn && (rm.MemorySA > 0 || rm.MemoryRA > 0)) ? 9 : 0;
if (rm.LightBright != want) rm.LightBright = want;
}
if (fanChanged || damperChanged)
{
_seq.NotifyCommandChanged();
Log?.Invoke($"[댐퍼] {logTag}");
}
_prevSmartSleep = _state.SmartSleep; // 다음 틱의 스마트수면 진입 감지용
}
}
}
@@ -0,0 +1,169 @@
using System;
using System.Windows.Threading;
namespace ERVSimulator.Model
{
// 펌웨어 [MyMotor.c] Damper_Mode() + Diffuser_Damper_process() 시퀀스를 흉내
// - Cmd 변경 시 18초 시퀀스 트리거 (slot 180 / 120 / 60, 100ms tick)
// - 본체 댐퍼 6개: Run_Mode 에 따라 즉시 목표각 세팅
// - 디퓨저 댐퍼: Memory → Current 슬롯별 복사
// - 팬 PWM: 매 tick ±1 ramp
public class DamperSequencer
{
public ErvState State { get; }
private readonly DispatcherTimer _timer;
private int _diffuserSlot; // 180..0 카운트다운
private int _seqType; // 1=on, 2=off, 3=decrease, 4=increase
private int _prevAirVolume;
private bool _pendingSequence;
public DamperSequencer(ErvState state)
{
State = state;
_timer = new DispatcherTimer(DispatcherPriority.Normal)
{
Interval = TimeSpan.FromMilliseconds(100)
};
_timer.Tick += OnTick;
_timer.Start();
}
// RoomCon/HomeNet 핸들러가 Run_Mode/Fan_Mode 갱신 후 호출
public void NotifyCommandChanged()
{
_pendingSequence = true;
}
void OnTick(object? sender, EventArgs e)
{
// ---- Fan_Speed_process() 시작부: VENT && Fan=0 → 정지 진입 ----
int newAirVolume = (State.RunMode != RunMode.Off && State.FanMode != 0) ? State.FanMode : 0;
if (_pendingSequence)
{
_pendingSequence = false;
_diffuserSlot = 180;
_seqType = DetermineSeqType(_prevAirVolume, newAirVolume);
_prevAirVolume = newAirVolume;
}
// ---- Damper_Mode(Run_Mode) — 본체 댐퍼 즉시 목표각 ----
ApplyBodyDamperMode(EffectiveBodyMode());
// ---- Diffuser_Damper_process() — 슬롯 기반 적용 ----
if (_diffuserSlot == 180)
{
if (_seqType == 1 || _seqType == 4)
CopyMemoryToCurrent(1, 2);
else if (_seqType == 2 || _seqType == 3)
SetFanTargets(); // 즉시 ramp 시작
}
else if (_diffuserSlot == 120)
{
if (_seqType == 1 || _seqType == 4 || _seqType == 2 || _seqType == 3)
CopyMemoryToCurrent(3, 4);
}
else if (_diffuserSlot == 60)
{
if (_seqType == 1 || _seqType == 4)
SetFanTargets();
else if (_seqType == 2 || _seqType == 3)
CopyMemoryToCurrent(1, 2);
}
if (_diffuserSlot > 0) _diffuserSlot--;
// ---- 팬 ramp ±1 (펌웨어와 동일) ----
if (State.Fan1Current < State.Fan1Target) State.Fan1Current++;
else if (State.Fan1Current > State.Fan1Target) State.Fan1Current--;
if (State.Fan2Current < State.Fan2Target) State.Fan2Current++;
else if (State.Fan2Current > State.Fan2Target) State.Fan2Current--;
}
int DetermineSeqType(int prev, int now)
{
if (prev == 0 && now != 0) return 4; // increase (power on)
if (prev != 0 && now == 0) return 3; // decrease (power off)
if (prev > now) return 3;
return 4;
}
RunMode EffectiveBodyMode()
{
// VENT && Fan=0 → 본체 댐퍼는 MODE_OFF 로 진입 (펌웨어 Fan_Speed_process 분기)
if (State.RunMode == RunMode.Off) return RunMode.Off;
if (State.RunMode == RunMode.Ventilation && State.FanMode == 0 &&
State.Fan1Current == 0 && State.Fan2Current == 0)
return RunMode.Off;
return State.RunMode;
}
// 펌웨어 Damper_Mode() — MyMotor.c:472
void ApplyBodyDamperMode(RunMode mode)
{
switch (mode)
{
case RunMode.Ventilation:
SetBody(DamperId.OA, 0); SetBody(DamperId.EA, 0); SetBody(DamperId.BYPASS, 100);
SetBody(DamperId.SA, 0); SetBody(DamperId.RA, 70); SetBody(DamperId.AIR, 105);
break;
case RunMode.AirClean:
SetBody(DamperId.OA, 100); SetBody(DamperId.EA, 100); SetBody(DamperId.BYPASS, 100);
SetBody(DamperId.SA, 0); SetBody(DamperId.RA, 140); SetBody(DamperId.AIR, 0);
break;
case RunMode.Bypass:
SetBody(DamperId.OA, 0); SetBody(DamperId.EA, 0); SetBody(DamperId.BYPASS, 0);
SetBody(DamperId.SA, 0); SetBody(DamperId.RA, 140); SetBody(DamperId.AIR, 105);
break;
case RunMode.Auto:
// 펌웨어는 자동 시 공기질에 따라 VENT/AIR 선택. 단순화: VENT 와 동일.
SetBody(DamperId.OA, 0); SetBody(DamperId.EA, 0); SetBody(DamperId.BYPASS, 100);
SetBody(DamperId.SA, 0); SetBody(DamperId.RA, 70); SetBody(DamperId.AIR, 105);
break;
case RunMode.Off:
default:
SetBody(DamperId.OA, 100); SetBody(DamperId.EA, 100); SetBody(DamperId.BYPASS, 100);
SetBody(DamperId.SA, 100); SetBody(DamperId.RA, 0); SetBody(DamperId.AIR, 105);
break;
}
}
void SetBody(DamperId id, int angle) => State.GetDamper(id).TargetAngle = angle;
void CopyMemoryToCurrent(int fromRoom, int toRoom)
{
for (int r = fromRoom; r <= toRoom; r++)
{
var room = State.GetRoom(r);
room.CurrentSA = room.MemorySA;
room.CurrentRA = room.MemoryRA;
}
}
// 펌웨어 Fan_Speed_Setting(Run_Mode, Fan_Mode) — MyMotor.c:1233
void SetFanTargets()
{
int idx = System.Math.Clamp(State.FanMode, (byte)0, (byte)4);
switch (State.RunMode)
{
case RunMode.Ventilation:
case RunMode.Auto:
State.Fan1Target = State.FanSAPreset_Vent[idx];
State.Fan2Target = State.FanEAPreset_Vent[idx];
break;
case RunMode.Bypass:
State.Fan1Target = State.FanSAPreset_Bypass[idx];
State.Fan2Target = State.FanEAPreset_Bypass[idx];
break;
case RunMode.AirClean:
State.Fan1Target = State.FanSAPreset_Air[idx];
State.Fan2Target = State.FanEAPreset_Air[idx];
break;
default:
State.Fan1Target = 0;
State.Fan2Target = 0;
break;
}
}
}
}
@@ -0,0 +1,267 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace ERVSimulator.Model
{
// 6 본체 댐퍼 (Damper_Mode()가 직접 각도 명령)
public class BodyDamper : INotifyPropertyChanged
{
public string Name { get; }
public string Connector { get; } // CN2, CN10 ...
public string ColorTag { get; } // GREEN, YELLOW ...
public DamperId Id { get; }
private int _targetAngle;
public int TargetAngle
{
get => _targetAngle;
set { if (_targetAngle != value) { _targetAngle = value; OnChanged(); OnChanged(nameof(IsOpen)); } }
}
// 펌웨어 주석: 90 = close, 0 = open, 100/105 = close 변형
// RA(환기)만 '3step--reverse' → 0=닫힘, 70/140=열림 으로 규칙 반대 (MyMotor.c:482)
// → 전원 OFF(RA=0) 시 6개 댐퍼 모두 닫힘으로 표시
public bool IsOpen => Id == DamperId.RA ? TargetAngle >= 50 : TargetAngle < 50;
public BodyDamper(DamperId id, string name, string cn, string color)
{
Id = id; Name = name; Connector = cn; ColorTag = color;
}
public event PropertyChangedEventHandler? PropertyChanged;
void OnChanged([CallerMemberName] string? n = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
}
// 디퓨저 각실 (1~4)
public class DiffuserRoom : INotifyPropertyChanged
{
public int RoomId { get; }
public string Name { get; }
int _memorySA, _memoryRA, _currentSA, _currentRA, _light;
// Memory_* (RoomCon/HomeNet 핸들러가 즉시 갱신, 목표값)
public int MemorySA { get => _memorySA; set { if (_memorySA != value) { _memorySA = value; OnChanged(); } } }
public int MemoryRA { get => _memoryRA; set { if (_memoryRA != value) { _memoryRA = value; OnChanged(); } } }
// Diffuser_Dmp_Ang_* (시퀀서가 슬롯 시간에 Memory→Current 복사)
public int CurrentSA { get => _currentSA; set { if (_currentSA != value) { _currentSA = value; OnChanged(); OnChanged(nameof(IsOpenSA)); } } }
public int CurrentRA { get => _currentRA; set { if (_currentRA != value) { _currentRA = value; OnChanged(); OnChanged(nameof(IsOpenRA)); } } }
public int LightBright { get => _light; set { if (_light != value) { _light = value; OnChanged(); } } }
// 디퓨저 응답이 echo 한 실제 LED 단수 (디퓨저 수동 LED 제어 시 ERV 명령과 다를 수 있음) → STATUS 로 송신
int _ledReported;
public int LedReported { get => _ledReported; set { if (_ledReported != value) { _ledReported = value; OnChanged(); } } }
// 수동 LED 조작(CTRL_LED) 시 true → 자동로직이 LED 를 덮어쓰지 않음(예외). 비-수동모드 진입 시 해제.
public bool LedManual { get; set; }
// 수동 댐퍼 조작(CTRL_DAMPER) 시 true → 비자동(환기/공청/바이패스)에서 자동개방 덮어쓰기 안 함. 자동/부가모드/전원OFF/모드전환 시 해제.
public bool DamperManual { get; set; }
public bool IsOpenSA => CurrentSA > 0;
public bool IsOpenRA => CurrentRA > 0;
// ---- 공기질 센서값 (DiffuserSimulator 응답에서 수신) ----
int _co2, _pm25, _pm10, _voc, _level, _airQuality = 4, _temp, _humi;
public int Co2 { get => _co2; set { if (_co2 != value) { _co2 = value; OnChanged(); } } }
public int Pm25 { get => _pm25; set { if (_pm25 != value) { _pm25 = value; OnChanged(); } } }
public int Pm10 { get => _pm10; set { if (_pm10 != value) { _pm10 = value; OnChanged(); } } }
public int Voc { get => _voc; set { if (_voc != value) { _voc = value; OnChanged(); } } }
public int Temp { get => _temp; set { if (_temp != value) { _temp = value; OnChanged(); } } }
public int Humi { get => _humi; set { if (_humi != value) { _humi = value; OnChanged(); } } }
// 오염 단계 0~4 (자동로직 산출)
public int Level { get => _level; set { if (_level != value) { _level = value; OnChanged(); OnChanged(nameof(SensorText)); } } }
// 공기질 코드 1매우나쁨~4좋음 (STATUS 송신용)
public int AirQuality { get => _airQuality; set { if (_airQuality != value) { _airQuality = value; OnChanged(); } } }
public string SensorText => $"CO2 {Co2} PM2.5 {Pm25} PM10 {Pm10} VOC {Voc} → Lv{Level}";
public DiffuserRoom(int id, string name) { RoomId = id; Name = name; }
public event PropertyChangedEventHandler? PropertyChanged;
void OnChanged([CallerMemberName] string? n = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
}
public class ErvState : INotifyPropertyChanged
{
// ---- 상위 상태 ----
bool _powerOn;
RunMode _runMode = RunMode.Off;
RunMode _setRunMode = RunMode.Off;
byte _fanMode; // 0~4 단
byte _setFanMode;
public bool PowerOn { get => _powerOn; set { if (_powerOn != value) { _powerOn = value; OnChanged(); } } }
public RunMode RunMode { get => _runMode; set { if (_runMode != value) { _runMode = value; OnChanged(); } } }
public RunMode SetRunMode { get => _setRunMode; set { if (_setRunMode != value) { _setRunMode = value; OnChanged(); } } }
public byte FanMode { get => _fanMode; set { if (_fanMode != value) { _fanMode = value; OnChanged(); } } }
public byte SetFanMode { get => _setFanMode; set { if (_setFanMode != value) { _setFanMode = value; OnChanged(); } } }
// ---- 260520 자동 동작로직 상태 ----
byte _hystPreset = 1; // 0 ECO / 1 NORMAL / 2 TURBO
bool _autoConcentrate; // false 분산 / true 집중
int _loadScore, _pMax, _dP;
public byte HystPreset { get => _hystPreset; set { if (_hystPreset != value) { _hystPreset = value; OnChanged(); } } }
public bool AutoConcentrate { get => _autoConcentrate; set { if (_autoConcentrate != value) { _autoConcentrate = value; OnChanged(); OnChanged(nameof(AutoStateText)); } } }
public int LoadScore { get => _loadScore; set { if (_loadScore != value) { _loadScore = value; OnChanged(); OnChanged(nameof(AutoStateText)); } } }
public int PMax { get => _pMax; set { if (_pMax != value) { _pMax = value; OnChanged(); } } }
public int DP { get => _dP; set { if (_dP != value) { _dP = value; OnChanged(); } } }
public string AutoStateText => RunMode == RunMode.Auto
? $"{(PMax == 0 ? "" : AutoConcentrate ? "" : "")} · Score {LoadScore} · {FanMode}단"
: "(자동모드 아님)";
// 부가모드 (월패드 토글/버튼)
byte _extRunMode;
bool _hoodEnable;
public byte ExtRunMode { get => _extRunMode; set { if (_extRunMode != value) { _extRunMode = value; OnChanged(); OnChanged(nameof(SubModeText)); } } } // 1 안심회복 / 4 스마트수면
public bool HoodEnable { get => _hoodEnable; set { if (_hoodEnable != value) { _hoodEnable = value; OnChanged(); OnChanged(nameof(SubModeText)); } } } // 후드연동(쾌적조리)
public bool HoodStatus { get; set; }
public byte ResetState { get; set; } // ERV 리셋 토글 echo
// ---- 후드(HOOD 프로토콜 Rev1.3) 슬레이브 보고값 ----
bool _hoodConnected;
public bool HoodConnected { get => _hoodConnected; set { if (_hoodConnected != value) { _hoodConnected = value; OnChanged(); } } } // 후드 폴 응답 생존(통신 연결)
int _hoodFan; bool _hoodLight; bool _hoodCmd; int _hoodError;
public int HoodFan { get => _hoodFan; set { if (_hoodFan != value) { _hoodFan = value; OnChanged(); } } } // 후드 FAN STATUS 0~5
public bool HoodLight { get => _hoodLight; set { if (_hoodLight != value) { _hoodLight = value; OnChanged(); } } } // 후드 LIGHT STATUS
public bool HoodCmd { get => _hoodCmd; set { if (_hoodCmd != value) { _hoodCmd = value; OnChanged(); } } } // 연동 CMD(후드 동작중)
public int HoodError { get => _hoodError; set { if (_hoodError != value) { _hoodError = value; OnChanged(); } } } // ERROR : 0 정상 / 1 FAN / 2 기타
public bool SmartSleep { get => ExtRunMode == 4; } // 스마트수면
public bool CookingMode { get => HoodEnable; } // 쾌적조리(후드연동)
public bool RecoveryMode { get => ExtRunMode == 1; } // 안심회복
public string SubModeText
{
get
{
var s = "";
if (SmartSleep) s += "스마트수면 ";
if (CookingMode) s += "쾌적조리 ";
if (RecoveryMode) s += "안심회복 ";
return s.Length == 0 ? "없음" : s.Trim();
}
}
// ---- (꺼짐)예약 0~8시간 ----
int _reserveHours; // 0 = 해제
int _reserveRemainSec;
public int ReserveHours { get => _reserveHours; set { if (_reserveHours != value) { _reserveHours = value; OnChanged(); } } }
public int ReserveRemainSec { get => _reserveRemainSec; set { if (_reserveRemainSec != value) { _reserveRemainSec = value; OnChanged(); OnChanged(nameof(ReserveText)); } } }
public string ReserveText => ReserveRemainSec > 0
? $"예약 꺼짐까지 {ReserveRemainSec / 3600}:{(ReserveRemainSec % 3600) / 60:00}:{ReserveRemainSec % 60:00}"
: "예약 없음";
// 히스테리시스 데드밴드(하강) [preset] : CO2,PM2.5,PM10,VOC (사양서 10p)
public ushort[] Co2Db { get; } = { 50, 50, 30 };
public ushort[] Pm25Db { get; } = { 2, 2, 2 };
public ushort[] Pm10Db { get; } = { 5, 5, 5 };
public ushort[] VocDb { get; } = { 5, 5, 3 };
// 모드별(ECO/NORMAL/TURBO) 오염단계 상한 임계 [preset][레벨1~4]
public ushort[][] Co2Thr { get; } = { new ushort[]{1000,1300,1600,2000}, new ushort[]{800,1100,1400,1700}, new ushort[]{700,1000,1300,1600} };
public ushort[][] Pm25Thr { get; } = { new ushort[]{20,38,60,86}, new ushort[]{14,29,49,69}, new ushort[]{12,23,38,52} };
public ushort[][] Pm10Thr { get; } = { new ushort[]{40,86,126,173}, new ushort[]{28,66,102,138}, new ushort[]{24,53,78,104} };
public ushort[][] VocThr { get; } = { new ushort[]{171,195,308,438}, new ushort[]{120,150,250,350}, new ushort[]{103,120,192,263} };
// ---- 본체 6 댐퍼 ----
public ObservableCollection<BodyDamper> BodyDampers { get; }
// ---- 각실 디퓨저 4 룸 ----
public ObservableCollection<DiffuserRoom> Rooms { get; }
// ---- 팬 (BLDC SA/EA) ----
// 펌웨어 PWM duty 0~10000 매핑. UI는 0~10000 슬라이드로 표시 + 환산 RPM 추정.
int _fan1Target, _fan1Current; // SA
int _fan2Target, _fan2Current; // EA
public int Fan1Target { get => _fan1Target; set { if (_fan1Target != value) { _fan1Target = value; OnChanged(); } } }
public int Fan1Current { get => _fan1Current; set { if (_fan1Current != value) { _fan1Current = value; OnChanged(); } } }
public int Fan2Target { get => _fan2Target; set { if (_fan2Target != value) { _fan2Target = value; OnChanged(); } } }
public int Fan2Current { get => _fan2Current; set { if (_fan2Current != value) { _fan2Current = value; OnChanged(); } } }
// ---- 에러 코드 (PPT 매핑 + HERV 펌웨어 My_define.h:206 비트맵) ----
public const byte ERR_FILTER_CLEAN = 0x01;
public const byte ERR_FILTER_CHANGE = 0x02;
public const byte ERR_SOJA_CHANGE = 0x04;
public const byte ERR_TEMP_SENSOR = 0x08; // E02 온도센서 에러
public const byte ERR_PROTECT = 0x10; // COLD 장비보호모드
public const byte ERR_EA_FAN = 0x20; // E10 배기(EA)팬 에러
public const byte ERR_SOMETIME = 0x40; // E07 내부통신 에러
public const byte ERR_SA_FAN = 0x80; // E09 급기(SA)팬 에러
byte _errorCode;
public byte ErrorCode
{
get => _errorCode;
set
{
if (_errorCode != value)
{
_errorCode = value;
OnChanged();
OnChanged(nameof(E02_TempSensor));
OnChanged(nameof(E09_SaFan));
OnChanged(nameof(E10_EaFan));
OnChanged(nameof(COLD_Protect));
OnChanged(nameof(E07_InternalComm));
OnChanged(nameof(FilterClean));
OnChanged(nameof(FilterChange));
}
}
}
// 알람(유지보수) — 필터 청소/교환. 룸콘·대시보드로 ErrorCode 비트로 전달.
public bool FilterClean { get => (ErrorCode & ERR_FILTER_CLEAN) != 0; set => SetErr(ERR_FILTER_CLEAN, value); }
public bool FilterChange { get => (ErrorCode & ERR_FILTER_CHANGE) != 0; set => SetErr(ERR_FILTER_CHANGE, value); }
public bool E02_TempSensor { get => (ErrorCode & ERR_TEMP_SENSOR) != 0; set => SetErr(ERR_TEMP_SENSOR, value); }
public bool E09_SaFan { get => (ErrorCode & ERR_SA_FAN) != 0; set => SetErr(ERR_SA_FAN, value); }
public bool E10_EaFan { get => (ErrorCode & ERR_EA_FAN) != 0; set => SetErr(ERR_EA_FAN, value); }
public bool COLD_Protect { get => (ErrorCode & ERR_PROTECT) != 0; set => SetErr(ERR_PROTECT, value); }
public bool E07_InternalComm { get => (ErrorCode & ERR_SOMETIME) != 0; set => SetErr(ERR_SOMETIME, value); }
void SetErr(byte bit, bool on)
{
byte newVal = on ? (byte)(_errorCode | bit) : (byte)(_errorCode & ~bit);
ErrorCode = newVal;
}
// 1~4단 VSP preset (1바이트 0~255). 기본값 = 사양서 DL H-ERV VSP 실측표 (index 1~4)
public ushort[] FanSAPreset_Vent { get; } = { 0, 56, 63, 70, 86 }; // 환기 SA
public ushort[] FanEAPreset_Vent { get; } = { 0, 57, 63, 70, 85 }; // 환기 EA
public ushort[] FanSAPreset_Bypass { get; } = { 0, 67, 0, 0, 0 }; // 바이패스 SA (기본단)
public ushort[] FanEAPreset_Bypass { get; } = { 0, 75, 0, 0, 0 }; // 바이패스 EA
public ushort[] FanSAPreset_Air { get; } = { 0, 65, 72, 78, 80 }; // 공청 SA
public ushort[] FanEAPreset_Air { get; } = { 0, 0, 0, 0, 0 }; // 공청 EA (미사용 '-')
public ErvState()
{
BodyDampers = new ObservableCollection<BodyDamper>
{
// PPT 순서/색상 매핑
new(DamperId.OA, "외기(OA)", "CN2", "GREEN"),
new(DamperId.AIR, "공청(AIR)", "CN10", "YELLOW"),
new(DamperId.BYPASS, "바이패스", "CN5", "RED"),
new(DamperId.EA, "배기(EA)", "CN3", "BLACK"),
new(DamperId.SA, "급기(SA)", "CN7", "BLUE"),
new(DamperId.RA, "환기(RA) 3단", "CN9", "WHITE"),
};
Rooms = new ObservableCollection<DiffuserRoom>
{
new(1, "거실"),
new(2, "침실1"),
new(3, "침실2"),
new(4, "침실3"),
};
}
public BodyDamper GetDamper(DamperId id)
{
foreach (var d in BodyDampers) if (d.Id == id) return d;
throw new System.InvalidOperationException();
}
public DiffuserRoom GetRoom(int roomId)
{
foreach (var r in Rooms) if (r.RoomId == roomId) return r;
throw new System.InvalidOperationException();
}
public event PropertyChangedEventHandler? PropertyChanged;
void OnChanged([CallerMemberName] string? n = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
}
}
@@ -0,0 +1,24 @@
namespace ERVSimulator.Model
{
// HERV 펌웨어 SPEC_MODE_INFO=0x16 (대림사양, 히터X, 바이패스O, 공청X) 기준
// My_define.h:271 #if !((SPEC_MODE_INFO&0x0F)==0x03||==0x06) 분기
public enum RunMode : byte
{
Ventilation = 0,
Auto = 1,
Bypass = 2,
AirClean = 3,
FanTest = 4,
Off = 10, // MODE_OFF (MyMotor.c)
}
public enum DamperId
{
EA = 1, // 배기
OA = 2, // 외기
BYPASS = 3,
SA = 4, // 급기
RA = 5, // 환기
AIR = 6, // 공청
}
}