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