claude --resume


CronCreate CronDelete CronList DesignSync EnterPlanMode EnterWorktree ExitPlanMode ExitWorktree Monitor NotebookEdit PushNotification RemoteTrigger TaskOutput TaskStop TodoWrite WebFetch WebSearch


claude claude-code-guide Explore general-purpose Plan statusline-setup

- claude: Catch-all for any task that doesn't fit a more specific agent. FleetView's default when no agent name is typed. (Tools: *)

- claude-code-guide: Use this agent when the user asks questions ("Can Claude...", "Does Claude...", "How do I...") about: (1) Claude Code (the CLI tool) - features, hooks, slash commands, MCP servers, settings, IDE integrations, keyboard shortcuts; (2) Claude Agent SDK - building custom agents; (3) Claude API (formerly Anthropic API) - API usage, tool use, Anthropic SDK usage. **IMPORTANT:** Before spawning a new agent, check if there is already a running or recently completed claude-code-guide agent that you can continue via SendMessage. (Tools: Glob, Grep, Read, WebFetch, WebSearch)

- Explore: Read-only search agent for broad fan-out searches — when answering means sweeping many files, directories, or naming conventions and you only need the conclusion, not the file dumps. It reads excerpts rather than whole files, so it locates code; it doesn't review or audit it. Specify search breadth: "medium" for moderate exploration, "very thorough" for multiple locations and naming conventions. (Tools: All tools except Agent, ExitPlanMode, Edit, Write, NotebookEdit)

- general-purpose: General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. (Tools: *)

- Plan: Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs. (Tools: All tools except Agent, ExitPlanMode, Edit, Write, NotebookEdit)

- statusline-setup: Use this agent to configure the user's Claude Code status line setting. (Tools: Read, Edit)


