feat: 06-17 신규 작업본 반영 (개발사양서/기능검토/승인원/Source 등 추가)
.claude/ 제외(.gitignore 추가). 기존 초기커밋(5a96a69) 위에 신규·수정·이동분 커밋.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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}"/>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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, 거실
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ namespace ERVSimulator.Protocol
|
||||
p[10] = (byte)rm.CurrentSA;
|
||||
p[11] = (byte)rm.CurrentRA;
|
||||
ushort crc = Crc16.Modbus(p, 0, 27);
|
||||
// lo-first : 펌웨어 CRC16()이 표준MODBUS 바이트스왑값 반환 + [27]=icrc>>8 배치 → 와이어는 리틀엔디안
|
||||
p[27] = (byte)(crc & 0xFF);
|
||||
p[28] = (byte)(crc >> 8);
|
||||
_ch.Send(p, 29);
|
||||
@@ -100,7 +101,7 @@ namespace ERVSimulator.Protocol
|
||||
|
||||
void HandleResponse(byte[] p)
|
||||
{
|
||||
ushort rxcrc = (ushort)(p[37] | (p[38] << 8));
|
||||
ushort rxcrc = (ushort)(p[37] | (p[38] << 8)); // lo-first (표준 리틀엔디안)
|
||||
if (Crc16.Modbus(p, 0, 37) != rxcrc)
|
||||
{
|
||||
PacketReceived?.Invoke($"Diff RX CRC오류 {HexFormat.Bytes(p, 39)}");
|
||||
|
||||
@@ -12,11 +12,11 @@ namespace HoodSimulator
|
||||
readonly HoodProtocol _hood = new();
|
||||
int _rxCount;
|
||||
|
||||
// 조리 종료 후 메이크업 유지(잔여 냄새 배출) — 후드측이 담당. 유지중에는 ERV 에 계속 '켜짐' 보고,
|
||||
// 조리 종료 후 지연배기(잔여 냄새 배출) — 후드측이 담당. 배기중에는 ERV 에 계속 '켜짐' 보고,
|
||||
// 종료 시점에 OFF 전송 → ERV 가 원래 모드/풍량으로 복귀. (사양 260613 9p 3.3)
|
||||
readonly System.Windows.Threading.DispatcherTimer _makeupTimer =
|
||||
new() { Interval = TimeSpan.FromSeconds(1) };
|
||||
const int MakeupHoldSec = 10; // 메이크업 유지 시간 (10초)
|
||||
const int MakeupHoldSec = 10; // 지연배기 유지 시간 — 시간 불변(시뮬 10초; 사양 원래 30초)
|
||||
int _makeupRemainSec;
|
||||
|
||||
static readonly Brush AccentCyan = (Brush)new BrushConverter().ConvertFromString("#7DCFFF")!;
|
||||
@@ -83,7 +83,7 @@ namespace HoodSimulator
|
||||
{
|
||||
if (tglPower.IsChecked == true)
|
||||
{
|
||||
// 켜기 : 진행중인 메이크업 유지 취소 후 즉시 ON (풍량 1)
|
||||
// 켜기 : 진행중인 지연배기 취소 후 즉시 ON (풍량 1)
|
||||
StopMakeupHold();
|
||||
_hood.PowerOn = true;
|
||||
_hood.FanStage = 1;
|
||||
@@ -94,27 +94,27 @@ namespace HoodSimulator
|
||||
}
|
||||
else
|
||||
{
|
||||
// 끄기 : OFF 표시 + 옆에 메이크업 유지(1분) 카운트다운 시작. 그동안 ERV엔 계속 '켜짐' 보고.
|
||||
// 유지 종료 시 후드 OFF 전송 → ERV 가 원래 모드/풍량으로 복귀.
|
||||
// 끄기 : OFF 표시 + 옆에 지연배기 카운트다운 시작. 그동안 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엔 계속 켜짐 보고)");
|
||||
txtMakeup.Text = $"지연배기(원래는 30초) {_makeupRemainSec}s";
|
||||
OnLog($"[제어] 전원 OFF 요청 → 지연배기 {MakeupHoldSec}s 유지 (ERV엔 계속 켜짐 보고)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 메이크업 유지 카운트다운 (1초). 0이 되면 실제 OFF 전송.
|
||||
// 지연배기 카운트다운 (1초). 0이 되면 실제 OFF 전송.
|
||||
void MakeupTick(object? s, EventArgs e)
|
||||
{
|
||||
_makeupRemainSec--;
|
||||
if (_makeupRemainSec > 0)
|
||||
{
|
||||
txtMakeup.Text = $"메이크업 {_makeupRemainSec}s";
|
||||
txtMakeup.Text = $"지연배기(원래는 30초) {_makeupRemainSec}s";
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -122,7 +122,7 @@ namespace HoodSimulator
|
||||
_hood.PowerOn = false;
|
||||
_hood.FanStage = 0;
|
||||
UpdateFanButtons();
|
||||
OnLog("[제어] 메이크업 유지 종료 → 후드 OFF 전송 (ERV 원래 모드/풍량 복귀)");
|
||||
OnLog("[제어] 지연배기 종료 → 후드 OFF 전송 (ERV 원래 모드/풍량 복귀)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ namespace HoodSimulator
|
||||
// 에러 발생 시 전원 OFF / 풍량 0 / 조명 OFF (다음 응답에 반영되어 전송)
|
||||
if (_hood.ErrorCode != 0)
|
||||
{
|
||||
StopMakeupHold(); // 진행중인 메이크업 유지 즉시 취소
|
||||
StopMakeupHold(); // 진행중인 지연배기 즉시 취소
|
||||
_hood.PowerOn = false;
|
||||
_hood.FanStage = 0;
|
||||
_hood.Light = false;
|
||||
|
||||
Reference in New Issue
Block a user