chore: HERV 통합 저장소 초기 커밋

- 펌웨어(program), C# 대시보드(TestProgram), 시뮬레이터(Simulator),
  프로토콜/문서(Protocol, doc) 전체를 단일 저장소로 통합
- program 폴더의 별도 git 저장소를 제거하고 통합 저장소에 흡수
- 빌드 산출물(program/build, bin/obj, *.o/.elf/.bin/.hex 등) .gitignore 처리
- 사내 Synology NAS Git 원격 연결 예정

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 21:44:23 +09:00
commit 5a96a696b1
265 changed files with 76458 additions and 0 deletions
@@ -0,0 +1,161 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using ErvProtocol;
namespace ErvDashboard.Model
{
// 대시보드 전체 상태 (STATUS 0x81 수신값 + 사용자 제어 의도)
public class DashboardState : INotifyPropertyChanged
{
// ---- ERV 제어/상태 ----
bool _powerOn;
RunMode _runMode = RunMode.Off;
byte _fanMode;
bool _hood, _hoodConnected;
bool _smartSleep, _comfortCook, _reliefRecover;
public bool PowerOn { get => _powerOn; set { if (_powerOn != value) { _powerOn = value; OnChanged(); } } }
public RunMode RunMode { get => _runMode; set { if (_runMode != value) { _runMode = value; OnChanged(); OnChanged(nameof(IsAuto)); OnChanged(nameof(CanRoomControl)); } } }
public bool IsAuto => RunMode == RunMode.Auto;
// 각실 댐퍼/LED 수동 제어 가능 여부 (환기/공청/바이패스에서만, 자동 제외)
public bool CanRoomControl => RunMode != RunMode.Auto;
public byte FanMode { get => _fanMode; set { if (_fanMode != value) { _fanMode = value; OnChanged(); } } }
public bool Hood { get => _hood; set { if (_hood != value) { _hood = value; OnChanged(); } } }
// 후드 485 통신연결 여부 (STATUS byte5 bit2). 후드연동 ON일 때 연결/미연결 텍스트 표시용
public bool HoodConnected { get => _hoodConnected; set { if (_hoodConnected != value) { _hoodConnected = value; OnChanged(); } } }
// ---- (꺼짐)예약 : 잔여초(STATUS 수신) ----
int _reserveRemainSec;
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}"
: "예약 없음";
public bool SmartSleep { get => _smartSleep; set { if (_smartSleep != value) { _smartSleep = value; OnChanged(); } } }
// 스마트수면 시간설정(대시보드 전용 — 종료 시각에 자동 해제). 자정 기준 분(0~1439).
int _sleepStartMin = 0; // 오전 12:00
int _sleepEndMin = 7 * 60 + 30; // 오전 7:30
public int SleepStartMin { get => _sleepStartMin; set { if (_sleepStartMin != value) { _sleepStartMin = value; OnChanged(); OnChanged(nameof(SleepSummary)); } } }
public int SleepEndMin { get => _sleepEndMin; set { if (_sleepEndMin != value) { _sleepEndMin = value; OnChanged(); OnChanged(nameof(SleepSummary)); } } }
public string SleepSummary => $"{DashboardState.FmtTime(SleepStartMin)} ~ {DashboardState.FmtTime(SleepEndMin)}";
public static string FmtTime(int min)
{
min = ((min % 1440) + 1440) % 1440;
int h = min / 60, m = min % 60, h12 = h % 12; if (h12 == 0) h12 = 12;
return $"{(h < 12 ? "" : "")} {h12}:{m:00}";
}
public bool ComfortCook { get => _comfortCook; set { if (_comfortCook != value) { _comfortCook = value; OnChanged(); } } }
public bool ReliefRecover { get => _reliefRecover; set { if (_reliefRecover != value) { _reliefRecover = value; OnChanged(); } } }
public byte SubModeBitmap
{
get
{
byte b = 0;
if (SmartSleep) b |= SubModeBits.SmartSleep;
if (ComfortCook) b |= SubModeBits.ComfortCook;
if (ReliefRecover) b |= SubModeBits.ReliefRecover;
return b;
}
set
{
SmartSleep = (value & SubModeBits.SmartSleep) != 0;
ComfortCook = (value & SubModeBits.ComfortCook) != 0;
ReliefRecover = (value & SubModeBits.ReliefRecover) != 0;
}
}
// ---- 자동운전 상태 (읽기전용) ----
AutoState _autoState = AutoState.Distribute;
public AutoState AutoState
{
get => _autoState;
set { if (_autoState != value) { _autoState = value; OnChanged(); OnChanged(nameof(AutoStateText)); } }
}
public string AutoStateText => AutoState == AutoState.Focus ? "집중" : "분산";
// 합산 부하점수 (4실 Level 합, 0~16) — STATUS 수신 시 StatusMapper 가 갱신
int _totalLoadScore;
public int TotalLoadScore
{
get => _totalLoadScore;
set { if (_totalLoadScore != value) { _totalLoadScore = value; OnChanged(); OnChanged(nameof(TotalLoadScoreText)); } }
}
public string TotalLoadScoreText => $"{TotalLoadScore} / 16";
// ---- 히스테리시스 ----
HystPreset _hystPreset = HystPreset.Normal;
int _hystPm25, _hystPm10, _hystVoc, _hystCo2;
public HystPreset HystPreset { get => _hystPreset; set { if (_hystPreset != value) { _hystPreset = value; OnChanged(); } } }
public int HystPm25 { get => _hystPm25; set { if (_hystPm25 != value) { _hystPm25 = value; OnChanged(); } } }
public int HystPm10 { get => _hystPm10; set { if (_hystPm10 != value) { _hystPm10 = value; OnChanged(); } } }
public int HystVoc { get => _hystVoc; set { if (_hystVoc != value) { _hystVoc = value; OnChanged(); } } }
public int HystCo2 { get => _hystCo2; set { if (_hystCo2 != value) { _hystCo2 = value; OnChanged(); } } }
// ---- 에러코드 ----
int _errorCode;
public int ErrorCode { get => _errorCode; set { if (_errorCode != value) { _errorCode = value; OnChanged(); OnChanged(nameof(ErrorCodeHex)); } } }
public string ErrorCodeHex => $"0x{ErrorCode:X4}";
// ---- ERV 리셋 (토글) ----
bool _reset;
public bool Reset { get => _reset; set { if (_reset != value) { _reset = value; OnChanged(); } } }
// ---- 풍량 VSP (9엔트리) ----
public ObservableCollection<VspRow> Vsp { get; }
// ---- 히스테리시스 데드밴드 테이블 (ECO/NORMAL/TURBO 별 PM2.5/PM10/VOC/CO2) ----
public ObservableCollection<HystRow> HystTable { get; }
// ---- 모드별 오염단계 임계표 [preset 0 ECO/1 NORMAL/2 TURBO][L1~L4 상한] ----
public int[][] Co2Thr { get; } = { new int[4], new int[4], new int[4] };
public int[][] Pm25Thr { get; } = { new int[4], new int[4], new int[4] };
public int[][] Pm10Thr { get; } = { new int[4], new int[4], new int[4] };
public int[][] VocThr { get; } = { new int[4], new int[4], new int[4] };
// ---- 각실 ----
public ObservableCollection<RoomState> Rooms { get; }
public DashboardState()
{
Rooms = new ObservableCollection<RoomState>
{
new(1, "거실"),
new(2, "침실1"),
new(3, "침실2"),
new(4, "침실3"),
};
// 히스테리시스 기본값 (NORMAL 가정, 펌웨어 m_*_Level 기준)
HystPm25 = 30; HystPm10 = 50; HystVoc = 300; HystCo2 = 700;
// 풍량 VSP 9엔트리 (환기1~4, 바이패스, 공청1~4)
Vsp = new ObservableCollection<VspRow>();
for (int i = 0; i < VspInfo.Count; i++)
Vsp.Add(new VspRow(VspInfo.Labels[i], VspInfo.Group[i], VspInfo.Index[i]));
// 히스테리시스 데드밴드(하강) 기본값 - 사양서
HystTable = new ObservableCollection<HystRow>
{
new("ECO", 0) { Pm25 = 2, Pm10 = 5, Voc = 5, Co2 = 50 },
new("NORMAL", 1) { Pm25 = 2, Pm10 = 5, Voc = 5, Co2 = 50 },
new("TURBO", 2) { Pm25 = 2, Pm10 = 5, Voc = 3, Co2 = 30 },
};
// 모드별 오염단계 임계 기본값 - 사양서 (CO2/PM2.5/PM10/VOC 의 L1~L4 상한)
int[][] co2 = { new[]{1000,1300,1600,2000}, new[]{800,1100,1400,1700}, new[]{700,1000,1300,1600} };
int[][] pm25 = { new[]{20,38,60,86}, new[]{14,29,49,69}, new[]{12,23,38,52} };
int[][] pm10 = { new[]{40,86,126,173}, new[]{28,66,102,138}, new[]{24,53,78,104} };
int[][] voc = { new[]{171,195,308,438}, new[]{120,150,250,350}, new[]{103,120,192,263} };
for (int i = 0; i < 3; i++) for (int k = 0; k < 4; k++)
{ Co2Thr[i][k] = co2[i][k]; Pm25Thr[i][k] = pm25[i][k]; Pm10Thr[i][k] = pm10[i][k]; VocThr[i][k] = voc[i][k]; }
}
public RoomState Room(int id) => Rooms[id - 1];
public event PropertyChangedEventHandler? PropertyChanged;
void OnChanged([CallerMemberName] string? n = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
}
}
+24
View File
@@ -0,0 +1,24 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace ErvDashboard.Model
{
// 히스테리시스 한 프리셋(ECO/NORMAL/TURBO)의 임계값 — 편집 가능
public class HystRow : INotifyPropertyChanged
{
public string Name { get; }
public int Preset { get; } // 0 ECO / 1 NORMAL / 2 TURBO (CTRL_HYST_VALUE)
public HystRow(string name, int preset) { Name = name; Preset = preset; }
int _pm25, _pm10, _voc, _co2;
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 Co2 { get => _co2; set { if (_co2 != value) { _co2 = value; OnChanged(); } } }
public event PropertyChangedEventHandler? PropertyChanged;
void OnChanged([CallerMemberName] string? n = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
}
}
@@ -0,0 +1,23 @@
using System;
namespace ErvDashboard.Model
{
// 그래프/DB용 시계열 1샘플 (5초 간격 기록 → SQLite 저장).
public sealed class LogSample
{
public DateTime Time;
public bool Power;
public byte RunMode; // 0 환기 / 1 자동 / 2 바이패스 / 3 공청 (RunMode enum)
public byte AutoMode; // 자동운전 세부 : 0 비자동 / 1 자동-집중 / 2 자동-분산
public byte HystPreset; // 공기질 프리셋 : 0 ECO / 1 NORMAL / 2 TURBO
public byte FanMode; // 0~4
public bool SmartSleep, ComfortCook, ReliefRecover;
public RoomSample[] Rooms = Array.Empty<RoomSample>();
}
public struct RoomSample
{
public bool DamperSa, DamperRa; // 급기/배기 댐퍼 열림
public int Co2, Pm25, Pm10, Voc, Temp, Humi, Led, Level;
}
}
@@ -0,0 +1,80 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using ErvProtocol;
namespace ErvDashboard.Model
{
// 각실(거실/침실1~3) 상태 + 제어값
public class RoomState : INotifyPropertyChanged
{
public int RoomId { get; } // 1=거실, 2~4=침실1~3
public string Name { get; }
public RoomState(int id, string name) { RoomId = id; Name = name; }
bool _damperSaOpen, _damperEaOpen;
int _pm25, _pm10, _voc, _co2, _temp, _humi;
AirQuality _airQuality = AirQuality.Normal;
int _ledDim;
int _loadScore;
int _finalVolume;
// 급기(SA) 댐퍼 열림/닫힘 (토글, 제어 가능)
public bool DamperSaOpen
{
get => _damperSaOpen;
set { if (_damperSaOpen != value) { _damperSaOpen = value; OnChanged(); } }
}
// 배기(EA) 댐퍼 열림/닫힘 (토글, 제어 가능)
public bool DamperEaOpen
{
get => _damperEaOpen;
set { if (_damperEaOpen != value) { _damperEaOpen = 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 Co2 { get => _co2; set { if (_co2 != value) { _co2 = 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(); } } }
// 공기질 상태코드(1~4, 프로토콜) — L3/L4 가 모두 매우나쁨(1)으로 합쳐지므로 표시는 LoadScore(Level) 사용
public AirQuality AirQuality
{
get => _airQuality;
set { if (_airQuality != value) { _airQuality = value; OnChanged(); } }
}
// 공기질 표시(좋음/보통/나쁨/매우나쁨/최악)는 각실 Level(=LoadScore 0~4) 기준 — L4(최악, 빨강)까지 구분
public string AirQualityText => LoadScore switch
{
0 => "좋음",
1 => "보통",
2 => "나쁨",
3 => "매우나쁨",
4 => "최악",
_ => "-",
};
// LED 디밍 0~9 (슬라이드, 제어 가능)
public int LedDim
{
get => _ledDim;
set { var v = value < 0 ? 0 : value > 9 ? 9 : value; if (_ledDim != v) { _ledDim = v; OnChanged(); } }
}
// 자동운전 - 각실 부하점수(=Level 0~4, 읽기전용). 변경 시 공기질 표시도 갱신.
public int LoadScore { get => _loadScore; set { if (_loadScore != value) { _loadScore = value; OnChanged(); OnChanged(nameof(AirQualityText)); } } }
// 자동운전 - 최종 풍량 (읽기전용)
public int FinalVolume { get => _finalVolume; set { if (_finalVolume != value) { _finalVolume = value; OnChanged(); } } }
public event PropertyChangedEventHandler? PropertyChanged;
void OnChanged([CallerMemberName] string? n = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
}
}
+23
View File
@@ -0,0 +1,23 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace ErvDashboard.Model
{
// 풍량 VSP 한 엔트리 (환기1~4 / 바이패스 / 공청1~4) — SA/EA 편집 가능
public class VspRow : INotifyPropertyChanged
{
public string Name { get; }
public int Group { get; } // 0환기 1바이패스 2공청 (CTRL_VSP)
public int Index { get; } // 환기/공청 1~4, 바이패스 1
public VspRow(string name, int group, int index) { Name = name; Group = group; Index = index; }
int _sa, _ea;
public int Sa { get => _sa; set { if (_sa != value) { _sa = value; OnChanged(); } } }
public int Ea { get => _ea; set { if (_ea != value) { _ea = value; OnChanged(); } } }
public event PropertyChangedEventHandler? PropertyChanged;
void OnChanged([CallerMemberName] string? n = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
}
}