- deep-research: Deep research harness — fan-out web searches, fetch sources, adversarially verify claims, synthesize a cited report. - When the user wants a deep, multi-source, fact-checked research report on any topic. BEFORE invoking, check if the question is specific enough to research directly — if underspecified (e.g., "what car to buy" without budget/use-case/region), ask 2-3 clarifying questions to narrow scope. Then pass the refined question as args, weaving the answers in.
- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- verify: Verify that a code change actually does what it's supposed to by running the app and observing behavior. Use when asked to verify a PR, confirm a fix works, test a change manually, check that a feature works, or validate local changes before pushing.
- code-review: Review the current diff for correctness bugs and reuse/simplification/efficiency cleanups at the given effort level (low/medium: fewer, high-confidence findings; high→max: broader coverage, may include uncertain findings; ultra: deep multi-agent review in the cloud). Pass --comment to post findings as inline PR comments, or --fix to apply the findings to the working tree after the review.
- simplify: Review the changed code for reuse, simplification, efficiency, and altitude cleanups, then apply the fixes. Quality only — it does not hunt for bugs; use /code-review for that.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled cloud agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring cloud agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Reference for the Claude API / Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration.
TRIGGER — read BEFORE opening the target file; don't skip because it "looks like a one-liner" — whenever: the prompt names Claude/Anthropic in any form (Claude, Anthropic, Fable, Opus, Sonnet, Haiku, `anthropic`, `@anthropic-ai`, `claude-*`, `us.anthropic.*`, `[1m]`); the user asks about an LLM (pricing/model choice/limits/caching) — never answer from memory; OR the task is LLM-shaped with provider unstated (agent/MCP/tool-definition/multi-agent/RAG/LLM-judge/computer-use; generate/summarize/extract/classify/rewrite/converse over NL; debugging refusals/cutoffs/streaming/tool-calls/tokens).
SKIP only when another provider is being worked on (overrides all triggers): OpenAI/GPT/Gemini/Llama/Mistral/Cohere/Ollama named in the query; OR `grep -rE 'openai|langchain_openai|google.generativeai|genai|mistralai|cohere|ollama'` over the project hits (run this grep FIRST if no provider named — don't Read the file).
- run: Launch and drive this project's app to see a change working. Use when asked to run, start, or screenshot the app, or to confirm a change works in the real app (not just tests). First looks for a project skill that already covers launching the app; otherwise falls back to built-in patterns per project type (CLI, server, TUI, Electron, browser-driven, library).
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a pull request
- security-review: Complete a security review of the pending changes on the current branch

deep-research update-config keybindings-help verify code-review simplify fewer-permission-prompts loop schedule claude-api run init review security-review


`claude --resume`는 터미널에서 실행하는 CLI 명령이라 지금 이 대화 안에서는 제가 실행할 수 없습니다. 그건 새 터미널 창에서 `claude --resume`를 입력하면 이전 세션 목록이 떠서 골라 이어갈 수 있는 기능이에요.

지금 이 세션에서 바로 작업을 이어가고 싶으시면, 무엇을 하려고 하셨는지 알려주세요. 참고로 최근 커밋을 보면 이런 작업들이 있었네요:

- `c5e4c48` — 260618 내부댐퍼·스마트수면·쾌적조리 동작 수정
- `6934f09` — 대화 이력(transcript) 사본 추가

혹시 이 중 이어서 하실 작업이 있나요? 아니면:

1. **이전 대화 내용 확인** — `doc/` 폴더에 백업된 transcript/메모리를 읽어서 직전 작업 맥락을 복원해 드릴 수 있습니다.
2. **새 작업 시작** — 펌웨어(`SOURCE/HECO2/`)나 대시보드(`TestProgram/`) 관련 작업을 바로 시작.

어떻게 진행할까요?


이전 대화 내용 확인


이전 대화 백업이 `doc/` 폴더에 있다고 했으니 거기부터 확인하겠습니다.


doc\260602_소스_개발사양서_260318.md
doc\260602_소스_개발사양서_260520.md
doc\260617_수정내용.md
doc\260618_내부댐퍼_모드변경시_미동작_수정.md
doc\Gitea_저장소_접속_및_협업_가이드.md
doc\HERV_Firmware_Analysis.pptx
doc\HERV_MCU PINMAP & CONNECTOR.txt
doc\VSP 테이블.pdf
doc\VSP 테이블.pptx
doc\claude-memory\MEMORY.md
doc\claude-memory\herv-master-source.md
doc\claude-memory\README.md
doc\claude-memory\internal-damper-fan-gate.md
doc\claude-memory\session\5209cdb2-53ba-4f8b-9aa0-4a7911424cf1.jsonl
doc\pba_connector.png
doc\스마트폰_원격조종_RemoteControl_가이드.md
doc\팀공유_이동작업_환경구성.md
doc\펌웨어_정적분석_버그리포트.md

doc\팀공유_이동작업_환경구성.md

doc\펌웨어_정적분석_버그리포트.md


1	# Memory Index
2
3	- [HERV 마스터 소스](herv-master-source.md) — 검증된 펌웨어 = D:\Project\nuvoton\HERV_DL_MH_2nd\Program, 댐퍼/팬 회귀 비교 기준
4	- [내부댐퍼 팬 게이트](internal-damper-fan-gate.md) — 본체 댐퍼는 팬이 0까지 내려가야 모드별 이동, 명령경로 끼어들면 미동작
5

# Memory Index

- [HERV 마스터 소스](herv-master-source.md) — 검증된 펌웨어 = D:\Project\nuvoton\HERV_DL_MH_2nd\Program, 댐퍼/팬 회귀 비교 기준
- [내부댐퍼 팬 게이트](internal-damper-fan-gate.md) — 본체 댐퍼는 팬이 0까지 내려가야 모드별 이동, 명령경로 끼어들면 미동작


1	# 260618 수정 요약 (핸드오프용)
2
3	> 작업 PC 이전 시점 기준. 아래 상세 항목 (1)~(5) 의 최종 변경 정리.
4
5	## 변경 파일 (커밋 포함)
6	| 파일 | 변경 요지 | 관련 항목 |
7	|---|---|---|
8	| `Source/HECO2/User/MyMotor.c` | 모드전환 중(`Damper_wait_time==5`) 팬 강제 0 → 내부댐퍼 이동 게이트 보호 | (3) |
9	| `Source/HECO2/User/My_Homenet.c` | 모드변경 시 명령경로 즉시 `Fan_Speed_Setting` 호출 주석(L259/293) — 마스터 정렬 | (1) |
10	| `Source/HECO2/User/My_Hood.c` | 후드연동 모드변경 시 즉시 `Fan_Speed_Setting` 주석(L192/207) — 마스터 정렬 | (1) |
11	| `Source/HECO2/User/My_system.c` | 스마트수면 거실(room1) CO2 무관 항상 CLOSE | (2) |
12	| `TestProgram/PCDashBoard/MainWindow.xaml.cs` | 쾌적조리 버튼 강조=ComfortCook(L583), 활성=항상(L587) | (4)(5) |
13	| `개발사양서/...DL_동작로직_260613.pptx` | 사양서 갱신(바이너리) | - |
14
15	## 빌드 상태
16	- 펌웨어: `cd SOURCE/HECO2 && bash build.sh all` → 경고/오류 0.
17	- 대시보드: `dotnet publish ErvDashboard.csproj -c Release` → 오류 0 (NU1701 경고만).
18	- 대시보드 exe: `TestProgram/PCDashBoard/bin/Release/net10.0-windows/win-x64/publish/ErvDashboard.exe`
19
20	## 실장비/대시보드 확인 필요(테스트 대기)
21	- 환기→공청 반복 전환 시 내부댐퍼 매번 이동(간헐성 해소). 전환 시 팬이 잠깐 0까지 멈췄다 복귀하는 것이 정상.
22	- 스마트수면 진입 시 거실 닫힘 + 침실 CO2 개폐.
23	- 쾌적조리 버튼이 후드 연결/전원 무관하게 항상 토글되는지.
24
25	## 미적용/보류
26	- (선택) 쾌적조리를 스마트수면/안심회복과의 **상호배타에서 분리**(사양상 독립 토글) — SubMode_Click L401~406.
27	- (보류) 펌웨어 쾌적조리를 `Ext_Run_Mode==2` 상태머신으로 배선(현재 My_system 846/952 dead code). 1차 대시보드 수정으로 충분한지 검증 후 결정.
28	- 1차 명령경로 주석(My_Homenet/My_Hood)은 (3) 게이트 보호로 중복이나 무해 — 정리 보류.
29
30	---
31
32	# 260618 내부댐퍼 — 운전모드 변경 시 미동작 수정 (1차: 명령경로 즉시 팬설정 제거)
33
34	## 증상
35	- 환기 → 공청 전환 시 **팬은 정상 동작하는데 내부댐퍼(본체 6개)가 안 움직임.**
36
37	## 원인 (마스터 `D:\Project\nuvoton\HERV_DL_MH_2nd\Program` 비교로 확정)
38	- 내부댐퍼 이동 트리거(`Fan_Speed_process`가 모드변경 시 `Damper_Mode()` 호출)는 HECO2/마스터 **동일하게 존재**. 빠진 게 아님.
39	- 그 트리거는 **"팬이 0까지 감속"되어야 발동**하도록 게이팅됨(공기 흐름 중 댐퍼 미동작 의도):
40	  `모드변경 → 팬타깃0 → 팬=0 도달 → Damper_Mode 호출(댐퍼 이동) → 정렬(Step_Status==0x3F) → 팬 복원`
41	- **마스터:** 명령 핸들러가 팬 타깃을 안 건드림 → 팬이 0 도달 → 댐퍼 이동.
42	- **HECO2:** 명령 핸들러가 모드변경 즉시 `Fan_Speed_Setting()` 직접 호출 → 팬 타깃이 곧바로 운전속도로 올라감 → **팬이 0에 도달 못 함 → `if(Fan!=0) if(wait==5) goto PASS1` 게이트가 `Damper_Mode` 영원히 스킵 → 댐퍼 미동작, 팬만 정상.**
43	- 마스터엔 없고 HECO2에만 추가된 명령경로 `Fan_Speed_Setting` 호출이 게이트를 막은 것.
44
45	## 변경 파일/내용 (1차 = 모드 변경 시 호출만 제거, 풍량단수 변경 호출은 유지)
46	주석 처리(삭제 아님, 복원 용이). 마커: `[모드변경댐퍼테스트]`
47	- `Source/HECO2/User/My_Homenet.c`
48	  - L259 CTRL_POWER(전원ON→환기) `Fan_Speed_Setting` 주석
49	  - L293 CTRL_RUNMODE(운전모드 변경) `Fan_Speed_Setting` 주석  ← 보고 증상(대시보드 환기→공청)의 직접 경로
50	  - **L306 CTRL_FAN(풍량단수만 변경)은 유지** (댐퍼 무관, 팬 단수 반응 필요)
51	- `Source/HECO2/User/My_Hood.c`
52	  - L192 후드ON(→환기) `Fan_Speed_Setting` 주석
53	  - L207 후드OFF(모드 복귀) `Fan_Speed_Setting` 주석
54	  - **L219 후드 단수변경(풍량만)은 유지**
55
56	## 핵심 결정
57	- 모드 변경 후 팬 복원은 `Fan_Speed_process`의 wait==1 호출(MyMotor.c 1018/1056/1090/1124, ③)이 담당 → 팬이 꺼진 채 남지 않음.
58	- RJ2(`My_RJ2.c`)·분배기(`My_bunbaegi.c`)는 두 소스 동일 → 미수정.
59	- 이번엔 "동작모드 변경 트리거 복원(①, 디퓨저 시퀀스 명령트리거)"은 보류 — ②(땜빵) 제거만으로 댐퍼가 움직이는지 먼저 검증.
60
61	## 빌드 결과
62	- `cd SOURCE/HECO2 && bash build.sh all` → 성공, 경고/오류 0.
63	- text 43920 / data 1940 / bss 3388 (HERV.elf). (이전 44008 → 코드 제거로 감소)
64
65	## 미해결 / 후속
66	- 실장비/시뮬레이터에서 **환기→공청 시 내부댐퍼 이동** 확인 필요(특히 고풍량 전환 시 팬 감속 대기시간 체감).
67	- 검증 후: 필요하면 ①(동작모드 변경 트리거 = MyMotor.c 디퓨저/팬복원 타이밍의 명령 트리거)도 마스터 정렬 검토.
68	- 풍량단수만 바꿀 때(L306/L219) 팬 즉시 반응 정상인지 확인.
69
70	---
71
72	# 260618 (2) 스마트수면 모드 — 거실 댐퍼 미닫힘 수정
73
74	## 증상
75	- 스마트수면 동작 중 **거실 댐퍼가 열려 있음**. 사양(개발사양서 8p)상 거실은 항상 CLOSE여야 함.
76
77	## 사양 (개발사양서 8p, 스마트수면)
78	- 환기 수동·풍량 1단 고정.
79	- 초기상태: **거실 CLOSE, 침실1~3 OPEN**.
80	- 거실 급기(SA)/배기(RA) 모두 **X(항상 닫힘)** — CO2 무관.
81	- 침실1~3: CO2 >= 1000 PPM → 해당 침실 OPEN, CO2 <= 800 PPM → CLOSE.
82
83	## 원인
84	- `My_system.c` `Air_Quality_damper_process()` `Ext_Run_Mode == 4` 블록.
85	- 진입 1회(L905)는 거실(room1)을 올바르게 CLOSE 하지만, **매 틱 CO2 루프가 `Room_Num = 1`부터 돌아(L916) 거실까지 CO2 기준으로 다시 OPEN** 시킴.
86	- 룸 인덱스: room1=거실, room2~4=침실1~3.
87
88	## 변경 파일/내용
89	- `Source/HECO2/User/My_system.c` (스마트수면 매 틱 처리)
90	  - 거실(room1) 매 틱 강제 CLOSE + LED off 추가.
91	  - CO2 히스테리시스 루프 시작을 `Room_Num = 1` → **`Room_Num = 2`** (침실1~3만 개폐).
92
93	## 빌드 결과
94	- `bash build.sh all` → 성공, 경고/오류 0. text 43940 / data 1940 / bss 3388.
95
96	## 후속
97	- 실장비에서 스마트수면 진입 시 거실 닫힘 + 침실 CO2 개폐 확인.
98
99	---
100
101	# 260618 (3) 내부댐퍼 미동작 — 진짜 원인(타이밍 레이스) 수정
102
103	## 추가 진단 (사용자 확인)
104	- 테스트 경로 = **PC 대시보드**(My_Homenet 경로).
105	- 팬 거동 = **0까지 안 내려감**, 그리고 **간헐적**(어쩌다 정상 댐퍼 동작).
106	- → 전형적 **타이밍 레이스**. (1)차 수정으로 CTRL_RUNMODE(293)는 막았지만, 대시보드가 모드변경과 함께 보내는 **CTRL_FAN(306, 유지됨)** 이 모드전환 감속창(팬→0 구간)에 끼어드는 타이밍이면 팬 타깃을 다시 켜서 팬이 0 도달 못 함 → Damper_Mode 게이트 안 열림.
107	- 끼어드는 타이밍이 아니면 정상 → 간헐성 설명됨.
108
109	## 수정 (2차 = 게이트 보호, 단일 지점)
110	- `Source/HECO2/User/MyMotor.c` `Fan_Speed_process()` PASS1 직후(정상운전 분기, VSP 제외):
111	  - `Damper_wait_time == 5`(=모드전환 진행중 전용 신호. 4개 모드 진입에서만 set, Step 정렬 전까지 5 유지)일 때
112	    `Target_Fan1_Speed = 0; Target_Fan2_Speed = 0;` **강제** → 명령경로(CTRL_FAN 등)가 감속창에 끼어들어도 무시하고 팬을 0으로 떨굼 → 댐퍼 이동 보장.
113	  - 전환 완료(Step_Status==0x3F) 후 wait가 5→1로 감소하며 wait==1에서 Fan_Speed_Setting(③)이 팬 복원.
114	- 풍량단수만 변경(같은 모드, wait==0)은 force-zero 미적용 → CTRL_FAN(306) 즉시 반응 유지.
115
116	## 동작 변화(주의)
117	- 이제 환기→공청 전환 시 **팬이 잠깐 0까지 멈췄다가**(댐퍼 이동) 다시 도는 게 정상(설계상 공기흐름 중 댐퍼 미동작). 기존 "안 멈추던" 거동에서 바뀜.
118
119	## 빌드 결과
120	- `bash build.sh all` → 성공, 경고/오류 0. text 43948.
121
122	## 후속
123	- 실장비에서 환기→공청 **반복 전환** 시 매번 댐퍼 이동(간헐성 사라짐) 확인.
124	- 같은 모드 풍량단수 변경 시 팬 즉시 반응 정상 확인.
125
126	---
127
128	# 260618 (4) 쾌적조리 재선택 안됨 — 대시보드 버튼 강조 버그
129
130	## 증상
131	- 쾌적조리 동작 마치고 이전 모드 복귀 후 다시 쾌적조리 버튼 누르면 선택(ON)이 안 됨. 눌러도 토글 반응이 안 보임.
132
133	## 사양 (개발사양서 9p)
134	- 쾌적조리 = 운전모드가 아닌 **'후드 연동 UI 토글 스위치'**. 사용자가 켤 때까지 ON 유지.
135	- 3.1 매트릭스: 토글 ON + 후드꺼짐 = 대기(본래 모드 가동), 토글 ON + 후드켜짐 = 메이크업 강제연동.
136	- 3.3 Roll-back: 후드 OFF 후 20분 환기 약 후 이전 모드 복귀 — 이는 메이크업 복귀 타이밍이지 토글 OFF가 아님.
137	- → 토글은 **계속 armed**가 정상. 펌웨어 `Hood_YeunDong_Enable`(토글)은 정상.
138
139	## 원인 (순수 대시보드 버그)
140	- `_state.ComfortCook` = status byte4 bit1 = 펌웨어 `Hood_YeunDong_Enable`(토글 Enable).
141	- 대시보드가 **토글 판단은 `_state.ComfortCook`(L386), 버튼 강조는 `_state.HoodRunning`(L581, 후드 실제 가동중)** 으로 서로 다른 변수 사용.
142	- 대기/Roll-back 상태(HoodRunning=0, ComfortCook=1)에서 버튼이 꺼져 보이고, 다시 누르면 `next=!ComfortCook=false` → OFF 전송 → "선택 안됨".
143
144	## 변경 파일/내용
145	- `TestProgram/PCDashBoard/MainWindow.xaml.cs` L581:
146	  - `SetActive(ComfortCookBtn, _state.HoodRunning);` → `SetActive(ComfortCookBtn, _state.ComfortCook);`
147	  - 토글 상태(=Enable)를 강조하도록 일치. (모드/풍량 잠금 `subActive`는 사양 3.1대로 HoodRunning 유지 — 대기 중 본래 모드 조작 허용.)
148
149	## 빌드 결과
150	- `dotnet build ErvDashboard.csproj` → 오류 0 (NU1701 경고 9개는 기존 패키지 호환 경고, 무관).
151
152	## 미해결 / 후속(선택)
153	- 사양상 쾌적조리는 운전모드 시나리오(스마트수면/안심회복)와 **독립**인데, 대시보드는 셋을 상호배타 처리(SubMode_Click L401~406, ApplySubModeLocal). 스마트수면/안심회복 켤 때 쾌적조리가 강제 OFF됨 → 사양과 불일치 가능. 필요 시 분리 검토.
154
155	---
156
157	# 260618 (5) 쾌적조리 버튼 활성(Enable)이 후드 상태에 묶임 — 독립 토글로 수정
158
159	## 증상
160	- 쾌적조리 버튼이 디폴트 비활성, 전원 ON/후드 연결 시에야 토글됨. 사양 9p 3.1상 쾌적조리는 후드 연결과 무관한 독립 토글.
161
162	## 원인
163	- `MainWindow.xaml.cs` L587 `ComfortCookBtn.IsEnabled = !subActive || _state.ComfortCook;`
164	- `subActive = SmartSleep || HoodRunning || ReliefRecover` 에 **HoodRunning(후드 가동중)** 포함 → 후드 상태가 쾌적조리 버튼 활성을 좌우.
165
166	## 변경 파일/내용
167	- `TestProgram/PCDashBoard/MainWindow.xaml.cs` L587:
168	  - `ComfortCookBtn.IsEnabled = !subActive || _state.ComfortCook;` → `ComfortCookBtn.IsEnabled = true;`
169	  - 쾌적조리는 독립 토글 → 후드 연결/가동·다른 시나리오와 무관하게 항상 토글 가능. (모드/풍량 버튼은 기존대로 subActive 잠금 유지.)
170
171	## 빌드/배포
172	- `dotnet publish ErvDashboard.csproj -c Release` → 오류 0. publish 단일 exe 갱신.
173

# 260618 수정 요약 (핸드오프용)

> 작업 PC 이전 시점 기준. 아래 상세 항목 (1)~(5) 의 최종 변경 정리.

## 변경 파일 (커밋 포함)
| 파일 | 변경 요지 | 관련 항목 |
|---|---|---|
| `Source/HECO2/User/MyMotor.c` | 모드전환 중(`Damper_wait_time==5`) 팬 강제 0 → 내부댐퍼 이동 게이트 보호 | (3) |
| `Source/HECO2/User/My_Homenet.c` | 모드변경 시 명령경로 즉시 `Fan_Speed_Setting` 호출 주석(L259/293) — 마스터 정렬 | (1) |
| `Source/HECO2/User/My_Hood.c` | 후드연동 모드변경 시 즉시 `Fan_Speed_Setting` 주석(L192/207) — 마스터 정렬 | (1) |
| `Source/HECO2/User/My_system.c` | 스마트수면 거실(room1) CO2 무관 항상 CLOSE | (2) |
| `TestProgram/PCDashBoard/MainWindow.xaml.cs` | 쾌적조리 버튼 강조=ComfortCook(L583), 활성=항상(L587) | (4)(5) |
| `개발사양서/...DL_동작로직_260613.pptx` | 사양서 갱신(바이너리) | - |

## 빌드 상태
- 펌웨어: `cd SOURCE/HECO2 && bash build.sh all` → 경고/오류 0.
- 대시보드: `dotnet publish ErvDashboard.csproj -c Release` → 오류 0 (NU1701 경고만).
- 대시보드 exe: `TestProgram/PCDashBoard/bin/Release/net10.0-windows/win-x64/publish/ErvDashboard.exe`

## 실장비/대시보드 확인 필요(테스트 대기)
- 환기→공청 반복 전환 시 내부댐퍼 매번 이동(간헐성 해소). 전환 시 팬이 잠깐 0까지 멈췄다 복귀하는 것이 정상.
- 스마트수면 진입 시 거실 닫힘 + 침실 CO2 개폐.
- 쾌적조리 버튼이 후드 연결/전원 무관하게 항상 토글되는지.

## 미적용/보류
- (선택) 쾌적조리를 스마트수면/안심회복과의 **상호배타에서 분리**(사양상 독립 토글) — SubMode_Click L401~406.
- (보류) 펌웨어 쾌적조리를 `Ext_Run_Mode==2` 상태머신으로 배선(현재 My_system 846/952 dead code). 1차 대시보드 수정으로 충분한지 검증 후 결정.
- 1차 명령경로 주석(My_Homenet/My_Hood)은 (3) 게이트 보호로 중복이나 무해 — 정리 보류.

---

# 260618 내부댐퍼 — 운전모드 변경 시 미동작 수정 (1차: 명령경로 즉시 팬설정 제거)

## 증상
- 환기 → 공청 전환 시 **팬은 정상 동작하는데 내부댐퍼(본체 6개)가 안 움직임.**

## 원인 (마스터 `D:\Project\nuvoton\HERV_DL_MH_2nd\Program` 비교로 확정)
- 내부댐퍼 이동 트리거(`Fan_Speed_process`가 모드변경 시 `Damper_Mode()` 호출)는 HECO2/마스터 **동일하게 존재**. 빠진 게 아님.
- 그 트리거는 **"팬이 0까지 감속"되어야 발동**하도록 게이팅됨(공기 흐름 중 댐퍼 미동작 의도):
  `모드변경 → 팬타깃0 → 팬=0 도달 → Damper_Mode 호출(댐퍼 이동) → 정렬(Step_Status==0x3F) → 팬 복원`
- **마스터:** 명령 핸들러가 팬 타깃을 안 건드림 → 팬이 0 도달 → 댐퍼 이동.
- **HECO2:** 명령 핸들러가 모드변경 즉시 `Fan_Speed_Setting()` 직접 호출 → 팬 타깃이 곧바로 운전속도로 올라감 → **팬이 0에 도달 못 함 → `if(Fan!=0) if(wait==5) goto PASS1` 게이트가 `Damper_Mode` 영원히 스킵 → 댐퍼 미동작, 팬만 정상.**
- 마스터엔 없고 HECO2에만 추가된 명령경로 `Fan_Speed_Setting` 호출이 게이트를 막은 것.

## 변경 파일/내용 (1차 = 모드 변경 시 호출만 제거, 풍량단수 변경 호출은 유지)
주석 처리(삭제 아님, 복원 용이). 마커: `[모드변경댐퍼테스트]`
- `Source/HECO2/User/My_Homenet.c`
  - L259 CTRL_POWER(전원ON→환기) `Fan_Speed_Setting` 주석
  - L293 CTRL_RUNMODE(운전모드 변경) `Fan_Speed_Setting` 주석  ← 보고 증상(대시보드 환기→공청)의 직접 경로
  - **L306 CTRL_FAN(풍량단수만 변경)은 유지** (댐퍼 무관, 팬 단수 반응 필요)
- `Source/HECO2/User/My_Hood.c`
  - L192 후드ON(→환기) `Fan_Speed_Setting` 주석
  - L207 후드OFF(모드 복귀) `Fan_Speed_Setting` 주석
  - **L219 후드 단수변경(풍량만)은 유지**

## 핵심 결정
- 모드 변경 후 팬 복원은 `Fan_Speed_process`의 wait==1 호출(MyMotor.c 1018/1056/1090/1124, ③)이 담당 → 팬이 꺼진 채 남지 않음.
- RJ2(`My_RJ2.c`)·분배기(`My_bunbaegi.c`)는 두 소스 동일 → 미수정.
- 이번엔 "동작모드 변경 트리거 복원(①, 디퓨저 시퀀스 명령트리거)"은 보류 — ②(땜빵) 제거만으로 댐퍼가 움직이는지 먼저 검증.

## 빌드 결과
- `cd SOURCE/HECO2 && bash build.sh all` → 성공, 경고/오류 0.
- text 43920 / data 1940 / bss 3388 (HERV.elf). (이전 44008 → 코드 제거로 감소)

## 미해결 / 후속
- 실장비/시뮬레이터에서 **환기→공청 시 내부댐퍼 이동** 확인 필요(특히 고풍량 전환 시 팬 감속 대기시간 체감).
- 검증 후: 필요하면 ①(동작모드 변경 트리거 = MyMotor.c 디퓨저/팬복원 타이밍의 명령 트리거)도 마스터 정렬 검토.
- 풍량단수만 바꿀 때(L306/L219) 팬 즉시 반응 정상인지 확인.

---

# 260618 (2) 스마트수면 모드 — 거실 댐퍼 미닫힘 수정

## 증상
- 스마트수면 동작 중 **거실 댐퍼가 열려 있음**. 사양(개발사양서 8p)상 거실은 항상 CLOSE여야 함.

## 사양 (개발사양서 8p, 스마트수면)
- 환기 수동·풍량 1단 고정.
- 초기상태: **거실 CLOSE, 침실1~3 OPEN**.
- 거실 급기(SA)/배기(RA) 모두 **X(항상 닫힘)** — CO2 무관.
- 침실1~3: CO2 >= 1000 PPM → 해당 침실 OPEN, CO2 <= 800 PPM → CLOSE.

## 원인
- `My_system.c` `Air_Quality_damper_process()` `Ext_Run_Mode == 4` 블록.
- 진입 1회(L905)는 거실(room1)을 올바르게 CLOSE 하지만, **매 틱 CO2 루프가 `Room_Num = 1`부터 돌아(L916) 거실까지 CO2 기준으로 다시 OPEN** 시킴.
- 룸 인덱스: room1=거실, room2~4=침실1~3.

## 변경 파일/내용
- `Source/HECO2/User/My_system.c` (스마트수면 매 틱 처리)
  - 거실(room1) 매 틱 강제 CLOSE + LED off 추가.
  - CO2 히스테리시스 루프 시작을 `Room_Num = 1` → **`Room_Num = 2`** (침실1~3만 개폐).

## 빌드 결과
- `bash build.sh all` → 성공, 경고/오류 0. text 43940 / data 1940 / bss 3388.

## 후속
- 실장비에서 스마트수면 진입 시 거실 닫힘 + 침실 CO2 개폐 확인.

---

# 260618 (3) 내부댐퍼 미동작 — 진짜 원인(타이밍 레이스) 수정

## 추가 진단 (사용자 확인)
- 테스트 경로 = **PC 대시보드**(My_Homenet 경로).
- 팬 거동 = **0까지 안 내려감**, 그리고 **간헐적**(어쩌다 정상 댐퍼 동작).
- → 전형적 **타이밍 레이스**. (1)차 수정으로 CTRL_RUNMODE(293)는 막았지만, 대시보드가 모드변경과 함께 보내는 **CTRL_FAN(306, 유지됨)** 이 모드전환 감속창(팬→0 구간)에 끼어드는 타이밍이면 팬 타깃을 다시 켜서 팬이 0 도달 못 함 → Damper_Mode 게이트 안 열림.
- 끼어드는 타이밍이 아니면 정상 → 간헐성 설명됨.

## 수정 (2차 = 게이트 보호, 단일 지점)
- `Source/HECO2/User/MyMotor.c` `Fan_Speed_process()` PASS1 직후(정상운전 분기, VSP 제외):
  - `Damper_wait_time == 5`(=모드전환 진행중 전용 신호. 4개 모드 진입에서만 set, Step 정렬 전까지 5 유지)일 때
    `Target_Fan1_Speed = 0; Target_Fan2_Speed = 0;` **강제** → 명령경로(CTRL_FAN 등)가 감속창에 끼어들어도 무시하고 팬을 0으로 떨굼 → 댐퍼 이동 보장.
  - 전환 완료(Step_Status==0x3F) 후 wait가 5→1로 감소하며 wait==1에서 Fan_Speed_Setting(③)이 팬 복원.
- 풍량단수만 변경(같은 모드, wait==0)은 force-zero 미적용 → CTRL_FAN(306) 즉시 반응 유지.

## 동작 변화(주의)
- 이제 환기→공청 전환 시 **팬이 잠깐 0까지 멈췄다가**(댐퍼 이동) 다시 도는 게 정상(설계상 공기흐름 중 댐퍼 미동작). 기존 "안 멈추던" 거동에서 바뀜.

## 빌드 결과
- `bash build.sh all` → 성공, 경고/오류 0. text 43948.

## 후속
- 실장비에서 환기→공청 **반복 전환** 시 매번 댐퍼 이동(간헐성 사라짐) 확인.
- 같은 모드 풍량단수 변경 시 팬 즉시 반응 정상 확인.

---

# 260618 (4) 쾌적조리 재선택 안됨 — 대시보드 버튼 강조 버그

## 증상
- 쾌적조리 동작 마치고 이전 모드 복귀 후 다시 쾌적조리 버튼 누르면 선택(ON)이 안 됨. 눌러도 토글 반응이 안 보임.

## 사양 (개발사양서 9p)
- 쾌적조리 = 운전모드가 아닌 **'후드 연동 UI 토글 스위치'**. 사용자가 켤 때까지 ON 유지.
- 3.1 매트릭스: 토글 ON + 후드꺼짐 = 대기(본래 모드 가동), 토글 ON + 후드켜짐 = 메이크업 강제연동.
- 3.3 Roll-back: 후드 OFF 후 20분 환기 약 후 이전 모드 복귀 — 이는 메이크업 복귀 타이밍이지 토글 OFF가 아님.
- → 토글은 **계속 armed**가 정상. 펌웨어 `Hood_YeunDong_Enable`(토글)은 정상.

## 원인 (순수 대시보드 버그)
- `_state.ComfortCook` = status byte4 bit1 = 펌웨어 `Hood_YeunDong_Enable`(토글 Enable).
- 대시보드가 **토글 판단은 `_state.ComfortCook`(L386), 버튼 강조는 `_state.HoodRunning`(L581, 후드 실제 가동중)** 으로 서로 다른 변수 사용.
- 대기/Roll-back 상태(HoodRunning=0, ComfortCook=1)에서 버튼이 꺼져 보이고, 다시 누르면 `next=!ComfortCook=false` → OFF 전송 → "선택 안됨".

## 변경 파일/내용
- `TestProgram/PCDashBoard/MainWindow.xaml.cs` L581:
  - `SetActive(ComfortCookBtn, _state.HoodRunning);` → `SetActive(ComfortCookBtn, _state.ComfortCook);`
  - 토글 상태(=Enable)를 강조하도록 일치. (모드/풍량 잠금 `subActive`는 사양 3.1대로 HoodRunning 유지 — 대기 중 본래 모드 조작 허용.)

## 빌드 결과
- `dotnet build ErvDashboard.csproj` → 오류 0 (NU1701 경고 9개는 기존 패키지 호환 경고, 무관).

## 미해결 / 후속(선택)
- 사양상 쾌적조리는 운전모드 시나리오(스마트수면/안심회복)와 **독립**인데, 대시보드는 셋을 상호배타 처리(SubMode_Click L401~406, ApplySubModeLocal). 스마트수면/안심회복 켤 때 쾌적조리가 강제 OFF됨 → 사양과 불일치 가능. 필요 시 분리 검토.

---

# 260618 (5) 쾌적조리 버튼 활성(Enable)이 후드 상태에 묶임 — 독립 토글로 수정

## 증상
- 쾌적조리 버튼이 디폴트 비활성, 전원 ON/후드 연결 시에야 토글됨. 사양 9p 3.1상 쾌적조리는 후드 연결과 무관한 독립 토글.

## 원인
- `MainWindow.xaml.cs` L587 `ComfortCookBtn.IsEnabled = !subActive || _state.ComfortCook;`
- `subActive = SmartSleep || HoodRunning || ReliefRecover` 에 **HoodRunning(후드 가동중)** 포함 → 후드 상태가 쾌적조리 버튼 활성을 좌우.

## 변경 파일/내용
- `TestProgram/PCDashBoard/MainWindow.xaml.cs` L587:
  - `ComfortCookBtn.IsEnabled = !subActive || _state.ComfortCook;` → `ComfortCookBtn.IsEnabled = true;`
  - 쾌적조리는 독립 토글 → 후드 연결/가동·다른 시나리오와 무관하게 항상 토글 가능. (모드/풍량 버튼은 기존대로 subActive 잠금 유지.)

## 빌드/배포
- `dotnet publish ErvDashboard.csproj -c Release` → 오류 0. publish 단일 exe 갱신.


이전 작업 맥락이 `doc/260618_...md` 핸드오프 문서에 잘 정리돼 있습니다. 메모리 파일 2개도 확인하겠습니다.


1	---
2	name: internal-damper-fan-gate
3	description: 내부댐퍼(본체 6개)는 팬이 0까지 감속해야 모드별로 이동하는 게이팅 설계
4	metadata:
5	  node_type: memory
6	  type: project
7	  originSessionId: 5209cdb2-53ba-4f8b-9aa0-4a7911424cf1
8	---
9
10	HECO2 내부댐퍼(본체 6개: EA/OA/BYPASS/SA/RA/공청, `Damper_Mode()` in MyMotor.c)는 운전모드 변경 시 **"팬 타깃0 → 팬이 실제 0 도달 → Damper_Mode 호출(댐퍼 이동) → Step_Status==0x3F 정렬 → 팬 복원"** 순서로 동작한다. 공기 흐르는 중 댐퍼를 안 움직이려는 의도. `Damper_wait_time == 5` 가 "모드전환 진행중" 전용 신호.
11
12	**Why:** 명령경로(대시보드 `My_Homenet.c` CTRL_FAN 등)가 모드전환 감속창에 끼어들어 `Fan_Speed_Setting()` 으로 팬 타깃을 다시 켜면, 팬이 0에 도달 못 해 게이트가 안 열리고 내부댐퍼가 (간헐적으로) 안 움직인다. 2026-06-18 이 버그를 수정함.
13
14	**How to apply:** 모드전환 중 팬은 명령경로와 무관하게 0으로 강제해야 한다. 수정 위치 = MyMotor.c `Fan_Speed_process()` PASS1 직후 정상운전 분기: `if(Damper_wait_time == 5){ Target_Fan1_Speed=0; Target_Fan2_Speed=0; }`. 같은 모드 풍량단수만 변경(wait==0)은 즉시 반응 유지. 마스터 [[herv-master-source]] 는 애초에 명령경로에서 Fan_Speed_Setting 을 호출하지 않아 이 문제가 없었다.
15

---
name: internal-damper-fan-gate
description: 내부댐퍼(본체 6개)는 팬이 0까지 감속해야 모드별로 이동하는 게이팅 설계
metadata:
  node_type: memory
  type: project
  originSessionId: 5209cdb2-53ba-4f8b-9aa0-4a7911424cf1
---

HECO2 내부댐퍼(본체 6개: EA/OA/BYPASS/SA/RA/공청, `Damper_Mode()` in MyMotor.c)는 운전모드 변경 시 **"팬 타깃0 → 팬이 실제 0 도달 → Damper_Mode 호출(댐퍼 이동) → Step_Status==0x3F 정렬 → 팬 복원"** 순서로 동작한다. 공기 흐르는 중 댐퍼를 안 움직이려는 의도. `Damper_wait_time == 5` 가 "모드전환 진행중" 전용 신호.

**Why:** 명령경로(대시보드 `My_Homenet.c` CTRL_FAN 등)가 모드전환 감속창에 끼어들어 `Fan_Speed_Setting()` 으로 팬 타깃을 다시 켜면, 팬이 0에 도달 못 해 게이트가 안 열리고 내부댐퍼가 (간헐적으로) 안 움직인다. 2026-06-18 이 버그를 수정함.

**How to apply:** 모드전환 중 팬은 명령경로와 무관하게 0으로 강제해야 한다. 수정 위치 = MyMotor.c `Fan_Speed_process()` PASS1 직후 정상운전 분기: `if(Damper_wait_time == 5){ Target_Fan1_Speed=0; Target_Fan2_Speed=0; }`. 같은 모드 풍량단수만 변경(wait==0)은 즉시 반응 유지. 마스터 [[herv-master-source]] 는 애초에 명령경로에서 Fan_Speed_Setting 을 호출하지 않아 이 문제가 없었다.


1	---
2	name: herv-master-source
3	description: 검증된 마스터 펌웨어 소스 위치 (HECO2 댐퍼/팬 로직 비교 기준)
4	metadata:
5	  node_type: memory
6	  type: reference
7	  originSessionId: 5209cdb2-53ba-4f8b-9aa0-4a7911424cf1
8	---
9
10	`D:\Project\nuvoton\HERV_DL_MH_2nd\Program` 가 사용자가 지정한 **마스터(검증된) 펌웨어 소스**다. HECO2 펌웨어의 댐퍼/팬 동작이 의심되면 이 트리와 diff 해서 회귀를 찾는다.
11
12	차이 요약: MyMotor.c 의 댐퍼 구동 코드(Damper_Mode/Step_process/Fan_Speed_process 게이팅)는 거의 동일. 통신부가 다름 — 마스터는 `My_Uart.c`(CVnet 월패드), HECO2 는 `My_Homenet.c`(RJ2/홈넷). My_RJ2.c·pwm_duty10000.c 는 동일. My_system.c 는 HECO2 가 크게 다름(DL 각실제어/시나리오 신규).
13
14	마스터 설계 원칙: 명령경로(CVnet/RJ2)는 `Fan_Speed_Setting` 을 절대 호출 안 함 — 팬 타깃은 `Fan_Speed_process`(게이팅) + `Diffuser_Damper_process` 만 관리. 관련 [[internal-damper-fan-gate]].
15

---
name: herv-master-source
description: 검증된 마스터 펌웨어 소스 위치 (HECO2 댐퍼/팬 로직 비교 기준)
metadata:
  node_type: memory
  type: reference
  originSessionId: 5209cdb2-53ba-4f8b-9aa0-4a7911424cf1
---

`D:\Project\nuvoton\HERV_DL_MH_2nd\Program` 가 사용자가 지정한 **마스터(검증된) 펌웨어 소스**다. HECO2 펌웨어의 댐퍼/팬 동작이 의심되면 이 트리와 diff 해서 회귀를 찾는다.

차이 요약: MyMotor.c 의 댐퍼 구동 코드(Damper_Mode/Step_process/Fan_Speed_process 게이팅)는 거의 동일. 통신부가 다름 — 마스터는 `My_Uart.c`(CVnet 월패드), HECO2 는 `My_Homenet.c`(RJ2/홈넷). My_RJ2.c·pwm_duty10000.c 는 동일. My_system.c 는 HECO2 가 크게 다름(DL 각실제어/시나리오 신규).

마스터 설계 원칙: 명령경로(CVnet/RJ2)는 `Fan_Speed_Setting` 을 절대 호출 안 함 — 팬 타깃은 `Fan_Speed_process`(게이팅) + `Diffuser_Damper_process` 만 관리. 관련 [[internal-damper-fan-gate]].


이전 대화 맥락을 모두 복원했습니다. 직전 세션(2026-06-18)은 **내부댐퍼 미동작 + 스마트수면 + 쾌적조리** 3건을 수정한 작업이었고, 다음과 같이 정리됩니다.

## 직전 세션 요약 (2026-06-18, 커밋 `c5e4c48`)

마스터 검증 소스(`D:\Project\nuvoton\HERV_DL_MH_2nd\Program`)와 비교해 회귀를 찾아 수정한 작업입니다.

| # | 증상 | 원인 | 수정 |
|---|------|------|------|
| **(1)(3)** | 환기→공청 전환 시 **내부댐퍼(본체 6개) 미동작**, 팬은 정상·간헐적 | 명령경로(`CTRL_RUNMODE`/`CTRL_FAN`)가 모드전환 감속창에 끼어들어 `Fan_Speed_Setting()`으로 팬 타깃을 다시 켬 → 팬이 0에 도달 못 함 → `Damper_Mode` 게이트가 안 열림 | `My_Homenet.c`/`My_Hood.c` 모드변경 시 즉시 팬설정 호출 주석 처리 + **핵심:** `MyMotor.c` `Fan_Speed_process()`에서 `Damper_wait_time==5`(모드전환 중)이면 팬 강제 0 |
| **(2)** | 스마트수면 중 **거실 댐퍼 열려있음** (사양상 항상 CLOSE) | 매 틱 CO2 루프가 `Room_Num=1`(거실)부터 돌아 다시 OPEN | `My_system.c` 거실 강제 CLOSE + 루프 시작 `Room_Num=2`(침실만 개폐) |
| **(4)** | 쾌적조리 버튼 **재선택 안 됨** | 토글 판단은 `ComfortCook`, 버튼 강조는 `HoodRunning`으로 서로 다른 변수 사용 | `MainWindow.xaml.cs` L581 강조를 `_state.ComfortCook`으로 일치 |
| **(5)** | 쾌적조리 버튼이 후드 상태에 묶여 비활성 | `subActive`에 `HoodRunning` 포함 | L587 `IsEnabled = true` (독립 토글) |

**빌드 상태:** 펌웨어 `bash build.sh all` 경고/오류 0 (text 43948), 대시보드 publish 오류 0 — 모두 빌드 완료, **실장비 확인 대기** 상태.

## 핵심 설계 원칙 (메모리에 저장됨)
- **내부댐퍼는 팬이 0까지 감속해야** 모드별로 이동하는 게이팅 설계 → 모드전환 시 팬을 명령경로와 무관하게 0으로 강제해야 함.
- 마스터 소스는 명령경로에서 `Fan_Speed_Setting`을 **절대 호출 안 함** — 이게 HECO2와의 핵심 차이.

## 아직 남은 것 (테스트 대기 / 보류)
1. **실장비 검증** — 환기↔공청 반복 전환 시 매번 댐퍼 이동(간헐성 해소), 스마트수면 거실 닫힘, 쾌적조리 항상 토글.
   - ⚠ 동작 변화: 이제 모드 전환 시 **팬이 잠깐 0까지 멈췄다 복귀**하는 게 정상.
2. **(보류)** 쾌적조리를 스마트수면/안심회복과의 상호배타에서 **분리**(사양상 독립 토글) — `SubMode_Click` L401~406.
3. **(보류)** 펌웨어 쾌적조리를 `Ext_Run_Mode==2` 상태머신으로 배선 (현재 `My_system.c` 846/952가 dead code).

---

어디서 이어가시겠습니까? 실장비 테스트 결과를 반영하실 거면 그 결과를 알려주시고, 보류 항목(2)·(3) 중 하나를 진행하실 거면 말씀해 주세요.


마지막으로 빌드한게 뭐지..펌웨어..대시보드?


실제 빌드 산출물 시각을 확인해서 답하겠습니다.


PowerShell

$f = Get-ChildItem "d:\project\nuvoton\HECO2\SOURCE\HECO2\build\HERV.bin","d:\project\nuvoton\HECO2\SOURCE\HECO2\build\HERV.elf" -ErrorAction SilentlyContinue; $d = Get-ChildItem "d:\project\nuvoton\HERV\TestProgram\PCDashBoard\bin\Release\net10.0-windows\win-x64\publish\ErvDashboard.exe" -ErrorAction SilentlyContinue; $f + $d | Sort-Object LastWriteTime | Select-Object LastWriteTime, FullName | Format-Table -AutoSize

Compare build artifact timestamps


(PowerShell completed with no output)


No files found


$elf = Get-ChildItem -Path "d:\project\nuvoton" -Recurse -Include "HERV.elf","HERV.bin" -ErrorAction SilentlyContinue; $exe = Get-ChildItem -Path "d:\project\nuvoton" -Recurse -Include "ErvDashboard.exe","ErvCollector.exe" -ErrorAction SilentlyContinue; $elf + $exe | Sort-Object LastWriteTime | Select-Object LastWriteTime, FullName | Format-Table -AutoSize -Wrap

Find and compare firmware/dashboard build artifact times


LastWriteTime         FullName
-------------         --------
2026-05-22 오후 8:06:07 D:\project\nuvoton\HERV_DL_MH_2nd\Program\build\HERV.elf
2026-05-22 오후 8:06:08 D:\project\nuvoton\HERV_DL_MH_2nd\Program\build\HERV.bin


"--- HECO2 build dir ---"; Test-Path "d:\project\nuvoton\HECO2\SOURCE\HECO2\build"; Get-ChildItem "d:\project\nuvoton\HECO2\SOURCE\HECO2\build" -ErrorAction SilentlyContinue | Select-Object Name,LastWriteTime; "--- HECO2 TestProgram dir ---"; Test-Path "d:\project\nuvoton\HECO2\TestProgram"; "--- any exe under HECO2 TestProgram ---"; Get-ChildItem "d:\project\nuvoton\HECO2\TestProgram" -Recurse -Include *.exe -ErrorAction SilentlyContinue | Select-Object LastWriteTime,FullName

Check HECO2 build and TestProgram outputs


--- HECO2 build dir ---
False
--- HECO2 TestProgram dir ---
True
--- any exe under HECO2 TestProgram ---


확인 결과를 정확히 말씀드리면 — **이 PC에는 HECO2 펌웨어도, 대시보드도 빌드 산출물이 없습니다.**

| 항목 | 빌드 산출물 위치 | 상태 |
|------|------------------|------|
| HECO2 펌웨어 | `SOURCE/HECO2/build/` | **폴더 자체가 없음** (미빌드) |
| 대시보드 | `TestProgram/.../ErvDashboard.exe` | **exe 없음** (미빌드) |
| (참고) 마스터 펌웨어 | `HERV_DL_MH_2nd\Program\build\HERV.bin/.elf` | 2026-05-22 빌드 — 비교용 검증소스 |

디스크에 실제로 존재하는 빌드 결과물은 **마스터(검증) 펌웨어(5/22)** 하나뿐입니다.

이유는 핸드오프 문서 첫 줄에 적힌 **"작업 PC 이전 시점 기준"** — 즉 2026-06-18 빌드(`bash build.sh all` 성공, 대시보드 publish 성공)는 **이전 PC에서 한 것**이고, 이 PC로 옮긴 뒤로는 `build/` 폴더나 exe가 따라오지 않았습니다 (소스만 git으로 동기화됨).

정리하면:
- 문서상 "마지막 빌드"는 **펌웨어 + 대시보드 둘 다** (260618 세션, 이전 PC).
- 하지만 **이 PC 기준으로는 둘 다 아직 빌드 안 된 상태**입니다.

지금 이 PC에서 다시 빌드해서 산출물을 만들까요? 원하시면 펌웨어부터(`cd SOURCE/HECO2 && bash build.sh all`) 돌리겠습니다.


펌웨어부터 빌드해 줘.


Bash

cd "d:/project/nuvoton/HECO2/SOURCE/HECO2" && bash build.sh all 2>&1 | tail -40

Build HECO2 firmware


[CC]  User/MyMotor.c
[CC]  User/My_Hood.c
[CC]  User/My_RJ2.c
[CC]  User/My_bunbaegi.c
[CC]  User/My_system.c
[CC]  User/main.c
[CC]  User/pwm_duty10000.c
[CC]  Library/StdDriver/src/EEPROM_Emulate.c
[CC]  Library/StdDriver/src/adc.c
[CC]  Library/StdDriver/src/clk.c
[CC]  Library/StdDriver/src/crc.c
[CC]  Library/StdDriver/src/dac.c
[CC]  Library/StdDriver/src/ebi.c
[CC]  Library/StdDriver/src/fmc.c
[CC]  Library/StdDriver/src/gpio.c
[CC]  Library/StdDriver/src/i2c.c
[CC]  Library/StdDriver/src/i2s.c
[CC]  Library/StdDriver/src/lcd.c
[CC]  Library/StdDriver/src/pdma.c
[CC]  Library/StdDriver/src/retarget.c
[CC]  Library/StdDriver/src/rtc.c
[CC]  Library/StdDriver/src/sc.c
[CC]  Library/StdDriver/src/scuart.c
[CC]  Library/StdDriver/src/spi.c
[CC]  Library/StdDriver/src/sys.c
[CC]  Library/StdDriver/src/timer.c
[CC]  Library/StdDriver/src/uart.c
[CC]  Library/StdDriver/src/usbd.c
[CC]  Library/StdDriver/src/wwdt.c
[CC]  Library/StdDriver/src/wdt.c
[CC]  Library/Device/Nuvoton/Nano100Series/Source/system_Nano100Series.c
[CC]  Library/Device/Nuvoton/Nano100Series/Source/GCC/_syscalls.c
[AS]  Library/Device/Nuvoton/Nano100Series/Source/GCC/startup_Nano100Series.S
[LD]  build/HERV.elf
[HEX] build/HERV.hex
[BIN] build/HERV.bin

   text	   data	    bss	    dec	    hex	filename
  43948	   1940	   3388	  49276	   c07c	build/HERV.elf


펌웨어 빌드 성공 — **경고/오류 0**.

| 항목 | 값 |
|------|-----|
| text | 43948 |
| data | 1940 |
| bss | 3388 |
| 산출물 | `build/HERV.elf` / `.hex` / `.bin` |

`text 43948`은 핸드오프 문서의 260618 (3)차 수정 후 기대값과 **정확히 일치**합니다 — 즉 이전 PC에서 빌드했던 그 소스가 git으로 잘 동기화됐고, 이 PC에서 재빌드한 결과도 동일합니다.

이제 `SOURCE/HECO2/build/`에 산출물이 생겼습니다. 다음으로 **대시보드(`ErvDashboard.csproj`)도 빌드**할까요?


그래


ls -la "d:/project/nuvoton/HECO2/TestProgram/PCDashBoard/ErvDashboard.csproj" 2>&1; echo "---"; ls "d:/project/nuvoton/HECO2/TestProgram/" 2>&1

Check dashboard csproj location


-rw-r--r-- 1 user 197609 1188 Jun 18 19:10 d:/project/nuvoton/HECO2/TestProgram/PCDashBoard/ErvDashboard.csproj
---
ErvProtocol
PCDashBoard
PC_ERV_Protocol.md
WebDashBoard
사양서_260604.txt


csproj는 HECO2 트리(`d:\project\nuvoton\HECO2\TestProgram\PCDashBoard`)에 있습니다. 이걸로 publish 하겠습니다.


PowerShell

dotnet publish "d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\ErvDashboard.csproj" -c Release

Publish PC dashboard (Release)


  복원할 프로젝트를 확인하는 중...
  d:\project\nuvoton\HECO2\TestProgram\ErvProtocol\ErvProtocol.csproj을(를) 162밀리초 동안 복원했습니다.
d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\ErvDashboard.csproj : warning NU1701: 프로젝트 대상 프레임워크 'net10.0-windows7.0' 대신 '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8, .NETFramework,Version=v4.8.1'을(를) 사용하여 패키지 'OpenTK 3.3.1'을(를) 복원했습니다. 이 패키지는 프로젝트와 완벽하게 호환되지 않을 수 있습니다.
d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\ErvDashboard.csproj : warning NU1701: 프로젝트 대상 프레임워크 'net10.0-windows7.0' 대신 '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8, .NETFramework,Version=v4.8.1'을(를) 사용하여 패키지 'OpenTK.GLWpfControl 3.3.0'을(를) 복원했습니다. 이 패키지는 프로젝트와 완벽하게 호환되지 않을 수 있습니다.
d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\ErvDashboard.csproj : warning NU1701: 프로젝트 대상 프레임워크 'net10.0-windows7.0' 대신 '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8, .NETFramework,Version=v4.8.1'을(를) 사용하여 패키지 'SkiaSharp.Views.WPF 3.119.1'을(를) 복원했습니다. 이 패키지는 프로젝트와 완벽하게 호환되지 않을 수 있습니다.
  d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\ErvDashboard.csproj을(를) 1.2초 동안 복원했습니다.
d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\ErvDashboard.csproj : warning NU1701: 프로젝트 대상 프레임워크 'net10.0-windows7.0' 대신 '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8, .NETFramework,Version=v4.8.1'을(를) 사용하여 패키지 'OpenTK 3.3.1'을(를) 복원했습니다. 이 패키지는 프로젝트와 완벽하게 호환되지 않을 수 있습니다.
d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\ErvDashboard.csproj : warning NU1701: 프로젝트 대상 프레임워크 'net10.0-windows7.0' 대신 '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8, .NETFramework,Version=v4.8.1'을(를) 사용하여 패키지 'OpenTK.GLWpfControl 3.3.0'을(를) 복원했습니다. 이 패키지는 프로젝트와 완벽하게 호환되지 않을 수 있습니다.
d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\ErvDashboard.csproj : warning NU1701: 프로젝트 대상 프레임워크 'net10.0-windows7.0' 대신 '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8, .NETFramework,Version=v4.8.1'을(를) 사용하여 패키지 'SkiaSharp.Views.WPF 3.119.1'을(를) 복원했습니다. 이 패키지는 프로젝트와 완벽하게 호환되지 않을 수 있습니다.
  ErvProtocol -> d:\project\nuvoton\HECO2\TestProgram\ErvProtocol\bin\Release\net10.0\ErvProtocol.dll
d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\ErvDashboard_caiwpzkl_wpftmp.csproj : warning NU1701: 프로젝트 대상 프레임워크 'net10.0-windows7.0' 대신 '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8, .NETFramework,Version=v4.8.1'을(를) 사용하여 패키지 'OpenTK 3.3.1'을(를) 복원했습니다. 이 패키지는 프로젝트와 완벽하게 호환되지 않을 수 있습니다.
d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\ErvDashboard_caiwpzkl_wpftmp.csproj : warning NU1701: 프로젝트 대상 프레임워크 'net10.0-windows7.0' 대신 '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8, .NETFramework,Version=v4.8.1'을(를) 사용하여 패키지 'OpenTK.GLWpfControl 3.3.0'을(를) 복원했습니다. 이 패키지는 프로젝트와 완벽하게 호환되지 않을 수 있습니다.
d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\ErvDashboard_caiwpzkl_wpftmp.csproj : warning NU1701: 프로젝트 대상 프레임워크 'net10.0-windows7.0' 대신 '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8, .NETFramework,Version=v4.8.1'을(를) 사용하여 패키지 'SkiaSharp.Views.WPF 3.119.1'을(를) 복원했습니다. 이 패키지는 프로젝트와 완벽하게 호환되지 않을 수 있습니다.
  ErvDashboard -> d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\bin\Release\net10.0-windows\win-x64\ErvDashboard.dll
  ErvDashboard -> d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\bin\Release\net10.0-windows\win-x64\publish\


Get-ChildItem "d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\bin\Release\net10.0-windows\win-x64\publish\ErvDashboard.exe" | Select-Object LastWriteTime, @{n='MB';e={[math]::Round($_.Length/1MB,1)}}, FullName | Format-List

Confirm published dashboard exe


LastWriteTime : 2026-06-18 오후 9:59:59
MB            : 155.5
FullName      : D:\project\nuvoton\HECO2\TestProgram\PCDashBoard\bin\Release\net10.0-windows\win-x64\publish\ErvDashboa
                rd.exe


대시보드 publish 성공 — **오류 0** (NU1701 경고만, 기존 패키지 호환 경고라 무관).

| 항목 | 값 |
|------|-----|
| 산출물 | `TestProgram\PCDashBoard\bin\Release\net10.0-windows\win-x64\publish\ErvDashboard.exe` |
| 크기 | 155.5 MB (SelfContained 단일 exe) |
| 생성 시각 | 2026-06-18 21:59 |

이제 이 PC에서 **펌웨어 + 대시보드 둘 다 빌드 완료**됐습니다.

| 항목 | 상태 | 산출물 |
|------|------|--------|
| HECO2 펌웨어 | ✅ 경고/오류 0 (text 43948) | `SOURCE/HECO2/build/HERV.bin·hex·elf` |
| PC 대시보드 | ✅ 오류 0 | `...\publish\ErvDashboard.exe` (155.5 MB) |

다음으로 하실 일이 있으면 말씀해 주세요. (실장비 플래시/테스트, 또는 보류 항목 진행 등)


지금 검토할 내용 좀 알려줘


이전 세션 핸드오프 기준으로, 지금 **검토(확인/결정)해야 할 항목**들을 우선순위로 정리합니다.

## 1. 실장비 검증 대기 — 260618 수정 4건이 실제로 동작하는지 확인 (최우선)

빌드는 됐지만 **실장비/시뮬레이터 확인이 안 된** 상태입니다.

| # | 확인 포인트 | 주의점 |
|---|-------------|--------|
| (3) 내부댐퍼 | 환기↔공청 **반복 전환** 시 내부댐퍼(본체 6개)가 **매번** 이동하는지 (간헐성 해소) | ⚠ **동작 변화** — 이제 전환 시 팬이 **잠깐 0까지 멈췄다 복귀**하는 게 정상. 기존 "안 멈추던" 거동과 다름 |
| (3) 풍량단수 | **같은 모드**에서 풍량 단수만 바꿀 때 팬이 **즉시** 반응하는지 (force-zero 안 걸려야 함) | 모드전환과 단수변경 동작 분리가 맞는지 |
| (2) 스마트수면 | 진입 시 **거실 닫힘 + 침실1~3 CO2 개폐** | 거실은 CO2 무관 항상 CLOSE |
| (4)(5) 쾌적조리 | 후드 연결/전원/다른 시나리오와 **무관하게 항상 토글**, 한번 켠 뒤 다시 눌러도 재선택되는지 | 대기/Roll-back 상태에서도 버튼 강조 유지 |

## 2. 보류 — 검증 후 결정할 사항

| 항목 | 내용 | 결정 포인트 |
|------|------|-------------|
| 쾌적조리 상호배타 분리 | 사양상 쾌적조리는 스마트수면/안심회복과 **독립**인데, 대시보드가 셋을 상호배타 처리 (`SubMode_Click` L401~406). 스마트수면 켤 때 쾌적조리가 강제 OFF됨 | 사양 위반 여부 확정 후 분리할지 |
| 펌웨어 쾌적조리 상태머신 배선 | `Ext_Run_Mode==2` 경로가 현재 dead code (`My_system.c` 846/952). 이번엔 대시보드 수정만으로 해결 시도 | **1차(대시보드) 수정으로 충분한지** 검증 후 펌웨어까지 손댈지 결정 |
| 1차 명령경로 주석 정리 | `My_Homenet.c`/`My_Hood.c`의 `Fan_Speed_Setting` 주석은 (3)차 게이트 보호로 **중복이나 무해** | 정리(삭제)할지 보류 중 |

## 핵심 의존관계
- **2번 항목들은 1번(실장비 검증)이 끝나야 결정 가능**합니다. 특히 "펌웨어 쾌적조리 배선"은 "대시보드 수정만으로 쾌적조리가 정상 동작하는가"를 먼저 봐야 합니다.

---

어느 쪽을 보시겠습니까?

1. **실장비 결과를 알려주시면** → 그에 맞춰 다음 수정/결정 진행
2. **코드를 다시 검토**하고 싶다 → 특정 수정 건의 실제 코드를 같이 열어 확인 (예: MyMotor.c force-zero 게이트가 의도대로 들어갔는지)
3. **보류 항목 중 하나를 지금 진행** (예: 쾌적조리 상호배타 분리)


쾌적조리 선택 후 후드 동작 후 후드 전원 off했는데 쾌적조리 버튼이 토글이 안돼.
이전 모드로는 운전하고..이거부터 수정하자.


쾌적조리 토글 로직의 현재 코드를 보겠습니다.


167:            ApplySubModeLocal("SmartSleep", false);
203:                SmartSleep = _state.SmartSleep, ComfortCook = _state.ComfortCook, ReliefRecover = _state.ReliefRecover,
379:        void SubMode_Click(object sender, RoutedEventArgs e)
382:            bool anyBefore = _state.SmartSleep || _state.ComfortCook || _state.ReliefRecover;
386:                "ComfortCook"   => (SubModeType.ComfortCook,   !_state.ComfortCook),
394:                ApplySubModeLocal(tag, next);
404:                if (tag != "ComfortCook"   && _state.ComfortCook)   _api.SetSubMode(SubModeType.ComfortCook,   false);
408:            ApplySubModeLocal(tag, next);
413:        bool NoScenarioActive() => !_state.SmartSleep && !_state.ComfortCook && !_state.ReliefRecover;
424:        void ApplySubModeLocal(string tag, bool on)
430:                _state.ComfortCook   = tag == "ComfortCook";
438:                    case "ComfortCook":   _state.ComfortCook = false; break;
568:            // 쾌적조리는 '연동운전중(HoodRunning=후드 가동중)' 기준으로 시나리오 활성 판단.
570:            bool subActive = _state.SmartSleep || _state.HoodRunning || _state.ReliefRecover;
575:                fb.IsEnabled = !subActive && !_state.IsAuto && sp <= fanMax;
582:            // (HoodRunning 으로 강조하면 대기/Roll-back 상태에서 버튼이 꺼져 보여 재선택 시 토글이 반대로 먹던 문제 수정)
583:            SetActive(ComfortCookBtn,   _state.ComfortCook);
586:            SmartSleepBtn.IsEnabled    = !subActive || _state.SmartSleep;
588:            ComfortCookBtn.IsEnabled   = true;
589:            ReliefRecoverBtn.IsEnabled = !subActive || _state.ReliefRecover;
592:            foreach (var mb in _modeButtons) mb.IsEnabled = !subActive;
597:            bool presetEnabled = _state.IsAuto && !subActive;


379	        void SubMode_Click(object sender, RoutedEventArgs e)
380	        {
381	            if (sender is not Button b || b.Tag is not string tag) return;
382	            bool anyBefore = _state.SmartSleep || _state.ComfortCook || _state.ReliefRecover;
383	            (SubModeType type, bool next) = tag switch
384	            {
385	                "SmartSleep"    => (SubModeType.SmartSleep,    !_state.SmartSleep),
386	                "ComfortCook"   => (SubModeType.ComfortCook,   !_state.ComfortCook),
387	                _               => (SubModeType.ReliefRecover, !_state.ReliefRecover),
388	            };
389	            // 시나리오 첫 진입 → 직전 운전모드/풍량 기억(해제 시 복귀용)
390	            if (next && !anyBefore) { _modeBeforeScenario = _state.RunMode; _fanBeforeScenario = _state.FanMode; }
391	            if (tag == "SmartSleep" && !next) _sleepEndAt = null;   // 스마트수면 수동 해제 → 자동해제 예약 취소
392	            if (_demoTimer.IsEnabled)
393	            {
394	                ApplySubModeLocal(tag, next);
395	                if (NoScenarioActive()) RestorePreviousMode();
396	                return;
397	            }
398	            if (!CanSend()) return;
399	            // 상호배타: 새 모드를 켤 때 기존 활성 모드는 장치에도 OFF 전송
400	            // (펌웨어는 시나리오모드를 독립 변수로 유지 → status 재수신 시 부활 방지)
401	            if (next)
402	            {
403	                if (tag != "SmartSleep"    && _state.SmartSleep)    _api.SetSubMode(SubModeType.SmartSleep,    false);
404	                if (tag != "ComfortCook"   && _state.ComfortCook)   _api.SetSubMode(SubModeType.ComfortCook,   false);
405	                if (tag != "ReliefRecover" && _state.ReliefRecover) _api.SetSubMode(SubModeType.ReliefRecover, false);
406	            }
407	            _api.SetSubMode(type, next);
408	            ApplySubModeLocal(tag, next);
409	            Log($"[제어] 시나리오모드 {b.Content} → {(next ? "ON" : "OFF")}");
410	            if (NoScenarioActive()) RestorePreviousMode();
411	        }
412
413	        bool NoScenarioActive() => !_state.SmartSleep && !_state.ComfortCook && !_state.ReliefRecover;
414
415	        // 시나리오모드 해제 → 진입 직전 운전모드/풍량으로 동작 복귀(이전모드로 동작).
416	        // 실연결 시 ERV(펌웨어/시뮬)도 자체 복원하므로 로컬은 즉시 반영하고 STATUS 로 재동기화.
417	        void RestorePreviousMode()
418	        {
419	            _state.RunMode = _modeBeforeScenario;
420	            _state.FanMode = _fanBeforeScenario;
421	            Log($"[시나리오] 해제 → 이전 운전모드({ModeName(_modeBeforeScenario)} {_fanBeforeScenario}단) 복귀");
422	        }
423
424	        void ApplySubModeLocal(string tag, bool on)
425	        {
426	            // 시나리오모드는 상호배타: 하나를 켜면 나머지는 해제
427	            if (on)
428	            {
429	                _state.SmartSleep    = tag == "SmartSleep";
430	                _state.ComfortCook   = tag == "ComfortCook";
431	                _state.ReliefRecover = tag == "ReliefRecover";
432	            }
433	            else
434	            {
435	                switch (tag)
436	                {
437	                    case "SmartSleep":    _state.SmartSleep = false; break;
438	                    case "ComfortCook":   _state.ComfortCook = false; break;
439	                    case "ReliefRecover": _state.ReliefRecover = false; break;
440	                }
441	            }
442	        }
443
444	        void Hood_Click(object sender, RoutedEventArgs e)
445	        {
446	            bool next = !_state.Hood;
447	            if (_demoTimer.IsEnabled) { _state.Hood = next; return; }
448	            if (!CanSend()) return;
449	            _api.SetHood(next);
450	            _state.Hood = next;
451	            Log($"[제어] 연동후드 → {(next ? "ON" : "OFF")}");
452	        }
453

        void SubMode_Click(object sender, RoutedEventArgs e)
        {
            if (sender is not Button b || b.Tag is not string tag) return;
            bool anyBefore = _state.SmartSleep || _state.ComfortCook || _state.ReliefRecover;
            (SubModeType type, bool next) = tag switch
            {
                "SmartSleep"    => (SubModeType.SmartSleep,    !_state.SmartSleep),
                "ComfortCook"   => (SubModeType.ComfortCook,   !_state.ComfortCook),
                _               => (SubModeType.ReliefRecover, !_state.ReliefRecover),
            };
            // 시나리오 첫 진입 → 직전 운전모드/풍량 기억(해제 시 복귀용)
            if (next && !anyBefore) { _modeBeforeScenario = _state.RunMode; _fanBeforeScenario = _state.FanMode; }
            if (tag == "SmartSleep" && !next) _sleepEndAt = null;   // 스마트수면 수동 해제 → 자동해제 예약 취소
            if (_demoTimer.IsEnabled)
            {
                ApplySubModeLocal(tag, next);
                if (NoScenarioActive()) RestorePreviousMode();
                return;
            }
            if (!CanSend()) return;
            // 상호배타: 새 모드를 켤 때 기존 활성 모드는 장치에도 OFF 전송
            // (펌웨어는 시나리오모드를 독립 변수로 유지 → status 재수신 시 부활 방지)
            if (next)
            {
                if (tag != "SmartSleep"    && _state.SmartSleep)    _api.SetSubMode(SubModeType.SmartSleep,    false);
                if (tag != "ComfortCook"   && _state.ComfortCook)   _api.SetSubMode(SubModeType.ComfortCook,   false);
                if (tag != "ReliefRecover" && _state.ReliefRecover) _api.SetSubMode(SubModeType.ReliefRecover, false);
            }
            _api.SetSubMode(type, next);
            ApplySubModeLocal(tag, next);
            Log($"[제어] 시나리오모드 {b.Content} → {(next ? "ON" : "OFF")}");
            if (NoScenarioActive()) RestorePreviousMode();
        }

        bool NoScenarioActive() => !_state.SmartSleep && !_state.ComfortCook && !_state.ReliefRecover;

        // 시나리오모드 해제 → 진입 직전 운전모드/풍량으로 동작 복귀(이전모드로 동작).
        // 실연결 시 ERV(펌웨어/시뮬)도 자체 복원하므로 로컬은 즉시 반영하고 STATUS 로 재동기화.
        void RestorePreviousMode()
        {
            _state.RunMode = _modeBeforeScenario;
            _state.FanMode = _fanBeforeScenario;
            Log($"[시나리오] 해제 → 이전 운전모드({ModeName(_modeBeforeScenario)} {_fanBeforeScenario}단) 복귀");
        }

        void ApplySubModeLocal(string tag, bool on)
        {
            // 시나리오모드는 상호배타: 하나를 켜면 나머지는 해제
            if (on)
            {
                _state.SmartSleep    = tag == "SmartSleep";
                _state.ComfortCook   = tag == "ComfortCook";
                _state.ReliefRecover = tag == "ReliefRecover";
            }
            else
            {
                switch (tag)
                {
                    case "SmartSleep":    _state.SmartSleep = false; break;
                    case "ComfortCook":   _state.ComfortCook = false; break;
                    case "ReliefRecover": _state.ReliefRecover = false; break;
                }
            }
        }

        void Hood_Click(object sender, RoutedEventArgs e)
        {
            bool next = !_state.Hood;
            if (_demoTimer.IsEnabled) { _state.Hood = next; return; }
            if (!CanSend()) return;
            _api.SetHood(next);
            _state.Hood = next;
            Log($"[제어] 연동후드 → {(next ? "ON" : "OFF")}");
        }


쾌적조리 선택 후 후드 동작 후 후드 전원 off했는데 쾌적조리 버튼이 토글이 안돼. 이전 모드로는 운전하고..이거부터 수정하자.


토글 로직과 상태 파싱, 펌웨어 후드 로직을 같이 봐야 정확히 짚을 수 있습니다. 병렬로 확인하겠습니다.


TestProgram\ErvProtocol\Enums.cs:41:        ComfortCook   = 0x02,  // 쾌적조리
TestProgram\ErvProtocol\Enums.cs:48:        public const byte ComfortCook   = 0x02;
TestProgram\ErvProtocol\StatusDecoder.cs:26:                Hood       = p[5],
TestProgram\ErvProtocol\CtrlFrame.cs:50:        public static byte[] Hood(int on)          => Build(CTRL_HOOD, (byte)(on != 0 ? 1 : 0));
TestProgram\ErvProtocol\StatusRecord.cs:12:        public byte Hood;         // 비트맵 bit0=연동Enable / bit1=연동운전중 / bit2=후드 통신연결(폴응답 생존)
TestProgram\ErvProtocol\StatusRecord.cs:13:        public bool HoodEnable    => (Hood & 0x01) != 0;   // 후드연동 활성
TestProgram\ErvProtocol\StatusRecord.cs:14:        public bool HoodRunning   => (Hood & 0x02) != 0;   // 후드연동에 의한 운전중
TestProgram\ErvProtocol\StatusRecord.cs:15:        public bool HoodConnected => (Hood & 0x04) != 0;   // 후드 485 통신 연결됨(ERV가 폴 응답 수신중)
TestProgram\ErvProtocol\StatusEncoder.cs:24:            p[5] = s.Hood;


SOURCE\HECO2\User\My_define.h:479:extern uint8_t Hood_YeunDong_Enable;
SOURCE\HECO2\User\My_Hood.c:123:    Tx_Hood_Buff[5] = Hood_YeunDong_Enable;  /* 연동 Enable */
SOURCE\HECO2\User\My_Hood.c:172:    if((Hood_YeunDong_Enable == 0)||(HREV_Hood_Control == 1))return(0);
SOURCE\HECO2\User\My_Homenet.c:128:                   | (Hood_YeunDong_Enable ? 0x02 : 0)   /* 쾌적조리   */
SOURCE\HECO2\User\My_Homenet.c:131:    p[5] = (uint8_t)((Hood_YeunDong_Enable ? 0x01 : 0) | ((Hood_Status != 0) ? 0x02 : 0) | ((Hood_Conn_Timeout != 0) ? 0x04 : 0));
SOURCE\HECO2\User\My_Homenet.c:318:                else if(pl[0] == 2) Hood_YeunDong_Enable = pl[1] ? 1 : 0;
SOURCE\HECO2\User\My_Homenet.c:326:            if(len >= 1) Hood_YeunDong_Enable = pl[0] ? 1 : 0; else result = 1;
SOURCE\HECO2\User\My_RJ2.c:296:	if((Hood_Yeundong_flag == 1) && (Hood_YeunDong_Enable == 1))//2023.02.14 Hood Enable Add
SOURCE\HECO2\User\My_RJ2.c:417:                        if((Memory_Hood_Status != Hood_Status)&&(Hood_YeunDong_Enable == 1))
SOURCE\HECO2\User\My_system.c:687:uint8_t Hood_YeunDong_Enable = 0;
SOURCE\HECO2\User\My_system.c:848:        Hood_YeunDong_Enable = 1;
SOURCE\HECO2\User\My_system.c:954:        Hood_YeunDong_Enable = 0;
SOURCE\HECO2\User\My_system.c:1117:        if((Hood_YeunDong_Enable == 1)&&((Hood_Status != 0)||(Hood_Yeundong_flag == 1)))
SOURCE\HECO2\User\My_Hood_old.c:127:    Tx_Hood_Buff[5] = Hood_YeunDong_Enable;
SOURCE\HECO2\User\My_Hood_old.c:182:    if((Hood_YeunDong_Enable == 0)||(HREV_Hood_Control == 1))return(0);


1	namespace ErvProtocol
2	{
3	    // STATUS(0x81) 1프레임을 디코드한 결과 (PC_ERV_Protocol.md 4장)
4	    // 필드는 raw 수치(byte/int). 소비자(WPF)가 필요 시 enum 으로 캐스팅.
5	    public sealed class StatusRecord
6	    {
7	        public byte Power;        // 0/1
8	        public byte RunMode;      // 1환기 2자동 3공청 4바이패스
9	        public byte AutoState;    // 0분산 1집중
10	        public byte FanMode;      // 0~4
11	        public byte SubMode;      // 비트맵 bit0수면 bit1조리 bit2회복
12	        public byte Hood;         // 비트맵 bit0=연동Enable / bit1=연동운전중 / bit2=후드 통신연결(폴응답 생존)
13	        public bool HoodEnable    => (Hood & 0x01) != 0;   // 후드연동 활성
14	        public bool HoodRunning   => (Hood & 0x02) != 0;   // 후드연동에 의한 운전중
15	        public bool HoodConnected => (Hood & 0x04) != 0;   // 후드 485 통신 연결됨(ERV가 폴 응답 수신중)
16	        public byte HystPreset;   // 0 ECO 1 NORMAL 2 TURBO
17	        public int  HystPm25;
18	        public int  HystPm10;
19	        public int  HystVoc;
20	        public int  HystCo2;
21	        public int  ErrorCode;    // 비트맵
22	        public byte Reset;        // ERV 리셋 토글 0/1
23	        public int  ReserveRemainSec;  // (꺼짐)예약 잔여 초 (0=예약없음) — STATUS 끝 블록
24
25	        public readonly RoomRecord[] Rooms = new RoomRecord[4];
26
27	        // 풍량 VSP 9엔트리 (VspInfo.Labels 순서: 환기1~4, 바이패스, 공청1~4)
28	        public readonly VspEntry[] Vsp = new VspEntry[9];
29
30	        // 히스테리시스 프리셋별 데드밴드(하강) [0]=ECO [1]=NORMAL [2]=TURBO
31	        public readonly HystSet[] HystTable = new HystSet[3];
32
33	        // 모드(프리셋)별 오염단계 임계 경계표 [0]=ECO [1]=NORMAL [2]=TURBO
34	        public readonly ThrSet[] ThrTable = new ThrSet[3];
35
36	        public StatusRecord()
37	        {
38	            for (int i = 0; i < 4; i++) Rooms[i] = new RoomRecord();
39	            for (int i = 0; i < 9; i++) Vsp[i] = new VspEntry();
40	            for (int i = 0; i < 3; i++) HystTable[i] = new HystSet();
41	            for (int i = 0; i < 3; i++) ThrTable[i] = new ThrSet();
42	        }
43	    }
44
45	    // 히스테리시스 한 프리셋의 데드밴드(하강폭)
46	    public sealed class HystSet
47	    {
48	        public int Pm25;
49	        public int Pm10;
50	        public int Voc;
51	        public int Co2;
52	    }
53
54	    // 한 프리셋의 오염단계 임계(각 오염원별 레벨 1~4 상한 경계)
55	    public sealed class ThrSet
56	    {
57	        public int[] Co2  = new int[4];   // L1~L4 상한
58	        public int[] Pm25 = new int[4];
59	        public int[] Pm10 = new int[4];
60	        public int[] Voc  = new int[4];
61	    }
62
63	    public sealed class VspEntry
64	    {
65	        public int Sa;   // 급기 풍량 설정값
66	        public int Ea;   // 배기 풍량 설정값
67	    }
68
69	    // 풍량 VSP 엔트리 인덱스 ↔ 라벨/group/index 매핑 (PC_ERV_Protocol.md 3.5)
70	    public static class VspInfo
71	    {
72	        public static readonly string[] Labels =
73	            { "환기1", "환기2", "환기3", "환기4", "바이패스", "공청1", "공청2", "공청3", "공청4" };
74
75	        // InfluxDB 태그/JSON 용 ASCII 키
76	        public static readonly string[] Keys =
77	            { "vent1", "vent2", "vent3", "vent4", "bypass", "air1", "air2", "air3", "air4" };
78
79	        // group: 0 Vent / 1 Bypass / 2 AirClean
80	        public static readonly int[] Group = { 0, 0, 0, 0, 1, 2, 2, 2, 2 };
81	        // index: Vent/AirClean 1~4, Bypass 1
82	        public static readonly int[] Index = { 1, 2, 3, 4, 1, 1, 2, 3, 4 };
83	        public const int Count = 9;
84	    }
85
86	    public sealed class RoomRecord
87	    {
88	        public byte Damper;       // 비트맵 : bit0=급기(SA) 열림 / bit1=배기(EA) 열림
89	        public bool DamperSa => (Damper & 0x01) != 0;   // 급기 댐퍼 열림
90	        public bool DamperEa => (Damper & 0x02) != 0;   // 배기 댐퍼 열림
91	        public int  Pm25;
92	        public int  Pm10;
93	        public int  Voc;
94	        public int  Co2;
95	        public byte AirQuality;   // 1매우나쁨~4좋음
96	        public byte LedDim;       // 0~9
97	        public int  LoadScore;    // 부하점수
98	        public byte FinalVolume;  // 최종풍량
99	        public byte Temp;         // 온도(℃)  — STATUS 끝 블록(offset 230~)
100	        public byte Humi;         // 습도(%)  — STATUS 끝 블록(offset 230~)
101	    }
102	}
103

namespace ErvProtocol
{
    // STATUS(0x81) 1프레임을 디코드한 결과 (PC_ERV_Protocol.md 4장)
    // 필드는 raw 수치(byte/int). 소비자(WPF)가 필요 시 enum 으로 캐스팅.
    public sealed class StatusRecord
    {
        public byte Power;        // 0/1
        public byte RunMode;      // 1환기 2자동 3공청 4바이패스
        public byte AutoState;    // 0분산 1집중
        public byte FanMode;      // 0~4
        public byte SubMode;      // 비트맵 bit0수면 bit1조리 bit2회복
        public byte Hood;         // 비트맵 bit0=연동Enable / bit1=연동운전중 / bit2=후드 통신연결(폴응답 생존)
        public bool HoodEnable    => (Hood & 0x01) != 0;   // 후드연동 활성
        public bool HoodRunning   => (Hood & 0x02) != 0;   // 후드연동에 의한 운전중
        public bool HoodConnected => (Hood & 0x04) != 0;   // 후드 485 통신 연결됨(ERV가 폴 응답 수신중)
        public byte HystPreset;   // 0 ECO 1 NORMAL 2 TURBO
        public int  HystPm25;
        public int  HystPm10;
        public int  HystVoc;
        public int  HystCo2;
        public int  ErrorCode;    // 비트맵
        public byte Reset;        // ERV 리셋 토글 0/1
        public int  ReserveRemainSec;  // (꺼짐)예약 잔여 초 (0=예약없음) — STATUS 끝 블록

        public readonly RoomRecord[] Rooms = new RoomRecord[4];

        // 풍량 VSP 9엔트리 (VspInfo.Labels 순서: 환기1~4, 바이패스, 공청1~4)
        public readonly VspEntry[] Vsp = new VspEntry[9];

        // 히스테리시스 프리셋별 데드밴드(하강) [0]=ECO [1]=NORMAL [2]=TURBO
        public readonly HystSet[] HystTable = new HystSet[3];

        // 모드(프리셋)별 오염단계 임계 경계표 [0]=ECO [1]=NORMAL [2]=TURBO
        public readonly ThrSet[] ThrTable = new ThrSet[3];

        public StatusRecord()
        {
            for (int i = 0; i < 4; i++) Rooms[i] = new RoomRecord();
            for (int i = 0; i < 9; i++) Vsp[i] = new VspEntry();
            for (int i = 0; i < 3; i++) HystTable[i] = new HystSet();
            for (int i = 0; i < 3; i++) ThrTable[i] = new ThrSet();
        }
    }

    // 히스테리시스 한 프리셋의 데드밴드(하강폭)
    public sealed class HystSet
    {
        public int Pm25;
        public int Pm10;
        public int Voc;
        public int Co2;
    }

    // 한 프리셋의 오염단계 임계(각 오염원별 레벨 1~4 상한 경계)
    public sealed class ThrSet
    {
        public int[] Co2  = new int[4];   // L1~L4 상한
        public int[] Pm25 = new int[4];
        public int[] Pm10 = new int[4];
        public int[] Voc  = new int[4];
    }

    public sealed class VspEntry
    {
        public int Sa;   // 급기 풍량 설정값
        public int Ea;   // 배기 풍량 설정값
    }

    // 풍량 VSP 엔트리 인덱스 ↔ 라벨/group/index 매핑 (PC_ERV_Protocol.md 3.5)
    public static class VspInfo
    {
        public static readonly string[] Labels =
            { "환기1", "환기2", "환기3", "환기4", "바이패스", "공청1", "공청2", "공청3", "공청4" };

        // InfluxDB 태그/JSON 용 ASCII 키
        public static readonly string[] Keys =
            { "vent1", "vent2", "vent3", "vent4", "bypass", "air1", "air2", "air3", "air4" };

        // group: 0 Vent / 1 Bypass / 2 AirClean
        public static readonly int[] Group = { 0, 0, 0, 0, 1, 2, 2, 2, 2 };
        // index: Vent/AirClean 1~4, Bypass 1
        public static readonly int[] Index = { 1, 2, 3, 4, 1, 1, 2, 3, 4 };
        public const int Count = 9;
    }

    public sealed class RoomRecord
    {
        public byte Damper;       // 비트맵 : bit0=급기(SA) 열림 / bit1=배기(EA) 열림
        public bool DamperSa => (Damper & 0x01) != 0;   // 급기 댐퍼 열림
        public bool DamperEa => (Damper & 0x02) != 0;   // 배기 댐퍼 열림
        public int  Pm25;
        public int  Pm10;
        public int  Voc;
        public int  Co2;
        public byte AirQuality;   // 1매우나쁨~4좋음
        public byte LedDim;       // 0~9
        public int  LoadScore;    // 부하점수
        public byte FinalVolume;  // 최종풍량
        public byte Temp;         // 온도(℃)  — STATUS 끝 블록(offset 230~)
        public byte Humi;         // 습도(%)  — STATUS 끝 블록(offset 230~)
    }
}


1	namespace ErvProtocol
2	{
3	    public static class StatusDecoder
4	    {
5	        public const byte STATUS = 0x81;   // ERV→PC 상태
6	        public const byte ACK    = 0x82;   // ERV→PC 제어 응답
7	        public const int  STATUS_LEN  = 240;  // 238 + 2((꺼짐)예약 잔여초 u16)
8	        public const int  THR_OFF     = 134;  // 모드별 오염단계 임계표 시작 오프셋
9	        public const int  TEMPHUMI_OFF = 230; // 각실 온도/습도 블록 시작 (4실 × [Temp,Humi])
10	        public const int  RESERVE_OFF  = 238; // (꺼짐)예약 잔여초 u16
11
12	        static int U16(byte[] p, int off) => (p[off] << 8) | p[off + 1];  // 빅엔디안
13
14	        // STATUS(0x81) payload(73B) → StatusRecord. 길이 부족 시 null.
15	        public static StatusRecord? Decode(byte[] p)
16	        {
17	            if (p.Length < STATUS_LEN) return null;
18
19	            var s = new StatusRecord
20	            {
21	                Power      = p[0],
22	                RunMode    = p[1],
23	                AutoState  = p[2],
24	                FanMode    = p[3],
25	                SubMode    = p[4],
26	                Hood       = p[5],
27	                HystPreset = p[6],
28	                HystPm25   = U16(p, 7),
29	                HystPm10   = U16(p, 9),
30	                HystVoc    = U16(p, 11),
31	                HystCo2    = U16(p, 13),
32	                ErrorCode  = U16(p, 15),
33	            };
34
35	            const int baseOff = 17;
36	            for (int r = 0; r < 4; r++)
37	            {
38	                int o = baseOff + r * 14;
39	                var room = s.Rooms[r];
40	                room.Damper      = p[o + 0];
41	                room.Pm25        = U16(p, o + 1);
42	                room.Pm10        = U16(p, o + 3);
43	                room.Voc         = U16(p, o + 5);
44	                room.Co2         = U16(p, o + 7);
45	                room.AirQuality  = p[o + 9];
46	                room.LedDim      = p[o + 10];
47	                room.LoadScore   = U16(p, o + 11);
48	                room.FinalVolume = p[o + 13];
49	            }
50
51	            // ERV 리셋 (offset 73)
52	            s.Reset = p[73];
53
54	            // 풍량 VSP 블록 (offset 74~, 9엔트리 × u16 SA·EA)
55	            const int vspOff = 74;
56	            for (int i = 0; i < 9; i++)
57	            {
58	                int o = vspOff + i * 4;
59	                s.Vsp[i].Sa = U16(p, o);
60	                s.Vsp[i].Ea = U16(p, o + 2);
61	            }
62
63	            // 히스테리시스 프리셋 테이블 (offset 110~, 3프리셋 × PM2.5/PM10/VOC/CO2 u16)
64	            const int hystOff = 110;
65	            for (int i = 0; i < 3; i++)
66	            {
67	                int o = hystOff + i * 8;
68	                s.HystTable[i].Pm25 = U16(p, o);
69	                s.HystTable[i].Pm10 = U16(p, o + 2);
70	                s.HystTable[i].Voc  = U16(p, o + 4);
71	                s.HystTable[i].Co2  = U16(p, o + 6);
72	            }
73
74	            // 모드별 오염단계 임계표 (offset 134~, 3프리셋 × [CO2,PM2.5,PM10,VOC] × L1~L4 u16)
75	            for (int i = 0; i < 3; i++)
76	            {
77	                int o = THR_OFF + i * 32;
78	                for (int k = 0; k < 4; k++) s.ThrTable[i].Co2[k]  = U16(p, o + 0 + k * 2);
79	                for (int k = 0; k < 4; k++) s.ThrTable[i].Pm25[k] = U16(p, o + 8 + k * 2);
80	                for (int k = 0; k < 4; k++) s.ThrTable[i].Pm10[k] = U16(p, o + 16 + k * 2);
81	                for (int k = 0; k < 4; k++) s.ThrTable[i].Voc[k]  = U16(p, o + 24 + k * 2);
82	            }
83
84	            // 각실 온도/습도 (offset 230~, 4실 × [Temp, Humi])
85	            for (int r = 0; r < 4; r++)
86	            {
87	                int o = TEMPHUMI_OFF + r * 2;
88	                s.Rooms[r].Temp = p[o + 0];
89	                s.Rooms[r].Humi = p[o + 1];
90	            }
91
92	            // (꺼짐)예약 잔여초 (offset 238, u16)
93	            s.ReserveRemainSec = U16(p, RESERVE_OFF);
94	            return s;
95	        }
96	    }
97	}
98

namespace ErvProtocol
{
    public static class StatusDecoder
    {
        public const byte STATUS = 0x81;   // ERV→PC 상태
        public const byte ACK    = 0x82;   // ERV→PC 제어 응답
        public const int  STATUS_LEN  = 240;  // 238 + 2((꺼짐)예약 잔여초 u16)
        public const int  THR_OFF     = 134;  // 모드별 오염단계 임계표 시작 오프셋
        public const int  TEMPHUMI_OFF = 230; // 각실 온도/습도 블록 시작 (4실 × [Temp,Humi])
        public const int  RESERVE_OFF  = 238; // (꺼짐)예약 잔여초 u16

        static int U16(byte[] p, int off) => (p[off] << 8) | p[off + 1];  // 빅엔디안

        // STATUS(0x81) payload(73B) → StatusRecord. 길이 부족 시 null.
        public static StatusRecord? Decode(byte[] p)
        {
            if (p.Length < STATUS_LEN) return null;

            var s = new StatusRecord
            {
                Power      = p[0],
                RunMode    = p[1],
                AutoState  = p[2],
                FanMode    = p[3],
                SubMode    = p[4],
                Hood       = p[5],
                HystPreset = p[6],
                HystPm25   = U16(p, 7),
                HystPm10   = U16(p, 9),
                HystVoc    = U16(p, 11),
                HystCo2    = U16(p, 13),
                ErrorCode  = U16(p, 15),
            };

            const int baseOff = 17;
            for (int r = 0; r < 4; r++)
            {
                int o = baseOff + r * 14;
                var room = s.Rooms[r];
                room.Damper      = p[o + 0];
                room.Pm25        = U16(p, o + 1);
                room.Pm10        = U16(p, o + 3);
                room.Voc         = U16(p, o + 5);
                room.Co2         = U16(p, o + 7);
                room.AirQuality  = p[o + 9];
                room.LedDim      = p[o + 10];
                room.LoadScore   = U16(p, o + 11);
                room.FinalVolume = p[o + 13];
            }

            // ERV 리셋 (offset 73)
            s.Reset = p[73];

            // 풍량 VSP 블록 (offset 74~, 9엔트리 × u16 SA·EA)
            const int vspOff = 74;
            for (int i = 0; i < 9; i++)
            {
                int o = vspOff + i * 4;
                s.Vsp[i].Sa = U16(p, o);
                s.Vsp[i].Ea = U16(p, o + 2);
            }

            // 히스테리시스 프리셋 테이블 (offset 110~, 3프리셋 × PM2.5/PM10/VOC/CO2 u16)
            const int hystOff = 110;
            for (int i = 0; i < 3; i++)
            {
                int o = hystOff + i * 8;
                s.HystTable[i].Pm25 = U16(p, o);
                s.HystTable[i].Pm10 = U16(p, o + 2);
                s.HystTable[i].Voc  = U16(p, o + 4);
                s.HystTable[i].Co2  = U16(p, o + 6);
            }

            // 모드별 오염단계 임계표 (offset 134~, 3프리셋 × [CO2,PM2.5,PM10,VOC] × L1~L4 u16)
            for (int i = 0; i < 3; i++)
            {
                int o = THR_OFF + i * 32;
                for (int k = 0; k < 4; k++) s.ThrTable[i].Co2[k]  = U16(p, o + 0 + k * 2);
                for (int k = 0; k < 4; k++) s.ThrTable[i].Pm25[k] = U16(p, o + 8 + k * 2);
                for (int k = 0; k < 4; k++) s.ThrTable[i].Pm10[k] = U16(p, o + 16 + k * 2);
                for (int k = 0; k < 4; k++) s.ThrTable[i].Voc[k]  = U16(p, o + 24 + k * 2);
            }

            // 각실 온도/습도 (offset 230~, 4실 × [Temp, Humi])
            for (int r = 0; r < 4; r++)
            {
                int o = TEMPHUMI_OFF + r * 2;
                s.Rooms[r].Temp = p[o + 0];
                s.Rooms[r].Humi = p[o + 1];
            }

            // (꺼짐)예약 잔여초 (offset 238, u16)
            s.ReserveRemainSec = U16(p, RESERVE_OFF);
            return s;
        }
    }
}


`_state.ComfortCook`이 들어오는 STATUS(SubMode bit1)로 어떻게 갱신되는지가 핵심입니다. `_state` 정의와 STATUS 반영 지점을 찾겠습니다.


22:        readonly DashboardState _state = new();
166:            if (!_demoTimer.IsEnabled && CanSend()) _api.SetSubMode(SubModeType.SmartSleep, false);
167:            ApplySubModeLocal("SmartSleep", false);
379:        void SubMode_Click(object sender, RoutedEventArgs e)
383:            (SubModeType type, bool next) = tag switch
385:                "SmartSleep"    => (SubModeType.SmartSleep,    !_state.SmartSleep),
386:                "ComfortCook"   => (SubModeType.ComfortCook,   !_state.ComfortCook),
387:                _               => (SubModeType.ReliefRecover, !_state.ReliefRecover),
394:                ApplySubModeLocal(tag, next);
403:                if (tag != "SmartSleep"    && _state.SmartSleep)    _api.SetSubMode(SubModeType.SmartSleep,    false);
404:                if (tag != "ComfortCook"   && _state.ComfortCook)   _api.SetSubMode(SubModeType.ComfortCook,   false);
405:                if (tag != "ReliefRecover" && _state.ReliefRecover) _api.SetSubMode(SubModeType.ReliefRecover, false);
407:            _api.SetSubMode(type, next);
408:            ApplySubModeLocal(tag, next);
424:        void ApplySubModeLocal(string tag, bool on)
429:                _state.SmartSleep    = tag == "SmartSleep";
437:                    case "SmartSleep":    _state.SmartSleep = false; break;
635:            sb.Append($"hood={(_state.Hood ? 1 : 0)} sub=0x{_state.SubModeBitmap:X2} auto={_state.AutoStateText} ");


1	using System.Collections.ObjectModel;
2	using System.ComponentModel;
3	using System.Runtime.CompilerServices;
4	using ErvProtocol;
5
6	namespace ErvDashboard.Model
7	{
8	    // 대시보드 전체 상태 (STATUS 0x81 수신값 + 사용자 제어 의도)
9	    public class DashboardState : INotifyPropertyChanged
10	    {
11	        // ---- ERV 제어/상태 ----
12	        bool _powerOn;
13	        RunMode _runMode = RunMode.Off;
14	        byte _fanMode;
15	        bool _hood, _hoodConnected, _hoodRunning;
16	        bool _smartSleep, _comfortCook, _reliefRecover;
17
18	        public bool PowerOn { get => _powerOn; set { if (_powerOn != value) { _powerOn = value; OnChanged(); } } }
19	        public RunMode RunMode { get => _runMode; set { if (_runMode != value) { _runMode = value; OnChanged(); OnChanged(nameof(IsAuto)); OnChanged(nameof(CanRoomControl)); } } }
20	        public bool IsAuto => RunMode == RunMode.Auto;
21	        // 각실 댐퍼/LED 수동 제어 가능 여부 (환기/공청/바이패스에서만, 자동 제외)
22	        public bool CanRoomControl => RunMode != RunMode.Auto;
23	        public byte FanMode { get => _fanMode; set { if (_fanMode != value) { _fanMode = value; OnChanged(); } } }
24	        public bool Hood { get => _hood; set { if (_hood != value) { _hood = value; OnChanged(); } } }
25	        // 후드 485 통신연결 여부 (STATUS byte5 bit2). 후드연동 ON일 때 연결/미연결 텍스트 표시용
26	        public bool HoodConnected { get => _hoodConnected; set { if (_hoodConnected != value) { _hoodConnected = value; OnChanged(); } } }
27	        // 후드연동에 의한 운전중(메이크업 에어 실제 동작중, STATUS byte5 bit1). 후드 OFF면 false → 쾌적조리 표시 해제·운전모드 활성.
28	        public bool HoodRunning { get => _hoodRunning; set { if (_hoodRunning != value) { _hoodRunning = value; OnChanged(); } } }
29
30	        // ---- (꺼짐)예약 : 잔여초(STATUS 수신) ----
31	        int _reserveRemainSec;
32	        public int ReserveRemainSec { get => _reserveRemainSec; set { if (_reserveRemainSec != value) { _reserveRemainSec = value; OnChanged(); OnChanged(nameof(ReserveText)); } } }
33	        public string ReserveText => ReserveRemainSec > 0
34	            ? $"꺼짐까지 {ReserveRemainSec / 3600}:{(ReserveRemainSec % 3600) / 60:00}:{ReserveRemainSec % 60:00}"
35	            : "예약 없음";
36
37	        public bool SmartSleep    { get => _smartSleep;    set { if (_smartSleep != value)    { _smartSleep = value;    OnChanged(); } } }
38
39	        // 스마트수면 시간설정(대시보드 전용 — 종료 시각에 자동 해제). 자정 기준 분(0~1439).
40	        int _sleepStartMin = 0;          // 오전 12:00
41	        int _sleepEndMin   = 7 * 60 + 30; // 오전 7:30
42	        public int SleepStartMin { get => _sleepStartMin; set { if (_sleepStartMin != value) { _sleepStartMin = value; OnChanged(); OnChanged(nameof(SleepSummary)); } } }
43	        public int SleepEndMin   { get => _sleepEndMin;   set { if (_sleepEndMin != value)   { _sleepEndMin = value;   OnChanged(); OnChanged(nameof(SleepSummary)); } } }
44	        public string SleepSummary => $"{DashboardState.FmtTime(SleepStartMin)} ~ {DashboardState.FmtTime(SleepEndMin)}";
45	        public static string FmtTime(int min)
46	        {
47	            min = ((min % 1440) + 1440) % 1440;
48	            int h = min / 60, m = min % 60, h12 = h % 12; if (h12 == 0) h12 = 12;
49	            return $"{(h < 12 ? "오전" : "오후")} {h12}:{m:00}";
50	        }
51	        public bool ComfortCook   { get => _comfortCook;   set { if (_comfortCook != value)   { _comfortCook = value;   OnChanged(); } } }
52	        public bool ReliefRecover { get => _reliefRecover; set { if (_reliefRecover != value) { _reliefRecover = value; OnChanged(); } } }
53
54	        public byte SubModeBitmap
55	        {
56	            get
57	            {
58	                byte b = 0;
59	                if (SmartSleep)    b |= SubModeBits.SmartSleep;
60	                if (ComfortCook)   b |= SubModeBits.ComfortCook;
61	                if (ReliefRecover) b |= SubModeBits.ReliefRecover;
62	                return b;
63	            }
64	            set
65	            {
66	                SmartSleep    = (value & SubModeBits.SmartSleep)    != 0;
67	                ComfortCook   = (value & SubModeBits.ComfortCook)   != 0;
68	                ReliefRecover = (value & SubModeBits.ReliefRecover) != 0;
69	            }
70	        }
71
72	        // ---- 자동운전 상태 (읽기전용) ----
73	        AutoState _autoState = AutoState.Distribute;
74	        public AutoState AutoState
75	        {
76	            get => _autoState;
77	            set { if (_autoState != value) { _autoState = value; OnChanged(); OnChanged(nameof(AutoStateText)); } }
78	        }
79	        public string AutoStateText => AutoState == AutoState.Focus ? "집중" : "분산";
80
81	        // 합산 부하점수 (4실 Level 합, 0~16) — STATUS 수신 시 StatusMapper 가 갱신
82	        int _totalLoadScore;
83	        public int TotalLoadScore
84	        {
85	            get => _totalLoadScore;
86	            set { if (_totalLoadScore != value) { _totalLoadScore = value; OnChanged(); OnChanged(nameof(TotalLoadScoreText)); } }
87	        }
88	        public string TotalLoadScoreText => $"{TotalLoadScore} / 16";
89
90	        // ---- 히스테리시스 ----
91	        HystPreset _hystPreset = HystPreset.Normal;
92	        int _hystPm25, _hystPm10, _hystVoc, _hystCo2;
93	        public HystPreset HystPreset { get => _hystPreset; set { if (_hystPreset != value) { _hystPreset = value; OnChanged(); } } }
94	        public int HystPm25 { get => _hystPm25; set { if (_hystPm25 != value) { _hystPm25 = value; OnChanged(); } } }
95	        public int HystPm10 { get => _hystPm10; set { if (_hystPm10 != value) { _hystPm10 = value; OnChanged(); } } }
96	        public int HystVoc  { get => _hystVoc;  set { if (_hystVoc  != value) { _hystVoc  = value; OnChanged(); } } }
97	        public int HystCo2  { get => _hystCo2;  set { if (_hystCo2  != value) { _hystCo2  = value; OnChanged(); } } }
98
99	        // ---- 에러코드 ----
100	        int _errorCode;
101	        public int ErrorCode { get => _errorCode; set { if (_errorCode != value) { _errorCode = value; OnChanged(); OnChanged(nameof(ErrorCodeHex)); } } }
102	        public string ErrorCodeHex => $"0x{ErrorCode:X4}";
103
104	        // ---- ERV 리셋 (토글) ----
105	        bool _reset;
106	        public bool Reset { get => _reset; set { if (_reset != value) { _reset = value; OnChanged(); } } }
107
108	        // ---- 풍량 VSP (9엔트리) ----
109	        public ObservableCollection<VspRow> Vsp { get; }
110
111	        // ---- 히스테리시스 데드밴드 테이블 (ECO/NORMAL/TURBO 별 PM2.5/PM10/VOC/CO2) ----
112	        public ObservableCollection<HystRow> HystTable { get; }
113
114	        // ---- 모드별 오염단계 임계표 [preset 0 ECO/1 NORMAL/2 TURBO][L1~L4 상한] ----
115	        public int[][] Co2Thr  { get; } = { new int[4], new int[4], new int[4] };
116	        public int[][] Pm25Thr { get; } = { new int[4], new int[4], new int[4] };
117	        public int[][] Pm10Thr { get; } = { new int[4], new int[4], new int[4] };
118	        public int[][] VocThr  { get; } = { new int[4], new int[4], new int[4] };
119
120	        // ===== 사양서 기본값 (히스테리시스/VSP 창의 '프리셋' 버튼용 — 불변) =====
121	        // 오염단계 임계 [preset 0 ECO/1 NORMAL/2 TURBO][L0~L3 상한] — 개발사양서 p.10
122	        public static readonly int[][] DefCo2Thr  = { new[]{1000,1300,1600,2000}, new[]{800,1100,1400,1700}, new[]{700,1000,1300,1600} };
123	        public static readonly int[][] DefPm25Thr = { new[]{20,38,60,86}, new[]{14,29,49,69}, new[]{12,23,38,52} };
124	        public static readonly int[][] DefPm10Thr = { new[]{40,86,126,173}, new[]{28,66,102,138}, new[]{24,53,78,104} };
125	        public static readonly int[][] DefVocThr  = { new[]{171,195,308,438}, new[]{120,150,250,350}, new[]{103,120,192,263} };
126	        // 데드밴드(하강) [preset] (Pm25,Pm10,Voc,Co2) — 개발사양서 p.10
127	        public static readonly (int Pm25,int Pm10,int Voc,int Co2)[] DefDeadband =
128	            { (2,5,5,50), (2,5,5,50), (2,5,3,30) };
129	        // 풍량 VSP 기본값 [9엔트리: 환기1~4/바이패스/공청1~4] (Sa,Ea)
130	        // 개발사양서 p.12 첫 표(휴벤 ECO2/좌타입 HRD1-150EPI) = 펌웨어 MyControl.c 기본값. 환기4(터보)=자동250CMH 행.
131	        public static readonly (int Sa,int Ea)[] DefVsp =
132	            { (56,57), (63,63), (70,70), (86,85), (67,75), (65,0), (72,0), (78,0), (80,0) };
133
134	        // ---- 각실 ----
135	        public ObservableCollection<RoomState> Rooms { get; }
136
137	        public DashboardState()
138	        {
139	            Rooms = new ObservableCollection<RoomState>
140	            {
141	                new(1, "거실"),
142	                new(2, "침실1"),
143	                new(3, "침실2"),
144	                new(4, "침실3"),
145	            };
146	            // 히스테리시스 기본값 (NORMAL 가정, 펌웨어 m_*_Level 기준)
147	            HystPm25 = 30; HystPm10 = 50; HystVoc = 300; HystCo2 = 700;
148
149	            // 풍량 VSP 9엔트리 (환기1~4, 바이패스, 공청1~4)
150	            Vsp = new ObservableCollection<VspRow>();
151	            for (int i = 0; i < VspInfo.Count; i++)
152	                Vsp.Add(new VspRow(VspInfo.Labels[i], VspInfo.Group[i], VspInfo.Index[i]));
153
154	            // 히스테리시스 데드밴드(하강) 기본값 - 사양서
155	            HystTable = new ObservableCollection<HystRow>
156	            {
157	                new("ECO",    0) { Pm25 = 2, Pm10 = 5, Voc = 5, Co2 = 50 },
158	                new("NORMAL", 1) { Pm25 = 2, Pm10 = 5, Voc = 5, Co2 = 50 },
159	                new("TURBO",  2) { Pm25 = 2, Pm10 = 5, Voc = 3, Co2 = 30 },
160	            };
161
162	            // 모드별 오염단계 임계 기본값 - 사양서 (CO2/PM2.5/PM10/VOC 의 L1~L4 상한)
163	            int[][] co2 = { new[]{1000,1300,1600,2000}, new[]{800,1100,1400,1700}, new[]{700,1000,1300,1600} };
164	            int[][] pm25 = { new[]{20,38,60,86}, new[]{14,29,49,69}, new[]{12,23,38,52} };
165	            int[][] pm10 = { new[]{40,86,126,173}, new[]{28,66,102,138}, new[]{24,53,78,104} };
166	            int[][] voc = { new[]{171,195,308,438}, new[]{120,150,250,350}, new[]{103,120,192,263} };
167	            for (int i = 0; i < 3; i++) for (int k = 0; k < 4; k++)
168	            { Co2Thr[i][k] = co2[i][k]; Pm25Thr[i][k] = pm25[i][k]; Pm10Thr[i][k] = pm10[i][k]; VocThr[i][k] = voc[i][k]; }
169	        }
170
171	        public RoomState Room(int id) => Rooms[id - 1];
172
173	        public event PropertyChangedEventHandler? PropertyChanged;
174	        void OnChanged([CallerMemberName] string? n = null) =>
175	            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
176	    }
177	}
178

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, _hoodRunning;
        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 byte5 bit1). 후드 OFF면 false → 쾌적조리 표시 해제·운전모드 활성.
        public bool HoodRunning { get => _hoodRunning; set { if (_hoodRunning != value) { _hoodRunning = 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] };

        // ===== 사양서 기본값 (히스테리시스/VSP 창의 '프리셋' 버튼용 — 불변) =====
        // 오염단계 임계 [preset 0 ECO/1 NORMAL/2 TURBO][L0~L3 상한] — 개발사양서 p.10
        public static readonly int[][] DefCo2Thr  = { new[]{1000,1300,1600,2000}, new[]{800,1100,1400,1700}, new[]{700,1000,1300,1600} };
        public static readonly int[][] DefPm25Thr = { new[]{20,38,60,86}, new[]{14,29,49,69}, new[]{12,23,38,52} };
        public static readonly int[][] DefPm10Thr = { new[]{40,86,126,173}, new[]{28,66,102,138}, new[]{24,53,78,104} };
        public static readonly int[][] DefVocThr  = { new[]{171,195,308,438}, new[]{120,150,250,350}, new[]{103,120,192,263} };
        // 데드밴드(하강) [preset] (Pm25,Pm10,Voc,Co2) — 개발사양서 p.10
        public static readonly (int Pm25,int Pm10,int Voc,int Co2)[] DefDeadband =
            { (2,5,5,50), (2,5,5,50), (2,5,3,30) };
        // 풍량 VSP 기본값 [9엔트리: 환기1~4/바이패스/공청1~4] (Sa,Ea)
        // 개발사양서 p.12 첫 표(휴벤 ECO2/좌타입 HRD1-150EPI) = 펌웨어 MyControl.c 기본값. 환기4(터보)=자동250CMH 행.
        public static readonly (int Sa,int Ea)[] DefVsp =
            { (56,57), (63,63), (70,70), (86,85), (67,75), (65,0), (72,0), (78,0), (80,0) };

        // ---- 각실 ----
        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));
    }
}


