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,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, // 공청
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user