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,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));
}
}