1	using ErvDashboard.Model;
2	using ErvProtocol;
3
4	namespace ErvDashboard.Api
5	{
6	    // StatusRecord(공용 프로토콜 디코드 결과) → DashboardState(WPF UI 모델) 매핑.
7	    // (이전 DashboardProtocol.ApplyStatus 로직 — API 는 UI 비종속, 매핑은 여기로 분리)
8	    public static class StatusMapper
9	    {
10	        public static void Apply(StatusRecord s, DashboardState state)
11	        {
12	            state.PowerOn   = s.Power != 0;
13	            state.RunMode   = (RunMode)s.RunMode;
14	            state.AutoState = (AutoState)s.AutoState;
15	            state.FanMode   = s.FanMode;
16	            state.SubModeBitmap = s.SubMode;
17	            state.Hood          = s.HoodEnable;      // byte5 bit0 = 연동 Enable
18	            state.HoodRunning   = s.HoodRunning;     // byte5 bit1 = 연동운전중(메이크업 동작중)
19	            state.HoodConnected = s.HoodConnected;   // byte5 bit2 = 후드 통신연결
20	            state.HystPreset = (HystPreset)s.HystPreset;
21	            state.HystPm25  = s.HystPm25;
22	            state.HystPm10  = s.HystPm10;
23	            state.HystVoc   = s.HystVoc;
24	            state.HystCo2   = s.HystCo2;
25	            state.ErrorCode = s.ErrorCode;
26	            state.Reset     = s.Reset != 0;
27	            state.ReserveRemainSec = s.ReserveRemainSec;
28
29	            for (int i = 0; i < state.Vsp.Count && i < s.Vsp.Length; i++)
30	            {
31	                state.Vsp[i].Sa = s.Vsp[i].Sa;
32	                state.Vsp[i].Ea = s.Vsp[i].Ea;
33	            }
34
35	            for (int i = 0; i < state.HystTable.Count && i < s.HystTable.Length; i++)
36	            {
37	                state.HystTable[i].Pm25 = s.HystTable[i].Pm25;
38	                state.HystTable[i].Pm10 = s.HystTable[i].Pm10;
39	                state.HystTable[i].Voc  = s.HystTable[i].Voc;
40	                state.HystTable[i].Co2  = s.HystTable[i].Co2;
41	            }
42
43	            // 모드별 오염단계 임계표
44	            for (int i = 0; i < 3 && i < s.ThrTable.Length; i++)
45	                for (int k = 0; k < 4; k++)
46	                {
47	                    state.Co2Thr[i][k]  = s.ThrTable[i].Co2[k];
48	                    state.Pm25Thr[i][k] = s.ThrTable[i].Pm25[k];
49	                    state.Pm10Thr[i][k] = s.ThrTable[i].Pm10[k];
50	                    state.VocThr[i][k]  = s.ThrTable[i].Voc[k];
51	                }
52
53	            int totalLoad = 0;
54	            for (int r = 0; r < 4; r++)
55	            {
56	                var src = s.Rooms[r];
57	                var room = state.Rooms[r];
58	                room.DamperSaOpen = src.DamperSa;   // 비트맵 bit0
59	                room.DamperEaOpen = src.DamperEa;   // 비트맵 bit1
60	                room.Pm25        = src.Pm25;
61	                room.Pm10        = src.Pm10;
62	                room.Voc         = src.Voc;
63	                room.Co2         = src.Co2;
64	                room.AirQuality  = (AirQuality)src.AirQuality;
65	                room.LedDim      = src.LedDim;
66	                room.LoadScore   = src.LoadScore;
67	                room.FinalVolume = src.FinalVolume;
68	                room.Temp        = src.Temp;
69	                room.Humi        = src.Humi;
70	                totalLoad += src.LoadScore;
71	            }
72	            state.TotalLoadScore = totalLoad;   // 합산 부하점수 (0~16)
73	        }
74	    }
75	}
76

