chore: HERV 통합 저장소 초기 커밋
- 펌웨어(program), C# 대시보드(TestProgram), 시뮬레이터(Simulator), 프로토콜/문서(Protocol, doc) 전체를 단일 저장소로 통합 - program 폴더의 별도 git 저장소를 제거하고 통합 저장소에 흡수 - 빌드 산출물(program/build, bin/obj, *.o/.elf/.bin/.hex 등) .gitignore 처리 - 사내 Synology NAS Git 원격 연결 예정 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
|||||||
|
# ── 펌웨어 빌드 산출물 ──
|
||||||
|
program/build/
|
||||||
|
*.o
|
||||||
|
*.d
|
||||||
|
*.elf
|
||||||
|
*.bin
|
||||||
|
*.hex
|
||||||
|
*.map
|
||||||
|
*.lst
|
||||||
|
*.crf
|
||||||
|
*.axf
|
||||||
|
*.htm
|
||||||
|
*.scvd
|
||||||
|
*.dep
|
||||||
|
*.bak
|
||||||
|
*.lnp
|
||||||
|
*.iex
|
||||||
|
*.sct
|
||||||
|
*.__i
|
||||||
|
|
||||||
|
# ── C# / .NET 빌드 산출물 ──
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Dd]ebug/
|
||||||
|
[Rr]elease/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
.vs/
|
||||||
|
*_wpftmp.csproj
|
||||||
|
|
||||||
|
# ── OS / 에디터 ──
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# ── 로그 / 임시 ──
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
DL 각실제어 시스템 구성
|
||||||
|
HERV (전열교환기) -(485통신) BUNBAGI -(485통신) 배기(RA) DIFFUSER -(485통신) 급기(SA) DIFFUSER
|
||||||
|
|
||||||
|
각실 DIFFUSER 구성
|
||||||
|
거실 : 배기(RA) DIFFUSER / 급기(SA1) DIFFUSER / 급기(SA2) DIFFUSER
|
||||||
|
방1 ~ 방3 : 배기(RA) DIFFUSER / 급기(SA) DIFFUSER
|
||||||
|
|
||||||
|
프로토콜
|
||||||
|
HERV / BUNBAGI / DIFFUSER : 수정_Each_Room_Jushin_protocol_RS485_Rev1.2_20260115_CWS
|
||||||
|
|
||||||
|
대시보드 프로그램 (모두 TestProgram 폴더 하위)
|
||||||
|
C# WPF .NET 10
|
||||||
|
|
||||||
|
빌드 및 배포방법
|
||||||
|
PC 대시보드
|
||||||
|
dotnet cleand 프로젝트명
|
||||||
|
dotnet build 프로젝트명
|
||||||
|
dotnet publish "d:\project\nuvoton\HERV\TestProgram\PCDashBoard\ErvDashboard.csproj" -c Release
|
||||||
|
csproj에 SelfContained / PublishSingleFile / win-x64 가 이미 설정돼 있어서 추가 옵션이 필요 없습니다.
|
||||||
|
결과물: TestProgram\PCDashBoard\bin\Release\net10.0-windows\win-x64\publish\ErvDashboard.exe
|
||||||
|
|
||||||
|
WEB 대시보드
|
||||||
|
# 런타임 미설치 PC용 (단일 exe, 권장)
|
||||||
|
dotnet publish "d:\project\nuvoton\HERV\TestProgram\WebDashBoard\ErvCollector\ErvCollector.csproj" `
|
||||||
|
-c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
|
||||||
|
|
||||||
|
# .NET 10 런타임 이미 깔린 PC용 (가벼움)
|
||||||
|
dotnet publish "d:\project\nuvoton\HERV\TestProgram\WebDashBoard\ErvCollector\ErvCollector.csproj" -c Release
|
||||||
|
결과물: ...\ErvCollector\bin\Release\net10.0\win-x64\publish\
|
||||||
|
appsettings.json 과 wwwroot\ (웹 모니터 정적파일)가 publish 폴더에 같이 복사됩니다 — publish 폴더 전체를 통째로 복사하세요. exe만 떼면 웹 화면이 안 뜹니다.
|
||||||
|
|
||||||
|
해상도 : 1920 X 1200 (노트북 기준) 안 넘게 제작.
|
||||||
|
프로토콜 : PC_ERV_Protocol.MD
|
||||||
|
공용 라이브러리 : TestProgram/ErvProtocol 폴더 (단일 진실원본)
|
||||||
|
PC용 (485통신) - TestProgram/PCDashBoard (ErvDashboard.csproj)
|
||||||
|
WEB용 (WIFI) - TestProgram/WebDashBoard (ErvCollector 수집·제어 서버 + ErvWebDashboard 모니터)
|
||||||
|
|
||||||
|
시뮬레이터 프로그램
|
||||||
|
C# WPF
|
||||||
|
UI 내용은 정적생성 할 것. 반복생성은 제외.
|
||||||
|
ERV SIMULATOR :
|
||||||
|
DIFFUSER SIMULATOR :
|
||||||
|
|
||||||
|
HERV 사양
|
||||||
|
동작사양
|
||||||
|
전원 : ON, OFF
|
||||||
|
운전모드 : 환기(수동), 자동, 바이패스, 공기청정
|
||||||
|
(운전모드 전환 시 풍량 1단, 자동 제외)
|
||||||
|
특이사항 : 기저운전/장비보호
|
||||||
|
|
||||||
|
(꺼짐예약) : 0 ~ 8 시간, 1시간 단위, 0은 꺼짐
|
||||||
|
VSP 설정값 (1바이트 0~255, 사양서 DL 대덕연구소 H-ERV 실측표)
|
||||||
|
|
||||||
|
| 구분 | 단수 | 풍량(CMH) | VSP SA | VSP EA | 전압 SA(V) | 전압 EA(V) |
|
||||||
|
|----------|--------|-----------|--------|--------|-----------|-----------|
|
||||||
|
| 환기 | 약-1 | 100 | 57 | 56 | 3.347 | 3.296 |
|
||||||
|
| 환기 | 중-2 | 150 | 63 | 61 | 3.634 | 3.571 |
|
||||||
|
| 환기 | 강-3 | 200 | 69 | 67 | 3.968 | 3.893 |
|
||||||
|
| 환기 | 터보-4 | 250 | 86 | 85 | 4.391 | 4.125 |
|
||||||
|
| 바이패스 | 기본 | 150 | 66 | 80 | 3.776 | 4.539 |
|
||||||
|
| 공청 | 약-1 | 80 | 66 | - | 3.778 | - |
|
||||||
|
| 공청 | 중-2 | 100 | 70 | - | 4.018 | - |
|
||||||
|
| 공청 | 강-3 | 120 | 77 | - | 4.354 | - |
|
||||||
|
| 공청 | 터보-4 | 150 | 80 | - | 4.50 | - |
|
||||||
|
|
||||||
|
VSP 테이블 (VSP ↔ 실측전압, V = VSP × 0.05 + 0.5)
|
||||||
|
|
||||||
|
| VSP | V | VSP | V | VSP | V | VSP | V | VSP | V |
|
||||||
|
|----|------|----|------|----|------|----|------|----|------|
|
||||||
|
| 5 | 0.75 | 25 | 1.75 | 45 | 2.75 | 65 | 3.75 | 85 | 4.75 |
|
||||||
|
| 6 | 0.80 | 26 | 1.80 | 46 | 2.80 | 66 | 3.80 | 86 | 4.80 |
|
||||||
|
| 7 | 0.85 | 27 | 1.85 | 47 | 2.85 | 67 | 3.85 | 87 | 4.85 |
|
||||||
|
| 8 | 0.90 | 28 | 1.90 | 48 | 2.90 | 68 | 3.90 | 88 | 4.90 |
|
||||||
|
| 9 | 0.95 | 29 | 1.95 | 49 | 2.95 | 69 | 3.95 | 89 | 4.95 |
|
||||||
|
| 10 | 1.00 | 30 | 2.00 | 50 | 3.00 | 70 | 4.00 | 90 | 5.00 |
|
||||||
|
| 11 | 1.05 | 31 | 2.05 | 51 | 3.05 | 71 | 4.05 | 91 | 5.05 |
|
||||||
|
| 12 | 1.10 | 32 | 2.10 | 52 | 3.10 | 72 | 4.10 | 92 | 5.10 |
|
||||||
|
| 13 | 1.15 | 33 | 2.15 | 53 | 3.15 | 73 | 4.15 | 93 | 5.15 |
|
||||||
|
| 14 | 1.20 | 34 | 2.20 | 54 | 3.20 | 74 | 4.20 | 94 | 5.20 |
|
||||||
|
| 15 | 1.25 | 35 | 2.25 | 55 | 3.25 | 75 | 4.25 | 95 | 5.25 |
|
||||||
|
| 16 | 1.30 | 36 | 2.30 | 56 | 3.30 | 76 | 4.30 | | |
|
||||||
|
| 17 | 1.35 | 37 | 2.35 | 57 | 3.35 | 77 | 4.35 | | |
|
||||||
|
| 18 | 1.40 | 38 | 2.40 | 58 | 3.40 | 78 | 4.40 | | |
|
||||||
|
| 19 | 1.45 | 39 | 2.45 | 59 | 3.45 | 79 | 4.45 | | |
|
||||||
|
| 20 | 1.50 | 40 | 2.50 | 60 | 3.50 | 80 | 4.50 | | |
|
||||||
|
| 21 | 1.55 | 41 | 2.55 | 61 | 3.55 | 81 | 4.55 | | |
|
||||||
|
| 22 | 1.60 | 42 | 2.60 | 62 | 3.60 | 82 | 4.60 | | |
|
||||||
|
| 23 | 1.65 | 43 | 2.65 | 63 | 3.65 | 83 | 4.65 | | |
|
||||||
|
| 24 | 1.70 | 44 | 2.70 | 64 | 3.70 | 84 | 4.70 | | |
|
||||||
|
|
||||||
|
공기질 센서 모드별 오염단계 임계 및 히스테리시스(하강)
|
||||||
|
|
||||||
|
| 오염단계 | ECO CO2 | ECO PM2.5 | ECO PM10 | ECO VOC | NORMAL CO2 | NORMAL PM2.5 | NORMAL PM10 | NORMAL VOC | TURBO CO2 | TURBO PM2.5 | TURBO PM10 | TURBO VOC | 상태/색상 |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 0 | 0~1000 | 0~20 | 0~40 | 0~171 | 0~800 | 0~14 | 0~28 | 0~120 | 0~700 | 0~12 | 0~24 | 0~103 | 좋음(파랑) |
|
||||||
|
| 1 | 1001~1300 | 21~38 | 41~86 | 172~195 | 801~1100 | 15~29 | 29~66 | 121~150 | 701~1000 | 13~23 | 25~53 | 104~120 | 보통(초록) |
|
||||||
|
| 2 | 1301~1600 | 39~60 | 87~126 | 196~308 | 1101~1400 | 30~49 | 67~102 | 151~250 | 1001~1300 | 24~38 | 54~78 | 121~192 | 나쁨(노랑) |
|
||||||
|
| 3 | 1601~2000 | 61~86 | 127~173 | 309~438 | 1401~1700 | 50~69 | 103~138 | 251~350 | 1301~1600 | 39~52 | 79~104 | 193~263 | 매우나쁨(주황) |
|
||||||
|
| 4 | 2001~ | 87~ | 174~ | 439~ | 1701~ | 70~ | 139~ | 351~ | 1601~ | 53~ | 105~ | 264~ | 매우나쁨(빨강) |
|
||||||
|
| 히스(하강) | -50 | -2 | -5 | -5 | -50 | -2 | -5 | -5 | -30 | -2 | -5 | -3 | |
|
||||||
|
|
||||||
|
각실제어 시나리오모드 : 자동(분산,집중), 안심회복, 스마트 수면, 쾌적조리
|
||||||
|
|
||||||
|
회로도 : HERV_MAIN_REV1.1_20240826(회로도).PDF
|
||||||
|
|
||||||
|
EEPROM 저장 데이터 (My_define.h EEP_*, page 128B / 엔트리 약 122)
|
||||||
|
|
||||||
|
| 인덱스 | 항목 | 비고 |
|
||||||
|
|---|---|---|
|
||||||
|
| 0~3 | 서명 0x55AA55AA | 초기화 마커 |
|
||||||
|
| 5 | Modbus ID | |
|
||||||
|
| 10~33 | 팬 VSP 프리셋 s_FAN1/2 (환기·공청·바이패스 1~4) | 1바이트, 대시보드 CTRL_VSP 반영 |
|
||||||
|
| 34~39 | 필터/소자 청소·교체 시간 | u16 |
|
||||||
|
| 40~42 | 정전복귀(전원/운전모드/풍량) | |
|
||||||
|
| 43 | 히스테리시스 활성 프리셋 (0 ECO/1 NORMAL/2 TURBO) | 유효성 마커 겸용 |
|
||||||
|
| 44~55 | 데드밴드(하강) Co2/Pm25/Pm10/Voc × 3프리셋 | u8 |
|
||||||
|
| 56~79 | CO2 임계 Co2_Thr[3][4] | u16 |
|
||||||
|
| 80~103 | VOC 임계 Voc_Thr[3][4] | u16 |
|
||||||
|
| 104~115 | PM2.5 임계 Pm25_Thr[3][4] | u8 |
|
||||||
|
| 116~127 | PM10 임계 Pm10_Thr[3][4] | u8 |
|
||||||
|
|
||||||
|
- 대시보드에서 전원/모드/VSP/히스테리시스(프리셋·임계·데드밴드) 변경 → ERV가 EEPROM 저장(재부팅 유지).
|
||||||
|
- 구버전 펌웨어(STATUS 134B, 구 m_*_Level) EEPROM은 신규 영역 미초기화 → 기본값(사양값) 사용 후 변경 시 저장.
|
||||||
|
|
||||||
|
BUNBAGI 사양
|
||||||
|
|
||||||
|
회로도 : HERV_MAIN_REV1.1_20240826(회로도).PDF
|
||||||
|
STATUS LED : STATUS_LED-PB.14, ACTIVE LOW
|
||||||
|
|
||||||
|
DIFFUSER 사양
|
||||||
|
급기(SA) DIFFUSER 구성 : 댐퍼
|
||||||
|
배기(RA) DIFFUSER 구성 : 댐퍼, 공기질센서, LED
|
||||||
|
|
||||||
|
회로도 : 전동디퓨져_REV1.0_20260130.PDF
|
||||||
|
STATUS LED : STATUS_LED-PA.11, ACTIVE LOW
|
||||||
|
SELECT SWITCH : SW1, DIPSW1-PA.10, LOW-SA, HIGH-RA
|
||||||
|
485 통신 : CN3, CN4, INT485TX-PB.5, INT485RX-PB.4, INT485DIR-PB.6
|
||||||
|
댐퍼 : CN2, STEP1_A-PC.8,STEP1_B-PC.9,STEP1_C-PC.10,STEP1_D-PC.11
|
||||||
|
공기질 센서 : CN5, SENSOR_ON-PA.3, SENSOR_SCL-PA.5, SENSOR_SDA-PA.4, SEN66
|
||||||
|
LED : CN6, LED_ON-PA.15, LED_R-PA.14,LED_G-PA.13,LED_B-PA.12
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<RootNamespace>AirPlanner</RootNamespace>
|
||||||
|
<AssemblyName>AirPlanner</AssemblyName>
|
||||||
|
<StartupObject>AirPlanner.App</StartupObject>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<Application x:Class="AirPlanner.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
StartupUri="MainWindow.xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceDictionary Source="Styles.xaml"/>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace AirPlanner
|
||||||
|
{
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<UserControl x:Class="AirPlanner.ControlView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
FontFamily="Malgun Gothic, Segoe UI" Background="#FFFFFF">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="210"/>
|
||||||
|
<ColumnDefinition Width="160"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 카테고리 -->
|
||||||
|
<Border Grid.Column="0" Background="#F6F7F9" BorderBrush="{StaticResource CardBorder}" BorderThickness="0,0,1,0">
|
||||||
|
<StackPanel Margin="0,28,0,0">
|
||||||
|
<Button Style="{StaticResource ZoneBtn}" Content="시스템 에어컨"/>
|
||||||
|
<Button Style="{StaticResource ZoneBtn}" Content="환기" Foreground="{StaticResource Ink}" FontWeight="Bold"/>
|
||||||
|
<Button Style="{StaticResource ZoneBtn}" Content="대기전력콘센트"/>
|
||||||
|
<Button Style="{StaticResource ZoneBtn}" Content="엘리베이터 호출"/>
|
||||||
|
<Button Style="{StaticResource ZoneBtn}" Content="쿡탑"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 구역 (클릭 → 우측 전환) -->
|
||||||
|
<Border Grid.Column="1" Background="#F6F7F9" BorderBrush="{StaticResource CardBorder}" BorderThickness="0,0,1,0">
|
||||||
|
<StackPanel Margin="0,28,0,0">
|
||||||
|
<Button x:Name="zAll" Style="{StaticResource ZoneBtn}" Content="전체" Click="Zone_Click" Tag="전체"/>
|
||||||
|
<Button x:Name="zLiving" Style="{StaticResource ZoneBtn}" Content="거실" Click="Zone_Click" Tag="거실"/>
|
||||||
|
<Button x:Name="zR1" Style="{StaticResource ZoneBtn}" Content="침실1" Click="Zone_Click" Tag="침실1"/>
|
||||||
|
<Button x:Name="zR2" Style="{StaticResource ZoneBtn}" Content="침실2" Click="Zone_Click" Tag="침실2"/>
|
||||||
|
<Button x:Name="zR3" Style="{StaticResource ZoneBtn}" Content="침실3" Click="Zone_Click" Tag="침실3"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 구역별 콘텐츠 -->
|
||||||
|
<ContentControl x:Name="ZoneContent" Grid.Column="2"/>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace AirPlanner
|
||||||
|
{
|
||||||
|
public partial class ControlView : UserControl
|
||||||
|
{
|
||||||
|
static readonly Brush Off = (Brush)new BrushConverter().ConvertFromString("#A6ABB6")!;
|
||||||
|
static readonly Brush On = (Brush)new BrushConverter().ConvertFromString("#161616")!;
|
||||||
|
|
||||||
|
public ControlView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
ShowZone("전체", zAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Zone_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Button b && b.Tag is string zone) ShowZone(zone, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShowZone(string zone, Button active)
|
||||||
|
{
|
||||||
|
ZoneContent.Content = zone == "전체" ? new VentAllView() : new RoomDetailView(zone);
|
||||||
|
foreach (var b in new[] { zAll, zLiving, zR1, zR2, zR3 })
|
||||||
|
{
|
||||||
|
bool on = b == active;
|
||||||
|
b.Foreground = on ? On : Off;
|
||||||
|
b.FontWeight = on ? FontWeights.Bold : FontWeights.Normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<UserControl x:Class="AirPlanner.HomeView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
FontFamily="Malgun Gothic, Segoe UI" Background="#FFFFFF">
|
||||||
|
<Grid Margin="26,22,26,22">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="600"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 좌: 환기 상태 -->
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock x:Name="txtClock" Text="10월 28일 (월) 오후 12시 37분" FontSize="16" Foreground="#6B7280"/>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,12,0,0">
|
||||||
|
<Grid Width="34" Height="34" VerticalAlignment="Center" Margin="0,0,12,0">
|
||||||
|
<Ellipse Fill="#141417"/>
|
||||||
|
<TextBlock Text="⏻" FontFamily="Segoe UI Symbol" FontSize="17" Foreground="White"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock x:Name="txtState" Text="자동운전 중입니다." FontSize="30" FontWeight="Bold" Foreground="{StaticResource Ink}" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 4실 원형 -->
|
||||||
|
<UniformGrid Columns="2" Rows="2" Width="360" HorizontalAlignment="Left" Margin="40,22,0,0">
|
||||||
|
<Grid Width="94" Height="94" Margin="0,0,0,18" HorizontalAlignment="Left">
|
||||||
|
<Ellipse Width="94" Height="94" StrokeThickness="3.5" Stroke="#5FB236" Fill="Transparent"/>
|
||||||
|
<TextBlock Text="거실" FontSize="17" Foreground="{StaticResource Ink}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Width="94" Height="94" Margin="0,0,0,18" HorizontalAlignment="Left">
|
||||||
|
<Ellipse Width="94" Height="94" StrokeThickness="3.5" Stroke="#5BB6E8" Fill="Transparent"/>
|
||||||
|
<TextBlock Text="침실1" FontSize="17" Foreground="{StaticResource Ink}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Width="94" Height="94" HorizontalAlignment="Left">
|
||||||
|
<Ellipse Width="94" Height="94" StrokeThickness="3.5" Stroke="#EF5350" Fill="Transparent"/>
|
||||||
|
<TextBlock Text="침실2" FontSize="17" Foreground="{StaticResource Ink}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Width="94" Height="94" HorizontalAlignment="Left">
|
||||||
|
<Ellipse Width="94" Height="94" StrokeThickness="3.5" Stroke="#F59E0B" Fill="Transparent"/>
|
||||||
|
<TextBlock Text="침실3" FontSize="17" Foreground="{StaticResource Ink}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
</UniformGrid>
|
||||||
|
|
||||||
|
<!-- 제어 카드 -->
|
||||||
|
<Border CornerRadius="14" Background="#FFFFFF" BorderBrush="{StaticResource CardBorder}" BorderThickness="1.5"
|
||||||
|
Padding="22,18" Margin="0,16,0,0" HorizontalAlignment="Left" Width="560">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 좌: 4 토글 -->
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<Grid Margin="0,0,0,12"><Grid.ColumnDefinitions><ColumnDefinition Width="100"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="자동운전" Style="{StaticResource ToggleLabel}"/>
|
||||||
|
<ToggleButton Grid.Column="1" Style="{StaticResource Toggle}" IsChecked="True"/></Grid>
|
||||||
|
<Grid Margin="0,0,0,12"><Grid.ColumnDefinitions><ColumnDefinition Width="100"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="스마트수면" Style="{StaticResource ToggleLabel}"/>
|
||||||
|
<ToggleButton Grid.Column="1" Style="{StaticResource Toggle}"/></Grid>
|
||||||
|
<Grid Margin="0,0,0,12"><Grid.ColumnDefinitions><ColumnDefinition Width="100"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="쾌적조리" Style="{StaticResource ToggleLabel}"/>
|
||||||
|
<ToggleButton Grid.Column="1" Style="{StaticResource Toggle}" IsChecked="True"/></Grid>
|
||||||
|
<Grid><Grid.ColumnDefinitions><ColumnDefinition Width="100"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="안심회복" Style="{StaticResource ToggleLabel}"/>
|
||||||
|
<ToggleButton Grid.Column="1" Style="{StaticResource Toggle}"/></Grid>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 우: 세그먼트 + 알약 + 제습기 연동 -->
|
||||||
|
<StackPanel Grid.Column="1" Margin="20,0,0,0" VerticalAlignment="Center">
|
||||||
|
<Border CornerRadius="9" Background="#EFEFF1" Padding="3" HorizontalAlignment="Left" Margin="0,0,0,12">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<RadioButton GroupName="seg" Style="{StaticResource Seg}" Content="Eco"/>
|
||||||
|
<RadioButton GroupName="seg" Style="{StaticResource Seg}" Content="Normal" IsChecked="True"/>
|
||||||
|
<RadioButton GroupName="seg" Style="{StaticResource Seg}" Content="Turbo"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
||||||
|
<Border CornerRadius="9" BorderBrush="{StaticResource CardBorder}" BorderThickness="1.5" Padding="13,9" Margin="0,0,10,0">
|
||||||
|
<StackPanel Orientation="Horizontal"><TextBlock Text="운전모드" Style="{StaticResource PillLabel}"/><TextBlock Text="청정환기" Style="{StaticResource PillValue}" Margin="8,0,0,0"/></StackPanel></Border>
|
||||||
|
<Border CornerRadius="9" BorderBrush="{StaticResource CardBorder}" BorderThickness="1.5" Padding="13,9">
|
||||||
|
<StackPanel Orientation="Horizontal"><TextBlock Text="풍량" Style="{StaticResource PillLabel}"/><TextBlock Text="3단" Style="{StaticResource PillValue}" Margin="8,0,0,0"/></StackPanel></Border>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
|
<TextBlock Text="제습기 연동" Style="{StaticResource ToggleLabel}" FontSize="15" Margin="0,0,12,0"/>
|
||||||
|
<ToggleButton Style="{StaticResource Toggle}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 우: 스마트홈 카드 -->
|
||||||
|
<UniformGrid Grid.Column="1" Columns="3" Rows="2" Margin="18,0,0,0">
|
||||||
|
|
||||||
|
<Border Style="{StaticResource HomeCard}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
|
||||||
|
<Grid><TextBlock Text="기상정보" Style="{StaticResource CardTitle}"/><TextBlock Text="›" Style="{StaticResource CardArrow}"/></Grid>
|
||||||
|
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="⛅" FontFamily="Segoe UI Emoji" FontSize="54" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="15°" FontSize="40" FontWeight="Bold" Foreground="{StaticResource Ink}" VerticalAlignment="Center" Margin="14,0,0,0"/>
|
||||||
|
<TextBlock Text="11° / 23°" FontSize="15" Foreground="{StaticResource Ink2}" VerticalAlignment="Bottom" Margin="12,0,0,12"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource HomeCard}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
|
||||||
|
<Grid><TextBlock Text="엘리베이터 호출" Style="{StaticResource CardTitle}"/><TextBlock Text="›" Style="{StaticResource CardArrow}"/></Grid>
|
||||||
|
<TextBlock Grid.Row="1" Text="7F" FontSize="20" Foreground="{StaticResource Ink2}" Margin="0,8,0,0"/>
|
||||||
|
<Border Grid.Row="2" CornerRadius="8" BorderBrush="{StaticResource CardBorder}" BorderThickness="1.5" Padding="0,10">
|
||||||
|
<TextBlock Text="CALL" FontSize="15" Foreground="{StaticResource Ink2}" HorizontalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource HomeCard}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
|
||||||
|
<Grid><TextBlock Text="방문차량등록" Style="{StaticResource CardTitle}"/><TextBlock Text="›" Style="{StaticResource CardArrow}"/></Grid>
|
||||||
|
<TextBlock Grid.Row="1" Text="총1대" FontSize="18" Foreground="{StaticResource Ink2}" Margin="0,8,0,0"/>
|
||||||
|
<TextBlock Grid.Row="3" Foreground="{StaticResource Ink2}" FontSize="15">
|
||||||
|
<Run Text="잔여마일리지"/><LineBreak/><Run Text="32시간 50분"/>
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource HomeCard}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
|
||||||
|
<Grid><TextBlock Text="주차위치" Style="{StaticResource CardTitle}"/><TextBlock Text="›" Style="{StaticResource CardArrow}"/></Grid>
|
||||||
|
<StackPanel Grid.Row="1" Margin="0,10,0,0">
|
||||||
|
<Grid Margin="0,0,0,6"><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="B1-A01" FontSize="17" Foreground="{StaticResource Ink2}"/><TextBlock Grid.Column="1" Text="9401" FontSize="17" Foreground="{StaticResource Ink2}"/></Grid>
|
||||||
|
<Grid><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="B2-C11" FontSize="17" Foreground="{StaticResource Ink2}"/><TextBlock Grid.Column="1" Text="3486" FontSize="17" Foreground="{StaticResource Ink2}"/></Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource HomeCard}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
|
||||||
|
<Grid><TextBlock Text="에너지사용량" Style="{StaticResource CardTitle}"/><TextBlock Text="›" Style="{StaticResource CardArrow}"/></Grid>
|
||||||
|
<TextBlock Grid.Row="1" Text="전기 256kwh" FontSize="17" Foreground="{StaticResource Ink2}" Margin="0,10,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource HomeCard}">
|
||||||
|
<TextBlock Text="+ 추가" FontSize="18" Foreground="{StaticResource Ink2}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</UniformGrid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace AirPlanner
|
||||||
|
{
|
||||||
|
public partial class HomeView : UserControl
|
||||||
|
{
|
||||||
|
readonly DispatcherTimer _clock = new() { Interval = TimeSpan.FromSeconds(10) };
|
||||||
|
|
||||||
|
public HomeView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_clock.Tick += (_, _) => UpdateClock();
|
||||||
|
_clock.Start();
|
||||||
|
UpdateClock();
|
||||||
|
Unloaded += (_, _) => _clock.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateClock()
|
||||||
|
{
|
||||||
|
var n = DateTime.Now;
|
||||||
|
string[] dow = { "일", "월", "화", "수", "목", "금", "토" };
|
||||||
|
string ap = n.Hour < 12 ? "오전" : "오후";
|
||||||
|
int h12 = n.Hour % 12; if (h12 == 0) h12 = 12;
|
||||||
|
txtClock.Text = $"{n.Month}월 {n.Day}일 ({dow[(int)n.DayOfWeek]}) {ap} {h12}시 {n.Minute:00}분";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<Window x:Class="AirPlanner.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="AirPlanner — 에어플래너 (e편한세상)" Height="600" Width="1400"
|
||||||
|
Background="#141417" FontFamily="Malgun Gothic, Segoe UI"
|
||||||
|
WindowStartupLocation="CenterScreen">
|
||||||
|
|
||||||
|
<Viewbox Stretch="Uniform">
|
||||||
|
<Grid Width="1400" Height="560" Background="#FFFFFF">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="92"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 좌측 네비게이션 (공유) -->
|
||||||
|
<Border Grid.Column="0" Background="#141417">
|
||||||
|
<StackPanel Margin="0,30,0,0">
|
||||||
|
<Button x:Name="navHome" Style="{StaticResource NavBtn}" Content="홈" Click="Nav_Home"/>
|
||||||
|
<Button x:Name="navCall" Style="{StaticResource NavBtn}" Content="통화" Click="Nav_Todo"/>
|
||||||
|
<Button x:Name="navCtrl" Style="{StaticResource NavBtn}" Content="제어" Click="Nav_Control"/>
|
||||||
|
<Button x:Name="navView" Style="{StaticResource NavBtn}" Content="조회" Click="Nav_Todo"/>
|
||||||
|
<Button x:Name="navSet" Style="{StaticResource NavBtn}" Content="설정" Click="Nav_Todo"/>
|
||||||
|
<Button x:Name="navAlarm" Style="{StaticResource NavBtn}" Click="Nav_Todo">
|
||||||
|
<Grid>
|
||||||
|
<TextBlock Text="알림" FontSize="18" Foreground="#7B8090"/>
|
||||||
|
<Ellipse Width="6" Height="6" Fill="#EF4444" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,2,-8,0"/>
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="navEmer" Style="{StaticResource NavBtn}" Content="비상" Foreground="#EF4444" Click="Nav_Todo"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 화면 영역 -->
|
||||||
|
<ContentControl x:Name="MainContent" Grid.Column="1"/>
|
||||||
|
</Grid>
|
||||||
|
</Viewbox>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace AirPlanner
|
||||||
|
{
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
static readonly Brush NavOff = (Brush)new BrushConverter().ConvertFromString("#7B8090")!;
|
||||||
|
static readonly Brush NavOn = Brushes.White;
|
||||||
|
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Show("home");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Show(string view)
|
||||||
|
{
|
||||||
|
if (view == "control") { MainContent.Content = new ControlView(); SetActive(navCtrl); }
|
||||||
|
else { MainContent.Content = new HomeView(); SetActive(navHome); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetActive(Button active)
|
||||||
|
{
|
||||||
|
foreach (var b in new[] { navHome, navCall, navCtrl, navView, navSet })
|
||||||
|
{
|
||||||
|
b.Foreground = b == active ? NavOn : NavOff;
|
||||||
|
b.FontWeight = b == active ? FontWeights.Bold : FontWeights.Normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Nav_Home(object sender, RoutedEventArgs e) => Show("home");
|
||||||
|
void Nav_Control(object sender, RoutedEventArgs e) => Show("control");
|
||||||
|
void Nav_Todo(object sender, RoutedEventArgs e) { /* 통화/조회/설정/알림/비상 — 추후 구현 */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<UserControl x:Class="AirPlanner.RoomCard"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
FontFamily="Malgun Gothic, Segoe UI">
|
||||||
|
<Border CornerRadius="12" Background="{StaticResource CardBg}"
|
||||||
|
BorderBrush="{StaticResource CardBorder}" BorderThickness="1.5" Margin="8" Padding="18,16">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 헤더 : 이름 + 공기질 원 -->
|
||||||
|
<Grid Grid.Row="0">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock x:Name="NameText" Grid.Column="0" Text="거실" FontSize="20" FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource Ink}" VerticalAlignment="Center"/>
|
||||||
|
<Grid Grid.Column="1" Width="64" Height="64">
|
||||||
|
<Ellipse x:Name="AqRing" Width="64" Height="64" StrokeThickness="3.5" Stroke="{StaticResource AqGood}" Fill="Transparent"/>
|
||||||
|
<TextBlock x:Name="AqText" Text="좋음" FontSize="13" FontWeight="SemiBold" Width="54" TextAlignment="Center" TextWrapping="Wrap"
|
||||||
|
Foreground="{StaticResource AqGood}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- 센서 4종 (글씨 검정) -->
|
||||||
|
<StackPanel Grid.Row="1" Margin="2,14,0,0" VerticalAlignment="Top">
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
|
||||||
|
<TextBlock Text="•" Foreground="{StaticResource AqVeryBad}" FontSize="13" Margin="0,0,7,0"/>
|
||||||
|
<TextBlock x:Name="S0" Text="초미세먼지: 좋음" FontSize="13.5" Foreground="{StaticResource Ink}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
|
||||||
|
<TextBlock Text="•" Foreground="{StaticResource AqVeryBad}" FontSize="13" Margin="0,0,7,0"/>
|
||||||
|
<TextBlock x:Name="S1" Text="미세먼지: 좋음" FontSize="13.5" Foreground="{StaticResource Ink}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
|
||||||
|
<TextBlock Text="•" Foreground="{StaticResource AqVeryBad}" FontSize="13" Margin="0,0,7,0"/>
|
||||||
|
<TextBlock x:Name="S2" Text="CO2: 좋음" FontSize="13.5" Foreground="{StaticResource Ink}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="•" Foreground="{StaticResource AqVeryBad}" FontSize="13" Margin="0,0,7,0"/>
|
||||||
|
<TextBlock x:Name="S3" Text="VOCs: 좋음" FontSize="13.5" Foreground="{StaticResource Ink}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- ON / Standby / OFF -->
|
||||||
|
<ToggleButton x:Name="OnBtn" Grid.Row="2" Style="{StaticResource BigToggleBtn}" Content="ON"
|
||||||
|
Height="48" Margin="0,16,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace AirPlanner
|
||||||
|
{
|
||||||
|
public partial class RoomCard : UserControl
|
||||||
|
{
|
||||||
|
public RoomCard()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 실명 / 공기질 텍스트·색 / 센서 4종 상태 / 버튼(ON·Standby·OFF, active=검정)
|
||||||
|
public void Set(string name, string aqText, Brush aqColor,
|
||||||
|
string pm25, string pm10, string co2, string voc,
|
||||||
|
string btnText, bool btnActive)
|
||||||
|
{
|
||||||
|
NameText.Text = name;
|
||||||
|
AqText.Text = aqText;
|
||||||
|
AqText.Foreground = aqColor;
|
||||||
|
AqRing.Stroke = aqColor;
|
||||||
|
S0.Text = $"초미세먼지: {pm25}";
|
||||||
|
S1.Text = $"미세먼지: {pm10}";
|
||||||
|
S2.Text = $"CO2: {co2}";
|
||||||
|
S3.Text = $"VOCs: {voc}";
|
||||||
|
OnBtn.Content = btnText;
|
||||||
|
OnBtn.IsChecked = btnActive; // true → 검정(ON/Standby)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<UserControl x:Class="AirPlanner.RoomDetailView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
FontFamily="Malgun Gothic, Segoe UI" Background="#FFFFFF">
|
||||||
|
<Grid Margin="40,26,40,26">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 상단 : 전원원형 + 운전모드/토글 -->
|
||||||
|
<Grid Grid.Row="0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid Grid.Column="0" Width="200" Height="200" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||||
|
<Ellipse Fill="#141417"/>
|
||||||
|
<TextBlock Text="⏻" FontFamily="Segoe UI Symbol" FontSize="86" Foreground="White"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="40,0,0,0">
|
||||||
|
<TextBlock Text="청정환기" FontSize="24" FontWeight="Bold" Foreground="{StaticResource Ink}"/>
|
||||||
|
<TextBlock Text="자동운전" FontSize="24" FontWeight="Bold" Foreground="{StaticResource Ink}" Margin="0,8,0,16"/>
|
||||||
|
<!-- 실별 토글 — 코드에서 구성 -->
|
||||||
|
<StackPanel x:Name="TogglePanel"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- 하단 : 실내온습도 + 공기질 4종 (같은 줄) -->
|
||||||
|
<Grid Grid.Row="1" Margin="0,18,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 실내온도 / 실내습도 (크게) -->
|
||||||
|
<StackPanel Grid.Column="0" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<StackPanel HorizontalAlignment="Center">
|
||||||
|
<TextBlock Text="실내온도" FontSize="17" Foreground="{StaticResource Ink2}" HorizontalAlignment="Center"/>
|
||||||
|
<TextBlock Text="24°" FontSize="28" FontWeight="Bold" Foreground="{StaticResource Ink}" HorizontalAlignment="Center" Margin="0,4,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Border Width="1" Background="{StaticResource CardBorder}" Margin="26,4,26,4"/>
|
||||||
|
<StackPanel HorizontalAlignment="Center">
|
||||||
|
<TextBlock Text="실내습도" FontSize="17" Foreground="{StaticResource Ink2}" HorizontalAlignment="Center"/>
|
||||||
|
<TextBlock Text="24%" FontSize="28" FontWeight="Bold" Foreground="{StaticResource Ink}" HorizontalAlignment="Center" Margin="0,4,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 공기질 4종 (모두 좋음) -->
|
||||||
|
<UniformGrid Grid.Column="1" Columns="4" VerticalAlignment="Center">
|
||||||
|
<StackPanel HorizontalAlignment="Center">
|
||||||
|
<Grid Width="64" Height="64" HorizontalAlignment="Center">
|
||||||
|
<Ellipse Width="64" Height="64" StrokeThickness="3.5" Stroke="{StaticResource AqGood}" Fill="Transparent"/>
|
||||||
|
<TextBlock Text="좋음" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource AqGood}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="초미세먼지" FontSize="13" Foreground="{StaticResource Ink}" HorizontalAlignment="Center" Margin="0,8,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel HorizontalAlignment="Center">
|
||||||
|
<Grid Width="64" Height="64" HorizontalAlignment="Center">
|
||||||
|
<Ellipse Width="64" Height="64" StrokeThickness="3.5" Stroke="{StaticResource AqGood}" Fill="Transparent"/>
|
||||||
|
<TextBlock Text="좋음" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource AqGood}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="미세먼지" FontSize="13" Foreground="{StaticResource Ink}" HorizontalAlignment="Center" Margin="0,8,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel HorizontalAlignment="Center">
|
||||||
|
<Grid Width="64" Height="64" HorizontalAlignment="Center">
|
||||||
|
<Ellipse Width="64" Height="64" StrokeThickness="3.5" Stroke="{StaticResource AqGood}" Fill="Transparent"/>
|
||||||
|
<TextBlock Text="좋음" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource AqGood}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="이산화탄소" FontSize="13" Foreground="{StaticResource Ink}" HorizontalAlignment="Center" Margin="0,8,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel HorizontalAlignment="Center">
|
||||||
|
<Grid Width="64" Height="64" HorizontalAlignment="Center">
|
||||||
|
<Ellipse Width="64" Height="64" StrokeThickness="3.5" Stroke="{StaticResource AqGood}" Fill="Transparent"/>
|
||||||
|
<TextBlock Text="좋음" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource AqGood}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="휘발성" FontSize="13" Foreground="{StaticResource Ink}" HorizontalAlignment="Center" Margin="0,8,0,0"/>
|
||||||
|
<TextBlock Text="유기 화합물" FontSize="13" Foreground="{StaticResource Ink}" HorizontalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</UniformGrid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace AirPlanner
|
||||||
|
{
|
||||||
|
public partial class RoomDetailView : UserControl
|
||||||
|
{
|
||||||
|
// 거실 : 표시램프만
|
||||||
|
// 침실1 : 안심회복 + 표시램프 + 제습기 연동 (안심회복은 침실1 전용)
|
||||||
|
// 침실2·침실3 : 표시램프 + 제습기 연동
|
||||||
|
public RoomDetailView(string roomName)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
bool isBedroom = roomName != "거실";
|
||||||
|
if (roomName == "침실1") TogglePanel.Children.Add(MakeRow("안심회복", false));
|
||||||
|
TogglePanel.Children.Add(MakeRow("표시램프", true));
|
||||||
|
if (isBedroom) TogglePanel.Children.Add(MakeRow("제습기 연동", false));
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameworkElement MakeRow(string label, bool on)
|
||||||
|
{
|
||||||
|
var sp = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 12) };
|
||||||
|
sp.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = label, FontSize = 18, VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Foreground = (Brush)FindResource("Ink"), Margin = new Thickness(0, 0, 12, 0), MinWidth = 86
|
||||||
|
});
|
||||||
|
sp.Children.Add(new ToggleButton { Style = (Style)FindResource("Toggle"), IsChecked = on });
|
||||||
|
return sp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
|
||||||
|
<!-- ===== 색상 ===== -->
|
||||||
|
<Color x:Key="cBg">#FFFFFF</Color>
|
||||||
|
<SolidColorBrush x:Key="Bg" Color="#FFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="CardBg" Color="#FFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="CardBorder" Color="#E5E7EB"/>
|
||||||
|
<SolidColorBrush x:Key="Ink" Color="#161616"/>
|
||||||
|
<SolidColorBrush x:Key="Ink2" Color="#6B7280"/>
|
||||||
|
<SolidColorBrush x:Key="TrackOff" Color="#E3E5EA"/>
|
||||||
|
<SolidColorBrush x:Key="ThumbBr" Color="#FFFFFF"/>
|
||||||
|
<!-- 공기질 4단계 -->
|
||||||
|
<SolidColorBrush x:Key="AqGood" Color="#3B82F6"/> <!-- 좋음 파랑 -->
|
||||||
|
<SolidColorBrush x:Key="AqNormal" Color="#22C55E"/> <!-- 보통 초록 -->
|
||||||
|
<SolidColorBrush x:Key="AqBad" Color="#F59E0B"/> <!-- 나쁨 주황 -->
|
||||||
|
<SolidColorBrush x:Key="AqVeryBad" Color="#EF4444"/> <!-- 매우나쁨 빨강 -->
|
||||||
|
|
||||||
|
<!-- ===== iOS 스타일 토글 스위치 ===== -->
|
||||||
|
<Style x:Key="Toggle" TargetType="ToggleButton">
|
||||||
|
<Setter Property="Width" Value="50"/>
|
||||||
|
<Setter Property="Height" Value="29"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ToggleButton">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="track" CornerRadius="15" Background="{StaticResource TrackOff}"/>
|
||||||
|
<Ellipse x:Name="thumb" Width="23" Height="23" Fill="{StaticResource ThumbBr}"
|
||||||
|
HorizontalAlignment="Left" Margin="3,0,0,0">
|
||||||
|
<Ellipse.Effect><DropShadowEffect BlurRadius="4" ShadowDepth="1" Opacity="0.25"/></Ellipse.Effect>
|
||||||
|
</Ellipse>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="track" Property="Background" Value="{StaticResource Ink}"/>
|
||||||
|
<Setter TargetName="thumb" Property="HorizontalAlignment" Value="Right"/>
|
||||||
|
<Setter TargetName="thumb" Property="Margin" Value="0,0,3,0"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.35"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- 토글 행 라벨 -->
|
||||||
|
<Style x:Key="ToggleLabel" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="17"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Ink}"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== 큰 ON/OFF 버튼 (전체/각실) ===== -->
|
||||||
|
<Style x:Key="BigToggleBtn" TargetType="ToggleButton">
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="FontSize" Value="20"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Ink2}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ToggleButton">
|
||||||
|
<Border x:Name="b" CornerRadius="10" Background="{StaticResource CardBg}"
|
||||||
|
BorderBrush="{StaticResource CardBorder}" BorderThickness="1.5">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="b" Property="Background" Value="{StaticResource Ink}"/>
|
||||||
|
<Setter TargetName="b" Property="BorderBrush" Value="{StaticResource Ink}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== 좌측 네비 버튼 ===== -->
|
||||||
|
<Style x:Key="NavBtn" TargetType="Button">
|
||||||
|
<Setter Property="Foreground" Value="#7B8090"/>
|
||||||
|
<Setter Property="FontSize" Value="18"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,30"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border Background="Transparent" Padding="0,2">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== 카테고리/구역 리스트 버튼 ===== -->
|
||||||
|
<Style x:Key="ZoneBtn" TargetType="Button">
|
||||||
|
<Setter Property="Foreground" Value="#A6ABB6"/>
|
||||||
|
<Setter Property="FontSize" Value="18"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,40"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border Background="Transparent" Padding="0,2">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Eco/Normal/Turbo 세그먼트 (RadioButton) ===== -->
|
||||||
|
<Style x:Key="Seg" TargetType="RadioButton">
|
||||||
|
<Setter Property="Foreground" Value="#6B7280"/>
|
||||||
|
<Setter Property="FontSize" Value="15"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Padding" Value="18,7"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RadioButton">
|
||||||
|
<Border x:Name="b" CornerRadius="7" Background="Transparent" Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="b" Property="Background" Value="White"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Ink}"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
<Setter TargetName="b" Property="Effect">
|
||||||
|
<Setter.Value><DropShadowEffect BlurRadius="5" ShadowDepth="1" Opacity="0.18"/></Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== 스마트홈 카드 (홈 우측) ===== -->
|
||||||
|
<Style x:Key="HomeCard" TargetType="Border">
|
||||||
|
<Setter Property="CornerRadius" Value="14"/>
|
||||||
|
<Setter Property="Background" Value="{StaticResource CardBg}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource CardBorder}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1.5"/>
|
||||||
|
<Setter Property="Padding" Value="18,14"/>
|
||||||
|
<Setter Property="Margin" Value="8"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="CardTitle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="18"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Ink}"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="CardArrow" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="20"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Ink2}"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Top"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== 상태 알약 (운전모드/풍량) ===== -->
|
||||||
|
<Style x:Key="PillLabel" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Ink2}"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="PillValue" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="15"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Ink}"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="Margin" Value="10,0,0,0"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<UserControl x:Class="AirPlanner.VentAllView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="clr-namespace:AirPlanner"
|
||||||
|
FontFamily="Malgun Gothic, Segoe UI" Background="#FFFFFF">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="14,14,8,14">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/><ColumnDefinition Width="*"/><ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 전체 제어 카드 (2열 차지) -->
|
||||||
|
<Border Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Style="{StaticResource HomeCard}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid Grid.Row="0">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto"/><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0" Text="전체" FontSize="20" FontWeight="Bold" Foreground="{StaticResource Ink}" VerticalAlignment="Top" Margin="0,2,0,0"/>
|
||||||
|
|
||||||
|
<!-- 토글 4 -->
|
||||||
|
<StackPanel Grid.Column="1" HorizontalAlignment="Center">
|
||||||
|
<Grid Margin="0,0,0,11"><Grid.ColumnDefinitions><ColumnDefinition Width="92"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="자동운전" Style="{StaticResource ToggleLabel}" FontSize="15"/>
|
||||||
|
<ToggleButton Grid.Column="1" Style="{StaticResource Toggle}" IsChecked="True"/></Grid>
|
||||||
|
<Grid Margin="0,0,0,11"><Grid.ColumnDefinitions><ColumnDefinition Width="92"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="스마트수면" Style="{StaticResource ToggleLabel}" FontSize="15"/>
|
||||||
|
<ToggleButton Grid.Column="1" Style="{StaticResource Toggle}"/></Grid>
|
||||||
|
<Grid Margin="0,0,0,11"><Grid.ColumnDefinitions><ColumnDefinition Width="92"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="쾌적조리" Style="{StaticResource ToggleLabel}" FontSize="15"/>
|
||||||
|
<ToggleButton Grid.Column="1" Style="{StaticResource Toggle}" IsChecked="True"/></Grid>
|
||||||
|
<Grid><Grid.ColumnDefinitions><ColumnDefinition Width="92"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="안심회복" Style="{StaticResource ToggleLabel}" FontSize="15"/>
|
||||||
|
<ToggleButton Grid.Column="1" Style="{StaticResource Toggle}"/></Grid>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 알약 3 (자동운전/운전모드/풍량) -->
|
||||||
|
<StackPanel Grid.Column="2" VerticalAlignment="Center" Margin="14,0,0,0">
|
||||||
|
<Border CornerRadius="9" BorderBrush="{StaticResource CardBorder}" BorderThickness="1.5" Padding="13,9" Margin="0,0,0,9">
|
||||||
|
<StackPanel Orientation="Horizontal"><TextBlock Text="자동운전" Style="{StaticResource PillLabel}"/><TextBlock Text="Normal" Style="{StaticResource PillValue}" Margin="8,0,0,0"/></StackPanel></Border>
|
||||||
|
<Border CornerRadius="9" BorderBrush="{StaticResource CardBorder}" BorderThickness="1.5" Padding="13,9" Margin="0,0,0,9">
|
||||||
|
<StackPanel Orientation="Horizontal"><TextBlock Text="운전모드" Style="{StaticResource PillLabel}"/><TextBlock Text="청정환기" Style="{StaticResource PillValue}" Margin="8,0,0,0"/></StackPanel></Border>
|
||||||
|
<Border CornerRadius="9" BorderBrush="{StaticResource CardBorder}" BorderThickness="1.5" Padding="13,9">
|
||||||
|
<StackPanel Orientation="Horizontal"><TextBlock Text="풍량" Style="{StaticResource PillLabel}"/><TextBlock Text="3단" Style="{StaticResource PillValue}" Margin="8,0,0,0"/></StackPanel></Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- 하단 : ON + 제습기 연동 -->
|
||||||
|
<Grid Grid.Row="1" Margin="0,16,0,0">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||||
|
<ToggleButton Grid.Column="0" Style="{StaticResource BigToggleBtn}" Content="ON" IsChecked="True" Height="46" Margin="0,0,16,0"/>
|
||||||
|
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="제습기 연동" Style="{StaticResource ToggleLabel}" FontSize="15" Margin="0,0,12,0"/>
|
||||||
|
<ToggleButton Style="{StaticResource Toggle}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<local:RoomCard x:Name="cLiving" Grid.Row="0" Grid.Column="2"/>
|
||||||
|
<local:RoomCard x:Name="cR1" Grid.Row="1" Grid.Column="0"/>
|
||||||
|
<local:RoomCard x:Name="cR2" Grid.Row="1" Grid.Column="1"/>
|
||||||
|
<local:RoomCard x:Name="cR3" Grid.Row="1" Grid.Column="2"/>
|
||||||
|
</Grid>
|
||||||
|
</ScrollViewer>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace AirPlanner
|
||||||
|
{
|
||||||
|
public partial class VentAllView : UserControl
|
||||||
|
{
|
||||||
|
public VentAllView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Loaded += (_, _) =>
|
||||||
|
{
|
||||||
|
cLiving.Set("거실", "좋음", Aq("AqGood"), "좋음", "좋음", "좋음", "좋음", "Standby", true);
|
||||||
|
cR1.Set("침실1", "좋음", Aq("AqGood"), "좋음", "좋음", "좋음", "좋음", "Standby", true);
|
||||||
|
cR2.Set("침실2", "보통", Aq("AqNormal"), "좋음", "좋음", "보통", "보통", "ON", true);
|
||||||
|
cR3.Set("침실3", "나쁨", Aq("AqBad"), "좋음", "좋음", "보통", "나쁨", "ON", true);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Brush Aq(string key) => (Brush)FindResource(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# EW11 (RS-485 ↔ WiFi) HOMENET 연결 검토
|
||||||
|
|
||||||
|
- 작성일: 2026-05-26
|
||||||
|
- 대상 장비: Elfin EW11A (Hi-Flying) — RS-485 ↔ WiFi 투명 시리얼 브리지
|
||||||
|
- 대상 MCU: Nuvoton Nano100 시리즈 (HERV)
|
||||||
|
- 관련 문서:
|
||||||
|
- `EW11_RS485 TO WIFI/EW11A manual/Elfin-EW1X_User ManualV1.3(20200415).pdf`
|
||||||
|
- `EW11_RS485 TO WIFI/IOTService3.1.0_20220926/`
|
||||||
|
- `doc/HERV_MCU PINMAP & CONNECTOR.txt`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 결론
|
||||||
|
|
||||||
|
**하드웨어/펌웨어 모두 가능**. 단, UART1 채널이 PC_Monitoring(디버그)과 HOMENET(CVnet)에 동시에 사용될 수 없는 구조적 트레이드오프가 있음.
|
||||||
|
|
||||||
|
EW11은 **투명 시리얼 브리지(Transparent Serial Bridge)**이므로 UART ↔ TCP/WiFi 변환을 EW11 내부에서 처리한다. MCU 펌웨어 측에서는 EW11의 존재를 알 필요가 없고, 기존 CVnet 프로토콜 코드를 그대로 사용하면 된다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 현재 시스템 UART 채널 구성
|
||||||
|
|
||||||
|
| 채널 | 용도 | 초기화 위치 | Baud |
|
||||||
|
|---|---|---|---|
|
||||||
|
| UART0 | Hood (후드, CN6/CN13) | `program/User/My_system.c:36` | 9600 |
|
||||||
|
| **UART1** | **HOMENET (CVnet) — CN15** | `program/User/My_system.c:50` | 9600 |
|
||||||
|
| SC0 (SCUART) | Roomcon 232 | `program/User/My_system.c:64` | 1200 |
|
||||||
|
| SC1 (SCUART) | Bunbaegi 분배기 | `program/User/My_system.c:73` | 115200 |
|
||||||
|
|
||||||
|
### HOMENET 핀맵 (CN15, RED, HOMENET_485)
|
||||||
|
- TX: PB.5 (E485_TX)
|
||||||
|
- RX: PB.4 (E485_RX)
|
||||||
|
- DIR: PB.6 (E485_DIR)
|
||||||
|
|
||||||
|
> Nano100 시리즈는 **UART0/UART1 2개만 제공**하므로 추가 UART 확보가 불가능하다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 하드웨어 연결 방안
|
||||||
|
|
||||||
|
| 항목 | 기존 HOMENET | EW11 연결 |
|
||||||
|
|---|---|---|
|
||||||
|
| 커넥터 | CN15 (RED), HOMENET_485 | CN15 RS-485 A/B 라인에 EW11 RS-485 단자 병렬 연결 |
|
||||||
|
| MCU 핀 | UART1: PB.4(RX), PB.5(TX), PB.6(DIR) | 변경 없음 |
|
||||||
|
| Baud | 9600 8N1 | EW11도 9600 8N1로 설정 |
|
||||||
|
|
||||||
|
EW11은 자체 RS-485 트랜시버를 가지고 DIR을 자동 제어하므로 MCU의 PB.6 DIR 제어와 충돌하지 않는다(RS-485 멀티드롭 버스 구성).
|
||||||
|
|
||||||
|
```
|
||||||
|
[홈넷 서버/앱] ──(WiFi/TCP)── [EW11] ──(RS-485 A/B)── [CN15] ── [MCU UART1]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 펌웨어 측 필요 작업
|
||||||
|
|
||||||
|
### 4.1 CVnet 프로토콜 복구 (직전 작업 되돌리기)
|
||||||
|
|
||||||
|
직전 작업으로 비활성화한 두 곳을 다시 활성화해야 HOMENET 패킷을 처리할 수 있다.
|
||||||
|
|
||||||
|
- `program/User/main.c:110`
|
||||||
|
```c
|
||||||
|
CVnet_Com_process();
|
||||||
|
```
|
||||||
|
- `program/User/My_Uart.c:1758`
|
||||||
|
```c
|
||||||
|
rx_debug_check(u8InChar);
|
||||||
|
Rx_CVnet_TimeOut = 50;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 PC_Monitoring과 동시 사용 불가 ⚠️
|
||||||
|
|
||||||
|
UART1은 단 하나의 물리 채널이므로 PC_Monitoring(디버그)과 CVnet(홈넷)이 **동시에 사용 불가**하다. 다음 중 하나를 선택해야 한다.
|
||||||
|
|
||||||
|
| 옵션 | 설명 | 비용 | 권장 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| ① 평소 CVnet만, 디버그 시에만 PC_Monitoring | `#ifdef DEBUG_PC_MODE` 컴파일 스위치로 분리 | 펌웨어만 수정 | **★ 권장** |
|
||||||
|
| ② PC_Monitoring을 UART0(Hood)로 이전 | Hood 통신을 잠시 끊고 디버그 | Hood 제어 일시 중단 | △ |
|
||||||
|
| ③ 다른 시리얼(SC0/SC1) 사용 | 기존 통신 영향 | 다른 통신 끊김 | △ |
|
||||||
|
|
||||||
|
### 4.3 CVnet 타임아웃 검토
|
||||||
|
|
||||||
|
WiFi 경로는 **20 ~ 200 ms의 가변 지연**이 발생한다.
|
||||||
|
|
||||||
|
- MCU 측 `Rx_CVnet_TimeOut = 50` (50 ms): **패킷 수신 중단 감지용**이므로 EW11 bridge엔 직접 영향 없음.
|
||||||
|
- 홈넷 마스터(서버) 측의 응답 타임아웃은 EW11 RTT를 감안해 설정 필요. 너무 짧으면 재전송 빈발.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. EW11 설정 (PC에서 IOTService 도구로)
|
||||||
|
|
||||||
|
| 항목 | 권장값 |
|
||||||
|
|---|---|
|
||||||
|
| Serial Baudrate | 9600 |
|
||||||
|
| Data / Parity / Stop | 8 / None / 1 |
|
||||||
|
| Flow Control | None |
|
||||||
|
| Protocol | **TCP Server** (홈넷 서버가 클라이언트로 접속) 또는 **TCP Client** (EW11이 서버에 접속) |
|
||||||
|
| Port | 홈넷 게이트웨이와 합의된 포트 |
|
||||||
|
| Buffer length | 8 (CVnet 패킷이 8바이트 고정) — 패킷 단위 전송 보장 |
|
||||||
|
| Heartbeat / KeepAlive | 활성화 (WiFi 연결 유지) |
|
||||||
|
| WiFi 보안 | WPA2-PSK |
|
||||||
|
| Reconnect | 자동 재접속 활성화 |
|
||||||
|
|
||||||
|
IOTService 다운로드: `http://ftp.hi-flying.com:9000/IOTService/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 위험 요소 및 평가
|
||||||
|
|
||||||
|
| 항목 | 평가 |
|
||||||
|
|---|---|
|
||||||
|
| 펌웨어 변경량 | 매우 적음 (CVnet 코드 복구만) |
|
||||||
|
| 하드웨어 비용 | EW11 1개 + 5V 전원 + WiFi AP |
|
||||||
|
| 응답 지연 | WiFi RTT 20~200 ms 추가 — 시나리오 제어용으론 충분 |
|
||||||
|
| 신뢰성 | WiFi 끊김 시 홈넷 단절. EW11 자동 재접속 설정 필수 |
|
||||||
|
| 보안 | EW11 자체 암호화는 제한적. 동일 LAN 내 사용 권장. 외부 노출 시 VPN/방화벽 필요 |
|
||||||
|
| 디버그 병행 | **불가** — PC 디버그 채널을 별도 UART로 이전하거나 컴파일 분리 필요 |
|
||||||
|
| 멀티드롭 충돌 | MCU와 EW11이 같은 RS-485 버스 → 마스터-슬레이브 규칙 준수 시 문제없음 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 권장 진행 순서
|
||||||
|
|
||||||
|
1. **CVnet 코드 복구** — `main.c:110`, `My_Uart.c:1758` 주석 해제
|
||||||
|
2. **PC_Monitoring 조건부 컴파일 분리** — `#ifdef DEBUG_PC_MODE`로 양산 빌드에서 제외
|
||||||
|
3. **EW11 평가보드 1대 확보** 후 단독 테스트
|
||||||
|
- PC ↔ EW11(WiFi) ↔ RS-485 루프백 (송수신 확인)
|
||||||
|
- PC ↔ EW11(WiFi) ↔ RS-485 ↔ MCU 통신 (CVnet 패킷 응답 확인)
|
||||||
|
4. **응답 지연 측정** — 마스터-슬레이브 타임아웃 마진 검증
|
||||||
|
5. **장시간 안정성 시험** — WiFi 끊김/재접속 시나리오 포함
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 참고
|
||||||
|
|
||||||
|
- EW11A 매뉴얼: `EW11_RS485 TO WIFI/EW11A manual/Elfin-EW1X_User ManualV1.3(20200415).pdf`
|
||||||
|
- 운용 가이드: `EW11_RS485 TO WIFI/EW11A manual/HF2211S_EW1X_PW1X_Wport-W30_Operation Guide(20220525).pdf`
|
||||||
|
- 소프트웨어 기능: `EW11_RS485 TO WIFI/EW11A manual/IOT_Device_Series_Software_Funtion_20250328.pdf`
|
||||||
|
- IOTService 매뉴얼: `EW11_RS485 TO WIFI/IOTService3.1.0_20220926/IOTService_User_Manual_20200708.pdf`
|
||||||
|
- 핀맵: `doc/HERV_MCU PINMAP & CONNECTOR.txt`
|
||||||
|
- HOMENET 프로토콜: `Protocol/힘펠_환기장치프로토콜V3.7_240111.xlsx`
|
||||||
|
- 시나리오 프로토콜: `Protocol/DL 환기 시나리오 제어 프로토콜_20260319-1_노량진모델하우스_2차.xlsx`
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# EW11 → 인터넷(AWS) 데이터 전송 검토
|
||||||
|
|
||||||
|
- 작성일: 2026-06-03
|
||||||
|
- 목적: ERV 각실제어 데이터(신규 PC 대시보드 0xAA 프로토콜)를 **EW11(RS-485↔WiFi)** 로 인터넷 서버에 전송
|
||||||
|
- 대상 장비: Elfin EW11A (Hi-Flying)
|
||||||
|
- 관련 문서:
|
||||||
|
- `EW11A manual/IOT_Device_Series_Software_Funtion_20250328.pdf`
|
||||||
|
- `TestProgram/PC_ERV_Protocol.md` (전송할 프레임 규격)
|
||||||
|
- `260526_EW11_검토.md` (기존: 로컬 HOMENET 연결 검토)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 결론 요약
|
||||||
|
|
||||||
|
**EW11 사용 가능.** 가장 적합한 방식은 **EW11 = TCP Client → AWS 자체 서버(EC2/Lightsail)** 이며, 우리 0xAA 바이너리 프레임이 **투명 전송(Transparent)** 으로 그대로 서버에 도달한다(PC 대시보드의 파서/프로토콜 코드를 서버에서 재사용 가능).
|
||||||
|
|
||||||
|
- **AWS IoT Core 직결은 비권장** — IoT Core는 MQTT 연결 시 **X.509 클라이언트 인증서 기반 mutual TLS**(또는 커스텀 권한부여자)가 필수인데, EW11의 TLS는 매뉴얼상 **"no certificate method"**(클라이언트 인증서 제시 불가)라 직접 연결이 곤란하다.
|
||||||
|
- 표준 IoT 스택이 필요하면 **EC2에 Mosquitto MQTT 브로커**를 띄우고 EW11 MQTT Client(user/password + 서버 TLS)로 붙이는 방식이 차선.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. EW11 네트워크 기능 (매뉴얼 확인)
|
||||||
|
|
||||||
|
| 기능 | 지원 | 비고 |
|
||||||
|
|---|---|---|
|
||||||
|
| TCP Client / TCP Server | ✅ | Client 모드로 외부 서버 접속 가능 |
|
||||||
|
| UDP Client / Server | ✅ | |
|
||||||
|
| Multiple Socket | ✅ | 최대 5 소켓 동시 |
|
||||||
|
| HTTP (GET/POST) | ✅ | UART 데이터를 HTTP 본문/쿼리로 래핑, 헤더 커스텀 |
|
||||||
|
| WebSocket Client | ✅ | |
|
||||||
|
| **MQTT Client** | ✅ | Version 4(MQTT 3.1.1), Client ID/User/Password, Sub/Pub Topic, QoS |
|
||||||
|
| 암호화 | ✅ | **AES**(CBC, 16자 키), **DES3**(24자), **TLS(인증서 없음)** |
|
||||||
|
| TCP Keepalive / Reconnect | ✅ | 연결 끊김 자동 재접속 |
|
||||||
|
| Register / Heartbeat | ✅ | 접속 시 장치 ID 전송, 주기적 하트비트 |
|
||||||
|
| NTP | ✅ | 시각 동기 |
|
||||||
|
|
||||||
|
> ⚠️ **TLS 제약**: "TLS use no certificate method" — 서버/클라이언트 인증서 검증을 하지 않는 단순 암호화. → AWS IoT Core(mutual TLS 강제) 직결 불가, HTTPS(API Gateway) 서버 인증서 검증도 신뢰 어려움.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 전송 토폴로지
|
||||||
|
|
||||||
|
```
|
||||||
|
ERV 메인보드 (RS-485, 0xAA STATUS/ACK 프레임, 115200 8N1)
|
||||||
|
│ A / B
|
||||||
|
┌────────┐
|
||||||
|
│ EW11 │ WiFi
|
||||||
|
└────────┘────────── [현장 공유기] ──── 인터넷 ────┐
|
||||||
|
(TCP Client / MQTT Client, AES 암호화) │
|
||||||
|
▼
|
||||||
|
┌──────────────┐
|
||||||
|
│ AWS 서버 │
|
||||||
|
│ 0xAA 파서 + │
|
||||||
|
│ 저장 + 웹표시 │
|
||||||
|
└──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- PC 대시보드는 (a) 기존처럼 로컬 시리얼로 직결하거나, (b) 동일 0xAA 프레임을 AWS 서버에서 받아 보는 형태로 확장 가능.
|
||||||
|
- 양방향(제어 CTRL_*)도 같은 TCP 소켓으로 가능(서버→EW11→ERV).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. AWS 서버 옵션 비교
|
||||||
|
|
||||||
|
| 옵션 | 구성 | 우리 프로토콜 적합성 | 비용(개략) | 평가 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **A. EC2/Lightsail + 경량 TCP 서버** | EW11=TCP Client → 공인IP:Port. 서버가 0xAA 프레임 파싱·저장·웹표시 | ★★★ 바이너리 그대로 투명 전송, PC 파서 재사용 | t4g.nano ~$2~3/월, Lightsail $3.5~5/월 | **권장 1** |
|
||||||
|
| **B. EC2 + Mosquitto(MQTT)** | EW11=MQTT Client(user/pass+TLS) → up/down 토픽 | ★★ 표준 IoT, 장치 다수 확장 유리 | EC2 비용 + 운영 | 권장 2 |
|
||||||
|
| C. AWS IoT Core 직결 | EW11=MQTT Client | ✗ 클라이언트 인증서 필요(EW11 미지원) | 종량제 | **비권장**(브리지 필요) |
|
||||||
|
| D. HTTP→API Gateway+Lambda+DynamoDB | EW11=HTTP POST | △ HTTPS 인증서 이슈 + 바이너리 본문 번거로움 + 주기 폴링형 펌웨어 필요 | 서버리스 종량제 | 부적합 |
|
||||||
|
|
||||||
|
### 권장안 상세 (옵션 A)
|
||||||
|
1. **AWS Lightsail**(가장 운영이 간단) 또는 **EC2 t4g.nano** 1대 + 고정 공인 IP.
|
||||||
|
2. 그 위에 경량 TCP 서버(Node.js/Python) 실행 → 포트 개방.
|
||||||
|
3. EW11을 **TCP Client** 로 설정(서버 공인 IP/포트, AES 암호화, Keepalive/Register 활성).
|
||||||
|
4. 서버는 `PC_ERV_Protocol.md` 와 동일한 파서(STX 0xAA → CMD/LEN/PAYLOAD → CRC16-MODBUS 검증)로 STATUS(0x81) 73B 해석 → DB/파일 적재 → 간단 웹대시보드 또는 API 제공.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. EW11 설정 포인트
|
||||||
|
|
||||||
|
| 항목 | 권장값 |
|
||||||
|
|---|---|
|
||||||
|
| Serial Baudrate | **115200** (신규 PC 프로토콜 기준. 기존 HOMENET 9600과 다름 주의) |
|
||||||
|
| Data/Parity/Stop | 8 / None / 1 |
|
||||||
|
| Protocol | **TCP Client** (서버 공인 IP/포트) — 또는 MQTT |
|
||||||
|
| Buffer Size | ≥ 최대 프레임(STATUS 78B 이상 여유) |
|
||||||
|
| UART Auto-Frame | Frame Time(gap) 또는 Frame Length로 프레임 경계 정렬 (단, 프로토콜에 LEN 필드 있어 서버 재조립도 가능) |
|
||||||
|
| Security | AES(16자 키) 활성 + 서버 동일 키 복호화 |
|
||||||
|
| Keepalive / Reconnect / Heartbeat | 활성 |
|
||||||
|
| NTP | 활성(타임스탬프 정확도) |
|
||||||
|
|
||||||
|
설정 도구: IOTService (`IOTService3.1.0_20220926/`) 또는 내장 웹페이지.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 보안 / 위험 평가
|
||||||
|
|
||||||
|
| 항목 | 평가 / 대책 |
|
||||||
|
|---|---|
|
||||||
|
| EW11 TLS 신뢰성 | "no certificate" → MITM 방어 약함. **AES 암호화 병행** 필수 |
|
||||||
|
| 공인 포트 노출 | EC2 보안그룹 inbound를 **현장 공인 IP로 제한**, 불필요 포트 폐쇄 |
|
||||||
|
| 권장 폐쇄망 | 가능하면 VPN / 4G 라우터 / 사설망 경유 |
|
||||||
|
| WiFi 끊김 | EW11 자동 재접속 + 서버측 연결 상태 모니터링 |
|
||||||
|
| 응답 지연 | WiFi+인터넷 RTT 수십~수백 ms. 모니터링/시나리오 제어엔 충분, 실시간 폐루프 제어엔 부적합 |
|
||||||
|
| 트래픽 | STATUS 78B를 1초 주기 전송 시 ≈ 200MB/월 → 비용 영향 미미 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 권장 진행 순서
|
||||||
|
|
||||||
|
1. AWS **Lightsail nano**(또는 EC2 t4g.nano) 1대 생성 + 고정 IP.
|
||||||
|
2. 경량 TCP 서버 작성(0xAA 파서는 `DashboardProtocol` 로직 이식) → 수신 로그 확인.
|
||||||
|
3. EW11 1대 확보 → IOTService로 TCP Client + AES + 115200 설정.
|
||||||
|
4. **루프백 시험**: PC(0xAA 프레임 송신) → EW11 → 서버 수신 파싱 검증.
|
||||||
|
5. **실연동**: ERV → EW11 → 서버 STATUS 적재 확인.
|
||||||
|
6. 보안그룹/암호화/재접속 안정성 시험 후 운영.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 비용 메모 (2026-06 기준, 변동 가능)
|
||||||
|
|
||||||
|
- **EC2 t4g.nano**: us-east-1 약 $2~3/월 (ARM Graviton, 0.5GB RAM). t4g.small은 2026-12-31까지 750h/월 무료 체험.
|
||||||
|
- **신규 AWS 계정**: 12개월 t3.micro 750h/월 무료.
|
||||||
|
- **Lightsail**: 최저 $3.5/월(IPv6, 512MB/2vCPU/20GB/1TB), 공인 IPv4 포함 $5/월. 고정요금이라 가장 단순.
|
||||||
|
- AWS IoT Core: 종량제(연결/메시지/규칙별 과금). 본 케이스에선 비권장.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 참고
|
||||||
|
|
||||||
|
- EW11 소프트웨어 기능: `EW11A manual/IOT_Device_Series_Software_Funtion_20250328.pdf` (1.4 Working Mode, 1.5 암호화, 1.17 Register, 1.18 Heartbeat)
|
||||||
|
- AWS IoT Core 클라이언트 인증: https://docs.aws.amazon.com/iot/latest/developerguide/client-authentication.html
|
||||||
|
- 전송 프레임 규격: `TestProgram/PC_ERV_Protocol.md`
|
||||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
|||||||
|
IOTService工具下载地址:
|
||||||
|
IOTService tools download address:
|
||||||
|
http://ftp.hi-flying.com:9000/IOTService/
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,262 @@
|
|||||||
|
# PCDashBoard ↔ ERV 통신 — API 함수 & 프로토콜 상세 (2026.06.07)
|
||||||
|
|
||||||
|
PC 대시보드(ErvDashboard) ↔ ERV 메인보드 통신을 **485 프로토콜을 그대로 사용하되 `IErvApi` 함수로 감싼**
|
||||||
|
구조(인프로세스 API 파사드)로 정리한 문서. 선로 통신은 변함없이 485 프레임이며, UI 코드는 바이트/프레임을
|
||||||
|
다루지 않고 의미 있는 API 함수만 호출한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 구조 개요
|
||||||
|
|
||||||
|
```
|
||||||
|
[MainWindow.xaml.cs (UI)]
|
||||||
|
│ _api.SetPower(true) / SetFan(2) / RequestStatus() ...
|
||||||
|
▼
|
||||||
|
[IErvApi] ←─ 인터페이스 (TestProgram/PCDashBoard/Api/IErvApi.cs)
|
||||||
|
▼
|
||||||
|
[SerialErvApi] ←─ 구현 (Api/SerialErvApi.cs)
|
||||||
|
│ CtrlFrame 으로 485 프레임 빌드 / FrameParser 로 STATUS 파싱
|
||||||
|
▼
|
||||||
|
[SerialChannel] ── RS485 / USB-Serial ── [ERV 메인보드]
|
||||||
|
```
|
||||||
|
|
||||||
|
- 송신 : `IErvApi.SetXxx()` → `CtrlFrame.Xxx()` 가 `0xAA … CRC16` 프레임 생성 → 시리얼 전송
|
||||||
|
- 수신 : 시리얼 바이트 → `FrameParser` → `STATUS(0x81)` 디코드 → `StatusReceived(StatusRecord)` 이벤트
|
||||||
|
- UI 매핑 : `StatusRecord` → `DashboardState` 는 `StatusMapper.Apply()`(UI 측)가 담당 → API 는 UI 비종속
|
||||||
|
|
||||||
|
### 파일 구성
|
||||||
|
|
||||||
|
| 파일 | 역할 |
|
||||||
|
|---|---|
|
||||||
|
| `PCDashBoard/Api/IErvApi.cs` | 통신 API 인터페이스(함수/이벤트 정의) |
|
||||||
|
| `PCDashBoard/Api/SerialErvApi.cs` | RS485 구현(SerialChannel + FrameParser + CtrlFrame 내장) |
|
||||||
|
| `PCDashBoard/Api/StatusMapper.cs` | `StatusRecord → DashboardState` 매핑 |
|
||||||
|
| `PCDashBoard/Protocol/SerialChannel.cs` | 시리얼 포트 송수신(byte) |
|
||||||
|
| `ErvProtocol/CtrlFrame.cs` | PC→ERV 제어 프레임 빌더 |
|
||||||
|
| `ErvProtocol/StatusDecoder.cs` | ERV→PC STATUS 디코더 |
|
||||||
|
| `ErvProtocol/FrameParser.cs` | 0xAA/LEN/CRC 프레임 분리 |
|
||||||
|
| `ErvProtocol/Enums.cs` | RunMode/SubModeType/HystPreset/AirQuality/AutoState |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. API 함수 (IErvApi)
|
||||||
|
|
||||||
|
### 1.1 연결
|
||||||
|
|
||||||
|
| 함수 / 속성 | 설명 |
|
||||||
|
|---|---|
|
||||||
|
| `bool Connect(string port, int baud)` | 시리얼 연결 (대시보드 기본 115200) |
|
||||||
|
| `void Disconnect()` | 연결 해제 |
|
||||||
|
| `bool IsConnected { get; }` | 연결 여부 |
|
||||||
|
| `static string[] SerialErvApi.GetAvailablePorts()` | 사용 가능한 COM 포트 목록 |
|
||||||
|
| `event Action<bool> ConnectionChanged` | 연결 상태 변경 통지 |
|
||||||
|
|
||||||
|
### 1.2 수신
|
||||||
|
|
||||||
|
| 함수 / 이벤트 | 설명 |
|
||||||
|
|---|---|
|
||||||
|
| `event Action<StatusRecord> StatusReceived` | STATUS(0x81) 디코드 완료 시 발생 |
|
||||||
|
| `event Action<string> Log` | 프레임 hex / 이벤트 로그 |
|
||||||
|
| `void RequestStatus()` | STATUS 1회 즉시 요청 (CMD 0x0A) |
|
||||||
|
| `bool GetRoomStatus(int room, out bool damperSa, out bool damperEa, out AirQuality airQuality, out int led)` | 최근 STATUS 기준 각실(room 1~4) 급기·배기 댐퍼/공기질/LED 조회. 수신 이력 없거나 room 범위 밖이면 false |
|
||||||
|
|
||||||
|
### 1.3 제어 함수 ↔ 프로토콜 CMD 매핑
|
||||||
|
|
||||||
|
| API 함수 | CMD | PAYLOAD | 비고 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `SetPower(bool on)` | 0x01 | `[onoff]` | 전원 ON/OFF |
|
||||||
|
| `SetRunMode(RunMode mode)` | 0x02 | `[mode]` | 환기/자동/공청/바이패스 |
|
||||||
|
| `SetFan(int speed)` | 0x03 | `[speed]` | 0~4, 자동모드 시 무시 |
|
||||||
|
| `SetSubMode(SubModeType type, bool on)` | 0x04 | `[type][onoff]` | 스마트수면/쾌적조리/안심회복 |
|
||||||
|
| `SetHood(bool on)` | 0x05 | `[onoff]` | 연동후드 |
|
||||||
|
| `SetHystPreset(HystPreset preset)` | 0x06 | `[preset]` | ECO/NORMAL/TURBO |
|
||||||
|
| `SetHystDeadband(int preset, int pm25, int pm10, int voc, int co2)` | 0x07 | `[preset][pm25(2)][pm10(2)][voc(2)][co2(2)]` | 프리셋별 히스(하강) 값 |
|
||||||
|
| `SetDiffuserDamper(int room, int type, bool open)` | 0x08 | `[room][type][onoff]` | room 1~4, type 0=급기(SA)/1=배기(EA) |
|
||||||
|
| `SetDiffuserLed(int room, int dim)` | 0x09 | `[room][dim]` | dim 0~9 |
|
||||||
|
| `RequestStatus()` | 0x0A | (없음) | 상태 요청 |
|
||||||
|
| `SetReset(bool on)` | 0x0B | `[onoff]` | ERV 리셋 토글 |
|
||||||
|
| `SetVsp(int group, int index, int sa, int ea)` | 0x0C | `[group][index][sa(2)][ea(2)]` | 풍량 VSP 1엔트리 |
|
||||||
|
| `SetHystThreshold(int preset, int pollutant, int l1, int l2, int l3, int l4)` | 0x0D | `[preset][pollutant][L1(2)][L2(2)][L3(2)][L4(2)]` | 오염단계 임계 |
|
||||||
|
| `SetReserve(int hours)` | 0x0E | `[hours]` | (꺼짐)예약 0~8시간(0=해제) |
|
||||||
|
|
||||||
|
- `room` : 1=거실, 2=침실1, 3=침실2, 4=침실3
|
||||||
|
- `type`(댐퍼) : 0=급기(SA) / 1=배기(EA)
|
||||||
|
- `group` : 0=환기(Vent) / 1=바이패스(Bypass) / 2=공청(AirClean)
|
||||||
|
- `pollutant` : 0=CO2 / 1=PM2.5 / 2=PM10 / 3=VOC
|
||||||
|
- 멀티바이트(2) 값은 **빅엔디안**
|
||||||
|
|
||||||
|
### 1.4 데모
|
||||||
|
|
||||||
|
| 함수 | 설명 |
|
||||||
|
|---|---|
|
||||||
|
| `void InjectDemoStatus(int tick)` | 합성 STATUS 를 수신 경로로 주입(시리얼 없이 UI 테스트) |
|
||||||
|
|
||||||
|
### 1.5 enum 정의 (ErvProtocol/Enums.cs)
|
||||||
|
|
||||||
|
```
|
||||||
|
RunMode : Off=0, Vent=1, Auto=2, AirClean=3, Bypass=4
|
||||||
|
AutoState : Distribute=0(분산), Focus=1(집중)
|
||||||
|
HystPreset : Eco=0, Normal=1, Turbo=2
|
||||||
|
AirQuality : VeryBad=1, Bad=2, Normal=3, Good=4
|
||||||
|
SubModeType : SmartSleep=1, ComfortCook=2, ReliefRecover=3
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 사용법
|
||||||
|
|
||||||
|
### 2.1 초기화 / 이벤트 등록
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
readonly DashboardState _state = new();
|
||||||
|
readonly IErvApi _api = new SerialErvApi();
|
||||||
|
|
||||||
|
// 로그 / 연결상태 / STATUS 수신
|
||||||
|
_api.Log += Log;
|
||||||
|
_api.ConnectionChanged += b => Dispatcher.BeginInvoke(() => OnConnectionChanged(b));
|
||||||
|
_api.StatusReceived += rec => Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
StatusMapper.Apply(rec, _state); // StatusRecord → UI 모델
|
||||||
|
LogStatusSnapshot();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
> `SerialErvApi` 이벤트는 시리얼 수신(백그라운드) 스레드에서 발생하므로, UI 갱신은
|
||||||
|
> `Dispatcher.BeginInvoke` 로 마샬링한다.
|
||||||
|
|
||||||
|
### 2.2 연결 / 상태 요청
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var ports = SerialErvApi.GetAvailablePorts(); // COM 목록
|
||||||
|
_api.Connect("COM3", 115200); // 연결
|
||||||
|
_api.RequestStatus(); // 최초 STATUS 요청
|
||||||
|
...
|
||||||
|
_api.Disconnect();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 제어 송신 (예)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
_api.SetPower(true); // 전원 ON
|
||||||
|
_api.SetRunMode(RunMode.Auto); // 자동
|
||||||
|
_api.SetFan(2); // 풍량 2단
|
||||||
|
_api.SetSubMode(SubModeType.ComfortCook, true); // 쾌적조리 ON
|
||||||
|
_api.SetHood(true); // 연동후드 ON
|
||||||
|
_api.SetReserve(3); // 3시간 후 꺼짐 예약
|
||||||
|
_api.SetDiffuserDamper(1, 0, true); // 거실 급기(SA) 댐퍼 열림
|
||||||
|
_api.SetDiffuserDamper(1, 1, false); // 거실 배기(EA) 댐퍼 닫힘
|
||||||
|
_api.SetDiffuserLed(2, 5); // 침실1 LED 디밍 5
|
||||||
|
|
||||||
|
// 풍량 VSP (환기2단 SA/EA)
|
||||||
|
_api.SetVsp(group:0, index:2, sa:63, ea:61);
|
||||||
|
|
||||||
|
// 히스테리시스
|
||||||
|
_api.SetHystPreset(HystPreset.Normal);
|
||||||
|
_api.SetHystDeadband(preset:1, pm25:2, pm10:5, voc:5, co2:50);
|
||||||
|
_api.SetHystThreshold(preset:1, pollutant:0 /*CO2*/, l1:700, l2:1000, l3:1300, l4:1600);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 수신 처리
|
||||||
|
|
||||||
|
`StatusReceived` → `StatusMapper.Apply(rec, state)` 로 `DashboardState`(전원/모드/풍량/각실 센서·
|
||||||
|
댐퍼·LED·부하점수·온습도/VSP/히스테리시스/임계표) 전체가 갱신된다.
|
||||||
|
|
||||||
|
각실 일부 항목만 즉시 조회할 때는 `GetRoomStatus` 사용(최근 수신 STATUS 기준) :
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
if (_api.GetRoomStatus(1, out bool sa, out bool ea, out AirQuality aq, out int led))
|
||||||
|
Console.WriteLine($"거실: 급기={(sa ? "열림" : "닫힘")} 배기={(ea ? "열림" : "닫힘")} 공기질={aq} LED={led}");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 프로토콜 상세
|
||||||
|
|
||||||
|
### 3.1 물리계층
|
||||||
|
|
||||||
|
- RS-485 (또는 USB-Serial), **115200 bps, 8 Data, None Parity, 1 Stop (N81)**
|
||||||
|
|
||||||
|
### 3.2 공통 프레임 (Rev2.0 — 244byte 고정)
|
||||||
|
|
||||||
|
```
|
||||||
|
+------+------+----------------+--------+--------+
|
||||||
|
| STX | CMD | DATA[240] | CRC_L | CRC_H |
|
||||||
|
+------+------+----------------+--------+--------+
|
||||||
|
0xAA 1B 240B 고정 16-bit CRC(LE)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 필드 | 크기 | 설명 |
|
||||||
|
|---|---|---|
|
||||||
|
| STX | 1 | 고정 `0xAA` |
|
||||||
|
| CMD | 1 | 명령/응답 코드 |
|
||||||
|
| DATA | 240 | **고정 240byte.** 제어는 앞쪽 인자 + 나머지 `0` 패딩, ERV→PC 는 STATUS/ACK 데이터 |
|
||||||
|
| CRC | 2 | **CRC-16/MODBUS**(poly 0xA001, init 0xFFFF), **CMD~DATA(241byte)** 범위, **리틀엔디안** |
|
||||||
|
|
||||||
|
- **LEN 필드 폐기.** 모든 프레임은 항상 `1+1+240+2 = 244byte` 고정. 파서는 정해진 바이트 수만 읽고 CRC 검증.
|
||||||
|
- 멀티바이트 수치는 **빅엔디안**(CRC만 리틀엔디안)
|
||||||
|
- 프레임 동기 : STX(0xAA) 탐색 + 고정길이 수신 + CRC 검증. 60ms 이상 바이트 공백 시 파서 리셋.
|
||||||
|
|
||||||
|
### 3.3 명령 코드
|
||||||
|
|
||||||
|
**PC → ERV (제어)** : 1.3 표 참조 (0x01~0x0E)
|
||||||
|
|
||||||
|
**ERV → PC (상태/응답)**
|
||||||
|
|
||||||
|
| CMD | 이름 | PAYLOAD | 설명 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0x81 | STATUS | DATA 240 byte (3.5) | 전체 상태 스냅샷 (주기 송신 + REQ_STATUS 응답) |
|
||||||
|
| 0x82 | ACK | DATA `[echoCmd][result]` + 0패딩 | 제어 수신 응답 result 0=OK / 1=ERR |
|
||||||
|
|
||||||
|
### 3.4 값 정의
|
||||||
|
|
||||||
|
- 운전모드 : 0 OFF / 1 환기 / 2 자동 / 3 공청 / 4 바이패스
|
||||||
|
- 부가모드 type : 1 스마트수면 / 2 쾌적조리 / 3 안심회복 (STATUS subMode 는 비트맵 bit0/1/2)
|
||||||
|
- 공기질 : 1 매우나쁨(빨강) / 2 나쁨(주황) / 3 보통(초록) / 4 좋음(파랑)
|
||||||
|
- 자동상태 : 0 분산 / 1 집중
|
||||||
|
- 히스 프리셋 : 0 ECO / 1 NORMAL / 2 TURBO
|
||||||
|
|
||||||
|
### 3.5 STATUS(0x81) PAYLOAD 레이아웃 — **총 240 byte**
|
||||||
|
|
||||||
|
| 블록 | offset | 크기 | 내용 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 글로벌 | 0~16 | 17 | power, runMode, autoState, fanMode, subMode, hood, hystPreset, hystPM25(7,2), hystPM10(9,2), hystVOC(11,2), hystCO2(13,2), errorCode(15,2) |
|
||||||
|
| 각실 ×4 | 17~72 | 14×4=56 | 거실→침실1→2→3, 아래 표 |
|
||||||
|
| 리셋 | 73 | 1 | reset 토글 0/1 |
|
||||||
|
| VSP | 74~109 | 4×9=36 | 환기1~4, 바이패스, 공청1~4 의 SA(2)+EA(2) |
|
||||||
|
| 히스 프리셋표 | 110~133 | 8×3=24 | ECO/NORMAL/TURBO 의 PM2.5(2)/PM10(2)/VOC(2)/CO2(2) |
|
||||||
|
| 오염단계 임계표 | 134~229 | 32×3=96 | 프리셋×[CO2,PM2.5,PM10,VOC] 각 L1~L4 u16 |
|
||||||
|
| 각실 온습도 ×4 | 230~237 | 2×4=8 | 실별 Temp(1)+Humi(1) |
|
||||||
|
| (꺼짐)예약 | 238~239 | 2 | 잔여 초 u16 (0=예약없음) |
|
||||||
|
|
||||||
|
**각실 블록(14 byte) 상세** (상대 offset)
|
||||||
|
|
||||||
|
| +off | 크기 | 필드 |
|
||||||
|
|---|---|---|
|
||||||
|
| +0 | 1 | damper 비트맵 (bit0 급기 열림 / bit1 배기 열림) |
|
||||||
|
| +1 | 2 | pm25 |
|
||||||
|
| +3 | 2 | pm10 |
|
||||||
|
| +5 | 2 | voc |
|
||||||
|
| +7 | 2 | co2 |
|
||||||
|
| +9 | 1 | airQuality (3.4) |
|
||||||
|
| +10 | 1 | ledDim (0~9) |
|
||||||
|
| +11 | 2 | loadScore (부하점수) |
|
||||||
|
| +13 | 1 | finalVolume (최종풍량) |
|
||||||
|
|
||||||
|
**오염단계 임계표(프리셋당 32 byte)** : `CO2[L1..L4]` `PM2.5[L1..L4]` `PM10[L1..L4]` `VOC[L1..L4]` 순, 각 u16(빅엔디안).
|
||||||
|
|
||||||
|
> 계산식 : 17 + 56 + 1 + 36 + 24 + 96 + 8 + 2 = **240**
|
||||||
|
|
||||||
|
### 3.6 동작 시나리오
|
||||||
|
|
||||||
|
1. 연결 후 `RequestStatus()(0x0A)` 송신 → ERV 가 `STATUS(0x81)` 응답.
|
||||||
|
2. ERV 는 약 500ms~1s 주기로 `STATUS` 자동 송신 → 대시보드 실시간 갱신.
|
||||||
|
3. PC 제어 시 해당 `CTRL_*` 송신 → ERV 가 `ACK(0x82)` + 다음 STATUS 에 반영.
|
||||||
|
4. PC 는 STATUS 수신 시마다 로그(시각 + 전체 상태) 적재.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 비고
|
||||||
|
|
||||||
|
- 본 문서의 API(IErvApi) 도입은 **전송 방식(485 프로토콜)을 바꾸지 않는다.** 호출부를 함수로 캡슐화한 것.
|
||||||
|
- `IErvApi` 는 인터페이스이므로, 향후 다른 전송(예: HTTP) 구현(`HttpErvApi`)을 추가해 설정으로 교체 가능.
|
||||||
|
- 펌웨어(UART1/HOMENET_485) 세부 필드는 상호 합의하여 조정한다. (원본 초안: `TestProgram/PC_ERV_Protocol.md`)
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
# HuevenEco DL 각실제어 대시보드 ↔ ERV 메인보드 통신 프로토콜 (Rev2.0 고정패킷)
|
||||||
|
|
||||||
|
- 물리계층 : RS-485 (또는 USB-Serial), **115200 bps, 8 Data, None Parity, 1 Stop (N81)**
|
||||||
|
- 역할 : PC 대시보드(Host) ↔ ERV 메인보드(Main Board)
|
||||||
|
- **Rev2.0 변경점** : 가변 길이(`LEN` 필드) 프레임을 폐기하고 **모든 프레임을 244byte 고정**으로 통일.
|
||||||
|
파서는 "STX 탐색 → 정해진 바이트 수만 읽기 → CRC 검증"으로 단순화된다. CMD 의미·STATUS 레이아웃·CRC 방식은 종전과 동일.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 공통 프레임 (244byte 고정)
|
||||||
|
|
||||||
|
```
|
||||||
|
+------+------+------------------+--------+--------+
|
||||||
|
| STX | CMD | DATA[240] | CRC_L | CRC_H |
|
||||||
|
+------+------+------------------+--------+--------+
|
||||||
|
0xAA 1B 240B 고정 16-bit CRC
|
||||||
|
```
|
||||||
|
|
||||||
|
| 필드 | 크기 | 설명 |
|
||||||
|
|----------|------|-------------------------------------------------------------|
|
||||||
|
| STX | 1 | 고정 `0xAA` |
|
||||||
|
| CMD | 1 | 명령/응답 코드 (2장) |
|
||||||
|
| DATA | 240 | **고정 240byte.** PC→ERV 제어는 앞쪽에 인자 + 나머지 `0` 패딩, ERV→PC 는 STATUS/ACK 데이터 |
|
||||||
|
| CRC | 2 | **CRC-16/MODBUS** (poly 0xA001, init 0xFFFF), **CMD~DATA(241byte)**, **리틀엔디안** |
|
||||||
|
|
||||||
|
- **LEN 필드 없음.** 모든 프레임은 정확히 `1+1+240+2 = 244byte`.
|
||||||
|
- 멀티바이트 수치는 모두 **빅엔디안(상위 바이트 먼저)** 으로 표기한다. (CRC만 리틀엔디안)
|
||||||
|
- 프레임 동기 : STX(0xAA) 탐색 후 고정 길이로 수신, CRC 로 검증. 바이트 공백(예 60ms) 발생 시 파서를 STX 탐색으로 리셋해 재동기.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 명령 코드 (CMD)
|
||||||
|
|
||||||
|
DATA 앞쪽에 아래 인자를 싣고 나머지는 `0` 으로 패딩한다.
|
||||||
|
|
||||||
|
### 2.1 PC → ERV (제어)
|
||||||
|
|
||||||
|
| CMD | 이름 | DATA 앞쪽 인자 | 설명 |
|
||||||
|
|------|-----------------|-------------------------------------------|-----------------------------------|
|
||||||
|
| 0x01 | CTRL_POWER | `[onoff]` | 전원 0=OFF / 1=ON |
|
||||||
|
| 0x02 | CTRL_RUNMODE | `[mode]` | 운전모드 (3.1 참조) |
|
||||||
|
| 0x03 | CTRL_FAN | `[speed]` | 풍량 0~4 (자동모드에서는 무시) |
|
||||||
|
| 0x04 | CTRL_SUBMODE | `[type][onoff]` | 부가모드 토글 (3.2 참조) |
|
||||||
|
| 0x05 | CTRL_HOOD | `[onoff]` | 연동후드 0=OFF / 1=ON |
|
||||||
|
| 0x06 | CTRL_HYST_PRESET| `[preset]` | 히스테리시스 프리셋 0=ECO/1=NORMAL/2=TURBO |
|
||||||
|
| 0x07 | CTRL_HYST_VALUE | `[preset][pm25(2)][pm10(2)][voc(2)][co2(2)]` | 프리셋별 히스(하강) 데드밴드 설정 (값 BE) |
|
||||||
|
| 0x08 | CTRL_DAMPER | `[room][type][onoff]` | 각실 댐퍼 type 0=급기(SA)/1=배기(EA), 0=닫힘 / 1=열림 |
|
||||||
|
| 0x09 | CTRL_LED | `[room][dim]` | 각실 LED 디밍 0~9 |
|
||||||
|
| 0x0A | REQ_STATUS | (없음, 전부 0) | 상태 1회 즉시 요청 |
|
||||||
|
| 0x0B | CTRL_RESET | `[onoff]` | ERV 리셋 토글 0/1 |
|
||||||
|
| 0x0C | CTRL_VSP | `[group][index][sa(2)][ea(2)]` | 풍량 VSP 값 설정 (3.5 참조, sa/ea BE) |
|
||||||
|
| 0x0D | CTRL_HYST_THR | `[preset][pollutant][L1(2)][L2(2)][L3(2)][L4(2)]` | 오염단계 임계 설정 (pollutant 0=CO2/1=PM2.5/2=PM10/3=VOC, 값 BE) |
|
||||||
|
| 0x0E | CTRL_RESERVE | `[hours]` | (꺼짐)예약 0~8시간(0=해제) |
|
||||||
|
|
||||||
|
- `room` : 1=거실, 2=침실1, 3=침실2, 4=침실3
|
||||||
|
|
||||||
|
### 2.2 ERV → PC (상태/응답)
|
||||||
|
|
||||||
|
| CMD | 이름 | DATA | 설명 |
|
||||||
|
|------|-----------|--------------------|---------------------------------------------|
|
||||||
|
| 0x81 | STATUS | 240byte (4장 참조) | 전체 상태 스냅샷 (주기 송신 + REQ_STATUS 응답) |
|
||||||
|
| 0x82 | ACK | `[echoCmd][result]` + 0패딩 | 제어 명령 수신 응답 result 0=OK / 1=ERR |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 값 정의
|
||||||
|
|
||||||
|
### 3.1 운전모드 (RunMode)
|
||||||
|
|
||||||
|
| 코드 | 의미 | 펌웨어 매핑 |
|
||||||
|
|------|--------|----------------------------|
|
||||||
|
| 0x00 | OFF | Power_On = 0 |
|
||||||
|
| 0x01 | 환기 | MODE_VENTILATION (0) |
|
||||||
|
| 0x02 | 자동 | MODE_AUTO (1) |
|
||||||
|
| 0x03 | 공청 | MODE_AIRCLEAN (2) |
|
||||||
|
| 0x04 | 바이패스 | MODE_BYPASS (3) |
|
||||||
|
|
||||||
|
### 3.2 부가모드 (SubMode)
|
||||||
|
|
||||||
|
`CTRL_SUBMODE` 의 `type` :
|
||||||
|
|
||||||
|
| type | 의미 | 펌웨어 매핑 |
|
||||||
|
|------|------------|----------------------|
|
||||||
|
| 0x01 | 스마트수면 | Ext_Run_Mode = 4 |
|
||||||
|
| 0x02 | 쾌적조리 | Hood_YeunDong_Enable |
|
||||||
|
| 0x03 | 안심회복 | Ext_Run_Mode = 1 |
|
||||||
|
|
||||||
|
STATUS 의 `subMode` 는 비트맵 : bit0=스마트수면, bit1=쾌적조리, bit2=안심회복
|
||||||
|
|
||||||
|
### 3.3 공기질 상태 (AirQuality)
|
||||||
|
|
||||||
|
| 코드 | 등급 | 색상 |
|
||||||
|
|------|-----------|--------|
|
||||||
|
| 0x01 | 매우나쁨 | 빨강 |
|
||||||
|
| 0x02 | 나쁨 | 주황 |
|
||||||
|
| 0x03 | 보통 | 초록 |
|
||||||
|
| 0x04 | 좋음 | 파랑 |
|
||||||
|
|
||||||
|
### 3.4 자동운전 상태 (AutoState)
|
||||||
|
|
||||||
|
| 코드 | 의미 |
|
||||||
|
|------|--------|
|
||||||
|
| 0x00 | 분산 |
|
||||||
|
| 0x01 | 집중 |
|
||||||
|
|
||||||
|
### 3.5 풍량 VSP (CTRL_VSP 0x0C / STATUS VSP 블록)
|
||||||
|
|
||||||
|
급기(SA)/배기(EA) 풍량 설정값. `CTRL_VSP` 로 한 엔트리씩 설정, STATUS 로 전체 표시.
|
||||||
|
|
||||||
|
- `group` : `0`=환기(Vent), `1`=바이패스(Bypass), `2`=공청(AirClean)
|
||||||
|
- `index` : 환기/공청 `1~4`, 바이패스 `1`
|
||||||
|
- `sa`,`ea` : 각 **u16 빅엔디안**
|
||||||
|
|
||||||
|
**STATUS VSP 9 엔트리 순서** (각 SA,EA): `환기1, 환기2, 환기3, 환기4, 바이패스, 공청1, 공청2, 공청3, 공청4`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. STATUS(0x81) DATA 레이아웃 (240 byte)
|
||||||
|
|
||||||
|
| 블록 | offset | 크기 | 내용 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 글로벌 | 0~16 | 17 | power, runMode, autoState, fanMode, subMode, hood, hystPreset, hystPM25(7,2), hystPM10(9,2), hystVOC(11,2), hystCO2(13,2), errorCode(15,2) |
|
||||||
|
| 각실 ×4 | 17~72 | 14×4=56 | 거실→침실1→2→3, 4.2 표 |
|
||||||
|
| 리셋 | 73 | 1 | reset 토글 0/1 |
|
||||||
|
| VSP | 74~109 | 4×9=36 | 환기1~4, 바이패스, 공청1~4 의 SA(2)+EA(2) |
|
||||||
|
| 히스 프리셋표 | 110~133 | 8×3=24 | ECO/NORMAL/TURBO 의 PM2.5(2)/PM10(2)/VOC(2)/CO2(2) |
|
||||||
|
| 오염단계 임계표 | 134~229 | 32×3=96 | 프리셋×[CO2,PM2.5,PM10,VOC] 각 L1~L4 u16 |
|
||||||
|
| 각실 온습도 ×4 | 230~237 | 2×4=8 | 실별 Temp(1)+Humi(1) |
|
||||||
|
| (꺼짐)예약 | 238~239 | 2 | 잔여 초 u16 (0=예약없음) |
|
||||||
|
|
||||||
|
> 계산식 : 17 + 56 + 1 + 36 + 24 + 96 + 8 + 2 = **240 byte**
|
||||||
|
|
||||||
|
### 4.1 글로벌 (offset 0~16, 17 byte)
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|---------------|----------------------------------------|
|
||||||
|
| 0 | 1 | power | Power_On |
|
||||||
|
| 1 | 1 | runMode | 3.1 |
|
||||||
|
| 2 | 1 | autoState | 3.4 (분산/집중) |
|
||||||
|
| 3 | 1 | fanMode | 0~4 (Fan_Mode) |
|
||||||
|
| 4 | 1 | subMode | 비트맵 (3.2) |
|
||||||
|
| 5 | 1 | hood | bit0 Hood_YeunDong_Enable / bit1 Hood_Status |
|
||||||
|
| 6 | 1 | hystPreset | 0 ECO / 1 NORMAL / 2 TURBO |
|
||||||
|
| 7 | 2 | hystPM25 | 활성 프리셋 데드밴드 PM2.5 |
|
||||||
|
| 9 | 2 | hystPM10 | 활성 프리셋 데드밴드 PM10 |
|
||||||
|
| 11 | 2 | hystVOC | 활성 프리셋 데드밴드 VOC |
|
||||||
|
| 13 | 2 | hystCO2 | 활성 프리셋 데드밴드 CO2 |
|
||||||
|
| 15 | 2 | errorCode | 비트맵 (Err_Code) |
|
||||||
|
|
||||||
|
### 4.2 각실 블록 (offset 17~, 14 byte × 4실 = 56 byte)
|
||||||
|
|
||||||
|
순서: 거실 → 침실1 → 침실2 → 침실3
|
||||||
|
|
||||||
|
| off(상대) | 크기 | 필드 | 비고 |
|
||||||
|
|-----------|------|-------------|-----------------------------------|
|
||||||
|
| +0 | 1 | damper | 비트맵 bit0=급기(SA) 열림 / bit1=배기(EA) 열림 |
|
||||||
|
| +1 | 2 | pm25 | SEN66_pm2p5 |
|
||||||
|
| +3 | 2 | pm10 | SEN66_pm10p0 |
|
||||||
|
| +5 | 2 | voc | SEN66_VOC_value |
|
||||||
|
| +7 | 2 | co2 | SEN66_CO2_value |
|
||||||
|
| +9 | 1 | airQuality | 3.3 |
|
||||||
|
| +10 | 1 | ledDim | 0~9 (Light_Bright) |
|
||||||
|
| +11 | 2 | loadScore | 각실 부하점수 (Room_Level) |
|
||||||
|
| +13 | 1 | finalVolume | 최종 풍량 (Fan_Mode) |
|
||||||
|
|
||||||
|
### 4.3 VSP 블록 (offset 74~109, 9엔트리 × SA(2)·EA(2) = 36 byte)
|
||||||
|
|
||||||
|
순서(3.5): 환기1~4 → 바이패스 → 공청1~4. 각 엔트리 SA(2) + EA(2), 빅엔디안.
|
||||||
|
|
||||||
|
### 4.4 히스 데드밴드 프리셋표 (offset 110~133, 3프리셋 × PM2.5/PM10/VOC/CO2 u16 = 24 byte)
|
||||||
|
|
||||||
|
프리셋(ECO/NORMAL/TURBO)별 하강 데드밴드. 현재 적용값은 글로벌 `hystPM25~hystCO2`(off 7~14) = 본 표의 `[hystPreset]` 행.
|
||||||
|
|
||||||
|
### 4.5 오염단계 임계표 (offset 134~229, 3프리셋 × 32byte = 96 byte)
|
||||||
|
|
||||||
|
프리셋당 `CO2[L1..L4]` `PM2.5[L1..L4]` `PM10[L1..L4]` `VOC[L1..L4]` 순, 각 u16(빅엔디안).
|
||||||
|
|
||||||
|
### 4.6 각실 온습도 (offset 230~237, 4실 × Temp(1)+Humi(1) = 8 byte)
|
||||||
|
|
||||||
|
순서: 거실 → 침실1 → 침실2 → 침실3. 디퓨저 SEN66 값, 0~255 클램프.
|
||||||
|
|
||||||
|
### 4.7 (꺼짐)예약 잔여초 (offset 238~239, u16 BE)
|
||||||
|
|
||||||
|
전원 자동 OFF 까지 잔여 초. 0 = 예약 없음.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 동작 시나리오
|
||||||
|
|
||||||
|
1. PC 연결 후 `REQ_STATUS(0x0A)` 송신 → ERV 가 `STATUS(0x81)` 응답.
|
||||||
|
2. ERV 는 약 500ms~1s 주기로 `STATUS(0x81)` 를 자동 송신 (대시보드 실시간 갱신).
|
||||||
|
3. PC 가 토글/슬라이드/선택 시 해당 `CTRL_*` 송신 → ERV 가 `ACK(0x82)` + 다음 STATUS 에 반영.
|
||||||
|
4. PC 는 STATUS 수신 시마다 로그(날짜·시간 + 전체 상태)에 적재/저장.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 구현 매핑
|
||||||
|
|
||||||
|
| 계층 | 파일 | 역할 |
|
||||||
|
|---|---|---|
|
||||||
|
| 펌웨어 | `program/User/My_Homenet.c` | 244B 고정 프레임 수신/송신, CMD 적용, STATUS 빌드 |
|
||||||
|
| C# 공용 | `TestProgram/ErvProtocol/CtrlFrame.cs` | 244B 제어 프레임 빌더 |
|
||||||
|
| C# 공용 | `TestProgram/ErvProtocol/FrameParser.cs` | 고정 길이 프레임 분리 + CRC 검증 |
|
||||||
|
| C# 공용 | `TestProgram/ErvProtocol/StatusDecoder.cs` / `StatusEncoder.cs` | STATUS 240B 디코드/인코드 |
|
||||||
|
| C# 대시보드 | `TestProgram/PCDashBoard/Api/IErvApi.cs` / `SerialErvApi.cs` | API 파사드(함수 호출 → 프레임) |
|
||||||
|
|
||||||
|
> API 함수 목록·사용법은 `260607_PCDashBoard_API_및_프로토콜.md` 1·2장 참조. 본 문서가 와이어 프로토콜 정본이다.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,215 @@
|
|||||||
|
==========History==========
|
||||||
|
각실제어 프로토콜 | | | | | | | | | | | | | |
|
||||||
|
VERSION | | | DATE | | | | DESCRIPTER | | | | DESCRIPTION | | |
|
||||||
|
V 1.0 | | | 2024-11-20 00:00:00 | | | | 강은철 / 주신전자 | | | | 초 안 | | |
|
||||||
|
V 1.1 | | | 2024-12-18 00:00:00 | | | | 안준형 / 주신전자 | | | | 룸콘 응답 패킷 추가
|
||||||
|
Run Mode 수정
|
||||||
|
Fan Speed 수정
|
||||||
|
에러코드 패킷 내용 수정 | | |
|
||||||
|
1. 통신규격 | | | | | | | | | | | | | |
|
||||||
|
| 가. 통신방식 : RS-485 | | | | | | | | | | | | |
|
||||||
|
| 나. 통신속도 : 115200 bps | | | | | | | | | | | | |
|
||||||
|
| 다. 데이터 구성 : 1 Start Bit, 8 Data Bit, 1 Stop Bit, None Parity | | | | | | | | | | | | |
|
||||||
|
==========Main->Slave==========
|
||||||
|
| Protocol (UART - 115,200bps) | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 1. Normal Polling Packet | | | | | | 메인보드(Master) -> 분배기_디퓨져(Slave) | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | | | | | |
|
||||||
|
| 내용 | 0xAA | M/S 0x10 | ID 1 | ID 2 | VSP Mode | Power | Run Mode | Fan Speed | LED 밝기 | 공기질 상태 | 댐퍼 각도(SA) | 댐퍼 각도(RA) | SA RPM | | EA RPM | | SA Reset | RA Reset | 예약(Hour) | 예비(0x00) | 예비(0x00) | 예비(0x00) | | | | | | |
|
||||||
|
| | | | | | | | | | | | | | | | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | | | | | |
|
||||||
|
| | | | | | | | | | | | | | | | | 예비(0x00) | Version | | Error Code | | CRC | | | | | | | |
|
||||||
|
| < M/S > byte1 | | | | | | | | < Fan Speed > | | | | Control Cmd | 0x80 | | | < SA RPM > | | | | | | | | | | | | | |
|
||||||
|
| 0x10 : Master ( Main BxD ) | | | | | | | | 0x00 : OFF | | | | | | | 전열교환기 SA RPM | | | | | | | | | | | | | |
|
||||||
|
| 0x01 : Slave ( Display, Sensor Pack … ) | | | | | | | | 0x01 ~ 0x03 : 1~3 | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| | | | | | | | | | | | | | | | < EA RPM > | | | | | | | | | | | | | |
|
||||||
|
| < ID 1 > | | | | | | | | < LED > byte8 | | | | Control Cmd | 0x80 | | | 전열교환기 EA RPM | | | | | | | | | | | | | |
|
||||||
|
| 0x01 : SA Diffuser | | | | | | | | 디퓨저의 RGB LED 밝기 제어 | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x02 : RA Diffuser | | | | | | | | 0x00 : OFF | | | | | | | < SA Reset > | | | | | | | | | | | | | |
|
||||||
|
| 0x03 : Room Controller | | | | | | | | 0x01 ~ 0x0A : 1 ~ 10단 (10단: 최대) | | | | | | | SA댐퍼 초기화 데이터 | | | | | | | | | | | | | |
|
||||||
|
| < ID 2 > | | | | | | | | < 공기질 상태 > | | | | | | | < RA Reset > | | | | | | | | | | | | | |
|
||||||
|
| 0x01 ~ 0x06 : SA/RA/RoomCon ID | | | | | | | | 디퓨저의 RGB LED 색상 표시 | | | | | | | EA댐퍼 초기화 데이터 | | | | | | | | | | | | | |
|
||||||
|
| * RoomCon ID는 해당 SA Diffuser 같음. | | | | | | | | 0x01 : 매우나쁨 - 빨강 | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| | | | | | | | | 0x02 : 나쁨 - 주황 | | | | | | | < 예약 > | | | | | | | | | | | | | |
|
||||||
|
| < VSP Mode > : Diffuser는 해당 사항 없고, RoomCon ID 1 에 해당함 | | | | | | | | 0x03 : 보통 - 초록 | | | | | | | 시간(hour) 단위 | | | | | | | | | | | | | |
|
||||||
|
| 0x01, 0x02, 0x03, 0x10 : 아래내용 참고 | | | | | | | | 0x04 : 좋음 - 파랑 | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| | | | | | | | | * 공기질 판단 기준은 다음 Sheet 참조 | | | | | | | < 예비 > | | | | | | | | | | | | | |
|
||||||
|
| < Power > | | | | Control Cmd | 0x80 | | | | < 댐퍼각도 SA > | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x00 / 0x01 : Power On / Off | | | | | | | | 디퓨저 각도 제어(0~180도) | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| | | | | | | | | 0x00 ~ 0xB4 | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| < Run Mode > | | | | Control Cmd | 0x80 | | | | < 댐퍼각도 RA > | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x01 : 수동 | | | | | | | | 디퓨저 각도 제어(0~180도) | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x02 : 자동 | | | | | | | | 0x00 ~ 0xB4 | | | | | | | 예비 | | | | | | | | | | | | | |
|
||||||
|
| 0x04 : 바이패스 | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x08 : 공기청정 | | | | | | | | | | | | | | | < Version > | | | | | | | | | | | | | |
|
||||||
|
| | | | | | | | | | | | | | | | 예) Ver 1.23 | | | | | | | | | | | | | |
|
||||||
|
| | | | | | | | | | | | | | | | byte23 : 0x01 | | | | | | | | | | | | | |
|
||||||
|
| | | | | | | | | | | | | | | | byte24 : 0x17 | | | | | | | | | | | | | |
|
||||||
|
| < Error Code Echo> | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x0001 : 필터 청소 | | | | 0x0010 : 장비보호모드 | | | | 0x0100 : 통합센서 에러(거실) | | | | | 0x1000 : 통신에러(거실) | | | | | | | | | | | | | | | |
|
||||||
|
| 0x0002 : 필터 교체 | | | | 0x0020 : EA 팬 에러 | | | | 0x0200 : 통합센서 에러(방1) | | | | | 0x2000 : 통신에러(방1) | | | | | | | | | | | | | | | |
|
||||||
|
| 0x0004 : 소자 교체 | | | | 0x0040 : 간헐운전모드 | | | | 0x0400 : 통합센서 에러(방2) | | | | | 0x4000 : 통신에러(방2) | | | | | | | | | | | | | | | |
|
||||||
|
| 0x0008 : 온도센서 에러 | | | | 0x0080 : SA 팬 에러 | | | | 0x0800 : 통합센서 에러(방3) | | | | | 0x8000 : 통신에러(방3) | | | | | | | | | | | | | | | |
|
||||||
|
| 2. Nomal Mode ( Vsp Mode = 0 ) | | | | | | 분배기_디퓨져(Slave) -> 메인보드 ( Master ) | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | | | | | |
|
||||||
|
| 내용 | 0xAA | M/S 0x01 | ID 1 | ID 2 | VSP Mode | Power | Run Mode | Fan Speed | LED 밝기 | 공기질 상태 | 댐퍼 각도(SA) | 댐퍼 각도(RA) | PM10 | | PM4 | | PM2.5 | | PM1.0 | | Humidity | | | | | | | |
|
||||||
|
| | | | | | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | | | | | | |
|
||||||
|
| | | | | | | Temperature | | TVOC | | NOx | | CO2 | | 0x00 | 0x00 | 0x00 | Error Code | | Version | | CRC | | | | | | | |
|
||||||
|
| < M/S > byte1 | | | | | | | | < LED > byte8 | | | | Control Cmd | 0x80 | | | < 공기질센서 > byte12~29 | | | | | | | | | | | | | |
|
||||||
|
| 0x10 : Master ( Main BxD ) | | | | | | | | 디퓨저의 RGB LED 밝기 제어 | | | | | | | byte12~13 : PM10 | | | | | | | | | | | | | |
|
||||||
|
| 0x01 : Slave ( Display, Sensor Pack … ) | | | | | | | | 0x00 : OFF | | | | | | | byte14~15 : PM4 | | | | | | | | | | | | | |
|
||||||
|
| | | | | | | | | 0x01 ~ 0x0A : 1 ~ 10단 (10단: 최대) | | | | | | | byte16~17 : PM2.5 | | | | | | | | | | | | | |
|
||||||
|
| < ID 1 > | | | | | | | | | | | | | | | byte18~19 : PM1.0 | | | | | | | | | | | | | |
|
||||||
|
| 0x01 : SA Diffuser | | | | | | | | < 공기질 상태 > byte9 | | | | | | | byte20~21 : Humidity(습도) | | | | | | | | | | | | | |
|
||||||
|
| 0x02 : RA Diffuser | | | | | | | | 디퓨저의 RGB LED 색상 표시 | | | | | | | byte22~23 : Temperature(온도) | | | | | | | | | | | | | |
|
||||||
|
| 0x03 : Room Controller | | | | | | | | 0x01 : 매우나쁨 - 빨강 | | | | | | | byte24~25 : TVOC(총 휘발성 유기화합물) | | | | | | | | | | | | | |
|
||||||
|
| | | | | | | | | 0x02 : 나쁨 - 주황 | | | | | | | byte26~27 : NOx(질소산화물) | | | | | | | | | | | | | |
|
||||||
|
| < ID 2 > | | | | | | | | 0x03 : 보통 - 초록 | | | | | | | byte28~29 : CO2 | | | | | | | | | | | | | |
|
||||||
|
| 0x01 ~ 0x06 : SA/RA/RoomCon ID | | | | | | | | 0x04 : 좋음 - 파랑 | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| * RoomCon ID는 해당 SA Diffuser 같음. | | | | | | | | * 공기질 판단 기준은 다음 Sheet 참조 | | | | | | | < 예비 > byte30~32 | | | | | | | | | | | | | |
|
||||||
|
| | | | | | | | | | | | | | | | 예비 | | | | | | | | | | | | | |
|
||||||
|
| < VSP Mode > : Diffuser는 해당 사항 없고, RoomCon ID 1 에 해당함 | | | | | | | | < 댐퍼각도 SA > byte10 | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x01 ~ 0x06 : SA/EA/RoomCon ID | | | | | | | | 디퓨저 각도 제어(0~180도) | | | | | | | < Version > byte35~36 | | | | | | | | | | | | | |
|
||||||
|
| * RoomCon ID는 해당 SA Diffuser 같음. | | | | | | | | 0x00 ~ 0xB4 | | | | | | | 예) Ver 1.23 | | | | | | | | | | | | | |
|
||||||
|
| | | | | | | | | | | | | | | | byte23 : 0x01 | | | | | | | | | | | | | |
|
||||||
|
| < Power > | | | | Control Cmd | 0x80 | | | | < 댐퍼각도 RA > byte11 | | | | | | | byte24 : 0x17 | | | | | | | | | | | | | |
|
||||||
|
| 0x00 / 0x01 : Power On / Off | | | | | | | | 디퓨저 각도 제어(0~180도) | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| | | | | | | | | 0x00 ~ 0xB4 | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| < Run Mode > | | | | Control Cmd | 0x80 | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x01 : 수동 | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x02 : 자동 | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x04 : 바이패스 | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x08 : 공기청정 | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 2.1. 공기질 정보 Mode1 ( Vsp Mode = 0x11 ) 분배기_룸콘_거실_공기질 추가(Slave) <-> 메인보드 ( Master ) | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | | | | | |
|
||||||
|
| 내용 | 0xAA | M/S 0x01 | ID 1 | ID 2 | VSP Mode | 방 1
|
||||||
|
PM2.5 | | 방 1
|
||||||
|
Humidity | | 방 1
|
||||||
|
Temperature | | 방 1
|
||||||
|
CO2 | | 방 1
|
||||||
|
TVOC | | | | | | | | | | | | | | |
|
||||||
|
| | | | | | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | | | | | | |
|
||||||
|
| | | | | | | 방 2
|
||||||
|
PM2.5 | | 방 2
|
||||||
|
Humidity | | 방 2
|
||||||
|
Temperature | | 방 2
|
||||||
|
CO2 | | 방 2
|
||||||
|
TVOC | | | Error Code | | COM
|
||||||
|
SA 1~6 | COM
|
||||||
|
EA 1~6 | CRC | | | | | | | |
|
||||||
|
| 2.2. 공기질 정보 Mode1 ( Vsp Mode = 0x12 ) 분배기_룸콘_거실_공기질 추가(Slave) <-> 메인보드 ( Master ) | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | | | | | |
|
||||||
|
| 내용 | 0xAA | M/S 0x01 | ID 1 | ID 2 | VSP Mode | 방 3
|
||||||
|
PM2.5 | | 방 3
|
||||||
|
Humidity | | 방 3
|
||||||
|
Temperature | | 방 3
|
||||||
|
CO2 | | 방 3
|
||||||
|
TVOC | | | | | | | | | | | | | | |
|
||||||
|
| | | | | | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | | | | | | |
|
||||||
|
| | | | | | | 방 4
|
||||||
|
PM2.5 | | 방 4
|
||||||
|
Humidity | | 방 4
|
||||||
|
Temperature | | 방 4
|
||||||
|
CO2 | | 방 4
|
||||||
|
TVOC | | | Error Code | | COM
|
||||||
|
SA 1~6 | COM
|
||||||
|
EA 1~6 | CRC | | | | | | | |
|
||||||
|
| 2.3. Nomal Mode ( Vsp Mode = 0 ) | | | | | | 분배기_룸콘(Slave) <-> 메인보드 ( Master ) | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | | | | | |
|
||||||
|
| 내용 | 0xAA | M/S 0x01 | ID 1 | ID 2 | VSP Mode | Power | Run Mode | Fan Speed | 예약
|
||||||
|
시간 | 히터
|
||||||
|
/UV | 필터
|
||||||
|
리셋 | RPM
|
||||||
|
START | RPM
|
||||||
|
STOP | PM2.5 | | PM10 | | Humidity | | Temperature | | Co2 | | | | | | |
|
||||||
|
| | | | | | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | | | | | | |
|
||||||
|
| | | | | | | Co2 | Room
|
||||||
|
Power | 소비전류 | | CO2
|
||||||
|
Hysteresys | TVOC | | 필터시간 | | 소자시간 | | Error Code | | COM
|
||||||
|
SA 1~6 | COM
|
||||||
|
EA 1~6 | CRC | | | | | | | |
|
||||||
|
| < M/S > byte1 | | | | | | | | < Fan Speed > | | | | Control Cmd | 0x80 | | < RPM START > byte11 | | | | | < COM SA 1~6, EA 1~6 > | | | | | | | | | |
|
||||||
|
| 0x10 : Master ( Main BxD ) | | | | | | | | 0x00 : OFF | | | | | | RPM 측정 시작 Flag | | | | | 통신 상태 : On/OFF - 1/0 | | | | | | | | | |
|
||||||
|
| 0x01 : Slave ( Display, Sensor Pack … ) | | | | | | | | 0x01 ~ 0x03 : 1~3 | | | | | | 0x01 : Start | | | | | 0x01 : 거실1 | | | | | | | | | |
|
||||||
|
| | | | | | | | | | | | | | | Main 응답은 Echo 처리 | | | | | 0x02 : 거실2 | | | | | | | | | |
|
||||||
|
| < ID 1 > | | | | | | | | | | | | | | | | | | | 0x04 : 방 1 | | | | | | | | | |
|
||||||
|
| 0x01 : SA Diffuser | | | | | | | | | | | | | | < RPM START > byte12 | | | | | 0x08 : 방 2 | | | | | | | | | |
|
||||||
|
| 0x02 : EA Diffuser | | | | | | | | | | | | | | RPM 측정 중지 Flag | | | | | 0x10 : 방 3 | | | | | | | | | |
|
||||||
|
| 0x03 : Room Controller | | | | | | | | | | | | | | 0x01 : 메인에서 RPM 측정 완료 시 응답 | | | | | 0x20 : 방 4 | | | | | | | | | |
|
||||||
|
| | | | | | | | | | | | | | | 룸콘 응답은 Echo 처리 | | | | | | | | | | | | | | |
|
||||||
|
| < ID 2 > | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x01 ~ 0x06 : SA/EA/RoomCon ID | | | | | | | | | | | | | | < 예비 > byte30~32 | | | | | | | | | | | | | | |
|
||||||
|
| * RoomCon ID는 해당 SA Diffuser 같음. | | | | | | | | | | | | | | 예비 | | | | | | | | | | | | | | |
|
||||||
|
| < VSP Mode > : Diffuser는 해당 사항 없고, RoomCon ID 1 에 해당함 | | | | | | | | < 예약시간 > byte8 | | | | Control Cmd | 0x80 | | < Version > byte35~36 | | | | | | | | | | | | | | |
|
||||||
|
| 0x01 ~ 0x06 : SA/EA/RoomCon ID | | | | | | | | 룸콘에서 설정한 예약 시간(hour) | | | | | | 예) Ver 1.23 | | | | | | | | | | | | | | |
|
||||||
|
| * RoomCon ID는 해당 SA Diffuser 같음. | | | | | | | | 0x00 : OFF | | | | | | byte23 : 0x01 | | | | | | | | | | | | | | |
|
||||||
|
| | | | | | | | | 0x01 ~ 0x08 : 1 ~ 8시간 | | | | | | byte24 : 0x17 | | | | | | | | | | | | | | |
|
||||||
|
| < Power > | | | | Control Cmd | 0x80 | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x00 / 0x01 : Power On / Off | | | | | | | | < 히터 / UV > byte9 | | | | Control Cmd | 0x80 | | < Room Power > | | | | Control Cmd | 0x80 | | 0x01 | 방1 | | | | | | | |
|
||||||
|
| | | | | | | | | 히터 및 UV ON/OFF Flag | | | | | | ID2 가 1의 경우만 사용 | | | | | | 0x02 | 방2 | | | | | | | |
|
||||||
|
| | | | | | | | | bit1(0x01) : 1-ON / 0-OFF 히터 | | | | | | 각방의 POWER ON/OFF 제어 | | | | | | 0x04 | 방3 | | | | | | | |
|
||||||
|
| < Run Mode > | | | | Control Cmd | 0x80 | | | | bit5(0x10) : 1-ON / 0-OFF UV | | | | | | | | | | | | 0x08 | 방4 | | | | | | | |
|
||||||
|
| 0x01 : 수동 | | | | | | | | | | | | | | < 소비전류 > | | | | | | | | | | | | | | |
|
||||||
|
| 0x02 : 자동 | | | | | | | | < 필터리셋 > byte10 | | | | | | MAIN -> 룸컨 | | | | | | | | | | | | | | |
|
||||||
|
| 0x04 : 바이패스 | | | | | | | | 필터리셋 시 전달 Flag | | | | | | Wh 단위로 전송 ( uint16_t ) | | | | | | | | | | | | | | |
|
||||||
|
| 0x08 : 공기청정 | | | | | | | | 0x01 : 필터리셋 | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| | | | | | | | | 0x00 : OFF | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 3. VSP 상태 전송모드 ( Vsp Mode = 0x01 ~ 0x03 ) : 메인보드(Master) -> 분배기_룸컨(Slave) ( 룸컨의 요청으로 메인보드에서 polling 주기에서 함) | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| ( 룸컨의 요청은 2.1 packet 으로함) | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | | | | | |
|
||||||
|
| 내용 | 0xAA | M/S 0x10 | ID 1
|
||||||
|
= 0x03 | ID 2
|
||||||
|
= 0x01 | 0x01 | 환기 SA 1 | 환기 EA 1 | 환기 SA 2 | 환기 EA 2 | 환기 SA 3 | 환기 EA 3 | 환기 SA 4 | 환기 EA 4 | 환기 SA 5 | 환기 EA 5 | 공청 SA 1 | 공청 EA 1 | 공청 SA 2 | 공청 EA 2 | 공청 SA 3 | 공청 EA 3 | 공청 SA 4 | | | | | | |
|
||||||
|
| | | | | | | | | | | | | | | | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | | | | | |
|
||||||
|
| | | | | | | | | | | | | | | | | 공청 EA 4 | 공청 SA 5 | 공청 EA 5 | | | CRC | | | | | | | |
|
||||||
|
| byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | | | | | |
|
||||||
|
| 내용 | 0xAA | M/S 0x10 | ID 1
|
||||||
|
= 0x03 | ID 2
|
||||||
|
= 0x01 | 0x02 | 바이 SA 1 | 바이 EA 1 | 바이 SA 2 | 바이 EA 2 | 바이 SA 3 | 바이 EA 3 | 바이 SA 4 | 바이 EA 4 | 바이 SA 5 | 바이 EA 5 | | | RoomCon
|
||||||
|
개수 | Diffuser
|
||||||
|
개수 | | | | | | | | | |
|
||||||
|
| | | | | | | | | | | | | | | | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | | | | | |
|
||||||
|
| | | | | | | | | | | | | | | | | | | | | | CRC | | | | | | | |
|
||||||
|
| byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | | | | | |
|
||||||
|
| 내용 | 0xAA | M/S 0x10 | ID 1
|
||||||
|
= 0x03 | ID 2
|
||||||
|
= 0x01 | 0x03 | 환기 RPM REF 중 | | 환기 RPM DELTA 중 | | 환기 RPM REF 강 | | 환기 RPM DELTA 강 | | 공청 RPM REF 중 | | 공청 RPM DELTA 중 | | 공청 RPM REF 강 | | 공청 RPM DELTA 강 | | Modbus
|
||||||
|
ID | | | | | | |
|
||||||
|
| | | | | | | | | | | | | | | | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | | | | | |
|
||||||
|
| | | | | | | | | | | | | | | | | | | | | | CRC | | | | | | | |
|
||||||
|
| 4. VSP 세팅 모드 ( Vsp Mode = 0x10 ) : 분배기_룸컨(Slave) -> 메인보드(Master) ( VSP 세팅 모드이면 메인보드 polling(2.1 packet)에 대한 응답 ) | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | | | | | |
|
||||||
|
| 내용 | 0xAA | M/S 0x01 | ID 1
|
||||||
|
= 0x03 | ID 2
|
||||||
|
= 0x01 | 0x10 | Vsp
|
||||||
|
Select | SA1
|
||||||
|
Vsp | EA1
|
||||||
|
Vsp | SA2
|
||||||
|
Vsp | EA2
|
||||||
|
Vsp | SA3
|
||||||
|
Vsp | EA3
|
||||||
|
Vsp | SA4
|
||||||
|
Vsp | EA4
|
||||||
|
Vsp | SA5
|
||||||
|
Vsp | EA5
|
||||||
|
Vsp | | FAN
|
||||||
|
Mode | 풍량 중
|
||||||
|
RPM REF | | 풍량 중
|
||||||
|
RPM DELTA | | | | | | | |
|
||||||
|
| | | | | | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | | | | | | |
|
||||||
|
| | | | | | | 풍량 강
|
||||||
|
RPM REF | | 풍량 강
|
||||||
|
RPM DELTA | | | | | | RoomCon
|
||||||
|
개수 설정 | Diffuser
|
||||||
|
개수 설정 | | | Modbus
|
||||||
|
ID | WR | | CRC | | | | | | | |
|
||||||
|
| < VSP Select > | | | | < SA1 Vsp ~ EA5 Vsp > | | | | | | | < Modbus ID > | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x00 : None | | | | VSP Select(해당모드) 에 의한 SA, EA 설정값을 표현 | | | | | | | 메인보드에서 외부홈넷과 연결될 경우 필요에 따라 ID를 부여 | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x01 : 환기모드 | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x02 : 공청모드 | | | | < 풍량중 RPM REF ~ 풍량강 RPM DELTA > | | | | | | | < WR > : 설정한 값들을 저장하기 위한 Command | | | | | | | | | | | | | | | | | |
|
||||||
|
| 0x03 : 바이패스모드 | | | | VSP Select(해당모드) 에 의한 RPM REF 및 DELTA 설정 | | | | | | | 0x0000 : 저장 없음 | | | | | | | | | | | | | | | | | |
|
||||||
|
| | | | | 단, 환기모드와 공청모드만 해당함 | | | | | | | 0x0001 : 메인보드에 설정값 저장 | | | | | | | | | | | | | | | | | |
|
||||||
|
| | | | | < RoomCon 개수, Diffuser 개수 > | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| | | | | 메인보드가 Polling 해야할 RoomCon 및 Diffuser 개수 | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| | | | | RoomCon Min 1 ( 거실 1개 ) ~ Max 5 | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
| | | | | Diffuser Min 2 (거실 지정된 2개) ~ Max 6 | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||||
|
==========Reference==========
|
||||||
|
| | * 공기질 판단 기준은 힘펠과 협의 예정
|
||||||
@@ -0,0 +1,489 @@
|
|||||||
|
# 각실제어 내부 통신 프로토콜 (Rev 2.0, CMD 기반)
|
||||||
|
|
||||||
|
> 휴벤ECO(ERV) ↔ 각실분배기 ↔ 디퓨저 ↔ 룸컨 **내부 RS-485 통신** 규격
|
||||||
|
>
|
||||||
|
> 본 문서는 `Protocol/수정_Each_Room_Jushin_protocol_RS485_Rev1.2` (주신전자) 와
|
||||||
|
> 펌웨어 `program/User/My_Uart.c` (각실분배기/디퓨저 폴링) 구현을 기반으로,
|
||||||
|
> **CMD 기반으로 단순화·확장**하여 새로 정의한 규격이다.
|
||||||
|
> (DL 룸컨 232 프로토콜 `My_RJ2.c` 는 DL 사양으로 본 규격과 무관 — 변경하지 않는다.)
|
||||||
|
|
||||||
|
- **물리계층** : RS-485, **115200 bps, 8 Data, None Parity, 1 Stop (N81)**
|
||||||
|
- **토폴로지** : 1 Master(메인보드) ↔ N Slave(디퓨저/룸컨), 메인보드 폴링 방식
|
||||||
|
- **대상 시스템** : 사양서 5페이지(힘펠 배선도) 구성 = **거실 + 방1~4 (5실)**, 각 실 룸컨
|
||||||
|
- 디퓨저 : 거실 SA 2·RA 2, 방1~4 각 SA 1·RA 1 → **SA 디퓨저 6 + RA 디퓨저 6**, 룸컨 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 무엇이 바뀌었나 (Rev 1.2 → Rev 2.0)
|
||||||
|
|
||||||
|
| 구분 | 기존 (Rev 1.2) | 개선 (Rev 2.0) |
|
||||||
|
|------|----------------|----------------|
|
||||||
|
| 패킷 식별 | `VSP Mode` 값(0x00/0x11/0x12/0x01~03/0x10)에 따라 **같은 byte 위치의 의미가 계속 바뀜** | **CMD 1바이트로 패킷 종류 고정** → 한 패킷은 항상 같은 레이아웃 |
|
||||||
|
| 거실 공기질 | 별도 모드 0x11 / 0x12 로 방을 쪼개서 전송 | **방별 센서 패킷 1종으로 통합**, ROOM 필드로 구분 |
|
||||||
|
| 길이 | 29/39 byte 가변, 위치로 추정 | **LEN 필드 명시** → 파싱 단순 |
|
||||||
|
| 장치 주소 | ID1(종류)+ID2(번호) 혼용, 거실=5 등 불규칙 | **DEV / ROOM / IDX 3필드**로 직관적 주소화 |
|
||||||
|
| 에러코드 | **16비트 전부 소진** → 방4 추가 불가 (한계) | **32비트로 확장** → 방4 + 향후 여유 |
|
||||||
|
| 확장성 | 4실 고정 | **5실(거실+방1~4) + 룸컨 5 / SA·RA 디퓨저 각 6** 명시 |
|
||||||
|
| 역할 정의 | 명령/상태 방향이 패킷마다 섞여 모호 | **버스 마스터=ERV(폴링) / 명령 주체=룸컨 / 실행=ERV→디퓨저** 로 명확화 |
|
||||||
|
|
||||||
|
> **제어 권한 정리** : 사용자는 룸컨에서 전원·모드·풍량·예약·필터리셋과 VSP 풍량 테이블을 설정한다.
|
||||||
|
> 룸컨은 버스상 Slave이므로 이 명령들을 ERV 폴링에 대한 응답(`CMD_ROOMCON`/`CMD_VSP_SET`)으로 올린다.
|
||||||
|
> ERV는 명령을 받아 분배기를 통해 디퓨저 댐퍼·풍량·LED를 제어하고, 센서값을 읽어 룸컨에 전달한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 공통 프레임
|
||||||
|
|
||||||
|
```
|
||||||
|
+------+------+------+------------------+--------+--------+
|
||||||
|
| STX | CMD | LEN | PAYLOAD[LEN] | CRC_H | CRC_L |
|
||||||
|
+------+------+------+------------------+--------+--------+
|
||||||
|
0xAA 1B 1B LEN bytes 16-bit CRC
|
||||||
|
```
|
||||||
|
|
||||||
|
| 필드 | 크기 | 설명 |
|
||||||
|
|---------|------|------|
|
||||||
|
| STX | 1 | 고정 `0xAA` |
|
||||||
|
| CMD | 1 | 명령/응답 코드 (2장) |
|
||||||
|
| LEN | 1 | PAYLOAD 바이트 수 (0~255) |
|
||||||
|
| PAYLOAD | LEN | CMD 별 데이터 (3·4장) |
|
||||||
|
| CRC | 2 | **CRC-16/MODBUS** (poly 0xA001, init 0xFFFF), **CMD~PAYLOAD** 까지, **빅엔디안(CRC_H 먼저)** |
|
||||||
|
|
||||||
|
- 모든 멀티바이트 수치는 **빅엔디안(상위 바이트 먼저)**. (CRC 포함, 펌웨어 `My_Uart.c` 관례 유지)
|
||||||
|
- 프레임 구분 : `STX(0xAA)` 탐색 → `LEN` 으로 길이 확보 → `CRC` 검증.
|
||||||
|
- 수신 타임아웃(예: 50ms) 내 미완성 프레임은 폐기하고 STX 재탐색.
|
||||||
|
|
||||||
|
> **CRC 계산** : `My_Uart.c` 의 `CRC16()` (MODBUS 룩업테이블) 동일. 결과 16비트를 `CRC_H = (crc>>8)`, `CRC_L = (crc&0xFF)` 순으로 전송.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 명령 코드 (CMD)
|
||||||
|
|
||||||
|
**버스 레벨과 제어 권한을 구분해서 이해해야 한다.**
|
||||||
|
|
||||||
|
- **버스 마스터 = ERV(메인보드)** : 모든 통신을 ERV가 폴링한다. 룸컨/디퓨저는 폴링을 받아야만 송신하는 Slave.
|
||||||
|
- **제어 권한(명령 발생) = 룸컨** : 사용자가 룸컨에서 전원/모드/풍량/예약/필터리셋, **VSP 풍량 테이블 설정·저장**을 한다.
|
||||||
|
룸컨은 Slave이므로 이 명령들을 ERV 폴링에 대한 **응답에 실어** ERV로 보낸다 (룸컨 → ERV).
|
||||||
|
- **실행 = ERV** : 룸컨 명령을 받아 분배기를 통해 **디퓨저 댐퍼 개폐·풍량·LED를 제어**하고, **센서값을 읽어 룸컨에 전달**한다.
|
||||||
|
|
||||||
|
즉 데이터 흐름은: **룸컨(명령) → ERV(실행) → 디퓨저(댐퍼/센서) → ERV(센서수집) → 룸컨(표시)**.
|
||||||
|
|
||||||
|
CMD 상위 비트로 송신 방향을 구분한다. **0x10~0x7F = ERV(Master) 송신**, **0x90~0xFF = Slave 송신(응답)**.
|
||||||
|
|
||||||
|
### 2.1 ERV(메인보드, Master) 송신
|
||||||
|
|
||||||
|
| CMD | 이름 | 대상 | PAYLOAD | 설명 |
|
||||||
|
|------|----------------------|--------|---------|------|
|
||||||
|
| 0x10 | `CMD_DIFFUSER_CTRL` | 디퓨저 | 4.1 | **ERV가 디퓨저 제어**(댐퍼각/풍량/LED/리셋) + 폴링. ERV가 제어 주체 |
|
||||||
|
| 0x20 | `POLL_ROOMCON` | 룸컨 | 4.3 | ERV가 룸컨 폴링 + **표시용 데이터 전달**(ERV 동작상태/해당 실 공기질/에러/온도). 명령 수용 결과 echo |
|
||||||
|
| 0x30 | `RSP_VSP_STATUS` | 룸컨 | 4.5 | 룸컨 요청에 대한 **현재 VSP 풍량 테이블 값 회신**(룸컨 표시용) |
|
||||||
|
| 0x40 | `POLL_SPEC` | 공통 | 없음 | 장치 사양/버전 요청 |
|
||||||
|
|
||||||
|
### 2.2 Slave(룸컨/디퓨저) 송신 = 명령/상태 보고
|
||||||
|
|
||||||
|
| CMD | 이름 | 송신자 | PAYLOAD | 설명 |
|
||||||
|
|------|------------------------|--------|---------|------|
|
||||||
|
| 0xA0 | `CMD_ROOMCON` | 룸컨 | 4.4 | **룸컨이 사용자 명령 전달**: 전원/모드/풍량/예약/히터·UV/필터리셋 (0x20 응답) |
|
||||||
|
| 0xB0 | `CMD_VSP_SET` | 룸컨 | 4.6 | **룸컨이 VSP 풍량 테이블 설정·저장** + 장치 개수 설정 (0x20 응답, VSP 세팅모드) |
|
||||||
|
| 0x90 | `RSP_DIFFUSER_STATUS` | 디퓨저 | 4.2 | 디퓨저 댐퍼각/공기질/RPM/에러/버전 (0x10 응답) |
|
||||||
|
| 0x91 | `RSP_DIFFUSER_SENSOR` | 디퓨저 | 4.7 | 통합공기질 센서값(PM/온습도/VOC/NOx/CO2) (방별) |
|
||||||
|
| 0xC0 | `RSP_SPEC` | 공통 | 4.8 | 사양/버전 (0x40 응답) |
|
||||||
|
|
||||||
|
> 모든 패킷은 대상 장치 주소(DEV/ROOM/IDX)를 선두에 싣고, 응답은 이를 그대로 에코한다.
|
||||||
|
> `CMD_ROOMCON`(0xA0)·`CMD_VSP_SET`(0xB0)은 이름은 "CMD"지만 버스상으로는 **룸컨이 ERV 폴링(0x20)에 응답하는 형태**로 전송된다. (룸컨이 명령 주체이기 때문)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 공통 값 정의
|
||||||
|
|
||||||
|
### 3.1 장치 주소 (DEV / ROOM / IDX)
|
||||||
|
|
||||||
|
각 PAYLOAD 선두 3바이트는 항상 장치 주소다.
|
||||||
|
|
||||||
|
| 필드 | 크기 | 값 | 의미 |
|
||||||
|
|------|------|----|------|
|
||||||
|
| DEV | 1 | `0x10`=SA 디퓨저, `0x20`=RA 디퓨저, `0x30`=룸컨 | 장치 종류 |
|
||||||
|
| ROOM | 1 | `1`=거실, `2`=방1, `3`=방2, `4`=방3, `5`=방4 | 실 번호 |
|
||||||
|
| IDX | 1 | `1`~ | 같은 실·같은 종류 장치의 일련번호 (거실 SA·RA는 `1`,`2`) |
|
||||||
|
|
||||||
|
**5실 디바이스 맵 (힘펠 배선도 기준 — SA 6 / RA 6 / 룸컨 5)**
|
||||||
|
|
||||||
|
| 실 | ROOM | SA 디퓨저 | RA 디퓨저 | 룸컨 |
|
||||||
|
|----|------|-----------|-----------|------|
|
||||||
|
| 거실 | 1 | (0x10,1,1) 거실급기1 · (0x10,1,2) 거실급기2 | (0x20,1,1) 거실배기1 · (0x20,1,2) 거실배기2 | (0x30,1,1) |
|
||||||
|
| 방1 | 2 | (0x10,2,1) | (0x20,2,1) | (0x30,2,1) |
|
||||||
|
| 방2 | 3 | (0x10,3,1) | (0x20,3,1) | (0x30,3,1) |
|
||||||
|
| 방3 | 4 | (0x10,4,1) | (0x20,4,1) | (0x30,4,1) |
|
||||||
|
| 방4 | 5 | (0x10,5,1) | (0x20,5,1) | (0x30,5,1) |
|
||||||
|
|
||||||
|
> 방1~4 구성은 모두 동일(SA 1 · RA 1 · 룸컨 1). 거실만 SA·RA 각 2대.
|
||||||
|
|
||||||
|
### 3.2 전원 (Power)
|
||||||
|
|
||||||
|
| 값 | 의미 |
|
||||||
|
|----|------|
|
||||||
|
| 0x00 | OFF |
|
||||||
|
| 0x01 | ON |
|
||||||
|
|
||||||
|
### 3.3 운전모드 (RunMode)
|
||||||
|
|
||||||
|
| 값 | 의미 | 펌웨어 매핑 |
|
||||||
|
|----|------|-------------|
|
||||||
|
| 0x01 | 수동(환기) | MODE_VENTILATION |
|
||||||
|
| 0x02 | 자동 | MODE_AUTO |
|
||||||
|
| 0x04 | 바이패스 | MODE_BYPASS |
|
||||||
|
| 0x08 | 공기청정 | MODE_AIRCLEAN |
|
||||||
|
|
||||||
|
### 3.4 풍량 (FanSpeed)
|
||||||
|
|
||||||
|
| 값 | 의미 |
|
||||||
|
|----|------|
|
||||||
|
| 0x00 | OFF/정지 |
|
||||||
|
| 0x01 | 1단 (약) |
|
||||||
|
| 0x02 | 2단 (중) |
|
||||||
|
| 0x03 | 3단 (강) |
|
||||||
|
| 0x04 | 4단 (터보) |
|
||||||
|
|
||||||
|
> 자동/공청 모드에서는 메인보드 로직(부하점수)이 단수를 결정하므로 디퓨저로 보내는 풍량은 계산 결과값.
|
||||||
|
|
||||||
|
### 3.5 LED 밝기 (LedDim)
|
||||||
|
|
||||||
|
| 값 | 의미 |
|
||||||
|
|----|------|
|
||||||
|
| 0x00 | OFF |
|
||||||
|
| 0x01~0x0A | 1~10단 (10단 최대) |
|
||||||
|
|
||||||
|
> 월패드/스마트스위치 UI는 0~9 단계. 0=OFF 포함 시 0~10으로 매핑.
|
||||||
|
|
||||||
|
### 3.6 댐퍼 각도 (DamperAngle)
|
||||||
|
|
||||||
|
| 값 | 의미 |
|
||||||
|
|----|------|
|
||||||
|
| 0x00 | 0° (닫힘) |
|
||||||
|
| 0x00~0xB4 | 0~180° (0xB4 = 180° 완전개방) |
|
||||||
|
|
||||||
|
### 3.7 에러코드 (ErrorCode, **u32 비트맵** — 확장)
|
||||||
|
|
||||||
|
| 비트 | 마스크 | 의미 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 0 | 0x00000001 | 필터 청소 |
|
||||||
|
| 1 | 0x00000002 | 필터 교체 |
|
||||||
|
| 2 | 0x00000004 | 소자 교체 |
|
||||||
|
| 3 | 0x00000008 | 온도센서 에러 |
|
||||||
|
| 4 | 0x00000010 | 장비보호 모드 |
|
||||||
|
| 5 | 0x00000020 | EA 팬 에러 |
|
||||||
|
| 6 | 0x00000040 | 간헐운전 모드 |
|
||||||
|
| 7 | 0x00000080 | SA 팬 에러 |
|
||||||
|
| 8 | 0x00000100 | 통합센서 에러 — 거실 |
|
||||||
|
| 9 | 0x00000200 | 통합센서 에러 — 방1 |
|
||||||
|
| 10 | 0x00000400 | 통합센서 에러 — 방2 |
|
||||||
|
| 11 | 0x00000800 | 통합센서 에러 — 방3 |
|
||||||
|
| 12 | 0x00001000 | **통합센서 에러 — 방4 (신규)** |
|
||||||
|
| 16 | 0x00010000 | 통신 에러 — 거실 |
|
||||||
|
| 17 | 0x00020000 | 통신 에러 — 방1 |
|
||||||
|
| 18 | 0x00040000 | 통신 에러 — 방2 |
|
||||||
|
| 19 | 0x00080000 | 통신 에러 — 방3 |
|
||||||
|
| 20 | 0x00100000 | **통신 에러 — 방4 (신규)** |
|
||||||
|
|
||||||
|
> 비트 13~15, 21~31 은 향후 확장용 예약.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. PAYLOAD 상세
|
||||||
|
|
||||||
|
> 모든 PAYLOAD는 `[DEV][ROOM][IDX]` 3바이트로 시작. (3.1)
|
||||||
|
> `u16` 은 빅엔디안. 아래 off 는 PAYLOAD 내 상대 오프셋.
|
||||||
|
|
||||||
|
### 4.1 `CMD_DIFFUSER_CTRL` (0x10, Master→디퓨저)
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | DEV=0x10(SA) 또는 0x20(RA) |
|
||||||
|
| 3 | 1 | power | 3.2 |
|
||||||
|
| 4 | 1 | runMode | 3.3 |
|
||||||
|
| 5 | 1 | fanSpeed | 3.4 |
|
||||||
|
| 6 | 1 | ledDim | 3.5 (SA 디퓨저만 유효) |
|
||||||
|
| 7 | 1 | dmpAngle | 3.6 (해당 포트의 댐퍼 목표각) |
|
||||||
|
| 8 | 1 | dmpReset | 0=정상 / 1=댐퍼 초기화 |
|
||||||
|
| 9 | 1 | reserveHour | 예약 정지 0~8시간 (0=없음) |
|
||||||
|
|
||||||
|
LEN = 10.
|
||||||
|
|
||||||
|
### 4.2 `RSP_DIFFUSER_STATUS` (0x90, 디퓨저→Master)
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | 요청 에코 |
|
||||||
|
| 3 | 1 | power | 현재 전원 |
|
||||||
|
| 4 | 1 | runMode | 현재 운전모드 |
|
||||||
|
| 5 | 1 | fanSpeed | 현재 풍량 |
|
||||||
|
| 6 | 1 | ledDim | 현재 LED 밝기 |
|
||||||
|
| 7 | 1 | dmpAngle | 현재 댐퍼 각도 (3.6) |
|
||||||
|
| 8 | 2 | rpm | 해당 팬 실측 RPM (u16) |
|
||||||
|
| 10 | 4 | errorCode | 3.7 (u32) |
|
||||||
|
| 14 | 2 | version | 예) 0x0117 = Ver 1.23 |
|
||||||
|
|
||||||
|
LEN = 16.
|
||||||
|
|
||||||
|
### 4.3 `POLL_ROOMCON` (0x20, ERV→룸컨)
|
||||||
|
|
||||||
|
ERV가 룸컨을 폴링하면서 **룸컨 화면에 표시할 데이터**(ERV 동작상태·에러·온도)를 전달하고,
|
||||||
|
직전에 받은 룸컨 명령의 **수용 결과(ackFlags)** 를 에코한다.
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | DEV=0x30 |
|
||||||
|
| 3 | 1 | power | ERV 현재 전원 (3.2) |
|
||||||
|
| 4 | 1 | runMode | ERV 현재 운전모드 (3.3) |
|
||||||
|
| 5 | 1 | fanSpeed | ERV 현재 풍량 (3.4) |
|
||||||
|
| 6 | 1 | autoState | 0=분산, 1=집중 (자동모드) |
|
||||||
|
| 7 | 1 | reserveRemain | 예약 잔여 시간(hour) |
|
||||||
|
| 8 | 4 | errorCode | ERV 현재 에러 통보 (3.7, u32) |
|
||||||
|
| 12 | 2 | outTemp | 외기온도 ×10 (signed, ℃) |
|
||||||
|
| 14 | 2 | inTemp | 내기온도 ×10 (signed, ℃) |
|
||||||
|
| 16 | 1 | ackFlags | 직전 룸컨 명령 수용 비트(4.4 cmdFlags 동일 배치) |
|
||||||
|
|
||||||
|
LEN = 17.
|
||||||
|
|
||||||
|
### 4.4 `CMD_ROOMCON` (0xA0, 룸컨→ERV) — **룸컨이 명령 주체**
|
||||||
|
|
||||||
|
사용자가 룸컨에서 조작한 명령을 ERV에 전달한다. `cmdFlags` 로 **이번에 실제로 바꾼 항목만** 표시한다(나머지는 무시).
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | DEV=0x30, ERV 폴링(0x20)에 대한 응답으로 송신 |
|
||||||
|
| 3 | 1 | cmdFlags | bit0 power, bit1 runMode, bit2 fanSpeed, bit3 reserveHour, bit4 heaterUV, bit5 filterReset, bit6 ledDim (1=이 필드 명령 유효) |
|
||||||
|
| 4 | 1 | power | 3.2 |
|
||||||
|
| 5 | 1 | runMode | 3.3 |
|
||||||
|
| 6 | 1 | fanSpeed | 3.4 |
|
||||||
|
| 7 | 1 | reserveHour | 0~8시간 |
|
||||||
|
| 8 | 1 | heaterUV | bit0=히터, bit4=UV (1=ON) |
|
||||||
|
| 9 | 1 | filterReset | 1=필터 리셋 |
|
||||||
|
| 10 | 1 | ledDim | 3.5 (해당 실 디퓨저 LED 밝기) |
|
||||||
|
| 11 | 2 | version | 룸컨 버전 |
|
||||||
|
|
||||||
|
LEN = 13.
|
||||||
|
|
||||||
|
> ERV는 받은 명령을 실행(분배기→디퓨저 제어)하고, 다음 `POLL_ROOMCON(0x20)`의 `ackFlags`·동작상태로 결과를 회신한다.
|
||||||
|
|
||||||
|
### 4.5 `RSP_VSP_STATUS` (0x30, ERV→룸컨)
|
||||||
|
|
||||||
|
룸컨이 현재 VSP 값을 요청(`CMD_VSP_SET` 의 reqStatus=1)하면, ERV가 **저장된 현재 VSP 풍량 테이블**을 회신한다(룸컨 표시용). 레이아웃은 4.6과 동일(끝의 save/장치개수 필드는 제외, vspSelect로 어떤 모드값인지 표시).
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | DEV=0x30 |
|
||||||
|
| 3 | 1 | vspSelect | 회신하는 모드 (`1`=환기/`2`=공청/`3`=바이패스) |
|
||||||
|
| 4 | 10 | sa1,ea1 … sa5,ea5 | SA/EA 1~5단 VSP (각 1B) |
|
||||||
|
| 14 | 2 | rpmRefMid | 중(2단) RPM 기준 |
|
||||||
|
| 16 | 2 | rpmDeltaMid | 중(2단) RPM 허용편차 |
|
||||||
|
| 18 | 2 | rpmRefHigh | 강(3단) RPM 기준 |
|
||||||
|
| 20 | 2 | rpmDeltaHigh | 강(3단) RPM 허용편차 |
|
||||||
|
|
||||||
|
LEN = 22.
|
||||||
|
|
||||||
|
### 4.6 `CMD_VSP_SET` (0xB0, 룸컨→ERV) — **VSP 설정·저장 주체는 룸컨**
|
||||||
|
|
||||||
|
룸컨에서 VSP 풍량 테이블을 한 모드씩(vspSelect) 설정하고 ERV에 저장 요청. 폴링할 장치 개수도 함께 설정.
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | DEV=0x30, ERV 폴링(0x20)에 대한 응답으로 송신 |
|
||||||
|
| 3 | 1 | vspSelect | `0`=None, `1`=환기, `2`=공청, `3`=바이패스 |
|
||||||
|
| 4 | 1 | sa1 | SA 1단 VSP |
|
||||||
|
| 5 | 1 | ea1 | EA 1단 VSP |
|
||||||
|
| 6 | 1 | sa2 | SA 2단 |
|
||||||
|
| 7 | 1 | ea2 | EA 2단 |
|
||||||
|
| 8 | 1 | sa3 | SA 3단 |
|
||||||
|
| 9 | 1 | ea3 | EA 3단 |
|
||||||
|
| 10 | 1 | sa4 | SA 4단 |
|
||||||
|
| 11 | 1 | ea4 | EA 4단 |
|
||||||
|
| 12 | 1 | sa5 | SA 5단 |
|
||||||
|
| 13 | 1 | ea5 | EA 5단 |
|
||||||
|
| 14 | 2 | rpmRefMid | 중(2단) RPM 기준 (환기/공청만) |
|
||||||
|
| 16 | 2 | rpmDeltaMid | 중(2단) RPM 허용편차 |
|
||||||
|
| 18 | 2 | rpmRefHigh | 강(3단) RPM 기준 |
|
||||||
|
| 20 | 2 | rpmDeltaHigh | 강(3단) RPM 허용편차 |
|
||||||
|
| 22 | 1 | roomconNum | 폴링할 룸컨 수 (1~5) |
|
||||||
|
| 23 | 1 | saDiffuserNum | 폴링할 SA 디퓨저 수 (2~6) |
|
||||||
|
| 24 | 1 | raDiffuserNum | 폴링할 RA 디퓨저 수 (2~6) |
|
||||||
|
| 25 | 1 | modbusId | 외부 홈넷 연동용 ID (선택) |
|
||||||
|
| 26 | 1 | save | 0=저장안함, 1=EEPROM 저장 |
|
||||||
|
| 27 | 1 | reqStatus | 1=현재 VSP 값 회신 요청(ERV가 `RSP_VSP_STATUS`로 응답) |
|
||||||
|
|
||||||
|
LEN = 28.
|
||||||
|
|
||||||
|
### 4.7 `RSP_DIFFUSER_SENSOR` (0x91, 디퓨저→Master)
|
||||||
|
|
||||||
|
통합공기질 센서(SEN66) 1실분. 기존 0x11/0x12 분리 모드를 **이 패킷 1종**으로 대체.
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 단위/비고 |
|
||||||
|
|-----|------|------|-----------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | 센서 부착 디퓨저(통상 RA, DEV=0x20) |
|
||||||
|
| 3 | 2 | pm1p0 | ㎍/㎥ |
|
||||||
|
| 5 | 2 | pm2p5 | ㎍/㎥ |
|
||||||
|
| 7 | 2 | pm4p0 | ㎍/㎥ |
|
||||||
|
| 9 | 2 | pm10p0 | ㎍/㎥ |
|
||||||
|
| 11 | 2 | humidity | %RH ×10 |
|
||||||
|
| 13 | 2 | temperature | ℃ ×10 (signed) |
|
||||||
|
| 15 | 2 | voc | TVOC index |
|
||||||
|
| 17 | 2 | nox | NOx index |
|
||||||
|
| 19 | 2 | co2 | ppm |
|
||||||
|
| 21 | 4 | errorCode | 센서/통신 에러 (3.7) |
|
||||||
|
|
||||||
|
LEN = 25.
|
||||||
|
|
||||||
|
### 4.8 `RSP_SPEC` (0xC0)
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | |
|
||||||
|
| 3 | 2 | version | 펌웨어 버전 |
|
||||||
|
| 5 | 1 | deviceType | 장치 타입 코드 |
|
||||||
|
| 6 | 1 | capability | bit0=히터, bit1=UV, bit2=후드연동 … |
|
||||||
|
|
||||||
|
LEN = 7.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 동작 시나리오 (폴링)
|
||||||
|
|
||||||
|
ERV가 버스 마스터로서 **룸컨 → 디퓨저 순으로 라운드로빈 폴링**하고, 명령은 룸컨이 응답에 실어 올린다.
|
||||||
|
|
||||||
|
1. **룸컨 폴링**
|
||||||
|
- ERV → 룸컨 : `POLL_ROOMCON(0x20)` (ERV 동작상태·해당 실 공기질·온도·에러 전달 + 직전 명령 ack)
|
||||||
|
- 룸컨 → ERV : 사용자가 조작했으면 `CMD_ROOMCON(0xA0)` (cmdFlags로 바뀐 항목 표시), VSP 설정중이면 `CMD_VSP_SET(0xB0)` 으로 응답
|
||||||
|
2. **디퓨저 폴링 (ERV가 제어 주체)**
|
||||||
|
- ERV → 디퓨저 : `CMD_DIFFUSER_CTRL(0x10)` (댐퍼각/풍량/LED 지시)
|
||||||
|
- 디퓨저 → ERV : `RSP_DIFFUSER_STATUS(0x90)`, 센서 부착 디퓨저는 이어서 `RSP_DIFFUSER_SENSOR(0x91)`
|
||||||
|
3. **명령 처리 흐름** : 룸컨 명령(0xA0) 수신 → ERV가 운전모드/풍량 결정 → 각 디퓨저에 `CMD_DIFFUSER_CTRL(0x10)`로 댐퍼 개폐·풍량 지시 → 결과를 다음 `POLL_ROOMCON(0x20)`의 ackFlags·동작상태로 룸컨에 회신.
|
||||||
|
4. **자동/공청 모드** : ERV가 각 실 센서(0x91)로 부하점수·집중/분산 계산 → 디퓨저 댐퍼각·풍량을 `CMD_DIFFUSER_CTRL`로 지시 (사양서 10~11P 로직).
|
||||||
|
5. **VSP 시운전** : 룸컨이 `CMD_VSP_SET(0xB0)`으로 단별 VSP·RPM·장치개수 설정(save=1 시 ERV가 EEPROM 저장), reqStatus=1이면 ERV가 `RSP_VSP_STATUS(0x30)`로 현재값 회신.
|
||||||
|
6. **통신 단절** : 폴링 타임아웃 N회 시 ERV가 해당 실 통신에러 비트(3.7 bit16~20) set, 재연결 시 clear.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 펌웨어 반영 메모
|
||||||
|
|
||||||
|
- 대상 파일 : `program/User/My_Uart.c` (SC1, 각실분배기/디퓨저), `bunbaegi_parsing()` / `Bunbaegi_Polling()` 를 CMD 기반으로 교체.
|
||||||
|
- **`My_RJ2.c` (DL 룸컨 232) 는 변경하지 않는다** — DL 사양 별도 유지.
|
||||||
|
- CRC : 기존 `CRC16()` (MODBUS) 그대로 사용, 전송은 빅엔디안(Hi→Lo).
|
||||||
|
- 센서/디퓨저 배열은 현행 `[7]` (index 1~6) 유지 가능하나, ROOM(1~5)+IDX 매핑 테이블 1개로 주소→배열 변환.
|
||||||
|
- 에러코드는 `uint16_t Err_Code` → **`uint32_t`** 로 확장 (방4 비트 수용).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 송수신 예제 — 거실 룸컨이 전 실 상태 표시
|
||||||
|
|
||||||
|
**목표** : 거실 룸컨에서 모든 방 디퓨저(SA/RA) 댐퍼·LED·센서 + ERV 동작상태(모드/풍량/예약)를 표시.
|
||||||
|
|
||||||
|
**전제** : 거실 룸컨은 디퓨저를 직접 못 읽는다. 버스 마스터는 ERV 하나뿐이므로 항상 2단계.
|
||||||
|
|
||||||
|
```
|
||||||
|
① 수집 : ERV ──폴링──> 각 디퓨저(SA/RA) ──상태/센서──> ERV (캐싱)
|
||||||
|
② 표시 : ERV ──전달──> 거실 룸컨 (화면 표시)
|
||||||
|
```
|
||||||
|
|
||||||
|
> 아래 모든 프레임의 CRC는 **STX(0xAA) 제외, `CMD~PAYLOAD` 구간**에 대한 CRC-16/MODBUS 결과를 빅엔디안(Hi,Lo)으로 붙인 실제 값이다.
|
||||||
|
> 예시 값 : runMode=`0x02`(자동), fanSpeed=`2`, 댐퍼 `0xB4`=180°(열림)/`0x00`=닫힘, 온도·습도 ×10.
|
||||||
|
> **폴링 시간** : 1회 폴링(TX 요청 → RX 응답)을 **300ms**로 가정한다. (각 [TX]/[RX] 쌍 = 300ms)
|
||||||
|
|
||||||
|
### ① 수집 단계 — ERV가 각 디퓨저를 라운드로빈 폴링
|
||||||
|
|
||||||
|
ERV가 `거실SA1 → 거실SA2 → 거실RA1 → 거실RA2 → 방1SA → 방1RA → … → 방4RA` 순으로 폴링. 대표 예 (괄호는 누적시간):
|
||||||
|
|
||||||
|
**거실 SA1 디퓨저 폴링/제어** — `CMD_DIFFUSER_CTRL(0x10)` *(t=0 ~ 300ms)*
|
||||||
|
```
|
||||||
|
[TX] AA 10 0A | 10 01 01 | 01 02 02 05 B4 00 00 | E4 61
|
||||||
|
STX CMD LEN DEV ROOM IDX power runMode fan led dmp reset reserve
|
||||||
|
(SA,거실,1) ON 02(자동) 2 5 B4 0 0
|
||||||
|
```
|
||||||
|
```
|
||||||
|
[RX] AA 90 10 | 10 01 01 | 01 02 02 05 B4 03 52 00 00 00 00 01 17 | A6 08
|
||||||
|
└RSP_DIFFUSER_STATUS power mode fan led dmp rpm=0352(850) err=0(4B) ver=0117
|
||||||
|
```
|
||||||
|
|
||||||
|
**거실 RA1 디퓨저 폴링** — 상태 `RSP_DIFFUSER_STATUS(0x90)` + 센서 `RSP_DIFFUSER_SENSOR(0x91)` *(t=600 ~ 900ms)*
|
||||||
|
```
|
||||||
|
[RX] AA 91 19 | 20 01 01 | 00 05 00 08 00 09 00 0B 01 C2 00 DC 00 64 00 01 02 8A | 00 00 00 00 | E8 AF
|
||||||
|
└센서 (RA,거실,1) pm1=5 pm2.5=8 pm4=9 pm10=11 습45.0% 온22.0℃ voc100 nox1 co2=028A(650) err=0
|
||||||
|
```
|
||||||
|
> RA 디퓨저는 한 폴링 슬롯(300ms)에서 상태(0x90)+센서(0x91)를 함께 응답.
|
||||||
|
> RA2·방1~4 디퓨저도 같은 방식으로 폴링 → ERV가 전 실 댐퍼각/LED/RPM/센서를 모두 캐싱.
|
||||||
|
|
||||||
|
**ERV 동작상태를 거실 룸컨에 전달 (+ 룸컨 명령 수신)** — `POLL_ROOMCON(0x20)` *(룸컨 폴링 슬롯, 각 300ms)*
|
||||||
|
```
|
||||||
|
[TX] AA 20 11 | 30 01 01 | 01 02 02 01 00 | 00 00 00 00 | 00 96 | 00 DC | 00 | B5 61
|
||||||
|
└ERV→룸컨 (룸컨,거실,1) power mode fan auto=1(집중) reserveRemain=0
|
||||||
|
err=0(4B) 외기15.0℃ 내기22.0℃ ack=0
|
||||||
|
[RX] (거실 룸컨이 조작했으면 CMD_ROOMCON(0xA0)/CMD_VSP_SET(0xB0)로 응답, 없으면 빈 응답)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ② 표시 단계 — 전 실 집계 전달
|
||||||
|
|
||||||
|
거실 룸컨이 전 실 디퓨저·센서를 한 화면에 표시하려면, ERV가 ①에서 모은 값을 집계해 거실 룸컨에 전달해야 한다.
|
||||||
|
|
||||||
|
> **참고(본 규격 미반영 제안)** : 아래 `CMD_ALLROOM_STATUS(0x21)` 는 "전 실 집계"용으로 검토한 예시 패킷이다.
|
||||||
|
> 정식 채택은 보류 상태이며, 필요 시 별도 협의 후 2·4장에 추가한다.
|
||||||
|
|
||||||
|
**헤더(9B)** + **실별 블록(18B) × 5실** :
|
||||||
|
|
||||||
|
| 헤더 off | 필드 | 실 블록 off | 필드 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0~2 | 룸컨 주소(30·01·01) | +0 | roomNo |
|
||||||
|
| 3 | ERV power | +1,+2 | SA댐퍼1·2 (`0xFF`=없음) |
|
||||||
|
| 4 | ERV runMode | +3,+4 | RA댐퍼1·2 |
|
||||||
|
| 5 | ERV fanSpeed | +5 | LED 밝기 |
|
||||||
|
| 6 | 예약 잔여(h) | +6,+7 | PM2.5 |
|
||||||
|
| 7 | autoState | +8,+9 | PM10 |
|
||||||
|
| 8 | roomCount(=5) | +10,+11 | CO2 |
|
||||||
|
| | | +12,+13 | VOC |
|
||||||
|
| | | +14,+15 | 온도 ×10 |
|
||||||
|
| | | +16,+17 | 습도 ×10 |
|
||||||
|
|
||||||
|
```
|
||||||
|
[TX] AA 21 63 | 30 01 01 01 02 02 00 01 05 | <실1..실5 블록> | D9 F4
|
||||||
|
└CMD_ALLROOM 헤더(LEN=0x63=99) 거실은 SA·RA 2개 모두 사용
|
||||||
|
|
||||||
|
실1(거실): 01 B4 B4 B4 B4 05 00 08 00 0B 02 8A 00 64 00 DC 01 C2 SA열림×2,RA열림×2,LED5,PM2.5=8,PM10=11,CO2=650,VOC=100,22.0℃,45.0%
|
||||||
|
실2(방1) : 02 B4 FF B4 FF 04 00 0C 00 12 02 D0 00 82 00 DD 01 C4 SA/RA 각1, slot2=FF
|
||||||
|
실3(방2) : 03 00 FF 00 FF 00 00 06 00 09 02 62 00 5A 00 DC 01 C0 댐퍼 닫힘, LED OFF
|
||||||
|
실4(방3) : 04 B4 FF B4 FF 06 00 14 00 1C 03 2A 00 A0 00 DB 01 C7
|
||||||
|
실5(방4) : 05 B4 FF B4 FF 03 00 07 00 0A 02 80 00 5F 00 DC 01 C1
|
||||||
|
```
|
||||||
|
> 전체 104 byte 1프레임으로 거실 룸컨이 **전 실 SA/RA 댐퍼·LED·센서 + ERV 모드/풍량/예약**을 모두 표시.
|
||||||
|
|
||||||
|
### 폴링 주기와 전체 시간 (1회 = 300ms)
|
||||||
|
|
||||||
|
5실 구성에서 ERV가 한 바퀴 도는 동안 폴링하는 장치:
|
||||||
|
|
||||||
|
| 장치 | 개수 | 폴링 시간 (×300ms) |
|
||||||
|
|------|------|--------------------|
|
||||||
|
| SA 디퓨저 (거실2 + 방1~4 각1) | 6 | 1,800 ms |
|
||||||
|
| RA 디퓨저 (거실2 + 방1~4 각1, 상태+센서 동시) | 6 | 1,800 ms |
|
||||||
|
| 룸컨 (거실 + 방1~4) | 5 | 1,500 ms |
|
||||||
|
| **합계 (1주기)** | **17** | **5,100 ms ≈ 5.1초** |
|
||||||
|
|
||||||
|
- 거실 룸컨 화면은 **약 5.1초마다 전 실 데이터가 1회 갱신**된다.
|
||||||
|
- 갱신을 더 빠르게 하려면 : 폴링 시간 단축(예: 200ms → 17×200 = 3.4초), 또는 폴링 대상 축소.
|
||||||
|
- `0x21` 집계 프레임은 거실 룸컨 폴링 슬롯 안에서 전달되므로 별도 시간이 추가되지 않는다.
|
||||||
|
|
||||||
|
```
|
||||||
|
[1 주기 = 5.1초]
|
||||||
|
SA1 SA2 RA1 RA2 | 방1SA 방1RA | 방2SA 방2RA | 방3SA 방3RA | 방4SA 방4RA | RC거실 RC방1 RC방2 RC방3 RC방4
|
||||||
|
└─거실 4슬롯─┘ └─방1 2─┘ └─방2 2─┘ └─방3 2─┘ └─방4 2─┘ └────── 룸컨 5슬롯 ──────┘
|
||||||
|
각 슬롯 300ms × 17 = 5,100ms
|
||||||
|
```
|
||||||
|
|
||||||
|
### 요약
|
||||||
|
|
||||||
|
| 단계 | 방향 | 패킷 | 역할 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| ① | ERV→디퓨저 / 디퓨저→ERV | `0x10` / `0x90`,`0x91` | 전 실 댐퍼·LED·센서 수집 |
|
||||||
|
| ② | ERV→거실룸컨 | `0x20` | ERV 동작상태(모드/풍량/예약) 전달 |
|
||||||
|
| ② | ERV→거실룸컨 | `0x21` (미반영 제안) | 전 실 디퓨저·센서 집계 전달 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 본 문서는 내부 통신 재정의 초안(Rev 2.0)이다. 디퓨저/룸컨 펌웨어 담당(주신전자)과
|
||||||
|
> CMD 코드·필드 세부값을 상호 합의하여 확정한다.
|
||||||
@@ -0,0 +1,546 @@
|
|||||||
|
# 각실제어 내부 통신 프로토콜 (Rev 3.0, 2-Tier 계층형)
|
||||||
|
|
||||||
|
> 휴벤ECO(ERV) ↔ **각실분배기** ↔ 디퓨저(SA/RA) · 룸컨 **내부 통신** 규격
|
||||||
|
>
|
||||||
|
> 본 규격은 [각실제어_내부프로토콜_Rev2.0_CMD.md](각실제어_내부프로토콜_Rev2.0_CMD.md) 를
|
||||||
|
> **계층형(2-Tier)** 으로 재구성한 것이다. 분배기 회로도 `Schematic/BUNBAGI_REV4.1_20251124(회로도).pdf`
|
||||||
|
> 와 펌웨어 `program/User/My_Uart.c` 를 근거로 한다.
|
||||||
|
> (Rev 2.0 은 그대로 보존하며, 본 Rev 3.0 이 상위 규격이다. DL 룸컨 232 `My_RJ2.c` 는 무관 — 변경 안 함.)
|
||||||
|
|
||||||
|
- **물리계층** : RS-485, **115200 bps, 8 Data, None Parity, 1 Stop (N81)**
|
||||||
|
- **대상 시스템** : 사양서 5페이지 구성 = **거실 + 방1~4 (5실)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 무엇이 바뀌었나 (Rev 2.0 단일버스 → Rev 3.0 2-Tier)
|
||||||
|
|
||||||
|
회로도 분석 결과, 각실분배기는 **자체 MCU(Nuvoton NANO100SE3BN)를 가진 능동 컨트롤러**임이 확인되었다. 따라서 통신을 두 계층으로 분리한다.
|
||||||
|
|
||||||
|
| 구분 | Rev 2.0 (단일 버스) | Rev 3.0 (2-Tier) |
|
||||||
|
|------|---------------------|------------------|
|
||||||
|
| 버스 구조 | ERV가 모든 디퓨저/룸컨(17대)을 1버스로 직접 폴링 | **상위: ERV↔분배기 1버스 / 하위: 분배기↔실별 디퓨저·룸컨** |
|
||||||
|
| ERV 폴링 대상 | 17대 | **분배기 1대** |
|
||||||
|
| 분배기 역할 | 없음(전원·배선 통과) | **하위 5채널 로컬 마스터 + 에러격리 + 포트별 통신상태 LED** |
|
||||||
|
| 디퓨저 LED 제어 | 디퓨저에 ledDim 전송 | **디퓨저에 ledDim 전송** (유지). 조명 LED는 **RA 디퓨저에만** 있고 ERV→분배기→RA디퓨저로 밝기 제어 |
|
||||||
|
| 자동운전 판단 | ERV | **ERV** (분배기는 센서수집·명령실행만) |
|
||||||
|
| 집계 패킷 | 0x21 제안(미반영) | **상위버스 정식 패킷(`RSP_ALLROOM_*`)으로 채택** |
|
||||||
|
|
||||||
|
> **하드웨어 근거 (BUNBAGI Rev4.1)** : 상위 `M485`(RS485 1채널, U5, 커넥터 CN3) / 하위 `SA485`(RS485 **5채널**, 실별 RJ45 J1~J5, RX는 8:1 MUX U6로 실 선택, DIR 채널별) / 전원 24V(60W)·3.3V.
|
||||||
|
> ※ 분배기의 2×74HC595(U10·U12, SA=초록·RA=노랑) LED는 **각 포트 SA/RA 디퓨저의 통신상태 표시등**(분배기 보드 진단용)이며, 프로토콜과 무관하다.
|
||||||
|
> ※ **방의 조명용 LED는 RA 디퓨저에만 달려 있고**, ERV가 밝기(0~9)를 명령한다 → 분배기가 해당 RA 디퓨저로 전달.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0-1. Rev 2.0(단일버스) → Rev 3.0(2-Tier) 장단점
|
||||||
|
|
||||||
|
> 채택 전 의사결정용 요약. 결론적으로 **하드웨어(분배기 MCU·하위 5채널·SPOF 구조)와 5실 확장 요구를 고려하면 2-Tier 도입 권장**이되, 분배기 펌웨어 신규 개발과 단일 장애점(SPOF) 대비가 전제다.
|
||||||
|
|
||||||
|
### ✅ 장점
|
||||||
|
|
||||||
|
| # | 항목 | 내용 | 효과 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| 1 | **ERV 폴링 부하 격감** | ERV가 17대 직접 폴링 → **분배기 1대만** 상대 | ERV 펌웨어·루프 단순, CPU 여유 |
|
||||||
|
| 2 | **갱신 속도 향상** | ERV 입장 1주기 **5.1초 → 0.6초** (상·하위 병렬, 하위는 분배기가 빠르게 순환) | 룸컨·대시보드 응답성 ↑ |
|
||||||
|
| 3 | **버스 트래픽·충돌 감소** | 상위 버스엔 노드 1개, 하위는 5채널로 부하 분산(MUX+채널별 DIR) | 패킷 충돌/재전송 ↓ |
|
||||||
|
| 4 | **실별 통신에러 격리** | 한 실 디퓨저 고장이 채널 단위로 격리 → 타 실·ERV에 영향 최소 | 가용성 ↑, 원인 국소화 |
|
||||||
|
| 5 | **신호 무결성·배선 이점** | 하위는 짧은 로컬 세그먼트 5채널 분리, 채널별 종단·DIR | 노이즈·반사 ↓ (긴 데이지체인 대비) |
|
||||||
|
| 6 | **ERV–분배기 인터페이스 추상화** | ERV는 디퓨저 주소·개수를 몰라도 됨. 디퓨저 증설/사양변경을 **분배기가 흡수** | ERV 코드 변경 없이 하위 확장 |
|
||||||
|
| 7 | **역할·개발 분담** | ERV(시스템 로직) ↔ 분배기/디퓨저/룸컨(주신) 펌웨어 디커플링 | 병행 개발·유지보수 용이 |
|
||||||
|
| 8 | **확장성** | 실/디퓨저 추가는 하위에서 처리, 멀티 분배기(`nodeId`) 대비 | 향후 평면 확장 수월 |
|
||||||
|
| 9 | **MCU 동일(Nano100)** | ERV·분배기 동일 계열 → 프레임/CRC/드라이버 코드 공유 | 개발 재사용 |
|
||||||
|
|
||||||
|
### ⚠️ 단점 / 리스크
|
||||||
|
|
||||||
|
| # | 항목 | 내용 | 완화책 |
|
||||||
|
|---|------|------|--------|
|
||||||
|
| 1 | **분배기 펌웨어 신규 개발** | 하위 마스터 + 상위 슬레이브 + MUX/DIR + 595 + 집계 캐시 로직 필요 | Nano100 공통 드라이버 재사용, `My_Uart.c` 골격 활용 |
|
||||||
|
| 2 | **지연(latency) 1홉 추가** | 명령이 ERV→분배기→디퓨저 2단. 최악 상위주기+하위주기 합산 | 긴급 제어는 분배기가 즉시 중계, 댐퍼는 비실시간이라 영향 작음 |
|
||||||
|
| 3 | **상태 동기화 시차** | ERV가 보는 값은 분배기 캐시(직전 하위 폴링 결과) → 한 박자 지연 가능 | 변경 이벤트 우선 보고, 타임스탬프/시퀀스로 정합 |
|
||||||
|
| 4 | **단일 장애점(SPOF)** | 중앙 분배기 1대 고장 시 **전 실 통신 두절** (단일버스는 ERV-디퓨저 직접이라 무관) | 분배기 워치독·자기진단, 통신두절 시 ERV 안전모드 |
|
||||||
|
| 5 | **집계 패킷이 큼** | `RSP_ALLROOM_STATUS` 93B 등 → 1프레임 손상 시 전 실 갱신 실패(재전송 단위 큼) | CRC+재요청, 필요 시 실별 분할 응답 옵션 |
|
||||||
|
| 6 | **디버깅·추적 복잡** | 장애 시 상위/하위/분배기 로직 분리 진단, 버스 스니핑 2곳 | 분배기 진단로그·상태 LED(595), SPEC/버전 패킷 |
|
||||||
|
| 7 | **프로토콜 2종 관리** | 상위·하위 CMD 세트 2개 정의·문서화·버전관리 | 본 문서 단일화, 프레임 규칙 공통 |
|
||||||
|
| 8 | **3자 버전 정합** | ERV·분배기·디퓨저/룸컨 펌웨어 호환성 관리 | `SPEC`(0x1F/0x9F) 버전 교환·검증 |
|
||||||
|
|
||||||
|
### 한눈에
|
||||||
|
|
||||||
|
| 관점 | Rev 2.0 단일버스 | Rev 3.0 2-Tier |
|
||||||
|
|------|------------------|----------------|
|
||||||
|
| ERV 부담 | 큼(17대 폴링) | **작음(1대)** |
|
||||||
|
| 갱신 주기(ERV) | 5.1초 | **0.6초** |
|
||||||
|
| 분배기 펌웨어 | 불필요 | **필요(신규)** |
|
||||||
|
| 장애 격리 | 디퓨저별(분배기 무관) | 실별(단, 분배기 SPOF) |
|
||||||
|
| 확장/유지보수 | ERV가 전부 관리 | **계층 분리로 용이** |
|
||||||
|
| 구현 난이도 | 낮음 | 중(분배기 추가) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 시스템 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
[상위 버스: M485 · RS485 1채널] [하위 버스: SA485 · RS485 5채널 (MUX)]
|
||||||
|
┌── ch1 거실 : SA디퓨저×2, RA디퓨저×2, 룸컨
|
||||||
|
ERV ───────────────────────── 각실분배기 ─┼── ch2 방1 : SA디퓨저, RA디퓨저, 룸컨
|
||||||
|
(Master) (Nano100) ├── ch3 방2 : SA디퓨저, RA디퓨저, 룸컨
|
||||||
|
· 상위 Slave ├── ch4 방3 : SA디퓨저, RA디퓨저, 룸컨
|
||||||
|
· 하위 Master └── ch5 방4 : SA디퓨저, RA디퓨저, 룸컨
|
||||||
|
· 포트별 통신상태 LED(74HC595, 진단용)
|
||||||
|
· 실별 전원(24V)·통신에러 격리
|
||||||
|
```
|
||||||
|
|
||||||
|
**역할 분담**
|
||||||
|
|
||||||
|
| 주체 | 책임 |
|
||||||
|
|------|------|
|
||||||
|
| **ERV (메인)** | 전열교환기 팬/VSP 제어, **자동운전 판단**(부하점수·집중/분산, 사양서 10~11P), **부가모드(스마트수면/쾌적조리/안심회복)·후드 연동(HOOD-485) 처리**, 분배기에 전원/모드/풍량 + 실별 댐퍼·LED 타겟 하달, 집계상태 수신 |
|
||||||
|
| **분배기** | 하위 5채널 폴링(실별 SA/RA 디퓨저·룸컨), **댐퍼·LED 명령 중계·실행**, 센서·룸컨명령 수집→ERV에 집계 보고, 실별 통신에러 격리, **포트별 통신상태 표시 LED**(74HC595, 진단용) |
|
||||||
|
| **디퓨저(SA/RA)** | 댐퍼 구동·각도/RPM 회신, 센서값 회신. **RA 디퓨저는 자체 조명 LED 점등(밝기 0~9)** |
|
||||||
|
| **룸컨** | 사용자 입력(전원/모드/풍량/예약 등) 보고, ERV 상태 표시 |
|
||||||
|
|
||||||
|
**데이터 흐름 (자동운전)**
|
||||||
|
|
||||||
|
```
|
||||||
|
분배기 ──(실별 원시 센서)──> ERV ──[부하점수·집중/분산 계산]──> 실별 댐퍼/풍량/LED 타겟
|
||||||
|
ERV ──(타겟)──> 분배기 ──(댐퍼·LED 실행 / 디퓨저 제어)──> 각 실
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 공통 프레임 (상·하위 동일)
|
||||||
|
|
||||||
|
```
|
||||||
|
+------+------+------+------------------+--------+--------+
|
||||||
|
| STX | CMD | LEN | PAYLOAD[LEN] | CRC_H | CRC_L |
|
||||||
|
+------+------+------+------------------+--------+--------+
|
||||||
|
0xAA 1B 1B LEN bytes 16-bit CRC
|
||||||
|
```
|
||||||
|
|
||||||
|
| 필드 | 크기 | 설명 |
|
||||||
|
|---------|------|------|
|
||||||
|
| STX | 1 | 고정 `0xAA` |
|
||||||
|
| CMD | 1 | 명령/응답 코드 (상위 4장 / 하위 5장) |
|
||||||
|
| LEN | 1 | PAYLOAD 바이트 수 (0~255) |
|
||||||
|
| PAYLOAD | LEN | CMD 별 데이터 |
|
||||||
|
| CRC | 2 | **CRC-16/MODBUS** (poly 0xA001, init 0xFFFF), **CMD~PAYLOAD** 까지, **빅엔디안(CRC_H 먼저)** |
|
||||||
|
|
||||||
|
- 모든 멀티바이트 수치는 **빅엔디안**. 상·하위 버스 모두 같은 프레임/CRC 규칙을 쓴다.
|
||||||
|
- 분배기 MCU와 ERV MCU가 동일 계열(Nano100)이므로 CRC·프레임 파서 코드를 공유한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 공통 값 정의
|
||||||
|
|
||||||
|
### 3.1 전원 (Power)
|
||||||
|
`0x00`=OFF, `0x01`=ON
|
||||||
|
|
||||||
|
### 3.2 운전모드 (RunMode)
|
||||||
|
| 값 | 의미 | 펌웨어 |
|
||||||
|
|----|------|--------|
|
||||||
|
| 0x01 | 수동(환기) | MODE_VENTILATION |
|
||||||
|
| 0x02 | 자동 | MODE_AUTO |
|
||||||
|
| 0x04 | 바이패스 | MODE_BYPASS |
|
||||||
|
| 0x08 | 공기청정 | MODE_AIRCLEAN |
|
||||||
|
|
||||||
|
### 3.3 풍량 (FanSpeed)
|
||||||
|
`0`=정지, `1`=약, `2`=중, `3`=강, `4`=터보
|
||||||
|
|
||||||
|
### 3.4 LED 밝기 (LedDim) — **RA 디퓨저 조명**
|
||||||
|
`0`=OFF, `1`~`9`=1~9단(9=최대). 조명 LED는 **RA 디퓨저에만** 존재(단색, 색온도 3800K). ERV가 실별 밝기를 지시 → 분배기가 해당 실 RA 디퓨저로 전달 → RA 디퓨저가 자체 점등.
|
||||||
|
> 분배기 보드의 SA(초록)/RA(노랑) LED는 별개로, 포트별 통신상태 표시등이다(프로토콜 무관).
|
||||||
|
|
||||||
|
### 3.5 댐퍼 각도 (DamperAngle)
|
||||||
|
`0x00`=0°(닫힘) ~ `0xB4`=180°(완전개방). 슬롯 미사용 시 `0xFF`(장치 없음).
|
||||||
|
|
||||||
|
### 3.6 에러코드 (ErrorCode, **u32 비트맵**)
|
||||||
|
| 비트 | 마스크 | 의미 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 0 | 0x00000001 | 필터 청소 |
|
||||||
|
| 1 | 0x00000002 | 필터 교체 |
|
||||||
|
| 2 | 0x00000004 | 소자 교체 |
|
||||||
|
| 3 | 0x00000008 | 온도센서 에러 |
|
||||||
|
| 4 | 0x00000010 | 장비보호 모드 |
|
||||||
|
| 5 | 0x00000020 | EA 팬 에러 |
|
||||||
|
| 6 | 0x00000040 | 간헐운전 모드 |
|
||||||
|
| 7 | 0x00000080 | SA 팬 에러 |
|
||||||
|
| 8~12 | 0x00000100~0x00001000 | 통합센서 에러 — 거실/방1/방2/방3/방4 |
|
||||||
|
| 16~20 | 0x00010000~0x00100000 | 통신 에러 — 거실/방1/방2/방3/방4 |
|
||||||
|
|
||||||
|
### 3.7 실 번호 (Room)
|
||||||
|
`1`=거실, `2`=방1, `3`=방2, `4`=방3, `5`=방4. (하위버스 MUX 채널 ch1~ch5 와 1:1)
|
||||||
|
|
||||||
|
### 3.8 부가모드 (AddMode, 비트맵)
|
||||||
|
DL 사양 시나리오 모드. 룸컨에서 토글하며 운전모드(3.2)에 덧씌워진다. 여러 비트 동시 가능.
|
||||||
|
|
||||||
|
| 비트 | 마스크 | 의미 | 비고 (사양서 8~9P) |
|
||||||
|
|------|--------|------|--------------------|
|
||||||
|
| 0 | 0x01 | 스마트수면 | 자동·풍량1단 고정, 1시간마다 CO2 기준 댐퍼 |
|
||||||
|
| 1 | 0x02 | 쾌적조리 | 렌지후드 연동(아래 3.9), 환기·3단 급기 |
|
||||||
|
| 2 | 0x04 | 안심회복 | 침실1 음압, 환기·2단 |
|
||||||
|
|
||||||
|
> `0x00` = 부가모드 없음. (회복모드 중 수면모드 불가 등 배타조건은 ERV 로직에서 처리)
|
||||||
|
|
||||||
|
### 3.9 후드 연동 (Hood, 비트맵)
|
||||||
|
렌지후드와 전열교환기 연동. 후드 동작 시 ERV도 동작해야 하므로 ERV가 후드 상태를 감지·연동한다(HOOD-485). 쾌적조리(3.8 bit1) 활성 시 적용.
|
||||||
|
|
||||||
|
| 비트 | 마스크 | 의미 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 0 | 0x01 | 후드연동 활성화(enable) |
|
||||||
|
| 1 | 0x02 | 후드 현재 동작중(ON) |
|
||||||
|
|
||||||
|
> 예: `0x03` = 연동 ON + 후드 가동중 → ERV 환기·3단 급기. `0x00` = 연동 없음.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 상위 버스 — ERV ↔ 각실분배기
|
||||||
|
|
||||||
|
> ERV(Master)가 분배기(유일 Slave)를 폴링한다. 분배기는 하위에서 모은 전 실 데이터를 **집계해 응답**한다.
|
||||||
|
> 멀티 분배기 확장 대비로 페이로드 선두에 `nodeId`(분배기 번호, 기본 `0x01`)를 둔다.
|
||||||
|
|
||||||
|
## 4. 상위 CMD 및 PAYLOAD
|
||||||
|
|
||||||
|
| CMD | 이름 | 방향 | PAYLOAD | 설명 |
|
||||||
|
|------|------|------|---------|------|
|
||||||
|
| 0x10 | `CMD_SYSTEM` | ERV→분배기 | 4.1 | 전원/모드/풍량/예약 + 실별 댐퍼·LED 타겟 (폴링 겸용) |
|
||||||
|
| 0x12 | `POLL_SENSOR` | ERV→분배기 | nodeId(1) | 전 실 센서 집계 요청 |
|
||||||
|
| 0x14 | `CMD_CONFIG` | ERV→분배기 | 4.5 | VSP 테이블·장치개수·ID 설정/저장 |
|
||||||
|
| 0x1F | `POLL_SPEC` | ERV→분배기 | nodeId(1) | 분배기 사양/버전 요청 |
|
||||||
|
| 0x90 | `RSP_ALLROOM_STATUS` | 분배기→ERV | 4.2 | 전 실 댐퍼·RPM·LED·룸컨명령·에러 집계 (0x10 응답) |
|
||||||
|
| 0x92 | `RSP_ALLROOM_SENSOR` | 분배기→ERV | 4.3 | 전 실 통합공기질 센서 집계 (0x12 응답) |
|
||||||
|
| 0x94 | `RSP_CONFIG` | 분배기→ERV | 4.5 | 설정 에코/저장결과 (0x14 응답) |
|
||||||
|
| 0x9F | `RSP_SPEC` | 분배기→ERV | 4.6 | 분배기 사양/버전 |
|
||||||
|
|
||||||
|
### 4.1 `CMD_SYSTEM` (0x10, ERV→분배기)
|
||||||
|
|
||||||
|
**글로벌(off 0~15, 16B)** + **실별 타겟 블록(6B) × 5실**
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 1 | nodeId | 분배기 번호(0x01) |
|
||||||
|
| 1 | 1 | power | 3.1 |
|
||||||
|
| 2 | 1 | runMode | 3.2 |
|
||||||
|
| 3 | 1 | fanSpeed | 3.3 |
|
||||||
|
| 4 | 1 | addMode | 부가모드 비트맵 (3.8) |
|
||||||
|
| 5 | 1 | hood | 후드 연동 비트맵 (3.9) |
|
||||||
|
| 6 | 1 | reserveHour | 0~8시간 |
|
||||||
|
| 7 | 4 | errorCode | ERV 시스템 에러 통보 (3.6) |
|
||||||
|
| 11 | 2 | outTemp | 외기온도 ×10 (signed) |
|
||||||
|
| 13 | 2 | inTemp | 내기온도 ×10 (signed) |
|
||||||
|
| 15 | 1 | roomCount | 실 수(=5) |
|
||||||
|
|
||||||
|
실별 타겟 블록(6B, roomCount회 반복) :
|
||||||
|
|
||||||
|
| off(상대) | 크기 | 필드 | 비고 |
|
||||||
|
|-----------|------|------|------|
|
||||||
|
| +0 | 1 | roomNo | 3.7 |
|
||||||
|
| +1 | 1 | saDamper1 | SA 디퓨저1 목표각 (3.5) |
|
||||||
|
| +2 | 1 | saDamper2 | SA 디퓨저2 (거실만, 그 외 0xFF) |
|
||||||
|
| +3 | 1 | raDamper1 | RA 디퓨저1 목표각 |
|
||||||
|
| +4 | 1 | raDamper2 | RA 디퓨저2 (거실만, 그 외 0xFF) |
|
||||||
|
| +5 | 1 | ledDim | 해당 실 RA 디퓨저 조명 밝기 (3.4) |
|
||||||
|
|
||||||
|
LEN = 16 + 6×5 = **46**.
|
||||||
|
|
||||||
|
### 4.2 `RSP_ALLROOM_STATUS` (0x90, 분배기→ERV)
|
||||||
|
|
||||||
|
**글로벌(8B)** + **실별 상태 블록(17B) × 5실**
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 1 | nodeId | |
|
||||||
|
| 1 | 1 | bunbagiState | 분배기 동작/에러 요약 |
|
||||||
|
| 2 | 1 | addModeReq | 룸컨이 요청한 부가모드 비트맵 (3.8) |
|
||||||
|
| 3 | 4 | errorCode | 전 실 통신·센서 에러 집계 (3.6) |
|
||||||
|
| 7 | 1 | roomCount | =5 |
|
||||||
|
|
||||||
|
실별 상태 블록(17B) :
|
||||||
|
|
||||||
|
| off(상대) | 크기 | 필드 | 비고 |
|
||||||
|
|-----------|------|------|------|
|
||||||
|
| +0 | 1 | roomNo | |
|
||||||
|
| +1 | 1 | saDamper1 | 실제 각도 |
|
||||||
|
| +2 | 1 | saDamper2 | (없으면 0xFF) |
|
||||||
|
| +3 | 1 | raDamper1 | |
|
||||||
|
| +4 | 1 | raDamper2 | |
|
||||||
|
| +5 | 2 | saRpm | SA 팬 실측 RPM |
|
||||||
|
| +7 | 2 | raRpm | RA 팬 실측 RPM |
|
||||||
|
| +9 | 1 | ledDim | 현재 RA 조명 LED 밝기 |
|
||||||
|
| +10 | 1 | rcCmdFlags | 룸컨 명령 비트(아래 5.5 cmdFlags 동일, bit7=부가모드 변경) |
|
||||||
|
| +11 | 1 | rcPower | 룸컨 설정 전원 |
|
||||||
|
| +12 | 1 | rcRunMode | 룸컨 설정 모드 |
|
||||||
|
| +13 | 1 | rcFanSpeed | 룸컨 설정 풍량 |
|
||||||
|
| +14 | 1 | rcReserveHour | 룸컨 설정 예약 |
|
||||||
|
| +15 | 1 | rcHeaterUV | bit0=히터,bit4=UV |
|
||||||
|
| +16 | 1 | rcFilterReset | 1=필터리셋 요청 |
|
||||||
|
|
||||||
|
LEN = 8 + 17×5 = **93**.
|
||||||
|
|
||||||
|
> `rcCmdFlags` 가 0 이 아니면, 해당 실 룸컨에서 사용자 조작이 발생했다는 뜻 → ERV가 수용해 운전상태 갱신.
|
||||||
|
|
||||||
|
### 4.3 `RSP_ALLROOM_SENSOR` (0x92, 분배기→ERV)
|
||||||
|
|
||||||
|
**글로벌(2B)** + **실별 센서 블록(19B) × 5실**
|
||||||
|
|
||||||
|
| off | 크기 | 필드 |
|
||||||
|
|-----|------|------|
|
||||||
|
| 0 | 1 | nodeId |
|
||||||
|
| 1 | 1 | roomCount(=5) |
|
||||||
|
|
||||||
|
실별 센서 블록(19B) :
|
||||||
|
|
||||||
|
| off(상대) | 크기 | 필드 | 단위 |
|
||||||
|
|-----------|------|------|------|
|
||||||
|
| +0 | 1 | roomNo | |
|
||||||
|
| +1 | 2 | pm1p0 | ㎍/㎥ |
|
||||||
|
| +3 | 2 | pm2p5 | ㎍/㎥ |
|
||||||
|
| +5 | 2 | pm4p0 | ㎍/㎥ |
|
||||||
|
| +7 | 2 | pm10p0 | ㎍/㎥ |
|
||||||
|
| +9 | 2 | humidity | %RH ×10 |
|
||||||
|
| +11 | 2 | temperature | ℃ ×10 (signed) |
|
||||||
|
| +13 | 2 | voc | TVOC index |
|
||||||
|
| +15 | 2 | nox | NOx index |
|
||||||
|
| +17 | 2 | co2 | ppm |
|
||||||
|
|
||||||
|
LEN = 2 + 19×5 = **97**.
|
||||||
|
|
||||||
|
### 4.5 `CMD_CONFIG` (0x14) / `RSP_CONFIG` (0x94)
|
||||||
|
|
||||||
|
VSP 풍량 테이블·장치개수·Modbus ID 설정.
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 1 | nodeId | |
|
||||||
|
| 1 | 1 | vspSelect | 0=None,1=환기,2=공청,3=바이패스 |
|
||||||
|
| 2 | 10 | sa1,ea1 … sa5,ea5 | 단별 VSP (각 1B) |
|
||||||
|
| 12 | 2 | rpmRefMid | 중 RPM 기준 |
|
||||||
|
| 14 | 2 | rpmDeltaMid | 중 RPM 편차 |
|
||||||
|
| 16 | 2 | rpmRefHigh | 강 RPM 기준 |
|
||||||
|
| 18 | 2 | rpmDeltaHigh | 강 RPM 편차 |
|
||||||
|
| 20 | 1 | roomconNum | 룸컨 수(1~5) |
|
||||||
|
| 21 | 1 | saDiffuserNum | SA 디퓨저 수(2~6) |
|
||||||
|
| 22 | 1 | raDiffuserNum | RA 디퓨저 수(2~6) |
|
||||||
|
| 23 | 1 | modbusId | 외부 홈넷 연동 ID |
|
||||||
|
| 24 | 1 | save | 1=EEPROM 저장 |
|
||||||
|
|
||||||
|
LEN = 25. `RSP_CONFIG` 는 동일 레이아웃 에코 + save 결과.
|
||||||
|
|
||||||
|
### 4.6 `RSP_SPEC` (0x9F)
|
||||||
|
| off | 크기 | 필드 |
|
||||||
|
|-----|------|------|
|
||||||
|
| 0 | 1 | nodeId |
|
||||||
|
| 1 | 2 | version (예 0x0117=Ver1.23) |
|
||||||
|
| 3 | 1 | deviceType |
|
||||||
|
| 4 | 1 | capability (bit0 히터/bit1 UV/bit2 후드연동) |
|
||||||
|
|
||||||
|
LEN = 5.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 하위 버스 — 각실분배기 ↔ 디퓨저 · 룸컨
|
||||||
|
|
||||||
|
> 분배기(로컬 Master)가 **MUX로 실 채널(ch1~ch5)을 선택**한 뒤 그 실의 SA/RA 디퓨저·룸컨을 폴링한다.
|
||||||
|
> 조명 LED(RA 디퓨저)는 `LCMD_DIFFUSER` 의 `ledDim` 으로 제어한다(3.4). 분배기 보드의 74HC595 LED는 별개(포트별 통신상태 표시, 진단용).
|
||||||
|
|
||||||
|
## 5. 하위 CMD 및 PAYLOAD
|
||||||
|
|
||||||
|
PAYLOAD 선두 3바이트는 장치 주소 `[DEV][ROOM][IDX]`.
|
||||||
|
- DEV : `0x10`=SA 디퓨저, `0x20`=RA 디퓨저, `0x30`=룸컨
|
||||||
|
- ROOM : 3.7 (채널과 1:1) / IDX : 같은 실·종류 일련번호(거실 SA·RA는 1,2)
|
||||||
|
|
||||||
|
| CMD | 이름 | 방향 | PAYLOAD | 설명 |
|
||||||
|
|------|------|------|---------|------|
|
||||||
|
| 0x20 | `LCMD_DIFFUSER` | 분배기→디퓨저 | 5.1 | 디퓨저 댐퍼/풍량 제어 + 폴링 |
|
||||||
|
| 0x21 | `LPOLL_ROOMCON` | 분배기→룸컨 | 5.4 | 룸컨 폴링 + ERV 상태 표시데이터 전달 |
|
||||||
|
| 0xA0 | `LRSP_DIFFUSER` | 디퓨저→분배기 | 5.2 | 댐퍼각/RPM/에러/버전 (0x20 응답) |
|
||||||
|
| 0xA1 | `LRSP_DIFFUSER_SENSOR` | 디퓨저→분배기 | 5.3 | 통합공기질 센서 (방별, 센서 부착 디퓨저) |
|
||||||
|
| 0xB0 | `LRSP_ROOMCON` | 룸컨→분배기 | 5.5 | 룸컨 사용자 명령 (0x21 응답) |
|
||||||
|
|
||||||
|
### 5.1 `LCMD_DIFFUSER` (0x20, 분배기→디퓨저)
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | DEV=0x10(SA)/0x20(RA) |
|
||||||
|
| 3 | 1 | power | 3.1 |
|
||||||
|
| 4 | 1 | runMode | 3.2 |
|
||||||
|
| 5 | 1 | fanSpeed | 3.3 |
|
||||||
|
| 6 | 1 | ledDim | RA 디퓨저 조명 밝기 (3.4) — **RA(0x20)만 유효, SA는 0** |
|
||||||
|
| 7 | 1 | dmpAngle | 목표 댐퍼각 (3.5) |
|
||||||
|
| 8 | 1 | dmpReset | 1=댐퍼 초기화 |
|
||||||
|
|
||||||
|
LEN = 9.
|
||||||
|
|
||||||
|
### 5.2 `LRSP_DIFFUSER` (0xA0, 디퓨저→분배기)
|
||||||
|
| off | 크기 | 필드 |
|
||||||
|
|-----|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX (에코) |
|
||||||
|
| 3 | 1 | power |
|
||||||
|
| 4 | 1 | runMode |
|
||||||
|
| 5 | 1 | fanSpeed |
|
||||||
|
| 6 | 1 | ledDim (현재 RA 조명 밝기, SA는 0) |
|
||||||
|
| 7 | 1 | dmpAngle (현재각) |
|
||||||
|
| 8 | 2 | rpm |
|
||||||
|
| 10 | 4 | errorCode (3.6) |
|
||||||
|
| 14 | 2 | version |
|
||||||
|
|
||||||
|
LEN = 16.
|
||||||
|
|
||||||
|
### 5.3 `LRSP_DIFFUSER_SENSOR` (0xA1, 디퓨저→분배기)
|
||||||
|
| off | 크기 | 필드 | 단위 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | 센서 부착 디퓨저(통상 RA) |
|
||||||
|
| 3 | 2 | pm1p0 | ㎍/㎥ |
|
||||||
|
| 5 | 2 | pm2p5 | ㎍/㎥ |
|
||||||
|
| 7 | 2 | pm4p0 | ㎍/㎥ |
|
||||||
|
| 9 | 2 | pm10p0 | ㎍/㎥ |
|
||||||
|
| 11 | 2 | humidity | %RH ×10 |
|
||||||
|
| 13 | 2 | temperature | ℃ ×10 |
|
||||||
|
| 15 | 2 | voc | TVOC |
|
||||||
|
| 17 | 2 | nox | NOx |
|
||||||
|
| 19 | 2 | co2 | ppm |
|
||||||
|
| 21 | 4 | errorCode | 3.6 |
|
||||||
|
|
||||||
|
LEN = 25.
|
||||||
|
|
||||||
|
### 5.4 `LPOLL_ROOMCON` (0x21, 분배기→룸컨)
|
||||||
|
분배기가 ERV로부터 받은 동작상태를 룸컨에 전달(표시용) + 룸컨 명령 회수.
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | DEV=0x30 |
|
||||||
|
| 3 | 1 | power | ERV 현재 전원 |
|
||||||
|
| 4 | 1 | runMode | ERV 현재 모드 |
|
||||||
|
| 5 | 1 | fanSpeed | ERV 현재 풍량 |
|
||||||
|
| 6 | 1 | addMode | 현재 부가모드 비트맵 (3.8) |
|
||||||
|
| 7 | 1 | hood | 후드 연동 상태 (3.9) |
|
||||||
|
| 8 | 1 | reserveRemain | 예약 잔여(h) |
|
||||||
|
| 9 | 4 | errorCode | 3.6 |
|
||||||
|
| 13 | 2 | outTemp | ×10 |
|
||||||
|
| 15 | 2 | inTemp | ×10 |
|
||||||
|
| 17 | 1 | ackFlags | 직전 룸컨 명령 수용 비트 |
|
||||||
|
|
||||||
|
LEN = 18.
|
||||||
|
|
||||||
|
### 5.5 `LRSP_ROOMCON` (0xB0, 룸컨→분배기)
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | DEV=0x30 |
|
||||||
|
| 3 | 1 | cmdFlags | bit0 power,1 runMode,2 fanSpeed,3 reserveHour,4 heaterUV,5 filterReset,6 ledDim,**7 addMode** |
|
||||||
|
| 4 | 1 | power | 3.1 |
|
||||||
|
| 5 | 1 | runMode | 3.2 |
|
||||||
|
| 6 | 1 | fanSpeed | 3.3 |
|
||||||
|
| 7 | 1 | reserveHour | 0~8 |
|
||||||
|
| 8 | 1 | heaterUV | bit0 히터,bit4 UV |
|
||||||
|
| 9 | 1 | filterReset | 1=리셋 |
|
||||||
|
| 10 | 1 | addMode | 부가모드 토글 비트맵 (3.8) |
|
||||||
|
| 11 | 2 | version | |
|
||||||
|
|
||||||
|
LEN = 13.
|
||||||
|
> 룸컨이 보낸 명령은 분배기가 모아 상위 `RSP_ALLROOM_STATUS` 의 `rc*` 필드로 ERV에 전달 → ERV가 판단 후 다시 하달.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 동작 시나리오 & 폴링 타이밍
|
||||||
|
|
||||||
|
**두 버스가 독립적으로 동작**한다.
|
||||||
|
|
||||||
|
**하위 버스 (분배기 로컬 루프)** — 분배기가 ch1~ch5를 MUX로 돌며 각 실의 SA/RA 디퓨저·룸컨 폴링. 케이블이 짧고 전용 채널이라 빠르게 순환하며 전 실 상태를 캐싱.
|
||||||
|
|
||||||
|
**상위 버스 (ERV 루프)** — ERV는 **분배기 1대만** 주기적으로 폴링:
|
||||||
|
1. ERV → 분배기 `CMD_SYSTEM(0x10)` (전원/모드/풍량 + 실별 댐퍼·LED 타겟) → 분배기 `RSP_ALLROOM_STATUS(0x90)` (전 실 댐퍼·RPM·LED·룸컨명령·에러)
|
||||||
|
2. ERV → 분배기 `POLL_SENSOR(0x12)` → 분배기 `RSP_ALLROOM_SENSOR(0x92)` (전 실 센서)
|
||||||
|
3. ERV가 센서로 **부하점수·집중/분산 계산**(사양서 10~11P) → 다음 `CMD_SYSTEM` 의 실별 댐퍼/LED/풍량 타겟에 반영
|
||||||
|
4. 룸컨 조작은 `RSP_ALLROOM_STATUS.rcCmdFlags`(+`addModeReq`) 로 ERV에 보고 → ERV 수용 → 다음 `CMD_SYSTEM` 으로 반영
|
||||||
|
5. **부가모드(3.8)** : 룸컨에서 스마트수면/쾌적조리/안심회복 토글 → ERV가 해당 시나리오 로직 수행(댐퍼·풍량 타겟 조정) → `CMD_SYSTEM.addMode` 로 현재 상태 회신(룸컨 표시)
|
||||||
|
6. **후드 연동(3.9)** : ERV가 HOOD-485로 후드 ON 감지 → (쾌적조리 활성 시) ERV 환기·3단 급기 동작 → `CMD_SYSTEM.hood` 로 후드 상태를 분배기·룸컨에 전달. 후드 OFF 시 이전 모드 복귀
|
||||||
|
7. 통신 단절 : 분배기가 실별 통신에러 비트(3.6)를 set 해 ERV에 보고, 재연결 시 clear
|
||||||
|
|
||||||
|
### 폴링 시간 (1회 = 300ms 가정)
|
||||||
|
|
||||||
|
| 버스 | 폴링 대상 | 1주기 |
|
||||||
|
|------|-----------|-------|
|
||||||
|
| **상위 (ERV↔분배기)** | 2회 (CMD_SYSTEM + POLL_SENSOR) | **600 ms** |
|
||||||
|
| 하위 (분배기↔실) | 17회(SA6+RA6+룸컨5) — **로컬에서 병렬 진행** | 분배기 내부에서 순환(ERV와 무관) |
|
||||||
|
|
||||||
|
> Rev 2.0 은 ERV가 17대를 직접 폴링 → 1주기 **5.1초**.
|
||||||
|
> Rev 3.0 은 ERV 입장에서 **0.6초**면 전 실 상태를 받는다. 하위 루프는 분배기가 별도로 빠르게 순환하므로 ERV 주기와 분리된다. → **갱신 약 8배 빨라지고 ERV 부하 격감.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 펌웨어 반영 메모
|
||||||
|
|
||||||
|
- **분배기 펌웨어(Nano100)** : 신규. UART1(`SA485`, PB.4/PB.5)=하위 로컬 마스터, UART2(`M485`, PA.8/PA.9)=상위 슬레이브. MUX(SA_MUX_A/B/C/EN)로 채널 선택, DIR(SA485_DIR_01~05) 채널별. 74HC595(LED_DS/SCK/LCK)는 **포트별 SA/RA 통신상태 표시 LED** 구동(진단용). 방 조명 LED는 RA 디퓨저가 자체 점등하므로 ledDim 명령만 중계.
|
||||||
|
- **ERV 펌웨어** : `My_Uart.c` 의 17대 직접 폴링 → **분배기 1대 상위 프로토콜**로 교체. 자동로직(부하점수·집중/분산)은 ERV에 유지.
|
||||||
|
- **`My_RJ2.c`(DL 룸컨 232)는 변경하지 않는다.**
|
||||||
|
- CRC : 기존 `CRC16()`(MODBUS) 공유, 빅엔디안(Hi→Lo).
|
||||||
|
- 에러코드 `uint32_t` (방4 비트 수용).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 송수신 예제
|
||||||
|
|
||||||
|
> 모든 프레임 CRC는 **STX 제외 `CMD~PAYLOAD`** 구간 CRC-16/MODBUS(빅엔디안)의 실제 계산값.
|
||||||
|
> 예시 : runMode=`0x02`(자동), fan=`2`(중), addMode=`0x02`(쾌적조리), hood=`0x03`(연동ON+가동중), 댐퍼 `0xB4`=열림/`0x00`=닫힘/`0xFF`=없음, 온·습도 ×10. 폴링 1회=300ms.
|
||||||
|
|
||||||
|
### 8-A. 상위 버스 (ERV ↔ 분배기)
|
||||||
|
|
||||||
|
**① ERV → 분배기 : 전원ON·자동·풍량2·쾌적조리·후드가동 + 실별 댐퍼/LED 타겟** — `CMD_SYSTEM(0x10)` *(t=0~300ms)*
|
||||||
|
```
|
||||||
|
[TX] AA 10 2E | 01 01 02 02 02 03 00 00000000 0096 00DC 05 | <실1..5 타겟 6B> | AA 01
|
||||||
|
CMD LEN node pw md fan add hood rsv err 외15℃ 내22℃ rooms=5
|
||||||
|
↑쾌적조리 ↑후드연동ON+가동
|
||||||
|
실1(거실): 01 B4 B4 B4 B4 05 (SA1·SA2·RA1·RA2 열림, RA조명 LED 5)
|
||||||
|
실2(방1) : 02 B4 FF B4 FF 04
|
||||||
|
실3(방2) : 03 00 FF 00 FF 00 (댐퍼 닫힘, LED OFF)
|
||||||
|
실4(방3) : 04 B4 FF B4 FF 06
|
||||||
|
실5(방4) : 05 B4 FF B4 FF 03
|
||||||
|
```
|
||||||
|
|
||||||
|
**② 분배기 → ERV : 전 실 상태 집계** — `RSP_ALLROOM_STATUS(0x90)`
|
||||||
|
```
|
||||||
|
[RX] AA 90 5D | 01 00 02 00000000 05 | <실1..5 상태 17B> | C5 31
|
||||||
|
node bunSt addModeReq=02 err rooms=5
|
||||||
|
실1(거실): 01 B4 B4 B4 B4 0352 0334 05 00 01 02 02 00 00 00
|
||||||
|
roomNo SA1 SA2 RA1 RA2 saRpm=850 raRpm=820 ledDim | rcFlags pw md fan rsv hu fr
|
||||||
|
실2(방1) : 02 B4 FF B4 FF 0348 032A 04 00 01 02 02 00 00 00
|
||||||
|
실3(방2) : 03 00 FF 00 FF 0000 0000 00 00 01 02 02 00 00 00
|
||||||
|
실4(방3) : 04 B4 FF B4 FF 034D 032F 06 00 01 02 02 00 00 00
|
||||||
|
실5(방4) : 05 B4 FF B4 FF 0350 0332 03 00 01 02 02 00 00 00
|
||||||
|
```
|
||||||
|
|
||||||
|
**③ ERV → 분배기 : 센서 요청 / 분배기 응답** — `POLL_SENSOR(0x12)` → `RSP_ALLROOM_SENSOR(0x92)` *(t=300~600ms)*
|
||||||
|
```
|
||||||
|
[TX] AA 12 01 | 01 | <CRC>
|
||||||
|
[RX] AA 92 61 | 01 05 | <실1..5 센서 19B> | DC 05
|
||||||
|
실1(거실): 01 0005 0008 0009 000B 01C2 00DC 0064 0001 028A
|
||||||
|
roomNo pm1 pm2.5 pm4 pm10 습45.0% 온22.0℃ voc100 nox1 co2=650
|
||||||
|
실2(방1) : 02 0007 000C 000D 0012 01C4 00DD 0082 0001 02D0 (co2=720)
|
||||||
|
실3(방2) : 03 0004 0006 0007 0009 01C0 00DC 005A 0001 0262 (co2=610)
|
||||||
|
실4(방3) : 04 0009 0014 0016 001C 01C7 00DB 00A0 0002 032A (co2=810)
|
||||||
|
실5(방4) : 05 0005 0007 0008 000A 01C1 00DC 005F 0001 0280 (co2=640)
|
||||||
|
```
|
||||||
|
> ERV는 ②③으로 **전 실 상태+센서를 0.6초만에** 확보 → 부하점수·집중/분산 계산 → 다음 `CMD_SYSTEM` 타겟 갱신.
|
||||||
|
|
||||||
|
### 8-B. 하위 버스 (분배기 ↔ 디퓨저/룸컨) — 분배기 로컬 루프
|
||||||
|
|
||||||
|
**거실 RA1 디퓨저 제어/폴링** — `LCMD_DIFFUSER(0x20)` → `LRSP_DIFFUSER(0xA0)` (RA = 조명 LED 보유)
|
||||||
|
```
|
||||||
|
[TX] AA 20 09 | 20 01 01 | 01 02 02 05 B4 00 | 6E 2B (RA,거실,1 / power mode fan led=5 dmp=B4 reset=0)
|
||||||
|
[RX] AA A0 10 | 20 01 01 | 01 02 02 05 B4 0352 00000000 0117 | ... (led현재5, dmp B4, rpm850, err0, ver1.23)
|
||||||
|
```
|
||||||
|
> SA 디퓨저는 LED가 없으므로 `ledDim=0`. 예) 거실 SA1 TX: `AA 20 09 10 01 01 01 02 02 00 B4 00 6E 6F` → RX `AA A0 10 10 01 01 01 02 02 00 B4 03 52 00 00 00 00 01 17 B9 17`.
|
||||||
|
|
||||||
|
**거실 RA1 디퓨저 센서** — `LRSP_DIFFUSER_SENSOR(0xA1)`
|
||||||
|
```
|
||||||
|
[RX] AA A1 19 20 01 01 00 05 00 08 00 09 00 0B 01 C2 00 DC 00 64 00 01 02 8A 00 00 00 00 58 AE
|
||||||
|
(RA,거실,1) pm1=5 pm2.5=8 pm4=9 pm10=11 습45.0% 온22.0℃ voc100 nox1 co2=650 err0
|
||||||
|
```
|
||||||
|
|
||||||
|
**거실 룸컨 폴링** — `LPOLL_ROOMCON(0x21)` → `LRSP_ROOMCON(0xB0)`
|
||||||
|
```
|
||||||
|
[TX] AA 21 12 30 01 01 01 02 02 02 03 00 00 00 00 00 00 96 00 DC 00 92 FA
|
||||||
|
(ERV상태 전달: 자동·풍량2·쾌적조리(02)·후드연동ON+가동(03))
|
||||||
|
[RX] AA B0 0D 30 01 01 80 01 02 02 00 00 00 02 01 10 34 4D
|
||||||
|
cmdFlags=80(부가모드 변경) ... addMode=02(쾌적조리 토글) ver0x0110
|
||||||
|
```
|
||||||
|
> 룸컨이 부가모드(쾌적조리)를 토글하면 `cmdFlags` bit7=1·`addMode`=0x02 로 보고 → 분배기가 상위 `RSP_ALLROOM_STATUS.addModeReq`·`rcCmdFlags`에 실어 ERV에 전달 → ERV가 후드연동 운전 수행.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 본 문서는 2-Tier 재정의 초안(Rev 3.0)이다. 분배기/디퓨저/룸컨 펌웨어 담당과 CMD 코드·필드 세부값을 상호 합의하여 확정한다.
|
||||||
@@ -0,0 +1,489 @@
|
|||||||
|
# 각실제어 내부 통신 프로토콜 (Rev 2.0, CMD 기반)
|
||||||
|
|
||||||
|
> 휴벤ECO(ERV) ↔ 각실분배기 ↔ 디퓨저 ↔ 룸컨 **내부 RS-485 통신** 규격
|
||||||
|
>
|
||||||
|
> 본 문서는 `Protocol/수정_Each_Room_Jushin_protocol_RS485_Rev1.2` (주신전자) 와
|
||||||
|
> 펌웨어 `program/User/My_Uart.c` (각실분배기/디퓨저 폴링) 구현을 기반으로,
|
||||||
|
> **CMD 기반으로 단순화·확장**하여 새로 정의한 규격이다.
|
||||||
|
> (DL 룸컨 232 프로토콜 `My_RJ2.c` 는 DL 사양으로 본 규격과 무관 — 변경하지 않는다.)
|
||||||
|
|
||||||
|
- **물리계층** : RS-485, **115200 bps, 8 Data, None Parity, 1 Stop (N81)**
|
||||||
|
- **토폴로지** : 1 Master(메인보드) ↔ N Slave(디퓨저/룸컨), 메인보드 폴링 방식
|
||||||
|
- **대상 시스템** : 사양서 5페이지(힘펠 배선도) 구성 = **거실 + 방1~4 (5실)**, 각 실 룸컨
|
||||||
|
- 디퓨저 : 거실 SA 2·RA 2, 방1~4 각 SA 1·RA 1 → **SA 디퓨저 6 + RA 디퓨저 6**, 룸컨 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 무엇이 바뀌었나 (Rev 1.2 → Rev 2.0)
|
||||||
|
|
||||||
|
| 구분 | 기존 (Rev 1.2) | 개선 (Rev 2.0) |
|
||||||
|
|------|----------------|----------------|
|
||||||
|
| 패킷 식별 | `VSP Mode` 값(0x00/0x11/0x12/0x01~03/0x10)에 따라 **같은 byte 위치의 의미가 계속 바뀜** | **CMD 1바이트로 패킷 종류 고정** → 한 패킷은 항상 같은 레이아웃 |
|
||||||
|
| 거실 공기질 | 별도 모드 0x11 / 0x12 로 방을 쪼개서 전송 | **방별 센서 패킷 1종으로 통합**, ROOM 필드로 구분 |
|
||||||
|
| 길이 | 29/39 byte 가변, 위치로 추정 | **LEN 필드 명시** → 파싱 단순 |
|
||||||
|
| 장치 주소 | ID1(종류)+ID2(번호) 혼용, 거실=5 등 불규칙 | **DEV / ROOM / IDX 3필드**로 직관적 주소화 |
|
||||||
|
| 에러코드 | **16비트 전부 소진** → 방4 추가 불가 (한계) | **32비트로 확장** → 방4 + 향후 여유 |
|
||||||
|
| 확장성 | 4실 고정 | **5실(거실+방1~4) + 룸컨 5 / SA·RA 디퓨저 각 6** 명시 |
|
||||||
|
| 역할 정의 | 명령/상태 방향이 패킷마다 섞여 모호 | **버스 마스터=ERV(폴링) / 명령 주체=룸컨 / 실행=ERV→디퓨저** 로 명확화 |
|
||||||
|
|
||||||
|
> **제어 권한 정리** : 사용자는 룸컨에서 전원·모드·풍량·예약·필터리셋과 VSP 풍량 테이블을 설정한다.
|
||||||
|
> 룸컨은 버스상 Slave이므로 이 명령들을 ERV 폴링에 대한 응답(`CMD_ROOMCON`/`CMD_VSP_SET`)으로 올린다.
|
||||||
|
> ERV는 명령을 받아 분배기를 통해 디퓨저 댐퍼·풍량·LED를 제어하고, 센서값을 읽어 룸컨에 전달한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 공통 프레임
|
||||||
|
|
||||||
|
```
|
||||||
|
+------+------+------+------------------+--------+--------+
|
||||||
|
| STX | CMD | LEN | PAYLOAD[LEN] | CRC_H | CRC_L |
|
||||||
|
+------+------+------+------------------+--------+--------+
|
||||||
|
0xAA 1B 1B LEN bytes 16-bit CRC
|
||||||
|
```
|
||||||
|
|
||||||
|
| 필드 | 크기 | 설명 |
|
||||||
|
|---------|------|------|
|
||||||
|
| STX | 1 | 고정 `0xAA` |
|
||||||
|
| CMD | 1 | 명령/응답 코드 (2장) |
|
||||||
|
| LEN | 1 | PAYLOAD 바이트 수 (0~255) |
|
||||||
|
| PAYLOAD | LEN | CMD 별 데이터 (3·4장) |
|
||||||
|
| CRC | 2 | **CRC-16/MODBUS** (poly 0xA001, init 0xFFFF), **CMD~PAYLOAD** 까지, **빅엔디안(CRC_H 먼저)** |
|
||||||
|
|
||||||
|
- 모든 멀티바이트 수치는 **빅엔디안(상위 바이트 먼저)**. (CRC 포함, 펌웨어 `My_Uart.c` 관례 유지)
|
||||||
|
- 프레임 구분 : `STX(0xAA)` 탐색 → `LEN` 으로 길이 확보 → `CRC` 검증.
|
||||||
|
- 수신 타임아웃(예: 50ms) 내 미완성 프레임은 폐기하고 STX 재탐색.
|
||||||
|
|
||||||
|
> **CRC 계산** : `My_Uart.c` 의 `CRC16()` (MODBUS 룩업테이블) 동일. 결과 16비트를 `CRC_H = (crc>>8)`, `CRC_L = (crc&0xFF)` 순으로 전송.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 명령 코드 (CMD)
|
||||||
|
|
||||||
|
**버스 레벨과 제어 권한을 구분해서 이해해야 한다.**
|
||||||
|
|
||||||
|
- **버스 마스터 = ERV(메인보드)** : 모든 통신을 ERV가 폴링한다. 룸컨/디퓨저는 폴링을 받아야만 송신하는 Slave.
|
||||||
|
- **제어 권한(명령 발생) = 룸컨** : 사용자가 룸컨에서 전원/모드/풍량/예약/필터리셋, **VSP 풍량 테이블 설정·저장**을 한다.
|
||||||
|
룸컨은 Slave이므로 이 명령들을 ERV 폴링에 대한 **응답에 실어** ERV로 보낸다 (룸컨 → ERV).
|
||||||
|
- **실행 = ERV** : 룸컨 명령을 받아 분배기를 통해 **디퓨저 댐퍼 개폐·풍량·LED를 제어**하고, **센서값을 읽어 룸컨에 전달**한다.
|
||||||
|
|
||||||
|
즉 데이터 흐름은: **룸컨(명령) → ERV(실행) → 디퓨저(댐퍼/센서) → ERV(센서수집) → 룸컨(표시)**.
|
||||||
|
|
||||||
|
CMD 상위 비트로 송신 방향을 구분한다. **0x10~0x7F = ERV(Master) 송신**, **0x90~0xFF = Slave 송신(응답)**.
|
||||||
|
|
||||||
|
### 2.1 ERV(메인보드, Master) 송신
|
||||||
|
|
||||||
|
| CMD | 이름 | 대상 | PAYLOAD | 설명 |
|
||||||
|
|------|----------------------|--------|---------|------|
|
||||||
|
| 0x10 | `CMD_DIFFUSER_CTRL` | 디퓨저 | 4.1 | **ERV가 디퓨저 제어**(댐퍼각/풍량/LED/리셋) + 폴링. ERV가 제어 주체 |
|
||||||
|
| 0x20 | `POLL_ROOMCON` | 룸컨 | 4.3 | ERV가 룸컨 폴링 + **표시용 데이터 전달**(ERV 동작상태/해당 실 공기질/에러/온도). 명령 수용 결과 echo |
|
||||||
|
| 0x30 | `RSP_VSP_STATUS` | 룸컨 | 4.5 | 룸컨 요청에 대한 **현재 VSP 풍량 테이블 값 회신**(룸컨 표시용) |
|
||||||
|
| 0x40 | `POLL_SPEC` | 공통 | 없음 | 장치 사양/버전 요청 |
|
||||||
|
|
||||||
|
### 2.2 Slave(룸컨/디퓨저) 송신 = 명령/상태 보고
|
||||||
|
|
||||||
|
| CMD | 이름 | 송신자 | PAYLOAD | 설명 |
|
||||||
|
|------|------------------------|--------|---------|------|
|
||||||
|
| 0xA0 | `CMD_ROOMCON` | 룸컨 | 4.4 | **룸컨이 사용자 명령 전달**: 전원/모드/풍량/예약/히터·UV/필터리셋 (0x20 응답) |
|
||||||
|
| 0xB0 | `CMD_VSP_SET` | 룸컨 | 4.6 | **룸컨이 VSP 풍량 테이블 설정·저장** + 장치 개수 설정 (0x20 응답, VSP 세팅모드) |
|
||||||
|
| 0x90 | `RSP_DIFFUSER_STATUS` | 디퓨저 | 4.2 | 디퓨저 댐퍼각/공기질/RPM/에러/버전 (0x10 응답) |
|
||||||
|
| 0x91 | `RSP_DIFFUSER_SENSOR` | 디퓨저 | 4.7 | 통합공기질 센서값(PM/온습도/VOC/NOx/CO2) (방별) |
|
||||||
|
| 0xC0 | `RSP_SPEC` | 공통 | 4.8 | 사양/버전 (0x40 응답) |
|
||||||
|
|
||||||
|
> 모든 패킷은 대상 장치 주소(DEV/ROOM/IDX)를 선두에 싣고, 응답은 이를 그대로 에코한다.
|
||||||
|
> `CMD_ROOMCON`(0xA0)·`CMD_VSP_SET`(0xB0)은 이름은 "CMD"지만 버스상으로는 **룸컨이 ERV 폴링(0x20)에 응답하는 형태**로 전송된다. (룸컨이 명령 주체이기 때문)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 공통 값 정의
|
||||||
|
|
||||||
|
### 3.1 장치 주소 (DEV / ROOM / IDX)
|
||||||
|
|
||||||
|
각 PAYLOAD 선두 3바이트는 항상 장치 주소다.
|
||||||
|
|
||||||
|
| 필드 | 크기 | 값 | 의미 |
|
||||||
|
|------|------|----|------|
|
||||||
|
| DEV | 1 | `0x10`=SA 디퓨저, `0x20`=RA 디퓨저, `0x30`=룸컨 | 장치 종류 |
|
||||||
|
| ROOM | 1 | `1`=거실, `2`=방1, `3`=방2, `4`=방3, `5`=방4 | 실 번호 |
|
||||||
|
| IDX | 1 | `1`~ | 같은 실·같은 종류 장치의 일련번호 (거실 SA·RA는 `1`,`2`) |
|
||||||
|
|
||||||
|
**5실 디바이스 맵 (힘펠 배선도 기준 — SA 6 / RA 6 / 룸컨 5)**
|
||||||
|
|
||||||
|
| 실 | ROOM | SA 디퓨저 | RA 디퓨저 | 룸컨 |
|
||||||
|
|----|------|-----------|-----------|------|
|
||||||
|
| 거실 | 1 | (0x10,1,1) 거실급기1 · (0x10,1,2) 거실급기2 | (0x20,1,1) 거실배기1 · (0x20,1,2) 거실배기2 | (0x30,1,1) |
|
||||||
|
| 방1 | 2 | (0x10,2,1) | (0x20,2,1) | (0x30,2,1) |
|
||||||
|
| 방2 | 3 | (0x10,3,1) | (0x20,3,1) | (0x30,3,1) |
|
||||||
|
| 방3 | 4 | (0x10,4,1) | (0x20,4,1) | (0x30,4,1) |
|
||||||
|
| 방4 | 5 | (0x10,5,1) | (0x20,5,1) | (0x30,5,1) |
|
||||||
|
|
||||||
|
> 방1~4 구성은 모두 동일(SA 1 · RA 1 · 룸컨 1). 거실만 SA·RA 각 2대.
|
||||||
|
|
||||||
|
### 3.2 전원 (Power)
|
||||||
|
|
||||||
|
| 값 | 의미 |
|
||||||
|
|----|------|
|
||||||
|
| 0x00 | OFF |
|
||||||
|
| 0x01 | ON |
|
||||||
|
|
||||||
|
### 3.3 운전모드 (RunMode)
|
||||||
|
|
||||||
|
| 값 | 의미 | 펌웨어 매핑 |
|
||||||
|
|----|------|-------------|
|
||||||
|
| 0x01 | 수동(환기) | MODE_VENTILATION |
|
||||||
|
| 0x02 | 자동 | MODE_AUTO |
|
||||||
|
| 0x04 | 바이패스 | MODE_BYPASS |
|
||||||
|
| 0x08 | 공기청정 | MODE_AIRCLEAN |
|
||||||
|
|
||||||
|
### 3.4 풍량 (FanSpeed)
|
||||||
|
|
||||||
|
| 값 | 의미 |
|
||||||
|
|----|------|
|
||||||
|
| 0x00 | OFF/정지 |
|
||||||
|
| 0x01 | 1단 (약) |
|
||||||
|
| 0x02 | 2단 (중) |
|
||||||
|
| 0x03 | 3단 (강) |
|
||||||
|
| 0x04 | 4단 (터보) |
|
||||||
|
|
||||||
|
> 자동/공청 모드에서는 메인보드 로직(부하점수)이 단수를 결정하므로 디퓨저로 보내는 풍량은 계산 결과값.
|
||||||
|
|
||||||
|
### 3.5 LED 밝기 (LedDim)
|
||||||
|
|
||||||
|
| 값 | 의미 |
|
||||||
|
|----|------|
|
||||||
|
| 0x00 | OFF |
|
||||||
|
| 0x01~0x0A | 1~10단 (10단 최대) |
|
||||||
|
|
||||||
|
> 월패드/스마트스위치 UI는 0~9 단계. 0=OFF 포함 시 0~10으로 매핑.
|
||||||
|
|
||||||
|
### 3.6 댐퍼 각도 (DamperAngle)
|
||||||
|
|
||||||
|
| 값 | 의미 |
|
||||||
|
|----|------|
|
||||||
|
| 0x00 | 0° (닫힘) |
|
||||||
|
| 0x00~0xB4 | 0~180° (0xB4 = 180° 완전개방) |
|
||||||
|
|
||||||
|
### 3.7 에러코드 (ErrorCode, **u32 비트맵** — 확장)
|
||||||
|
|
||||||
|
| 비트 | 마스크 | 의미 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 0 | 0x00000001 | 필터 청소 |
|
||||||
|
| 1 | 0x00000002 | 필터 교체 |
|
||||||
|
| 2 | 0x00000004 | 소자 교체 |
|
||||||
|
| 3 | 0x00000008 | 온도센서 에러 |
|
||||||
|
| 4 | 0x00000010 | 장비보호 모드 |
|
||||||
|
| 5 | 0x00000020 | EA 팬 에러 |
|
||||||
|
| 6 | 0x00000040 | 간헐운전 모드 |
|
||||||
|
| 7 | 0x00000080 | SA 팬 에러 |
|
||||||
|
| 8 | 0x00000100 | 통합센서 에러 — 거실 |
|
||||||
|
| 9 | 0x00000200 | 통합센서 에러 — 방1 |
|
||||||
|
| 10 | 0x00000400 | 통합센서 에러 — 방2 |
|
||||||
|
| 11 | 0x00000800 | 통합센서 에러 — 방3 |
|
||||||
|
| 12 | 0x00001000 | **통합센서 에러 — 방4 (신규)** |
|
||||||
|
| 16 | 0x00010000 | 통신 에러 — 거실 |
|
||||||
|
| 17 | 0x00020000 | 통신 에러 — 방1 |
|
||||||
|
| 18 | 0x00040000 | 통신 에러 — 방2 |
|
||||||
|
| 19 | 0x00080000 | 통신 에러 — 방3 |
|
||||||
|
| 20 | 0x00100000 | **통신 에러 — 방4 (신규)** |
|
||||||
|
|
||||||
|
> 비트 13~15, 21~31 은 향후 확장용 예약.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. PAYLOAD 상세
|
||||||
|
|
||||||
|
> 모든 PAYLOAD는 `[DEV][ROOM][IDX]` 3바이트로 시작. (3.1)
|
||||||
|
> `u16` 은 빅엔디안. 아래 off 는 PAYLOAD 내 상대 오프셋.
|
||||||
|
|
||||||
|
### 4.1 `CMD_DIFFUSER_CTRL` (0x10, Master→디퓨저)
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | DEV=0x10(SA) 또는 0x20(RA) |
|
||||||
|
| 3 | 1 | power | 3.2 |
|
||||||
|
| 4 | 1 | runMode | 3.3 |
|
||||||
|
| 5 | 1 | fanSpeed | 3.4 |
|
||||||
|
| 6 | 1 | ledDim | 3.5 (SA 디퓨저만 유효) |
|
||||||
|
| 7 | 1 | dmpAngle | 3.6 (해당 포트의 댐퍼 목표각) |
|
||||||
|
| 8 | 1 | dmpReset | 0=정상 / 1=댐퍼 초기화 |
|
||||||
|
| 9 | 1 | reserveHour | 예약 정지 0~8시간 (0=없음) |
|
||||||
|
|
||||||
|
LEN = 10.
|
||||||
|
|
||||||
|
### 4.2 `RSP_DIFFUSER_STATUS` (0x90, 디퓨저→Master)
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | 요청 에코 |
|
||||||
|
| 3 | 1 | power | 현재 전원 |
|
||||||
|
| 4 | 1 | runMode | 현재 운전모드 |
|
||||||
|
| 5 | 1 | fanSpeed | 현재 풍량 |
|
||||||
|
| 6 | 1 | ledDim | 현재 LED 밝기 |
|
||||||
|
| 7 | 1 | dmpAngle | 현재 댐퍼 각도 (3.6) |
|
||||||
|
| 8 | 2 | rpm | 해당 팬 실측 RPM (u16) |
|
||||||
|
| 10 | 4 | errorCode | 3.7 (u32) |
|
||||||
|
| 14 | 2 | version | 예) 0x0117 = Ver 1.23 |
|
||||||
|
|
||||||
|
LEN = 16.
|
||||||
|
|
||||||
|
### 4.3 `POLL_ROOMCON` (0x20, ERV→룸컨)
|
||||||
|
|
||||||
|
ERV가 룸컨을 폴링하면서 **룸컨 화면에 표시할 데이터**(ERV 동작상태·에러·온도)를 전달하고,
|
||||||
|
직전에 받은 룸컨 명령의 **수용 결과(ackFlags)** 를 에코한다.
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | DEV=0x30 |
|
||||||
|
| 3 | 1 | power | ERV 현재 전원 (3.2) |
|
||||||
|
| 4 | 1 | runMode | ERV 현재 운전모드 (3.3) |
|
||||||
|
| 5 | 1 | fanSpeed | ERV 현재 풍량 (3.4) |
|
||||||
|
| 6 | 1 | autoState | 0=분산, 1=집중 (자동모드) |
|
||||||
|
| 7 | 1 | reserveRemain | 예약 잔여 시간(hour) |
|
||||||
|
| 8 | 4 | errorCode | ERV 현재 에러 통보 (3.7, u32) |
|
||||||
|
| 12 | 2 | outTemp | 외기온도 ×10 (signed, ℃) |
|
||||||
|
| 14 | 2 | inTemp | 내기온도 ×10 (signed, ℃) |
|
||||||
|
| 16 | 1 | ackFlags | 직전 룸컨 명령 수용 비트(4.4 cmdFlags 동일 배치) |
|
||||||
|
|
||||||
|
LEN = 17.
|
||||||
|
|
||||||
|
### 4.4 `CMD_ROOMCON` (0xA0, 룸컨→ERV) — **룸컨이 명령 주체**
|
||||||
|
|
||||||
|
사용자가 룸컨에서 조작한 명령을 ERV에 전달한다. `cmdFlags` 로 **이번에 실제로 바꾼 항목만** 표시한다(나머지는 무시).
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | DEV=0x30, ERV 폴링(0x20)에 대한 응답으로 송신 |
|
||||||
|
| 3 | 1 | cmdFlags | bit0 power, bit1 runMode, bit2 fanSpeed, bit3 reserveHour, bit4 heaterUV, bit5 filterReset, bit6 ledDim (1=이 필드 명령 유효) |
|
||||||
|
| 4 | 1 | power | 3.2 |
|
||||||
|
| 5 | 1 | runMode | 3.3 |
|
||||||
|
| 6 | 1 | fanSpeed | 3.4 |
|
||||||
|
| 7 | 1 | reserveHour | 0~8시간 |
|
||||||
|
| 8 | 1 | heaterUV | bit0=히터, bit4=UV (1=ON) |
|
||||||
|
| 9 | 1 | filterReset | 1=필터 리셋 |
|
||||||
|
| 10 | 1 | ledDim | 3.5 (해당 실 디퓨저 LED 밝기) |
|
||||||
|
| 11 | 2 | version | 룸컨 버전 |
|
||||||
|
|
||||||
|
LEN = 13.
|
||||||
|
|
||||||
|
> ERV는 받은 명령을 실행(분배기→디퓨저 제어)하고, 다음 `POLL_ROOMCON(0x20)`의 `ackFlags`·동작상태로 결과를 회신한다.
|
||||||
|
|
||||||
|
### 4.5 `RSP_VSP_STATUS` (0x30, ERV→룸컨)
|
||||||
|
|
||||||
|
룸컨이 현재 VSP 값을 요청(`CMD_VSP_SET` 의 reqStatus=1)하면, ERV가 **저장된 현재 VSP 풍량 테이블**을 회신한다(룸컨 표시용). 레이아웃은 4.6과 동일(끝의 save/장치개수 필드는 제외, vspSelect로 어떤 모드값인지 표시).
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | DEV=0x30 |
|
||||||
|
| 3 | 1 | vspSelect | 회신하는 모드 (`1`=환기/`2`=공청/`3`=바이패스) |
|
||||||
|
| 4 | 10 | sa1,ea1 … sa5,ea5 | SA/EA 1~5단 VSP (각 1B) |
|
||||||
|
| 14 | 2 | rpmRefMid | 중(2단) RPM 기준 |
|
||||||
|
| 16 | 2 | rpmDeltaMid | 중(2단) RPM 허용편차 |
|
||||||
|
| 18 | 2 | rpmRefHigh | 강(3단) RPM 기준 |
|
||||||
|
| 20 | 2 | rpmDeltaHigh | 강(3단) RPM 허용편차 |
|
||||||
|
|
||||||
|
LEN = 22.
|
||||||
|
|
||||||
|
### 4.6 `CMD_VSP_SET` (0xB0, 룸컨→ERV) — **VSP 설정·저장 주체는 룸컨**
|
||||||
|
|
||||||
|
룸컨에서 VSP 풍량 테이블을 한 모드씩(vspSelect) 설정하고 ERV에 저장 요청. 폴링할 장치 개수도 함께 설정.
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | DEV=0x30, ERV 폴링(0x20)에 대한 응답으로 송신 |
|
||||||
|
| 3 | 1 | vspSelect | `0`=None, `1`=환기, `2`=공청, `3`=바이패스 |
|
||||||
|
| 4 | 1 | sa1 | SA 1단 VSP |
|
||||||
|
| 5 | 1 | ea1 | EA 1단 VSP |
|
||||||
|
| 6 | 1 | sa2 | SA 2단 |
|
||||||
|
| 7 | 1 | ea2 | EA 2단 |
|
||||||
|
| 8 | 1 | sa3 | SA 3단 |
|
||||||
|
| 9 | 1 | ea3 | EA 3단 |
|
||||||
|
| 10 | 1 | sa4 | SA 4단 |
|
||||||
|
| 11 | 1 | ea4 | EA 4단 |
|
||||||
|
| 12 | 1 | sa5 | SA 5단 |
|
||||||
|
| 13 | 1 | ea5 | EA 5단 |
|
||||||
|
| 14 | 2 | rpmRefMid | 중(2단) RPM 기준 (환기/공청만) |
|
||||||
|
| 16 | 2 | rpmDeltaMid | 중(2단) RPM 허용편차 |
|
||||||
|
| 18 | 2 | rpmRefHigh | 강(3단) RPM 기준 |
|
||||||
|
| 20 | 2 | rpmDeltaHigh | 강(3단) RPM 허용편차 |
|
||||||
|
| 22 | 1 | roomconNum | 폴링할 룸컨 수 (1~5) |
|
||||||
|
| 23 | 1 | saDiffuserNum | 폴링할 SA 디퓨저 수 (2~6) |
|
||||||
|
| 24 | 1 | raDiffuserNum | 폴링할 RA 디퓨저 수 (2~6) |
|
||||||
|
| 25 | 1 | modbusId | 외부 홈넷 연동용 ID (선택) |
|
||||||
|
| 26 | 1 | save | 0=저장안함, 1=EEPROM 저장 |
|
||||||
|
| 27 | 1 | reqStatus | 1=현재 VSP 값 회신 요청(ERV가 `RSP_VSP_STATUS`로 응답) |
|
||||||
|
|
||||||
|
LEN = 28.
|
||||||
|
|
||||||
|
### 4.7 `RSP_DIFFUSER_SENSOR` (0x91, 디퓨저→Master)
|
||||||
|
|
||||||
|
통합공기질 센서(SEN66) 1실분. 기존 0x11/0x12 분리 모드를 **이 패킷 1종**으로 대체.
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 단위/비고 |
|
||||||
|
|-----|------|------|-----------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | 센서 부착 디퓨저(통상 RA, DEV=0x20) |
|
||||||
|
| 3 | 2 | pm1p0 | ㎍/㎥ |
|
||||||
|
| 5 | 2 | pm2p5 | ㎍/㎥ |
|
||||||
|
| 7 | 2 | pm4p0 | ㎍/㎥ |
|
||||||
|
| 9 | 2 | pm10p0 | ㎍/㎥ |
|
||||||
|
| 11 | 2 | humidity | %RH ×10 |
|
||||||
|
| 13 | 2 | temperature | ℃ ×10 (signed) |
|
||||||
|
| 15 | 2 | voc | TVOC index |
|
||||||
|
| 17 | 2 | nox | NOx index |
|
||||||
|
| 19 | 2 | co2 | ppm |
|
||||||
|
| 21 | 4 | errorCode | 센서/통신 에러 (3.7) |
|
||||||
|
|
||||||
|
LEN = 25.
|
||||||
|
|
||||||
|
### 4.8 `RSP_SPEC` (0xC0)
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | |
|
||||||
|
| 3 | 2 | version | 펌웨어 버전 |
|
||||||
|
| 5 | 1 | deviceType | 장치 타입 코드 |
|
||||||
|
| 6 | 1 | capability | bit0=히터, bit1=UV, bit2=후드연동 … |
|
||||||
|
|
||||||
|
LEN = 7.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 동작 시나리오 (폴링)
|
||||||
|
|
||||||
|
ERV가 버스 마스터로서 **룸컨 → 디퓨저 순으로 라운드로빈 폴링**하고, 명령은 룸컨이 응답에 실어 올린다.
|
||||||
|
|
||||||
|
1. **룸컨 폴링**
|
||||||
|
- ERV → 룸컨 : `POLL_ROOMCON(0x20)` (ERV 동작상태·해당 실 공기질·온도·에러 전달 + 직전 명령 ack)
|
||||||
|
- 룸컨 → ERV : 사용자가 조작했으면 `CMD_ROOMCON(0xA0)` (cmdFlags로 바뀐 항목 표시), VSP 설정중이면 `CMD_VSP_SET(0xB0)` 으로 응답
|
||||||
|
2. **디퓨저 폴링 (ERV가 제어 주체)**
|
||||||
|
- ERV → 디퓨저 : `CMD_DIFFUSER_CTRL(0x10)` (댐퍼각/풍량/LED 지시)
|
||||||
|
- 디퓨저 → ERV : `RSP_DIFFUSER_STATUS(0x90)`, 센서 부착 디퓨저는 이어서 `RSP_DIFFUSER_SENSOR(0x91)`
|
||||||
|
3. **명령 처리 흐름** : 룸컨 명령(0xA0) 수신 → ERV가 운전모드/풍량 결정 → 각 디퓨저에 `CMD_DIFFUSER_CTRL(0x10)`로 댐퍼 개폐·풍량 지시 → 결과를 다음 `POLL_ROOMCON(0x20)`의 ackFlags·동작상태로 룸컨에 회신.
|
||||||
|
4. **자동/공청 모드** : ERV가 각 실 센서(0x91)로 부하점수·집중/분산 계산 → 디퓨저 댐퍼각·풍량을 `CMD_DIFFUSER_CTRL`로 지시 (사양서 10~11P 로직).
|
||||||
|
5. **VSP 시운전** : 룸컨이 `CMD_VSP_SET(0xB0)`으로 단별 VSP·RPM·장치개수 설정(save=1 시 ERV가 EEPROM 저장), reqStatus=1이면 ERV가 `RSP_VSP_STATUS(0x30)`로 현재값 회신.
|
||||||
|
6. **통신 단절** : 폴링 타임아웃 N회 시 ERV가 해당 실 통신에러 비트(3.7 bit16~20) set, 재연결 시 clear.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 펌웨어 반영 메모
|
||||||
|
|
||||||
|
- 대상 파일 : `program/User/My_Uart.c` (SC1, 각실분배기/디퓨저), `bunbaegi_parsing()` / `Bunbaegi_Polling()` 를 CMD 기반으로 교체.
|
||||||
|
- **`My_RJ2.c` (DL 룸컨 232) 는 변경하지 않는다** — DL 사양 별도 유지.
|
||||||
|
- CRC : 기존 `CRC16()` (MODBUS) 그대로 사용, 전송은 빅엔디안(Hi→Lo).
|
||||||
|
- 센서/디퓨저 배열은 현행 `[7]` (index 1~6) 유지 가능하나, ROOM(1~5)+IDX 매핑 테이블 1개로 주소→배열 변환.
|
||||||
|
- 에러코드는 `uint16_t Err_Code` → **`uint32_t`** 로 확장 (방4 비트 수용).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 송수신 예제 — 거실 룸컨이 전 실 상태 표시
|
||||||
|
|
||||||
|
**목표** : 거실 룸컨에서 모든 방 디퓨저(SA/RA) 댐퍼·LED·센서 + ERV 동작상태(모드/풍량/예약)를 표시.
|
||||||
|
|
||||||
|
**전제** : 거실 룸컨은 디퓨저를 직접 못 읽는다. 버스 마스터는 ERV 하나뿐이므로 항상 2단계.
|
||||||
|
|
||||||
|
```
|
||||||
|
① 수집 : ERV ──폴링──> 각 디퓨저(SA/RA) ──상태/센서──> ERV (캐싱)
|
||||||
|
② 표시 : ERV ──전달──> 거실 룸컨 (화면 표시)
|
||||||
|
```
|
||||||
|
|
||||||
|
> 아래 모든 프레임의 CRC는 **STX(0xAA) 제외, `CMD~PAYLOAD` 구간**에 대한 CRC-16/MODBUS 결과를 빅엔디안(Hi,Lo)으로 붙인 실제 값이다.
|
||||||
|
> 예시 값 : runMode=`0x02`(자동), fanSpeed=`2`, 댐퍼 `0xB4`=180°(열림)/`0x00`=닫힘, 온도·습도 ×10.
|
||||||
|
> **폴링 시간** : 1회 폴링(TX 요청 → RX 응답)을 **300ms**로 가정한다. (각 [TX]/[RX] 쌍 = 300ms)
|
||||||
|
|
||||||
|
### ① 수집 단계 — ERV가 각 디퓨저를 라운드로빈 폴링
|
||||||
|
|
||||||
|
ERV가 `거실SA1 → 거실SA2 → 거실RA1 → 거실RA2 → 방1SA → 방1RA → … → 방4RA` 순으로 폴링. 대표 예 (괄호는 누적시간):
|
||||||
|
|
||||||
|
**거실 SA1 디퓨저 폴링/제어** — `CMD_DIFFUSER_CTRL(0x10)` *(t=0 ~ 300ms)*
|
||||||
|
```
|
||||||
|
[TX] AA 10 0A | 10 01 01 | 01 02 02 05 B4 00 00 | E4 61
|
||||||
|
STX CMD LEN DEV ROOM IDX power runMode fan led dmp reset reserve
|
||||||
|
(SA,거실,1) ON 02(자동) 2 5 B4 0 0
|
||||||
|
```
|
||||||
|
```
|
||||||
|
[RX] AA 90 10 | 10 01 01 | 01 02 02 05 B4 03 52 00 00 00 00 01 17 | A6 08
|
||||||
|
└RSP_DIFFUSER_STATUS power mode fan led dmp rpm=0352(850) err=0(4B) ver=0117
|
||||||
|
```
|
||||||
|
|
||||||
|
**거실 RA1 디퓨저 폴링** — 상태 `RSP_DIFFUSER_STATUS(0x90)` + 센서 `RSP_DIFFUSER_SENSOR(0x91)` *(t=600 ~ 900ms)*
|
||||||
|
```
|
||||||
|
[RX] AA 91 19 | 20 01 01 | 00 05 00 08 00 09 00 0B 01 C2 00 DC 00 64 00 01 02 8A | 00 00 00 00 | E8 AF
|
||||||
|
└센서 (RA,거실,1) pm1=5 pm2.5=8 pm4=9 pm10=11 습45.0% 온22.0℃ voc100 nox1 co2=028A(650) err=0
|
||||||
|
```
|
||||||
|
> RA 디퓨저는 한 폴링 슬롯(300ms)에서 상태(0x90)+센서(0x91)를 함께 응답.
|
||||||
|
> RA2·방1~4 디퓨저도 같은 방식으로 폴링 → ERV가 전 실 댐퍼각/LED/RPM/센서를 모두 캐싱.
|
||||||
|
|
||||||
|
**ERV 동작상태를 거실 룸컨에 전달 (+ 룸컨 명령 수신)** — `POLL_ROOMCON(0x20)` *(룸컨 폴링 슬롯, 각 300ms)*
|
||||||
|
```
|
||||||
|
[TX] AA 20 11 | 30 01 01 | 01 02 02 01 00 | 00 00 00 00 | 00 96 | 00 DC | 00 | B5 61
|
||||||
|
└ERV→룸컨 (룸컨,거실,1) power mode fan auto=1(집중) reserveRemain=0
|
||||||
|
err=0(4B) 외기15.0℃ 내기22.0℃ ack=0
|
||||||
|
[RX] (거실 룸컨이 조작했으면 CMD_ROOMCON(0xA0)/CMD_VSP_SET(0xB0)로 응답, 없으면 빈 응답)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ② 표시 단계 — 전 실 집계 전달
|
||||||
|
|
||||||
|
거실 룸컨이 전 실 디퓨저·센서를 한 화면에 표시하려면, ERV가 ①에서 모은 값을 집계해 거실 룸컨에 전달해야 한다.
|
||||||
|
|
||||||
|
> **참고(본 규격 미반영 제안)** : 아래 `CMD_ALLROOM_STATUS(0x21)` 는 "전 실 집계"용으로 검토한 예시 패킷이다.
|
||||||
|
> 정식 채택은 보류 상태이며, 필요 시 별도 협의 후 2·4장에 추가한다.
|
||||||
|
|
||||||
|
**헤더(9B)** + **실별 블록(18B) × 5실** :
|
||||||
|
|
||||||
|
| 헤더 off | 필드 | 실 블록 off | 필드 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0~2 | 룸컨 주소(30·01·01) | +0 | roomNo |
|
||||||
|
| 3 | ERV power | +1,+2 | SA댐퍼1·2 (`0xFF`=없음) |
|
||||||
|
| 4 | ERV runMode | +3,+4 | RA댐퍼1·2 |
|
||||||
|
| 5 | ERV fanSpeed | +5 | LED 밝기 |
|
||||||
|
| 6 | 예약 잔여(h) | +6,+7 | PM2.5 |
|
||||||
|
| 7 | autoState | +8,+9 | PM10 |
|
||||||
|
| 8 | roomCount(=5) | +10,+11 | CO2 |
|
||||||
|
| | | +12,+13 | VOC |
|
||||||
|
| | | +14,+15 | 온도 ×10 |
|
||||||
|
| | | +16,+17 | 습도 ×10 |
|
||||||
|
|
||||||
|
```
|
||||||
|
[TX] AA 21 63 | 30 01 01 01 02 02 00 01 05 | <실1..실5 블록> | D9 F4
|
||||||
|
└CMD_ALLROOM 헤더(LEN=0x63=99) 거실은 SA·RA 2개 모두 사용
|
||||||
|
|
||||||
|
실1(거실): 01 B4 B4 B4 B4 05 00 08 00 0B 02 8A 00 64 00 DC 01 C2 SA열림×2,RA열림×2,LED5,PM2.5=8,PM10=11,CO2=650,VOC=100,22.0℃,45.0%
|
||||||
|
실2(방1) : 02 B4 FF B4 FF 04 00 0C 00 12 02 D0 00 82 00 DD 01 C4 SA/RA 각1, slot2=FF
|
||||||
|
실3(방2) : 03 00 FF 00 FF 00 00 06 00 09 02 62 00 5A 00 DC 01 C0 댐퍼 닫힘, LED OFF
|
||||||
|
실4(방3) : 04 B4 FF B4 FF 06 00 14 00 1C 03 2A 00 A0 00 DB 01 C7
|
||||||
|
실5(방4) : 05 B4 FF B4 FF 03 00 07 00 0A 02 80 00 5F 00 DC 01 C1
|
||||||
|
```
|
||||||
|
> 전체 104 byte 1프레임으로 거실 룸컨이 **전 실 SA/RA 댐퍼·LED·센서 + ERV 모드/풍량/예약**을 모두 표시.
|
||||||
|
|
||||||
|
### 폴링 주기와 전체 시간 (1회 = 300ms)
|
||||||
|
|
||||||
|
5실 구성에서 ERV가 한 바퀴 도는 동안 폴링하는 장치:
|
||||||
|
|
||||||
|
| 장치 | 개수 | 폴링 시간 (×300ms) |
|
||||||
|
|------|------|--------------------|
|
||||||
|
| SA 디퓨저 (거실2 + 방1~4 각1) | 6 | 1,800 ms |
|
||||||
|
| RA 디퓨저 (거실2 + 방1~4 각1, 상태+센서 동시) | 6 | 1,800 ms |
|
||||||
|
| 룸컨 (거실 + 방1~4) | 5 | 1,500 ms |
|
||||||
|
| **합계 (1주기)** | **17** | **5,100 ms ≈ 5.1초** |
|
||||||
|
|
||||||
|
- 거실 룸컨 화면은 **약 5.1초마다 전 실 데이터가 1회 갱신**된다.
|
||||||
|
- 갱신을 더 빠르게 하려면 : 폴링 시간 단축(예: 200ms → 17×200 = 3.4초), 또는 폴링 대상 축소.
|
||||||
|
- `0x21` 집계 프레임은 거실 룸컨 폴링 슬롯 안에서 전달되므로 별도 시간이 추가되지 않는다.
|
||||||
|
|
||||||
|
```
|
||||||
|
[1 주기 = 5.1초]
|
||||||
|
SA1 SA2 RA1 RA2 | 방1SA 방1RA | 방2SA 방2RA | 방3SA 방3RA | 방4SA 방4RA | RC거실 RC방1 RC방2 RC방3 RC방4
|
||||||
|
└─거실 4슬롯─┘ └─방1 2─┘ └─방2 2─┘ └─방3 2─┘ └─방4 2─┘ └────── 룸컨 5슬롯 ──────┘
|
||||||
|
각 슬롯 300ms × 17 = 5,100ms
|
||||||
|
```
|
||||||
|
|
||||||
|
### 요약
|
||||||
|
|
||||||
|
| 단계 | 방향 | 패킷 | 역할 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| ① | ERV→디퓨저 / 디퓨저→ERV | `0x10` / `0x90`,`0x91` | 전 실 댐퍼·LED·센서 수집 |
|
||||||
|
| ② | ERV→거실룸컨 | `0x20` | ERV 동작상태(모드/풍량/예약) 전달 |
|
||||||
|
| ② | ERV→거실룸컨 | `0x21` (미반영 제안) | 전 실 디퓨저·센서 집계 전달 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 본 문서는 내부 통신 재정의 초안(Rev 2.0)이다. 디퓨저/룸컨 펌웨어 담당(주신전자)과
|
||||||
|
> CMD 코드·필드 세부값을 상호 합의하여 확정한다.
|
||||||
@@ -0,0 +1,546 @@
|
|||||||
|
# 각실제어 내부 통신 프로토콜 (Rev 3.0, 2-Tier 계층형)
|
||||||
|
|
||||||
|
> 휴벤ECO(ERV) ↔ **각실분배기** ↔ 디퓨저(SA/RA) · 룸컨 **내부 통신** 규격
|
||||||
|
>
|
||||||
|
> 본 규격은 [각실제어_내부프로토콜_Rev2.0_CMD.md](각실제어_내부프로토콜_Rev2.0_CMD.md) 를
|
||||||
|
> **계층형(2-Tier)** 으로 재구성한 것이다. 분배기 회로도 `Schematic/BUNBAGI_REV4.1_20251124(회로도).pdf`
|
||||||
|
> 와 펌웨어 `program/User/My_Uart.c` 를 근거로 한다.
|
||||||
|
> (Rev 2.0 은 그대로 보존하며, 본 Rev 3.0 이 상위 규격이다. DL 룸컨 232 `My_RJ2.c` 는 무관 — 변경 안 함.)
|
||||||
|
|
||||||
|
- **물리계층** : RS-485, **115200 bps, 8 Data, None Parity, 1 Stop (N81)**
|
||||||
|
- **대상 시스템** : 사양서 5페이지 구성 = **거실 + 방1~4 (5실)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 무엇이 바뀌었나 (Rev 2.0 단일버스 → Rev 3.0 2-Tier)
|
||||||
|
|
||||||
|
회로도 분석 결과, 각실분배기는 **자체 MCU(Nuvoton NANO100SE3BN)를 가진 능동 컨트롤러**임이 확인되었다. 따라서 통신을 두 계층으로 분리한다.
|
||||||
|
|
||||||
|
| 구분 | Rev 2.0 (단일 버스) | Rev 3.0 (2-Tier) |
|
||||||
|
|------|---------------------|------------------|
|
||||||
|
| 버스 구조 | ERV가 모든 디퓨저/룸컨(17대)을 1버스로 직접 폴링 | **상위: ERV↔분배기 1버스 / 하위: 분배기↔실별 디퓨저·룸컨** |
|
||||||
|
| ERV 폴링 대상 | 17대 | **분배기 1대** |
|
||||||
|
| 분배기 역할 | 없음(전원·배선 통과) | **하위 5채널 로컬 마스터 + 에러격리 + 포트별 통신상태 LED** |
|
||||||
|
| 디퓨저 LED 제어 | 디퓨저에 ledDim 전송 | **디퓨저에 ledDim 전송** (유지). 조명 LED는 **RA 디퓨저에만** 있고 ERV→분배기→RA디퓨저로 밝기 제어 |
|
||||||
|
| 자동운전 판단 | ERV | **ERV** (분배기는 센서수집·명령실행만) |
|
||||||
|
| 집계 패킷 | 0x21 제안(미반영) | **상위버스 정식 패킷(`RSP_ALLROOM_*`)으로 채택** |
|
||||||
|
|
||||||
|
> **하드웨어 근거 (BUNBAGI Rev4.1)** : 상위 `M485`(RS485 1채널, U5, 커넥터 CN3) / 하위 `SA485`(RS485 **5채널**, 실별 RJ45 J1~J5, RX는 8:1 MUX U6로 실 선택, DIR 채널별) / 전원 24V(60W)·3.3V.
|
||||||
|
> ※ 분배기의 2×74HC595(U10·U12, SA=초록·RA=노랑) LED는 **각 포트 SA/RA 디퓨저의 통신상태 표시등**(분배기 보드 진단용)이며, 프로토콜과 무관하다.
|
||||||
|
> ※ **방의 조명용 LED는 RA 디퓨저에만 달려 있고**, ERV가 밝기(0~9)를 명령한다 → 분배기가 해당 RA 디퓨저로 전달.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0-1. Rev 2.0(단일버스) → Rev 3.0(2-Tier) 장단점
|
||||||
|
|
||||||
|
> 채택 전 의사결정용 요약. 결론적으로 **하드웨어(분배기 MCU·하위 5채널·SPOF 구조)와 5실 확장 요구를 고려하면 2-Tier 도입 권장**이되, 분배기 펌웨어 신규 개발과 단일 장애점(SPOF) 대비가 전제다.
|
||||||
|
|
||||||
|
### ✅ 장점
|
||||||
|
|
||||||
|
| # | 항목 | 내용 | 효과 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| 1 | **ERV 폴링 부하 격감** | ERV가 17대 직접 폴링 → **분배기 1대만** 상대 | ERV 펌웨어·루프 단순, CPU 여유 |
|
||||||
|
| 2 | **갱신 속도 향상** | ERV 입장 1주기 **5.1초 → 0.6초** (상·하위 병렬, 하위는 분배기가 빠르게 순환) | 룸컨·대시보드 응답성 ↑ |
|
||||||
|
| 3 | **버스 트래픽·충돌 감소** | 상위 버스엔 노드 1개, 하위는 5채널로 부하 분산(MUX+채널별 DIR) | 패킷 충돌/재전송 ↓ |
|
||||||
|
| 4 | **실별 통신에러 격리** | 한 실 디퓨저 고장이 채널 단위로 격리 → 타 실·ERV에 영향 최소 | 가용성 ↑, 원인 국소화 |
|
||||||
|
| 5 | **신호 무결성·배선 이점** | 하위는 짧은 로컬 세그먼트 5채널 분리, 채널별 종단·DIR | 노이즈·반사 ↓ (긴 데이지체인 대비) |
|
||||||
|
| 6 | **ERV–분배기 인터페이스 추상화** | ERV는 디퓨저 주소·개수를 몰라도 됨. 디퓨저 증설/사양변경을 **분배기가 흡수** | ERV 코드 변경 없이 하위 확장 |
|
||||||
|
| 7 | **역할·개발 분담** | ERV(시스템 로직) ↔ 분배기/디퓨저/룸컨(주신) 펌웨어 디커플링 | 병행 개발·유지보수 용이 |
|
||||||
|
| 8 | **확장성** | 실/디퓨저 추가는 하위에서 처리, 멀티 분배기(`nodeId`) 대비 | 향후 평면 확장 수월 |
|
||||||
|
| 9 | **MCU 동일(Nano100)** | ERV·분배기 동일 계열 → 프레임/CRC/드라이버 코드 공유 | 개발 재사용 |
|
||||||
|
|
||||||
|
### ⚠️ 단점 / 리스크
|
||||||
|
|
||||||
|
| # | 항목 | 내용 | 완화책 |
|
||||||
|
|---|------|------|--------|
|
||||||
|
| 1 | **분배기 펌웨어 신규 개발** | 하위 마스터 + 상위 슬레이브 + MUX/DIR + 595 + 집계 캐시 로직 필요 | Nano100 공통 드라이버 재사용, `My_Uart.c` 골격 활용 |
|
||||||
|
| 2 | **지연(latency) 1홉 추가** | 명령이 ERV→분배기→디퓨저 2단. 최악 상위주기+하위주기 합산 | 긴급 제어는 분배기가 즉시 중계, 댐퍼는 비실시간이라 영향 작음 |
|
||||||
|
| 3 | **상태 동기화 시차** | ERV가 보는 값은 분배기 캐시(직전 하위 폴링 결과) → 한 박자 지연 가능 | 변경 이벤트 우선 보고, 타임스탬프/시퀀스로 정합 |
|
||||||
|
| 4 | **단일 장애점(SPOF)** | 중앙 분배기 1대 고장 시 **전 실 통신 두절** (단일버스는 ERV-디퓨저 직접이라 무관) | 분배기 워치독·자기진단, 통신두절 시 ERV 안전모드 |
|
||||||
|
| 5 | **집계 패킷이 큼** | `RSP_ALLROOM_STATUS` 93B 등 → 1프레임 손상 시 전 실 갱신 실패(재전송 단위 큼) | CRC+재요청, 필요 시 실별 분할 응답 옵션 |
|
||||||
|
| 6 | **디버깅·추적 복잡** | 장애 시 상위/하위/분배기 로직 분리 진단, 버스 스니핑 2곳 | 분배기 진단로그·상태 LED(595), SPEC/버전 패킷 |
|
||||||
|
| 7 | **프로토콜 2종 관리** | 상위·하위 CMD 세트 2개 정의·문서화·버전관리 | 본 문서 단일화, 프레임 규칙 공통 |
|
||||||
|
| 8 | **3자 버전 정합** | ERV·분배기·디퓨저/룸컨 펌웨어 호환성 관리 | `SPEC`(0x1F/0x9F) 버전 교환·검증 |
|
||||||
|
|
||||||
|
### 한눈에
|
||||||
|
|
||||||
|
| 관점 | Rev 2.0 단일버스 | Rev 3.0 2-Tier |
|
||||||
|
|------|------------------|----------------|
|
||||||
|
| ERV 부담 | 큼(17대 폴링) | **작음(1대)** |
|
||||||
|
| 갱신 주기(ERV) | 5.1초 | **0.6초** |
|
||||||
|
| 분배기 펌웨어 | 불필요 | **필요(신규)** |
|
||||||
|
| 장애 격리 | 디퓨저별(분배기 무관) | 실별(단, 분배기 SPOF) |
|
||||||
|
| 확장/유지보수 | ERV가 전부 관리 | **계층 분리로 용이** |
|
||||||
|
| 구현 난이도 | 낮음 | 중(분배기 추가) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 시스템 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
[상위 버스: M485 · RS485 1채널] [하위 버스: SA485 · RS485 5채널 (MUX)]
|
||||||
|
┌── ch1 거실 : SA디퓨저×2, RA디퓨저×2, 룸컨
|
||||||
|
ERV ───────────────────────── 각실분배기 ─┼── ch2 방1 : SA디퓨저, RA디퓨저, 룸컨
|
||||||
|
(Master) (Nano100) ├── ch3 방2 : SA디퓨저, RA디퓨저, 룸컨
|
||||||
|
· 상위 Slave ├── ch4 방3 : SA디퓨저, RA디퓨저, 룸컨
|
||||||
|
· 하위 Master └── ch5 방4 : SA디퓨저, RA디퓨저, 룸컨
|
||||||
|
· 포트별 통신상태 LED(74HC595, 진단용)
|
||||||
|
· 실별 전원(24V)·통신에러 격리
|
||||||
|
```
|
||||||
|
|
||||||
|
**역할 분담**
|
||||||
|
|
||||||
|
| 주체 | 책임 |
|
||||||
|
|------|------|
|
||||||
|
| **ERV (메인)** | 전열교환기 팬/VSP 제어, **자동운전 판단**(부하점수·집중/분산, 사양서 10~11P), **부가모드(스마트수면/쾌적조리/안심회복)·후드 연동(HOOD-485) 처리**, 분배기에 전원/모드/풍량 + 실별 댐퍼·LED 타겟 하달, 집계상태 수신 |
|
||||||
|
| **분배기** | 하위 5채널 폴링(실별 SA/RA 디퓨저·룸컨), **댐퍼·LED 명령 중계·실행**, 센서·룸컨명령 수집→ERV에 집계 보고, 실별 통신에러 격리, **포트별 통신상태 표시 LED**(74HC595, 진단용) |
|
||||||
|
| **디퓨저(SA/RA)** | 댐퍼 구동·각도/RPM 회신, 센서값 회신. **RA 디퓨저는 자체 조명 LED 점등(밝기 0~9)** |
|
||||||
|
| **룸컨** | 사용자 입력(전원/모드/풍량/예약 등) 보고, ERV 상태 표시 |
|
||||||
|
|
||||||
|
**데이터 흐름 (자동운전)**
|
||||||
|
|
||||||
|
```
|
||||||
|
분배기 ──(실별 원시 센서)──> ERV ──[부하점수·집중/분산 계산]──> 실별 댐퍼/풍량/LED 타겟
|
||||||
|
ERV ──(타겟)──> 분배기 ──(댐퍼·LED 실행 / 디퓨저 제어)──> 각 실
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 공통 프레임 (상·하위 동일)
|
||||||
|
|
||||||
|
```
|
||||||
|
+------+------+------+------------------+--------+--------+
|
||||||
|
| STX | CMD | LEN | PAYLOAD[LEN] | CRC_H | CRC_L |
|
||||||
|
+------+------+------+------------------+--------+--------+
|
||||||
|
0xAA 1B 1B LEN bytes 16-bit CRC
|
||||||
|
```
|
||||||
|
|
||||||
|
| 필드 | 크기 | 설명 |
|
||||||
|
|---------|------|------|
|
||||||
|
| STX | 1 | 고정 `0xAA` |
|
||||||
|
| CMD | 1 | 명령/응답 코드 (상위 4장 / 하위 5장) |
|
||||||
|
| LEN | 1 | PAYLOAD 바이트 수 (0~255) |
|
||||||
|
| PAYLOAD | LEN | CMD 별 데이터 |
|
||||||
|
| CRC | 2 | **CRC-16/MODBUS** (poly 0xA001, init 0xFFFF), **CMD~PAYLOAD** 까지, **빅엔디안(CRC_H 먼저)** |
|
||||||
|
|
||||||
|
- 모든 멀티바이트 수치는 **빅엔디안**. 상·하위 버스 모두 같은 프레임/CRC 규칙을 쓴다.
|
||||||
|
- 분배기 MCU와 ERV MCU가 동일 계열(Nano100)이므로 CRC·프레임 파서 코드를 공유한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 공통 값 정의
|
||||||
|
|
||||||
|
### 3.1 전원 (Power)
|
||||||
|
`0x00`=OFF, `0x01`=ON
|
||||||
|
|
||||||
|
### 3.2 운전모드 (RunMode)
|
||||||
|
| 값 | 의미 | 펌웨어 |
|
||||||
|
|----|------|--------|
|
||||||
|
| 0x01 | 수동(환기) | MODE_VENTILATION |
|
||||||
|
| 0x02 | 자동 | MODE_AUTO |
|
||||||
|
| 0x04 | 바이패스 | MODE_BYPASS |
|
||||||
|
| 0x08 | 공기청정 | MODE_AIRCLEAN |
|
||||||
|
|
||||||
|
### 3.3 풍량 (FanSpeed)
|
||||||
|
`0`=정지, `1`=약, `2`=중, `3`=강, `4`=터보
|
||||||
|
|
||||||
|
### 3.4 LED 밝기 (LedDim) — **RA 디퓨저 조명**
|
||||||
|
`0`=OFF, `1`~`9`=1~9단(9=최대). 조명 LED는 **RA 디퓨저에만** 존재(단색, 색온도 3800K). ERV가 실별 밝기를 지시 → 분배기가 해당 실 RA 디퓨저로 전달 → RA 디퓨저가 자체 점등.
|
||||||
|
> 분배기 보드의 SA(초록)/RA(노랑) LED는 별개로, 포트별 통신상태 표시등이다(프로토콜 무관).
|
||||||
|
|
||||||
|
### 3.5 댐퍼 각도 (DamperAngle)
|
||||||
|
`0x00`=0°(닫힘) ~ `0xB4`=180°(완전개방). 슬롯 미사용 시 `0xFF`(장치 없음).
|
||||||
|
|
||||||
|
### 3.6 에러코드 (ErrorCode, **u32 비트맵**)
|
||||||
|
| 비트 | 마스크 | 의미 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 0 | 0x00000001 | 필터 청소 |
|
||||||
|
| 1 | 0x00000002 | 필터 교체 |
|
||||||
|
| 2 | 0x00000004 | 소자 교체 |
|
||||||
|
| 3 | 0x00000008 | 온도센서 에러 |
|
||||||
|
| 4 | 0x00000010 | 장비보호 모드 |
|
||||||
|
| 5 | 0x00000020 | EA 팬 에러 |
|
||||||
|
| 6 | 0x00000040 | 간헐운전 모드 |
|
||||||
|
| 7 | 0x00000080 | SA 팬 에러 |
|
||||||
|
| 8~12 | 0x00000100~0x00001000 | 통합센서 에러 — 거실/방1/방2/방3/방4 |
|
||||||
|
| 16~20 | 0x00010000~0x00100000 | 통신 에러 — 거실/방1/방2/방3/방4 |
|
||||||
|
|
||||||
|
### 3.7 실 번호 (Room)
|
||||||
|
`1`=거실, `2`=방1, `3`=방2, `4`=방3, `5`=방4. (하위버스 MUX 채널 ch1~ch5 와 1:1)
|
||||||
|
|
||||||
|
### 3.8 부가모드 (AddMode, 비트맵)
|
||||||
|
DL 사양 시나리오 모드. 룸컨에서 토글하며 운전모드(3.2)에 덧씌워진다. 여러 비트 동시 가능.
|
||||||
|
|
||||||
|
| 비트 | 마스크 | 의미 | 비고 (사양서 8~9P) |
|
||||||
|
|------|--------|------|--------------------|
|
||||||
|
| 0 | 0x01 | 스마트수면 | 자동·풍량1단 고정, 1시간마다 CO2 기준 댐퍼 |
|
||||||
|
| 1 | 0x02 | 쾌적조리 | 렌지후드 연동(아래 3.9), 환기·3단 급기 |
|
||||||
|
| 2 | 0x04 | 안심회복 | 침실1 음압, 환기·2단 |
|
||||||
|
|
||||||
|
> `0x00` = 부가모드 없음. (회복모드 중 수면모드 불가 등 배타조건은 ERV 로직에서 처리)
|
||||||
|
|
||||||
|
### 3.9 후드 연동 (Hood, 비트맵)
|
||||||
|
렌지후드와 전열교환기 연동. 후드 동작 시 ERV도 동작해야 하므로 ERV가 후드 상태를 감지·연동한다(HOOD-485). 쾌적조리(3.8 bit1) 활성 시 적용.
|
||||||
|
|
||||||
|
| 비트 | 마스크 | 의미 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 0 | 0x01 | 후드연동 활성화(enable) |
|
||||||
|
| 1 | 0x02 | 후드 현재 동작중(ON) |
|
||||||
|
|
||||||
|
> 예: `0x03` = 연동 ON + 후드 가동중 → ERV 환기·3단 급기. `0x00` = 연동 없음.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 상위 버스 — ERV ↔ 각실분배기
|
||||||
|
|
||||||
|
> ERV(Master)가 분배기(유일 Slave)를 폴링한다. 분배기는 하위에서 모은 전 실 데이터를 **집계해 응답**한다.
|
||||||
|
> 멀티 분배기 확장 대비로 페이로드 선두에 `nodeId`(분배기 번호, 기본 `0x01`)를 둔다.
|
||||||
|
|
||||||
|
## 4. 상위 CMD 및 PAYLOAD
|
||||||
|
|
||||||
|
| CMD | 이름 | 방향 | PAYLOAD | 설명 |
|
||||||
|
|------|------|------|---------|------|
|
||||||
|
| 0x10 | `CMD_SYSTEM` | ERV→분배기 | 4.1 | 전원/모드/풍량/예약 + 실별 댐퍼·LED 타겟 (폴링 겸용) |
|
||||||
|
| 0x12 | `POLL_SENSOR` | ERV→분배기 | nodeId(1) | 전 실 센서 집계 요청 |
|
||||||
|
| 0x14 | `CMD_CONFIG` | ERV→분배기 | 4.5 | VSP 테이블·장치개수·ID 설정/저장 |
|
||||||
|
| 0x1F | `POLL_SPEC` | ERV→분배기 | nodeId(1) | 분배기 사양/버전 요청 |
|
||||||
|
| 0x90 | `RSP_ALLROOM_STATUS` | 분배기→ERV | 4.2 | 전 실 댐퍼·RPM·LED·룸컨명령·에러 집계 (0x10 응답) |
|
||||||
|
| 0x92 | `RSP_ALLROOM_SENSOR` | 분배기→ERV | 4.3 | 전 실 통합공기질 센서 집계 (0x12 응답) |
|
||||||
|
| 0x94 | `RSP_CONFIG` | 분배기→ERV | 4.5 | 설정 에코/저장결과 (0x14 응답) |
|
||||||
|
| 0x9F | `RSP_SPEC` | 분배기→ERV | 4.6 | 분배기 사양/버전 |
|
||||||
|
|
||||||
|
### 4.1 `CMD_SYSTEM` (0x10, ERV→분배기)
|
||||||
|
|
||||||
|
**글로벌(off 0~15, 16B)** + **실별 타겟 블록(6B) × 5실**
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 1 | nodeId | 분배기 번호(0x01) |
|
||||||
|
| 1 | 1 | power | 3.1 |
|
||||||
|
| 2 | 1 | runMode | 3.2 |
|
||||||
|
| 3 | 1 | fanSpeed | 3.3 |
|
||||||
|
| 4 | 1 | addMode | 부가모드 비트맵 (3.8) |
|
||||||
|
| 5 | 1 | hood | 후드 연동 비트맵 (3.9) |
|
||||||
|
| 6 | 1 | reserveHour | 0~8시간 |
|
||||||
|
| 7 | 4 | errorCode | ERV 시스템 에러 통보 (3.6) |
|
||||||
|
| 11 | 2 | outTemp | 외기온도 ×10 (signed) |
|
||||||
|
| 13 | 2 | inTemp | 내기온도 ×10 (signed) |
|
||||||
|
| 15 | 1 | roomCount | 실 수(=5) |
|
||||||
|
|
||||||
|
실별 타겟 블록(6B, roomCount회 반복) :
|
||||||
|
|
||||||
|
| off(상대) | 크기 | 필드 | 비고 |
|
||||||
|
|-----------|------|------|------|
|
||||||
|
| +0 | 1 | roomNo | 3.7 |
|
||||||
|
| +1 | 1 | saDamper1 | SA 디퓨저1 목표각 (3.5) |
|
||||||
|
| +2 | 1 | saDamper2 | SA 디퓨저2 (거실만, 그 외 0xFF) |
|
||||||
|
| +3 | 1 | raDamper1 | RA 디퓨저1 목표각 |
|
||||||
|
| +4 | 1 | raDamper2 | RA 디퓨저2 (거실만, 그 외 0xFF) |
|
||||||
|
| +5 | 1 | ledDim | 해당 실 RA 디퓨저 조명 밝기 (3.4) |
|
||||||
|
|
||||||
|
LEN = 16 + 6×5 = **46**.
|
||||||
|
|
||||||
|
### 4.2 `RSP_ALLROOM_STATUS` (0x90, 분배기→ERV)
|
||||||
|
|
||||||
|
**글로벌(8B)** + **실별 상태 블록(17B) × 5실**
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 1 | nodeId | |
|
||||||
|
| 1 | 1 | bunbagiState | 분배기 동작/에러 요약 |
|
||||||
|
| 2 | 1 | addModeReq | 룸컨이 요청한 부가모드 비트맵 (3.8) |
|
||||||
|
| 3 | 4 | errorCode | 전 실 통신·센서 에러 집계 (3.6) |
|
||||||
|
| 7 | 1 | roomCount | =5 |
|
||||||
|
|
||||||
|
실별 상태 블록(17B) :
|
||||||
|
|
||||||
|
| off(상대) | 크기 | 필드 | 비고 |
|
||||||
|
|-----------|------|------|------|
|
||||||
|
| +0 | 1 | roomNo | |
|
||||||
|
| +1 | 1 | saDamper1 | 실제 각도 |
|
||||||
|
| +2 | 1 | saDamper2 | (없으면 0xFF) |
|
||||||
|
| +3 | 1 | raDamper1 | |
|
||||||
|
| +4 | 1 | raDamper2 | |
|
||||||
|
| +5 | 2 | saRpm | SA 팬 실측 RPM |
|
||||||
|
| +7 | 2 | raRpm | RA 팬 실측 RPM |
|
||||||
|
| +9 | 1 | ledDim | 현재 RA 조명 LED 밝기 |
|
||||||
|
| +10 | 1 | rcCmdFlags | 룸컨 명령 비트(아래 5.5 cmdFlags 동일, bit7=부가모드 변경) |
|
||||||
|
| +11 | 1 | rcPower | 룸컨 설정 전원 |
|
||||||
|
| +12 | 1 | rcRunMode | 룸컨 설정 모드 |
|
||||||
|
| +13 | 1 | rcFanSpeed | 룸컨 설정 풍량 |
|
||||||
|
| +14 | 1 | rcReserveHour | 룸컨 설정 예약 |
|
||||||
|
| +15 | 1 | rcHeaterUV | bit0=히터,bit4=UV |
|
||||||
|
| +16 | 1 | rcFilterReset | 1=필터리셋 요청 |
|
||||||
|
|
||||||
|
LEN = 8 + 17×5 = **93**.
|
||||||
|
|
||||||
|
> `rcCmdFlags` 가 0 이 아니면, 해당 실 룸컨에서 사용자 조작이 발생했다는 뜻 → ERV가 수용해 운전상태 갱신.
|
||||||
|
|
||||||
|
### 4.3 `RSP_ALLROOM_SENSOR` (0x92, 분배기→ERV)
|
||||||
|
|
||||||
|
**글로벌(2B)** + **실별 센서 블록(19B) × 5실**
|
||||||
|
|
||||||
|
| off | 크기 | 필드 |
|
||||||
|
|-----|------|------|
|
||||||
|
| 0 | 1 | nodeId |
|
||||||
|
| 1 | 1 | roomCount(=5) |
|
||||||
|
|
||||||
|
실별 센서 블록(19B) :
|
||||||
|
|
||||||
|
| off(상대) | 크기 | 필드 | 단위 |
|
||||||
|
|-----------|------|------|------|
|
||||||
|
| +0 | 1 | roomNo | |
|
||||||
|
| +1 | 2 | pm1p0 | ㎍/㎥ |
|
||||||
|
| +3 | 2 | pm2p5 | ㎍/㎥ |
|
||||||
|
| +5 | 2 | pm4p0 | ㎍/㎥ |
|
||||||
|
| +7 | 2 | pm10p0 | ㎍/㎥ |
|
||||||
|
| +9 | 2 | humidity | %RH ×10 |
|
||||||
|
| +11 | 2 | temperature | ℃ ×10 (signed) |
|
||||||
|
| +13 | 2 | voc | TVOC index |
|
||||||
|
| +15 | 2 | nox | NOx index |
|
||||||
|
| +17 | 2 | co2 | ppm |
|
||||||
|
|
||||||
|
LEN = 2 + 19×5 = **97**.
|
||||||
|
|
||||||
|
### 4.5 `CMD_CONFIG` (0x14) / `RSP_CONFIG` (0x94)
|
||||||
|
|
||||||
|
VSP 풍량 테이블·장치개수·Modbus ID 설정.
|
||||||
|
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 1 | nodeId | |
|
||||||
|
| 1 | 1 | vspSelect | 0=None,1=환기,2=공청,3=바이패스 |
|
||||||
|
| 2 | 10 | sa1,ea1 … sa5,ea5 | 단별 VSP (각 1B) |
|
||||||
|
| 12 | 2 | rpmRefMid | 중 RPM 기준 |
|
||||||
|
| 14 | 2 | rpmDeltaMid | 중 RPM 편차 |
|
||||||
|
| 16 | 2 | rpmRefHigh | 강 RPM 기준 |
|
||||||
|
| 18 | 2 | rpmDeltaHigh | 강 RPM 편차 |
|
||||||
|
| 20 | 1 | roomconNum | 룸컨 수(1~5) |
|
||||||
|
| 21 | 1 | saDiffuserNum | SA 디퓨저 수(2~6) |
|
||||||
|
| 22 | 1 | raDiffuserNum | RA 디퓨저 수(2~6) |
|
||||||
|
| 23 | 1 | modbusId | 외부 홈넷 연동 ID |
|
||||||
|
| 24 | 1 | save | 1=EEPROM 저장 |
|
||||||
|
|
||||||
|
LEN = 25. `RSP_CONFIG` 는 동일 레이아웃 에코 + save 결과.
|
||||||
|
|
||||||
|
### 4.6 `RSP_SPEC` (0x9F)
|
||||||
|
| off | 크기 | 필드 |
|
||||||
|
|-----|------|------|
|
||||||
|
| 0 | 1 | nodeId |
|
||||||
|
| 1 | 2 | version (예 0x0117=Ver1.23) |
|
||||||
|
| 3 | 1 | deviceType |
|
||||||
|
| 4 | 1 | capability (bit0 히터/bit1 UV/bit2 후드연동) |
|
||||||
|
|
||||||
|
LEN = 5.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 하위 버스 — 각실분배기 ↔ 디퓨저 · 룸컨
|
||||||
|
|
||||||
|
> 분배기(로컬 Master)가 **MUX로 실 채널(ch1~ch5)을 선택**한 뒤 그 실의 SA/RA 디퓨저·룸컨을 폴링한다.
|
||||||
|
> 조명 LED(RA 디퓨저)는 `LCMD_DIFFUSER` 의 `ledDim` 으로 제어한다(3.4). 분배기 보드의 74HC595 LED는 별개(포트별 통신상태 표시, 진단용).
|
||||||
|
|
||||||
|
## 5. 하위 CMD 및 PAYLOAD
|
||||||
|
|
||||||
|
PAYLOAD 선두 3바이트는 장치 주소 `[DEV][ROOM][IDX]`.
|
||||||
|
- DEV : `0x10`=SA 디퓨저, `0x20`=RA 디퓨저, `0x30`=룸컨
|
||||||
|
- ROOM : 3.7 (채널과 1:1) / IDX : 같은 실·종류 일련번호(거실 SA·RA는 1,2)
|
||||||
|
|
||||||
|
| CMD | 이름 | 방향 | PAYLOAD | 설명 |
|
||||||
|
|------|------|------|---------|------|
|
||||||
|
| 0x20 | `LCMD_DIFFUSER` | 분배기→디퓨저 | 5.1 | 디퓨저 댐퍼/풍량 제어 + 폴링 |
|
||||||
|
| 0x21 | `LPOLL_ROOMCON` | 분배기→룸컨 | 5.4 | 룸컨 폴링 + ERV 상태 표시데이터 전달 |
|
||||||
|
| 0xA0 | `LRSP_DIFFUSER` | 디퓨저→분배기 | 5.2 | 댐퍼각/RPM/에러/버전 (0x20 응답) |
|
||||||
|
| 0xA1 | `LRSP_DIFFUSER_SENSOR` | 디퓨저→분배기 | 5.3 | 통합공기질 센서 (방별, 센서 부착 디퓨저) |
|
||||||
|
| 0xB0 | `LRSP_ROOMCON` | 룸컨→분배기 | 5.5 | 룸컨 사용자 명령 (0x21 응답) |
|
||||||
|
|
||||||
|
### 5.1 `LCMD_DIFFUSER` (0x20, 분배기→디퓨저)
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | DEV=0x10(SA)/0x20(RA) |
|
||||||
|
| 3 | 1 | power | 3.1 |
|
||||||
|
| 4 | 1 | runMode | 3.2 |
|
||||||
|
| 5 | 1 | fanSpeed | 3.3 |
|
||||||
|
| 6 | 1 | ledDim | RA 디퓨저 조명 밝기 (3.4) — **RA(0x20)만 유효, SA는 0** |
|
||||||
|
| 7 | 1 | dmpAngle | 목표 댐퍼각 (3.5) |
|
||||||
|
| 8 | 1 | dmpReset | 1=댐퍼 초기화 |
|
||||||
|
|
||||||
|
LEN = 9.
|
||||||
|
|
||||||
|
### 5.2 `LRSP_DIFFUSER` (0xA0, 디퓨저→분배기)
|
||||||
|
| off | 크기 | 필드 |
|
||||||
|
|-----|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX (에코) |
|
||||||
|
| 3 | 1 | power |
|
||||||
|
| 4 | 1 | runMode |
|
||||||
|
| 5 | 1 | fanSpeed |
|
||||||
|
| 6 | 1 | ledDim (현재 RA 조명 밝기, SA는 0) |
|
||||||
|
| 7 | 1 | dmpAngle (현재각) |
|
||||||
|
| 8 | 2 | rpm |
|
||||||
|
| 10 | 4 | errorCode (3.6) |
|
||||||
|
| 14 | 2 | version |
|
||||||
|
|
||||||
|
LEN = 16.
|
||||||
|
|
||||||
|
### 5.3 `LRSP_DIFFUSER_SENSOR` (0xA1, 디퓨저→분배기)
|
||||||
|
| off | 크기 | 필드 | 단위 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | 센서 부착 디퓨저(통상 RA) |
|
||||||
|
| 3 | 2 | pm1p0 | ㎍/㎥ |
|
||||||
|
| 5 | 2 | pm2p5 | ㎍/㎥ |
|
||||||
|
| 7 | 2 | pm4p0 | ㎍/㎥ |
|
||||||
|
| 9 | 2 | pm10p0 | ㎍/㎥ |
|
||||||
|
| 11 | 2 | humidity | %RH ×10 |
|
||||||
|
| 13 | 2 | temperature | ℃ ×10 |
|
||||||
|
| 15 | 2 | voc | TVOC |
|
||||||
|
| 17 | 2 | nox | NOx |
|
||||||
|
| 19 | 2 | co2 | ppm |
|
||||||
|
| 21 | 4 | errorCode | 3.6 |
|
||||||
|
|
||||||
|
LEN = 25.
|
||||||
|
|
||||||
|
### 5.4 `LPOLL_ROOMCON` (0x21, 분배기→룸컨)
|
||||||
|
분배기가 ERV로부터 받은 동작상태를 룸컨에 전달(표시용) + 룸컨 명령 회수.
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | DEV=0x30 |
|
||||||
|
| 3 | 1 | power | ERV 현재 전원 |
|
||||||
|
| 4 | 1 | runMode | ERV 현재 모드 |
|
||||||
|
| 5 | 1 | fanSpeed | ERV 현재 풍량 |
|
||||||
|
| 6 | 1 | addMode | 현재 부가모드 비트맵 (3.8) |
|
||||||
|
| 7 | 1 | hood | 후드 연동 상태 (3.9) |
|
||||||
|
| 8 | 1 | reserveRemain | 예약 잔여(h) |
|
||||||
|
| 9 | 4 | errorCode | 3.6 |
|
||||||
|
| 13 | 2 | outTemp | ×10 |
|
||||||
|
| 15 | 2 | inTemp | ×10 |
|
||||||
|
| 17 | 1 | ackFlags | 직전 룸컨 명령 수용 비트 |
|
||||||
|
|
||||||
|
LEN = 18.
|
||||||
|
|
||||||
|
### 5.5 `LRSP_ROOMCON` (0xB0, 룸컨→분배기)
|
||||||
|
| off | 크기 | 필드 | 비고 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| 0 | 3 | DEV/ROOM/IDX | DEV=0x30 |
|
||||||
|
| 3 | 1 | cmdFlags | bit0 power,1 runMode,2 fanSpeed,3 reserveHour,4 heaterUV,5 filterReset,6 ledDim,**7 addMode** |
|
||||||
|
| 4 | 1 | power | 3.1 |
|
||||||
|
| 5 | 1 | runMode | 3.2 |
|
||||||
|
| 6 | 1 | fanSpeed | 3.3 |
|
||||||
|
| 7 | 1 | reserveHour | 0~8 |
|
||||||
|
| 8 | 1 | heaterUV | bit0 히터,bit4 UV |
|
||||||
|
| 9 | 1 | filterReset | 1=리셋 |
|
||||||
|
| 10 | 1 | addMode | 부가모드 토글 비트맵 (3.8) |
|
||||||
|
| 11 | 2 | version | |
|
||||||
|
|
||||||
|
LEN = 13.
|
||||||
|
> 룸컨이 보낸 명령은 분배기가 모아 상위 `RSP_ALLROOM_STATUS` 의 `rc*` 필드로 ERV에 전달 → ERV가 판단 후 다시 하달.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 동작 시나리오 & 폴링 타이밍
|
||||||
|
|
||||||
|
**두 버스가 독립적으로 동작**한다.
|
||||||
|
|
||||||
|
**하위 버스 (분배기 로컬 루프)** — 분배기가 ch1~ch5를 MUX로 돌며 각 실의 SA/RA 디퓨저·룸컨 폴링. 케이블이 짧고 전용 채널이라 빠르게 순환하며 전 실 상태를 캐싱.
|
||||||
|
|
||||||
|
**상위 버스 (ERV 루프)** — ERV는 **분배기 1대만** 주기적으로 폴링:
|
||||||
|
1. ERV → 분배기 `CMD_SYSTEM(0x10)` (전원/모드/풍량 + 실별 댐퍼·LED 타겟) → 분배기 `RSP_ALLROOM_STATUS(0x90)` (전 실 댐퍼·RPM·LED·룸컨명령·에러)
|
||||||
|
2. ERV → 분배기 `POLL_SENSOR(0x12)` → 분배기 `RSP_ALLROOM_SENSOR(0x92)` (전 실 센서)
|
||||||
|
3. ERV가 센서로 **부하점수·집중/분산 계산**(사양서 10~11P) → 다음 `CMD_SYSTEM` 의 실별 댐퍼/LED/풍량 타겟에 반영
|
||||||
|
4. 룸컨 조작은 `RSP_ALLROOM_STATUS.rcCmdFlags`(+`addModeReq`) 로 ERV에 보고 → ERV 수용 → 다음 `CMD_SYSTEM` 으로 반영
|
||||||
|
5. **부가모드(3.8)** : 룸컨에서 스마트수면/쾌적조리/안심회복 토글 → ERV가 해당 시나리오 로직 수행(댐퍼·풍량 타겟 조정) → `CMD_SYSTEM.addMode` 로 현재 상태 회신(룸컨 표시)
|
||||||
|
6. **후드 연동(3.9)** : ERV가 HOOD-485로 후드 ON 감지 → (쾌적조리 활성 시) ERV 환기·3단 급기 동작 → `CMD_SYSTEM.hood` 로 후드 상태를 분배기·룸컨에 전달. 후드 OFF 시 이전 모드 복귀
|
||||||
|
7. 통신 단절 : 분배기가 실별 통신에러 비트(3.6)를 set 해 ERV에 보고, 재연결 시 clear
|
||||||
|
|
||||||
|
### 폴링 시간 (1회 = 300ms 가정)
|
||||||
|
|
||||||
|
| 버스 | 폴링 대상 | 1주기 |
|
||||||
|
|------|-----------|-------|
|
||||||
|
| **상위 (ERV↔분배기)** | 2회 (CMD_SYSTEM + POLL_SENSOR) | **600 ms** |
|
||||||
|
| 하위 (분배기↔실) | 17회(SA6+RA6+룸컨5) — **로컬에서 병렬 진행** | 분배기 내부에서 순환(ERV와 무관) |
|
||||||
|
|
||||||
|
> Rev 2.0 은 ERV가 17대를 직접 폴링 → 1주기 **5.1초**.
|
||||||
|
> Rev 3.0 은 ERV 입장에서 **0.6초**면 전 실 상태를 받는다. 하위 루프는 분배기가 별도로 빠르게 순환하므로 ERV 주기와 분리된다. → **갱신 약 8배 빨라지고 ERV 부하 격감.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 펌웨어 반영 메모
|
||||||
|
|
||||||
|
- **분배기 펌웨어(Nano100)** : 신규. UART1(`SA485`, PB.4/PB.5)=하위 로컬 마스터, UART2(`M485`, PA.8/PA.9)=상위 슬레이브. MUX(SA_MUX_A/B/C/EN)로 채널 선택, DIR(SA485_DIR_01~05) 채널별. 74HC595(LED_DS/SCK/LCK)는 **포트별 SA/RA 통신상태 표시 LED** 구동(진단용). 방 조명 LED는 RA 디퓨저가 자체 점등하므로 ledDim 명령만 중계.
|
||||||
|
- **ERV 펌웨어** : `My_Uart.c` 의 17대 직접 폴링 → **분배기 1대 상위 프로토콜**로 교체. 자동로직(부하점수·집중/분산)은 ERV에 유지.
|
||||||
|
- **`My_RJ2.c`(DL 룸컨 232)는 변경하지 않는다.**
|
||||||
|
- CRC : 기존 `CRC16()`(MODBUS) 공유, 빅엔디안(Hi→Lo).
|
||||||
|
- 에러코드 `uint32_t` (방4 비트 수용).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 송수신 예제
|
||||||
|
|
||||||
|
> 모든 프레임 CRC는 **STX 제외 `CMD~PAYLOAD`** 구간 CRC-16/MODBUS(빅엔디안)의 실제 계산값.
|
||||||
|
> 예시 : runMode=`0x02`(자동), fan=`2`(중), addMode=`0x02`(쾌적조리), hood=`0x03`(연동ON+가동중), 댐퍼 `0xB4`=열림/`0x00`=닫힘/`0xFF`=없음, 온·습도 ×10. 폴링 1회=300ms.
|
||||||
|
|
||||||
|
### 8-A. 상위 버스 (ERV ↔ 분배기)
|
||||||
|
|
||||||
|
**① ERV → 분배기 : 전원ON·자동·풍량2·쾌적조리·후드가동 + 실별 댐퍼/LED 타겟** — `CMD_SYSTEM(0x10)` *(t=0~300ms)*
|
||||||
|
```
|
||||||
|
[TX] AA 10 2E | 01 01 02 02 02 03 00 00000000 0096 00DC 05 | <실1..5 타겟 6B> | AA 01
|
||||||
|
CMD LEN node pw md fan add hood rsv err 외15℃ 내22℃ rooms=5
|
||||||
|
↑쾌적조리 ↑후드연동ON+가동
|
||||||
|
실1(거실): 01 B4 B4 B4 B4 05 (SA1·SA2·RA1·RA2 열림, RA조명 LED 5)
|
||||||
|
실2(방1) : 02 B4 FF B4 FF 04
|
||||||
|
실3(방2) : 03 00 FF 00 FF 00 (댐퍼 닫힘, LED OFF)
|
||||||
|
실4(방3) : 04 B4 FF B4 FF 06
|
||||||
|
실5(방4) : 05 B4 FF B4 FF 03
|
||||||
|
```
|
||||||
|
|
||||||
|
**② 분배기 → ERV : 전 실 상태 집계** — `RSP_ALLROOM_STATUS(0x90)`
|
||||||
|
```
|
||||||
|
[RX] AA 90 5D | 01 00 02 00000000 05 | <실1..5 상태 17B> | C5 31
|
||||||
|
node bunSt addModeReq=02 err rooms=5
|
||||||
|
실1(거실): 01 B4 B4 B4 B4 0352 0334 05 00 01 02 02 00 00 00
|
||||||
|
roomNo SA1 SA2 RA1 RA2 saRpm=850 raRpm=820 ledDim | rcFlags pw md fan rsv hu fr
|
||||||
|
실2(방1) : 02 B4 FF B4 FF 0348 032A 04 00 01 02 02 00 00 00
|
||||||
|
실3(방2) : 03 00 FF 00 FF 0000 0000 00 00 01 02 02 00 00 00
|
||||||
|
실4(방3) : 04 B4 FF B4 FF 034D 032F 06 00 01 02 02 00 00 00
|
||||||
|
실5(방4) : 05 B4 FF B4 FF 0350 0332 03 00 01 02 02 00 00 00
|
||||||
|
```
|
||||||
|
|
||||||
|
**③ ERV → 분배기 : 센서 요청 / 분배기 응답** — `POLL_SENSOR(0x12)` → `RSP_ALLROOM_SENSOR(0x92)` *(t=300~600ms)*
|
||||||
|
```
|
||||||
|
[TX] AA 12 01 | 01 | <CRC>
|
||||||
|
[RX] AA 92 61 | 01 05 | <실1..5 센서 19B> | DC 05
|
||||||
|
실1(거실): 01 0005 0008 0009 000B 01C2 00DC 0064 0001 028A
|
||||||
|
roomNo pm1 pm2.5 pm4 pm10 습45.0% 온22.0℃ voc100 nox1 co2=650
|
||||||
|
실2(방1) : 02 0007 000C 000D 0012 01C4 00DD 0082 0001 02D0 (co2=720)
|
||||||
|
실3(방2) : 03 0004 0006 0007 0009 01C0 00DC 005A 0001 0262 (co2=610)
|
||||||
|
실4(방3) : 04 0009 0014 0016 001C 01C7 00DB 00A0 0002 032A (co2=810)
|
||||||
|
실5(방4) : 05 0005 0007 0008 000A 01C1 00DC 005F 0001 0280 (co2=640)
|
||||||
|
```
|
||||||
|
> ERV는 ②③으로 **전 실 상태+센서를 0.6초만에** 확보 → 부하점수·집중/분산 계산 → 다음 `CMD_SYSTEM` 타겟 갱신.
|
||||||
|
|
||||||
|
### 8-B. 하위 버스 (분배기 ↔ 디퓨저/룸컨) — 분배기 로컬 루프
|
||||||
|
|
||||||
|
**거실 RA1 디퓨저 제어/폴링** — `LCMD_DIFFUSER(0x20)` → `LRSP_DIFFUSER(0xA0)` (RA = 조명 LED 보유)
|
||||||
|
```
|
||||||
|
[TX] AA 20 09 | 20 01 01 | 01 02 02 05 B4 00 | 6E 2B (RA,거실,1 / power mode fan led=5 dmp=B4 reset=0)
|
||||||
|
[RX] AA A0 10 | 20 01 01 | 01 02 02 05 B4 0352 00000000 0117 | ... (led현재5, dmp B4, rpm850, err0, ver1.23)
|
||||||
|
```
|
||||||
|
> SA 디퓨저는 LED가 없으므로 `ledDim=0`. 예) 거실 SA1 TX: `AA 20 09 10 01 01 01 02 02 00 B4 00 6E 6F` → RX `AA A0 10 10 01 01 01 02 02 00 B4 03 52 00 00 00 00 01 17 B9 17`.
|
||||||
|
|
||||||
|
**거실 RA1 디퓨저 센서** — `LRSP_DIFFUSER_SENSOR(0xA1)`
|
||||||
|
```
|
||||||
|
[RX] AA A1 19 20 01 01 00 05 00 08 00 09 00 0B 01 C2 00 DC 00 64 00 01 02 8A 00 00 00 00 58 AE
|
||||||
|
(RA,거실,1) pm1=5 pm2.5=8 pm4=9 pm10=11 습45.0% 온22.0℃ voc100 nox1 co2=650 err0
|
||||||
|
```
|
||||||
|
|
||||||
|
**거실 룸컨 폴링** — `LPOLL_ROOMCON(0x21)` → `LRSP_ROOMCON(0xB0)`
|
||||||
|
```
|
||||||
|
[TX] AA 21 12 30 01 01 01 02 02 02 03 00 00 00 00 00 00 96 00 DC 00 92 FA
|
||||||
|
(ERV상태 전달: 자동·풍량2·쾌적조리(02)·후드연동ON+가동(03))
|
||||||
|
[RX] AA B0 0D 30 01 01 80 01 02 02 00 00 00 02 01 10 34 4D
|
||||||
|
cmdFlags=80(부가모드 변경) ... addMode=02(쾌적조리 토글) ver0x0110
|
||||||
|
```
|
||||||
|
> 룸컨이 부가모드(쾌적조리)를 토글하면 `cmdFlags` bit7=1·`addMode`=0x02 로 보고 → 분배기가 상위 `RSP_ALLROOM_STATUS.addModeReq`·`rcCmdFlags`에 실어 ERV에 전달 → ERV가 후드연동 운전 수행.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 본 문서는 2-Tier 재정의 초안(Rev 3.0)이다. 분배기/디퓨저/룸컨 펌웨어 담당과 CMD 코드·필드 세부값을 상호 합의하여 확정한다.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
@@ -0,0 +1,112 @@
|
|||||||
|
<Application x:Class="DiffuserSimulator.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
StartupUri="MainWindow.xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
<Color x:Key="PrimaryBg">#1E1E2E</Color>
|
||||||
|
<Color x:Key="SecondaryBg">#2B2B3D</Color>
|
||||||
|
<Color x:Key="CardBg">#313147</Color>
|
||||||
|
<Color x:Key="AccentBlue">#7AA2F7</Color>
|
||||||
|
<Color x:Key="AccentGreen">#9ECE6A</Color>
|
||||||
|
<Color x:Key="AccentRed">#F7768E</Color>
|
||||||
|
<Color x:Key="AccentYellow">#E0AF68</Color>
|
||||||
|
<Color x:Key="AccentCyan">#7DCFFF</Color>
|
||||||
|
<Color x:Key="AccentPurple">#BB9AF7</Color>
|
||||||
|
<Color x:Key="TextPrimary">#C0CAF5</Color>
|
||||||
|
<Color x:Key="TextSecondary">#565F89</Color>
|
||||||
|
<Color x:Key="BorderColor">#3B3B55</Color>
|
||||||
|
|
||||||
|
<SolidColorBrush x:Key="PrimaryBgBrush" Color="{StaticResource PrimaryBg}"/>
|
||||||
|
<SolidColorBrush x:Key="SecondaryBgBrush" Color="{StaticResource SecondaryBg}"/>
|
||||||
|
<SolidColorBrush x:Key="CardBgBrush" Color="{StaticResource CardBg}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentBlueBrush" Color="{StaticResource AccentBlue}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentGreenBrush" Color="{StaticResource AccentGreen}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentRedBrush" Color="{StaticResource AccentRed}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentYellowBrush" Color="{StaticResource AccentYellow}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentCyanBrush" Color="{StaticResource AccentCyan}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentPurpleBrush" Color="{StaticResource AccentPurple}"/>
|
||||||
|
<SolidColorBrush x:Key="TextPrimaryBrush" Color="{StaticResource TextPrimary}"/>
|
||||||
|
<SolidColorBrush x:Key="TextSecondaryBrush" Color="{StaticResource TextSecondary}"/>
|
||||||
|
<SolidColorBrush x:Key="BorderBrush" Color="{StaticResource BorderColor}"/>
|
||||||
|
|
||||||
|
<Style x:Key="ModernButton" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="{StaticResource AccentBlueBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Padding" Value="18,8"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="border" Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="6" Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="border" Property="Opacity" Value="0.85"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="border" Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ToggleSwitch" TargetType="ToggleButton">
|
||||||
|
<Setter Property="Width" Value="56"/>
|
||||||
|
<Setter Property="Height" Value="28"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ToggleButton">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="track" CornerRadius="14" Background="#3B3B55" Width="56" Height="28"/>
|
||||||
|
<Border x:Name="thumb" CornerRadius="11" Background="#565F89" Width="22" Height="22"
|
||||||
|
HorizontalAlignment="Left" Margin="3,0,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="track" Property="Background" Value="{StaticResource AccentGreenBrush}"/>
|
||||||
|
<Setter TargetName="thumb" Property="Background" Value="White"/>
|
||||||
|
<Setter TargetName="thumb" Property="HorizontalAlignment" Value="Right"/>
|
||||||
|
<Setter TargetName="thumb" Property="Margin" Value="0,0,3,0"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModernComboBox" TargetType="ComboBox">
|
||||||
|
<Setter Property="Background" Value="{StaticResource CardBgBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="8,5"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<!-- 드롭다운 목록은 시스템 기본 흰색 배경이므로 항목 글자색을 검정으로 -->
|
||||||
|
<Setter Property="ItemContainerStyle">
|
||||||
|
<Setter.Value>
|
||||||
|
<Style TargetType="ComboBoxItem">
|
||||||
|
<Setter Property="Foreground" Value="Black"/>
|
||||||
|
</Style>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModernTextBox" TargetType="TextBox">
|
||||||
|
<Setter Property="Background" Value="{StaticResource CardBgBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="8,5"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
</Style>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace DiffuserSimulator
|
||||||
|
{
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>DiffuserSimulator</RootNamespace>
|
||||||
|
<AssemblyName>DiffuserSimulator</AssemblyName>
|
||||||
|
<StartupObject>DiffuserSimulator.App</StartupObject>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
|
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="System.IO.Ports" Version="10.0.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<Window x:Class="DiffuserSimulator.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="디퓨져 시뮬레이터 - Diffuser Simulator"
|
||||||
|
Width="1400" Height="970"
|
||||||
|
MinWidth="1300" MinHeight="930"
|
||||||
|
Background="{StaticResource PrimaryBgBrush}"
|
||||||
|
WindowStartupLocation="CenterScreen">
|
||||||
|
|
||||||
|
<Grid Margin="14">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="120"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Row 0: 연결 설정 -->
|
||||||
|
<Border Grid.Row="0" Background="{StaticResource SecondaryBgBrush}"
|
||||||
|
CornerRadius="10" Padding="18,10" Margin="0,0,0,8">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="디퓨져 시뮬레이터" FontSize="18" FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource AccentCyanBrush}" Margin="0,0,14,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="0,0,20,0">
|
||||||
|
<TextBlock Text="만든이 : 전경선" Foreground="{StaticResource TextSecondaryBrush}" FontSize="10"/>
|
||||||
|
<TextBlock Text="수정일 : 2026.03.28 ~ 2026.06.08" Foreground="{StaticResource TextSecondaryBrush}" FontSize="10"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 제품 모드 전역 선택 : 토글 버튼 (DL ⇄ 힘펠). DL=LED디밍 활성·RA2 비활성·방4 비활성 -->
|
||||||
|
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="제품사양" Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,8,0" FontSize="12" FontWeight="SemiBold"/>
|
||||||
|
<Button x:Name="btnProductMode" Content="DL" Width="96" Padding="14,7"
|
||||||
|
Style="{StaticResource ModernButton}" Background="{StaticResource AccentBlueBrush}"
|
||||||
|
Click="ProductMode_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="3" Orientation="Horizontal">
|
||||||
|
<TextBlock Text="통신포트" Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,6,0" FontSize="12" FontWeight="SemiBold"/>
|
||||||
|
<ComboBox x:Name="cmbPort" Width="100" Style="{StaticResource ModernComboBox}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||||
|
<Button Content="⟳" Width="30" Height="30" FontSize="13"
|
||||||
|
Style="{StaticResource ModernButton}" Click="RefreshPorts_Click"
|
||||||
|
Background="{StaticResource CardBgBrush}" Margin="0,0,12,0" Padding="0"/>
|
||||||
|
|
||||||
|
<Button x:Name="btnAutoChange" Content="자동변경" Style="{StaticResource ModernButton}"
|
||||||
|
Background="{StaticResource AccentBlueBrush}" VerticalAlignment="Center"
|
||||||
|
Margin="0,0,12,0" Padding="14,7" FontSize="11" Click="AutoChange_Click"
|
||||||
|
ToolTip="거실→방1~3 순서로 30초마다 오염레벨 0→1→2→3→4 자동 변경"/>
|
||||||
|
|
||||||
|
<TextBlock Text="폴링(ms)" Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,6,0" FontSize="11" FontWeight="SemiBold"/>
|
||||||
|
<ComboBox x:Name="cmbInterval" Width="75" Style="{StaticResource ModernComboBox}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,0" SelectedIndex="3">
|
||||||
|
<ComboBoxItem Content="200"/>
|
||||||
|
<ComboBoxItem Content="300"/>
|
||||||
|
<ComboBoxItem Content="500"/>
|
||||||
|
<ComboBoxItem Content="1000"/>
|
||||||
|
<ComboBoxItem Content="2000"/>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<Ellipse x:Name="statusLed" Width="10" Height="10" Fill="#F7768E" Margin="0,0,6,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="txtStatus" Text="미연결" Foreground="{StaticResource TextSecondaryBrush}"
|
||||||
|
FontSize="12" VerticalAlignment="Center" Margin="0,0,12,0"/>
|
||||||
|
|
||||||
|
<Button x:Name="btnConnect" Content="연결" Style="{StaticResource ModernButton}"
|
||||||
|
Click="Connect_Click" Margin="0,0,6,0" Padding="14,7"/>
|
||||||
|
<Button x:Name="btnStart" Content="통신 시작" Style="{StaticResource ModernButton}"
|
||||||
|
Background="{StaticResource AccentGreenBrush}" Click="Start_Click"
|
||||||
|
IsEnabled="False" Margin="0,0,6,0" Padding="14,7"/>
|
||||||
|
<Button x:Name="btnStop" Content="통신 중지" Style="{StaticResource ModernButton}"
|
||||||
|
Background="{StaticResource AccentRedBrush}" Click="Stop_Click"
|
||||||
|
IsEnabled="False" Padding="14,7"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Row 1: 5개 방 패널 -->
|
||||||
|
<UniformGrid Grid.Row="1" x:Name="roomGrid" Rows="1" Columns="5" Margin="0,0,0,8"/>
|
||||||
|
|
||||||
|
<!-- Row 2: 통신 로그 -->
|
||||||
|
<Border Grid.Row="2" Background="{StaticResource SecondaryBgBrush}" CornerRadius="10" Padding="12">
|
||||||
|
<DockPanel>
|
||||||
|
<Grid DockPanel.Dock="Top" Margin="0,0,0,5">
|
||||||
|
<TextBlock Text="통신 로그" FontSize="12" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button Content="로그 저장" Style="{StaticResource ModernButton}" Background="{StaticResource AccentBlueBrush}"
|
||||||
|
Padding="10,3" FontSize="11" Click="SaveLog_Click" Margin="0,0,6,0"/>
|
||||||
|
<Button Content="로그 지우기" Style="{StaticResource ModernButton}" Background="{StaticResource CardBgBrush}"
|
||||||
|
Padding="10,3" FontSize="11" Click="ClearLog_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<TextBox x:Name="txtLog" IsReadOnly="True" Background="{StaticResource CardBgBrush}"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}"
|
||||||
|
BorderThickness="1" FontFamily="Consolas" FontSize="10"
|
||||||
|
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
|
||||||
|
TextWrapping="NoWrap" Padding="6"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,811 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace DiffuserSimulator
|
||||||
|
{
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
private readonly SlaveProtocol _slave = new();
|
||||||
|
// 각실 패널(레이아웃은 RoomPanel.xaml — 디자이너 편집). 컨트롤은 internal 필드로 직접 접근.
|
||||||
|
private readonly RoomPanel[] _ui = new RoomPanel[5];
|
||||||
|
private bool _updating;
|
||||||
|
private bool _himpel; // 제품 모드 : false=DL / true=힘펠 (전역)
|
||||||
|
|
||||||
|
// 자동변경 : 거실→방1→방2→방3 순서로 30초마다 오염레벨 0→1→2→3→4 자동 적용
|
||||||
|
private readonly System.Windows.Threading.DispatcherTimer _autoTimer =
|
||||||
|
new() { Interval = TimeSpan.FromSeconds(30) };
|
||||||
|
private int _autoStep; // 0..19 (room = step/5, level = step%5)
|
||||||
|
private bool _autoRunning;
|
||||||
|
|
||||||
|
private static readonly string[] RoomNames = { "거실", "방 1", "방 2", "방 3", "방 4" };
|
||||||
|
private static readonly Color[] RoomColors =
|
||||||
|
{
|
||||||
|
Color.FromRgb(0x7D,0xCF,0xFF), Color.FromRgb(0x9E,0xCE,0x6A),
|
||||||
|
Color.FromRgb(0xE0,0xAF,0x68), Color.FromRgb(0xBB,0x9A,0xF7),
|
||||||
|
Color.FromRgb(0xF7,0x76,0x8E)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 프리셋 값 — 히스테리시스 프리셋별 임계 밴드(CLAUDE.md)의 '중앙값'.
|
||||||
|
// 선택한 프리셋모드에 맞춰 좋음=L0 / 보통=L1 / 나쁨=L2 / 매우나쁨=L3 / 최악(빨강)=L4 로 정확히 분류되도록 함.
|
||||||
|
// [프리셋 0 ECO / 1 NORMAL / 2 TURBO / 3 힘펠][레벨 0~4] — index 4 = L4(임계 상한 초과, ERV 부하점수 4)
|
||||||
|
// 힘펠 사양(룸컨 COLOR) : CO2 0-700/701-1000/1001-1500/1501↑, PM2.5 0-15/16-35/36-75/76↑, TVOC 0-100/101-400/401-1000/1001↑
|
||||||
|
// (힘펠은 PM10/VOC 임계가 99999 캡이라 Band 분류상 L4 도달 불가 → 4단계는 ECO/NORMAL/TURBO 용)
|
||||||
|
private static readonly int[][] PrePM25 = { new[]{10,30,50,75,95}, new[]{7,22,40,60,80}, new[]{6,18,31,45,60}, new[]{7,25,55,90,110} };
|
||||||
|
private static readonly int[][] PrePM10 = { new[]{20,63,106,150,185}, new[]{14,47,85,120,150}, new[]{12,39,66,91,115}, new[]{0,0,0,0,0} };
|
||||||
|
private static readonly int[][] PreCO2 = { new[]{500,1150,1450,1800,2100}, new[]{400,850,1150,1450,1700}, new[]{300,700,900,1100,1300}, new[]{350,850,1250,1750,1700} };
|
||||||
|
private static readonly int[][] PreVOC = { new[]{85,183,252,370,460}, new[]{60,135,200,300,400}, new[]{52,112,156,228,290}, new[]{17,115,270,408,500} };
|
||||||
|
private static readonly int[][] PreTVOC = { new[]{50,250,700,1500,1800}, new[]{50,250,700,1500,1800}, new[]{50,250,700,1500,1800}, new[]{50,250,700,1500,1800} };
|
||||||
|
|
||||||
|
// 분류용 상한 임계 [프리셋][L1~L3] (그 이상 = 매우나쁨) — ECO/NORMAL/TURBO 는 ErvState 와 동일, 힘펠은 룸컨 사양
|
||||||
|
private static readonly int[][] ThrCO2 = { new[]{1000,1300,1600,2000}, new[]{700,1000,1300,1600}, new[]{600,800,1000,1200}, new[]{700,1000,1500,99999} };
|
||||||
|
private static readonly int[][] ThrPM25 = { new[]{20,38,60,86}, new[]{14,29,49,69}, new[]{12,23,38,52}, new[]{15,35,75,99999} };
|
||||||
|
private static readonly int[][] ThrPM10 = { new[]{40,86,126,173}, new[]{28,66,102,138}, new[]{24,53,78,104}, new[]{99999,99999,99999,99999} };
|
||||||
|
private static readonly int[][] ThrVOC = { new[]{171,195,308,438}, new[]{120,150,250,350}, new[]{103,120,192,263}, new[]{99999,99999,99999,99999} };
|
||||||
|
|
||||||
|
private static readonly byte[] PreStatus = { 0x04, 0x03, 0x02, 0x01, 0x01 }; /* L4 도 매우나쁨(0x01) */
|
||||||
|
private const int PresetNoSensor = 5; /* level 5 = 센서없음 (L0~3 + L4 최악) */
|
||||||
|
|
||||||
|
// 힘펠 제품 모드 : 공기질 레벨(0 좋음 / 1 보통 / 2 나쁨 / 3 매우나쁨, 4 최악) → 댐퍼 각도 자동
|
||||||
|
private static readonly byte[] HimpelDamperAngle = { 0, 50, 65, 110, 110 };
|
||||||
|
|
||||||
|
// 실별 선택 상태 : 프리셋모드(0 ECO/1 NORMAL/2 TURBO, 기본 NORMAL) / 공기질 레벨(0~3 or 센서없음, 기본 보통)
|
||||||
|
private readonly int[] _roomPreset = { 1, 1, 1, 1, 1 };
|
||||||
|
private readonly int[] _roomQuality = { 1, 1, 1, 1, 1 };
|
||||||
|
|
||||||
|
private static int Band(int v, int[] t) => v <= t[0] ? 0 : v <= t[1] ? 1 : v <= t[2] ? 2 : v <= t[3] ? 3 : 4;
|
||||||
|
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_slave.LogMessage += OnLog;
|
||||||
|
_slave.MasterPacketReceived += OnMasterPacket;
|
||||||
|
_slave.SlavePacketReceived += OnSlavePacket;
|
||||||
|
_slave.ResponseSent += OnResponseSent;
|
||||||
|
_slave.MasterPollSent += OnMasterPollSent;
|
||||||
|
_slave.ConnectionChanged += OnConnectionChanged;
|
||||||
|
|
||||||
|
BuildRoomPanels();
|
||||||
|
RefreshPorts();
|
||||||
|
ApplySlaveUi(); // 슬레이브 전용 UI 상태(각도 readonly 등)
|
||||||
|
_autoTimer.Tick += AutoTick;
|
||||||
|
Closed += (_, _) => { _autoTimer.Stop(); _slave.Dispose(); };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 5개 방 패널 생성 (레이아웃=RoomPanel.xaml, 동작=여기서 연결) ==========
|
||||||
|
private void BuildRoomPanels()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
int idx = i;
|
||||||
|
var u = new RoomPanel();
|
||||||
|
u.SetHeader(RoomNames[i], RoomColors[i]);
|
||||||
|
_ui[i] = u;
|
||||||
|
roomGrid.Children.Add(u);
|
||||||
|
|
||||||
|
// ---- 헤더 활성 체크 ----
|
||||||
|
u.ChkEnabled.Checked += (s, e) =>
|
||||||
|
{
|
||||||
|
var room = _slave.Rooms[idx];
|
||||||
|
// 처음 상태 reset: damper 0, LED 0, 센서 보통 preset, 양쪽 toggle OFF
|
||||||
|
room.Enabled = true;
|
||||||
|
room.DamperAngleSA = 0;
|
||||||
|
room.DamperAngleEA = 0;
|
||||||
|
room.LedBrightness = 0;
|
||||||
|
room.PollSA = true; // Enabled 면 SA/RA 모두 응답
|
||||||
|
room.PollRA = true;
|
||||||
|
|
||||||
|
// UI 동기화 (event re-entrant 차단)
|
||||||
|
_updating = true;
|
||||||
|
_ui[idx].TbPM25.Text = "25";
|
||||||
|
_ui[idx].TbPM10.Text = "30";
|
||||||
|
_ui[idx].TbCO2.Text = "850";
|
||||||
|
_ui[idx].TbVOC.Text = "115";
|
||||||
|
_ui[idx].TbTVOC.Text = "250";
|
||||||
|
_ui[idx].TbTemp.Text = "25";
|
||||||
|
_ui[idx].TbHumidity.Text = "50";
|
||||||
|
_ui[idx].TbSAAngle.Text = "0";
|
||||||
|
_ui[idx].TbEAAngle.Text = "0";
|
||||||
|
_ui[idx].SldLed.Value = 0;
|
||||||
|
_ui[idx].TxtLedVal.Text = "0 (OFF)";
|
||||||
|
_ui[idx].TglSA.IsChecked = false;
|
||||||
|
_ui[idx].TglEA.IsChecked = false;
|
||||||
|
_ui[idx].RbNormal.IsChecked = true;
|
||||||
|
|
||||||
|
// 거실(idx 0) : 거실2(ID2 0x00 = RA2/SA2)도 함께 활성·초기화
|
||||||
|
if (idx == 0)
|
||||||
|
{
|
||||||
|
var r2 = _slave.Rooms[SlaveProtocol.LivingRoom2Index];
|
||||||
|
r2.Enabled = true;
|
||||||
|
r2.PollSA = true;
|
||||||
|
r2.PollRA = true;
|
||||||
|
r2.DamperAngleSA = 0;
|
||||||
|
r2.DamperAngleEA = 0;
|
||||||
|
_ui[0].TbEAAngle2.Text = "0";
|
||||||
|
_ui[0].TbSAAngle2.Text = "0";
|
||||||
|
}
|
||||||
|
_updating = false;
|
||||||
|
|
||||||
|
SyncRoomFromUI(idx);
|
||||||
|
};
|
||||||
|
u.ChkEnabled.Unchecked += (s, e) =>
|
||||||
|
{
|
||||||
|
_slave.Rooms[idx].Enabled = false;
|
||||||
|
if (idx == 0) _slave.Rooms[SlaveProtocol.LivingRoom2Index].Enabled = false;
|
||||||
|
};
|
||||||
|
_slave.Rooms[i].Enabled = (i == 0);
|
||||||
|
|
||||||
|
// ---- 배기(RA) 디퓨저 ----
|
||||||
|
// Slave 모드: ON → master 의 RA polling 에 응답 / OFF → 무응답
|
||||||
|
// Master 모드: ON → RA 폴링 송신 / OFF → skip
|
||||||
|
u.TglEA.Checked += (s, e) => { if (!_updating) _slave.Rooms[idx].PollRA = true; };
|
||||||
|
u.TglEA.Unchecked += (s, e) => { if (!_updating) _slave.Rooms[idx].PollRA = false; };
|
||||||
|
u.TbEAAngle.TextChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
if (_slave.Mode == SimMode.Master && byte.TryParse(u.TbEAAngle.Text, out byte v))
|
||||||
|
_slave.Rooms[idx].DamperAngleEA = (byte)Math.Min(v, (byte)0xB4);
|
||||||
|
};
|
||||||
|
// 수동 닫기 (RA) — Slave 모드에서 마스터 개방명령 무시하고 닫힘 유지
|
||||||
|
u.ChkCloseRA.Checked += (s, e) =>
|
||||||
|
{
|
||||||
|
if (_updating) return;
|
||||||
|
_slave.Rooms[idx].ManualCloseRA = true; _slave.Rooms[idx].DamperAngleEA = 0;
|
||||||
|
if (idx == 0) { var r2 = _slave.Rooms[SlaveProtocol.LivingRoom2Index]; r2.ManualCloseRA = true; r2.DamperAngleEA = 0; }
|
||||||
|
RefreshAngleUI(idx);
|
||||||
|
};
|
||||||
|
u.ChkCloseRA.Unchecked += (s, e) =>
|
||||||
|
{
|
||||||
|
if (_updating) return;
|
||||||
|
_slave.Rooms[idx].ManualCloseRA = false;
|
||||||
|
if (idx == 0) _slave.Rooms[SlaveProtocol.LivingRoom2Index].ManualCloseRA = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- 공기질 센서값 ----
|
||||||
|
u.TbPM25.PreviewTextInput += NumericOnly;
|
||||||
|
u.TbPM10.PreviewTextInput += NumericOnly;
|
||||||
|
u.TbCO2.PreviewTextInput += NumericOnly;
|
||||||
|
u.TbVOC.PreviewTextInput += NumericOnly;
|
||||||
|
u.TbTVOC.PreviewTextInput += NumericOnly;
|
||||||
|
u.TbTemp.PreviewTextInput += NumericOnly;
|
||||||
|
u.TbHumidity.PreviewTextInput += NumericOnly;
|
||||||
|
u.TbPM25.TextChanged += (s, e) => { if (!_updating) SyncRoomFromUI(idx); };
|
||||||
|
u.TbPM10.TextChanged += (s, e) => { if (!_updating) SyncRoomFromUI(idx); };
|
||||||
|
u.TbCO2.TextChanged += (s, e) => { if (!_updating) SyncRoomFromUI(idx); };
|
||||||
|
u.TbVOC.TextChanged += (s, e) => { if (!_updating) SyncRoomFromUI(idx); };
|
||||||
|
u.TbTVOC.TextChanged += (s, e) => { if (!_updating) SyncRoomFromUI(idx); };
|
||||||
|
u.TbTemp.TextChanged += (s, e) => { if (!_updating) SyncRoomFromUI(idx); };
|
||||||
|
u.TbHumidity.TextChanged += (s, e) => { if (!_updating) SyncRoomFromUI(idx); };
|
||||||
|
|
||||||
|
// 프리셋 (좋음/보통/나쁨/매우나쁨/최악/센서없음)
|
||||||
|
u.RbGood.Checked += (s, e) => ApplyPreset(idx, 0);
|
||||||
|
u.RbNormal.Checked += (s, e) => ApplyPreset(idx, 1);
|
||||||
|
u.RbBad.Checked += (s, e) => ApplyPreset(idx, 2);
|
||||||
|
u.RbVeryBad.Checked += (s, e) => ApplyPreset(idx, 3);
|
||||||
|
u.RbWorst.Checked += (s, e) => ApplyPreset(idx, 4);
|
||||||
|
u.RbNoSensor.Checked += (s, e) => ApplyPreset(idx, PresetNoSensor);
|
||||||
|
|
||||||
|
// 프리셋모드 (ECO/NORMAL/TURBO/힘펠)
|
||||||
|
u.RbEco.Checked += (s, e) => ApplyHystPreset(idx, 0);
|
||||||
|
u.RbNorm.Checked += (s, e) => ApplyHystPreset(idx, 1);
|
||||||
|
u.RbTurbo.Checked += (s, e) => ApplyHystPreset(idx, 2);
|
||||||
|
|
||||||
|
// LED 슬라이더 + 수동 제어
|
||||||
|
u.SldLed.ValueChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
int v = (int)_ui[idx].SldLed.Value;
|
||||||
|
_ui[idx].TxtLedVal.Text = v == 0 ? "0 (OFF)" : $"{v}단";
|
||||||
|
// Master 모드 또는 LED 수동 제어 시 슬라이더 값을 LED 밝기로 적용
|
||||||
|
if (_slave.Mode == SimMode.Master || _slave.Rooms[idx].ManualLed)
|
||||||
|
_slave.Rooms[idx].LedBrightness = (byte)v;
|
||||||
|
};
|
||||||
|
u.ChkLedManual.Checked += (s, e) =>
|
||||||
|
{
|
||||||
|
if (_updating) return;
|
||||||
|
_slave.Rooms[idx].ManualLed = true;
|
||||||
|
_ui[idx].SldLed.IsEnabled = true;
|
||||||
|
_slave.Rooms[idx].LedBrightness = (byte)_ui[idx].SldLed.Value;
|
||||||
|
};
|
||||||
|
u.ChkLedManual.Unchecked += (s, e) =>
|
||||||
|
{
|
||||||
|
if (_updating) return;
|
||||||
|
_slave.Rooms[idx].ManualLed = false;
|
||||||
|
// 수동 해제 시 슬라이더는 다시 마스터 명령 추종(Slave 모드면 읽기전용)
|
||||||
|
_ui[idx].SldLed.IsEnabled = _slave.Mode == SimMode.Master;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- 급기(SA) 디퓨저 ----
|
||||||
|
u.TglSA.Checked += (s, e) => { if (!_updating) _slave.Rooms[idx].PollSA = true; };
|
||||||
|
u.TglSA.Unchecked += (s, e) => { if (!_updating) _slave.Rooms[idx].PollSA = false; };
|
||||||
|
u.TbSAAngle.TextChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
if (_slave.Mode == SimMode.Master && byte.TryParse(u.TbSAAngle.Text, out byte v))
|
||||||
|
_slave.Rooms[idx].DamperAngleSA = (byte)Math.Min(v, (byte)0xB4);
|
||||||
|
};
|
||||||
|
u.ChkCloseSA.Checked += (s, e) =>
|
||||||
|
{
|
||||||
|
if (_updating) return;
|
||||||
|
_slave.Rooms[idx].ManualCloseSA = true; _slave.Rooms[idx].DamperAngleSA = 0;
|
||||||
|
if (idx == 0) { var r2 = _slave.Rooms[SlaveProtocol.LivingRoom2Index]; r2.ManualCloseSA = true; r2.DamperAngleSA = 0; }
|
||||||
|
RefreshAngleUI(idx);
|
||||||
|
};
|
||||||
|
u.ChkCloseSA.Unchecked += (s, e) =>
|
||||||
|
{
|
||||||
|
if (_updating) return;
|
||||||
|
_slave.Rooms[idx].ManualCloseSA = false;
|
||||||
|
if (idx == 0) _slave.Rooms[SlaveProtocol.LivingRoom2Index].ManualCloseSA = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 거실 전용 : DL/힘펠 제품 모드 + RA2/SA2 (거실2 = ID2 0x00) =====
|
||||||
|
if (idx != 0)
|
||||||
|
{
|
||||||
|
// 방1~4 : RA2/SA2 자리는 비워두되 공간은 유지(Hidden) → 거실과 세로 정렬
|
||||||
|
u.GridEA2.Visibility = Visibility.Hidden;
|
||||||
|
u.GridSA2.Visibility = Visibility.Hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idx == 0)
|
||||||
|
{
|
||||||
|
u.GridEA2.Visibility = Visibility.Visible;
|
||||||
|
u.GridSA2.Visibility = Visibility.Visible;
|
||||||
|
u.TxtEALabel.Text = "RA1 각도";
|
||||||
|
u.TxtSALabel.Text = "SA1 각도";
|
||||||
|
|
||||||
|
u.TbEAAngle2.PreviewTextInput += NumericOnly;
|
||||||
|
u.TbSAAngle2.PreviewTextInput += NumericOnly;
|
||||||
|
u.TbEAAngle2.TextChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
if (_slave.Mode == SimMode.Master && byte.TryParse(u.TbEAAngle2.Text, out byte v))
|
||||||
|
_slave.Rooms[SlaveProtocol.LivingRoom2Index].DamperAngleEA = (byte)Math.Min(v, (byte)0xB4);
|
||||||
|
};
|
||||||
|
u.TbSAAngle2.TextChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
if (_slave.Mode == SimMode.Master && byte.TryParse(u.TbSAAngle2.Text, out byte v))
|
||||||
|
_slave.Rooms[SlaveProtocol.LivingRoom2Index].DamperAngleSA = (byte)Math.Min(v, (byte)0xB4);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slave 모드 기본 : 댐퍼 토글/각도/LED 는 읽기전용(마스터 명령 표시용)
|
||||||
|
u.TglSA.IsEnabled = false;
|
||||||
|
u.TglEA.IsEnabled = false;
|
||||||
|
u.TbSAAngle.IsReadOnly = true;
|
||||||
|
u.TbEAAngle.IsReadOnly = true;
|
||||||
|
u.TbSAAngle2.IsReadOnly = true;
|
||||||
|
u.TbEAAngle2.IsReadOnly = true;
|
||||||
|
u.SldLed.IsEnabled = false;
|
||||||
|
|
||||||
|
// 초기 동기화
|
||||||
|
SyncRoomFromUI(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 초기값 : 거실, 방1~방3 활성(응답) + 센서 '좋음'. 댐퍼는 닫힘(각도0=토글OFF) ----
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
_ui[i].ChkEnabled.IsChecked = true; // Enabled → SA/RA 응답
|
||||||
|
_ui[i].RbGood.IsChecked = true; // 공기질 '좋음' preset
|
||||||
|
}
|
||||||
|
_ui[4].RbGood.IsChecked = true; // 방4 기본 '좋음' (Enabled 는 제품모드가 제어)
|
||||||
|
|
||||||
|
// 제품 모드 기본 = DL (전역) — LED 디밍 활성(거실·방1~3), RA2 비활성, 방4 비활성
|
||||||
|
ApplyProductMode(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 제품 모드(DL/힘펠) 전역 토글 ==========
|
||||||
|
private void ProductMode_Click(object s, RoutedEventArgs e) => ApplyProductMode(!_himpel);
|
||||||
|
|
||||||
|
// 전역 적용
|
||||||
|
// DL : byte24~25=VOC, LED 디밍 활성(거실·방1~3), RA2(거실 배기) 비활성, 방4 비활성화
|
||||||
|
// 힘펠 : byte24~25=TVOC, LED 디밍 비활성(전체), RA2 활성, 방4 활성화
|
||||||
|
private void ApplyProductMode(bool himpel)
|
||||||
|
{
|
||||||
|
_himpel = himpel;
|
||||||
|
if (btnProductMode != null) btnProductMode.Content = himpel ? "힘펠" : "DL";
|
||||||
|
|
||||||
|
// 송신 모드(byte24/25 VOC vs TVOC) — 모든 방 + 거실2
|
||||||
|
for (int i = 0; i < 5; i++) _slave.Rooms[i].Himpel = himpel;
|
||||||
|
var r2 = _slave.Rooms[SlaveProtocol.LivingRoom2Index];
|
||||||
|
r2.Himpel = himpel;
|
||||||
|
r2.RaActive = himpel; // 거실 RA2 = 힘펠일 때만 응답
|
||||||
|
|
||||||
|
// 거실 RA2 입력 활성/비활성
|
||||||
|
_ui[0].GridEA2.IsEnabled = himpel;
|
||||||
|
|
||||||
|
// 공기질 센서 입력 : 힘펠=TVOC 활성/VOC 비활성, DL=VOC 활성/TVOC 비활성 (전체 방)
|
||||||
|
// 프리셋모드(ECO/NORMAL/TURBO) : DL=활성 / 힘펠=비활성 (전체 방)
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
_ui[i].TbTVOC.IsEnabled = himpel;
|
||||||
|
_ui[i].TbVOC.IsEnabled = !himpel;
|
||||||
|
_ui[i].RbEco.IsEnabled = !himpel;
|
||||||
|
_ui[i].RbNorm.IsEnabled = !himpel;
|
||||||
|
_ui[i].RbTurbo.IsEnabled = !himpel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LED 디밍 : DL=활성 / 힘펠=비활성 — 거실(0)·방1~3(1~3)
|
||||||
|
for (int i = 0; i < 4; i++) SetLedDimming(i, enabled: !himpel);
|
||||||
|
|
||||||
|
// 방4(idx 4) : DL=비활성화 / 힘펠=활성화(센서 기본 '좋음')
|
||||||
|
SetRoomActive(4, active: himpel);
|
||||||
|
if (himpel) _ui[4].RbGood.IsChecked = true;
|
||||||
|
|
||||||
|
// 힘펠 전환 시 현재 공기질에 맞춰 댐퍼 각도 즉시 반영
|
||||||
|
if (himpel)
|
||||||
|
for (int i = 0; i < 5; i++) SyncRoomFromUI(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// LED 디밍 수동 제어 활성/비활성 (방 1개)
|
||||||
|
private void SetLedDimming(int idx, bool enabled)
|
||||||
|
{
|
||||||
|
var u = _ui[idx];
|
||||||
|
if (enabled)
|
||||||
|
{
|
||||||
|
u.ChkLedManual.IsEnabled = true;
|
||||||
|
u.SldLed.IsEnabled = _slave.Mode == SimMode.Master || u.ChkLedManual.IsChecked == true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_updating = true; u.ChkLedManual.IsChecked = false; _updating = false;
|
||||||
|
_slave.Rooms[idx].ManualLed = false;
|
||||||
|
u.ChkLedManual.IsEnabled = false;
|
||||||
|
u.SldLed.IsEnabled = _slave.Mode == SimMode.Master;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 방 전체 활성/비활성 — 비활성 시 응답 중지(Enabled off) + 패널 잠금
|
||||||
|
private void SetRoomActive(int idx, bool active)
|
||||||
|
{
|
||||||
|
var u = _ui[idx];
|
||||||
|
if (u.ChkEnabled.IsChecked != active) u.ChkEnabled.IsChecked = active; // Checked/Unchecked 핸들러가 Rooms[idx].Enabled 처리
|
||||||
|
u.IsEnabled = active; // 패널 잠금/해제
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== UI → RoomSimData 즉시 동기화 ==========
|
||||||
|
private void SyncRoomFromUI(int idx)
|
||||||
|
{
|
||||||
|
var room = _slave.Rooms[idx];
|
||||||
|
var u = _ui[idx];
|
||||||
|
if (u == null) return;
|
||||||
|
|
||||||
|
// 센서값만 UI에서 동기화 (제어값은 마스터에서만 변경)
|
||||||
|
int.TryParse(u.TbPM10?.Text, out int pm10); room.PM10 = pm10;
|
||||||
|
int.TryParse(u.TbTemp?.Text, out int temp); room.Temperature = temp;
|
||||||
|
int.TryParse(u.TbHumidity?.Text, out int hum); room.Humidity = hum;
|
||||||
|
int.TryParse(u.TbPM25?.Text, out int pm25); room.PM25 = pm25;
|
||||||
|
int.TryParse(u.TbCO2?.Text, out int co2); room.CO2 = co2;
|
||||||
|
int.TryParse(u.TbTVOC?.Text, out int tvoc); room.TVOC = tvoc;
|
||||||
|
int.TryParse(u.TbVOC?.Text, out int voc); room.VOC = voc;
|
||||||
|
|
||||||
|
// 공기질 상태 자동 계산 — 선택한 프리셋모드(ECO/NORMAL/TURBO)의 임계 밴드로
|
||||||
|
int p = _roomPreset[idx];
|
||||||
|
int worst = Math.Max(
|
||||||
|
Math.Max(Band(pm25, ThrPM25[p]), Band(co2, ThrCO2[p])),
|
||||||
|
Math.Max(Band(voc, ThrVOC[p]), Band(pm10, ThrPM10[p])));
|
||||||
|
room.AirQualityStatus = PreStatus[worst];
|
||||||
|
|
||||||
|
// 프리셋 라디오 버튼 동기화 (RbNoSensor 체크 상태면 skip — 사용자 선택 보존).
|
||||||
|
if (u.RbGood != null && (u.RbNoSensor == null || u.RbNoSensor.IsChecked != true))
|
||||||
|
{
|
||||||
|
_updating = true;
|
||||||
|
switch (worst)
|
||||||
|
{
|
||||||
|
case 0: u.RbGood.IsChecked = true; break;
|
||||||
|
case 1: u.RbNormal.IsChecked = true; break;
|
||||||
|
case 2: u.RbBad.IsChecked = true; break;
|
||||||
|
case 3: u.RbVeryBad.IsChecked = true; break;
|
||||||
|
case 4: u.RbWorst.IsChecked = true; break;
|
||||||
|
}
|
||||||
|
_updating = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 힘펠 제품 모드 : 공기질 레벨에 따라 댐퍼 각도 자동 (이미지 사양 0/50/65/110)
|
||||||
|
if (_himpel) ApplyHimpelDamper(idx, worst);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 힘펠 모드 자동 댐퍼 — 공기질 레벨(0~3) → 각도. SA/RA 동시 적용, 수동닫기 우선.
|
||||||
|
private void ApplyHimpelDamper(int idx, int level)
|
||||||
|
{
|
||||||
|
byte ang = HimpelDamperAngle[level];
|
||||||
|
var room = _slave.Rooms[idx];
|
||||||
|
if (!room.ManualCloseSA) room.DamperAngleSA = ang;
|
||||||
|
if (!room.ManualCloseRA) room.DamperAngleEA = ang;
|
||||||
|
|
||||||
|
_updating = true;
|
||||||
|
var u = _ui[idx];
|
||||||
|
u.TbSAAngle.Text = room.DamperAngleSA.ToString();
|
||||||
|
u.TbEAAngle.Text = room.DamperAngleEA.ToString();
|
||||||
|
u.TglSA.IsChecked = room.DamperAngleSA > 0;
|
||||||
|
u.TglEA.IsChecked = room.DamperAngleEA > 0;
|
||||||
|
|
||||||
|
// 거실(0) : 거실2(RA2/SA2)도 동일 적용 — RA2 는 힘펠일 때만 활성
|
||||||
|
if (idx == 0)
|
||||||
|
{
|
||||||
|
var r2 = _slave.Rooms[SlaveProtocol.LivingRoom2Index];
|
||||||
|
if (!r2.ManualCloseSA) r2.DamperAngleSA = ang;
|
||||||
|
if (r2.RaActive && !r2.ManualCloseRA) r2.DamperAngleEA = ang;
|
||||||
|
u.TbSAAngle2.Text = r2.DamperAngleSA.ToString();
|
||||||
|
u.TbEAAngle2.Text = r2.DamperAngleEA.ToString();
|
||||||
|
}
|
||||||
|
_updating = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 프리셋 적용 ==========
|
||||||
|
// level 0~4: 좋음 / 보통 / 나쁨 / 매우나쁨 / 최악(빨강) (Pre*[프리셋모드] 배열 lookup)
|
||||||
|
// level 5 : 센서없음 — 모든 sensor TextBox 0
|
||||||
|
private void ApplyPreset(int idx, int level)
|
||||||
|
{
|
||||||
|
if (_updating) return;
|
||||||
|
_roomQuality[idx] = level;
|
||||||
|
_updating = true;
|
||||||
|
var u = _ui[idx];
|
||||||
|
int p = _roomPreset[idx];
|
||||||
|
if (level == PresetNoSensor)
|
||||||
|
{
|
||||||
|
if (u?.TbPM25 != null) u.TbPM25.Text = "0";
|
||||||
|
if (u?.TbPM10 != null) u.TbPM10.Text = "0";
|
||||||
|
if (u?.TbCO2 != null) u.TbCO2.Text = "0";
|
||||||
|
if (u?.TbVOC != null) u.TbVOC.Text = "0";
|
||||||
|
if (u?.TbTVOC != null) u.TbTVOC.Text = "0";
|
||||||
|
if (u?.TbTemp != null) u.TbTemp.Text = "0";
|
||||||
|
if (u?.TbHumidity != null) u.TbHumidity.Text = "0";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (u?.TbPM25 != null) u.TbPM25.Text = PrePM25[p][level].ToString();
|
||||||
|
if (u?.TbPM10 != null) u.TbPM10.Text = PrePM10[p][level].ToString();
|
||||||
|
if (u?.TbCO2 != null) u.TbCO2.Text = PreCO2[p][level].ToString();
|
||||||
|
if (u?.TbTVOC != null) u.TbTVOC.Text = PreTVOC[p][level].ToString();
|
||||||
|
if (u?.TbVOC != null) u.TbVOC.Text = PreVOC[p][level].ToString();
|
||||||
|
}
|
||||||
|
_updating = false;
|
||||||
|
SyncRoomFromUI(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프리셋모드(ECO/NORMAL/TURBO) 변경 → 선택 밴드로 현재 공기질 프리셋 재적용
|
||||||
|
private void ApplyHystPreset(int idx, int preset)
|
||||||
|
{
|
||||||
|
if (_updating) return;
|
||||||
|
_roomPreset[idx] = preset;
|
||||||
|
// 센서없음(5)은 값 0 유지, 그 외 좋음/보통/나쁨/매우나쁨/최악은 새 밴드 중앙값으로 재적용
|
||||||
|
if (_roomQuality[idx] != PresetNoSensor)
|
||||||
|
ApplyPreset(idx, _roomQuality[idx]);
|
||||||
|
else
|
||||||
|
SyncRoomFromUI(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== UI 헬퍼 ==========
|
||||||
|
// 수동 닫기 등으로 댐퍼 각도가 바뀐 즉시 UI 표시 갱신
|
||||||
|
private void RefreshAngleUI(int idx)
|
||||||
|
{
|
||||||
|
_updating = true;
|
||||||
|
_ui[idx].TbSAAngle.Text = _slave.Rooms[idx].DamperAngleSA.ToString();
|
||||||
|
_ui[idx].TbEAAngle.Text = _slave.Rooms[idx].DamperAngleEA.ToString();
|
||||||
|
_ui[idx].TglSA.IsChecked = _slave.Rooms[idx].DamperAngleSA > 0;
|
||||||
|
_ui[idx].TglEA.IsChecked = _slave.Rooms[idx].DamperAngleEA > 0;
|
||||||
|
_updating = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 숫자만 입력 허용
|
||||||
|
private void NumericOnly(object sender, TextCompositionEventArgs e)
|
||||||
|
{
|
||||||
|
e.Handled = !Regex.IsMatch(e.Text, @"^[0-9]$");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 연결 ==========
|
||||||
|
private void RefreshPorts()
|
||||||
|
{
|
||||||
|
cmbPort.Items.Clear();
|
||||||
|
foreach (var p in _slave.GetAvailablePorts()) cmbPort.Items.Add(p);
|
||||||
|
if (cmbPort.Items.Count > 0) cmbPort.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
private void RefreshPorts_Click(object s, RoutedEventArgs e) => RefreshPorts();
|
||||||
|
|
||||||
|
private void Connect_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_slave.IsConnected)
|
||||||
|
{
|
||||||
|
_slave.Disconnect();
|
||||||
|
btnConnect.Content = "연결";
|
||||||
|
ResetAllRooms(); // 연결해제 시 체크박스 / toggle / damper 초기화
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (cmbPort.SelectedItem == null) { MessageBox.Show("COM 포트를 선택하세요."); return; }
|
||||||
|
if (_slave.Connect(cmbPort.SelectedItem.ToString()!)) btnConnect.Content = "연결 해제";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 연결해제 시 호출 — 모든 방의 Enabled / Poll toggle OFF, damper 각도 0.
|
||||||
|
/// _updating 플래그로 toggle 이벤트 chain 회피.
|
||||||
|
/// </summary>
|
||||||
|
private void ResetAllRooms()
|
||||||
|
{
|
||||||
|
_updating = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
var room = _slave.Rooms[i];
|
||||||
|
room.Enabled = false;
|
||||||
|
room.PollSA = false;
|
||||||
|
room.PollRA = false;
|
||||||
|
room.DamperAngleSA = 0;
|
||||||
|
room.DamperAngleEA = 0;
|
||||||
|
|
||||||
|
var u = _ui[i];
|
||||||
|
u.ChkEnabled.IsChecked = false;
|
||||||
|
u.TglSA.IsChecked = false;
|
||||||
|
u.TglEA.IsChecked = false;
|
||||||
|
u.TbSAAngle.Text = "0";
|
||||||
|
u.TbEAAngle.Text = "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 거실2 (RA2/SA2)
|
||||||
|
var r2 = _slave.Rooms[SlaveProtocol.LivingRoom2Index];
|
||||||
|
r2.Enabled = false; r2.PollSA = false; r2.PollRA = false;
|
||||||
|
r2.DamperAngleSA = 0; r2.DamperAngleEA = 0;
|
||||||
|
_ui[0].TbSAAngle2.Text = "0";
|
||||||
|
_ui[0].TbEAAngle2.Text = "0";
|
||||||
|
}
|
||||||
|
finally { _updating = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_slave.IsConnected) return;
|
||||||
|
int interval = int.Parse(((ComboBoxItem)cmbInterval.SelectedItem).Content.ToString()!);
|
||||||
|
_slave.StartResponding(interval); // 슬레이브 전용
|
||||||
|
btnStart.IsEnabled = false;
|
||||||
|
btnStop.IsEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Stop_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_slave.StopResponding();
|
||||||
|
btnStart.IsEnabled = true;
|
||||||
|
btnStop.IsEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 슬레이브 전용 UI 상태 ==========
|
||||||
|
// 각도 필드는 readonly(ERV가 댐퍼 제어), LED 슬라이더는 LED 수동제어 시에만 활성.
|
||||||
|
private void ApplySlaveUi()
|
||||||
|
{
|
||||||
|
if (_ui == null || _ui[0] == null) return;
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
var u = _ui[i];
|
||||||
|
if (u == null) continue;
|
||||||
|
u.TglSA.IsEnabled = true;
|
||||||
|
u.TglEA.IsEnabled = true;
|
||||||
|
u.TbSAAngle.IsReadOnly = true;
|
||||||
|
u.TbEAAngle.IsReadOnly = true;
|
||||||
|
u.TbSAAngle2.IsReadOnly = true;
|
||||||
|
u.TbEAAngle2.IsReadOnly = true;
|
||||||
|
u.SldLed.IsEnabled = u.ChkLedManual.IsChecked == true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 자동변경 : 거실→방1→방2→방3, 각 방 오염레벨 0~4를 30초 단위로 ==========
|
||||||
|
private void AutoChange_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_autoRunning)
|
||||||
|
{
|
||||||
|
_autoTimer.Stop();
|
||||||
|
_autoRunning = false;
|
||||||
|
btnAutoChange.Content = "자동변경";
|
||||||
|
OnLog("[자동변경] 중지");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 거실~방3(0~3) 활성화 (이미 켜져 있으면 무시) 후 전체 0(좋음)에서 시작
|
||||||
|
for (int i = 0; i <= 3; i++)
|
||||||
|
if (_ui[i].ChkEnabled.IsChecked != true) _ui[i].ChkEnabled.IsChecked = true;
|
||||||
|
for (int r = 0; r <= 3; r++) ApplyPreset(r, 0);
|
||||||
|
|
||||||
|
_autoStep = 0;
|
||||||
|
_autoRunning = true;
|
||||||
|
btnAutoChange.Content = "자동변경 중지";
|
||||||
|
OnLog("[자동변경] 시작 — 전체 0에서 30초 대기 후 방1→방2→방3→거실 순 누적(0→4)");
|
||||||
|
_autoTimer.Start(); // 즉시 적용하지 않음 → 초기 0 0 0 0 을 30초 유지 후 첫 변경
|
||||||
|
}
|
||||||
|
|
||||||
|
// 레벨 스윕(누적) : 매 30초 한 방씩 현재 레벨로 올림(방1→방2→방3→거실).
|
||||||
|
// 한 바퀴(4방) 다 올리면 레벨+1. 앞서 올린 방은 값 유지(누적). 전체 4 도달 후 0으로 리셋 반복.
|
||||||
|
private static readonly int[] AutoOrder = { 1, 2, 3, 0 }; // 방1, 방2, 방3, 거실
|
||||||
|
private void AutoTick(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_autoStep >= 16) // 4레벨 × 4방 완료 → 전체 0 리셋 후 새 사이클
|
||||||
|
{
|
||||||
|
_autoStep = 0;
|
||||||
|
for (int r = 0; r <= 3; r++) ApplyPreset(r, 0);
|
||||||
|
OnLog("[자동변경] 사이클 완료 — 전체 0 리셋 후 반복");
|
||||||
|
}
|
||||||
|
int level = _autoStep / 4 + 1; // 1~4
|
||||||
|
int room = AutoOrder[_autoStep % 4]; // 방1→방2→방3→거실
|
||||||
|
ApplyPreset(room, level); // 누적: 다른 방은 건드리지 않음
|
||||||
|
OnLog($"[자동변경] {RoomNames[room]} 오염레벨 {level}");
|
||||||
|
_autoStep++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 마스터 패킷 수신 ==========
|
||||||
|
private void OnMasterPacket(byte[] data, byte id2)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
int ri = SlaveProtocol.Id2ToIndex(id2);
|
||||||
|
if (ri < 0) return;
|
||||||
|
bool secondary = (id2 == 0); // 거실2 → 거실 패널의 RA2/SA2
|
||||||
|
var u = _ui[secondary ? 0 : ri];
|
||||||
|
u.RxCount++;
|
||||||
|
u.TxtRxCount.Text = $"수신: {u.RxCount}";
|
||||||
|
|
||||||
|
// 마스터 제어 명령 → UI 동기화 (시각 만, PollSA/PollRA 변경 안 함)
|
||||||
|
_updating = true;
|
||||||
|
var room = _slave.Rooms[ri];
|
||||||
|
|
||||||
|
if (secondary)
|
||||||
|
{
|
||||||
|
// 거실2 : RA2/SA2 각도만 표시
|
||||||
|
u.TbSAAngle2.Text = room.DamperAngleSA.ToString();
|
||||||
|
u.TbEAAngle2.Text = room.DamperAngleEA.ToString();
|
||||||
|
_updating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LED — 수동 제어 중이면 슬라이더(사용자값) 보존
|
||||||
|
if (!room.ManualLed)
|
||||||
|
{
|
||||||
|
u.SldLed.Value = Math.Min(room.LedBrightness, (byte)9);
|
||||||
|
u.TxtLedVal.Text = room.LedBrightness == 0 ? "0 (OFF)" : $"{room.LedBrightness}단";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 급기/배기 각도 + 댐퍼 토글(열림/닫힘) — 각도 연동 (Slave 모드, 마스터 명령 표시)
|
||||||
|
u.TbSAAngle.Text = room.DamperAngleSA.ToString();
|
||||||
|
u.TbEAAngle.Text = room.DamperAngleEA.ToString();
|
||||||
|
u.TglSA.IsChecked = room.DamperAngleSA > 0;
|
||||||
|
u.TglEA.IsChecked = room.DamperAngleEA > 0;
|
||||||
|
|
||||||
|
// TglSA/TglEA visual 은 user 의 toggle 클릭으로만 변경 — master 응답 gate 역할.
|
||||||
|
// 이전엔 damper 값에 따라 auto-sync 했으나, master polling 이 매 cycle 마다
|
||||||
|
// toggle 을 강제 ON 시키면서 user OFF 가 즉시 덮어쓰이는 문제 발생 → 제거.
|
||||||
|
|
||||||
|
// 공기질 프리셋은 master 가 보내지 않음 (byte 9 = 0) — 사용자 선택 보존.
|
||||||
|
|
||||||
|
_updating = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Slave 응답 수신 (Master Mode) ==========
|
||||||
|
private void OnSlavePacket(byte[] data, byte id1, byte id2)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
int ri = SlaveProtocol.Id2ToIndex(id2);
|
||||||
|
if (ri < 0) return;
|
||||||
|
bool secondary = (id2 == 0); // 거실2 → 거실 패널의 RA2/SA2
|
||||||
|
var u = _ui[secondary ? 0 : ri];
|
||||||
|
var room = _slave.Rooms[ri];
|
||||||
|
|
||||||
|
u.RxCount++;
|
||||||
|
u.TxtRxCount.Text = $"수신: {u.RxCount} (ID1=0x{id1:X2})";
|
||||||
|
|
||||||
|
_updating = true;
|
||||||
|
if (secondary)
|
||||||
|
{
|
||||||
|
// 거실2 : RA2/SA2 각도만 표시 (센서는 거실 패널 공용 표시 유지)
|
||||||
|
u.TbSAAngle2.Text = room.DamperAngleSA.ToString();
|
||||||
|
u.TbEAAngle2.Text = room.DamperAngleEA.ToString();
|
||||||
|
_updating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SEN66 값 UI 갱신 (STM32 slave 가 보낸 값)
|
||||||
|
u.TbPM10.Text = room.PM10.ToString();
|
||||||
|
u.TbPM25.Text = room.PM25.ToString();
|
||||||
|
u.TbTemp.Text = room.Temperature.ToString();
|
||||||
|
u.TbHumidity.Text = room.Humidity.ToString();
|
||||||
|
u.TbCO2.Text = room.CO2.ToString();
|
||||||
|
u.TbVOC.Text = room.VOC.ToString();
|
||||||
|
u.TbTVOC.Text = room.TVOC.ToString();
|
||||||
|
_updating = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Master Polling 송신 콜백 (Master Mode) ==========
|
||||||
|
private void OnMasterPollSent(byte id1, byte id2)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
int ri = SlaveProtocol.Id2ToIndex(id2);
|
||||||
|
if (ri < 0) return;
|
||||||
|
int panel = (id2 == 0) ? 0 : ri; // 거실2 → 거실 패널 표시
|
||||||
|
_ui[panel].TxtStatus.Text = $"→ Poll ID1=0x{id1:X2}";
|
||||||
|
_ui[panel].TxtStatus.Foreground = new SolidColorBrush(Color.FromRgb(0x7D, 0xCF, 0xFF));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnResponseSent(byte id2, bool responded)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
int ri = SlaveProtocol.Id2ToIndex(id2);
|
||||||
|
if (ri < 0) return;
|
||||||
|
var u = _ui[(id2 == 0) ? 0 : ri]; // 거실2 → 거실 패널 표시
|
||||||
|
|
||||||
|
if (responded)
|
||||||
|
{
|
||||||
|
u.TxtStatus.Text = "● 응답";
|
||||||
|
u.TxtStatus.Foreground = new SolidColorBrush(Color.FromRgb(0x9E, 0xCE, 0x6A));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
u.TxtStatus.Text = "✗ 무응답";
|
||||||
|
u.TxtStatus.Foreground = Brushes.Gray;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 로그 ==========
|
||||||
|
private void OnLog(string msg)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
txtLog.AppendText(msg + Environment.NewLine);
|
||||||
|
if (txtLog.LineCount > 500)
|
||||||
|
{
|
||||||
|
var lines = txtLog.Text.Split(Environment.NewLine);
|
||||||
|
txtLog.Text = string.Join(Environment.NewLine, lines[^300..]);
|
||||||
|
}
|
||||||
|
txtLog.ScrollToEnd();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearLog_Click(object s, RoutedEventArgs e) => txtLog.Clear();
|
||||||
|
|
||||||
|
private void SaveLog_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var dlg = new SaveFileDialog
|
||||||
|
{
|
||||||
|
Filter = "텍스트 파일 (*.txt)|*.txt",
|
||||||
|
FileName = $"SimLog_{DateTime.Now:yyyyMMdd_HHmmss}.txt"
|
||||||
|
};
|
||||||
|
if (dlg.ShowDialog() == true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string h = $"========================================\r\n 디퓨져 시뮬레이터 통신 로그\r\n 저장 일시: {DateTime.Now:yyyy-MM-dd HH:mm:ss}\r\n========================================\r\n\r\n";
|
||||||
|
File.WriteAllText(dlg.FileName, h + txtLog.Text);
|
||||||
|
MessageBox.Show($"저장 완료: {dlg.FileName}");
|
||||||
|
}
|
||||||
|
catch (Exception ex) { MessageBox.Show($"저장 실패: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnConnectionChanged(bool connected)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
if (connected)
|
||||||
|
{
|
||||||
|
statusLed.Fill = new SolidColorBrush(Color.FromRgb(0x9E, 0xCE, 0x6A));
|
||||||
|
txtStatus.Text = "연결됨";
|
||||||
|
btnStart.IsEnabled = true;
|
||||||
|
btnConnect.Content = "연결 해제";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
statusLed.Fill = new SolidColorBrush(Color.FromRgb(0xF7, 0x76, 0x8E));
|
||||||
|
txtStatus.Text = "미연결";
|
||||||
|
btnStart.IsEnabled = false;
|
||||||
|
btnStop.IsEnabled = false;
|
||||||
|
btnConnect.Content = "연결";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
<UserControl x:Class="DiffuserSimulator.RoomPanel"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<!-- 디퓨저 각실(방) 1개 패널 — 디자이너에서 이 레이아웃만 수정하면 5실에 모두 반영됨.
|
||||||
|
컨트롤 동작(이벤트)은 MainWindow.BuildRoomPanels 에서 연결한다. -->
|
||||||
|
<Border Background="{StaticResource SecondaryBgBrush}" CornerRadius="8" Padding="10" Margin="3">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel>
|
||||||
|
|
||||||
|
<!-- 헤더: 활성 체크 + 색상 + 이름 / 상태 -->
|
||||||
|
<Grid Margin="0,0,0,6">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<CheckBox x:Name="ChkEnabled" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||||
|
<Ellipse x:Name="HdrColor" Width="8" Height="8" Fill="#7DCFFF"
|
||||||
|
Margin="0,0,5,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="HdrName" Text="거실" FontSize="14" FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock x:Name="TxtStatus" Text="대기" FontSize="10" HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center" Foreground="Gray"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock x:Name="TxtRxCount" Text="수신: 0" FontSize="9"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4"/>
|
||||||
|
<Separator Background="{StaticResource BorderBrush}" Margin="0,4,0,4"/>
|
||||||
|
|
||||||
|
<!-- 배기(RA) 디퓨저 -->
|
||||||
|
<Grid Margin="0,4,0,2">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="배기 댐퍼(열림)" FontSize="11" VerticalAlignment="Center"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<ToggleButton x:Name="TglEA" Grid.Column="1" Style="{StaticResource ToggleSwitch}"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Margin="0,0,0,3">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="52"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock x:Name="TxtEALabel" Text="RA 각도" FontSize="10" VerticalAlignment="Center"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<TextBox x:Name="TbEAAngle" Grid.Column="1" Text="0" FontSize="11" Padding="4,2"
|
||||||
|
Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
</Grid>
|
||||||
|
<!-- RA2 (거실2 = ID2 0x00) — 거실 전용. 힘펠 모드에서만 활성 -->
|
||||||
|
<Grid x:Name="GridEA2" Margin="0,0,0,3" Visibility="Collapsed">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="52"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="RA2 각도" FontSize="10" VerticalAlignment="Center"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<TextBox x:Name="TbEAAngle2" Grid.Column="1" Text="0" FontSize="11" Padding="4,2"
|
||||||
|
Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
</Grid>
|
||||||
|
<CheckBox x:Name="ChkCloseRA" Content="RA 수동 닫기" FontSize="10"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,2,0,2"/>
|
||||||
|
|
||||||
|
<Separator Background="{StaticResource BorderBrush}" Margin="0,4,0,4"/>
|
||||||
|
|
||||||
|
<!-- 공기질 센서값 -->
|
||||||
|
<TextBlock Text="공기질 센서값" FontSize="11" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,2,0,4"/>
|
||||||
|
<Grid Margin="0,0,0,3">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="52"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="PM2.5" FontSize="10" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<TextBox x:Name="TbPM25" Grid.Column="1" Text="25" FontSize="11" Padding="4,2"
|
||||||
|
Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Margin="0,0,0,3">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="52"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="PM10" FontSize="10" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<TextBox x:Name="TbPM10" Grid.Column="1" Text="30" FontSize="11" Padding="4,2"
|
||||||
|
Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Margin="0,0,0,3">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="52"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="CO₂" FontSize="10" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<TextBox x:Name="TbCO2" Grid.Column="1" Text="850" FontSize="11" Padding="4,2"
|
||||||
|
Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Margin="0,0,0,3">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="52"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="VOC" FontSize="10" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<TextBox x:Name="TbVOC" Grid.Column="1" Text="115" FontSize="11" Padding="4,2"
|
||||||
|
Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Margin="0,0,0,3">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="52"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="TVOC" FontSize="10" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<TextBox x:Name="TbTVOC" Grid.Column="1" Text="250" FontSize="11" Padding="4,2"
|
||||||
|
Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Margin="0,0,0,3">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="52"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="온도" FontSize="10" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<TextBox x:Name="TbTemp" Grid.Column="1" Text="25" FontSize="11" Padding="4,2"
|
||||||
|
Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Margin="0,0,0,3">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="52"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="습도" FontSize="10" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<TextBox x:Name="TbHumidity" Grid.Column="1" Text="50" FontSize="11" Padding="4,2"
|
||||||
|
Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- 공기질 프리셋 (RbNoSensor=센서없음). GroupName 없이 부모 UniformGrid 단위로 그룹화 → 실별 독립 -->
|
||||||
|
<TextBlock Text="프리셋" FontSize="9" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,2,0,4"/>
|
||||||
|
<UniformGrid Columns="2" Margin="0,2,0,4">
|
||||||
|
<RadioButton x:Name="RbGood" Content="좋음" Foreground="DodgerBlue" FontSize="10" FontWeight="SemiBold" Margin="0,0,0,2"/>
|
||||||
|
<RadioButton x:Name="RbNormal" Content="보통" Foreground="LimeGreen" FontSize="10" FontWeight="SemiBold" Margin="0,0,0,2" IsChecked="True"/>
|
||||||
|
<RadioButton x:Name="RbBad" Content="나쁨" Foreground="Orange" FontSize="10" FontWeight="SemiBold" Margin="0,0,0,2"/>
|
||||||
|
<RadioButton x:Name="RbVeryBad" Content="매우나쁨" Foreground="OrangeRed" FontSize="10" FontWeight="SemiBold" Margin="0,0,0,2"/>
|
||||||
|
<RadioButton x:Name="RbWorst" Content="최악" Foreground="Red" FontSize="10" FontWeight="SemiBold" Margin="0,0,0,2"/>
|
||||||
|
<RadioButton x:Name="RbNoSensor" Content="센서없음" Foreground="Gray" FontSize="10" FontWeight="SemiBold" Margin="0,0,0,2"/>
|
||||||
|
</UniformGrid>
|
||||||
|
|
||||||
|
<!-- 프리셋모드 (센서값 밴드 선택). 기본 NORMAL -->
|
||||||
|
<TextBlock Text="프리셋모드" FontSize="9" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,2,0,4"/>
|
||||||
|
<UniformGrid Columns="2" Margin="0,2,0,4">
|
||||||
|
<RadioButton x:Name="RbEco" Content="ECO" Foreground="MediumAquamarine" FontSize="10" FontWeight="SemiBold" Margin="0,0,0,2"/>
|
||||||
|
<RadioButton x:Name="RbNorm" Content="NORMAL" Foreground="LimeGreen" FontSize="10" FontWeight="SemiBold" Margin="0,0,0,2" IsChecked="True"/>
|
||||||
|
<RadioButton x:Name="RbTurbo" Content="TURBO" Foreground="Orange" FontSize="10" FontWeight="SemiBold" Margin="0,0,0,2"/>
|
||||||
|
</UniformGrid>
|
||||||
|
|
||||||
|
<!-- LED -->
|
||||||
|
<Grid Margin="0,4,0,2">
|
||||||
|
<TextBlock Text="LED" FontSize="11" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<TextBlock x:Name="TxtLedVal" Text="0 (OFF)" FontSize="11" FontWeight="Bold"
|
||||||
|
HorizontalAlignment="Right" Foreground="{StaticResource AccentYellowBrush}"/>
|
||||||
|
</Grid>
|
||||||
|
<Slider x:Name="SldLed" Minimum="0" Maximum="9" IsSnapToTickEnabled="True"
|
||||||
|
TickFrequency="1" TickPlacement="BottomRight" Value="0"/>
|
||||||
|
<CheckBox x:Name="ChkLedManual" Content="LED 디밍 수동 제어" FontSize="10"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,2,0,2"/>
|
||||||
|
|
||||||
|
<Separator Background="{StaticResource BorderBrush}" Margin="0,4,0,4"/>
|
||||||
|
|
||||||
|
<!-- 급기(SA) 디퓨저 -->
|
||||||
|
<Grid Margin="0,4,0,2">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="급기 댐퍼(열림)" FontSize="11" VerticalAlignment="Center"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<ToggleButton x:Name="TglSA" Grid.Column="1" Style="{StaticResource ToggleSwitch}"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Margin="0,0,0,3">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="52"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock x:Name="TxtSALabel" Text="SA 각도" FontSize="10" VerticalAlignment="Center"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<TextBox x:Name="TbSAAngle" Grid.Column="1" Text="0" FontSize="11" Padding="4,2"
|
||||||
|
Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
</Grid>
|
||||||
|
<!-- SA2 (거실2 = ID2 0x00) — 거실 전용 -->
|
||||||
|
<Grid x:Name="GridSA2" Margin="0,0,0,3" Visibility="Collapsed">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="52"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="SA2 각도" FontSize="10" VerticalAlignment="Center"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<TextBox x:Name="TbSAAngle2" Grid.Column="1" Text="0" FontSize="11" Padding="4,2"
|
||||||
|
Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
</Grid>
|
||||||
|
<CheckBox x:Name="ChkCloseSA" Content="SA 수동 닫기" FontSize="10"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,2,0,2"/>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace DiffuserSimulator
|
||||||
|
{
|
||||||
|
// 디퓨저 각실(방) 1개 패널. 레이아웃은 RoomPanel.xaml(디자이너 편집), 동작 연결은 MainWindow 가 담당.
|
||||||
|
// x:Name 컨트롤들은 같은 어셈블리의 MainWindow 에서 internal 필드로 직접 접근한다.
|
||||||
|
public partial class RoomPanel : UserControl
|
||||||
|
{
|
||||||
|
public int RxCount;
|
||||||
|
|
||||||
|
public RoomPanel()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 방 이름 + 헤더 색상 설정
|
||||||
|
public void SetHeader(string name, Color color)
|
||||||
|
{
|
||||||
|
HdrName.Text = name;
|
||||||
|
HdrColor.Fill = new SolidColorBrush(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,419 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DiffuserSimulator
|
||||||
|
{
|
||||||
|
public class RoomSimData
|
||||||
|
{
|
||||||
|
public byte Id2;
|
||||||
|
public bool Enabled;
|
||||||
|
/* Master 모드 폴링 toggle — 급기/배기 디퓨저 별 활성화 */
|
||||||
|
public bool PollSA = false;
|
||||||
|
public bool PollRA = false;
|
||||||
|
public byte Power = 0x01;
|
||||||
|
public byte RunMode = 0x01;
|
||||||
|
public byte FanSpeed = 0x00;
|
||||||
|
public byte LedBrightness = 0x00;
|
||||||
|
public byte AirQualityStatus = 0x03; // 보통 - green
|
||||||
|
public byte DamperAngleSA = 0;
|
||||||
|
public byte DamperAngleEA = 0;
|
||||||
|
/* 수동 닫기 오버라이드 (Slave) — true면 마스터 개방명령 무시하고 0 유지 */
|
||||||
|
public bool ManualCloseSA = false;
|
||||||
|
public bool ManualCloseRA = false;
|
||||||
|
/* LED 디밍 수동 제어 (Slave) — true면 마스터 LED 명령 무시하고 슬라이더 값 유지 */
|
||||||
|
public bool ManualLed = false;
|
||||||
|
/* 제품 모드 : false=DL(byte24~25 VOC 송신) / true=힘펠(TVOC 송신). RA2 활성에도 영향 (거실 한정) */
|
||||||
|
public bool Himpel = false;
|
||||||
|
/* RA(배기) 디퓨저 응답 활성 — 거실2는 힘펠 모드에서만 RA 동작 (DL 모드는 SA만) */
|
||||||
|
public bool RaActive = true;
|
||||||
|
/* 디폴트: 보통 preset */
|
||||||
|
public int PM10 = 30;
|
||||||
|
public int PM25 = 25;
|
||||||
|
public int PM4 = 20;
|
||||||
|
public int PM1 = 10;
|
||||||
|
public int Humidity = 50;
|
||||||
|
public int Temperature = 25;
|
||||||
|
public int TVOC = 250;
|
||||||
|
public int VOC = 115; /* VOC index (0~500), 보통 preset */
|
||||||
|
public int NOx = 0;
|
||||||
|
public int CO2 = 850;
|
||||||
|
public ushort ErrorCode = 0x0000;
|
||||||
|
public byte VersionMajor = 0x01;
|
||||||
|
public byte VersionMinor = 0x00;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SimMode { Slave, Master }
|
||||||
|
|
||||||
|
public class SlaveProtocol : IDisposable
|
||||||
|
{
|
||||||
|
private SerialPort? _serialPort;
|
||||||
|
private CancellationTokenSource? _listenCts;
|
||||||
|
private readonly object _lock = new();
|
||||||
|
private bool _disposed;
|
||||||
|
private bool _responding;
|
||||||
|
|
||||||
|
// Rooms 인덱스 : 0=거실(Id2 1), 1=방1(Id2 2), 2=방2(Id2 3), 3=방3(Id2 4), 4=방4(Id2 5), 5=거실2(Id2 0).
|
||||||
|
// 거실2(RA2/SA2)는 거실 패널이 제어. Rev1.3 : ID2 0x00=거실2, 0x01=거실, 0x02~0x05=방1~4.
|
||||||
|
public RoomSimData[] Rooms = new RoomSimData[6];
|
||||||
|
public const int LivingRoom2Index = 5;
|
||||||
|
public SimMode Mode { get; private set; } = SimMode.Slave;
|
||||||
|
|
||||||
|
public event Action<byte[], byte>? MasterPacketReceived; // Slave mode: master packet received
|
||||||
|
public event Action<byte[], byte, byte>? SlavePacketReceived; // Master mode: slave response received (id1, id2)
|
||||||
|
public event Action<byte, bool>? ResponseSent; // id2, responded
|
||||||
|
public event Action<byte, byte>? MasterPollSent; // Master mode: id1, id2 polled
|
||||||
|
public event Action<string>? LogMessage;
|
||||||
|
public event Action<bool>? ConnectionChanged;
|
||||||
|
|
||||||
|
public bool IsConnected => _serialPort?.IsOpen == true;
|
||||||
|
public bool IsResponding => _responding;
|
||||||
|
|
||||||
|
public SlaveProtocol()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
Rooms[i] = new RoomSimData { Id2 = (byte)(i + 1) };
|
||||||
|
Rooms[LivingRoom2Index] = new RoomSimData { Id2 = 0x00 }; // 거실2
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID2 → Rooms 인덱스. 거실2(0)=5, 거실(1)=0, 방1~4(2~5)=1~4. 범위 밖이면 -1.
|
||||||
|
public static int Id2ToIndex(byte id2) => id2 == 0 ? LivingRoom2Index : (id2 <= 5 ? id2 - 1 : -1);
|
||||||
|
|
||||||
|
public static ushort CalcCRC16(byte[] data, int length)
|
||||||
|
{
|
||||||
|
ushort crc = 0xFFFF;
|
||||||
|
for (int i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
crc ^= data[i];
|
||||||
|
for (int j = 0; j < 8; j++)
|
||||||
|
crc = ((crc & 1) != 0) ? (ushort)((crc >> 1) ^ 0xA001) : (ushort)(crc >> 1);
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] GetAvailablePorts() => SerialPort.GetPortNames();
|
||||||
|
|
||||||
|
public bool Connect(string portName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Disconnect();
|
||||||
|
_serialPort = new SerialPort(portName)
|
||||||
|
{
|
||||||
|
BaudRate = 115200, DataBits = 8,
|
||||||
|
StopBits = StopBits.One, Parity = Parity.None,
|
||||||
|
ReadTimeout = 100, WriteTimeout = 500
|
||||||
|
};
|
||||||
|
_serialPort.Open();
|
||||||
|
Log($"[연결] {portName} (115200, 8N1)");
|
||||||
|
ConnectionChanged?.Invoke(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[오류] 연결 실패: {ex.Message}");
|
||||||
|
ConnectionChanged?.Invoke(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Disconnect()
|
||||||
|
{
|
||||||
|
StopResponding();
|
||||||
|
try { if (_serialPort?.IsOpen == true) { _serialPort.Close(); Log("[연결 해제]"); } } catch { }
|
||||||
|
_serialPort?.Dispose();
|
||||||
|
_serialPort = null;
|
||||||
|
ConnectionChanged?.Invoke(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartResponding(int intervalMs = 1000)
|
||||||
|
{
|
||||||
|
StopResponding();
|
||||||
|
Mode = SimMode.Slave;
|
||||||
|
_responding = true;
|
||||||
|
_listenCts = new CancellationTokenSource();
|
||||||
|
var token = _listenCts.Token;
|
||||||
|
|
||||||
|
// 능동 송신 제거됨 — 마스터 polling 수신 시에만 응답.
|
||||||
|
// (이전: 능동 송신 + 마스터 응답 dual mode → STM32 master 와 bus 충돌 우려 + STM32 측 cycle 안에 garbage 유입 가능)
|
||||||
|
|
||||||
|
// 수신 Task: 마스터 폴링이 오면 응답
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_serialPort?.IsOpen != true) { Thread.Sleep(50); continue; }
|
||||||
|
if (_serialPort.BytesToRead < 1) { Thread.Sleep(3); continue; }
|
||||||
|
|
||||||
|
byte b = (byte)_serialPort.ReadByte();
|
||||||
|
if (b != 0xAA) continue;
|
||||||
|
|
||||||
|
byte[] rxBuf = new byte[29];
|
||||||
|
rxBuf[0] = 0xAA;
|
||||||
|
int totalRead = 1, remaining = 28, retries = 100;
|
||||||
|
while (remaining > 0 && retries-- > 0)
|
||||||
|
{
|
||||||
|
if (_serialPort.BytesToRead > 0)
|
||||||
|
{ int r = _serialPort.Read(rxBuf, totalRead, remaining); totalRead += r; remaining -= r; }
|
||||||
|
else Thread.Sleep(2);
|
||||||
|
}
|
||||||
|
if (totalRead < 29) continue;
|
||||||
|
|
||||||
|
ushort rxCrc = (ushort)(rxBuf[27] | (rxBuf[28] << 8));
|
||||||
|
ushort calcCrc = CalcCRC16(rxBuf, 27);
|
||||||
|
if (rxCrc != calcCrc) { Log($"[CRC오류] 수신:0x{rxCrc:X4} 계산:0x{calcCrc:X4}"); continue; }
|
||||||
|
if (rxBuf[1] != 0x10) continue;
|
||||||
|
|
||||||
|
byte id2 = rxBuf[3];
|
||||||
|
Log($"[RX] {BitConverter.ToString(rxBuf)}");
|
||||||
|
/* MasterPacketReceived event invoke 는 room.DamperAngleSA/EA + LED 등
|
||||||
|
실제 갱신 이후로 이동 — UI 가 새 값 표시. 이전엔 갱신 전 호출이라
|
||||||
|
UI 가 한 cycle 전 (옛) 값 표시 → 사용자가 0/110 mismatch 보고. */
|
||||||
|
|
||||||
|
int ri = Id2ToIndex(id2);
|
||||||
|
if (ri < 0) continue;
|
||||||
|
var room = Rooms[ri];
|
||||||
|
byte id1 = rxBuf[2];
|
||||||
|
|
||||||
|
// 응답 조건: 방 Enabled 면 SA/RA 모두 응답.
|
||||||
|
// (배기/급기 토글은 댐퍼 열림/닫힘 표시용 — 각도 연동, 응답 게이트 아님)
|
||||||
|
if (!room.Enabled)
|
||||||
|
{
|
||||||
|
ResponseSent?.Invoke(id2, false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// RA(배기 0x02) 디퓨저 비활성(거실2 DL 모드)이면 RA 폴링 무응답
|
||||||
|
if (id1 == 0x02 && !room.RaActive)
|
||||||
|
{
|
||||||
|
ResponseSent?.Invoke(id2, false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option B 패킷 구분 (250624 dump 패턴 일치):
|
||||||
|
// byte 5 = 0x01 → 명령 (Power ON, state 적용)
|
||||||
|
// byte 5 = 0x00 → 폴링 (상태 조회만, state 무변경)
|
||||||
|
// 폴링에서 byte 10/11/8 = 0 을 그대로 적용하면 댐퍼/LED 가 0 으로 reset 됨.
|
||||||
|
bool isCommand = (rxBuf[5] != 0x00);
|
||||||
|
if (isCommand)
|
||||||
|
{
|
||||||
|
// 마스터 명령 적용 — ID1 별로 해당 type 의 필드만 갱신.
|
||||||
|
// ID1=0x01 (SA): damper SA 만
|
||||||
|
// ID1=0x02 (RA): damper RA + LED + 공통 (Power/RunMode/Fan/Color)
|
||||||
|
if (id1 == 0x01)
|
||||||
|
{
|
||||||
|
room.DamperAngleSA = room.ManualCloseSA ? (byte)0 : rxBuf[10];
|
||||||
|
}
|
||||||
|
else if (id1 == 0x02)
|
||||||
|
{
|
||||||
|
room.DamperAngleEA = room.ManualCloseRA ? (byte)0 : rxBuf[11];
|
||||||
|
// 힘펠 모드는 LED 디밍 미사용 → 마스터 byte 8 무시(갱신 안 함)
|
||||||
|
if (!room.ManualLed && !room.Himpel) room.LedBrightness = rxBuf[8];
|
||||||
|
room.Power = rxBuf[5];
|
||||||
|
room.RunMode = rxBuf[6];
|
||||||
|
room.FanSpeed = rxBuf[7];
|
||||||
|
if (rxBuf[9] != 0) room.AirQualityStatus = rxBuf[9];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* room 갱신 후 UI 동기화 event. UI 의 TbSAAngle/TbEAAngle/LED 가 새 값 표시. */
|
||||||
|
MasterPacketReceived?.Invoke(rxBuf, id2);
|
||||||
|
|
||||||
|
// 응답 전송
|
||||||
|
byte[] tx = BuildResponse(room, id1, id2);
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_serialPort?.Write(tx, 0, tx.Length);
|
||||||
|
}
|
||||||
|
Log($"[TX 응답] {BitConverter.ToString(tx)}");
|
||||||
|
ResponseSent?.Invoke(id2, true);
|
||||||
|
}
|
||||||
|
catch (TimeoutException) { }
|
||||||
|
catch (OperationCanceledException) { break; }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!token.IsCancellationRequested) Log($"[오류] {ex.Message}");
|
||||||
|
Thread.Sleep(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, token);
|
||||||
|
Log("[통신 시작] 마스터 응답 모드");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopResponding()
|
||||||
|
{
|
||||||
|
_responding = false;
|
||||||
|
_listenCts?.Cancel();
|
||||||
|
_listenCts?.Dispose();
|
||||||
|
_listenCts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================= Master Mode =========================
|
||||||
|
// 시뮬레이터가 마스터 역할: enabled 된 room 들을 SA(0x01) + RA(0x02) 로 순회 polling.
|
||||||
|
// STM32 (slave) 는 ID1/ID2 매칭 시 39 byte 응답.
|
||||||
|
public void StartMasterPolling(int intervalMs = 1000)
|
||||||
|
{
|
||||||
|
StopResponding();
|
||||||
|
Mode = SimMode.Master;
|
||||||
|
_responding = true;
|
||||||
|
_listenCts = new CancellationTokenSource();
|
||||||
|
var token = _listenCts.Token;
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (_serialPort?.IsOpen != true) { await Task.Delay(100, token); continue; }
|
||||||
|
|
||||||
|
// Active polling slot 수 (Enabled + PollSA/RA 켜진 것)
|
||||||
|
int activeSlots = 0;
|
||||||
|
for (int i = 0; i < Rooms.Length; i++)
|
||||||
|
{
|
||||||
|
if (!Rooms[i].Enabled) continue;
|
||||||
|
if (Rooms[i].PollSA) activeSlots++;
|
||||||
|
if (Rooms[i].PollRA && Rooms[i].RaActive) activeSlots++;
|
||||||
|
}
|
||||||
|
if (activeSlots == 0) { await Task.Delay(intervalMs, token); continue; }
|
||||||
|
|
||||||
|
int slotMs = Math.Max(20, intervalMs / activeSlots);
|
||||||
|
|
||||||
|
// 각 enabled room 에 대해 SA → RA 폴링 (toggle 켜진 것만)
|
||||||
|
foreach (byte id1 in new byte[] { 0x01, 0x02 })
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Rooms.Length && !token.IsCancellationRequested; i++)
|
||||||
|
{
|
||||||
|
var room = Rooms[i];
|
||||||
|
if (!room.Enabled) continue;
|
||||||
|
if (id1 == 0x01 && !room.PollSA) continue;
|
||||||
|
if (id1 == 0x02 && (!room.PollRA || !room.RaActive)) continue;
|
||||||
|
|
||||||
|
byte[] tx = BuildMasterPacket(room, id1);
|
||||||
|
lock (_lock) { _serialPort?.Write(tx, 0, tx.Length); }
|
||||||
|
Log($"[TX-M] ID1=0x{id1:X2} ID2=0x{room.Id2:X2} {BitConverter.ToString(tx)}");
|
||||||
|
MasterPollSent?.Invoke(id1, room.Id2);
|
||||||
|
|
||||||
|
// Slave 응답 대기 (39 byte, 80ms timeout)
|
||||||
|
byte[]? resp = TryReceiveSlaveResponse(80);
|
||||||
|
if (resp != null)
|
||||||
|
{
|
||||||
|
Log($"[RX-S] ID1=0x{resp[2]:X2} ID2=0x{resp[3]:X2} {BitConverter.ToString(resp)}");
|
||||||
|
SlavePacketReceived?.Invoke(resp, resp[2], resp[3]);
|
||||||
|
|
||||||
|
// 받은 SEN66 값을 room 에 갱신 (sensor 만)
|
||||||
|
int ri = Id2ToIndex(resp[3]);
|
||||||
|
if (ri >= 0)
|
||||||
|
{
|
||||||
|
var r = Rooms[ri];
|
||||||
|
r.PM10 = (resp[12] << 8) | resp[13];
|
||||||
|
r.PM4 = (resp[14] << 8) | resp[15];
|
||||||
|
r.PM25 = (resp[16] << 8) | resp[17];
|
||||||
|
r.PM1 = (resp[18] << 8) | resp[19];
|
||||||
|
r.Humidity = (resp[20] << 8) | resp[21];
|
||||||
|
r.Temperature = (resp[22] << 8) | resp[23];
|
||||||
|
r.TVOC = (resp[24] << 8) | resp[25];
|
||||||
|
r.NOx = (resp[26] << 8) | resp[27];
|
||||||
|
r.CO2 = (resp[28] << 8) | resp[29];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(slotMs, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
catch (Exception ex) { if (!token.IsCancellationRequested) Log($"[마스터 오류] {ex.Message}"); }
|
||||||
|
}, token);
|
||||||
|
|
||||||
|
Log("[통신 시작] Master Mode — Polling 송신");
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] BuildMasterPacket(RoomSimData room, byte id1)
|
||||||
|
{
|
||||||
|
byte[] p = new byte[29];
|
||||||
|
p[0] = 0xAA; p[1] = 0x10; p[2] = id1; p[3] = room.Id2; p[4] = 0x00;
|
||||||
|
// 실제 protocol dump 분석 결과 0x80 Control bit 없음 — raw 값 그대로.
|
||||||
|
p[5] = room.Power;
|
||||||
|
p[6] = room.RunMode;
|
||||||
|
p[7] = room.FanSpeed;
|
||||||
|
// LED 디밍은 RA(0x02) 명령 패킷에만 전송 — SA(0x01)·힘펠은 0
|
||||||
|
p[8] = (id1 == 0x02 && !room.Himpel) ? room.LedBrightness : (byte)0;
|
||||||
|
p[9] = room.AirQualityStatus;
|
||||||
|
p[10] = room.DamperAngleSA;
|
||||||
|
p[11] = room.DamperAngleEA;
|
||||||
|
// byte 12~26: 0 (RPM / Reset / 예약 등 미사용)
|
||||||
|
ushort crc = CalcCRC16(p, 27);
|
||||||
|
p[27] = (byte)(crc & 0xFF);
|
||||||
|
p[28] = (byte)((crc >> 8) & 0xFF);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[]? TryReceiveSlaveResponse(int timeoutMs)
|
||||||
|
{
|
||||||
|
if (_serialPort?.IsOpen != true) return null;
|
||||||
|
long deadline = Environment.TickCount64 + timeoutMs;
|
||||||
|
// header 0xAA 대기
|
||||||
|
while (Environment.TickCount64 < deadline)
|
||||||
|
{
|
||||||
|
if (_serialPort.BytesToRead < 1) { Thread.Sleep(2); continue; }
|
||||||
|
byte b = (byte)_serialPort.ReadByte();
|
||||||
|
if (b != 0xAA) continue;
|
||||||
|
|
||||||
|
byte[] buf = new byte[39];
|
||||||
|
buf[0] = 0xAA;
|
||||||
|
int total = 1, remain = 38;
|
||||||
|
while (remain > 0 && Environment.TickCount64 < deadline)
|
||||||
|
{
|
||||||
|
if (_serialPort.BytesToRead > 0)
|
||||||
|
{
|
||||||
|
int r = _serialPort.Read(buf, total, remain);
|
||||||
|
total += r; remain -= r;
|
||||||
|
}
|
||||||
|
else Thread.Sleep(2);
|
||||||
|
}
|
||||||
|
if (total < 39) return null;
|
||||||
|
if (buf[1] != 0x01) continue; // not slave
|
||||||
|
|
||||||
|
ushort rxCrc = (ushort)(buf[37] | (buf[38] << 8));
|
||||||
|
ushort calc = CalcCRC16(buf, 37);
|
||||||
|
if (rxCrc != calc) { Log($"[CRC오류] 수신:0x{rxCrc:X4} 계산:0x{calc:X4}"); return null; }
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] BuildResponse(RoomSimData room, byte id1, byte id2)
|
||||||
|
{
|
||||||
|
byte[] p = new byte[39];
|
||||||
|
p[0] = 0xAA; p[1] = 0x01; p[2] = id1; p[3] = id2; p[4] = 0x00;
|
||||||
|
p[5] = room.Power; p[6] = room.RunMode; p[7] = room.FanSpeed;
|
||||||
|
p[8] = room.LedBrightness; p[9] = room.AirQualityStatus;
|
||||||
|
p[10] = room.DamperAngleSA; p[11] = room.DamperAngleEA;
|
||||||
|
void W16(int idx, int val) { p[idx] = (byte)((val >> 8) & 0xFF); p[idx + 1] = (byte)(val & 0xFF); }
|
||||||
|
W16(12, room.PM10); W16(14, room.PM4); W16(16, room.PM25); W16(18, room.PM1);
|
||||||
|
W16(20, room.Humidity); W16(22, room.Temperature);
|
||||||
|
W16(24, room.Himpel ? room.TVOC : room.VOC); /* byte 24,25 : DL=VOC / 힘펠=TVOC (Rev1.3) */
|
||||||
|
W16(26, room.NOx); W16(28, room.CO2);
|
||||||
|
p[30] = 0; p[31] = 0; p[32] = 0;
|
||||||
|
p[33] = (byte)((room.ErrorCode >> 8) & 0xFF); p[34] = (byte)(room.ErrorCode & 0xFF);
|
||||||
|
p[35] = room.VersionMajor; p[36] = room.VersionMinor;
|
||||||
|
ushort crc = CalcCRC16(p, 37);
|
||||||
|
p[37] = (byte)(crc & 0xFF); p[38] = (byte)((crc >> 8) & 0xFF);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Log(string msg) => LogMessage?.Invoke($"[{DateTime.Now:HH:mm:ss.fff}] {msg}");
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
Disconnect();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<Application x:Class="ERVSimulator.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
StartupUri="MainWindow.xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
<!-- Tokyo Night palette (DiffuserSimulator 와 동일) -->
|
||||||
|
<Color x:Key="PrimaryBg">#1E1E2E</Color>
|
||||||
|
<Color x:Key="SecondaryBg">#2B2B3D</Color>
|
||||||
|
<Color x:Key="CardBg">#313147</Color>
|
||||||
|
<Color x:Key="AccentBlue">#7AA2F7</Color>
|
||||||
|
<Color x:Key="AccentGreen">#9ECE6A</Color>
|
||||||
|
<Color x:Key="AccentRed">#F7768E</Color>
|
||||||
|
<Color x:Key="AccentYellow">#E0AF68</Color>
|
||||||
|
<Color x:Key="AccentCyan">#7DCFFF</Color>
|
||||||
|
<Color x:Key="AccentPurple">#BB9AF7</Color>
|
||||||
|
<Color x:Key="AccentOrange">#FF9E64</Color>
|
||||||
|
<Color x:Key="TextPrimary">#C0CAF5</Color>
|
||||||
|
<Color x:Key="TextSecondary">#565F89</Color>
|
||||||
|
<Color x:Key="BorderColor">#3B3B55</Color>
|
||||||
|
|
||||||
|
<SolidColorBrush x:Key="PrimaryBgBrush" Color="{StaticResource PrimaryBg}"/>
|
||||||
|
<SolidColorBrush x:Key="SecondaryBgBrush" Color="{StaticResource SecondaryBg}"/>
|
||||||
|
<SolidColorBrush x:Key="CardBgBrush" Color="{StaticResource CardBg}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentBlueBrush" Color="{StaticResource AccentBlue}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentGreenBrush" Color="{StaticResource AccentGreen}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentRedBrush" Color="{StaticResource AccentRed}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentYellowBrush" Color="{StaticResource AccentYellow}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentCyanBrush" Color="{StaticResource AccentCyan}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentPurpleBrush" Color="{StaticResource AccentPurple}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentOrangeBrush" Color="{StaticResource AccentOrange}"/>
|
||||||
|
<SolidColorBrush x:Key="TextPrimaryBrush" Color="{StaticResource TextPrimary}"/>
|
||||||
|
<SolidColorBrush x:Key="TextSecondaryBrush" Color="{StaticResource TextSecondary}"/>
|
||||||
|
<SolidColorBrush x:Key="BorderBrush" Color="{StaticResource BorderColor}"/>
|
||||||
|
|
||||||
|
<Style x:Key="ModernButton" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="{StaticResource AccentBlueBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Padding" Value="18,8"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="border" Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="6" Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="border" Property="Opacity" Value="0.85"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="border" Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModernComboBox" TargetType="ComboBox">
|
||||||
|
<Setter Property="Background" Value="{StaticResource CardBgBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="8,5"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<!-- 드롭다운 목록은 시스템 기본 흰색 배경이므로 항목 글자색을 검정으로 -->
|
||||||
|
<Setter Property="ItemContainerStyle">
|
||||||
|
<Setter.Value>
|
||||||
|
<Style TargetType="ComboBoxItem">
|
||||||
|
<Setter Property="Foreground" Value="Black"/>
|
||||||
|
</Style>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModernTextBox" TargetType="TextBox">
|
||||||
|
<Setter Property="Background" Value="{StaticResource CardBgBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="8,5"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- 공용 카드 (섹션 패널) -->
|
||||||
|
<Style x:Key="SectionCard" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{StaticResource SecondaryBgBrush}"/>
|
||||||
|
<Setter Property="CornerRadius" Value="10"/>
|
||||||
|
<Setter Property="Padding" Value="14,12"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="InnerCard" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{StaticResource CardBgBrush}"/>
|
||||||
|
<Setter Property="CornerRadius" Value="8"/>
|
||||||
|
<Setter Property="Padding" Value="10"/>
|
||||||
|
<Setter Property="Margin" Value="4"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SectionTitle" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource AccentCyanBrush}"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,6"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="FieldLabel" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<Setter Property="FontSize" Value="11"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="FieldValue" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="FontFamily" Value="Consolas"/>
|
||||||
|
</Style>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace ERVSimulator
|
||||||
|
{
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>ERVSimulator</RootNamespace>
|
||||||
|
<AssemblyName>ERVSimulator</AssemblyName>
|
||||||
|
<StartupObject>ERVSimulator.App</StartupObject>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
|
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="System.IO.Ports" Version="10.0.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- ERV↔Dashboard 공용 프로토콜 (단일 진실원본) -->
|
||||||
|
<ProjectReference Include="..\..\..\TestProgram\ErvProtocol\ErvProtocol.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<Window x:Class="ERVSimulator.HystWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="공기질 센서 히스테리시스" SizeToContent="WidthAndHeight"
|
||||||
|
ResizeMode="NoResize" WindowStartupLocation="CenterOwner"
|
||||||
|
Background="{StaticResource PrimaryBgBrush}">
|
||||||
|
<Border Style="{StaticResource SectionCard}" Margin="10">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="공기질 센서 히스테리시스 — 모드(프리셋)별 오염단계 임계 + 히스(하강)" Style="{StaticResource SectionTitle}"/>
|
||||||
|
|
||||||
|
<!-- 활성 프리셋 선택 -->
|
||||||
|
<StackPanel Orientation="Horizontal" x:Name="PresetPanel" Margin="0,2,0,10">
|
||||||
|
<TextBlock Text="활성 프리셋" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center" Width="84" FontSize="12" FontWeight="SemiBold"/>
|
||||||
|
<Button Content="ECO" Tag="0" Click="Preset_Click" Style="{StaticResource ModernButton}" Margin="0,0,6,0" MinWidth="80" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="NORMAL" Tag="1" Click="Preset_Click" Style="{StaticResource ModernButton}" Margin="0,0,6,0" MinWidth="80" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="TURBO" Tag="2" Click="Preset_Click" Style="{StaticResource ModernButton}" Margin="0,0,6,0" MinWidth="80" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Text="오염단계 0~4(좋음·보통·나쁨·매우나쁨·최악). 각 값은 해당 단계의 상한(이하). 4단계(최악)는 3단계 상한 초과. + 히스테리시스(하강)." Foreground="{StaticResource TextSecondaryBrush}" FontSize="10" Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="120"/>
|
||||||
|
<ColumnDefinition Width="84"/>
|
||||||
|
<ColumnDefinition Width="84"/>
|
||||||
|
<ColumnDefinition Width="84"/>
|
||||||
|
<ColumnDefinition Width="84"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="1" Text="CO2" TextAlignment="Center" FontSize="11" Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="2" Text="PM2.5" TextAlignment="Center" FontSize="11" Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="3" Text="PM10" TextAlignment="Center" FontSize="11" Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="4" Text="VOC" TextAlignment="Center" FontSize="11" Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="0단계(좋음)" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource AccentBlueBrush}"/>
|
||||||
|
<TextBox x:Name="TCo2_1" Grid.Row="1" Grid.Column="1" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
<TextBox x:Name="TPm25_1" Grid.Row="1" Grid.Column="2" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
<TextBox x:Name="TPm10_1" Grid.Row="1" Grid.Column="3" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
<TextBox x:Name="TVoc_1" Grid.Row="1" Grid.Column="4" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="1단계(보통)" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource AccentGreenBrush}"/>
|
||||||
|
<TextBox x:Name="TCo2_2" Grid.Row="2" Grid.Column="1" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
<TextBox x:Name="TPm25_2" Grid.Row="2" Grid.Column="2" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
<TextBox x:Name="TPm10_2" Grid.Row="2" Grid.Column="3" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
<TextBox x:Name="TVoc_2" Grid.Row="2" Grid.Column="4" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Text="2단계(나쁨)" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource AccentYellowBrush}"/>
|
||||||
|
<TextBox x:Name="TCo2_3" Grid.Row="3" Grid.Column="1" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
<TextBox x:Name="TPm25_3" Grid.Row="3" Grid.Column="2" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
<TextBox x:Name="TPm10_3" Grid.Row="3" Grid.Column="3" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
<TextBox x:Name="TVoc_3" Grid.Row="3" Grid.Column="4" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="0" Text="3단계(매우나쁨)" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource AccentRedBrush}"/>
|
||||||
|
<TextBox x:Name="TCo2_4" Grid.Row="4" Grid.Column="1" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
<TextBox x:Name="TPm25_4" Grid.Row="4" Grid.Column="2" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
<TextBox x:Name="TPm10_4" Grid.Row="4" Grid.Column="3" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
<TextBox x:Name="TVoc_4" Grid.Row="4" Grid.Column="4" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="5" Grid.Column="0" Text="4단계(최악)" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource AccentRedBrush}"/>
|
||||||
|
<Border Grid.Row="5" Grid.Column="1" Margin="2,1" Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1">
|
||||||
|
<TextBlock x:Name="MCo2" TextAlignment="Center" FontSize="11" Padding="3,2" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</Border>
|
||||||
|
<Border Grid.Row="5" Grid.Column="2" Margin="2,1" Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1">
|
||||||
|
<TextBlock x:Name="MPm25" TextAlignment="Center" FontSize="11" Padding="3,2" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</Border>
|
||||||
|
<Border Grid.Row="5" Grid.Column="3" Margin="2,1" Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1">
|
||||||
|
<TextBlock x:Name="MPm10" TextAlignment="Center" FontSize="11" Padding="3,2" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</Border>
|
||||||
|
<Border Grid.Row="5" Grid.Column="4" Margin="2,1" Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1">
|
||||||
|
<TextBlock x:Name="MVoc" TextAlignment="Center" FontSize="11" Padding="3,2" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="6" Grid.Column="0" Text="히스(하강)" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource AccentYellowBrush}"/>
|
||||||
|
<TextBox x:Name="DCo2" Grid.Row="6" Grid.Column="1" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource AccentYellowBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
<TextBox x:Name="DPm25" Grid.Row="6" Grid.Column="2" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource AccentYellowBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
<TextBox x:Name="DPm10" Grid.Row="6" Grid.Column="3" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource AccentYellowBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
<TextBox x:Name="DVoc" Grid.Row="6" Grid.Column="4" TextAlignment="Center" FontSize="11" Margin="2,1" Padding="3,2" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource AccentYellowBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,14,0,0" HorizontalAlignment="Right">
|
||||||
|
<Button Content="적용" Width="100" Style="{StaticResource ModernButton}" Click="Apply_Click"
|
||||||
|
Margin="0,0,6,0" Background="{StaticResource AccentBlueBrush}"/>
|
||||||
|
<Button Content="닫기" Width="90" Style="{StaticResource ModernButton}" Click="Close_Click"
|
||||||
|
Background="{StaticResource CardBgBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using ERVSimulator.Model;
|
||||||
|
|
||||||
|
namespace ERVSimulator
|
||||||
|
{
|
||||||
|
// 공기질 센서 히스테리시스 팝업 : 활성 프리셋(ECO/NORMAL/TURBO)의 오염단계 임계 + 히스(하강) 표시·수정
|
||||||
|
public partial class HystWindow : Window
|
||||||
|
{
|
||||||
|
readonly ErvState _state;
|
||||||
|
public event Action<string>? Applied;
|
||||||
|
|
||||||
|
Brush Br(string key) => (Brush)FindResource(key);
|
||||||
|
|
||||||
|
public HystWindow(ErvState state)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_state = state;
|
||||||
|
// MainWindow 프리셋 버튼 / 대시보드 CTRL 로 HystPreset 변경 시 팝업도 즉시 동기화
|
||||||
|
_state.PropertyChanged += OnStateChanged;
|
||||||
|
RefreshPreset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnStateChanged(object? s, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(ErvState.HystPreset))
|
||||||
|
Dispatcher.BeginInvoke(new Action(RefreshPreset));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnClosed(EventArgs e)
|
||||||
|
{
|
||||||
|
_state.PropertyChanged -= OnStateChanged;
|
||||||
|
base.OnClosed(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefreshPreset()
|
||||||
|
{
|
||||||
|
foreach (var child in PresetPanel.Children)
|
||||||
|
{
|
||||||
|
if (child is Button btn && btn.Tag is string tag && byte.TryParse(tag, out var p))
|
||||||
|
{
|
||||||
|
bool active = p == _state.HystPreset;
|
||||||
|
btn.Background = active ? Br("AccentCyanBrush") : Br("CardBgBrush");
|
||||||
|
btn.Foreground = active ? Brushes.Black : Br("TextPrimaryBrush");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FillGrid(_state.HystPreset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 활성 프리셋 값으로 표 채우기
|
||||||
|
void FillGrid(int p)
|
||||||
|
{
|
||||||
|
TCo2_1.Text = _state.Co2Thr[p][0].ToString(); TCo2_2.Text = _state.Co2Thr[p][1].ToString(); TCo2_3.Text = _state.Co2Thr[p][2].ToString(); TCo2_4.Text = _state.Co2Thr[p][3].ToString();
|
||||||
|
TPm25_1.Text = _state.Pm25Thr[p][0].ToString(); TPm25_2.Text = _state.Pm25Thr[p][1].ToString(); TPm25_3.Text = _state.Pm25Thr[p][2].ToString(); TPm25_4.Text = _state.Pm25Thr[p][3].ToString();
|
||||||
|
TPm10_1.Text = _state.Pm10Thr[p][0].ToString(); TPm10_2.Text = _state.Pm10Thr[p][1].ToString(); TPm10_3.Text = _state.Pm10Thr[p][2].ToString(); TPm10_4.Text = _state.Pm10Thr[p][3].ToString();
|
||||||
|
TVoc_1.Text = _state.VocThr[p][0].ToString(); TVoc_2.Text = _state.VocThr[p][1].ToString(); TVoc_3.Text = _state.VocThr[p][2].ToString(); TVoc_4.Text = _state.VocThr[p][3].ToString();
|
||||||
|
DCo2.Text = _state.Co2Db[p].ToString(); DPm25.Text = _state.Pm25Db[p].ToString(); DPm10.Text = _state.Pm10Db[p].ToString(); DVoc.Text = _state.VocDb[p].ToString();
|
||||||
|
// 4단계(최악) : 3단계 상한 초과 = (상한+1)~ (사양서 10p)
|
||||||
|
MCo2.Text = $"{_state.Co2Thr[p][3] + 1}~"; MPm25.Text = $"{_state.Pm25Thr[p][3] + 1}~";
|
||||||
|
MPm10.Text = $"{_state.Pm10Thr[p][3] + 1}~"; MVoc.Text = $"{_state.VocThr[p][3] + 1}~";
|
||||||
|
}
|
||||||
|
|
||||||
|
static ushort P(TextBox tb) { ushort.TryParse(tb.Text, out var v); return v; }
|
||||||
|
|
||||||
|
void Preset_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Button b && b.Tag is string tag && byte.TryParse(tag, out var p) && p < 3)
|
||||||
|
{
|
||||||
|
_state.HystPreset = p;
|
||||||
|
RefreshPreset();
|
||||||
|
Applied?.Invoke($"[Manual] 히스테리시스 프리셋 → {(p == 0 ? "ECO" : p == 1 ? "NORMAL" : "TURBO")}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Apply_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
int p = _state.HystPreset;
|
||||||
|
_state.Co2Thr[p][0] = P(TCo2_1); _state.Co2Thr[p][1] = P(TCo2_2); _state.Co2Thr[p][2] = P(TCo2_3); _state.Co2Thr[p][3] = P(TCo2_4);
|
||||||
|
_state.Pm25Thr[p][0] = P(TPm25_1); _state.Pm25Thr[p][1] = P(TPm25_2); _state.Pm25Thr[p][2] = P(TPm25_3); _state.Pm25Thr[p][3] = P(TPm25_4);
|
||||||
|
_state.Pm10Thr[p][0] = P(TPm10_1); _state.Pm10Thr[p][1] = P(TPm10_2); _state.Pm10Thr[p][2] = P(TPm10_3); _state.Pm10Thr[p][3] = P(TPm10_4);
|
||||||
|
_state.VocThr[p][0] = P(TVoc_1); _state.VocThr[p][1] = P(TVoc_2); _state.VocThr[p][2] = P(TVoc_3); _state.VocThr[p][3] = P(TVoc_4);
|
||||||
|
_state.Co2Db[p] = P(DCo2); _state.Pm25Db[p] = P(DPm25); _state.Pm10Db[p] = P(DPm10); _state.VocDb[p] = P(DVoc);
|
||||||
|
|
||||||
|
Applied?.Invoke($"[Manual] 공기질 센서 히스테리시스 적용 (프리셋 {(p == 0 ? "ECO" : p == 1 ? "NORMAL" : "TURBO")})");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Close_Click(object sender, RoutedEventArgs e) => Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
<Window x:Class="ERVSimulator.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="clr-namespace:ERVSimulator"
|
||||||
|
Title="ERV 시뮬레이터 - ERV Simulator"
|
||||||
|
Width="1500" Height="880"
|
||||||
|
MinWidth="1400" MinHeight="800"
|
||||||
|
Background="{StaticResource PrimaryBgBrush}"
|
||||||
|
WindowStartupLocation="CenterScreen">
|
||||||
|
<Window.Resources>
|
||||||
|
<local:BoolToOpenCloseConverter x:Key="BoolOC"/>
|
||||||
|
<local:BoolToBrushConverter x:Key="BoolBrush"/>
|
||||||
|
<local:ColorTagToBrushConverter x:Key="TagBrush"/>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Grid Margin="14">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/> <!-- 0: Title/Connection -->
|
||||||
|
<RowDefinition Height="Auto"/> <!-- 1: Power + RunMode + Errors -->
|
||||||
|
<RowDefinition Height="Auto"/> <!-- 2: BodyDampers -->
|
||||||
|
<RowDefinition Height="Auto"/> <!-- 3: 자동운전 상태 -->
|
||||||
|
<RowDefinition Height="*"/> <!-- 4: Log -->
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Row 0: 타이틀 + 통신 설정 -->
|
||||||
|
<Border Grid.Row="0" Style="{StaticResource SectionCard}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="ERV 시뮬레이터" FontSize="20" FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource AccentCyanBrush}" Margin="0,0,14,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="0,0,24,0">
|
||||||
|
<TextBlock Text="만든이 : 전경선" Foreground="{StaticResource TextSecondaryBrush}" FontSize="10"/>
|
||||||
|
<TextBlock Text="수정일 : 2026.05.22" Foreground="{StaticResource TextSecondaryBrush}" FontSize="10"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="3" Orientation="Vertical">
|
||||||
|
<!-- 통신 포트 카드 1x4 (각 포트 = 2줄: 상단 라벨·포트·통신속도 / 하단 연결·해제·상태) -->
|
||||||
|
<UniformGrid Columns="4">
|
||||||
|
<!-- RoomCon -->
|
||||||
|
<Border Style="{StaticResource InnerCard}" Margin="0,0,6,0" Padding="8,6">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
|
||||||
|
<TextBlock Text="RoomCon" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"
|
||||||
|
Width="62" FontSize="11" FontWeight="SemiBold"/>
|
||||||
|
<ComboBox x:Name="RoomConPortCombo" Width="92" Style="{StaticResource ModernComboBox}" Margin="0,0,6,0"/>
|
||||||
|
<TextBlock Text="9600 8N1" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" FontSize="11"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button x:Name="RoomConConnectBtn" Content="연결" Style="{StaticResource ModernButton}" Click="RoomConConnect_Click" Padding="12,5" Margin="0,0,4,0"/>
|
||||||
|
<Button x:Name="RoomConDisconnectBtn" Content="해제" Style="{StaticResource ModernButton}" Click="RoomConDisconnect_Click" Padding="12,5"
|
||||||
|
Background="{StaticResource AccentRedBrush}"/>
|
||||||
|
<Ellipse x:Name="RoomConStatus" Width="10" Height="10" Margin="10,0,4,0" Fill="#F7768E" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="RoomConStatusText" Text="미연결" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" FontSize="11"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<!-- HomeNet -->
|
||||||
|
<Border Style="{StaticResource InnerCard}" Margin="0,0,6,0" Padding="8,6">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
|
||||||
|
<TextBlock Text="HomeNet" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"
|
||||||
|
Width="62" FontSize="11" FontWeight="SemiBold"/>
|
||||||
|
<ComboBox x:Name="HomeNetPortCombo" Width="92" Style="{StaticResource ModernComboBox}" Margin="0,0,6,0"/>
|
||||||
|
<ComboBox x:Name="HomeNetBaudCombo" Width="76" Style="{StaticResource ModernComboBox}" SelectedIndex="5">
|
||||||
|
<ComboBoxItem Content="4800"/>
|
||||||
|
<ComboBoxItem Content="9600"/>
|
||||||
|
<ComboBoxItem Content="19200"/>
|
||||||
|
<ComboBoxItem Content="38400"/>
|
||||||
|
<ComboBoxItem Content="57600"/>
|
||||||
|
<ComboBoxItem Content="115200"/>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button x:Name="HomeNetConnectBtn" Content="연결" Style="{StaticResource ModernButton}" Click="HomeNetConnect_Click" Padding="12,5" Margin="0,0,4,0"/>
|
||||||
|
<Button x:Name="HomeNetDisconnectBtn" Content="해제" Style="{StaticResource ModernButton}" Click="HomeNetDisconnect_Click" Padding="12,5"
|
||||||
|
Background="{StaticResource AccentRedBrush}"/>
|
||||||
|
<Ellipse x:Name="HomeNetStatus" Width="10" Height="10" Margin="10,0,4,0" Fill="#F7768E" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="HomeNetStatusText" Text="미연결" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" FontSize="11"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<!-- Diffuser (DiffuserSimulator 센서 수신) -->
|
||||||
|
<Border Style="{StaticResource InnerCard}" Margin="0,0,6,0" Padding="8,6">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
|
||||||
|
<TextBlock Text="Diffuser" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"
|
||||||
|
Width="62" FontSize="11" FontWeight="SemiBold"/>
|
||||||
|
<ComboBox x:Name="DiffuserPortCombo" Width="92" Style="{StaticResource ModernComboBox}" Margin="0,0,6,0"/>
|
||||||
|
<TextBlock Text="115200 8N1" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" FontSize="11"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button x:Name="DiffuserConnectBtn" Content="연결" Style="{StaticResource ModernButton}" Click="DiffuserConnect_Click" Padding="12,5" Margin="0,0,4,0"/>
|
||||||
|
<Button x:Name="DiffuserDisconnectBtn" Content="해제" Style="{StaticResource ModernButton}" Click="DiffuserDisconnect_Click" Padding="12,5"
|
||||||
|
Background="{StaticResource AccentRedBrush}"/>
|
||||||
|
<Ellipse x:Name="DiffuserStatus" Width="10" Height="10" Margin="10,0,4,0" Fill="#F7768E" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="DiffuserStatusText" Text="미연결" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" FontSize="11"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<!-- Hood (후드메인 연동 - HOOD 프로토콜 Rev1.3) -->
|
||||||
|
<Border Style="{StaticResource InnerCard}" Padding="8,6">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
|
||||||
|
<TextBlock Text="Hood" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"
|
||||||
|
Width="62" FontSize="11" FontWeight="SemiBold"/>
|
||||||
|
<ComboBox x:Name="HoodPortCombo" Width="92" Style="{StaticResource ModernComboBox}" Margin="0,0,6,0"/>
|
||||||
|
<TextBlock Text="115200 8N1" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" FontSize="11"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button x:Name="HoodConnectBtn" Content="연결" Style="{StaticResource ModernButton}" Click="HoodConnect_Click" Padding="12,5" Margin="0,0,4,0"/>
|
||||||
|
<Button x:Name="HoodDisconnectBtn" Content="해제" Style="{StaticResource ModernButton}" Click="HoodDisconnect_Click" Padding="12,5"
|
||||||
|
Background="{StaticResource AccentRedBrush}"/>
|
||||||
|
<Ellipse x:Name="HoodStatus" Width="10" Height="10" Margin="10,0,4,0" Fill="#F7768E" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="HoodStatusText" Text="미연결" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" FontSize="11"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</UniformGrid>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,6,0,0">
|
||||||
|
<Button Content="⟳ 포트 새로고침" Style="{StaticResource ModernButton}" Click="RefreshPorts_Click"
|
||||||
|
Background="{StaticResource CardBgBrush}" Padding="10,4" FontSize="11"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Row 1: 전원 + 운전 모드 + 에러 코드 -->
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Border Grid.Column="0" Style="{StaticResource SectionCard}" Margin="0,0,8,8">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="전원" Style="{StaticResource SectionTitle}"/>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Border x:Name="PowerOnCard" Width="80" Padding="10,6" CornerRadius="6"
|
||||||
|
Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" Margin="0,0,6,0">
|
||||||
|
<TextBlock Text="ON" HorizontalAlignment="Center" FontWeight="Bold" FontSize="16"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
</Border>
|
||||||
|
<Border x:Name="PowerOffCard" Width="80" Padding="10,6" CornerRadius="6"
|
||||||
|
Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1">
|
||||||
|
<TextBlock Text="OFF" HorizontalAlignment="Center" FontWeight="Bold" FontSize="16"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Grid.Column="1" Style="{StaticResource SectionCard}" Margin="0,0,8,8">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="운전 모드" Style="{StaticResource SectionTitle}"/>
|
||||||
|
<!-- 운전모드 + 풍량 + (꺼짐)예약 한 줄 -->
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<StackPanel Orientation="Horizontal" x:Name="ModePanel" VerticalAlignment="Center">
|
||||||
|
<Button Content="환기" Tag="Ventilation" Click="ModeButton_Click" Style="{StaticResource ModernButton}" Margin="0,0,6,0" MinWidth="70"
|
||||||
|
Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="자동" Tag="Auto" Click="ModeButton_Click" Style="{StaticResource ModernButton}" Margin="0,0,6,0" MinWidth="70"
|
||||||
|
Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="공청" Tag="AirClean" Click="ModeButton_Click" Style="{StaticResource ModernButton}" Margin="0,0,6,0" MinWidth="70"
|
||||||
|
Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="바이패스" Tag="Bypass" Click="ModeButton_Click" Style="{StaticResource ModernButton}" Margin="0,0,0,0" MinWidth="70"
|
||||||
|
Background="{StaticResource CardBgBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Text="풍량" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center" FontSize="12" FontWeight="SemiBold" Margin="18,0,8,0"/>
|
||||||
|
<StackPanel Orientation="Horizontal" x:Name="FanPanel" VerticalAlignment="Center">
|
||||||
|
<Button Content="0" Tag="0" Click="FanButton_Click" Style="{StaticResource ModernButton}" Margin="0,0,4,0" MinWidth="42" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="1" Tag="1" Click="FanButton_Click" Style="{StaticResource ModernButton}" Margin="0,0,4,0" MinWidth="42" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="2" Tag="2" Click="FanButton_Click" Style="{StaticResource ModernButton}" Margin="0,0,4,0" MinWidth="42" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="3" Tag="3" Click="FanButton_Click" Style="{StaticResource ModernButton}" Margin="0,0,4,0" MinWidth="42" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="4" Tag="4" Click="FanButton_Click" Style="{StaticResource ModernButton}" Margin="0,0,0,0" MinWidth="42" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Text="예약(꺼짐)" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center" FontSize="12" FontWeight="SemiBold" Margin="18,0,8,0"/>
|
||||||
|
<ComboBox x:Name="ReserveCombo" Width="90" Style="{StaticResource ModernComboBox}" VerticalAlignment="Center" SelectionChanged="ReserveCombo_Changed" SelectedIndex="0">
|
||||||
|
<ComboBoxItem Content="해제"/>
|
||||||
|
<ComboBoxItem Content="1시간"/>
|
||||||
|
<ComboBoxItem Content="2시간"/>
|
||||||
|
<ComboBoxItem Content="3시간"/>
|
||||||
|
<ComboBoxItem Content="4시간"/>
|
||||||
|
<ComboBoxItem Content="5시간"/>
|
||||||
|
<ComboBoxItem Content="6시간"/>
|
||||||
|
<ComboBoxItem Content="7시간"/>
|
||||||
|
<ComboBoxItem Content="8시간"/>
|
||||||
|
</ComboBox>
|
||||||
|
<TextBlock x:Name="ReserveText" Text="예약 없음" Foreground="{StaticResource AccentYellowBrush}" VerticalAlignment="Center" FontSize="12" Margin="10,0,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 자동모드 프리셋(절전/표준/쾌속) : 자동 선택 시에만 활성, 기본 표준(NORMAL).
|
||||||
|
선택 프리셋이 공기질 판정 임계(=히스테리시스 임계)를 결정. Tag=프리셋 인덱스 0/1/2 -->
|
||||||
|
<StackPanel Orientation="Horizontal" x:Name="PresetPanel" Margin="0,8,0,0" VerticalAlignment="Center">
|
||||||
|
<Button Content="절전 (ECO)" Tag="0" Click="PresetButton_Click" Style="{StaticResource ModernButton}" Margin="0,0,6,0" MinWidth="96" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="표준 (NORMAL)" Tag="1" Click="PresetButton_Click" Style="{StaticResource ModernButton}" Margin="0,0,6,0" MinWidth="96" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="쾌속 (TURBO)" Tag="2" Click="PresetButton_Click" Style="{StaticResource ModernButton}" Margin="0,0,16,0" MinWidth="96" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<!-- 공기질 센서 히스테리시스 + 풍량 VSP + 후드연동 (쾌속 옆) -->
|
||||||
|
<Button Content="공기질 센서 히스테리시스 ▸" Click="OpenHyst_Click" Style="{StaticResource ModernButton}" Margin="0,0,6,0" MinWidth="84" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="풍량 VSP ▸" Click="OpenVsp_Click" Style="{StaticResource ModernButton}" Margin="0,0,6,0" Padding="12,4" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button x:Name="HoodLinkBtn" Content="후드연동" IsHitTestVisible="False" Style="{StaticResource ModernButton}"
|
||||||
|
Padding="12,4" Background="{StaticResource CardBgBrush}" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Ellipse x:Name="HoodCommLed" Width="9" Height="9" Fill="#F7768E" VerticalAlignment="Center" Margin="8,0,5,0"/>
|
||||||
|
<TextBlock x:Name="HoodCommText" Text="후드 통신 안됨" FontSize="11" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 에러 코드 (운전모드 옆, 클릭 토글) -->
|
||||||
|
<Border Grid.Column="2" Style="{StaticResource SectionCard}" Margin="0,0,0,8">
|
||||||
|
<StackPanel>
|
||||||
|
<DockPanel Margin="0,0,0,4">
|
||||||
|
<TextBlock Text="에러 코드" Style="{StaticResource SectionTitle}" Margin="0"/>
|
||||||
|
<TextBlock x:Name="ErrorCodeHex" DockPanel.Dock="Right" Style="{StaticResource FieldValue}"
|
||||||
|
VerticalAlignment="Center" Margin="10,0,0,0" Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
</DockPanel>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Border x:Name="ErrCard_E02" Tag="E02" MouseDown="ErrorCard_Click" Cursor="Hand" CornerRadius="4" Padding="6,3" Margin="0,0,4,0"
|
||||||
|
Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" ToolTip="온도센서 에러">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Ellipse x:Name="ErrLed_E02" Width="9" Height="9" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
|
<TextBlock Text="E02" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border x:Name="ErrCard_E09" Tag="E09" MouseDown="ErrorCard_Click" Cursor="Hand" CornerRadius="4" Padding="6,3" Margin="0,0,4,0"
|
||||||
|
Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" ToolTip="급기(SA) 팬 에러">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Ellipse x:Name="ErrLed_E09" Width="9" Height="9" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
|
<TextBlock Text="E09" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border x:Name="ErrCard_E10" Tag="E10" MouseDown="ErrorCard_Click" Cursor="Hand" CornerRadius="4" Padding="6,3" Margin="0,0,4,0"
|
||||||
|
Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" ToolTip="배기(EA) 팬 에러">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Ellipse x:Name="ErrLed_E10" Width="9" Height="9" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
|
<TextBlock Text="E10" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border x:Name="ErrCard_COLD" Tag="COLD" MouseDown="ErrorCard_Click" Cursor="Hand" CornerRadius="4" Padding="6,3" Margin="0,0,4,0"
|
||||||
|
Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" ToolTip="장비보호모드">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Ellipse x:Name="ErrLed_COLD" Width="9" Height="9" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
|
<TextBlock Text="COLD" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border x:Name="ErrCard_E07" Tag="E07" MouseDown="ErrorCard_Click" Cursor="Hand" CornerRadius="4" Padding="6,3"
|
||||||
|
Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" ToolTip="내부 통신 에러">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Ellipse x:Name="ErrLed_E07" Width="9" Height="9" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
|
<TextBlock Text="E07" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
<!-- 알람(유지보수) : 필터 청소/교환 -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
|
||||||
|
<Border x:Name="ErrCard_FCLEAN" Tag="FCLEAN" MouseDown="ErrorCard_Click" Cursor="Hand" CornerRadius="4" Padding="6,3" Margin="0,0,4,0"
|
||||||
|
Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" ToolTip="필터 청소 알람">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Ellipse x:Name="ErrLed_FCLEAN" Width="9" Height="9" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
|
<TextBlock Text="필터청소" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border x:Name="ErrCard_FCHANGE" Tag="FCHANGE" MouseDown="ErrorCard_Click" Cursor="Hand" CornerRadius="4" Padding="6,3"
|
||||||
|
Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" ToolTip="필터 교환 알람">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Ellipse x:Name="ErrLed_FCHANGE" Width="9" Height="9" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
|
<TextBlock Text="필터교환" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Row 2: 본체 댐퍼 (6개) -->
|
||||||
|
<Border Grid.Row="2" Style="{StaticResource SectionCard}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="댐퍼 동작 (본체)" Style="{StaticResource SectionTitle}"/>
|
||||||
|
<ItemsControl x:Name="DamperItems">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<UniformGrid Columns="6"/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border Style="{StaticResource InnerCard}">
|
||||||
|
<StackPanel>
|
||||||
|
<DockPanel>
|
||||||
|
<Border DockPanel.Dock="Left" Width="14" Height="14" CornerRadius="7"
|
||||||
|
Background="{Binding ColorTag, Converter={StaticResource TagBrush}}"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" Margin="0,0,6,0"/>
|
||||||
|
<TextBlock Text="{Binding Name}" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</DockPanel>
|
||||||
|
<TextBlock Text="{Binding Connector}" Foreground="{StaticResource TextSecondaryBrush}" FontSize="11"/>
|
||||||
|
<TextBlock FontSize="11" Foreground="{StaticResource TextPrimaryBrush}" FontFamily="Consolas">
|
||||||
|
<Run Text="각도: "/>
|
||||||
|
<Run Text="{Binding TargetAngle, Mode=OneWay}"/>
|
||||||
|
<Run Text="°"/>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock FontWeight="Bold" Margin="0,4,0,0" FontSize="13"
|
||||||
|
Text="{Binding IsOpen, Converter={StaticResource BoolOC}}"
|
||||||
|
Foreground="{Binding IsOpen, Converter={StaticResource BoolBrush}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Row 3: DL 각실제어 (시나리오모드 + 자동운전 상태) -->
|
||||||
|
<Border Grid.Row="3" Style="{StaticResource SectionCard}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="DL 각실제어" Style="{StaticResource SectionTitle}"/>
|
||||||
|
|
||||||
|
<!-- 시나리오모드 -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,2,0,0" x:Name="SubModePanel">
|
||||||
|
<TextBlock Text="시나리오모드" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center" Width="84" FontSize="12" FontWeight="SemiBold"/>
|
||||||
|
<Button Content="스마트수면" Tag="Sleep" Click="SubModeButton_Click" Style="{StaticResource ModernButton}" Margin="0,0,6,0" MinWidth="84" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="쾌적조리" Tag="Cook" Click="SubModeButton_Click" Style="{StaticResource ModernButton}" Margin="0,0,6,0" MinWidth="84" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="안심회복" Tag="Recovery" Click="SubModeButton_Click" Style="{StaticResource ModernButton}" Margin="0,0,16,0" MinWidth="84" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 자동운전 상태 -->
|
||||||
|
<DockPanel Margin="0,10,0,4">
|
||||||
|
<TextBlock Text="자동운전 상태" FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
<Border DockPanel.Dock="Right" Background="{StaticResource CardBgBrush}" CornerRadius="6" Padding="10,3"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1">
|
||||||
|
<TextBlock VerticalAlignment="Center" FontWeight="Bold" FontSize="13" Foreground="{StaticResource AccentCyanBrush}">
|
||||||
|
<Run Text="동작: "/>
|
||||||
|
<Run x:Name="AutoStateRun" Text="-"/>
|
||||||
|
<Run Text=" (분산 / 집중)" Foreground="{StaticResource TextSecondaryBrush}" FontWeight="Normal"/>
|
||||||
|
</TextBlock>
|
||||||
|
</Border>
|
||||||
|
</DockPanel>
|
||||||
|
<ItemsControl x:Name="RoomLoadItems">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate><UniformGrid Columns="4"/></ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border Style="{StaticResource InnerCard}" Margin="3">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="{Binding Name}" HorizontalAlignment="Center" FontSize="11" Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
|
<TextBlock Text="{Binding Level, Mode=OneWay}" HorizontalAlignment="Center" FontSize="20" FontWeight="Bold" Foreground="{StaticResource AccentBlueBrush}"/>
|
||||||
|
<TextBlock Text="{Binding SensorText, Mode=OneWay}" HorizontalAlignment="Center" FontSize="9" Foreground="{StaticResource TextSecondaryBrush}" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Row 4: 통신 로그 -->
|
||||||
|
<Border Grid.Row="4" Style="{StaticResource SectionCard}">
|
||||||
|
<DockPanel>
|
||||||
|
<Grid DockPanel.Dock="Top" Margin="0,0,0,6">
|
||||||
|
<TextBlock Text="통신 로그" Style="{StaticResource SectionTitle}" Margin="0"/>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button Content="로그 저장" Style="{StaticResource ModernButton}"
|
||||||
|
Background="{StaticResource AccentBlueBrush}" Padding="10,3" FontSize="11" Margin="0,0,6,0"
|
||||||
|
Click="SaveLog_Click"/>
|
||||||
|
<Button Content="로그 지우기" Style="{StaticResource ModernButton}"
|
||||||
|
Background="{StaticResource CardBgBrush}" Padding="10,3" FontSize="11"
|
||||||
|
Click="ClearLog_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<TextBox x:Name="LogList" IsReadOnly="True" Background="{StaticResource CardBgBrush}"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}"
|
||||||
|
BorderThickness="1" FontFamily="Consolas" FontSize="11"
|
||||||
|
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
|
||||||
|
TextWrapping="NoWrap" Padding="6"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,485 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using ERVSimulator.Model;
|
||||||
|
using ERVSimulator.Protocol;
|
||||||
|
|
||||||
|
namespace ERVSimulator
|
||||||
|
{
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
readonly ErvState _state = new();
|
||||||
|
readonly DamperSequencer _seq;
|
||||||
|
readonly SerialChannel _roomConCh = new("RoomCon");
|
||||||
|
readonly SerialChannel _homeNetCh = new("HomeNet");
|
||||||
|
readonly SerialChannel _diffuserCh = new("Diffuser");
|
||||||
|
readonly SerialChannel _hoodCh = new("Hood");
|
||||||
|
readonly HomeNetProtocol _homeNet;
|
||||||
|
readonly DiffuserMasterProtocol _diffuser;
|
||||||
|
readonly HoodMasterProtocol _hood;
|
||||||
|
readonly RoomConProtocol _roomCon;
|
||||||
|
readonly AutoLogic _autoLogic;
|
||||||
|
readonly DispatcherTimer _uiTick;
|
||||||
|
readonly DispatcherTimer _reserveTick;
|
||||||
|
|
||||||
|
// Tokyo Night palette refs
|
||||||
|
static readonly Brush ConnectedLed = (Brush)new BrushConverter().ConvertFromString("#9ECE6A")!;
|
||||||
|
static readonly Brush DisconnectedLed = (Brush)new BrushConverter().ConvertFromString("#F7768E")!;
|
||||||
|
static readonly Brush AccentCyan = (Brush)new BrushConverter().ConvertFromString("#7DCFFF")!;
|
||||||
|
static readonly Brush AccentGreen = (Brush)new BrushConverter().ConvertFromString("#9ECE6A")!;
|
||||||
|
static readonly Brush AccentRed = (Brush)new BrushConverter().ConvertFromString("#F7768E")!;
|
||||||
|
static readonly Brush AccentYellow = (Brush)new BrushConverter().ConvertFromString("#E0AF68")!;
|
||||||
|
static readonly Brush AccentOrange = (Brush)new BrushConverter().ConvertFromString("#FF9E64")!;
|
||||||
|
static readonly Brush AccentBlue = (Brush)new BrushConverter().ConvertFromString("#7AA2F7")!;
|
||||||
|
static readonly Brush CardBg = (Brush)new BrushConverter().ConvertFromString("#313147")!;
|
||||||
|
static readonly Brush TextSecondary = (Brush)new BrushConverter().ConvertFromString("#565F89")!;
|
||||||
|
static readonly Brush TextPrimary = (Brush)new BrushConverter().ConvertFromString("#C0CAF5")!;
|
||||||
|
static readonly Brush BorderColor = (Brush)new BrushConverter().ConvertFromString("#3B3B55")!;
|
||||||
|
static readonly Brush LedOff = (Brush)new BrushConverter().ConvertFromString("#3B3B55")!;
|
||||||
|
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_seq = new DamperSequencer(_state);
|
||||||
|
_homeNet = new HomeNetProtocol(_homeNetCh, _state, _seq, Dispatcher);
|
||||||
|
_diffuser = new DiffuserMasterProtocol(_diffuserCh, _state, Dispatcher);
|
||||||
|
_hood = new HoodMasterProtocol(_hoodCh, _state, Dispatcher);
|
||||||
|
_roomCon = new RoomConProtocol(_roomConCh, _state, _seq, Dispatcher);
|
||||||
|
_autoLogic = new AutoLogic(_state, _seq);
|
||||||
|
|
||||||
|
DamperItems.ItemsSource = _state.BodyDampers;
|
||||||
|
RoomLoadItems.ItemsSource = _state.Rooms; // 자동운전 상태 - 각실 부하점수
|
||||||
|
|
||||||
|
_roomConCh.Log += Log;
|
||||||
|
_homeNetCh.Log += Log;
|
||||||
|
_diffuserCh.Log += Log;
|
||||||
|
_hoodCh.Log += Log;
|
||||||
|
_roomConCh.ConnectionChanged += b => Dispatcher.BeginInvoke(() => UpdateChannelLed(RoomConStatus, RoomConStatusText, b));
|
||||||
|
_homeNetCh.ConnectionChanged += b => Dispatcher.BeginInvoke(() => UpdateChannelLed(HomeNetStatus, HomeNetStatusText, b));
|
||||||
|
_diffuserCh.ConnectionChanged += b => Dispatcher.BeginInvoke(() => UpdateChannelLed(DiffuserStatus, DiffuserStatusText, b));
|
||||||
|
_hoodCh.ConnectionChanged += b => Dispatcher.BeginInvoke(() => UpdateChannelLed(HoodStatus, HoodStatusText, b));
|
||||||
|
_homeNet.PacketReceived += Log;
|
||||||
|
_homeNet.PacketSent += Log;
|
||||||
|
_diffuser.PacketReceived += Log;
|
||||||
|
_diffuser.PacketSent += Log;
|
||||||
|
_hood.PacketReceived += Log;
|
||||||
|
_hood.PacketSent += Log;
|
||||||
|
_roomCon.PacketReceived += Log;
|
||||||
|
_roomCon.PacketSent += Log;
|
||||||
|
_autoLogic.Log += Log;
|
||||||
|
|
||||||
|
_state.PropertyChanged += (_, e) => Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
UpdateTopState();
|
||||||
|
if (e.PropertyName == nameof(ErvState.ErrorCode) ||
|
||||||
|
e.PropertyName == nameof(ErvState.E02_TempSensor) ||
|
||||||
|
e.PropertyName == nameof(ErvState.E09_SaFan) ||
|
||||||
|
e.PropertyName == nameof(ErvState.E10_EaFan) ||
|
||||||
|
e.PropertyName == nameof(ErvState.COLD_Protect) ||
|
||||||
|
e.PropertyName == nameof(ErvState.E07_InternalComm))
|
||||||
|
{
|
||||||
|
UpdateErrorIndicators();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_uiTick = new DispatcherTimer(DispatcherPriority.Normal) { Interval = TimeSpan.FromMilliseconds(100) };
|
||||||
|
_uiTick.Tick += (_, _) => UpdateRealtime();
|
||||||
|
_uiTick.Start();
|
||||||
|
|
||||||
|
// (꺼짐)예약 1초 카운트다운
|
||||||
|
_reserveTick = new DispatcherTimer(DispatcherPriority.Normal) { Interval = TimeSpan.FromSeconds(1) };
|
||||||
|
_reserveTick.Tick += (_, _) => ReserveTick();
|
||||||
|
_reserveTick.Start();
|
||||||
|
|
||||||
|
RefreshPortsList();
|
||||||
|
UpdateTopState();
|
||||||
|
UpdateRealtime();
|
||||||
|
UpdateErrorIndicators();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 시리얼 연결 ----
|
||||||
|
void RefreshPorts_Click(object sender, RoutedEventArgs e) => RefreshPortsList();
|
||||||
|
|
||||||
|
void RefreshPortsList()
|
||||||
|
{
|
||||||
|
var ports = SerialChannel.GetAvailablePorts();
|
||||||
|
RoomConPortCombo.ItemsSource = ports;
|
||||||
|
HomeNetPortCombo.ItemsSource = ports;
|
||||||
|
DiffuserPortCombo.ItemsSource = ports;
|
||||||
|
HoodPortCombo.ItemsSource = ports;
|
||||||
|
if (ports.Length > 0)
|
||||||
|
{
|
||||||
|
if (RoomConPortCombo.SelectedIndex < 0) RoomConPortCombo.SelectedIndex = 0;
|
||||||
|
if (HomeNetPortCombo.SelectedIndex < 0) HomeNetPortCombo.SelectedIndex = ports.Length > 1 ? 1 : 0;
|
||||||
|
if (DiffuserPortCombo.SelectedIndex < 0) DiffuserPortCombo.SelectedIndex = ports.Length > 2 ? 2 : 0;
|
||||||
|
if (HoodPortCombo.SelectedIndex < 0) HoodPortCombo.SelectedIndex = ports.Length > 3 ? 3 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DiffuserConnect_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DiffuserPortCombo.SelectedItem is string p) _diffuserCh.Connect(p, 115200);
|
||||||
|
}
|
||||||
|
void DiffuserDisconnect_Click(object sender, RoutedEventArgs e) => _diffuserCh.Disconnect();
|
||||||
|
|
||||||
|
void HoodConnect_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (HoodPortCombo.SelectedItem is string p) _hoodCh.Connect(p, 115200);
|
||||||
|
}
|
||||||
|
void HoodDisconnect_Click(object sender, RoutedEventArgs e) => _hoodCh.Disconnect();
|
||||||
|
|
||||||
|
void RoomConConnect_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (RoomConPortCombo.SelectedItem is string p) _roomConCh.Connect(p, 9600);
|
||||||
|
}
|
||||||
|
void RoomConDisconnect_Click(object sender, RoutedEventArgs e) => _roomConCh.Disconnect();
|
||||||
|
|
||||||
|
void HomeNetConnect_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (HomeNetPortCombo.SelectedItem is string p)
|
||||||
|
{
|
||||||
|
int baud = 9600;
|
||||||
|
if (HomeNetBaudCombo.SelectedItem is ComboBoxItem item &&
|
||||||
|
int.TryParse(item.Content?.ToString(), out var b)) baud = b;
|
||||||
|
_homeNetCh.Connect(p, baud);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void HomeNetDisconnect_Click(object sender, RoutedEventArgs e) => _homeNetCh.Disconnect();
|
||||||
|
|
||||||
|
void UpdateChannelLed(System.Windows.Shapes.Ellipse led, TextBlock text, bool connected)
|
||||||
|
{
|
||||||
|
led.Fill = connected ? ConnectedLed : DisconnectedLed;
|
||||||
|
text.Text = connected ? "연결됨" : "미연결";
|
||||||
|
text.Foreground = connected ? AccentGreen : TextSecondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 운전 모드 버튼 ----
|
||||||
|
void ModeButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Button b && b.Tag is string tag && Enum.TryParse<RunMode>(tag, out var m))
|
||||||
|
{
|
||||||
|
_state.RunMode = m;
|
||||||
|
_state.SetRunMode = m;
|
||||||
|
// 운전모드 전환 시 풍량 1단 (자동 제외 — 자동은 부하점수로 결정)
|
||||||
|
if (m != RunMode.Auto) _state.FanMode = _state.SetFanMode = 1;
|
||||||
|
_state.PowerOn = true;
|
||||||
|
_seq.NotifyCommandChanged();
|
||||||
|
Log($"[Manual] Mode → {m}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 풍량 0~4 ----
|
||||||
|
void FanButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Button b && b.Tag is string tag && byte.TryParse(tag, out var f))
|
||||||
|
{
|
||||||
|
_state.FanMode = _state.SetFanMode = f;
|
||||||
|
if (f > 0) _state.PowerOn = true;
|
||||||
|
_seq.NotifyCommandChanged();
|
||||||
|
Log($"[Manual] 풍량 → {f}단{(_state.RunMode == RunMode.Auto ? " (자동모드는 곧 부하점수로 재설정됨)" : "")}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 자동모드 프리셋 (절전/표준/쾌속) : 자동에서만, 공기질 판정 임계 = 선택 프리셋 ----
|
||||||
|
void PresetButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_state.RunMode != RunMode.Auto) { Log("프리셋은 자동모드에서만 선택할 수 있습니다."); return; }
|
||||||
|
if (sender is Button b && b.Tag is string tag && byte.TryParse(tag, out var p))
|
||||||
|
{
|
||||||
|
_state.HystPreset = p;
|
||||||
|
_seq.NotifyCommandChanged();
|
||||||
|
Log($"[Manual] 프리셋 → {(p == 0 ? "ECO" : p == 1 ? "NORMAL" : "TURBO")}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 시나리오모드 (스마트수면/쾌적조리/안심회복) ----
|
||||||
|
void SubModeButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Button b || b.Tag is not string tag) return;
|
||||||
|
switch (tag)
|
||||||
|
{
|
||||||
|
case "Sleep": _state.ExtRunMode = _state.SmartSleep ? (byte)0 : (byte)4; break;
|
||||||
|
case "Recovery": _state.ExtRunMode = _state.RecoveryMode ? (byte)0 : (byte)1; break;
|
||||||
|
case "Cook": _state.HoodEnable = !_state.HoodEnable; break;
|
||||||
|
}
|
||||||
|
if (_state.ExtRunMode != 0 || _state.HoodEnable) _state.PowerOn = true;
|
||||||
|
_seq.NotifyCommandChanged();
|
||||||
|
Log($"[Manual] 시나리오모드 → {_state.SubModeText}");
|
||||||
|
UpdateTopState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 풍량 VSP 설정 팝업 ----
|
||||||
|
VspWindow? _vspWin;
|
||||||
|
void OpenVsp_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_vspWin == null)
|
||||||
|
{
|
||||||
|
_vspWin = new VspWindow(_state) { Owner = this };
|
||||||
|
_vspWin.Applied += Log;
|
||||||
|
_vspWin.Closed += (_, _) => _vspWin = null;
|
||||||
|
_vspWin.Show();
|
||||||
|
}
|
||||||
|
else _vspWin.Activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 공기질 센서 히스테리시스 팝업 ----
|
||||||
|
HystWindow? _hystWin;
|
||||||
|
void OpenHyst_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_hystWin == null)
|
||||||
|
{
|
||||||
|
_hystWin = new HystWindow(_state) { Owner = this };
|
||||||
|
_hystWin.Applied += Log;
|
||||||
|
_hystWin.Closed += (_, _) => _hystWin = null;
|
||||||
|
_hystWin.Show();
|
||||||
|
}
|
||||||
|
else _hystWin.Activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- (꺼짐)예약 0~8시간 ----
|
||||||
|
bool _suppressReserveCombo; // 상태→콤보 동기화 중 ReserveCombo_Changed 재진입 차단
|
||||||
|
void ReserveCombo_Changed(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_suppressReserveCombo) return;
|
||||||
|
if (ReserveCombo.SelectedIndex < 0) return;
|
||||||
|
int hours = ReserveCombo.SelectedIndex; // 0=해제, 1~8시간
|
||||||
|
_state.ReserveHours = hours;
|
||||||
|
_state.ReserveRemainSec = hours * 3600;
|
||||||
|
Log(hours == 0 ? "[Manual] 예약 해제" : $"[Manual] {hours}시간 후 꺼짐 예약");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 에러 카드 토글 ----
|
||||||
|
void ErrorCard_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Border b || b.Tag is not string tag) return;
|
||||||
|
switch (tag)
|
||||||
|
{
|
||||||
|
case "E02": _state.E02_TempSensor = !_state.E02_TempSensor; break;
|
||||||
|
case "E09": _state.E09_SaFan = !_state.E09_SaFan; break;
|
||||||
|
case "E10": _state.E10_EaFan = !_state.E10_EaFan; break;
|
||||||
|
case "COLD": _state.COLD_Protect = !_state.COLD_Protect; break;
|
||||||
|
case "E07": _state.E07_InternalComm = !_state.E07_InternalComm; break;
|
||||||
|
case "FCLEAN": _state.FilterClean = !_state.FilterClean; break;
|
||||||
|
case "FCHANGE": _state.FilterChange = !_state.FilterChange; break;
|
||||||
|
}
|
||||||
|
Log($"[Manual] ErrorCode → 0x{_state.ErrorCode:X2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- UI 갱신 ----
|
||||||
|
void UpdateTopState()
|
||||||
|
{
|
||||||
|
// 전원 카드 강조
|
||||||
|
SetPowerCard(PowerOnCard, _state.PowerOn, AccentGreen);
|
||||||
|
SetPowerCard(PowerOffCard, !_state.PowerOn, AccentRed);
|
||||||
|
|
||||||
|
// 운전 모드 버튼 강조
|
||||||
|
foreach (var child in ModePanel.Children)
|
||||||
|
{
|
||||||
|
if (child is Button btn && btn.Tag is string tag)
|
||||||
|
{
|
||||||
|
bool active = tag == _state.RunMode.ToString();
|
||||||
|
btn.Background = active ? AccentCyan : CardBg;
|
||||||
|
btn.Foreground = active ? Brushes.Black : TextPrimary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 풍량 버튼 강조
|
||||||
|
foreach (var child in FanPanel.Children)
|
||||||
|
{
|
||||||
|
if (child is Button btn && btn.Tag is string tag)
|
||||||
|
{
|
||||||
|
bool active = tag == _state.FanMode.ToString();
|
||||||
|
btn.Background = active ? AccentBlue : CardBg;
|
||||||
|
btn.Foreground = active ? Brushes.Black : TextPrimary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 자동모드 프리셋 버튼 : 자동에서만 활성, 활성 프리셋 강조 (기본 표준=NORMAL)
|
||||||
|
bool presetEnabled = _state.RunMode == RunMode.Auto;
|
||||||
|
foreach (var child in PresetPanel.Children)
|
||||||
|
{
|
||||||
|
if (child is Button btn && btn.Tag is string tag)
|
||||||
|
{
|
||||||
|
bool active = presetEnabled && tag == _state.HystPreset.ToString();
|
||||||
|
btn.IsEnabled = presetEnabled;
|
||||||
|
btn.Background = active ? AccentBlue : CardBg;
|
||||||
|
btn.Foreground = active ? Brushes.Black : TextPrimary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 시나리오모드 버튼 강조
|
||||||
|
foreach (var child in SubModePanel.Children)
|
||||||
|
{
|
||||||
|
if (child is Button btn && btn.Tag is string tag)
|
||||||
|
{
|
||||||
|
bool active = tag switch { "Sleep" => _state.SmartSleep, "Cook" => _state.CookingMode, "Recovery" => _state.RecoveryMode, _ => false };
|
||||||
|
btn.Background = active ? AccentOrange : CardBg;
|
||||||
|
btn.Foreground = active ? Brushes.Black : TextPrimary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SetPowerCard(Border card, bool active, Brush accent)
|
||||||
|
{
|
||||||
|
if (card.Child is TextBlock tb)
|
||||||
|
{
|
||||||
|
tb.Foreground = active ? Brushes.White : TextSecondary;
|
||||||
|
}
|
||||||
|
card.Background = active ? accent : CardBg;
|
||||||
|
card.BorderBrush = active ? accent : BorderColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateErrorIndicators()
|
||||||
|
{
|
||||||
|
UpdateErrLed(ErrCard_E02, ErrLed_E02, _state.E02_TempSensor, AccentRed);
|
||||||
|
UpdateErrLed(ErrCard_E09, ErrLed_E09, _state.E09_SaFan, AccentRed);
|
||||||
|
UpdateErrLed(ErrCard_E10, ErrLed_E10, _state.E10_EaFan, AccentRed);
|
||||||
|
UpdateErrLed(ErrCard_COLD, ErrLed_COLD, _state.COLD_Protect, AccentBlue);
|
||||||
|
UpdateErrLed(ErrCard_E07, ErrLed_E07, _state.E07_InternalComm, AccentOrange);
|
||||||
|
UpdateErrLed(ErrCard_FCLEAN, ErrLed_FCLEAN, _state.FilterClean, AccentYellow);
|
||||||
|
UpdateErrLed(ErrCard_FCHANGE, ErrLed_FCHANGE, _state.FilterChange, AccentYellow);
|
||||||
|
ErrorCodeHex.Text = $"ErrorCode = 0x{_state.ErrorCode:X2}";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void UpdateErrLed(Border card, System.Windows.Shapes.Ellipse led, bool on, Brush onColor)
|
||||||
|
{
|
||||||
|
led.Fill = on ? onColor : LedOff;
|
||||||
|
card.BorderBrush = on ? onColor : BorderColor;
|
||||||
|
card.BorderThickness = new Thickness(on ? 2 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateRealtime()
|
||||||
|
{
|
||||||
|
ReserveText.Text = _state.ReserveText;
|
||||||
|
// 대시보드 등에서 CTRL_RESERVE 로 설정된 예약을 콤보에 반영(수신 명령도 ERVSim 에서 확인 가능)
|
||||||
|
if (ReserveCombo.SelectedIndex != _state.ReserveHours && _state.ReserveHours >= 0 && _state.ReserveHours <= 8)
|
||||||
|
{
|
||||||
|
_suppressReserveCombo = true;
|
||||||
|
ReserveCombo.SelectedIndex = _state.ReserveHours;
|
||||||
|
_suppressReserveCombo = false;
|
||||||
|
}
|
||||||
|
AutoStateRun.Text = _state.AutoStateText;
|
||||||
|
|
||||||
|
// 후드연동 버튼 — 쾌적조리(HoodEnable) ON 시 강조. 단, 통신중 후드 에러는 빨강+에러명 우선.
|
||||||
|
bool hoodOnline = (DateTime.UtcNow - _hood.LastRxUtc) < TimeSpan.FromSeconds(2);
|
||||||
|
if (hoodOnline && _state.HoodError != 0)
|
||||||
|
{
|
||||||
|
HoodLinkBtn.Background = AccentRed;
|
||||||
|
HoodLinkBtn.Foreground = Brushes.Black;
|
||||||
|
HoodLinkBtn.Content = _state.HoodError == 1 ? "후드연동 FAN에러" : "후드연동 기타에러";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HoodLinkBtn.Background = _state.HoodEnable ? AccentOrange : CardBg;
|
||||||
|
HoodLinkBtn.Foreground = _state.HoodEnable ? Brushes.Black : TextPrimary;
|
||||||
|
HoodLinkBtn.Content = "후드연동";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 후드 통신 상태 (HoodSimulator 폴 응답 생존) — 후드연동 버튼 옆 표시
|
||||||
|
HoodCommLed.Fill = hoodOnline ? AccentGreen : AccentRed;
|
||||||
|
HoodCommText.Text = hoodOnline ? "후드 통신 중" : "후드 통신 안됨";
|
||||||
|
HoodCommText.Foreground = hoodOnline ? AccentGreen : TextSecondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// (꺼짐)예약 1초 카운트다운 — 0 도달 시 전원 OFF
|
||||||
|
void ReserveTick()
|
||||||
|
{
|
||||||
|
if (_state.ReserveRemainSec <= 0) return;
|
||||||
|
_state.ReserveRemainSec--;
|
||||||
|
if (_state.ReserveRemainSec == 0)
|
||||||
|
{
|
||||||
|
_state.ReserveHours = 0;
|
||||||
|
_state.PowerOn = false;
|
||||||
|
_state.FanMode = _state.SetFanMode = 0;
|
||||||
|
_seq.NotifyCommandChanged();
|
||||||
|
if (ReserveCombo.SelectedIndex != 0) ReserveCombo.SelectedIndex = 0;
|
||||||
|
Log("[예약] 예약시간 종료 → 전원 OFF");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 로그 (DiffuserSimulator 와 동일 : 읽기전용 TextBox, 텍스트 드래그 선택/복사 가능) ----
|
||||||
|
void Log(string msg)
|
||||||
|
{
|
||||||
|
var line = $"[{DateTime.Now:HH:mm:ss.fff}] {msg}";
|
||||||
|
Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
LogList.AppendText(line + Environment.NewLine);
|
||||||
|
if (LogList.LineCount > 500)
|
||||||
|
{
|
||||||
|
var lines = LogList.Text.Split(Environment.NewLine);
|
||||||
|
LogList.Text = string.Join(Environment.NewLine, lines[^300..]);
|
||||||
|
}
|
||||||
|
LogList.ScrollToEnd();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearLog_Click(object sender, RoutedEventArgs e) => LogList.Clear();
|
||||||
|
|
||||||
|
void SaveLog_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var dlg = new Microsoft.Win32.SaveFileDialog
|
||||||
|
{
|
||||||
|
Filter = "텍스트 파일 (*.txt)|*.txt",
|
||||||
|
FileName = $"ErvSimLog_{DateTime.Now:yyyyMMdd_HHmmss}.txt"
|
||||||
|
};
|
||||||
|
if (dlg.ShowDialog() == true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string h = $"========================================\r\n ERV 시뮬레이터 통신 로그\r\n 저장 일시: {DateTime.Now:yyyy-MM-dd HH:mm:ss}\r\n========================================\r\n\r\n";
|
||||||
|
System.IO.File.WriteAllText(dlg.FileName, h + LogList.Text);
|
||||||
|
MessageBox.Show($"저장 완료: {dlg.FileName}");
|
||||||
|
}
|
||||||
|
catch (Exception ex) { MessageBox.Show($"저장 실패: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnClosed(EventArgs e)
|
||||||
|
{
|
||||||
|
_roomConCh.Dispose();
|
||||||
|
_homeNetCh.Dispose();
|
||||||
|
_diffuserCh.Dispose();
|
||||||
|
_hoodCh.Dispose();
|
||||||
|
base.OnClosed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Converters ----
|
||||||
|
|
||||||
|
public class BoolToOpenCloseConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type t, object p, CultureInfo c) =>
|
||||||
|
(value is bool b && b) ? "열림" : "닫힘";
|
||||||
|
public object ConvertBack(object v, Type t, object p, CultureInfo c) => DependencyProperty.UnsetValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BoolToBrushConverter : IValueConverter
|
||||||
|
{
|
||||||
|
static readonly Brush Open = (Brush)new BrushConverter().ConvertFromString("#9ECE6A")!;
|
||||||
|
static readonly Brush Close = (Brush)new BrushConverter().ConvertFromString("#F7768E")!;
|
||||||
|
public object Convert(object value, Type t, object p, CultureInfo c) =>
|
||||||
|
(value is bool b && b) ? Open : Close;
|
||||||
|
public object ConvertBack(object v, Type t, object p, CultureInfo c) => DependencyProperty.UnsetValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ColorTagToBrushConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type t, object p, CultureInfo c) =>
|
||||||
|
value switch
|
||||||
|
{
|
||||||
|
"GREEN" => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9ECE6A")!),
|
||||||
|
"YELLOW" => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E0AF68")!),
|
||||||
|
"RED" => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F7768E")!),
|
||||||
|
"BLACK" => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1E1E2E")!),
|
||||||
|
"BLUE" => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#7AA2F7")!),
|
||||||
|
"WHITE" => new SolidColorBrush(Colors.WhiteSmoke),
|
||||||
|
_ => new SolidColorBrush(Colors.Gray),
|
||||||
|
};
|
||||||
|
public object ConvertBack(object v, Type t, object p, CultureInfo c) => DependencyProperty.UnsetValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace ERVSimulator.Model
|
||||||
|
{
|
||||||
|
// 펌웨어 [My_system.c] Air_Quality_damper_process() 포팅 (260520 사양)
|
||||||
|
// - 실별 4종센서 → 0~4 Level (모드별 임계 + 하강 히스테리시스)
|
||||||
|
// - 부하총점(Score) → 풍량단수, P_max/dP → 댐퍼(대기/집중/분산)
|
||||||
|
// - 1초 주기. RunMode==Auto && PowerOn 일 때만 댐퍼/풍량 구동.
|
||||||
|
public class AutoLogic
|
||||||
|
{
|
||||||
|
readonly ErvState _state;
|
||||||
|
readonly DamperSequencer _seq;
|
||||||
|
readonly DispatcherTimer _timer;
|
||||||
|
public event Action<string>? Log;
|
||||||
|
|
||||||
|
// 센서별 이전 단계(히스테리시스 데드존 유지) [room 1..4]
|
||||||
|
readonly int[] _prevCo2 = new int[5];
|
||||||
|
readonly int[] _prevPm25 = new int[5];
|
||||||
|
readonly int[] _prevPm10 = new int[5];
|
||||||
|
readonly int[] _prevVoc = new int[5];
|
||||||
|
|
||||||
|
// ---- 쾌적조리(후드연동) 메이크업 에어 상태 (사양서 260613 9p) ----
|
||||||
|
// 쾌적조리는 운전모드가 아닌 후드연동 토글. 토글 ON + 후드 가동중일 때만 메이크업 에어 발동.
|
||||||
|
// 조리 종료 후 잔여 배출(메이크업 유지)은 후드측이 담당 → ERV는 후드 OFF 신호 받으면 즉시 원래 상태 복귀.
|
||||||
|
bool _makeup; // 메이크업 에어(강제 연동) 동작중
|
||||||
|
byte _makeupFan; // 후드 단수 추종 결과 풍량
|
||||||
|
|
||||||
|
// 시나리오모드(안심회복/스마트수면/쾌적조리) 해제 시 진입 직전 풍량 복귀용 (운전모드는 시뮬에서 유지됨)
|
||||||
|
bool _prevScenario;
|
||||||
|
byte _scenarioSavedFan;
|
||||||
|
|
||||||
|
// 스마트수면 : 실별 CO2 히스테리시스 댐퍼 개폐 상태 (사양서 8p, >=1000 OPEN / <=800 CLOSE)
|
||||||
|
bool _prevSmartSleep;
|
||||||
|
readonly bool[] _sleepOpen = new bool[5]; // [room 1..4] true=OPEN
|
||||||
|
|
||||||
|
public AutoLogic(ErvState state, DamperSequencer seq)
|
||||||
|
{
|
||||||
|
_state = state; _seq = seq;
|
||||||
|
_timer = new DispatcherTimer(DispatcherPriority.Normal) { Interval = TimeSpan.FromSeconds(1) };
|
||||||
|
_timer.Tick += (_, _) => Process();
|
||||||
|
_timer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 센서값 -> 0~4 단계. 하강 시 (임계-데드밴드) 이하라야 내려감. 데드존이면 이전 유지.
|
||||||
|
static int SensorLevel(int v, ushort[] t, ushort db, int prev)
|
||||||
|
{
|
||||||
|
int lv = prev;
|
||||||
|
if (v <= t[0] - db) lv = 0;
|
||||||
|
else if (v > t[0] && v <= t[1] - db) lv = 1;
|
||||||
|
else if (v > t[1] && v <= t[2] - db) lv = 2;
|
||||||
|
else if (v > t[2] && v <= t[3] - db) lv = 3;
|
||||||
|
else if (v > t[3]) lv = 4;
|
||||||
|
return lv;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ScoreToStage(int s)
|
||||||
|
{
|
||||||
|
if (s == 0) return 0;
|
||||||
|
if (s <= 4) return 1;
|
||||||
|
if (s <= 8) return 2;
|
||||||
|
if (s <= 12) return 3;
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level(0좋음~4매우나쁨) -> 공기질코드(1매우나쁨~4좋음)
|
||||||
|
static int AirqCode(int level) => level switch { 0 => 4, 1 => 3, 2 => 2, _ => 1 };
|
||||||
|
|
||||||
|
// 쾌적조리(후드연동) 메이크업 에어 상태 갱신. 반환=이번 틱 메이크업 에어 적용 여부.
|
||||||
|
// 매트릭스(9p 3.1) : 쾌적조리 OFF → 연동없음 / ON+후드꺼짐 → 대기(본래 설정) / ON+후드켜짐 → 메이크업 에어(강제 연동)
|
||||||
|
// 잔여 배출(메이크업 유지)은 후드측이 담당 → 후드 OFF 신호 받으면 ERV는 즉시 원래 상태 복귀(3.3).
|
||||||
|
bool UpdateCooking()
|
||||||
|
{
|
||||||
|
bool cookEnabled = _state.PowerOn && _state.HoodEnable; // 쾌적조리 토글(전원 ON 전제)
|
||||||
|
bool hoodOn = cookEnabled && _state.HoodCmd && _state.HoodFan > 0; // 후드 가동중(전원+풍량)
|
||||||
|
|
||||||
|
if (hoodOn)
|
||||||
|
{
|
||||||
|
if (!_makeup)
|
||||||
|
{
|
||||||
|
_makeup = true;
|
||||||
|
Log?.Invoke("[쾌적조리] 메이크업 에어 진입 — 자동/수동 일시정지");
|
||||||
|
}
|
||||||
|
_makeupFan = (byte)Math.Min((int)_state.HoodFan, 4); // 단수 추종(3.2) : 1→1,2→2,3→3,4→4,5→4
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 후드 정지 또는 쾌적조리 해제 → 즉시 원래 상태 복귀(메이크업 유지는 후드측이 담당)
|
||||||
|
if (_makeup) EndMakeup(cookEnabled ? "후드 OFF — 원래 상태 복귀" : "쾌적조리 해제");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndMakeup(string why)
|
||||||
|
{
|
||||||
|
_makeup = false;
|
||||||
|
// 진입 직전 풍량 복귀는 Process()의 시나리오 해제 처리에서 일괄 수행.
|
||||||
|
Log?.Invoke($"[쾌적조리] 메이크업 종료 ({why})");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Process()
|
||||||
|
{
|
||||||
|
int p = _state.HystPreset;
|
||||||
|
|
||||||
|
// ---- 실별 Level 산출 (항상 - 표시용) ----
|
||||||
|
for (int r = 1; r <= 4; r++)
|
||||||
|
{
|
||||||
|
var room = _state.GetRoom(r);
|
||||||
|
int lc = SensorLevel(room.Co2, _state.Co2Thr[p], _state.Co2Db[p], _prevCo2[r]);
|
||||||
|
int l25 = SensorLevel(room.Pm25, _state.Pm25Thr[p], _state.Pm25Db[p], _prevPm25[r]);
|
||||||
|
int l10 = SensorLevel(room.Pm10, _state.Pm10Thr[p], _state.Pm10Db[p], _prevPm10[r]);
|
||||||
|
int lv = SensorLevel(room.Voc, _state.VocThr[p], _state.VocDb[p], _prevVoc[r]);
|
||||||
|
_prevCo2[r] = lc; _prevPm25[r] = l25; _prevPm10[r] = l10; _prevVoc[r] = lv;
|
||||||
|
|
||||||
|
int level = Math.Max(Math.Max(lc, l25), Math.Max(l10, lv));
|
||||||
|
room.Level = level;
|
||||||
|
room.AirQuality = AirqCode(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 부하총점 / P_max / dP (260428 v.Final : dP = 정렬 내림차순[0]-[1], 동점 포함) ----
|
||||||
|
// 최고단계 실이 2개 이상 동점이면 P_2nd=P_max → dP=0 → 분산. 한 실만 확실히(2↑) 나쁠 때만 집중.
|
||||||
|
int score = 0;
|
||||||
|
int[] levels = new int[4];
|
||||||
|
for (int r = 1; r <= 4; r++) { levels[r - 1] = _state.GetRoom(r).Level; score += levels[r - 1]; }
|
||||||
|
Array.Sort(levels); // 오름차순
|
||||||
|
int pmax = levels[3]; // 최고 단계
|
||||||
|
int p2nd = levels[2]; // 두번째로 높은 단계(동점 포함)
|
||||||
|
int dP = pmax - p2nd;
|
||||||
|
|
||||||
|
_state.LoadScore = score;
|
||||||
|
_state.PMax = pmax;
|
||||||
|
_state.DP = dP;
|
||||||
|
|
||||||
|
// ---- 쾌적조리(후드연동) 메이크업 에어 상태 갱신 → 연동운전중(HoodStatus) 소유 ----
|
||||||
|
bool makeupEffective = UpdateCooking();
|
||||||
|
_state.HoodStatus = makeupEffective; // 후드 폴 응답 '연동운전중'(롤백 유지 포함)
|
||||||
|
|
||||||
|
// ---- 시나리오모드 해제 → 진입 직전 운전모드로 동작 복귀 ----
|
||||||
|
// 시뮬은 시나리오 중에도 RunMode 를 유지(오버레이)하므로 운전모드는 자동 복귀.
|
||||||
|
// 시나리오가 덮어쓴 풍량만 진입 직전 값으로 되돌린다(비자동 한정, 자동은 재계산).
|
||||||
|
bool scenarioActive = _state.RecoveryMode || _state.SmartSleep || makeupEffective;
|
||||||
|
if (scenarioActive && !_prevScenario)
|
||||||
|
_scenarioSavedFan = _state.SetFanMode; // 진입 직전 풍량 저장
|
||||||
|
else if (!scenarioActive && _prevScenario && _state.PowerOn && _state.RunMode != RunMode.Auto)
|
||||||
|
_state.FanMode = _state.SetFanMode = _scenarioSavedFan; // 해제 → 이전 풍량 복귀
|
||||||
|
_prevScenario = scenarioActive;
|
||||||
|
|
||||||
|
// ---- 댐퍼/풍량 구동 (펌웨어 Air_Quality_damper_process 와 동일) ----
|
||||||
|
// 대시보드 수동 댐퍼/LED 제어는 환기·공청·바이패스(비자동·시나리오모드 아님)에서만 유지.
|
||||||
|
// 그 외(자동·시나리오모드·전원OFF)에서는 수동 플래그 해제 → 자동 제어 복귀.
|
||||||
|
// 쾌적조리 '대기 상태'(토글 ON·후드 꺼짐)는 본래 설정대로 가동 → subActive 아님.
|
||||||
|
bool subActive = _state.RecoveryMode || _state.SmartSleep || makeupEffective;
|
||||||
|
bool manualAllowed = _state.PowerOn && _state.RunMode != RunMode.Auto && !subActive;
|
||||||
|
// 댐퍼 수동 : 환기/공청/바이패스(비자동·비시나리오)에서만 유지, 그 외 해제 → 자동 제어 복귀.
|
||||||
|
// LED 수동 : 모든 운전모드·댐퍼 변경에도 유지(사용자 요청). 전원 OFF 시에만 해제 → 자동 추종(소등) 복귀.
|
||||||
|
for (int r = 1; r <= 4; r++)
|
||||||
|
{
|
||||||
|
var rm = _state.GetRoom(r);
|
||||||
|
if (!manualAllowed) rm.DamperManual = false;
|
||||||
|
if (!_state.PowerOn) rm.LedManual = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool damperChanged = false;
|
||||||
|
void SetDamper(DiffuserRoom rm, int sa, int ra)
|
||||||
|
{
|
||||||
|
if (rm.MemorySA != sa || rm.MemoryRA != ra) damperChanged = true;
|
||||||
|
rm.MemorySA = sa; rm.MemoryRA = ra;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool fanChanged = false;
|
||||||
|
string logTag;
|
||||||
|
void SetFan(byte st) { if (_state.FanMode != st) { _state.FanMode = _state.SetFanMode = st; fanChanged = true; } }
|
||||||
|
|
||||||
|
if (!_state.PowerOn)
|
||||||
|
{
|
||||||
|
// 전원 OFF : 전 실 즉시 닫힘 (18초 슬롯 시퀀스 대기 없이 Current 직접 0)
|
||||||
|
_state.AutoConcentrate = false;
|
||||||
|
for (int r = 1; r <= 4; r++)
|
||||||
|
{
|
||||||
|
var rm = _state.GetRoom(r);
|
||||||
|
SetDamper(rm, 0, 0);
|
||||||
|
rm.CurrentSA = 0; rm.CurrentRA = 0;
|
||||||
|
}
|
||||||
|
logTag = "전원OFF 전실 닫힘";
|
||||||
|
}
|
||||||
|
else if (makeupEffective) // 쾌적조리 메이크업 에어 : 전실 급기(SA) 100% 개방, 배기(RA) 닫힘, 후드 단수 추종
|
||||||
|
{
|
||||||
|
_state.AutoConcentrate = false;
|
||||||
|
for (int r = 1; r <= 4; r++) SetDamper(_state.GetRoom(r), 110, 0);
|
||||||
|
SetFan(_makeupFan);
|
||||||
|
logTag = $"쾌적조리 메이크업에어(전실 급기) {_makeupFan}단";
|
||||||
|
}
|
||||||
|
else if (_state.RecoveryMode) // 안심회복 : 침실1 음압 (급기X 배기O), 나머지 급기O 배기X, 2단
|
||||||
|
{
|
||||||
|
_state.AutoConcentrate = false;
|
||||||
|
SetDamper(_state.GetRoom(1), 110, 0); // 거실 급기
|
||||||
|
SetDamper(_state.GetRoom(2), 0, 110); // 침실1 배기(음압)
|
||||||
|
SetDamper(_state.GetRoom(3), 110, 0); // 침실2 급기
|
||||||
|
SetDamper(_state.GetRoom(4), 110, 0); // 침실3 급기
|
||||||
|
SetFan(2);
|
||||||
|
logTag = "안심회복(침실1 음압) 2단";
|
||||||
|
}
|
||||||
|
else if (_state.SmartSleep) // 스마트수면 : 1단 고정, 실별 CO2 기준 댐퍼 개폐 (사양서 8p)
|
||||||
|
{
|
||||||
|
_state.AutoConcentrate = false;
|
||||||
|
// 진입 초기상태 : 거실 CLOSE, 침실1~3 OPEN (이후 CO2 히스테리시스의 데드존 시드)
|
||||||
|
if (!_prevSmartSleep)
|
||||||
|
{
|
||||||
|
_sleepOpen[1] = false;
|
||||||
|
_sleepOpen[2] = _sleepOpen[3] = _sleepOpen[4] = true;
|
||||||
|
}
|
||||||
|
// CO2 센서 기준 : 해당 실 CO2 >= 1000 OPEN, <= 800 CLOSE, 그 사이(데드존)는 현재 상태 유지
|
||||||
|
for (int r = 1; r <= 4; r++)
|
||||||
|
{
|
||||||
|
int co2 = _state.GetRoom(r).Co2;
|
||||||
|
if (co2 >= 1000) _sleepOpen[r] = true;
|
||||||
|
else if (co2 <= 800) _sleepOpen[r] = false;
|
||||||
|
int ang = _sleepOpen[r] ? 110 : 0;
|
||||||
|
SetDamper(_state.GetRoom(r), ang, ang);
|
||||||
|
}
|
||||||
|
SetFan(1);
|
||||||
|
logTag = "스마트수면 CO2기준 실별개폐 1단";
|
||||||
|
}
|
||||||
|
else if (_state.RunMode != RunMode.Auto)
|
||||||
|
{
|
||||||
|
// 환기/공청/바이패스 : 각실 SA/RA 개방. 단, 대시보드 수동 댐퍼(DamperManual) 실은 그 위치 유지.
|
||||||
|
_state.AutoConcentrate = false;
|
||||||
|
for (int r = 1; r <= 4; r++)
|
||||||
|
{
|
||||||
|
var rm = _state.GetRoom(r);
|
||||||
|
if (rm.DamperManual) continue; // 대시보드 수동 댐퍼 - 자동 개방 덮어쓰기 안 함
|
||||||
|
SetDamper(rm, 110, 110);
|
||||||
|
}
|
||||||
|
logTag = $"{_state.RunMode} 각실 SA/RA 개방";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 자동 : 대기 / 집중 / 분산
|
||||||
|
if (pmax == 0)
|
||||||
|
{
|
||||||
|
_state.AutoConcentrate = false;
|
||||||
|
for (int r = 1; r <= 4; r++) SetDamper(_state.GetRoom(r), 0, 0);
|
||||||
|
}
|
||||||
|
else if (dP >= 2)
|
||||||
|
{
|
||||||
|
_state.AutoConcentrate = true;
|
||||||
|
for (int r = 1; r <= 4; r++)
|
||||||
|
{
|
||||||
|
var rm = _state.GetRoom(r);
|
||||||
|
int ang = rm.Level == pmax ? 110 : 0;
|
||||||
|
SetDamper(rm, ang, ang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 분산 (260428 v.Final) : 1단계 이상 실만 개방, 0단계(좋음) 실은 닫음
|
||||||
|
_state.AutoConcentrate = false;
|
||||||
|
for (int r = 1; r <= 4; r++)
|
||||||
|
{
|
||||||
|
var rm = _state.GetRoom(r);
|
||||||
|
int ang = rm.Level >= 1 ? 110 : 0;
|
||||||
|
SetDamper(rm, ang, ang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 최종 풍량 : 부하총점 매핑
|
||||||
|
byte stage = (byte)ScoreToStage(score);
|
||||||
|
if (_state.FanMode != stage) { _state.FanMode = _state.SetFanMode = stage; fanChanged = true; }
|
||||||
|
logTag = $"자동 {(pmax == 0 ? "대기" : dP >= 2 ? "집중" : "분산")} Score={score} dP={dP} → {_state.FanMode}단";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- LED : 댐퍼(SA/RA 중 하나라도 개방) 추종. 닫히면 0. 수동 조작(LedManual)은 예외 ----
|
||||||
|
for (int r = 1; r <= 4; r++)
|
||||||
|
{
|
||||||
|
var rm = _state.GetRoom(r);
|
||||||
|
if (rm.LedManual) continue;
|
||||||
|
int want = (_state.PowerOn && (rm.MemorySA > 0 || rm.MemoryRA > 0)) ? 9 : 0;
|
||||||
|
if (rm.LightBright != want) rm.LightBright = want;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fanChanged || damperChanged)
|
||||||
|
{
|
||||||
|
_seq.NotifyCommandChanged();
|
||||||
|
Log?.Invoke($"[댐퍼] {logTag}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_prevSmartSleep = _state.SmartSleep; // 다음 틱의 스마트수면 진입 감지용
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace ERVSimulator.Model
|
||||||
|
{
|
||||||
|
// 펌웨어 [MyMotor.c] Damper_Mode() + Diffuser_Damper_process() 시퀀스를 흉내
|
||||||
|
// - Cmd 변경 시 18초 시퀀스 트리거 (slot 180 / 120 / 60, 100ms tick)
|
||||||
|
// - 본체 댐퍼 6개: Run_Mode 에 따라 즉시 목표각 세팅
|
||||||
|
// - 디퓨저 댐퍼: Memory → Current 슬롯별 복사
|
||||||
|
// - 팬 PWM: 매 tick ±1 ramp
|
||||||
|
public class DamperSequencer
|
||||||
|
{
|
||||||
|
public ErvState State { get; }
|
||||||
|
private readonly DispatcherTimer _timer;
|
||||||
|
private int _diffuserSlot; // 180..0 카운트다운
|
||||||
|
private int _seqType; // 1=on, 2=off, 3=decrease, 4=increase
|
||||||
|
private int _prevAirVolume;
|
||||||
|
private bool _pendingSequence;
|
||||||
|
|
||||||
|
public DamperSequencer(ErvState state)
|
||||||
|
{
|
||||||
|
State = state;
|
||||||
|
_timer = new DispatcherTimer(DispatcherPriority.Normal)
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromMilliseconds(100)
|
||||||
|
};
|
||||||
|
_timer.Tick += OnTick;
|
||||||
|
_timer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoomCon/HomeNet 핸들러가 Run_Mode/Fan_Mode 갱신 후 호출
|
||||||
|
public void NotifyCommandChanged()
|
||||||
|
{
|
||||||
|
_pendingSequence = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnTick(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
// ---- Fan_Speed_process() 시작부: VENT && Fan=0 → 정지 진입 ----
|
||||||
|
int newAirVolume = (State.RunMode != RunMode.Off && State.FanMode != 0) ? State.FanMode : 0;
|
||||||
|
|
||||||
|
if (_pendingSequence)
|
||||||
|
{
|
||||||
|
_pendingSequence = false;
|
||||||
|
_diffuserSlot = 180;
|
||||||
|
_seqType = DetermineSeqType(_prevAirVolume, newAirVolume);
|
||||||
|
_prevAirVolume = newAirVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Damper_Mode(Run_Mode) — 본체 댐퍼 즉시 목표각 ----
|
||||||
|
ApplyBodyDamperMode(EffectiveBodyMode());
|
||||||
|
|
||||||
|
// ---- Diffuser_Damper_process() — 슬롯 기반 적용 ----
|
||||||
|
if (_diffuserSlot == 180)
|
||||||
|
{
|
||||||
|
if (_seqType == 1 || _seqType == 4)
|
||||||
|
CopyMemoryToCurrent(1, 2);
|
||||||
|
else if (_seqType == 2 || _seqType == 3)
|
||||||
|
SetFanTargets(); // 즉시 ramp 시작
|
||||||
|
}
|
||||||
|
else if (_diffuserSlot == 120)
|
||||||
|
{
|
||||||
|
if (_seqType == 1 || _seqType == 4 || _seqType == 2 || _seqType == 3)
|
||||||
|
CopyMemoryToCurrent(3, 4);
|
||||||
|
}
|
||||||
|
else if (_diffuserSlot == 60)
|
||||||
|
{
|
||||||
|
if (_seqType == 1 || _seqType == 4)
|
||||||
|
SetFanTargets();
|
||||||
|
else if (_seqType == 2 || _seqType == 3)
|
||||||
|
CopyMemoryToCurrent(1, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_diffuserSlot > 0) _diffuserSlot--;
|
||||||
|
|
||||||
|
// ---- 팬 ramp ±1 (펌웨어와 동일) ----
|
||||||
|
if (State.Fan1Current < State.Fan1Target) State.Fan1Current++;
|
||||||
|
else if (State.Fan1Current > State.Fan1Target) State.Fan1Current--;
|
||||||
|
if (State.Fan2Current < State.Fan2Target) State.Fan2Current++;
|
||||||
|
else if (State.Fan2Current > State.Fan2Target) State.Fan2Current--;
|
||||||
|
}
|
||||||
|
|
||||||
|
int DetermineSeqType(int prev, int now)
|
||||||
|
{
|
||||||
|
if (prev == 0 && now != 0) return 4; // increase (power on)
|
||||||
|
if (prev != 0 && now == 0) return 3; // decrease (power off)
|
||||||
|
if (prev > now) return 3;
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
RunMode EffectiveBodyMode()
|
||||||
|
{
|
||||||
|
// VENT && Fan=0 → 본체 댐퍼는 MODE_OFF 로 진입 (펌웨어 Fan_Speed_process 분기)
|
||||||
|
if (State.RunMode == RunMode.Off) return RunMode.Off;
|
||||||
|
if (State.RunMode == RunMode.Ventilation && State.FanMode == 0 &&
|
||||||
|
State.Fan1Current == 0 && State.Fan2Current == 0)
|
||||||
|
return RunMode.Off;
|
||||||
|
return State.RunMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 펌웨어 Damper_Mode() — MyMotor.c:472
|
||||||
|
void ApplyBodyDamperMode(RunMode mode)
|
||||||
|
{
|
||||||
|
switch (mode)
|
||||||
|
{
|
||||||
|
case RunMode.Ventilation:
|
||||||
|
SetBody(DamperId.OA, 0); SetBody(DamperId.EA, 0); SetBody(DamperId.BYPASS, 100);
|
||||||
|
SetBody(DamperId.SA, 0); SetBody(DamperId.RA, 70); SetBody(DamperId.AIR, 105);
|
||||||
|
break;
|
||||||
|
case RunMode.AirClean:
|
||||||
|
SetBody(DamperId.OA, 100); SetBody(DamperId.EA, 100); SetBody(DamperId.BYPASS, 100);
|
||||||
|
SetBody(DamperId.SA, 0); SetBody(DamperId.RA, 140); SetBody(DamperId.AIR, 0);
|
||||||
|
break;
|
||||||
|
case RunMode.Bypass:
|
||||||
|
SetBody(DamperId.OA, 0); SetBody(DamperId.EA, 0); SetBody(DamperId.BYPASS, 0);
|
||||||
|
SetBody(DamperId.SA, 0); SetBody(DamperId.RA, 140); SetBody(DamperId.AIR, 105);
|
||||||
|
break;
|
||||||
|
case RunMode.Auto:
|
||||||
|
// 펌웨어는 자동 시 공기질에 따라 VENT/AIR 선택. 단순화: VENT 와 동일.
|
||||||
|
SetBody(DamperId.OA, 0); SetBody(DamperId.EA, 0); SetBody(DamperId.BYPASS, 100);
|
||||||
|
SetBody(DamperId.SA, 0); SetBody(DamperId.RA, 70); SetBody(DamperId.AIR, 105);
|
||||||
|
break;
|
||||||
|
case RunMode.Off:
|
||||||
|
default:
|
||||||
|
SetBody(DamperId.OA, 100); SetBody(DamperId.EA, 100); SetBody(DamperId.BYPASS, 100);
|
||||||
|
SetBody(DamperId.SA, 100); SetBody(DamperId.RA, 0); SetBody(DamperId.AIR, 105);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetBody(DamperId id, int angle) => State.GetDamper(id).TargetAngle = angle;
|
||||||
|
|
||||||
|
void CopyMemoryToCurrent(int fromRoom, int toRoom)
|
||||||
|
{
|
||||||
|
for (int r = fromRoom; r <= toRoom; r++)
|
||||||
|
{
|
||||||
|
var room = State.GetRoom(r);
|
||||||
|
room.CurrentSA = room.MemorySA;
|
||||||
|
room.CurrentRA = room.MemoryRA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 펌웨어 Fan_Speed_Setting(Run_Mode, Fan_Mode) — MyMotor.c:1233
|
||||||
|
void SetFanTargets()
|
||||||
|
{
|
||||||
|
int idx = System.Math.Clamp(State.FanMode, (byte)0, (byte)4);
|
||||||
|
switch (State.RunMode)
|
||||||
|
{
|
||||||
|
case RunMode.Ventilation:
|
||||||
|
case RunMode.Auto:
|
||||||
|
State.Fan1Target = State.FanSAPreset_Vent[idx];
|
||||||
|
State.Fan2Target = State.FanEAPreset_Vent[idx];
|
||||||
|
break;
|
||||||
|
case RunMode.Bypass:
|
||||||
|
State.Fan1Target = State.FanSAPreset_Bypass[idx];
|
||||||
|
State.Fan2Target = State.FanEAPreset_Bypass[idx];
|
||||||
|
break;
|
||||||
|
case RunMode.AirClean:
|
||||||
|
State.Fan1Target = State.FanSAPreset_Air[idx];
|
||||||
|
State.Fan2Target = State.FanEAPreset_Air[idx];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
State.Fan1Target = 0;
|
||||||
|
State.Fan2Target = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace ERVSimulator.Model
|
||||||
|
{
|
||||||
|
// 6 본체 댐퍼 (Damper_Mode()가 직접 각도 명령)
|
||||||
|
public class BodyDamper : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
public string Connector { get; } // CN2, CN10 ...
|
||||||
|
public string ColorTag { get; } // GREEN, YELLOW ...
|
||||||
|
public DamperId Id { get; }
|
||||||
|
|
||||||
|
private int _targetAngle;
|
||||||
|
public int TargetAngle
|
||||||
|
{
|
||||||
|
get => _targetAngle;
|
||||||
|
set { if (_targetAngle != value) { _targetAngle = value; OnChanged(); OnChanged(nameof(IsOpen)); } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 펌웨어 주석: 90 = close, 0 = open, 100/105 = close 변형
|
||||||
|
// RA(환기)만 '3step--reverse' → 0=닫힘, 70/140=열림 으로 규칙 반대 (MyMotor.c:482)
|
||||||
|
// → 전원 OFF(RA=0) 시 6개 댐퍼 모두 닫힘으로 표시
|
||||||
|
public bool IsOpen => Id == DamperId.RA ? TargetAngle >= 50 : TargetAngle < 50;
|
||||||
|
|
||||||
|
public BodyDamper(DamperId id, string name, string cn, string color)
|
||||||
|
{
|
||||||
|
Id = id; Name = name; Connector = cn; ColorTag = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
void OnChanged([CallerMemberName] string? n = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 디퓨저 각실 (1~4)
|
||||||
|
public class DiffuserRoom : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
public int RoomId { get; }
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
int _memorySA, _memoryRA, _currentSA, _currentRA, _light;
|
||||||
|
// Memory_* (RoomCon/HomeNet 핸들러가 즉시 갱신, 목표값)
|
||||||
|
public int MemorySA { get => _memorySA; set { if (_memorySA != value) { _memorySA = value; OnChanged(); } } }
|
||||||
|
public int MemoryRA { get => _memoryRA; set { if (_memoryRA != value) { _memoryRA = value; OnChanged(); } } }
|
||||||
|
// Diffuser_Dmp_Ang_* (시퀀서가 슬롯 시간에 Memory→Current 복사)
|
||||||
|
public int CurrentSA { get => _currentSA; set { if (_currentSA != value) { _currentSA = value; OnChanged(); OnChanged(nameof(IsOpenSA)); } } }
|
||||||
|
public int CurrentRA { get => _currentRA; set { if (_currentRA != value) { _currentRA = value; OnChanged(); OnChanged(nameof(IsOpenRA)); } } }
|
||||||
|
public int LightBright { get => _light; set { if (_light != value) { _light = value; OnChanged(); } } }
|
||||||
|
// 디퓨저 응답이 echo 한 실제 LED 단수 (디퓨저 수동 LED 제어 시 ERV 명령과 다를 수 있음) → STATUS 로 송신
|
||||||
|
int _ledReported;
|
||||||
|
public int LedReported { get => _ledReported; set { if (_ledReported != value) { _ledReported = value; OnChanged(); } } }
|
||||||
|
// 수동 LED 조작(CTRL_LED) 시 true → 자동로직이 LED 를 덮어쓰지 않음(예외). 비-수동모드 진입 시 해제.
|
||||||
|
public bool LedManual { get; set; }
|
||||||
|
// 수동 댐퍼 조작(CTRL_DAMPER) 시 true → 비자동(환기/공청/바이패스)에서 자동개방 덮어쓰기 안 함. 자동/부가모드/전원OFF/모드전환 시 해제.
|
||||||
|
public bool DamperManual { get; set; }
|
||||||
|
public bool IsOpenSA => CurrentSA > 0;
|
||||||
|
public bool IsOpenRA => CurrentRA > 0;
|
||||||
|
|
||||||
|
// ---- 공기질 센서값 (DiffuserSimulator 응답에서 수신) ----
|
||||||
|
int _co2, _pm25, _pm10, _voc, _level, _airQuality = 4, _temp, _humi;
|
||||||
|
public int Co2 { get => _co2; set { if (_co2 != value) { _co2 = value; OnChanged(); } } }
|
||||||
|
public int Pm25 { get => _pm25; set { if (_pm25 != value) { _pm25 = value; OnChanged(); } } }
|
||||||
|
public int Pm10 { get => _pm10; set { if (_pm10 != value) { _pm10 = value; OnChanged(); } } }
|
||||||
|
public int Voc { get => _voc; set { if (_voc != value) { _voc = value; OnChanged(); } } }
|
||||||
|
public int Temp { get => _temp; set { if (_temp != value) { _temp = value; OnChanged(); } } }
|
||||||
|
public int Humi { get => _humi; set { if (_humi != value) { _humi = value; OnChanged(); } } }
|
||||||
|
// 오염 단계 0~4 (자동로직 산출)
|
||||||
|
public int Level { get => _level; set { if (_level != value) { _level = value; OnChanged(); OnChanged(nameof(SensorText)); } } }
|
||||||
|
// 공기질 코드 1매우나쁨~4좋음 (STATUS 송신용)
|
||||||
|
public int AirQuality { get => _airQuality; set { if (_airQuality != value) { _airQuality = value; OnChanged(); } } }
|
||||||
|
|
||||||
|
public string SensorText => $"CO2 {Co2} PM2.5 {Pm25} PM10 {Pm10} VOC {Voc} → Lv{Level}";
|
||||||
|
|
||||||
|
public DiffuserRoom(int id, string name) { RoomId = id; Name = name; }
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
void OnChanged([CallerMemberName] string? n = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ErvState : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
// ---- 상위 상태 ----
|
||||||
|
bool _powerOn;
|
||||||
|
RunMode _runMode = RunMode.Off;
|
||||||
|
RunMode _setRunMode = RunMode.Off;
|
||||||
|
byte _fanMode; // 0~4 단
|
||||||
|
byte _setFanMode;
|
||||||
|
|
||||||
|
public bool PowerOn { get => _powerOn; set { if (_powerOn != value) { _powerOn = value; OnChanged(); } } }
|
||||||
|
public RunMode RunMode { get => _runMode; set { if (_runMode != value) { _runMode = value; OnChanged(); } } }
|
||||||
|
public RunMode SetRunMode { get => _setRunMode; set { if (_setRunMode != value) { _setRunMode = value; OnChanged(); } } }
|
||||||
|
public byte FanMode { get => _fanMode; set { if (_fanMode != value) { _fanMode = value; OnChanged(); } } }
|
||||||
|
public byte SetFanMode { get => _setFanMode; set { if (_setFanMode != value) { _setFanMode = value; OnChanged(); } } }
|
||||||
|
|
||||||
|
// ---- 260520 자동 동작로직 상태 ----
|
||||||
|
byte _hystPreset = 1; // 0 ECO / 1 NORMAL / 2 TURBO
|
||||||
|
bool _autoConcentrate; // false 분산 / true 집중
|
||||||
|
int _loadScore, _pMax, _dP;
|
||||||
|
public byte HystPreset { get => _hystPreset; set { if (_hystPreset != value) { _hystPreset = value; OnChanged(); } } }
|
||||||
|
public bool AutoConcentrate { get => _autoConcentrate; set { if (_autoConcentrate != value) { _autoConcentrate = value; OnChanged(); OnChanged(nameof(AutoStateText)); } } }
|
||||||
|
public int LoadScore { get => _loadScore; set { if (_loadScore != value) { _loadScore = value; OnChanged(); OnChanged(nameof(AutoStateText)); } } }
|
||||||
|
public int PMax { get => _pMax; set { if (_pMax != value) { _pMax = value; OnChanged(); } } }
|
||||||
|
public int DP { get => _dP; set { if (_dP != value) { _dP = value; OnChanged(); } } }
|
||||||
|
public string AutoStateText => RunMode == RunMode.Auto
|
||||||
|
? $"{(PMax == 0 ? "대기" : AutoConcentrate ? "집중" : "분산")} · Score {LoadScore} · {FanMode}단"
|
||||||
|
: "(자동모드 아님)";
|
||||||
|
|
||||||
|
// 부가모드 (월패드 토글/버튼)
|
||||||
|
byte _extRunMode;
|
||||||
|
bool _hoodEnable;
|
||||||
|
public byte ExtRunMode { get => _extRunMode; set { if (_extRunMode != value) { _extRunMode = value; OnChanged(); OnChanged(nameof(SubModeText)); } } } // 1 안심회복 / 4 스마트수면
|
||||||
|
public bool HoodEnable { get => _hoodEnable; set { if (_hoodEnable != value) { _hoodEnable = value; OnChanged(); OnChanged(nameof(SubModeText)); } } } // 후드연동(쾌적조리)
|
||||||
|
public bool HoodStatus { get; set; }
|
||||||
|
public byte ResetState { get; set; } // ERV 리셋 토글 echo
|
||||||
|
|
||||||
|
// ---- 후드(HOOD 프로토콜 Rev1.3) 슬레이브 보고값 ----
|
||||||
|
bool _hoodConnected;
|
||||||
|
public bool HoodConnected { get => _hoodConnected; set { if (_hoodConnected != value) { _hoodConnected = value; OnChanged(); } } } // 후드 폴 응답 생존(통신 연결)
|
||||||
|
int _hoodFan; bool _hoodLight; bool _hoodCmd; int _hoodError;
|
||||||
|
public int HoodFan { get => _hoodFan; set { if (_hoodFan != value) { _hoodFan = value; OnChanged(); } } } // 후드 FAN STATUS 0~5
|
||||||
|
public bool HoodLight { get => _hoodLight; set { if (_hoodLight != value) { _hoodLight = value; OnChanged(); } } } // 후드 LIGHT STATUS
|
||||||
|
public bool HoodCmd { get => _hoodCmd; set { if (_hoodCmd != value) { _hoodCmd = value; OnChanged(); } } } // 연동 CMD(후드 동작중)
|
||||||
|
public int HoodError { get => _hoodError; set { if (_hoodError != value) { _hoodError = value; OnChanged(); } } } // ERROR : 0 정상 / 1 FAN / 2 기타
|
||||||
|
|
||||||
|
public bool SmartSleep { get => ExtRunMode == 4; } // 스마트수면
|
||||||
|
public bool CookingMode { get => HoodEnable; } // 쾌적조리(후드연동)
|
||||||
|
public bool RecoveryMode { get => ExtRunMode == 1; } // 안심회복
|
||||||
|
public string SubModeText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var s = "";
|
||||||
|
if (SmartSleep) s += "스마트수면 ";
|
||||||
|
if (CookingMode) s += "쾌적조리 ";
|
||||||
|
if (RecoveryMode) s += "안심회복 ";
|
||||||
|
return s.Length == 0 ? "없음" : s.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- (꺼짐)예약 0~8시간 ----
|
||||||
|
int _reserveHours; // 0 = 해제
|
||||||
|
int _reserveRemainSec;
|
||||||
|
public int ReserveHours { get => _reserveHours; set { if (_reserveHours != value) { _reserveHours = value; OnChanged(); } } }
|
||||||
|
public int ReserveRemainSec { get => _reserveRemainSec; set { if (_reserveRemainSec != value) { _reserveRemainSec = value; OnChanged(); OnChanged(nameof(ReserveText)); } } }
|
||||||
|
public string ReserveText => ReserveRemainSec > 0
|
||||||
|
? $"예약 꺼짐까지 {ReserveRemainSec / 3600}:{(ReserveRemainSec % 3600) / 60:00}:{ReserveRemainSec % 60:00}"
|
||||||
|
: "예약 없음";
|
||||||
|
|
||||||
|
// 히스테리시스 데드밴드(하강) [preset] : CO2,PM2.5,PM10,VOC (사양서 10p)
|
||||||
|
public ushort[] Co2Db { get; } = { 50, 50, 30 };
|
||||||
|
public ushort[] Pm25Db { get; } = { 2, 2, 2 };
|
||||||
|
public ushort[] Pm10Db { get; } = { 5, 5, 5 };
|
||||||
|
public ushort[] VocDb { get; } = { 5, 5, 3 };
|
||||||
|
// 모드별(ECO/NORMAL/TURBO) 오염단계 상한 임계 [preset][레벨1~4]
|
||||||
|
public ushort[][] Co2Thr { get; } = { new ushort[]{1000,1300,1600,2000}, new ushort[]{800,1100,1400,1700}, new ushort[]{700,1000,1300,1600} };
|
||||||
|
public ushort[][] Pm25Thr { get; } = { new ushort[]{20,38,60,86}, new ushort[]{14,29,49,69}, new ushort[]{12,23,38,52} };
|
||||||
|
public ushort[][] Pm10Thr { get; } = { new ushort[]{40,86,126,173}, new ushort[]{28,66,102,138}, new ushort[]{24,53,78,104} };
|
||||||
|
public ushort[][] VocThr { get; } = { new ushort[]{171,195,308,438}, new ushort[]{120,150,250,350}, new ushort[]{103,120,192,263} };
|
||||||
|
|
||||||
|
// ---- 본체 6 댐퍼 ----
|
||||||
|
public ObservableCollection<BodyDamper> BodyDampers { get; }
|
||||||
|
|
||||||
|
// ---- 각실 디퓨저 4 룸 ----
|
||||||
|
public ObservableCollection<DiffuserRoom> Rooms { get; }
|
||||||
|
|
||||||
|
// ---- 팬 (BLDC SA/EA) ----
|
||||||
|
// 펌웨어 PWM duty 0~10000 매핑. UI는 0~10000 슬라이드로 표시 + 환산 RPM 추정.
|
||||||
|
int _fan1Target, _fan1Current; // SA
|
||||||
|
int _fan2Target, _fan2Current; // EA
|
||||||
|
public int Fan1Target { get => _fan1Target; set { if (_fan1Target != value) { _fan1Target = value; OnChanged(); } } }
|
||||||
|
public int Fan1Current { get => _fan1Current; set { if (_fan1Current != value) { _fan1Current = value; OnChanged(); } } }
|
||||||
|
public int Fan2Target { get => _fan2Target; set { if (_fan2Target != value) { _fan2Target = value; OnChanged(); } } }
|
||||||
|
public int Fan2Current { get => _fan2Current; set { if (_fan2Current != value) { _fan2Current = value; OnChanged(); } } }
|
||||||
|
|
||||||
|
// ---- 에러 코드 (PPT 매핑 + HERV 펌웨어 My_define.h:206 비트맵) ----
|
||||||
|
public const byte ERR_FILTER_CLEAN = 0x01;
|
||||||
|
public const byte ERR_FILTER_CHANGE = 0x02;
|
||||||
|
public const byte ERR_SOJA_CHANGE = 0x04;
|
||||||
|
public const byte ERR_TEMP_SENSOR = 0x08; // E02 온도센서 에러
|
||||||
|
public const byte ERR_PROTECT = 0x10; // COLD 장비보호모드
|
||||||
|
public const byte ERR_EA_FAN = 0x20; // E10 배기(EA)팬 에러
|
||||||
|
public const byte ERR_SOMETIME = 0x40; // E07 내부통신 에러
|
||||||
|
public const byte ERR_SA_FAN = 0x80; // E09 급기(SA)팬 에러
|
||||||
|
|
||||||
|
byte _errorCode;
|
||||||
|
public byte ErrorCode
|
||||||
|
{
|
||||||
|
get => _errorCode;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_errorCode != value)
|
||||||
|
{
|
||||||
|
_errorCode = value;
|
||||||
|
OnChanged();
|
||||||
|
OnChanged(nameof(E02_TempSensor));
|
||||||
|
OnChanged(nameof(E09_SaFan));
|
||||||
|
OnChanged(nameof(E10_EaFan));
|
||||||
|
OnChanged(nameof(COLD_Protect));
|
||||||
|
OnChanged(nameof(E07_InternalComm));
|
||||||
|
OnChanged(nameof(FilterClean));
|
||||||
|
OnChanged(nameof(FilterChange));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 알람(유지보수) — 필터 청소/교환. 룸콘·대시보드로 ErrorCode 비트로 전달.
|
||||||
|
public bool FilterClean { get => (ErrorCode & ERR_FILTER_CLEAN) != 0; set => SetErr(ERR_FILTER_CLEAN, value); }
|
||||||
|
public bool FilterChange { get => (ErrorCode & ERR_FILTER_CHANGE) != 0; set => SetErr(ERR_FILTER_CHANGE, value); }
|
||||||
|
|
||||||
|
public bool E02_TempSensor { get => (ErrorCode & ERR_TEMP_SENSOR) != 0; set => SetErr(ERR_TEMP_SENSOR, value); }
|
||||||
|
public bool E09_SaFan { get => (ErrorCode & ERR_SA_FAN) != 0; set => SetErr(ERR_SA_FAN, value); }
|
||||||
|
public bool E10_EaFan { get => (ErrorCode & ERR_EA_FAN) != 0; set => SetErr(ERR_EA_FAN, value); }
|
||||||
|
public bool COLD_Protect { get => (ErrorCode & ERR_PROTECT) != 0; set => SetErr(ERR_PROTECT, value); }
|
||||||
|
public bool E07_InternalComm { get => (ErrorCode & ERR_SOMETIME) != 0; set => SetErr(ERR_SOMETIME, value); }
|
||||||
|
|
||||||
|
void SetErr(byte bit, bool on)
|
||||||
|
{
|
||||||
|
byte newVal = on ? (byte)(_errorCode | bit) : (byte)(_errorCode & ~bit);
|
||||||
|
ErrorCode = newVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1~4단 VSP preset (1바이트 0~255). 기본값 = 사양서 DL H-ERV VSP 실측표 (index 1~4)
|
||||||
|
public ushort[] FanSAPreset_Vent { get; } = { 0, 56, 63, 70, 86 }; // 환기 SA
|
||||||
|
public ushort[] FanEAPreset_Vent { get; } = { 0, 57, 63, 70, 85 }; // 환기 EA
|
||||||
|
public ushort[] FanSAPreset_Bypass { get; } = { 0, 67, 0, 0, 0 }; // 바이패스 SA (기본단)
|
||||||
|
public ushort[] FanEAPreset_Bypass { get; } = { 0, 75, 0, 0, 0 }; // 바이패스 EA
|
||||||
|
public ushort[] FanSAPreset_Air { get; } = { 0, 65, 72, 78, 80 }; // 공청 SA
|
||||||
|
public ushort[] FanEAPreset_Air { get; } = { 0, 0, 0, 0, 0 }; // 공청 EA (미사용 '-')
|
||||||
|
|
||||||
|
public ErvState()
|
||||||
|
{
|
||||||
|
BodyDampers = new ObservableCollection<BodyDamper>
|
||||||
|
{
|
||||||
|
// PPT 순서/색상 매핑
|
||||||
|
new(DamperId.OA, "외기(OA)", "CN2", "GREEN"),
|
||||||
|
new(DamperId.AIR, "공청(AIR)", "CN10", "YELLOW"),
|
||||||
|
new(DamperId.BYPASS, "바이패스", "CN5", "RED"),
|
||||||
|
new(DamperId.EA, "배기(EA)", "CN3", "BLACK"),
|
||||||
|
new(DamperId.SA, "급기(SA)", "CN7", "BLUE"),
|
||||||
|
new(DamperId.RA, "환기(RA) 3단", "CN9", "WHITE"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Rooms = new ObservableCollection<DiffuserRoom>
|
||||||
|
{
|
||||||
|
new(1, "거실"),
|
||||||
|
new(2, "침실1"),
|
||||||
|
new(3, "침실2"),
|
||||||
|
new(4, "침실3"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public BodyDamper GetDamper(DamperId id)
|
||||||
|
{
|
||||||
|
foreach (var d in BodyDampers) if (d.Id == id) return d;
|
||||||
|
throw new System.InvalidOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DiffuserRoom GetRoom(int roomId)
|
||||||
|
{
|
||||||
|
foreach (var r in Rooms) if (r.RoomId == roomId) return r;
|
||||||
|
throw new System.InvalidOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
void OnChanged([CallerMemberName] string? n = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
namespace ERVSimulator.Model
|
||||||
|
{
|
||||||
|
// HERV 펌웨어 SPEC_MODE_INFO=0x16 (대림사양, 히터X, 바이패스O, 공청X) 기준
|
||||||
|
// My_define.h:271 #if !((SPEC_MODE_INFO&0x0F)==0x03||==0x06) 분기
|
||||||
|
public enum RunMode : byte
|
||||||
|
{
|
||||||
|
Ventilation = 0,
|
||||||
|
Auto = 1,
|
||||||
|
Bypass = 2,
|
||||||
|
AirClean = 3,
|
||||||
|
FanTest = 4,
|
||||||
|
Off = 10, // MODE_OFF (MyMotor.c)
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DamperId
|
||||||
|
{
|
||||||
|
EA = 1, // 배기
|
||||||
|
OA = 2, // 외기
|
||||||
|
BYPASS = 3,
|
||||||
|
SA = 4, // 급기
|
||||||
|
RA = 5, // 환기
|
||||||
|
AIR = 6, // 공청
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace ERVSimulator.Protocol
|
||||||
|
{
|
||||||
|
public static class ChecksumHelper
|
||||||
|
{
|
||||||
|
public static byte Xor(byte[] data, int start, int length)
|
||||||
|
{
|
||||||
|
byte x = 0;
|
||||||
|
for (int i = 0; i < length; i++) x ^= data[start + i];
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte Add(byte[] data, int start, int length)
|
||||||
|
{
|
||||||
|
int s = 0;
|
||||||
|
for (int i = 0; i < length; i++) s += data[start + i];
|
||||||
|
return (byte)(s & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using ERVSimulator.Model;
|
||||||
|
using ErvProtocol; // 공용 Crc16 (bunbaegi CRC 도 표준 MODBUS 동일)
|
||||||
|
using RunMode = ERVSimulator.Model.RunMode; // ErvProtocol.RunMode 와 이름 충돌 해소
|
||||||
|
|
||||||
|
namespace ERVSimulator.Protocol
|
||||||
|
{
|
||||||
|
// 디퓨저 버스 마스터 (115200) <-> DiffuserSimulator(슬레이브)
|
||||||
|
// 규격 : Protocol/수정_Each_Room_Jushin_protocol_RS485_Rev1.2 (펌웨어 My_Uart.c bunbaegi 미러)
|
||||||
|
// 목적 : DiffuserSimulator 로부터 각실 센서값(PM2.5/PM10/VOC/CO2) 수신 → ErvState → 자동로직
|
||||||
|
// - 마스터 폴(29B, 0x10): 실/타입(SA/RA)별 전원·모드·풍량·LED·댐퍼 송신 (poll-response 구조상 필수)
|
||||||
|
// - 슬레이브 응답(39B, 0x01): 센서값 수신
|
||||||
|
// ※ ERVSim 은 각실 댐퍼+LED 를 자체 표시하지 않음(DiffuserSimulator 가 표시). 통신만 수행.
|
||||||
|
public class DiffuserMasterProtocol
|
||||||
|
{
|
||||||
|
readonly SerialChannel _ch;
|
||||||
|
readonly ErvState _state;
|
||||||
|
readonly Dispatcher _dispatcher;
|
||||||
|
readonly DispatcherTimer _pollTimer;
|
||||||
|
|
||||||
|
int _pollIdx; // (room1 SA),(room1 RA)...(room4 RA) round-robin
|
||||||
|
|
||||||
|
readonly byte[] _rx = new byte[39];
|
||||||
|
int _rxPos;
|
||||||
|
DateTime _lastByte = DateTime.MinValue;
|
||||||
|
static readonly TimeSpan FrameGap = TimeSpan.FromMilliseconds(40);
|
||||||
|
|
||||||
|
public event Action<string>? PacketReceived;
|
||||||
|
public event Action<string>? PacketSent;
|
||||||
|
public bool Verbose { get; set; } = false; // true면 모든 폴 로그
|
||||||
|
|
||||||
|
public DiffuserMasterProtocol(SerialChannel ch, ErvState state, Dispatcher dispatcher)
|
||||||
|
{
|
||||||
|
_ch = ch; _state = state; _dispatcher = dispatcher;
|
||||||
|
_ch.ByteReceived += OnByte;
|
||||||
|
_pollTimer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(80) };
|
||||||
|
_pollTimer.Tick += (_, _) => { if (_ch.IsConnected) PollNext(); };
|
||||||
|
_pollTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte DiffRunMode() => _state.RunMode switch
|
||||||
|
{
|
||||||
|
RunMode.Ventilation => 0x01,
|
||||||
|
RunMode.Auto => 0x02,
|
||||||
|
RunMode.Bypass => 0x04,
|
||||||
|
RunMode.AirClean => 0x08,
|
||||||
|
_ => 0x01,
|
||||||
|
};
|
||||||
|
|
||||||
|
void PollNext()
|
||||||
|
{
|
||||||
|
int room = _pollIdx / 2 + 1; // 1~4
|
||||||
|
byte id1 = (byte)(_pollIdx % 2 == 0 ? 0x01 : 0x02); // SA / RA
|
||||||
|
_pollIdx = (_pollIdx + 1) % 8;
|
||||||
|
|
||||||
|
var rm = _state.GetRoom(room);
|
||||||
|
var p = new byte[29];
|
||||||
|
p[0] = 0xAA; p[1] = 0x10; p[2] = id1; p[3] = (byte)room; p[4] = 0x00;
|
||||||
|
p[5] = (byte)(_state.PowerOn ? 1 : 0);
|
||||||
|
p[6] = DiffRunMode();
|
||||||
|
p[7] = _state.FanMode;
|
||||||
|
p[8] = (byte)rm.LightBright;
|
||||||
|
p[9] = (byte)rm.AirQuality;
|
||||||
|
p[10] = (byte)rm.CurrentSA;
|
||||||
|
p[11] = (byte)rm.CurrentRA;
|
||||||
|
ushort crc = Crc16.Modbus(p, 0, 27);
|
||||||
|
p[27] = (byte)(crc & 0xFF);
|
||||||
|
p[28] = (byte)(crc >> 8);
|
||||||
|
_ch.Send(p, 29);
|
||||||
|
if (Verbose) PacketSent?.Invoke($"Diff TX poll room{room} {(id1 == 1 ? "SA" : "RA")} SA={rm.CurrentSA} RA={rm.CurrentRA} LED={rm.LightBright}");
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnByte(byte b)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (now - _lastByte > FrameGap) _rxPos = 0;
|
||||||
|
_lastByte = now;
|
||||||
|
|
||||||
|
if (_rxPos == 0)
|
||||||
|
{
|
||||||
|
if (b == 0xAA) { _rx[0] = b; _rxPos = 1; }
|
||||||
|
}
|
||||||
|
else if (_rxPos == 1)
|
||||||
|
{
|
||||||
|
if (b == 0x01) { _rx[1] = b; _rxPos = 2; }
|
||||||
|
else _rxPos = (b == 0xAA) ? 1 : 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_rx[_rxPos++] = b;
|
||||||
|
if (_rxPos >= 39)
|
||||||
|
{
|
||||||
|
var copy = (byte[])_rx.Clone();
|
||||||
|
_dispatcher.BeginInvoke(new Action(() => HandleResponse(copy)));
|
||||||
|
_rxPos = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleResponse(byte[] p)
|
||||||
|
{
|
||||||
|
ushort rxcrc = (ushort)(p[37] | (p[38] << 8));
|
||||||
|
if (Crc16.Modbus(p, 0, 37) != rxcrc)
|
||||||
|
{
|
||||||
|
PacketReceived?.Invoke($"Diff RX CRC오류 {HexFormat.Bytes(p, 39)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int id1 = p[2]; // 0x01 SA / 0x02 RA
|
||||||
|
int room = p[3]; // 1~4
|
||||||
|
if (room < 1 || room > 4) return;
|
||||||
|
|
||||||
|
// 센서 (응답 39B, 빅엔디안) : LED[8] PM10[12,13] PM2.5[16,17] 습도[20,21] 온도[22,23] VOC[24,25] CO2[28,29]
|
||||||
|
int led = p[8]; // 디퓨저가 echo 한 실제 LED 단수 (수동 제어 시 ERV 명령과 다를 수 있음)
|
||||||
|
int pm10 = (p[12] << 8) | p[13];
|
||||||
|
int pm25 = (p[16] << 8) | p[17];
|
||||||
|
int humi = (p[20] << 8) | p[21];
|
||||||
|
int temp = (p[22] << 8) | p[23];
|
||||||
|
int voc = (p[24] << 8) | p[25];
|
||||||
|
int co2 = (p[28] << 8) | p[29];
|
||||||
|
|
||||||
|
var rm = _state.GetRoom(room);
|
||||||
|
bool changed = rm.Co2 != co2 || rm.Pm25 != pm25 || rm.Pm10 != pm10 || rm.Voc != voc || rm.Temp != temp || rm.Humi != humi || rm.LedReported != led;
|
||||||
|
rm.Pm10 = pm10; rm.Pm25 = pm25; rm.Voc = voc; rm.Co2 = co2; rm.Temp = temp; rm.Humi = humi; rm.LedReported = led;
|
||||||
|
|
||||||
|
if (changed || Verbose)
|
||||||
|
PacketReceived?.Invoke($"Diff RX {rm.Name} 센서 CO2={co2} PM2.5={pm25} PM10={pm10} VOC={voc} 온도={temp} 습도={humi} LED={led} (from {(id1 == 1 ? "SA" : "RA")})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using ERVSimulator.Model;
|
||||||
|
using ErvProtocol; // 공용 프로토콜 (단일 진실원본) : FrameParser/CtrlFrame/StatusEncoder/StatusRecord
|
||||||
|
using RunMode = ERVSimulator.Model.RunMode; // ErvProtocol.RunMode 와 이름 충돌 해소
|
||||||
|
|
||||||
|
namespace ERVSimulator.Protocol
|
||||||
|
{
|
||||||
|
// HOMENET (UART1, 115200 N81) <-> ErvDashboard
|
||||||
|
// 규격/코덱 모두 공용 라이브러리 ErvProtocol 사용 (PC_ERV_Protocol.md).
|
||||||
|
// 본 클래스는 ErvState <-> ErvProtocol.StatusRecord 매핑 + 제어명령 적용만 담당.
|
||||||
|
public class HomeNetProtocol
|
||||||
|
{
|
||||||
|
readonly SerialChannel _ch;
|
||||||
|
readonly ErvState _state;
|
||||||
|
readonly DamperSequencer _seq;
|
||||||
|
readonly Dispatcher _dispatcher;
|
||||||
|
readonly DispatcherTimer _statusTimer;
|
||||||
|
readonly FrameParser _parser = new();
|
||||||
|
|
||||||
|
public event Action<string>? PacketReceived;
|
||||||
|
public event Action<string>? PacketSent;
|
||||||
|
|
||||||
|
public HomeNetProtocol(SerialChannel ch, ErvState state, DamperSequencer seq, Dispatcher dispatcher)
|
||||||
|
{
|
||||||
|
_ch = ch; _state = state; _seq = seq; _dispatcher = dispatcher;
|
||||||
|
|
||||||
|
_parser.OnFrame += (cmd, pl) => _dispatcher.BeginInvoke(new Action(() => HandleFrame(cmd, pl)));
|
||||||
|
_parser.OnError += msg => PacketReceived?.Invoke($"HomeNet {msg}");
|
||||||
|
_ch.ByteReceived += b => _parser.FeedByte(b);
|
||||||
|
|
||||||
|
_statusTimer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromSeconds(1) };
|
||||||
|
_statusTimer.Tick += (_, _) => { if (_ch.IsConnected) SendStatus(); };
|
||||||
|
_statusTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- ErvState → StatusRecord ----
|
||||||
|
byte RunModeCode()
|
||||||
|
{
|
||||||
|
if (!_state.PowerOn) return 0;
|
||||||
|
return _state.RunMode switch
|
||||||
|
{
|
||||||
|
RunMode.Ventilation => 1,
|
||||||
|
RunMode.Auto => 2,
|
||||||
|
RunMode.AirClean => 3,
|
||||||
|
RunMode.Bypass => 4,
|
||||||
|
_ => 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusRecord BuildRecord()
|
||||||
|
{
|
||||||
|
int hp = _state.HystPreset;
|
||||||
|
var s = new StatusRecord
|
||||||
|
{
|
||||||
|
Power = (byte)(_state.PowerOn ? 1 : 0),
|
||||||
|
RunMode = RunModeCode(),
|
||||||
|
AutoState = (byte)(_state.AutoConcentrate ? 1 : 0),
|
||||||
|
FanMode = _state.FanMode,
|
||||||
|
SubMode = (byte)((_state.SmartSleep ? 0x01 : 0) | (_state.CookingMode ? 0x02 : 0) | (_state.RecoveryMode ? 0x04 : 0)),
|
||||||
|
Hood = (byte)((_state.HoodEnable ? 0x01 : 0) | (_state.HoodStatus ? 0x02 : 0) | (_state.HoodConnected ? 0x04 : 0)),
|
||||||
|
HystPreset = (byte)hp,
|
||||||
|
HystPm25 = _state.Pm25Db[hp],
|
||||||
|
HystPm10 = _state.Pm10Db[hp],
|
||||||
|
HystVoc = _state.VocDb[hp],
|
||||||
|
HystCo2 = _state.Co2Db[hp],
|
||||||
|
ErrorCode = _state.ErrorCode,
|
||||||
|
Reset = _state.ResetState,
|
||||||
|
ReserveRemainSec = _state.ReserveRemainSec,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int r = 0; r < 4; r++)
|
||||||
|
{
|
||||||
|
var room = _state.GetRoom(r + 1);
|
||||||
|
var rr = s.Rooms[r];
|
||||||
|
// 비트맵 : bit0=급기(SA) 열림 / bit1=배기(RA) 열림 (StatusRecord.RoomRecord 와 일치)
|
||||||
|
rr.Damper = (byte)((room.MemorySA != 0 ? 0x01 : 0) | (room.MemoryRA != 0 ? 0x02 : 0));
|
||||||
|
rr.Pm25 = room.Pm25;
|
||||||
|
rr.Pm10 = room.Pm10;
|
||||||
|
rr.Voc = room.Voc;
|
||||||
|
rr.Co2 = room.Co2;
|
||||||
|
rr.AirQuality = (byte)room.AirQuality;
|
||||||
|
// 디퓨저가 응답마다 echo 하는 실제 LED 단수를 보고 → 디퓨저 수동 LED 제어가 대시보드에 반영됨
|
||||||
|
rr.LedDim = (byte)room.LedReported;
|
||||||
|
rr.LoadScore = room.Level;
|
||||||
|
rr.FinalVolume = _state.FanMode;
|
||||||
|
rr.Temp = (byte)Math.Clamp(room.Temp, 0, 255);
|
||||||
|
rr.Humi = (byte)Math.Clamp(room.Humi, 0, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
// VSP : 환기1~4, 바이패스, 공청1~4
|
||||||
|
for (int i = 1; i <= 4; i++) { s.Vsp[i - 1].Sa = _state.FanSAPreset_Vent[i]; s.Vsp[i - 1].Ea = _state.FanEAPreset_Vent[i]; }
|
||||||
|
s.Vsp[4].Sa = _state.FanSAPreset_Bypass[1]; s.Vsp[4].Ea = _state.FanEAPreset_Bypass[1];
|
||||||
|
for (int i = 1; i <= 4; i++) { s.Vsp[4 + i].Sa = _state.FanSAPreset_Air[i]; s.Vsp[4 + i].Ea = _state.FanEAPreset_Air[i]; }
|
||||||
|
|
||||||
|
// 히스테리시스 데드밴드 테이블
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
s.HystTable[i].Pm25 = _state.Pm25Db[i];
|
||||||
|
s.HystTable[i].Pm10 = _state.Pm10Db[i];
|
||||||
|
s.HystTable[i].Voc = _state.VocDb[i];
|
||||||
|
s.HystTable[i].Co2 = _state.Co2Db[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모드별 오염단계 임계표
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
for (int k = 0; k < 4; k++)
|
||||||
|
{
|
||||||
|
s.ThrTable[i].Co2[k] = _state.Co2Thr[i][k];
|
||||||
|
s.ThrTable[i].Pm25[k] = _state.Pm25Thr[i][k];
|
||||||
|
s.ThrTable[i].Pm10[k] = _state.Pm10Thr[i][k];
|
||||||
|
s.ThrTable[i].Voc[k] = _state.VocThr[i][k];
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendStatus()
|
||||||
|
{
|
||||||
|
var frame = StatusEncoder.BuildStatusFrame(BuildRecord());
|
||||||
|
_ch.Send(frame, frame.Length);
|
||||||
|
string autoTag = _state.RunMode == RunMode.Auto ? (_state.AutoConcentrate ? " 집중" : " 분산") : ""; // 집중/분산은 자동모드에서만
|
||||||
|
PacketSent?.Invoke($"HomeNet TX STATUS(0x81) [{(_state.PowerOn ? "ON" : "OFF")} {_state.RunMode} {_state.FanMode}단{autoTag}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 수신 제어명령 적용 + ACK ----
|
||||||
|
void HandleFrame(byte cmd, byte[] pl)
|
||||||
|
{
|
||||||
|
byte result = 0;
|
||||||
|
bool modeChanged = false;
|
||||||
|
|
||||||
|
switch (cmd)
|
||||||
|
{
|
||||||
|
case CtrlFrame.CTRL_POWER:
|
||||||
|
if (pl.Length >= 1)
|
||||||
|
{
|
||||||
|
bool on = pl[0] != 0;
|
||||||
|
_state.PowerOn = on;
|
||||||
|
if (on)
|
||||||
|
{
|
||||||
|
// 전원 ON : 환기 모드 + 풍량 1단. 디퓨저 개방·LED 는 AutoLogic 이 댐퍼 상태에 맞춰 구동.
|
||||||
|
_state.RunMode = _state.SetRunMode = RunMode.Ventilation;
|
||||||
|
_state.FanMode = _state.SetFanMode = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 전원 OFF : 정지 (댐퍼 닫힘·LED 소등은 AutoLogic 이 처리)
|
||||||
|
_state.FanMode = _state.SetFanMode = 0;
|
||||||
|
}
|
||||||
|
// 전원 토글 시 수동 LED·댐퍼 해제 → 자동 추종 복귀
|
||||||
|
for (int r = 1; r <= 4; r++) { var rm = _state.GetRoom(r); rm.LedManual = false; rm.DamperManual = false; }
|
||||||
|
modeChanged = true;
|
||||||
|
}
|
||||||
|
else result = 1;
|
||||||
|
break;
|
||||||
|
case CtrlFrame.CTRL_RUNMODE:
|
||||||
|
if (pl.Length >= 1)
|
||||||
|
{
|
||||||
|
if (pl[0] == 0) _state.PowerOn = false;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_state.PowerOn = true;
|
||||||
|
RunMode m = pl[0] switch { 1 => RunMode.Ventilation, 2 => RunMode.Auto, 3 => RunMode.AirClean, 4 => RunMode.Bypass, _ => RunMode.Ventilation };
|
||||||
|
_state.RunMode = _state.SetRunMode = m;
|
||||||
|
// 운전모드 전환 시 풍량 1단 (자동은 부하점수로 결정하므로 제외)
|
||||||
|
if (m != RunMode.Auto) _state.FanMode = _state.SetFanMode = 1;
|
||||||
|
}
|
||||||
|
// 모드 전환 시 수동 댐퍼만 해제 → 새 모드는 기본(전실 개방)에서 시작.
|
||||||
|
// 수동 LED 디밍값은 모드가 바뀌어도 유지(사용자 요청, 전원 OFF 시에만 해제).
|
||||||
|
for (int r = 1; r <= 4; r++) _state.GetRoom(r).DamperManual = false;
|
||||||
|
modeChanged = true;
|
||||||
|
}
|
||||||
|
else result = 1;
|
||||||
|
break;
|
||||||
|
case CtrlFrame.CTRL_FAN:
|
||||||
|
if (pl.Length >= 1)
|
||||||
|
{
|
||||||
|
// 모드별 풍량 상한 : 바이패스 1단, 그 외 4단 (자동은 부하점수로 결정)
|
||||||
|
byte sp = pl[0];
|
||||||
|
byte max = _state.RunMode == RunMode.Bypass ? (byte)1 : (byte)4;
|
||||||
|
if (sp > max) sp = max;
|
||||||
|
_state.FanMode = _state.SetFanMode = sp; modeChanged = true;
|
||||||
|
}
|
||||||
|
else result = 1;
|
||||||
|
break;
|
||||||
|
case CtrlFrame.CTRL_SUBMODE: // [type][on] 1수면 2조리 3회복
|
||||||
|
if (pl.Length >= 2)
|
||||||
|
{
|
||||||
|
if (pl[0] == 1) _state.ExtRunMode = (byte)(pl[1] != 0 ? 4 : 0);
|
||||||
|
else if (pl[0] == 2) _state.HoodEnable = pl[1] != 0;
|
||||||
|
else if (pl[0] == 3) _state.ExtRunMode = (byte)(pl[1] != 0 ? 1 : 0);
|
||||||
|
else result = 1;
|
||||||
|
}
|
||||||
|
else result = 1;
|
||||||
|
break;
|
||||||
|
case CtrlFrame.CTRL_HOOD:
|
||||||
|
if (pl.Length >= 1) _state.HoodEnable = pl[0] != 0; else result = 1;
|
||||||
|
break;
|
||||||
|
case CtrlFrame.CTRL_HYST_PRESET:
|
||||||
|
if (pl.Length >= 1 && pl[0] < 3) _state.HystPreset = pl[0]; else result = 1;
|
||||||
|
break;
|
||||||
|
case CtrlFrame.CTRL_HYST_VALUE: // [preset][pm25][pm10][voc][co2] u16 BE
|
||||||
|
if (pl.Length >= 9 && pl[0] < 3)
|
||||||
|
{
|
||||||
|
int ps = pl[0];
|
||||||
|
_state.Pm25Db[ps] = (ushort)((pl[1] << 8) | pl[2]);
|
||||||
|
_state.Pm10Db[ps] = (ushort)((pl[3] << 8) | pl[4]);
|
||||||
|
_state.VocDb[ps] = (ushort)((pl[5] << 8) | pl[6]);
|
||||||
|
_state.Co2Db[ps] = (ushort)((pl[7] << 8) | pl[8]);
|
||||||
|
}
|
||||||
|
else result = 1;
|
||||||
|
break;
|
||||||
|
case CtrlFrame.CTRL_DAMPER: // [room][onoff] — 수동 댐퍼 : 비자동(환기/공청/바이패스)에서 위치 유지
|
||||||
|
if (pl.Length >= 2 && pl[0] >= 1 && pl[0] <= 4)
|
||||||
|
{
|
||||||
|
var rm = _state.GetRoom(pl[0]);
|
||||||
|
int ang = pl[1] != 0 ? 110 : 0;
|
||||||
|
rm.MemorySA = rm.CurrentSA = ang;
|
||||||
|
rm.MemoryRA = rm.CurrentRA = ang;
|
||||||
|
rm.DamperManual = true; // 자동로직이 덮어쓰지 않도록 (자동/모드전환 시 해제)
|
||||||
|
}
|
||||||
|
else result = 1;
|
||||||
|
break;
|
||||||
|
case CtrlFrame.CTRL_LED: // [room][dim] — 수동 조작 : 자동 추종 해제하고 지정값 유지
|
||||||
|
if (pl.Length >= 2 && pl[0] >= 1 && pl[0] <= 4)
|
||||||
|
{
|
||||||
|
var rm = _state.GetRoom(pl[0]);
|
||||||
|
rm.LightBright = pl[1];
|
||||||
|
rm.LedManual = true;
|
||||||
|
}
|
||||||
|
else result = 1;
|
||||||
|
break;
|
||||||
|
case CtrlFrame.CTRL_RESERVE: // [hours 0~8] : N시간 후 전원 OFF (0=해제)
|
||||||
|
if (pl.Length >= 1 && pl[0] <= 8)
|
||||||
|
{
|
||||||
|
int h = pl[0];
|
||||||
|
_state.ReserveHours = h;
|
||||||
|
_state.ReserveRemainSec = h * 3600; // 0이면 해제. 카운트다운/전원OFF는 ReserveTick(1s)이 처리
|
||||||
|
}
|
||||||
|
else result = 1;
|
||||||
|
break;
|
||||||
|
case CtrlFrame.REQ_STATUS:
|
||||||
|
break;
|
||||||
|
case CtrlFrame.CTRL_RESET:
|
||||||
|
if (pl.Length >= 1) _state.ResetState = (byte)(pl[0] != 0 ? 1 : 0); else result = 1;
|
||||||
|
break;
|
||||||
|
case CtrlFrame.CTRL_VSP: // [group][index][sa(2)][ea(2)]
|
||||||
|
if (pl.Length >= 6) result = SetVsp(pl[0], pl[1], (pl[2] << 8) | pl[3], (pl[4] << 8) | pl[5]); else result = 1;
|
||||||
|
break;
|
||||||
|
case CtrlFrame.CTRL_HYST_THR: // [preset][pollutant][L1~L4 u16] : 오염단계 임계 설정
|
||||||
|
if (pl.Length >= 10 && pl[0] < 3 && pl[1] < 4)
|
||||||
|
{
|
||||||
|
int ps = pl[0], g = pl[1];
|
||||||
|
ushort[] arr = g switch { 0 => _state.Co2Thr[ps], 1 => _state.Pm25Thr[ps], 2 => _state.Pm10Thr[ps], 3 => _state.VocThr[ps], _ => null! };
|
||||||
|
if (arr != null) for (int k = 0; k < 4; k++) arr[k] = (ushort)((pl[2 + k * 2] << 8) | pl[3 + k * 2]);
|
||||||
|
else result = 1;
|
||||||
|
}
|
||||||
|
else result = 1;
|
||||||
|
break;
|
||||||
|
default: result = 1; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
PacketReceived?.Invoke($"HomeNet RX CMD=0x{cmd:X2} len={pl.Length} → {(result == 0 ? "OK" : "ERR")}");
|
||||||
|
|
||||||
|
if (modeChanged) _seq.NotifyCommandChanged();
|
||||||
|
|
||||||
|
var ack = StatusEncoder.BuildAckFrame(cmd, result);
|
||||||
|
_ch.Send(ack, ack.Length);
|
||||||
|
|
||||||
|
if (cmd == CtrlFrame.REQ_STATUS) SendStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte SetVsp(int grp, int idx, int sa, int ea)
|
||||||
|
{
|
||||||
|
if (grp == 0 && idx >= 1 && idx <= 4) { _state.FanSAPreset_Vent[idx] = (ushort)sa; _state.FanEAPreset_Vent[idx] = (ushort)ea; }
|
||||||
|
else if (grp == 1 && idx == 1) { _state.FanSAPreset_Bypass[1] = (ushort)sa; _state.FanEAPreset_Bypass[1] = (ushort)ea; }
|
||||||
|
else if (grp == 2 && idx >= 1 && idx <= 4) { _state.FanSAPreset_Air[idx] = (ushort)sa; _state.FanEAPreset_Air[idx] = (ushort)ea; }
|
||||||
|
else return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using ERVSimulator.Model;
|
||||||
|
using RunMode = ERVSimulator.Model.RunMode;
|
||||||
|
|
||||||
|
namespace ERVSimulator.Protocol
|
||||||
|
{
|
||||||
|
// 후드 버스 마스터 (115200) <-> 후드메인(슬레이브)
|
||||||
|
// 규격 : Protocol/HOOD/주신전자_protocol_hood_전열교환기_Rev1.3_20241125.xlsx
|
||||||
|
// - 9바이트 고정, 폴링주기 100~500ms, 응답 50ms 이내, CS = Preamble~CS직전 전체 XOR
|
||||||
|
// 목적 : ERV(Master) 가 후드메인(Slave) 을 폴 → 후드 FAN/LIGHT/연동CMD 수신 → ErvState 반영
|
||||||
|
// 마스터 폴(9B) : Preamble | M/S(0x21) | ID | MODE | FAN | 연동EN | 연동운전중 | ERROR | CS
|
||||||
|
// 슬레이브 응답(9B) : Preamble | M/S(0x11) | ID | FAN STATUS | LIGHT STATUS | 0x00 | 연동CMD | ERROR | CS
|
||||||
|
public class HoodMasterProtocol
|
||||||
|
{
|
||||||
|
const byte PREAMBLE = 0xAA;
|
||||||
|
const byte MS_MASTER = 0x21;
|
||||||
|
const byte MS_SLAVE = 0x11;
|
||||||
|
const byte HOOD_ID = 0x01;
|
||||||
|
const int FRAME_LEN = 9;
|
||||||
|
|
||||||
|
readonly SerialChannel _ch;
|
||||||
|
readonly ErvState _state;
|
||||||
|
readonly Dispatcher _dispatcher;
|
||||||
|
readonly DispatcherTimer _pollTimer;
|
||||||
|
|
||||||
|
readonly byte[] _rx = new byte[FRAME_LEN];
|
||||||
|
int _rxPos;
|
||||||
|
DateTime _lastByte = DateTime.MinValue;
|
||||||
|
static readonly TimeSpan FrameGap = TimeSpan.FromMilliseconds(40);
|
||||||
|
|
||||||
|
public event Action<string>? PacketReceived;
|
||||||
|
public event Action<string>? PacketSent;
|
||||||
|
public bool Verbose { get; set; } = false; // true면 모든 폴 로그
|
||||||
|
|
||||||
|
// 후드 통신 생존 표시용 — 마지막으로 유효한 응답을 받은 시각(UTC)
|
||||||
|
public DateTime LastRxUtc { get; private set; } = DateTime.MinValue;
|
||||||
|
// 폴(200ms) 기준 이 시간 내 응답이 없으면 미연결로 판정 (몇 회 누락 허용)
|
||||||
|
static readonly TimeSpan ConnTimeout = TimeSpan.FromMilliseconds(1000);
|
||||||
|
|
||||||
|
public HoodMasterProtocol(SerialChannel ch, ErvState state, Dispatcher dispatcher)
|
||||||
|
{
|
||||||
|
_ch = ch; _state = state; _dispatcher = dispatcher;
|
||||||
|
_ch.ByteReceived += OnByte;
|
||||||
|
// 폴링주기 200ms (사양 100~500ms 범위 내)
|
||||||
|
_pollTimer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(200) };
|
||||||
|
_pollTimer.Tick += (_, _) =>
|
||||||
|
{
|
||||||
|
if (_ch.IsConnected) Poll();
|
||||||
|
// 폴 주기마다 통신 생존 갱신 : 채널 연결 && 최근 응답 수신 → 연결됨
|
||||||
|
_state.HoodConnected = _ch.IsConnected && (DateTime.UtcNow - LastRxUtc) < ConnTimeout;
|
||||||
|
};
|
||||||
|
_pollTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// MODE : 전원 OFF시 0, ON시 1 환기 / 2 자동 / 3 공청 / 4 바이패스 / 5 기타
|
||||||
|
byte HoodMode()
|
||||||
|
{
|
||||||
|
if (!_state.PowerOn) return 0;
|
||||||
|
return _state.RunMode switch
|
||||||
|
{
|
||||||
|
RunMode.Ventilation => 1,
|
||||||
|
RunMode.Auto => 2,
|
||||||
|
RunMode.AirClean => 3,
|
||||||
|
RunMode.Bypass => 4,
|
||||||
|
RunMode.Off => 0,
|
||||||
|
_ => 5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Poll()
|
||||||
|
{
|
||||||
|
var p = new byte[FRAME_LEN];
|
||||||
|
p[0] = PREAMBLE;
|
||||||
|
p[1] = MS_MASTER;
|
||||||
|
p[2] = HOOD_ID;
|
||||||
|
p[3] = HoodMode();
|
||||||
|
p[4] = _state.FanMode; // 전열교환기 FAN 0 OFF, 1~5단
|
||||||
|
p[5] = (byte)(_state.HoodEnable ? 0x01 : 0x00); // 연동 Enable/Disable
|
||||||
|
p[6] = (byte)(_state.HoodStatus ? 0x01 : 0x00); // 연동 운전중(후드 연동에 의한 환기장치 동작중)
|
||||||
|
p[7] = 0x00; // ERROR
|
||||||
|
p[8] = ChecksumHelper.Xor(p, 0, 8); // CS = Preamble~CS직전 XOR
|
||||||
|
_ch.Send(p, FRAME_LEN);
|
||||||
|
if (Verbose)
|
||||||
|
PacketSent?.Invoke($"Hood TX poll MODE={p[3]} FAN={p[4]} EN={p[5]} 연동운전={p[6]}");
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnByte(byte b)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (now - _lastByte > FrameGap) _rxPos = 0;
|
||||||
|
_lastByte = now;
|
||||||
|
|
||||||
|
if (_rxPos == 0)
|
||||||
|
{
|
||||||
|
if (b == PREAMBLE) { _rx[0] = b; _rxPos = 1; }
|
||||||
|
}
|
||||||
|
else if (_rxPos == 1)
|
||||||
|
{
|
||||||
|
if (b == MS_SLAVE) { _rx[1] = b; _rxPos = 2; }
|
||||||
|
else _rxPos = (b == PREAMBLE) ? 1 : 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_rx[_rxPos++] = b;
|
||||||
|
if (_rxPos >= FRAME_LEN)
|
||||||
|
{
|
||||||
|
var copy = (byte[])_rx.Clone();
|
||||||
|
_dispatcher.BeginInvoke(new Action(() => HandleResponse(copy)));
|
||||||
|
_rxPos = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleResponse(byte[] p)
|
||||||
|
{
|
||||||
|
byte cs = ChecksumHelper.Xor(p, 0, 8);
|
||||||
|
if (cs != p[8])
|
||||||
|
{
|
||||||
|
PacketReceived?.Invoke($"Hood RX CS오류 {HexFormat.Bytes(p, FRAME_LEN)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (p[2] != HOOD_ID) return;
|
||||||
|
LastRxUtc = DateTime.UtcNow; // 유효 응답 수신 → 통신 생존
|
||||||
|
_state.HoodConnected = true; // 응답 받았으므로 즉시 연결 표시
|
||||||
|
|
||||||
|
int fan = p[3]; // 후드 FAN STATUS : 0 OFF, 1~5단
|
||||||
|
bool light = p[4] != 0; // 후드 LIGHT STATUS : 0 OFF, 1 ON
|
||||||
|
bool cmd = p[6] != 0; // 연동 CMD : 0 후드 꺼짐 / 1 후드 켜짐
|
||||||
|
int err = p[7];
|
||||||
|
|
||||||
|
bool changed = _state.HoodFan != fan || _state.HoodLight != light || _state.HoodCmd != cmd || _state.HoodError != err;
|
||||||
|
_state.HoodFan = fan;
|
||||||
|
_state.HoodLight = light;
|
||||||
|
_state.HoodCmd = cmd;
|
||||||
|
_state.HoodError = err;
|
||||||
|
// 연동운전중(HoodStatus)은 AutoLogic 이 메이크업 에어 상태(롤백 유지 포함)로 소유.
|
||||||
|
|
||||||
|
if (changed || Verbose)
|
||||||
|
PacketReceived?.Invoke($"Hood RX FAN={fan} LIGHT={(light ? "ON" : "OFF")} 연동CMD={(cmd ? "ON" : "OFF")}{(err != 0 ? $" ERR={err}" : "")}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
using System;
|
||||||
|
using ERVSimulator.Model;
|
||||||
|
|
||||||
|
namespace ERVSimulator.Protocol
|
||||||
|
{
|
||||||
|
// 룸콘 프로토콜 (UART2/SC0)
|
||||||
|
// 패킷: AA | Cmd | D[2..12] | XOR_SUM[13] | EE (15 byte)
|
||||||
|
// 펌웨어 [My_RJ2.c] rx_roomcon_check() + roomcon_parsing()
|
||||||
|
public class RoomConProtocol
|
||||||
|
{
|
||||||
|
public const byte HEADER = 0xAA;
|
||||||
|
public const byte TAIL = 0xEE;
|
||||||
|
public const int PACKET_LEN = 15;
|
||||||
|
|
||||||
|
// Cmd (Rx_roomcon232_buffer[1])
|
||||||
|
public const byte CMD_NORMAL = 0x00; // 상태 폴링
|
||||||
|
public const byte CMD_EVENT = 0x01; // 모드/팬 변경 이벤트
|
||||||
|
public const byte CMD_RESTART1 = 0x02; // 환기단 preset 요청
|
||||||
|
public const byte CMD_RESTART2 = 0x12; // bypass/air preset 요청
|
||||||
|
public const byte CMD_VSP = 0x03; // 테스트모드 진입
|
||||||
|
public const byte CMD_EXIT = 0x04; // 테스트모드 종료
|
||||||
|
public const byte CMD_HOOD_INFO = 0x0A; // ERV→룸콘 후드 연동 통지 (힘펠 V3.7 RX_DATA_HOOD_INFO)
|
||||||
|
|
||||||
|
readonly SerialChannel _ch;
|
||||||
|
readonly ErvState _state;
|
||||||
|
readonly DamperSequencer _seq;
|
||||||
|
readonly System.Windows.Threading.Dispatcher _dispatcher;
|
||||||
|
|
||||||
|
readonly byte[] _rx = new byte[PACKET_LEN];
|
||||||
|
int _rxPos;
|
||||||
|
bool _hoodLinkReported; // 마지막으로 룸콘에 통지한 후드 연동 상태(변화 시에만 0x0A 송신)
|
||||||
|
DateTime _lastByte = DateTime.MinValue;
|
||||||
|
static readonly TimeSpan FrameGap = TimeSpan.FromMilliseconds(50);
|
||||||
|
|
||||||
|
public event Action<string>? PacketReceived;
|
||||||
|
public event Action<string>? PacketSent;
|
||||||
|
|
||||||
|
public RoomConProtocol(SerialChannel ch, ErvState state, DamperSequencer seq,
|
||||||
|
System.Windows.Threading.Dispatcher dispatcher)
|
||||||
|
{
|
||||||
|
_ch = ch; _state = state; _seq = seq; _dispatcher = dispatcher;
|
||||||
|
_ch.ByteReceived += OnByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnByte(byte b)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (now - _lastByte > FrameGap) _rxPos = 0;
|
||||||
|
_lastByte = now;
|
||||||
|
|
||||||
|
// 펌웨어와 동일한 byte 파서 (My_RJ2.c:37)
|
||||||
|
if (_rxPos == 0)
|
||||||
|
{
|
||||||
|
if (b != HEADER) return;
|
||||||
|
_rx[_rxPos++] = b;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_rxPos >= 1 && _rxPos <= 12)
|
||||||
|
{
|
||||||
|
_rx[_rxPos++] = b;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_rxPos == 13)
|
||||||
|
{
|
||||||
|
byte cksum = ChecksumHelper.Xor(_rx, 0, 13);
|
||||||
|
if (cksum != b) { _rxPos = 0; return; }
|
||||||
|
_rx[_rxPos++] = b;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_rxPos == 14)
|
||||||
|
{
|
||||||
|
_rxPos = 0;
|
||||||
|
if (b != TAIL) return;
|
||||||
|
byte[] copy = (byte[])_rx.Clone();
|
||||||
|
_dispatcher.BeginInvoke(new Action(() => HandlePacket(copy)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandlePacket(byte[] p)
|
||||||
|
{
|
||||||
|
PacketReceived?.Invoke($"RoomCon RX: {HexFormat.Bytes(p, 14)} EE");
|
||||||
|
|
||||||
|
byte cmd = p[1];
|
||||||
|
switch (cmd)
|
||||||
|
{
|
||||||
|
case CMD_EVENT: HandleEvent(p); break;
|
||||||
|
case CMD_NORMAL: HandleNormal(p); break;
|
||||||
|
case CMD_RESTART1: HandleRestart1(); break;
|
||||||
|
case CMD_RESTART2: HandleRestart2(); break;
|
||||||
|
case CMD_VSP: HandleVsp(p); break;
|
||||||
|
case CMD_EXIT: HandleExit(); break;
|
||||||
|
default:
|
||||||
|
PacketReceived?.Invoke($" (unknown RoomCon cmd 0x{cmd:X2})");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [My_RJ2.c:387] RX_DATA_MODE_EVENT - 운전 모드/팬 변경
|
||||||
|
void HandleEvent(byte[] p)
|
||||||
|
{
|
||||||
|
byte runMode = p[2];
|
||||||
|
byte fanMode = p[3];
|
||||||
|
_state.RunMode = (RunMode)runMode;
|
||||||
|
_state.SetRunMode = (RunMode)runMode;
|
||||||
|
_state.FanMode = fanMode;
|
||||||
|
_state.SetFanMode = fanMode;
|
||||||
|
// VENT && fan=0 ⇒ Power OFF 진입
|
||||||
|
_state.PowerOn = !(runMode == 0 && fanMode == 0);
|
||||||
|
|
||||||
|
// 예약 (룸콘 EVENT [10]=flag / [11]=시 / [12]=분). HOMENET STATUS(reserve)로도 전달 → 대시보드 반영
|
||||||
|
if (p[10] == 1)
|
||||||
|
{
|
||||||
|
int hours = p[11];
|
||||||
|
_state.ReserveHours = hours; // 0이면 해제
|
||||||
|
_state.ReserveRemainSec = hours * 3600 + p[12] * 60; // 카운트다운/전원OFF는 ReserveTick(1s) 처리
|
||||||
|
}
|
||||||
|
|
||||||
|
_seq.NotifyCommandChanged();
|
||||||
|
|
||||||
|
// 응답: AA 01 RunMode FanMode 00 misc... XOR EE (펌웨어 [My_RJ2.c:489])
|
||||||
|
var tx = NewPacket();
|
||||||
|
tx[1] = 0x01;
|
||||||
|
tx[2] = runMode;
|
||||||
|
tx[3] = fanMode;
|
||||||
|
tx[5] = 0; // Heater/UV/Kijer
|
||||||
|
tx[7] = _state.ErrorCode; // ErrorCode (E02/E07/E09/E10/COLD 비트맵)
|
||||||
|
tx[8] = 0; // Out_Temperature sign
|
||||||
|
tx[9] = 20 + 25; // Out_Temperature = 25
|
||||||
|
tx[10] = 20 + 22; // In_Temperature = 22
|
||||||
|
FinalizeAndSend(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// [My_RJ2.c:327] RX_DATA_MODE_NORMAL - 상태 폴링 응답
|
||||||
|
void HandleNormal(byte[] p)
|
||||||
|
{
|
||||||
|
// 후드 연동 상태가 바뀌면 HOOD_INFO(0x0A)로 통지 (힘펠 V3.7, 펌웨어 Hood_info_command).
|
||||||
|
// HoodStatus = 연동운전중(후드 가동 → 메이크업 에어). 후드 OFF로 ERV 복귀 시 0x80(OFF) 전송.
|
||||||
|
if (_state.HoodStatus != _hoodLinkReported)
|
||||||
|
{
|
||||||
|
_hoodLinkReported = _state.HoodStatus;
|
||||||
|
var hi = NewPacket();
|
||||||
|
hi[1] = CMD_HOOD_INFO;
|
||||||
|
hi[2] = _state.HoodStatus ? (byte)RunMode.Ventilation : (byte)_state.SetRunMode; // 연동 시 환기
|
||||||
|
hi[3] = _state.HoodStatus ? (byte)1 : _state.SetFanMode;
|
||||||
|
hi[6] = _state.HoodStatus ? (byte)0x81 : (byte)0x80; // 후드 연동 ON / OFF
|
||||||
|
FinalizeAndSend(hi);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tx = NewPacket();
|
||||||
|
tx[1] = 0x07; // COMMAND_CONTROLL
|
||||||
|
tx[2] = (byte)_state.SetRunMode;
|
||||||
|
tx[3] = _state.SetFanMode;
|
||||||
|
tx[4] = 0; // Auto_Mode
|
||||||
|
tx[5] = 0;
|
||||||
|
tx[7] = _state.ErrorCode; // ErrorCode 도 동봉
|
||||||
|
FinalizeAndSend(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// [My_RJ2.c:522] RX_DATA_MODE_RESTART1 - 환기 1~4단 preset
|
||||||
|
void HandleRestart1()
|
||||||
|
{
|
||||||
|
var tx = NewPacket();
|
||||||
|
tx[1] = 0x02;
|
||||||
|
tx[4] = 0x10;
|
||||||
|
tx[5] = (byte)_state.FanSAPreset_Vent[1]; tx[6] = (byte)_state.FanEAPreset_Vent[1];
|
||||||
|
tx[7] = (byte)_state.FanSAPreset_Vent[2]; tx[8] = (byte)_state.FanEAPreset_Vent[2];
|
||||||
|
tx[9] = (byte)_state.FanSAPreset_Vent[3]; tx[10] = (byte)_state.FanEAPreset_Vent[3];
|
||||||
|
tx[11] = (byte)_state.FanSAPreset_Vent[4]; tx[12] = (byte)_state.FanEAPreset_Vent[4];
|
||||||
|
FinalizeAndSend(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// [My_RJ2.c:556] RX_DATA_MODE_RESTART2 - bypass/air preset
|
||||||
|
void HandleRestart2()
|
||||||
|
{
|
||||||
|
var tx = NewPacket();
|
||||||
|
tx[1] = 0x12;
|
||||||
|
tx[4] = 0x10;
|
||||||
|
tx[5] = (byte)_state.FanSAPreset_Bypass[1]; tx[6] = (byte)_state.FanEAPreset_Bypass[1];
|
||||||
|
tx[7] = (byte)_state.FanSAPreset_Air[1];
|
||||||
|
tx[8] = (byte)_state.FanSAPreset_Air[2];
|
||||||
|
tx[9] = (byte)_state.FanSAPreset_Air[3];
|
||||||
|
tx[10] = (byte)_state.FanSAPreset_Air[4];
|
||||||
|
FinalizeAndSend(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// [My_RJ2.c:579] RX_DATA_MODE_VSP - 테스트 모드 진입 (preset 갱신)
|
||||||
|
void HandleVsp(byte[] p)
|
||||||
|
{
|
||||||
|
// 본 시뮬레이터에선 RX만 기록, preset 변경은 생략
|
||||||
|
PacketReceived?.Invoke($" VSP select={p[3]} sa={p[4]} ea={p[5]}");
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleExit()
|
||||||
|
{
|
||||||
|
PacketReceived?.Invoke(" VSP exit");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] NewPacket()
|
||||||
|
{
|
||||||
|
var tx = new byte[PACKET_LEN];
|
||||||
|
tx[0] = HEADER;
|
||||||
|
return tx;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FinalizeAndSend(byte[] tx)
|
||||||
|
{
|
||||||
|
tx[13] = ChecksumHelper.Xor(tx, 0, 13);
|
||||||
|
tx[14] = TAIL;
|
||||||
|
if (_ch.Send(tx, PACKET_LEN))
|
||||||
|
PacketSent?.Invoke($"RoomCon TX: {HexFormat.Bytes(tx, 15)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ERVSimulator.Protocol
|
||||||
|
{
|
||||||
|
// 공용 시리얼 채널 - byte 단위 수신 콜백 + 송신 helper
|
||||||
|
public class SerialChannel : IDisposable
|
||||||
|
{
|
||||||
|
private SerialPort? _port;
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public string ChannelName { get; }
|
||||||
|
public event Action<byte>? ByteReceived;
|
||||||
|
public event Action<string>? Log;
|
||||||
|
public event Action<bool>? ConnectionChanged;
|
||||||
|
public bool IsConnected => _port?.IsOpen == true;
|
||||||
|
|
||||||
|
public SerialChannel(string channelName) { ChannelName = channelName; }
|
||||||
|
|
||||||
|
public static string[] GetAvailablePorts() => SerialPort.GetPortNames();
|
||||||
|
|
||||||
|
public bool Connect(string portName, int baudRate)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Disconnect();
|
||||||
|
_port = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One)
|
||||||
|
{
|
||||||
|
ReadTimeout = 100,
|
||||||
|
WriteTimeout = 200,
|
||||||
|
Handshake = Handshake.None,
|
||||||
|
DtrEnable = false,
|
||||||
|
RtsEnable = false,
|
||||||
|
};
|
||||||
|
_port.Open();
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
_ = Task.Run(() => ReadLoop(_cts.Token));
|
||||||
|
Log?.Invoke($"[{ChannelName}] Connected {portName} @ {baudRate}");
|
||||||
|
ConnectionChanged?.Invoke(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log?.Invoke($"[{ChannelName}] Connect FAIL: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Disconnect()
|
||||||
|
{
|
||||||
|
try { _cts?.Cancel(); } catch { }
|
||||||
|
try { _port?.Close(); } catch { }
|
||||||
|
_port?.Dispose();
|
||||||
|
_port = null;
|
||||||
|
ConnectionChanged?.Invoke(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReadLoop(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var buf = new byte[64];
|
||||||
|
while (!ct.IsCancellationRequested && _port != null && _port.IsOpen)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int n = _port.Read(buf, 0, buf.Length);
|
||||||
|
for (int i = 0; i < n; i++) ByteReceived?.Invoke(buf[i]);
|
||||||
|
}
|
||||||
|
catch (TimeoutException) { /* expected */ }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log?.Invoke($"[{ChannelName}] ReadLoop error: {ex.Message}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Send(byte[] data, int length)
|
||||||
|
{
|
||||||
|
if (_port == null || !_port.IsOpen) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_port.Write(data, 0, length);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log?.Invoke($"[{ChannelName}] Send FAIL: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
Disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class HexFormat
|
||||||
|
{
|
||||||
|
public static string Bytes(byte[] data, int length)
|
||||||
|
{
|
||||||
|
var sb = new System.Text.StringBuilder(length * 3);
|
||||||
|
for (int i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
if (i > 0) sb.Append(' ');
|
||||||
|
sb.Append(data[i].ToString("X2"));
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<Window x:Class="ERVSimulator.VspWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="풍량 VSP 설정" SizeToContent="WidthAndHeight"
|
||||||
|
ResizeMode="NoResize" WindowStartupLocation="CenterOwner"
|
||||||
|
Background="{StaticResource PrimaryBgBrush}">
|
||||||
|
<Border Style="{StaticResource SectionCard}" Margin="10">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="풍량 VSP 설정 (SA 급기 / EA 배기) — 수정 가능" Style="{StaticResource SectionTitle}"/>
|
||||||
|
<ItemsControl x:Name="VspItems" Width="990">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate><UniformGrid Columns="3"/></ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border Background="{StaticResource CardBgBrush}" CornerRadius="8" Margin="3" Padding="10,8"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1">
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding Name}" Width="60" FontSize="13" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="SA" Margin="0,0,4,0" FontSize="11" Foreground="{StaticResource AccentBlueBrush}" VerticalAlignment="Center"/>
|
||||||
|
<TextBox Text="{Binding Sa, UpdateSourceTrigger=PropertyChanged}" Width="80" Margin="0,0,10,0" Padding="4,3"
|
||||||
|
TextAlignment="Right" FontSize="12"
|
||||||
|
Background="{StaticResource PrimaryBgBrush}" Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
<TextBlock Text="EA" Margin="0,0,4,0" FontSize="11" Foreground="{StaticResource AccentGreenBrush}" VerticalAlignment="Center"/>
|
||||||
|
<TextBox Text="{Binding Ea, UpdateSourceTrigger=PropertyChanged}" Width="80" Padding="4,3"
|
||||||
|
TextAlignment="Right" FontSize="12"
|
||||||
|
Background="{StaticResource PrimaryBgBrush}" Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,12,0,0" HorizontalAlignment="Right">
|
||||||
|
<Button Content="VSP 적용" Width="100" Style="{StaticResource ModernButton}" Click="Apply_Click"
|
||||||
|
Margin="0,0,6,0" Background="{StaticResource AccentBlueBrush}"/>
|
||||||
|
<Button Content="닫기" Width="90" Style="{StaticResource ModernButton}" Click="Close_Click"
|
||||||
|
Background="{StaticResource CardBgBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Windows;
|
||||||
|
using ERVSimulator.Model;
|
||||||
|
|
||||||
|
namespace ERVSimulator
|
||||||
|
{
|
||||||
|
// 편집 가능한 풍량 VSP 행 (환기1~4, 바이패스, 공청1~4)
|
||||||
|
public class VspEditRow
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int Sa { get; set; }
|
||||||
|
public int Ea { get; set; }
|
||||||
|
public VspEditRow(string n, int sa, int ea) { Name = n; Sa = sa; Ea = ea; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class VspWindow : Window
|
||||||
|
{
|
||||||
|
readonly ErvState _state;
|
||||||
|
public ObservableCollection<VspEditRow> Rows { get; } = new();
|
||||||
|
public event Action<string>? Applied;
|
||||||
|
|
||||||
|
public VspWindow(ErvState state)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_state = state;
|
||||||
|
|
||||||
|
Rows.Add(new VspEditRow("환기1", state.FanSAPreset_Vent[1], state.FanEAPreset_Vent[1]));
|
||||||
|
Rows.Add(new VspEditRow("환기2", state.FanSAPreset_Vent[2], state.FanEAPreset_Vent[2]));
|
||||||
|
Rows.Add(new VspEditRow("환기3", state.FanSAPreset_Vent[3], state.FanEAPreset_Vent[3]));
|
||||||
|
Rows.Add(new VspEditRow("환기4", state.FanSAPreset_Vent[4], state.FanEAPreset_Vent[4]));
|
||||||
|
Rows.Add(new VspEditRow("바이패스", state.FanSAPreset_Bypass[1], state.FanEAPreset_Bypass[1]));
|
||||||
|
Rows.Add(new VspEditRow("공청1", state.FanSAPreset_Air[1], state.FanEAPreset_Air[1]));
|
||||||
|
Rows.Add(new VspEditRow("공청2", state.FanSAPreset_Air[2], state.FanEAPreset_Air[2]));
|
||||||
|
Rows.Add(new VspEditRow("공청3", state.FanSAPreset_Air[3], state.FanEAPreset_Air[3]));
|
||||||
|
Rows.Add(new VspEditRow("공청4", state.FanSAPreset_Air[4], state.FanEAPreset_Air[4]));
|
||||||
|
|
||||||
|
VspItems.ItemsSource = Rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ushort Clamp(int v) => (ushort)(v < 0 ? 0 : v > 255 ? 255 : v); // VSP 1바이트
|
||||||
|
|
||||||
|
void Apply_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_state.FanSAPreset_Vent[1] = Clamp(Rows[0].Sa); _state.FanEAPreset_Vent[1] = Clamp(Rows[0].Ea);
|
||||||
|
_state.FanSAPreset_Vent[2] = Clamp(Rows[1].Sa); _state.FanEAPreset_Vent[2] = Clamp(Rows[1].Ea);
|
||||||
|
_state.FanSAPreset_Vent[3] = Clamp(Rows[2].Sa); _state.FanEAPreset_Vent[3] = Clamp(Rows[2].Ea);
|
||||||
|
_state.FanSAPreset_Vent[4] = Clamp(Rows[3].Sa); _state.FanEAPreset_Vent[4] = Clamp(Rows[3].Ea);
|
||||||
|
_state.FanSAPreset_Bypass[1] = Clamp(Rows[4].Sa); _state.FanEAPreset_Bypass[1] = Clamp(Rows[4].Ea);
|
||||||
|
_state.FanSAPreset_Air[1] = Clamp(Rows[5].Sa); _state.FanEAPreset_Air[1] = Clamp(Rows[5].Ea);
|
||||||
|
_state.FanSAPreset_Air[2] = Clamp(Rows[6].Sa); _state.FanEAPreset_Air[2] = Clamp(Rows[6].Ea);
|
||||||
|
_state.FanSAPreset_Air[3] = Clamp(Rows[7].Sa); _state.FanEAPreset_Air[3] = Clamp(Rows[7].Ea);
|
||||||
|
_state.FanSAPreset_Air[4] = Clamp(Rows[8].Sa); _state.FanEAPreset_Air[4] = Clamp(Rows[8].Ea);
|
||||||
|
|
||||||
|
Applied?.Invoke("[Manual] 풍량 VSP 값 적용");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Close_Click(object sender, RoutedEventArgs e) => Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,6 @@
|
|||||||
|
외기(OA), CN2 , GREEN, U1(15~18),MCU (PB.12,PB.13,PB.14,PB.8)
|
||||||
|
공청(AIR), CN10, YELLOW, U4 (11~14), MCU (PD.14,PD.7, PD.6,PB.3)
|
||||||
|
바이패스(BYPASS), CN5, RED, U2 (15~18), MCU (PA.6,PA.14,PA.15,PC.8)
|
||||||
|
배기(EA), CN3, BLACK, U1(11~14), MCU (PB.15,PC.14,PC.15,PC.6)
|
||||||
|
급기(SA), CN7, BLUE, U2(11~14), MCU (PC.9,PC.10,PC.11,PB.9)
|
||||||
|
환기(RA), CN9, WHITE, U4(15~18), MCU (PB.10,PC.2,PC.3,PD.15)
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<Application x:Class="HoodSimulator.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
StartupUri="MainWindow.xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
<Color x:Key="PrimaryBg">#1E1E2E</Color>
|
||||||
|
<Color x:Key="SecondaryBg">#2B2B3D</Color>
|
||||||
|
<Color x:Key="CardBg">#313147</Color>
|
||||||
|
<Color x:Key="AccentBlue">#7AA2F7</Color>
|
||||||
|
<Color x:Key="AccentGreen">#9ECE6A</Color>
|
||||||
|
<Color x:Key="AccentRed">#F7768E</Color>
|
||||||
|
<Color x:Key="AccentYellow">#E0AF68</Color>
|
||||||
|
<Color x:Key="AccentCyan">#7DCFFF</Color>
|
||||||
|
<Color x:Key="AccentPurple">#BB9AF7</Color>
|
||||||
|
<Color x:Key="TextPrimary">#C0CAF5</Color>
|
||||||
|
<Color x:Key="TextSecondary">#565F89</Color>
|
||||||
|
<Color x:Key="BorderColor">#3B3B55</Color>
|
||||||
|
|
||||||
|
<SolidColorBrush x:Key="PrimaryBgBrush" Color="{StaticResource PrimaryBg}"/>
|
||||||
|
<SolidColorBrush x:Key="SecondaryBgBrush" Color="{StaticResource SecondaryBg}"/>
|
||||||
|
<SolidColorBrush x:Key="CardBgBrush" Color="{StaticResource CardBg}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentBlueBrush" Color="{StaticResource AccentBlue}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentGreenBrush" Color="{StaticResource AccentGreen}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentRedBrush" Color="{StaticResource AccentRed}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentYellowBrush" Color="{StaticResource AccentYellow}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentCyanBrush" Color="{StaticResource AccentCyan}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentPurpleBrush" Color="{StaticResource AccentPurple}"/>
|
||||||
|
<SolidColorBrush x:Key="TextPrimaryBrush" Color="{StaticResource TextPrimary}"/>
|
||||||
|
<SolidColorBrush x:Key="TextSecondaryBrush" Color="{StaticResource TextSecondary}"/>
|
||||||
|
<SolidColorBrush x:Key="BorderBrush" Color="{StaticResource BorderColor}"/>
|
||||||
|
|
||||||
|
<Style x:Key="ModernButton" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="{StaticResource AccentBlueBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Padding" Value="18,8"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="border" Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="6" Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="border" Property="Opacity" Value="0.85"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="border" Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ToggleSwitch" TargetType="ToggleButton">
|
||||||
|
<Setter Property="Width" Value="56"/>
|
||||||
|
<Setter Property="Height" Value="28"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ToggleButton">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="track" CornerRadius="14" Background="#3B3B55" Width="56" Height="28"/>
|
||||||
|
<Border x:Name="thumb" CornerRadius="11" Background="#565F89" Width="22" Height="22"
|
||||||
|
HorizontalAlignment="Left" Margin="3,0,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="track" Property="Background" Value="{StaticResource AccentGreenBrush}"/>
|
||||||
|
<Setter TargetName="thumb" Property="Background" Value="White"/>
|
||||||
|
<Setter TargetName="thumb" Property="HorizontalAlignment" Value="Right"/>
|
||||||
|
<Setter TargetName="thumb" Property="Margin" Value="0,0,3,0"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModernComboBox" TargetType="ComboBox">
|
||||||
|
<Setter Property="Background" Value="{StaticResource CardBgBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="8,5"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="ItemContainerStyle">
|
||||||
|
<Setter.Value>
|
||||||
|
<Style TargetType="ComboBoxItem">
|
||||||
|
<Setter Property="Foreground" Value="Black"/>
|
||||||
|
</Style>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace HoodSimulator
|
||||||
|
{
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace HoodSimulator
|
||||||
|
{
|
||||||
|
// 후드메인(Slave) 시뮬레이터 프로토콜
|
||||||
|
// 규격 : Protocol/HOOD/주신전자_protocol_hood_전열교환기_Rev1.3_20241125.xlsx
|
||||||
|
// - 9바이트 고정, 115200 8N1, 폴링주기 100~500ms, 응답 50ms 이내
|
||||||
|
// - CS = Preamble~CS직전(byte0~7) 전체 XOR
|
||||||
|
// Master(전열교환기) → Slave(후드) : AA 21 ID MODE FAN 연동EN 연동운전중 ERROR CS
|
||||||
|
// Slave(후드) → Master(전열교환기) : AA 11 ID FANSTATUS LIGHTSTATUS 00 연동CMD ERROR CS
|
||||||
|
// 시뮬레이터는 Slave 역할 — 마스터 폴 수신 시 현재 후드 상태로 응답.
|
||||||
|
public class HoodProtocol : IDisposable
|
||||||
|
{
|
||||||
|
public const byte PREAMBLE = 0xAA;
|
||||||
|
public const byte MS_MASTER = 0x21;
|
||||||
|
public const byte MS_SLAVE = 0x11;
|
||||||
|
public const byte HOOD_ID = 0x01;
|
||||||
|
public const int FRAME_LEN = 9;
|
||||||
|
|
||||||
|
SerialPort? _port;
|
||||||
|
CancellationTokenSource? _cts;
|
||||||
|
readonly object _lock = new();
|
||||||
|
bool _disposed;
|
||||||
|
bool _responding;
|
||||||
|
|
||||||
|
// ---- 후드 상태 (UI 제어) ----
|
||||||
|
public bool PowerOn; // 전원 on/off
|
||||||
|
public byte FanStage; // 풍량 0(꺼짐)~5
|
||||||
|
public bool Light; // 조명 on/off
|
||||||
|
public byte ErrorCode; // ERROR : 0 정상 / 1 FAN 에러 / 2 기타 에러
|
||||||
|
|
||||||
|
public event Action<byte, byte, byte, byte>? MasterPacketReceived; // mode, fan, en, run
|
||||||
|
public event Action<byte[]>? ResponseSent; // 송신한 9바이트 응답
|
||||||
|
public event Action<string>? LogMessage;
|
||||||
|
public event Action<bool>? ConnectionChanged;
|
||||||
|
|
||||||
|
public bool IsConnected => _port?.IsOpen == true;
|
||||||
|
public bool IsResponding => _responding;
|
||||||
|
|
||||||
|
public static byte Xor(byte[] d, int start, int len)
|
||||||
|
{
|
||||||
|
byte x = 0;
|
||||||
|
for (int i = 0; i < len; i++) x ^= d[start + i];
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] GetAvailablePorts() => SerialPort.GetPortNames();
|
||||||
|
|
||||||
|
public bool Connect(string portName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Disconnect();
|
||||||
|
_port = new SerialPort(portName)
|
||||||
|
{
|
||||||
|
BaudRate = 115200, DataBits = 8,
|
||||||
|
StopBits = StopBits.One, Parity = Parity.None,
|
||||||
|
ReadTimeout = 100, WriteTimeout = 500
|
||||||
|
};
|
||||||
|
_port.Open();
|
||||||
|
Log($"[연결] {portName} (115200, 8N1)");
|
||||||
|
ConnectionChanged?.Invoke(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[오류] 연결 실패: {ex.Message}");
|
||||||
|
ConnectionChanged?.Invoke(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Disconnect()
|
||||||
|
{
|
||||||
|
StopResponding();
|
||||||
|
try { if (_port?.IsOpen == true) { _port.Close(); Log("[연결 해제]"); } } catch { }
|
||||||
|
_port?.Dispose();
|
||||||
|
_port = null;
|
||||||
|
ConnectionChanged?.Invoke(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartResponding()
|
||||||
|
{
|
||||||
|
StopResponding();
|
||||||
|
_responding = true;
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
var token = _cts.Token;
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_port?.IsOpen != true) { Thread.Sleep(50); continue; }
|
||||||
|
if (_port.BytesToRead < 1) { Thread.Sleep(3); continue; }
|
||||||
|
|
||||||
|
byte b = (byte)_port.ReadByte();
|
||||||
|
if (b != PREAMBLE) continue;
|
||||||
|
|
||||||
|
byte[] rx = new byte[FRAME_LEN];
|
||||||
|
rx[0] = PREAMBLE;
|
||||||
|
int total = 1, remain = FRAME_LEN - 1, retries = 100;
|
||||||
|
while (remain > 0 && retries-- > 0)
|
||||||
|
{
|
||||||
|
if (_port.BytesToRead > 0)
|
||||||
|
{ int r = _port.Read(rx, total, remain); total += r; remain -= r; }
|
||||||
|
else Thread.Sleep(2);
|
||||||
|
}
|
||||||
|
if (total < FRAME_LEN) continue;
|
||||||
|
|
||||||
|
// 마스터 프레임만 처리
|
||||||
|
if (rx[1] != MS_MASTER) continue;
|
||||||
|
if (rx[2] != HOOD_ID) continue;
|
||||||
|
byte cs = Xor(rx, 0, 8);
|
||||||
|
if (cs != rx[8]) { Log($"[CS오류] 수신:0x{rx[8]:X2} 계산:0x{cs:X2} {BitConverter.ToString(rx)}"); continue; }
|
||||||
|
|
||||||
|
byte mode = rx[3], fan = rx[4], en = rx[5], run = rx[6];
|
||||||
|
Log($"[RX] {BitConverter.ToString(rx)} MODE={mode} FAN={fan} 연동EN={en} 연동운전={run}");
|
||||||
|
MasterPacketReceived?.Invoke(mode, fan, en, run);
|
||||||
|
|
||||||
|
// 응답 전송 (현재 후드 상태)
|
||||||
|
byte[] tx = BuildResponse();
|
||||||
|
lock (_lock) { _port?.Write(tx, 0, tx.Length); }
|
||||||
|
Log($"[TX 응답] {BitConverter.ToString(tx)}");
|
||||||
|
ResponseSent?.Invoke(tx);
|
||||||
|
}
|
||||||
|
catch (TimeoutException) { }
|
||||||
|
catch (OperationCanceledException) { break; }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!token.IsCancellationRequested) Log($"[오류] {ex.Message}");
|
||||||
|
Thread.Sleep(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, token);
|
||||||
|
Log("[통신 시작] 마스터 응답 모드");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopResponding()
|
||||||
|
{
|
||||||
|
_responding = false;
|
||||||
|
_cts?.Cancel();
|
||||||
|
_cts?.Dispose();
|
||||||
|
_cts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] BuildResponse()
|
||||||
|
{
|
||||||
|
byte fanStatus = PowerOn ? FanStage : (byte)0; // 후드 FAN STATUS 0~5
|
||||||
|
byte lightStatus = (byte)((PowerOn && Light) ? 1 : 0); // 후드 LIGHT STATUS
|
||||||
|
byte cmd = (byte)(PowerOn ? 1 : 0); // 연동 CMD : 0 꺼짐 / 1 켜짐
|
||||||
|
|
||||||
|
byte[] p = new byte[FRAME_LEN];
|
||||||
|
p[0] = PREAMBLE;
|
||||||
|
p[1] = MS_SLAVE;
|
||||||
|
p[2] = HOOD_ID;
|
||||||
|
p[3] = fanStatus;
|
||||||
|
p[4] = lightStatus;
|
||||||
|
p[5] = 0x00;
|
||||||
|
p[6] = cmd;
|
||||||
|
p[7] = ErrorCode; // ERROR : 0 정상 / 1 FAN / 2 기타
|
||||||
|
p[8] = Xor(p, 0, 8); // CS
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Log(string msg) => LogMessage?.Invoke($"[{DateTime.Now:HH:mm:ss.fff}] {msg}");
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
Disconnect();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>HoodSimulator</RootNamespace>
|
||||||
|
<AssemblyName>HoodSimulator</AssemblyName>
|
||||||
|
<StartupObject>HoodSimulator.App</StartupObject>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
|
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="System.IO.Ports" Version="10.0.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<Window x:Class="HoodSimulator.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="후드 시뮬레이터 - Hood Simulator"
|
||||||
|
Width="500" Height="820"
|
||||||
|
MinWidth="460" MinHeight="700"
|
||||||
|
Background="{StaticResource PrimaryBgBrush}"
|
||||||
|
WindowStartupLocation="CenterScreen">
|
||||||
|
|
||||||
|
<Grid Margin="12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/> <!-- 0: 연결 -->
|
||||||
|
<RowDefinition Height="Auto"/> <!-- 1: 후드 제어 -->
|
||||||
|
<RowDefinition Height="Auto"/> <!-- 2: 통신 상태 -->
|
||||||
|
<RowDefinition Height="*"/> <!-- 3: 로그 -->
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Row 0: 연결 설정 (2줄) -->
|
||||||
|
<Border Grid.Row="0" Background="{StaticResource SecondaryBgBrush}"
|
||||||
|
CornerRadius="10" Padding="14,10" Margin="0,0,0,8">
|
||||||
|
<StackPanel>
|
||||||
|
<DockPanel Margin="0,0,0,8">
|
||||||
|
<TextBlock Text="후드 시뮬레이터" FontSize="17" FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource AccentCyanBrush}" VerticalAlignment="Center"/>
|
||||||
|
<StackPanel DockPanel.Dock="Right" VerticalAlignment="Center" HorizontalAlignment="Right">
|
||||||
|
<TextBlock Text="만든이 : 전경선" Foreground="{StaticResource TextSecondaryBrush}" FontSize="10" TextAlignment="Right"/>
|
||||||
|
<TextBlock Text="수정일 : 2026.06.07" Foreground="{StaticResource TextSecondaryBrush}" FontSize="10" TextAlignment="Right"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<ComboBox x:Name="cmbPort" Width="92" Style="{StaticResource ModernComboBox}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
|
<Button Content="⟳" Width="28" Height="28" FontSize="13"
|
||||||
|
Style="{StaticResource ModernButton}" Click="RefreshPorts_Click"
|
||||||
|
Background="{StaticResource CardBgBrush}" Margin="0,0,8,0" Padding="0"/>
|
||||||
|
<Ellipse x:Name="statusLed" Width="10" Height="10" Fill="#F7768E" Margin="0,0,5,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="txtStatus" Text="미연결" Foreground="{StaticResource TextSecondaryBrush}"
|
||||||
|
FontSize="12" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
|
<Button x:Name="btnConnect" Content="연결" Style="{StaticResource ModernButton}"
|
||||||
|
Click="Connect_Click" Margin="0,0,4,0" Padding="12,6"/>
|
||||||
|
<Button x:Name="btnStart" Content="시작" Style="{StaticResource ModernButton}"
|
||||||
|
Background="{StaticResource AccentGreenBrush}" Click="Start_Click"
|
||||||
|
IsEnabled="False" Margin="0,0,4,0" Padding="12,6"/>
|
||||||
|
<Button x:Name="btnStop" Content="중지" Style="{StaticResource ModernButton}"
|
||||||
|
Background="{StaticResource AccentRedBrush}" Click="Stop_Click"
|
||||||
|
IsEnabled="False" Padding="12,6"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Row 1: 후드 제어 -->
|
||||||
|
<Border Grid.Row="1" Background="{StaticResource SecondaryBgBrush}" CornerRadius="10" Padding="16" Margin="0,0,0,8">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="후드 제어" FontSize="14" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,0,14"/>
|
||||||
|
|
||||||
|
<!-- 전원 -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,16">
|
||||||
|
<TextBlock Text="전원" Width="56" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
<ToggleButton x:Name="tglPower" Style="{StaticResource ToggleSwitch}" Click="Power_Click" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="txtPower" Text="OFF" FontSize="13" FontWeight="Bold" Margin="12,0,0,0"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="txtMakeup" Text="" FontSize="13" FontWeight="Bold" Margin="10,0,0,0"
|
||||||
|
Foreground="{StaticResource AccentCyanBrush}" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 풍량 -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,16">
|
||||||
|
<TextBlock Text="풍량" Width="56" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
<StackPanel Orientation="Horizontal" x:Name="FanPanel">
|
||||||
|
<Button Content="0" Tag="0" Click="Fan_Click" Style="{StaticResource ModernButton}" Margin="0,0,4,0" MinWidth="40" Padding="0,7" Background="{StaticResource CardBgBrush}" ToolTip="꺼짐"/>
|
||||||
|
<Button Content="1" Tag="1" Click="Fan_Click" Style="{StaticResource ModernButton}" Margin="0,0,4,0" MinWidth="40" Padding="0,7" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="2" Tag="2" Click="Fan_Click" Style="{StaticResource ModernButton}" Margin="0,0,4,0" MinWidth="40" Padding="0,7" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="3" Tag="3" Click="Fan_Click" Style="{StaticResource ModernButton}" Margin="0,0,4,0" MinWidth="40" Padding="0,7" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="4" Tag="4" Click="Fan_Click" Style="{StaticResource ModernButton}" Margin="0,0,4,0" MinWidth="40" Padding="0,7" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="5" Tag="5" Click="Fan_Click" Style="{StaticResource ModernButton}" Margin="0,0,0,0" MinWidth="40" Padding="0,7" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 조명 -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,16">
|
||||||
|
<TextBlock Text="조명" Width="56" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
<ToggleButton x:Name="tglLight" Style="{StaticResource ToggleSwitch}" Click="Light_Click" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="txtLight" Text="OFF" FontSize="13" FontWeight="Bold" Margin="12,0,0,0"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 에러코드 (체크 선택하여 발생) -->
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="에러코드" Width="56" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
<Border x:Name="ErrCard_Fan" Tag="1" MouseDown="ErrorCard_Click" Cursor="Hand" CornerRadius="6" Padding="8,5" Margin="0,0,6,0"
|
||||||
|
Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" ToolTip="FAN 에러 (ERROR=1)">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Ellipse x:Name="ErrLed_Fan" Width="10" Height="10" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<TextBlock Text="FAN 에러" FontSize="12" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border x:Name="ErrCard_Etc" Tag="2" MouseDown="ErrorCard_Click" Cursor="Hand" CornerRadius="6" Padding="8,5"
|
||||||
|
Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" ToolTip="기타 에러 (ERROR=2)">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Ellipse x:Name="ErrLed_Etc" Width="10" Height="10" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<TextBlock Text="기타 에러" FontSize="12" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Row 2: 통신 상태 -->
|
||||||
|
<Border Grid.Row="2" Background="{StaticResource SecondaryBgBrush}" CornerRadius="10" Padding="16" Margin="0,0,0,8">
|
||||||
|
<StackPanel>
|
||||||
|
<DockPanel Margin="0,0,0,12">
|
||||||
|
<TextBlock Text="통신 상태" FontSize="14" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<TextBlock x:Name="txtRxCount" DockPanel.Dock="Right" Text="수신: 0" FontSize="12"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<TextBlock Text="◇ 마스터 수신 명령 (전열교환기 → 후드)" FontSize="11" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource AccentCyanBrush}" Margin="0,0,0,6"/>
|
||||||
|
<Border Background="{StaticResource CardBgBrush}" CornerRadius="6" Padding="12,8" Margin="0,0,0,12"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1">
|
||||||
|
<StackPanel>
|
||||||
|
<DockPanel Margin="0,0,0,3"><TextBlock Text="운전모드 (MODE)" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}"/><TextBlock x:Name="txtRxMode" DockPanel.Dock="Right" Text="-" FontSize="12" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}"/></DockPanel>
|
||||||
|
<DockPanel Margin="0,0,0,3"><TextBlock Text="전열교환기 풍량 (FAN)" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}"/><TextBlock x:Name="txtRxFan" DockPanel.Dock="Right" Text="-" FontSize="12" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}"/></DockPanel>
|
||||||
|
<DockPanel Margin="0,0,0,3"><TextBlock Text="연동 Enable" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}"/><TextBlock x:Name="txtRxEn" DockPanel.Dock="Right" Text="-" FontSize="12" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}"/></DockPanel>
|
||||||
|
<DockPanel><TextBlock Text="연동 운전중" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}"/><TextBlock x:Name="txtRxRun" DockPanel.Dock="Right" Text="-" FontSize="12" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}"/></DockPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock Text="◇ 후드 응답 송신 (후드 → 전열교환기)" FontSize="11" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource AccentGreenBrush}" Margin="0,0,0,6"/>
|
||||||
|
<Border Background="{StaticResource CardBgBrush}" CornerRadius="6" Padding="12,8"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1">
|
||||||
|
<StackPanel>
|
||||||
|
<DockPanel Margin="0,0,0,3"><TextBlock Text="후드 FAN STATUS" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}"/><TextBlock x:Name="txtTxFan" DockPanel.Dock="Right" Text="-" FontSize="12" FontWeight="Bold" Foreground="{StaticResource AccentGreenBrush}"/></DockPanel>
|
||||||
|
<DockPanel Margin="0,0,0,3"><TextBlock Text="후드 LIGHT STATUS" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}"/><TextBlock x:Name="txtTxLight" DockPanel.Dock="Right" Text="-" FontSize="12" FontWeight="Bold" Foreground="{StaticResource AccentGreenBrush}"/></DockPanel>
|
||||||
|
<DockPanel Margin="0,0,0,3"><TextBlock Text="연동 CMD" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}"/><TextBlock x:Name="txtTxCmd" DockPanel.Dock="Right" Text="-" FontSize="12" FontWeight="Bold" Foreground="{StaticResource AccentGreenBrush}"/></DockPanel>
|
||||||
|
<DockPanel><TextBlock Text="ERROR" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}"/><TextBlock x:Name="txtTxError" DockPanel.Dock="Right" Text="-" FontSize="12" FontWeight="Bold" Foreground="{StaticResource AccentGreenBrush}"/></DockPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Row 3: 통신 로그 -->
|
||||||
|
<Border Grid.Row="3" Background="{StaticResource SecondaryBgBrush}" CornerRadius="10" Padding="12">
|
||||||
|
<DockPanel>
|
||||||
|
<Grid DockPanel.Dock="Top" Margin="0,0,0,5">
|
||||||
|
<TextBlock Text="통신 로그" FontSize="12" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button Content="저장" Style="{StaticResource ModernButton}" Background="{StaticResource AccentBlueBrush}"
|
||||||
|
Padding="10,3" FontSize="11" Click="SaveLog_Click" Margin="0,0,6,0"/>
|
||||||
|
<Button Content="지우기" Style="{StaticResource ModernButton}" Background="{StaticResource CardBgBrush}"
|
||||||
|
Padding="10,3" FontSize="11" Click="ClearLog_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<TextBox x:Name="txtLog" IsReadOnly="True" Background="{StaticResource CardBgBrush}"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}"
|
||||||
|
BorderThickness="1" FontFamily="Consolas" FontSize="10"
|
||||||
|
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
|
||||||
|
TextWrapping="NoWrap" Padding="6"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace HoodSimulator
|
||||||
|
{
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
readonly HoodProtocol _hood = new();
|
||||||
|
int _rxCount;
|
||||||
|
|
||||||
|
// 조리 종료 후 메이크업 유지(잔여 냄새 배출) — 후드측이 담당. 유지중에는 ERV 에 계속 '켜짐' 보고,
|
||||||
|
// 종료 시점에 OFF 전송 → ERV 가 원래 모드/풍량으로 복귀. (사양 260613 9p 3.3)
|
||||||
|
readonly System.Windows.Threading.DispatcherTimer _makeupTimer =
|
||||||
|
new() { Interval = TimeSpan.FromSeconds(1) };
|
||||||
|
const int MakeupHoldSec = 10; // 메이크업 유지 시간 (10초)
|
||||||
|
int _makeupRemainSec;
|
||||||
|
|
||||||
|
static readonly Brush AccentCyan = (Brush)new BrushConverter().ConvertFromString("#7DCFFF")!;
|
||||||
|
static readonly Brush AccentGreen = (Brush)new BrushConverter().ConvertFromString("#9ECE6A")!;
|
||||||
|
static readonly Brush AccentRed = (Brush)new BrushConverter().ConvertFromString("#F7768E")!;
|
||||||
|
static readonly Brush CardBg = (Brush)new BrushConverter().ConvertFromString("#313147")!;
|
||||||
|
static readonly Brush TextPrimary = (Brush)new BrushConverter().ConvertFromString("#C0CAF5")!;
|
||||||
|
static readonly Brush TextSecondary = (Brush)new BrushConverter().ConvertFromString("#565F89")!;
|
||||||
|
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_hood.LogMessage += OnLog;
|
||||||
|
_hood.ConnectionChanged += OnConnectionChanged;
|
||||||
|
_hood.MasterPacketReceived += OnMasterPacket;
|
||||||
|
_hood.ResponseSent += OnResponseSent;
|
||||||
|
_makeupTimer.Tick += MakeupTick;
|
||||||
|
|
||||||
|
RefreshPorts();
|
||||||
|
UpdateFanButtons();
|
||||||
|
Closed += (_, _) => { _makeupTimer.Stop(); _hood.Dispose(); };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 연결 ==========
|
||||||
|
void RefreshPorts()
|
||||||
|
{
|
||||||
|
cmbPort.Items.Clear();
|
||||||
|
foreach (var p in _hood.GetAvailablePorts()) cmbPort.Items.Add(p);
|
||||||
|
if (cmbPort.Items.Count > 0) cmbPort.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
void RefreshPorts_Click(object s, RoutedEventArgs e) => RefreshPorts();
|
||||||
|
|
||||||
|
void Connect_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_hood.IsConnected)
|
||||||
|
{
|
||||||
|
_hood.Disconnect();
|
||||||
|
btnConnect.Content = "연결";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (cmbPort.SelectedItem == null) { MessageBox.Show("COM 포트를 선택하세요."); return; }
|
||||||
|
if (_hood.Connect(cmbPort.SelectedItem.ToString()!)) btnConnect.Content = "연결 해제";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Start_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_hood.IsConnected) return;
|
||||||
|
_hood.StartResponding();
|
||||||
|
btnStart.IsEnabled = false;
|
||||||
|
btnStop.IsEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Stop_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_hood.StopResponding();
|
||||||
|
btnStart.IsEnabled = true;
|
||||||
|
btnStop.IsEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 후드 제어 ==========
|
||||||
|
void Power_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (tglPower.IsChecked == true)
|
||||||
|
{
|
||||||
|
// 켜기 : 진행중인 메이크업 유지 취소 후 즉시 ON (풍량 1)
|
||||||
|
StopMakeupHold();
|
||||||
|
_hood.PowerOn = true;
|
||||||
|
_hood.FanStage = 1;
|
||||||
|
txtPower.Text = "ON";
|
||||||
|
txtPower.Foreground = AccentGreen;
|
||||||
|
UpdateFanButtons();
|
||||||
|
OnLog("[제어] 전원 → ON (풍량 1)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 끄기 : OFF 표시 + 옆에 메이크업 유지(1분) 카운트다운 시작. 그동안 ERV엔 계속 '켜짐' 보고.
|
||||||
|
// 유지 종료 시 후드 OFF 전송 → ERV 가 원래 모드/풍량으로 복귀.
|
||||||
|
txtPower.Text = "OFF";
|
||||||
|
txtPower.Foreground = TextSecondary;
|
||||||
|
if (_hood.PowerOn && _makeupRemainSec == 0)
|
||||||
|
{
|
||||||
|
_makeupRemainSec = MakeupHoldSec;
|
||||||
|
_makeupTimer.Start();
|
||||||
|
txtMakeup.Text = $"메이크업 {_makeupRemainSec}s";
|
||||||
|
OnLog($"[제어] 전원 OFF 요청 → 메이크업 에어 {MakeupHoldSec}s 유지 (ERV엔 계속 켜짐 보고)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 메이크업 유지 카운트다운 (1초). 0이 되면 실제 OFF 전송.
|
||||||
|
void MakeupTick(object? s, EventArgs e)
|
||||||
|
{
|
||||||
|
_makeupRemainSec--;
|
||||||
|
if (_makeupRemainSec > 0)
|
||||||
|
{
|
||||||
|
txtMakeup.Text = $"메이크업 {_makeupRemainSec}s";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StopMakeupHold();
|
||||||
|
_hood.PowerOn = false;
|
||||||
|
_hood.FanStage = 0;
|
||||||
|
UpdateFanButtons();
|
||||||
|
OnLog("[제어] 메이크업 유지 종료 → 후드 OFF 전송 (ERV 원래 모드/풍량 복귀)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StopMakeupHold()
|
||||||
|
{
|
||||||
|
_makeupTimer.Stop();
|
||||||
|
_makeupRemainSec = 0;
|
||||||
|
txtMakeup.Text = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
void Fan_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (s is Button b && b.Tag is string tag && byte.TryParse(tag, out var f))
|
||||||
|
{
|
||||||
|
_hood.FanStage = f;
|
||||||
|
UpdateFanButtons();
|
||||||
|
OnLog($"[제어] 풍량 → {f}{(f == 0 ? " (꺼짐)" : "단")}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Light_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_hood.Light = tglLight.IsChecked == true;
|
||||||
|
txtLight.Text = _hood.Light ? "ON" : "OFF";
|
||||||
|
txtLight.Foreground = _hood.Light ? AccentGreen : TextSecondary;
|
||||||
|
OnLog($"[제어] 조명 → {(_hood.Light ? "ON" : "OFF")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 에러코드 토글 (FAN 에러=1 / 기타 에러=2). 둘 다 켜지면 FAN(1) 우선 송신.
|
||||||
|
bool _errFan, _errEtc;
|
||||||
|
void ErrorCard_Click(object s, System.Windows.Input.MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (s is not Border b || b.Tag is not string tag) return;
|
||||||
|
if (tag == "1") _errFan = !_errFan;
|
||||||
|
else if (tag == "2") _errEtc = !_errEtc;
|
||||||
|
|
||||||
|
_hood.ErrorCode = _errFan ? (byte)1 : _errEtc ? (byte)2 : (byte)0;
|
||||||
|
UpdateErrorCards();
|
||||||
|
OnLog($"[제어] 에러코드 → {ErrorName(_hood.ErrorCode)} (ERROR={_hood.ErrorCode})");
|
||||||
|
|
||||||
|
// 에러 발생 시 전원 OFF / 풍량 0 / 조명 OFF (다음 응답에 반영되어 전송)
|
||||||
|
if (_hood.ErrorCode != 0)
|
||||||
|
{
|
||||||
|
StopMakeupHold(); // 진행중인 메이크업 유지 즉시 취소
|
||||||
|
_hood.PowerOn = false;
|
||||||
|
_hood.FanStage = 0;
|
||||||
|
_hood.Light = false;
|
||||||
|
|
||||||
|
tglPower.IsChecked = false;
|
||||||
|
txtPower.Text = "OFF"; txtPower.Foreground = TextSecondary;
|
||||||
|
tglLight.IsChecked = false;
|
||||||
|
txtLight.Text = "OFF"; txtLight.Foreground = TextSecondary;
|
||||||
|
UpdateFanButtons();
|
||||||
|
OnLog("[제어] 에러 발생 → 전원 OFF / 풍량 0 / 조명 OFF");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateErrorCards()
|
||||||
|
{
|
||||||
|
UpdateErrLed(ErrCard_Fan, ErrLed_Fan, _errFan);
|
||||||
|
UpdateErrLed(ErrCard_Etc, ErrLed_Etc, _errEtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void UpdateErrLed(Border card, System.Windows.Shapes.Ellipse led, bool on)
|
||||||
|
{
|
||||||
|
led.Fill = on ? AccentRed : (Brush)new BrushConverter().ConvertFromString("#3B3B55")!;
|
||||||
|
card.BorderBrush = on ? AccentRed : (Brush)new BrushConverter().ConvertFromString("#3B3B55")!;
|
||||||
|
card.BorderThickness = new Thickness(on ? 2 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static string ErrorName(byte e) => e switch { 1 => "FAN 에러", 2 => "기타 에러", _ => "정상" };
|
||||||
|
|
||||||
|
void UpdateFanButtons()
|
||||||
|
{
|
||||||
|
foreach (var child in FanPanel.Children)
|
||||||
|
{
|
||||||
|
if (child is Button btn && btn.Tag is string tag && byte.TryParse(tag, out var f))
|
||||||
|
{
|
||||||
|
bool active = f == _hood.FanStage;
|
||||||
|
btn.Background = active ? AccentCyan : CardBg;
|
||||||
|
btn.Foreground = active ? Brushes.Black : TextPrimary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 마스터 패킷 수신 ==========
|
||||||
|
void OnMasterPacket(byte mode, byte fan, byte en, byte run)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
_rxCount++;
|
||||||
|
txtRxCount.Text = $"수신: {_rxCount}";
|
||||||
|
txtRxMode.Text = $"{mode} ({ModeName(mode)})";
|
||||||
|
txtRxFan.Text = fan == 0 ? "0 (OFF)" : $"{fan}단";
|
||||||
|
txtRxEn.Text = en != 0 ? "Enable" : "Disable";
|
||||||
|
txtRxRun.Text = run != 0 ? "운전중" : "정지";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 응답 송신 ==========
|
||||||
|
void OnResponseSent(byte[] tx)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
txtTxFan.Text = tx[3] == 0 ? "0 (OFF)" : $"{tx[3]}단";
|
||||||
|
txtTxLight.Text = tx[4] != 0 ? "ON" : "OFF";
|
||||||
|
txtTxCmd.Text = tx[6] != 0 ? "1 (켜짐)" : "0 (꺼짐)";
|
||||||
|
txtTxError.Text = $"{tx[7]} ({ErrorName(tx[7])})";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static string ModeName(byte m) => m switch
|
||||||
|
{
|
||||||
|
0 => "OFF", 1 => "환기", 2 => "자동", 3 => "공청", 4 => "바이패스", 5 => "기타", _ => "?"
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== 로그 ==========
|
||||||
|
void OnLog(string msg)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
txtLog.AppendText(msg + Environment.NewLine);
|
||||||
|
if (txtLog.LineCount > 500)
|
||||||
|
{
|
||||||
|
var lines = txtLog.Text.Split(Environment.NewLine);
|
||||||
|
txtLog.Text = string.Join(Environment.NewLine, lines[^300..]);
|
||||||
|
}
|
||||||
|
txtLog.ScrollToEnd();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearLog_Click(object s, RoutedEventArgs e) => txtLog.Clear();
|
||||||
|
|
||||||
|
void SaveLog_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var dlg = new SaveFileDialog
|
||||||
|
{
|
||||||
|
Filter = "텍스트 파일 (*.txt)|*.txt",
|
||||||
|
FileName = $"HoodSimLog_{DateTime.Now:yyyyMMdd_HHmmss}.txt"
|
||||||
|
};
|
||||||
|
if (dlg.ShowDialog() == true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string h = $"========================================\r\n 후드 시뮬레이터 통신 로그\r\n 저장 일시: {DateTime.Now:yyyy-MM-dd HH:mm:ss}\r\n========================================\r\n\r\n";
|
||||||
|
File.WriteAllText(dlg.FileName, h + txtLog.Text);
|
||||||
|
MessageBox.Show($"저장 완료: {dlg.FileName}");
|
||||||
|
}
|
||||||
|
catch (Exception ex) { MessageBox.Show($"저장 실패: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnConnectionChanged(bool connected)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
if (connected)
|
||||||
|
{
|
||||||
|
statusLed.Fill = AccentGreen;
|
||||||
|
txtStatus.Text = "연결됨";
|
||||||
|
btnStart.IsEnabled = true;
|
||||||
|
btnConnect.Content = "연결 해제";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
statusLed.Fill = AccentRed;
|
||||||
|
txtStatus.Text = "미연결";
|
||||||
|
btnStart.IsEnabled = false;
|
||||||
|
btnStop.IsEnabled = false;
|
||||||
|
btnConnect.Content = "연결";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# HoodSimulator 사양서
|
||||||
|
|
||||||
|
후드메인(Hood) 장치를 모사하는 C# WPF 시뮬레이터. ERVSimulator(전열교환기, Master)와 RS485로
|
||||||
|
통신하여 후드 상태를 응답한다. 스타일은 DiffuserSimulator와 동일(Tokyo Night 다크 테마).
|
||||||
|
|
||||||
|
## 1. 통신
|
||||||
|
- 프로토콜 : Protocol/HOOD/주신전자_protocol_hood_전열교환기_Rev1.3_20241125.xlsx
|
||||||
|
- 역할 : Slave (마스터 폴 수신 → 현재 후드 상태로 응답)
|
||||||
|
- 포트 : 115200 8N1, 9바이트 고정, 폴링주기 100~500ms, 응답 50ms 이내
|
||||||
|
- 체크섬(CS) : Preamble~CS직전(byte0~7) 전체 XOR
|
||||||
|
|
||||||
|
### 프레임 구조
|
||||||
|
- Master(전열교환기) → Slave(후드) : `AA 21 ID MODE FAN 연동EN 연동운전중 ERROR CS`
|
||||||
|
- Slave(후드) → Master(전열교환기) : `AA 11 ID FANSTATUS LIGHTSTATUS 00 연동CMD ERROR CS`
|
||||||
|
- Preamble 0xAA / M·S 0x21(Master)·0x11(Slave) / ID 0x01 고정
|
||||||
|
|
||||||
|
## 2. UI 기능
|
||||||
|
|
||||||
|
### 통신포트 설정
|
||||||
|
- COM 포트 선택 + 새로고침(⟳)
|
||||||
|
- 연결 / 연결 해제, 통신 시작(시작) / 통신 중지(중지)
|
||||||
|
- 연결 상태 LED(녹색 연결됨 / 빨강 미연결)
|
||||||
|
|
||||||
|
### 전원 on / off
|
||||||
|
- 전원 ON → 풍량 자동 1단
|
||||||
|
- 전원 OFF → 풍량 0
|
||||||
|
|
||||||
|
### 풍량 0(꺼짐) / 1 / 2 / 3 / 4 / 5
|
||||||
|
- 버튼 선택, 선택 단수 강조
|
||||||
|
|
||||||
|
### 조명 on / off
|
||||||
|
|
||||||
|
### 에러코드 (체크 선택하여 발생)
|
||||||
|
- FAN 에러(ERROR=1) / 기타 에러(ERROR=2) 토글 카드 (LED 표시)
|
||||||
|
- 둘 다 선택 시 FAN 에러(1) 우선 송신
|
||||||
|
- **에러 발생 시 전원 OFF / 풍량 0 / 조명 OFF로 강제 전환** 후 상태값 전송
|
||||||
|
|
||||||
|
### 통신 상태 표시
|
||||||
|
- 마스터 수신 명령 : MODE, 전열교환기 FAN, 연동 Enable, 연동 운전중
|
||||||
|
- 후드 응답 송신 : 후드 FAN STATUS, LIGHT STATUS, 연동 CMD, ERROR
|
||||||
|
- 수신 카운트
|
||||||
|
|
||||||
|
### 통신 로그
|
||||||
|
- 송수신 패킷 hex 로그, 저장 / 지우기
|
||||||
|
|
||||||
|
## 3. 응답 상태값 산출 규칙
|
||||||
|
| 응답 필드 | 값 |
|
||||||
|
|---|---|
|
||||||
|
| FAN STATUS (byte3) | 전원 ON 시 풍량 단수(0~5), OFF면 0 |
|
||||||
|
| LIGHT STATUS (byte4) | 전원 ON & 조명 ON → 1, 아니면 0 |
|
||||||
|
| 연동 CMD (byte6) | 전원 ON → 1(켜짐), OFF → 0(꺼짐) |
|
||||||
|
| ERROR (byte7) | 0 정상 / 1 FAN 에러 / 2 기타 에러 |
|
||||||
|
|
||||||
|
## 4. ERVSimulator 연동
|
||||||
|
- ERVSimulator는 Master로서 후드를 200ms 주기 폴 → 응답 수신
|
||||||
|
- ERV 측 "후드연동" 버튼 : 미연결(기본색) / 통신중 정상(녹색) / 통신중 에러(빨강 + 에러명)
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<Application x:Class="RJ2RoomConSimulator.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
StartupUri="MainWindow.xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
<Color x:Key="PrimaryBg">#1E1E2E</Color>
|
||||||
|
<Color x:Key="SecondaryBg">#2B2B3D</Color>
|
||||||
|
<Color x:Key="CardBg">#313147</Color>
|
||||||
|
<Color x:Key="AccentBlue">#7AA2F7</Color>
|
||||||
|
<Color x:Key="AccentGreen">#9ECE6A</Color>
|
||||||
|
<Color x:Key="AccentRed">#F7768E</Color>
|
||||||
|
<Color x:Key="AccentYellow">#E0AF68</Color>
|
||||||
|
<Color x:Key="AccentCyan">#7DCFFF</Color>
|
||||||
|
<Color x:Key="AccentPurple">#BB9AF7</Color>
|
||||||
|
<Color x:Key="TextPrimary">#C0CAF5</Color>
|
||||||
|
<Color x:Key="TextSecondary">#565F89</Color>
|
||||||
|
<Color x:Key="BorderColor">#3B3B55</Color>
|
||||||
|
|
||||||
|
<SolidColorBrush x:Key="PrimaryBgBrush" Color="{StaticResource PrimaryBg}"/>
|
||||||
|
<SolidColorBrush x:Key="SecondaryBgBrush" Color="{StaticResource SecondaryBg}"/>
|
||||||
|
<SolidColorBrush x:Key="CardBgBrush" Color="{StaticResource CardBg}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentBlueBrush" Color="{StaticResource AccentBlue}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentGreenBrush" Color="{StaticResource AccentGreen}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentRedBrush" Color="{StaticResource AccentRed}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentYellowBrush" Color="{StaticResource AccentYellow}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentCyanBrush" Color="{StaticResource AccentCyan}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentPurpleBrush" Color="{StaticResource AccentPurple}"/>
|
||||||
|
<SolidColorBrush x:Key="TextPrimaryBrush" Color="{StaticResource TextPrimary}"/>
|
||||||
|
<SolidColorBrush x:Key="TextSecondaryBrush" Color="{StaticResource TextSecondary}"/>
|
||||||
|
<SolidColorBrush x:Key="BorderBrush" Color="{StaticResource BorderColor}"/>
|
||||||
|
|
||||||
|
<Style x:Key="ModernButton" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="{StaticResource AccentBlueBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Padding" Value="18,8"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="border" Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="6" Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="border" Property="Opacity" Value="0.85"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="border" Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ToggleSwitch" TargetType="ToggleButton">
|
||||||
|
<Setter Property="Width" Value="56"/>
|
||||||
|
<Setter Property="Height" Value="28"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ToggleButton">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="track" CornerRadius="14" Background="#3B3B55" Width="56" Height="28"/>
|
||||||
|
<Border x:Name="thumb" CornerRadius="11" Background="#565F89" Width="22" Height="22"
|
||||||
|
HorizontalAlignment="Left" Margin="3,0,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="track" Property="Background" Value="{StaticResource AccentGreenBrush}"/>
|
||||||
|
<Setter TargetName="thumb" Property="Background" Value="White"/>
|
||||||
|
<Setter TargetName="thumb" Property="HorizontalAlignment" Value="Right"/>
|
||||||
|
<Setter TargetName="thumb" Property="Margin" Value="0,0,3,0"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModernComboBox" TargetType="ComboBox">
|
||||||
|
<Setter Property="Background" Value="{StaticResource CardBgBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="8,5"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="ItemContainerStyle">
|
||||||
|
<Setter.Value>
|
||||||
|
<Style TargetType="ComboBoxItem">
|
||||||
|
<Setter Property="Foreground" Value="Black"/>
|
||||||
|
</Style>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModernTextBox" TargetType="TextBox">
|
||||||
|
<Setter Property="Background" Value="{StaticResource CardBgBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="6,4"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
</Style>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace RJ2RoomConSimulator
|
||||||
|
{
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<Window x:Class="RJ2RoomConSimulator.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="RJ2-232 룸콘 시뮬레이터 - RoomCon Simulator"
|
||||||
|
Width="620" Height="860"
|
||||||
|
MinWidth="560" MinHeight="740"
|
||||||
|
Background="{StaticResource PrimaryBgBrush}"
|
||||||
|
WindowStartupLocation="CenterScreen">
|
||||||
|
|
||||||
|
<Grid Margin="12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/> <!-- 0: 연결 -->
|
||||||
|
<RowDefinition Height="Auto"/> <!-- 1: 룸콘 제어 -->
|
||||||
|
<RowDefinition Height="Auto"/> <!-- 2: ERV 응답 -->
|
||||||
|
<RowDefinition Height="*"/> <!-- 3: 로그 -->
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Row 0: 연결 설정 (2줄) -->
|
||||||
|
<Border Grid.Row="0" Background="{StaticResource SecondaryBgBrush}" CornerRadius="10" Padding="14,10" Margin="0,0,0,8">
|
||||||
|
<StackPanel>
|
||||||
|
<DockPanel Margin="0,0,0,8">
|
||||||
|
<TextBlock Text="RJ2-232 룸콘 시뮬레이터" FontSize="17" FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource AccentCyanBrush}" VerticalAlignment="Center"/>
|
||||||
|
<StackPanel DockPanel.Dock="Right" VerticalAlignment="Center" HorizontalAlignment="Right">
|
||||||
|
<TextBlock Text="만든이 : 전경선" Foreground="{StaticResource TextSecondaryBrush}" FontSize="10" TextAlignment="Right"/>
|
||||||
|
<TextBlock Text="수정일 : 2026.06.07" Foreground="{StaticResource TextSecondaryBrush}" FontSize="10" TextAlignment="Right"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<ComboBox x:Name="cmbPort" Width="92" Style="{StaticResource ModernComboBox}" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
|
<Button Content="⟳" Width="28" Height="28" FontSize="13" Style="{StaticResource ModernButton}" Click="RefreshPorts_Click"
|
||||||
|
Background="{StaticResource CardBgBrush}" Margin="0,0,8,0" Padding="0"/>
|
||||||
|
<TextBlock Text="9600 8N1" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" Margin="0,0,10,0" FontSize="11"/>
|
||||||
|
<Ellipse x:Name="statusLed" Width="10" Height="10" Fill="#F7768E" Margin="0,0,5,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="txtStatus" Text="미연결" Foreground="{StaticResource TextSecondaryBrush}" FontSize="12" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||||
|
<Button x:Name="btnConnect" Content="연결" Style="{StaticResource ModernButton}" Click="Connect_Click" Margin="0,0,4,0" Padding="12,6"/>
|
||||||
|
<Button x:Name="btnStart" Content="폴링 시작" Style="{StaticResource ModernButton}" Background="{StaticResource AccentGreenBrush}" Click="Start_Click" IsEnabled="False" Margin="0,0,4,0" Padding="12,6"/>
|
||||||
|
<Button x:Name="btnStop" Content="중지" Style="{StaticResource ModernButton}" Background="{StaticResource AccentRedBrush}" Click="Stop_Click" IsEnabled="False" Padding="12,6"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Row 1: 룸콘 제어 -->
|
||||||
|
<Border Grid.Row="1" Background="{StaticResource SecondaryBgBrush}" CornerRadius="10" Padding="16" Margin="0,0,0,8">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="룸콘 제어" FontSize="14" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,0,14"/>
|
||||||
|
|
||||||
|
<!-- 전원 + 후드 연동(표시 전용 : ERV 후드연동 상태 수신, 힘펠 V3.7 HOOD_INFO 0x0A) -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
|
||||||
|
<TextBlock Text="전원" Width="64" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
<ToggleButton x:Name="tglPower" Style="{StaticResource ToggleSwitch}" Click="Power_Click" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="txtPower" Text="OFF" FontSize="13" FontWeight="Bold" Margin="12,0,0,0" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
|
||||||
|
<Border Width="1" Background="{StaticResource BorderBrush}" Margin="18,2,16,2"/>
|
||||||
|
<TextBlock Text="후드 연동" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
|
<Ellipse x:Name="HoodLinkLed" Width="11" Height="11" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||||
|
<TextBlock x:Name="txtHoodLink" Text="OFF" FontSize="13" FontWeight="Bold" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 모드 -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
||||||
|
<TextBlock Text="모드" Width="64" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
<StackPanel Orientation="Horizontal" x:Name="ModePanel">
|
||||||
|
<Button Content="환기" Tag="0" Click="Mode_Click" Style="{StaticResource ModernButton}" Margin="0,0,6,0" MinWidth="78" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="자동" Tag="1" Click="Mode_Click" Style="{StaticResource ModernButton}" Margin="0,0,6,0" MinWidth="78" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="공기청정" Tag="3" Click="Mode_Click" Style="{StaticResource ModernButton}" Margin="0,0,6,0" MinWidth="78" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="바이패스" Tag="2" Click="Mode_Click" Style="{StaticResource ModernButton}" MinWidth="78" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 풍량 -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
||||||
|
<TextBlock Text="풍량" Width="64" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
<StackPanel Orientation="Horizontal" x:Name="FanPanel">
|
||||||
|
<Button Content="0" Tag="0" Click="Fan_Click" Style="{StaticResource ModernButton}" Margin="0,0,5,0" MinWidth="44" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="1" Tag="1" Click="Fan_Click" Style="{StaticResource ModernButton}" Margin="0,0,5,0" MinWidth="44" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="2" Tag="2" Click="Fan_Click" Style="{StaticResource ModernButton}" Margin="0,0,5,0" MinWidth="44" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="3" Tag="3" Click="Fan_Click" Style="{StaticResource ModernButton}" Margin="0,0,5,0" MinWidth="44" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
<Button Content="4" Tag="4" Click="Fan_Click" Style="{StaticResource ModernButton}" MinWidth="44" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Text="(자동:변경불가 · 바이패스:1단)" FontSize="10" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" Margin="10,0,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 예약 -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
||||||
|
<TextBlock Text="예약(꺼짐)" Width="64" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
<ComboBox x:Name="ReserveCombo" Width="90" Style="{StaticResource ModernComboBox}" VerticalAlignment="Center" SelectionChanged="Reserve_Changed" SelectedIndex="0">
|
||||||
|
<ComboBoxItem Content="해제"/>
|
||||||
|
<ComboBoxItem Content="1시간"/>
|
||||||
|
<ComboBoxItem Content="2시간"/>
|
||||||
|
<ComboBoxItem Content="3시간"/>
|
||||||
|
<ComboBoxItem Content="4시간"/>
|
||||||
|
<ComboBoxItem Content="5시간"/>
|
||||||
|
<ComboBoxItem Content="6시간"/>
|
||||||
|
<ComboBoxItem Content="7시간"/>
|
||||||
|
<ComboBoxItem Content="8시간"/>
|
||||||
|
</ComboBox>
|
||||||
|
<TextBlock Text="(모드 전환 시 해제)" FontSize="10" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" Margin="10,0,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- id / vsp -->
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="ID" Width="64" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
<TextBox x:Name="txtDeviceId" Text="1" Width="50" Style="{StaticResource ModernTextBox}" TextAlignment="Center" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||||
|
<Button Content="적용" Style="{StaticResource ModernButton}" Click="ApplyId_Click" Padding="12,5" Background="{StaticResource CardBgBrush}" Margin="0,0,18,0"/>
|
||||||
|
<Button Content="VSP 설정 ▸" Style="{StaticResource ModernButton}" Click="OpenVsp_Click" Padding="12,5" Background="{StaticResource CardBgBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Row 2: ERV 응답 상태 -->
|
||||||
|
<Border Grid.Row="2" Background="{StaticResource SecondaryBgBrush}" CornerRadius="10" Padding="16" Margin="0,0,0,8">
|
||||||
|
<StackPanel>
|
||||||
|
<DockPanel Margin="0,0,0,10">
|
||||||
|
<TextBlock Text="ERV 응답 상태" FontSize="14" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<TextBlock x:Name="txtRxCount" DockPanel.Dock="Right" Text="수신: 0" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
</DockPanel>
|
||||||
|
<Border Background="{StaticResource CardBgBrush}" CornerRadius="6" Padding="12,8" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1">
|
||||||
|
<StackPanel>
|
||||||
|
<DockPanel Margin="0,0,0,3"><TextBlock Text="운전모드" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}"/><TextBlock x:Name="txtErvMode" DockPanel.Dock="Right" Text="-" FontSize="12" FontWeight="Bold" Foreground="{StaticResource AccentCyanBrush}"/></DockPanel>
|
||||||
|
<DockPanel Margin="0,0,0,3"><TextBlock Text="풍량" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}"/><TextBlock x:Name="txtErvFan" DockPanel.Dock="Right" Text="-" FontSize="12" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}"/></DockPanel>
|
||||||
|
<DockPanel Margin="0,0,0,3"><TextBlock Text="에러코드" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}"/><TextBlock x:Name="txtErvErr" DockPanel.Dock="Right" Text="-" FontSize="12" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}"/></DockPanel>
|
||||||
|
<DockPanel Margin="0,0,0,3"><TextBlock Text="실내온도" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}"/><TextBlock x:Name="txtErvIn" DockPanel.Dock="Right" Text="-" FontSize="12" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}"/></DockPanel>
|
||||||
|
<DockPanel><TextBlock Text="외기온도" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}"/><TextBlock x:Name="txtErvOut" DockPanel.Dock="Right" Text="-" FontSize="12" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}"/></DockPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 알람 (필터 청소/교환) -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,10,0,4">
|
||||||
|
<TextBlock Text="알람" Width="40" FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
<Border CornerRadius="4" Padding="6,3" Margin="0,0,4,0" Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1">
|
||||||
|
<StackPanel Orientation="Horizontal"><Ellipse x:Name="AlFClean" Width="9" Height="9" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,4,0"/><TextBlock Text="필터청소" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource TextPrimaryBrush}"/></StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border CornerRadius="4" Padding="6,3" Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1">
|
||||||
|
<StackPanel Orientation="Horizontal"><Ellipse x:Name="AlFChange" Width="9" Height="9" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,4,0"/><TextBlock Text="필터교환" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource TextPrimaryBrush}"/></StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 에러 (ERV ErrorCode) -->
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="에러" Width="40" FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"/>
|
||||||
|
<Border CornerRadius="4" Padding="6,3" Margin="0,0,4,0" Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" ToolTip="온도센서">
|
||||||
|
<StackPanel Orientation="Horizontal"><Ellipse x:Name="ErE02" Width="9" Height="9" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,4,0"/><TextBlock Text="E02" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource TextPrimaryBrush}"/></StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border CornerRadius="4" Padding="6,3" Margin="0,0,4,0" Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" ToolTip="급기(SA) 팬">
|
||||||
|
<StackPanel Orientation="Horizontal"><Ellipse x:Name="ErE09" Width="9" Height="9" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,4,0"/><TextBlock Text="E09" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource TextPrimaryBrush}"/></StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border CornerRadius="4" Padding="6,3" Margin="0,0,4,0" Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" ToolTip="배기(EA) 팬">
|
||||||
|
<StackPanel Orientation="Horizontal"><Ellipse x:Name="ErE10" Width="9" Height="9" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,4,0"/><TextBlock Text="E10" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource TextPrimaryBrush}"/></StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border CornerRadius="4" Padding="6,3" Margin="0,0,4,0" Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" ToolTip="장비보호모드">
|
||||||
|
<StackPanel Orientation="Horizontal"><Ellipse x:Name="ErCold" Width="9" Height="9" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,4,0"/><TextBlock Text="COLD" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource TextPrimaryBrush}"/></StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border CornerRadius="4" Padding="6,3" Background="{StaticResource CardBgBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" ToolTip="내부 통신">
|
||||||
|
<StackPanel Orientation="Horizontal"><Ellipse x:Name="ErE07" Width="9" Height="9" Fill="#3B3B55" VerticalAlignment="Center" Margin="0,0,4,0"/><TextBlock Text="E07" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Foreground="{StaticResource TextPrimaryBrush}"/></StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Row 3: 통신 로그 -->
|
||||||
|
<Border Grid.Row="3" Background="{StaticResource SecondaryBgBrush}" CornerRadius="10" Padding="12">
|
||||||
|
<DockPanel>
|
||||||
|
<Grid DockPanel.Dock="Top" Margin="0,0,0,5">
|
||||||
|
<TextBlock Text="통신 로그" FontSize="12" FontWeight="Bold" Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button Content="저장" Style="{StaticResource ModernButton}" Background="{StaticResource AccentBlueBrush}" Padding="10,3" FontSize="11" Click="SaveLog_Click" Margin="0,0,6,0"/>
|
||||||
|
<Button Content="지우기" Style="{StaticResource ModernButton}" Background="{StaticResource CardBgBrush}" Padding="10,3" FontSize="11" Click="ClearLog_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<TextBox x:Name="txtLog" IsReadOnly="True" Background="{StaticResource CardBgBrush}"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}" BorderBrush="{StaticResource BorderBrush}"
|
||||||
|
BorderThickness="1" FontFamily="Consolas" FontSize="10"
|
||||||
|
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
|
||||||
|
TextWrapping="NoWrap" Padding="6"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace RJ2RoomConSimulator
|
||||||
|
{
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
readonly RoomConProtocol _proto = new();
|
||||||
|
readonly DispatcherTimer _pollTimer;
|
||||||
|
int _rxCount;
|
||||||
|
bool _suppressReserve;
|
||||||
|
VspWindow? _vspWin;
|
||||||
|
|
||||||
|
static readonly Brush AccentCyan = Br("#7DCFFF");
|
||||||
|
static readonly Brush AccentGreen = Br("#9ECE6A");
|
||||||
|
static readonly Brush AccentRed = Br("#F7768E");
|
||||||
|
static readonly Brush AccentYellow = Br("#E0AF68");
|
||||||
|
static readonly Brush CardBg = Br("#313147");
|
||||||
|
static readonly Brush TextPrimary = Br("#C0CAF5");
|
||||||
|
static readonly Brush TextSecondary = Br("#565F89");
|
||||||
|
static readonly Brush LedOff = Br("#3B3B55");
|
||||||
|
static Brush Br(string h) => (Brush)new BrushConverter().ConvertFromString(h)!;
|
||||||
|
|
||||||
|
static void SetChip(System.Windows.Shapes.Ellipse led, bool on, Brush onColor) => led.Fill = on ? onColor : LedOff;
|
||||||
|
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_proto.Log += OnLog;
|
||||||
|
_proto.ConnectionChanged += OnConnectionChanged;
|
||||||
|
_proto.ResponseReceived += OnResponse;
|
||||||
|
|
||||||
|
_pollTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
|
||||||
|
_pollTimer.Tick += (_, _) => { if (_proto.IsConnected) _proto.SendNormal(); };
|
||||||
|
|
||||||
|
RefreshPorts();
|
||||||
|
UpdateModeButtons();
|
||||||
|
UpdateFanButtons();
|
||||||
|
Closed += (_, _) => _proto.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 연결 =================
|
||||||
|
void RefreshPorts()
|
||||||
|
{
|
||||||
|
cmbPort.Items.Clear();
|
||||||
|
foreach (var p in _proto.GetAvailablePorts()) cmbPort.Items.Add(p);
|
||||||
|
if (cmbPort.Items.Count > 0) cmbPort.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
void RefreshPorts_Click(object s, RoutedEventArgs e) => RefreshPorts();
|
||||||
|
|
||||||
|
void Connect_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_proto.IsConnected) { _proto.Disconnect(); btnConnect.Content = "연결"; }
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (cmbPort.SelectedItem == null) { MessageBox.Show("COM 포트를 선택하세요."); return; }
|
||||||
|
if (_proto.Connect(cmbPort.SelectedItem.ToString()!)) btnConnect.Content = "연결 해제";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Start_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_proto.IsConnected) return;
|
||||||
|
_pollTimer.Start();
|
||||||
|
btnStart.IsEnabled = false; btnStop.IsEnabled = true;
|
||||||
|
OnLog("[폴링 시작] 상태 조회(NORMAL) 주기 송신");
|
||||||
|
}
|
||||||
|
void Stop_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_pollTimer.Stop();
|
||||||
|
btnStart.IsEnabled = _proto.IsConnected; btnStop.IsEnabled = false;
|
||||||
|
OnLog("[폴링 중지]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 룸콘 제어 =================
|
||||||
|
void Power_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_proto.PowerOn = tglPower.IsChecked == true;
|
||||||
|
if (_proto.PowerOn) { _proto.RunMode = 0; _proto.FanMode = 1; } // ON → 환기 1단
|
||||||
|
else _proto.FanMode = 0; // OFF → 풍량 0
|
||||||
|
txtPower.Text = _proto.PowerOn ? "ON" : "OFF";
|
||||||
|
txtPower.Foreground = _proto.PowerOn ? AccentGreen : TextSecondary;
|
||||||
|
UpdateModeButtons(); UpdateFanButtons();
|
||||||
|
_proto.SendEvent();
|
||||||
|
OnLog($"[제어] 전원 → {(_proto.PowerOn ? "ON (환기 1단)" : "OFF")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Mode_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (s is not Button b || b.Tag is not string tag || !byte.TryParse(tag, out var mode)) return;
|
||||||
|
_proto.PowerOn = true;
|
||||||
|
_proto.RunMode = mode;
|
||||||
|
// 운전모드 전환 시 풍량 1단 (자동 제외). 바이패스는 1단 고정, 공청/환기도 전환 기본 1단.
|
||||||
|
// (ERV/펌웨어는 룸컨이 보낸 fan을 그대로 따르므로 마스터인 룸컨이 1단을 보내야 동기화됨)
|
||||||
|
if (mode != 1) _proto.FanMode = 1;
|
||||||
|
// 모드 전환 시 예약 해제
|
||||||
|
ClearReserve();
|
||||||
|
tglPower.IsChecked = true; txtPower.Text = "ON"; txtPower.Foreground = AccentGreen;
|
||||||
|
UpdateModeButtons(); UpdateFanButtons();
|
||||||
|
_proto.SendEvent();
|
||||||
|
OnLog($"[제어] 모드 → {ModeName(mode)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Fan_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (s is not Button b || b.Tag is not string tag || !byte.TryParse(tag, out var sp)) return;
|
||||||
|
if (_proto.RunMode == 1) { OnLog("자동모드에서는 풍량 변경 불가"); return; } // 자동
|
||||||
|
if (_proto.RunMode == 2 && sp > 1) { OnLog("바이패스는 1단 고정"); return; } // 바이패스
|
||||||
|
_proto.FanMode = sp;
|
||||||
|
_proto.PowerOn = sp > 0 || _proto.PowerOn;
|
||||||
|
if (sp > 0) { tglPower.IsChecked = true; txtPower.Text = "ON"; txtPower.Foreground = AccentGreen; }
|
||||||
|
UpdateFanButtons();
|
||||||
|
_proto.SendEvent();
|
||||||
|
OnLog($"[제어] 풍량 → {sp}단");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Reserve_Changed(object s, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_suppressReserve || !IsLoaded || ReserveCombo.SelectedIndex < 0) return;
|
||||||
|
_proto.ReserveHours = ReserveCombo.SelectedIndex; // 0=해제
|
||||||
|
_proto.SendEvent();
|
||||||
|
OnLog(_proto.ReserveHours == 0 ? "[제어] 예약 해제" : $"[제어] {_proto.ReserveHours}시간 후 꺼짐 예약");
|
||||||
|
}
|
||||||
|
void ClearReserve()
|
||||||
|
{
|
||||||
|
_proto.ReserveHours = 0;
|
||||||
|
if (ReserveCombo.SelectedIndex != 0) { _suppressReserve = true; ReserveCombo.SelectedIndex = 0; _suppressReserve = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyId_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (byte.TryParse(txtDeviceId.Text, out var id)) { _proto.DeviceId = id; OnLog($"[설정] ID → {id}"); }
|
||||||
|
else OnLog("ID는 0~255 숫자만 가능");
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenVsp_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_vspWin == null)
|
||||||
|
{
|
||||||
|
_vspWin = new VspWindow(_proto) { Owner = this };
|
||||||
|
_vspWin.Closed += (_, _) => _vspWin = null;
|
||||||
|
_vspWin.Show();
|
||||||
|
}
|
||||||
|
else _vspWin.Activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 버튼 강조 =================
|
||||||
|
void UpdateModeButtons()
|
||||||
|
{
|
||||||
|
foreach (var child in ModePanel.Children)
|
||||||
|
if (child is Button btn && btn.Tag is string t && byte.TryParse(t, out var m))
|
||||||
|
{
|
||||||
|
bool active = _proto.PowerOn && m == _proto.RunMode;
|
||||||
|
btn.Background = active ? AccentCyan : CardBg;
|
||||||
|
btn.Foreground = active ? Brushes.Black : TextPrimary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void UpdateFanButtons()
|
||||||
|
{
|
||||||
|
foreach (var child in FanPanel.Children)
|
||||||
|
if (child is Button btn && btn.Tag is string t && byte.TryParse(t, out var sp))
|
||||||
|
{
|
||||||
|
bool active = sp == _proto.FanMode;
|
||||||
|
btn.Background = active ? AccentCyan : CardBg;
|
||||||
|
btn.Foreground = active ? Brushes.Black : TextPrimary;
|
||||||
|
// 자동:전 단 비활성 / 바이패스:2~4 비활성
|
||||||
|
btn.IsEnabled = !(_proto.RunMode == 1) && !(_proto.RunMode == 2 && sp > 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= ERV 응답 =================
|
||||||
|
void OnResponse(byte cmd)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
_rxCount++;
|
||||||
|
txtRxCount.Text = $"수신: {_rxCount}";
|
||||||
|
txtErvMode.Text = $"{_proto.ErvRunMode} ({ModeName(_proto.ErvRunMode)})";
|
||||||
|
txtErvFan.Text = _proto.ErvFanMode == 0 ? "0 (OFF)" : $"{_proto.ErvFanMode}단";
|
||||||
|
txtErvErr.Text = $"0x{_proto.ErvError:X2}";
|
||||||
|
txtErvErr.Foreground = _proto.ErvError != 0 ? AccentRed : TextPrimary;
|
||||||
|
|
||||||
|
// 알람/에러 비트 디코드 (ERV ErrorCode 비트맵)
|
||||||
|
byte ec = _proto.ErvError;
|
||||||
|
SetChip(AlFClean, (ec & 0x01) != 0, AccentYellow); // 필터 청소
|
||||||
|
SetChip(AlFChange, (ec & 0x02) != 0, AccentYellow); // 필터 교환
|
||||||
|
SetChip(ErE02, (ec & 0x08) != 0, AccentRed); // 온도센서
|
||||||
|
SetChip(ErE09, (ec & 0x80) != 0, AccentRed); // 급기팬
|
||||||
|
SetChip(ErE10, (ec & 0x20) != 0, AccentRed); // 배기팬
|
||||||
|
SetChip(ErCold,(ec & 0x10) != 0, AccentCyan); // 장비보호
|
||||||
|
SetChip(ErE07, (ec & 0x40) != 0, AccentRed); // 내부통신
|
||||||
|
txtErvIn.Text = _proto.ErvInTemp == 100 ? "센서없음" : $"{_proto.ErvInTemp}℃";
|
||||||
|
txtErvOut.Text = _proto.ErvOutTemp == 100 ? "센서없음" : $"{_proto.ErvOutTemp}℃";
|
||||||
|
|
||||||
|
// 후드 연동 표시 (ERV HOOD_INFO 0x0A 수신값) : 후드 ON 오면 연동 ON, ERV OFF면 OFF
|
||||||
|
SetChip(HoodLinkLed, _proto.HoodLinked, AccentGreen);
|
||||||
|
txtHoodLink.Text = _proto.HoodLinked ? "ON" : "OFF";
|
||||||
|
txtHoodLink.Foreground = _proto.HoodLinked ? AccentGreen : TextSecondary;
|
||||||
|
|
||||||
|
// 자동모드: 풍량은 ERV가 자동 결정 → 룸콘 표시를 ERV 보고값으로 동기화
|
||||||
|
if (_proto.RunMode == 1 && _proto.FanMode != _proto.ErvFanMode)
|
||||||
|
{
|
||||||
|
_proto.FanMode = _proto.ErvFanMode;
|
||||||
|
UpdateFanButtons();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static string ModeName(int m) => m switch
|
||||||
|
{
|
||||||
|
0 => "환기", 1 => "자동", 2 => "바이패스", 3 => "공기청정", 4 => "팬테스트", 10 => "OFF", _ => $"?{m}"
|
||||||
|
};
|
||||||
|
|
||||||
|
// ================= 로그 =================
|
||||||
|
void OnLog(string msg)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
txtLog.AppendText(msg + Environment.NewLine);
|
||||||
|
if (txtLog.LineCount > 500)
|
||||||
|
{
|
||||||
|
var lines = txtLog.Text.Split(Environment.NewLine);
|
||||||
|
txtLog.Text = string.Join(Environment.NewLine, lines[^300..]);
|
||||||
|
}
|
||||||
|
txtLog.ScrollToEnd();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
void ClearLog_Click(object s, RoutedEventArgs e) => txtLog.Clear();
|
||||||
|
void SaveLog_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var dlg = new SaveFileDialog { Filter = "텍스트 파일 (*.txt)|*.txt", FileName = $"RoomConSimLog_{DateTime.Now:yyyyMMdd_HHmmss}.txt" };
|
||||||
|
if (dlg.ShowDialog() == true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string h = $"========================================\r\n RJ2-232 룸콘 시뮬레이터 통신 로그\r\n 저장 일시: {DateTime.Now:yyyy-MM-dd HH:mm:ss}\r\n========================================\r\n\r\n";
|
||||||
|
File.WriteAllText(dlg.FileName, h + txtLog.Text);
|
||||||
|
MessageBox.Show($"저장 완료: {dlg.FileName}");
|
||||||
|
}
|
||||||
|
catch (Exception ex) { MessageBox.Show($"저장 실패: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnConnectionChanged(bool connected)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
statusLed.Fill = connected ? AccentGreen : AccentRed;
|
||||||
|
txtStatus.Text = connected ? "연결됨" : "미연결";
|
||||||
|
btnStart.IsEnabled = connected;
|
||||||
|
btnConnect.Content = connected ? "연결 해제" : "연결";
|
||||||
|
if (!connected) { _pollTimer.Stop(); btnStop.IsEnabled = false; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user