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 BodyDampers { get; } // ---- 각실 디퓨저 4 룸 ---- public ObservableCollection 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 { // 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 { 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)); } }