using ErvDashboard.Model;
using ErvProtocol;

namespace ErvDashboard.Api
{
    // StatusRecord(공용 프로토콜 디코드 결과) → DashboardState(WPF UI 모델) 매핑.
    // (이전 DashboardProtocol.ApplyStatus 로직 — API 는 UI 비종속, 매핑은 여기로 분리)
    public static class StatusMapper
    {
        public static void Apply(StatusRecord s, DashboardState state)
        {
            state.PowerOn   = s.Power != 0;
            state.RunMode   = (RunMode)s.RunMode;
            state.AutoState = (AutoState)s.AutoState;
            state.FanMode   = s.FanMode;
            state.SubModeBitmap = s.SubMode;
            state.Hood          = s.HoodEnable;      // byte5 bit0 = 연동 Enable
            state.HoodRunning   = s.HoodRunning;     // byte5 bit1 = 연동운전중(메이크업 동작중)
            state.HoodConnected = s.HoodConnected;   // byte5 bit2 = 후드 통신연결
            state.HystPreset = (HystPreset)s.HystPreset;
            state.HystPm25  = s.HystPm25;
            state.HystPm10  = s.HystPm10;
            state.HystVoc   = s.HystVoc;
            state.HystCo2   = s.HystCo2;
            state.ErrorCode = s.ErrorCode;
            state.Reset     = s.Reset != 0;
            state.ReserveRemainSec = s.ReserveRemainSec;

            for (int i = 0; i < state.Vsp.Count && i < s.Vsp.Length; i++)
            {
                state.Vsp[i].Sa = s.Vsp[i].Sa;
                state.Vsp[i].Ea = s.Vsp[i].Ea;
            }

            for (int i = 0; i < state.HystTable.Count && i < s.HystTable.Length; i++)
            {
                state.HystTable[i].Pm25 = s.HystTable[i].Pm25;
                state.HystTable[i].Pm10 = s.HystTable[i].Pm10;
                state.HystTable[i].Voc  = s.HystTable[i].Voc;
                state.HystTable[i].Co2  = s.HystTable[i].Co2;
            }

            // 모드별 오염단계 임계표
            for (int i = 0; i < 3 && i < s.ThrTable.Length; i++)
                for (int k = 0; k < 4; k++)
                {
                    state.Co2Thr[i][k]  = s.ThrTable[i].Co2[k];
                    state.Pm25Thr[i][k] = s.ThrTable[i].Pm25[k];
                    state.Pm10Thr[i][k] = s.ThrTable[i].Pm10[k];
                    state.VocThr[i][k]  = s.ThrTable[i].Voc[k];
                }

            int totalLoad = 0;
            for (int r = 0; r < 4; r++)
            {
                var src = s.Rooms[r];
                var room = state.Rooms[r];
                room.DamperSaOpen = src.DamperSa;   // 비트맵 bit0
                room.DamperEaOpen = src.DamperEa;   // 비트맵 bit1
                room.Pm25        = src.Pm25;
                room.Pm10        = src.Pm10;
                room.Voc         = src.Voc;
                room.Co2         = src.Co2;
                room.AirQuality  = (AirQuality)src.AirQuality;
                room.LedDim      = src.LedDim;
                room.LoadScore   = src.LoadScore;
                room.FinalVolume = src.FinalVolume;
                room.Temp        = src.Temp;
                room.Humi        = src.Humi;
                totalLoad += src.LoadScore;
            }
            state.TotalLoadScore = totalLoad;   // 합산 부하점수 (0~16)
        }
    }
}


데이터 흐름이 잡혔습니다: 대시보드 `_state.ComfortCook` ← STATUS byte4 bit1 ← 펌웨어 `Hood_YeunDong_Enable`. 이제 펌웨어가 이 값을 언제 set/clear 하는지가 핵심입니다. `My_system.c` 840~960과 `My_Homenet.c` 명령 처리부를 보겠습니다.