feat: 06-17 신규 작업본 반영 (개발사양서/기능검토/승인원/Source 등 추가)

.claude/ 제외(.gitignore 추가). 기존 초기커밋(5a96a69) 위에 신규·수정·이동분 커밋.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 07:54:58 +09:00
parent 5a96a696b1
commit 096111e983
529 changed files with 12439 additions and 1166 deletions
+32
View File
@@ -100,6 +100,38 @@
</Setter>
</Style>
<!-- 프리셋모드 선택 버튼(ECO/NORMAL/TURBO) — RadioButton 을 토글 버튼 모양으로 -->
<Style x:Key="PresetRadio" TargetType="RadioButton">
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="FontSize" Value="10"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
<Setter Property="Margin" Value="2,0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Border x:Name="border" Background="{StaticResource CardBgBrush}" CornerRadius="5"
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" Padding="10,4">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="border" Property="Background" Value="{StaticResource AccentBlueBrush}"/>
<Setter TargetName="border" Property="BorderBrush" Value="{StaticResource AccentBlueBrush}"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
<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="ModernTextBox" TargetType="TextBox">
<Setter Property="Background" Value="{StaticResource CardBgBrush}"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
+11 -4
View File
@@ -53,10 +53,17 @@
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 자동 변경"/>
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Margin="0,0,12,0">
<Button x:Name="btnAutoChange" Content="센서값 자동 변경" Style="{StaticResource ModernButton}"
Background="{StaticResource AccentBlueBrush}"
Padding="14,6" FontSize="11" Click="AutoChange_Click"
ToolTip="선택한 프리셋모드(ECO/NORMAL/TURBO) 밴드로 거실→방1~3 순서로 30초마다 오염레벨 0→1→2→3→4 자동 변경"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,4,0,0">
<RadioButton x:Name="RbAutoEco" Content="ECO" GroupName="AutoPreset" Style="{StaticResource PresetRadio}"/>
<RadioButton x:Name="RbAutoNorm" Content="NORMAL" GroupName="AutoPreset" Style="{StaticResource PresetRadio}" IsChecked="True"/>
<RadioButton x:Name="RbAutoTurbo" Content="TURBO" GroupName="AutoPreset" Style="{StaticResource PresetRadio}"/>
</StackPanel>
</StackPanel>
<TextBlock Text="폴링(ms)" Foreground="{StaticResource TextPrimaryBrush}"
VerticalAlignment="Center" Margin="0,0,6,0" FontSize="11" FontWeight="SemiBold"/>
+45 -6
View File
@@ -24,6 +24,7 @@ namespace DiffuserSimulator
new() { Interval = TimeSpan.FromSeconds(30) };
private int _autoStep; // 0..19 (room = step/5, level = step%5)
private bool _autoRunning;
private int _autoPreset = 1; // 자동변경 시 적용할 프리셋모드 (0 ECO/1 NORMAL/2 TURBO, 기본 NORMAL)
private static readonly string[] RoomNames = { "거실", "방 1", "방 2", "방 3", "방 4" };
private static readonly Color[] RoomColors =
@@ -40,12 +41,12 @@ namespace DiffuserSimulator
// (힘펠은 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[][] PreCO2 = { new[]{500,1150,1450,1800,2100}, new[]{400,950,1250,1550,1850}, new[]{350,850,1150,1450,1750}, 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[][] ThrCO2 = { new[]{1000,1300,1600,2000}, new[]{800,1100,1400,1700}, new[]{700,1000,1300,1600}, 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} };
@@ -75,6 +76,12 @@ namespace DiffuserSimulator
BuildRoomPanels();
RefreshPorts();
ApplySlaveUi(); // 슬레이브 전용 UI 상태(각도 readonly 등)
// 자동변경 프리셋모드 선택 버튼 (ECO/NORMAL/TURBO)
RbAutoEco.Checked += (s, e) => SetAutoPreset(0);
RbAutoNorm.Checked += (s, e) => SetAutoPreset(1);
RbAutoTurbo.Checked += (s, e) => SetAutoPreset(2);
_autoTimer.Tick += AutoTick;
Closed += (_, _) => { _autoTimer.Stop(); _slave.Dispose(); };
}
@@ -327,6 +334,14 @@ namespace DiffuserSimulator
_ui[i].RbTurbo.IsEnabled = !himpel;
}
// 자동변경 프리셋모드 선택 버튼 : DL=활성 / 힘펠=비활성
if (RbAutoEco != null)
{
RbAutoEco.IsEnabled = !himpel;
RbAutoNorm.IsEnabled = !himpel;
RbAutoTurbo.IsEnabled = !himpel;
}
// LED 디밍 : DL=활성 / 힘펠=비활성 — 거실(0)·방1~3(1~3)
for (int i = 0; i < 4; i++) SetLedDimming(i, enabled: !himpel);
@@ -598,22 +613,46 @@ namespace DiffuserSimulator
{
_autoTimer.Stop();
_autoRunning = false;
btnAutoChange.Content = "자동변경";
btnAutoChange.Content = "센서값 자동 변경";
OnLog("[자동변경] 중지");
return;
}
// 거실~방3(0~3) 활성화 (이미 켜져 있으면 무시) 후 전체 0(좋음)에서 시작
// 거실~방3(0~3) 활성화 (이미 켜져 있으면 무시) 후 선택 프리셋모드 밴드의 전체 0(좋음)에서 시작
for (int i = 0; i <= 3; i++)
if (_ui[i].ChkEnabled.IsChecked != true) _ui[i].ChkEnabled.IsChecked = true;
ApplyAutoPresetToRooms(); // 선택한 ECO/NORMAL/TURBO 밴드 적용
for (int r = 0; r <= 3; r++) ApplyPreset(r, 0);
_autoStep = 0;
_autoRunning = true;
btnAutoChange.Content = "자동변경 중지";
OnLog("[자동변경] 시작 — 전체 0에서 30초 대기 후 방1→방2→방3→거실 순 누적(0→4)");
btnAutoChange.Content = "자동 변경 중지";
OnLog($"[자동변경] 시작({AutoPresetName(_autoPreset)}) — 전체 0에서 30초 대기 후 방1→방2→방3→거실 순 누적(0→4)");
_autoTimer.Start(); // 즉시 적용하지 않음 → 초기 0 0 0 0 을 30초 유지 후 첫 변경
}
private static string AutoPresetName(int p) => p == 0 ? "ECO" : p == 2 ? "TURBO" : "NORMAL";
// 자동변경 프리셋모드 선택 → 실행 중이면 즉시 현재 방들에 새 밴드 재적용
private void SetAutoPreset(int preset)
{
_autoPreset = preset;
if (_autoRunning) ApplyAutoPresetToRooms();
}
// 선택한 자동변경 프리셋모드를 거실~방3(0~3) 패널에 반영 → 각 방의 센서 밴드(_roomPreset) 갱신
private void ApplyAutoPresetToRooms()
{
for (int i = 0; i <= 3; i++)
{
switch (_autoPreset)
{
case 0: _ui[i].RbEco.IsChecked = true; break;
case 1: _ui[i].RbNorm.IsChecked = true; break;
case 2: _ui[i].RbTurbo.IsChecked = true; break;
}
}
}
// 레벨 스윕(누적) : 매 30초 한 방씩 현재 레벨로 올림(방1→방2→방3→거실).
// 한 바퀴(4방) 다 올리면 레벨+1. 앞서 올린 방은 값 유지(누적). 전체 4 도달 후 0으로 리셋 반복.
private static readonly int[] AutoOrder = { 1, 2, 3, 0 }; // 방1, 방2, 방3, 거실
+20 -6
View File
@@ -162,6 +162,8 @@ namespace DiffuserSimulator
}
if (totalRead < 29) continue;
// CRC 바이트순서 = lo-first(표준 리틀엔디안). 펌웨어 CRC16()이 표준MODBUS의
// 바이트스왑값을 반환 + [27]=icrc>>8 배치 → 두 스왑 상쇄 → 와이어는 [27]=하위,[28]=상위.
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; }
@@ -192,12 +194,22 @@ namespace DiffuserSimulator
continue;
}
// Option B 패킷 구분 (250624 dump 패턴 일치):
// byte 5 = 0x01 → 명령 (Power ON, state 적용)
// byte 5 = 0x00 → 폴링 (상태 조회만, state 무변경)
// 폴링에서 byte 10/11/8 = 0 을 그대로 적용하면 댐퍼/LED 가 0 으로 reset 됨.
// 패킷 구분 : byte5 = Power. 하위비트 = 전원(0/1), 상위 0x80 = Control Cmd (dump.txt 스펙).
// 전원 OFF(하위비트 0) : 펌웨어가 댐퍼=0·LED=0 을 보냄 → 디퓨저 닫힘·소등 (전원우선)
// 전원 ON : 폴링/명령 모두 byte5 하위비트=1 → 댐퍼/LED 상태 적용
// (과거 isCommand=byte5!=0 만 적용 → 전원 OFF(byte5=0) 닫힘명령을 폴링으로 오인,
// 댐퍼 열림·LED 켜진 채 남던 문제 수정)
bool powerOff = ((rxBuf[5] & 0x7F) == 0x00);
bool isCommand = (rxBuf[5] != 0x00);
if (isCommand)
if (powerOff)
{
// 전원 OFF → 급기/배기 댐퍼 닫힘 + LED 소등 (수동닫기/수동LED 와 무관, 전원이 우선)
room.Power = 0;
room.DamperAngleSA = 0;
room.DamperAngleEA = 0;
room.LedBrightness = 0;
}
else if (isCommand)
{
// 마스터 명령 적용 — ID1 별로 해당 type 의 필드만 갱신.
// ID1=0x01 (SA): damper SA 만
@@ -347,6 +359,7 @@ namespace DiffuserSimulator
p[11] = room.DamperAngleEA;
// byte 12~26: 0 (RPM / Reset / 예약 등 미사용)
ushort crc = CalcCRC16(p, 27);
// lo-first (표준 리틀엔디안) — 와이어 [27]=하위, [28]=상위
p[27] = (byte)(crc & 0xFF);
p[28] = (byte)((crc >> 8) & 0xFF);
return p;
@@ -378,7 +391,7 @@ namespace DiffuserSimulator
if (total < 39) return null;
if (buf[1] != 0x01) continue; // not slave
ushort rxCrc = (ushort)(buf[37] | (buf[38] << 8));
ushort rxCrc = (ushort)(buf[37] | (buf[38] << 8)); // lo-first (표준 리틀엔디안)
ushort calc = CalcCRC16(buf, 37);
if (rxCrc != calc) { Log($"[CRC오류] 수신:0x{rxCrc:X4} 계산:0x{calc:X4}"); return null; }
return buf;
@@ -402,6 +415,7 @@ namespace DiffuserSimulator
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);
// lo-first : 펌웨어 RX가 (Rx[37]<<8)|Rx[38] 로 읽고 CRC16()=스왑값과 비교 → lo-first로 보내야 일치
p[37] = (byte)(crc & 0xFF); p[38] = (byte)((crc >> 8) & 0xFF);
return p;
}