Files
HECO2/Simulator/ERVSimulator/Program/Model/AutoLogic.cs
T
jeon a502322188 chore: HERV 통합 저장소 재초기화 커밋
손상된 .git 히스토리(missing tree)로 재초기화 후 작업트리 전체 커밋.
.claude/ 만 제외(로컬 에이전트 설정). 구 저장소 백업(.git_corrupt_backup/) 포함.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 09:32:17 +09:00

290 lines
14 KiB
C#

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; // 다음 틱의 스마트수면 진입 감지용
}
}
}