feat: 06-17 신규 작업본 반영 (개발사양서/기능검토/승인원/Source 등 추가)
.claude/ 제외(.gitignore 추가). 기존 초기커밋(5a96a69) 위에 신규·수정·이동분 커밋.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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; }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>RJ2RoomConSimulator</RootNamespace>
|
||||
<AssemblyName>RJ2RoomConSimulator</AssemblyName>
|
||||
<StartupObject>RJ2RoomConSimulator.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,226 @@
|
||||
using System;
|
||||
using System.IO.Ports;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RJ2RoomConSimulator
|
||||
{
|
||||
// RJ2-232 룸콘(마스터) 시뮬레이터 프로토콜
|
||||
// 규격 : Protocol/ROOMCON/힘펠_환기장치프로토콜V3.7 (펌웨어 My_RJ2.c / ERVSimulator RoomConProtocol.cs)
|
||||
// 프레임 : AA | CMD | D[2..12] | XOR(0~12) | EE (15 byte 고정), RS-232 9600 8N1
|
||||
// 역할 : 룸콘이 마스터 — 모드/풍량/예약 변경(EVENT) 송신 + 주기 폴링(NORMAL), ERV 응답 수신/표시
|
||||
// ERV(ERVSimulator)가 응답측. 모드코드 : 0환기/1자동/2바이패스/3공청 (ERVSim RunMode enum 일치)
|
||||
public class RoomConProtocol : IDisposable
|
||||
{
|
||||
public const byte HEADER = 0xAA;
|
||||
public const byte TAIL = 0xEE;
|
||||
public const int LEN = 15;
|
||||
|
||||
// CMD
|
||||
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; // 테스트(VSP) 진입/설정
|
||||
public const byte CMD_EXIT = 0x04; // 테스트 종료
|
||||
public const byte CMD_CONTROL = 0x07; // ERV→룸콘 상태응답(COMMAND_CONTROLL)
|
||||
public const byte CMD_HOOD_INFO = 0x0A; // ERV→룸콘 후드 연동 통지(힘펠 V3.7 RX_DATA_HOOD_INFO)
|
||||
|
||||
SerialPort? _port;
|
||||
CancellationTokenSource? _cts;
|
||||
readonly object _lock = new();
|
||||
bool _disposed;
|
||||
|
||||
// ---- 룸콘 제어 상태 (UI) ----
|
||||
public bool PowerOn;
|
||||
public byte RunMode; // 0환기/1자동/2바이패스/3공청
|
||||
public byte FanMode; // 0~4
|
||||
public int ReserveHours; // 0~8 (0=취소)
|
||||
public byte DeviceId = 1; // id 설정 (RS232 점대점 — 표시/로그용)
|
||||
|
||||
// ---- ERV 응답 파싱 결과 ----
|
||||
public byte ErvRunMode, ErvFanMode, ErvError;
|
||||
public bool HoodLinked; // 후드 연동중(HOOD_INFO byte[6] bit0 = 0x81 ON / 0x80 OFF)
|
||||
public int ErvInTemp, ErvOutTemp;
|
||||
public readonly int[] VentSa = new int[5], VentEa = new int[5]; // index 1~4
|
||||
public readonly int[] BypassSa = new int[2], BypassEa = new int[2];
|
||||
public readonly int[] AirSa = new int[5];
|
||||
|
||||
readonly byte[] _rx = new byte[LEN];
|
||||
int _rxPos;
|
||||
|
||||
public event Action<string>? Log;
|
||||
public event Action<bool>? ConnectionChanged;
|
||||
public event Action<byte>? ResponseReceived; // 파싱 완료된 CMD 전달
|
||||
|
||||
public bool IsConnected => _port?.IsOpen == true;
|
||||
|
||||
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, 9600, Parity.None, 8, StopBits.One)
|
||||
{
|
||||
ReadTimeout = 100, WriteTimeout = 300
|
||||
};
|
||||
_port.Open();
|
||||
_cts = new CancellationTokenSource();
|
||||
_ = Task.Run(() => ReadLoop(_cts.Token));
|
||||
Logm($"[연결] {portName} (9600, 8N1)");
|
||||
ConnectionChanged?.Invoke(true);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logm($"[오류] 연결 실패: {ex.Message}");
|
||||
ConnectionChanged?.Invoke(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
try { _cts?.Cancel(); } catch { }
|
||||
try { if (_port?.IsOpen == true) { _port.Close(); Logm("[연결 해제]"); } } catch { }
|
||||
_port?.Dispose(); _port = null;
|
||||
ConnectionChanged?.Invoke(false);
|
||||
}
|
||||
|
||||
// ================= 수신 (ERV 응답 파싱) =================
|
||||
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++) FeedByte(buf[i]);
|
||||
}
|
||||
catch (TimeoutException) { }
|
||||
catch (Exception ex) { if (!ct.IsCancellationRequested) Logm($"[오류] {ex.Message}"); break; }
|
||||
}
|
||||
}
|
||||
|
||||
DateTime _lastByte = DateTime.MinValue;
|
||||
static readonly TimeSpan FrameGap = TimeSpan.FromMilliseconds(50);
|
||||
void FeedByte(byte b)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (now - _lastByte > FrameGap) _rxPos = 0;
|
||||
_lastByte = now;
|
||||
|
||||
if (_rxPos == 0) { if (b == HEADER) _rx[_rxPos++] = b; return; }
|
||||
if (_rxPos <= 12) { _rx[_rxPos++] = b; return; }
|
||||
if (_rxPos == 13) { if (Xor(_rx, 0, 13) == b) _rx[_rxPos++] = b; else _rxPos = 0; return; }
|
||||
if (_rxPos == 14)
|
||||
{
|
||||
_rxPos = 0;
|
||||
if (b != TAIL) return;
|
||||
HandleResponse((byte[])_rx.Clone());
|
||||
}
|
||||
}
|
||||
|
||||
void HandleResponse(byte[] p)
|
||||
{
|
||||
Logm($"RX: {Hex(p, 14)} EE");
|
||||
byte cmd = p[1];
|
||||
switch (cmd)
|
||||
{
|
||||
case CMD_EVENT: // ERV ack : runMode/fanMode/err/temp
|
||||
ErvRunMode = p[2]; ErvFanMode = p[3]; ErvError = p[7];
|
||||
ErvOutTemp = (p[9] == 0xFF) ? 100 : (p[8] == 0x01 ? -(p[9] - 20) : p[9] - 20);
|
||||
ErvInTemp = (p[10] == 0xFF) ? 100 : p[10] - 20;
|
||||
break;
|
||||
case CMD_CONTROL: // 상태 폴링 응답
|
||||
ErvRunMode = p[2]; ErvFanMode = p[3]; ErvError = p[7];
|
||||
break;
|
||||
case CMD_RESTART1: // 환기 preset
|
||||
VentSa[1] = p[5]; VentEa[1] = p[6]; VentSa[2] = p[7]; VentEa[2] = p[8];
|
||||
VentSa[3] = p[9]; VentEa[3] = p[10]; VentSa[4] = p[11]; VentEa[4] = p[12];
|
||||
break;
|
||||
case CMD_RESTART2: // bypass/air preset
|
||||
BypassSa[1] = p[5]; BypassEa[1] = p[6];
|
||||
AirSa[1] = p[7]; AirSa[2] = p[8]; AirSa[3] = p[9]; AirSa[4] = p[10];
|
||||
break;
|
||||
case CMD_HOOD_INFO: // 후드 연동 통지 : byte[6] 0x81=ON / 0x80=OFF
|
||||
HoodLinked = (p[6] & 0x01) != 0;
|
||||
break;
|
||||
}
|
||||
ResponseReceived?.Invoke(cmd);
|
||||
}
|
||||
|
||||
// ================= 송신 (룸콘 → ERV) =================
|
||||
byte[] NewPacket(byte cmd)
|
||||
{
|
||||
var p = new byte[LEN];
|
||||
p[0] = HEADER; p[1] = cmd;
|
||||
return p;
|
||||
}
|
||||
void Send(byte[] p)
|
||||
{
|
||||
p[13] = Xor(p, 0, 13);
|
||||
p[14] = TAIL;
|
||||
if (_port?.IsOpen != true) return;
|
||||
try { lock (_lock) { _port.Write(p, 0, LEN); } Logm($"TX: {Hex(p, LEN)}"); }
|
||||
catch (Exception ex) { Logm($"[송신오류] {ex.Message}"); }
|
||||
}
|
||||
|
||||
// 모드/풍량/예약 변경 이벤트 (EVENT). 전원 OFF는 환기+풍량0으로 표현(ERV가 Power OFF 처리).
|
||||
public void SendEvent()
|
||||
{
|
||||
var p = NewPacket(CMD_EVENT);
|
||||
p[2] = PowerOn ? RunMode : (byte)0; // OFF → 환기(0)
|
||||
p[3] = PowerOn ? FanMode : (byte)0; // OFF → 풍량 0
|
||||
p[5] = 0; // Heater/UV/Kijer
|
||||
p[10] = 1; p[11] = (byte)ReserveHours; p[12] = 0; // 예약(시) — 0이면 해제. flag=1로 항상 전달
|
||||
Send(p);
|
||||
}
|
||||
|
||||
// 상태 폴링 (NORMAL)
|
||||
public void SendNormal()
|
||||
{
|
||||
var p = NewPacket(CMD_NORMAL);
|
||||
if (ReserveHours > 0) { p[11] = (byte)ReserveHours; p[12] = 0; }
|
||||
Send(p);
|
||||
}
|
||||
|
||||
public void SendRestart1() => Send(NewPacket(CMD_RESTART1)); // 환기 preset 조회
|
||||
public void SendRestart2() => Send(NewPacket(CMD_RESTART2)); // bypass/air preset 조회
|
||||
public void SendExit() => Send(NewPacket(CMD_EXIT)); // 테스트 종료
|
||||
|
||||
// VSP(테스트) 설정 : select / sa / ea
|
||||
public void SendVsp(byte select, byte sa, byte ea)
|
||||
{
|
||||
var p = NewPacket(CMD_VSP);
|
||||
p[3] = select; p[4] = sa; p[5] = ea;
|
||||
Send(p);
|
||||
}
|
||||
|
||||
void Logm(string m) => Log?.Invoke($"[{DateTime.Now:HH:mm:ss.fff}] {m}");
|
||||
static string Hex(byte[] d, int n)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder(n * 3);
|
||||
for (int i = 0; i < n; i++) { if (i > 0) sb.Append(' '); sb.Append(d[i].ToString("X2")); }
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
Disconnect();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<Window x:Class="RJ2RoomConSimulator.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 Background="{StaticResource SecondaryBgBrush}" CornerRadius="10" Margin="10" Padding="16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="VSP 설정 — 풍량 프리셋 조회/설정 (RESTART/VSP)" FontSize="14" FontWeight="Bold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,0,4"/>
|
||||
<TextBlock Text="조회 후 SA/EA 수정하고 '설정' 누르면 해당 프리셋 VSP 송신. 공청은 EA 미사용."
|
||||
FontSize="10" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,10"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<Button Content="환기 조회" Style="{StaticResource ModernButton}" Click="QueryVent_Click" Padding="12,5" Margin="0,0,6,0" Background="{StaticResource CardBgBrush}"/>
|
||||
<Button Content="바이패스/공청 조회" Style="{StaticResource ModernButton}" Click="QueryBypassAir_Click" Padding="12,5" Margin="0,0,6,0" Background="{StaticResource CardBgBrush}"/>
|
||||
<Button Content="테스트 종료" Style="{StaticResource ModernButton}" Click="Exit_Click" Padding="12,5" Background="{StaticResource AccentRedBrush}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 헤더 -->
|
||||
<Grid Margin="0,0,0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="90"/><ColumnDefinition Width="70"/><ColumnDefinition Width="70"/><ColumnDefinition Width="70"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="프리셋" FontSize="11" Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||
<TextBlock Grid.Column="1" Text="SA" TextAlignment="Center" FontSize="11" Foreground="{StaticResource AccentBlueBrush}"/>
|
||||
<TextBlock Grid.Column="2" Text="EA" TextAlignment="Center" FontSize="11" Foreground="{StaticResource AccentGreenBrush}"/>
|
||||
</Grid>
|
||||
|
||||
<StackPanel x:Name="RowsPanel"/>
|
||||
|
||||
<Button Content="닫기" Style="{StaticResource ModernButton}" Click="Close_Click" Padding="14,6" Margin="0,12,0,0"
|
||||
HorizontalAlignment="Right" Background="{StaticResource CardBgBrush}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Window>
|
||||
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace RJ2RoomConSimulator
|
||||
{
|
||||
// VSP(테스트 모드) 프리셋 조회/설정 팝업.
|
||||
// select 코드(펌웨어 My_RJ2.c) : 1~4 환기1~4 / 5 바이패스 / 6~9 공청1~4 (공청은 EA 미사용)
|
||||
public partial class VspWindow : Window
|
||||
{
|
||||
readonly RoomConProtocol _proto;
|
||||
|
||||
// 프리셋 행 정의 : (select, 라벨, EA 사용여부)
|
||||
static readonly (byte sel, string label, bool hasEa)[] Defs =
|
||||
{
|
||||
(1,"환기 1단",true),(2,"환기 2단",true),(3,"환기 3단",true),(4,"환기 4단",true),
|
||||
(5,"바이패스",true),
|
||||
(6,"공청 1단",false),(7,"공청 2단",false),(8,"공청 3단",false),(9,"공청 4단",false),
|
||||
};
|
||||
readonly TextBox[] _sa = new TextBox[10]; // index = select
|
||||
readonly TextBox[] _ea = new TextBox[10];
|
||||
|
||||
static Brush Br(string h) => (Brush)new BrushConverter().ConvertFromString(h)!;
|
||||
static readonly Brush CardBg = Br("#313147");
|
||||
static readonly Brush Border = Br("#3B3B55");
|
||||
static readonly Brush TextPri = Br("#C0CAF5");
|
||||
|
||||
public VspWindow(RoomConProtocol proto)
|
||||
{
|
||||
InitializeComponent();
|
||||
_proto = proto;
|
||||
BuildRows();
|
||||
_proto.ResponseReceived += OnResponse;
|
||||
Closed += (_, _) => _proto.ResponseReceived -= OnResponse;
|
||||
}
|
||||
|
||||
void BuildRows()
|
||||
{
|
||||
foreach (var d in Defs)
|
||||
{
|
||||
var g = new Grid { Margin = new Thickness(0, 2, 0, 2) };
|
||||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(90) });
|
||||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(70) });
|
||||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(70) });
|
||||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(70) });
|
||||
|
||||
var lab = new TextBlock { Text = d.label, FontSize = 12, Foreground = TextPri, VerticalAlignment = VerticalAlignment.Center };
|
||||
Grid.SetColumn(lab, 0); g.Children.Add(lab);
|
||||
|
||||
var sa = MakeBox(); Grid.SetColumn(sa, 1); g.Children.Add(sa); _sa[d.sel] = sa;
|
||||
var ea = MakeBox(); ea.IsEnabled = d.hasEa; Grid.SetColumn(ea, 2); g.Children.Add(ea); _ea[d.sel] = ea;
|
||||
|
||||
byte sel = d.sel;
|
||||
var btn = new Button { Content = "설정", Padding = new Thickness(10, 3, 10, 3), FontSize = 11, Margin = new Thickness(4, 0, 0, 0),
|
||||
Style = (Style)FindResource("ModernButton"), Background = CardBg };
|
||||
btn.Click += (_, _) => SetRow(sel);
|
||||
Grid.SetColumn(btn, 3); g.Children.Add(btn);
|
||||
|
||||
RowsPanel.Children.Add(g);
|
||||
}
|
||||
}
|
||||
|
||||
TextBox MakeBox() => new()
|
||||
{
|
||||
Width = 60, Margin = new Thickness(2, 0, 2, 0), Padding = new Thickness(3, 2, 3, 2),
|
||||
TextAlignment = TextAlignment.Center, FontSize = 12, Text = "0",
|
||||
Background = CardBg, Foreground = TextPri, BorderBrush = Border, BorderThickness = new Thickness(1),
|
||||
};
|
||||
|
||||
void SetRow(byte sel)
|
||||
{
|
||||
byte sa = ParseByte(_sa[sel].Text);
|
||||
byte ea = _ea[sel].IsEnabled ? ParseByte(_ea[sel].Text) : (byte)0;
|
||||
_proto.SendVsp(sel, sa, ea);
|
||||
}
|
||||
static byte ParseByte(string s) { int.TryParse(s, out var v); return (byte)(v < 0 ? 0 : v > 255 ? 255 : v); }
|
||||
|
||||
void QueryVent_Click(object s, RoutedEventArgs e) => _proto.SendRestart1();
|
||||
void QueryBypassAir_Click(object s, RoutedEventArgs e) => _proto.SendRestart2();
|
||||
void Exit_Click(object s, RoutedEventArgs e) => _proto.SendExit();
|
||||
void Close_Click(object s, RoutedEventArgs e) => Close();
|
||||
|
||||
// 조회 응답 수신 → 표 갱신
|
||||
void OnResponse(byte cmd)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (cmd == RoomConProtocol.CMD_RESTART1)
|
||||
{
|
||||
for (int i = 1; i <= 4; i++) { _sa[i].Text = _proto.VentSa[i].ToString(); _ea[i].Text = _proto.VentEa[i].ToString(); }
|
||||
}
|
||||
else if (cmd == RoomConProtocol.CMD_RESTART2)
|
||||
{
|
||||
_sa[5].Text = _proto.BypassSa[1].ToString(); _ea[5].Text = _proto.BypassEa[1].ToString();
|
||||
for (int i = 1; i <= 4; i++) { _sa[5 + i].Text = _proto.AirSa[i].ToString(); _ea[5 + i].Text = "0"; }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
# RJ2-232 Simulator 사양서
|
||||
C# WPF (스타일: HoodSimulator 동일 — Tokyo Night 다크 테마)
|
||||
최종 수정 : 2026.06.07
|
||||
|
||||
## UI 기능
|
||||
통신 설정
|
||||
전원 ON / OFF (on 시 환기모드, 풍량 1단, off 시 풍량 0)
|
||||
모드 환기 / 자동 / 공기청정 / 바이패스 (환기, 자동, 공기청정 0 ~ 4단, 바이패스 1단, 자동은 풍량 변경 안됨)
|
||||
풍량 0 / 1 / 2 / 3 / 4
|
||||
예약 0 ~ 8 시간 (0은 예약 취소, 1시간 단위, 모드 전환 시 예약 해제)
|
||||
id 설정
|
||||
vsp 설정
|
||||
|
||||
로그 저장 / 지우기
|
||||
|
||||
---
|
||||
|
||||
## 구현 내용 (2026.06.07)
|
||||
|
||||
### 역할 / 통신
|
||||
- **룸콘(마스터) 시뮬레이터** — ERVSimulator(ERV, 응답측)와 RS-232로 연동.
|
||||
- 규격 : `Protocol/ROOMCON/힘펠_환기장치프로토콜V3.7` (펌웨어 My_RJ2.c / ERVSimulator RoomConProtocol.cs)
|
||||
- 포트 : **RS-232, 9600 8N1**
|
||||
- 프레임 : `AA | CMD | D[2..12] | XOR(byte0~12) | EE` (15바이트 고정)
|
||||
- COM 페어(가상 널모뎀 또는 어댑터 2개)로 RJ2 ↔ ERVSimulator RoomCon 포트(9600) 연결.
|
||||
|
||||
### CMD (룸콘 → ERV)
|
||||
| CMD | 이름 | 내용 |
|
||||
|---|---|---|
|
||||
| 0x00 | NORMAL | 상태 폴링(폴링 시작 시 주기 송신) → ERV가 0x07 응답 |
|
||||
| 0x01 | EVENT | 모드/풍량/예약 변경 → ERV가 0x01 응답(모드·풍량·에러·온도) |
|
||||
| 0x02 | RESTART1 | 환기 1~4단 preset 조회 → ERV가 0x02 응답 |
|
||||
| 0x12 | RESTART2 | 바이패스/공청 preset 조회 → ERV가 0x12 응답 |
|
||||
| 0x03 | VSP | 테스트모드 프리셋 설정(select/sa/ea) |
|
||||
| 0x04 | EXIT | 테스트모드 종료 |
|
||||
|
||||
### 모드 코드 (EVENT byte[2]) — ERVSimulator RunMode enum 일치
|
||||
0 = 환기 / 1 = 자동 / 2 = 바이패스 / 3 = 공기청정
|
||||
- 전원 OFF = EVENT 모드 0(환기) + 풍량 0 으로 표현(ERV가 Power OFF 처리)
|
||||
|
||||
### EVENT 패킷 데이터
|
||||
| byte | 내용 |
|
||||
|---|---|
|
||||
| 2 | 운전모드 |
|
||||
| 3 | 풍량(0~4) |
|
||||
| 5 | Heater/UV (미사용 0) |
|
||||
| 10 | 예약 flag (항상 1로 전송) |
|
||||
| 11 | 예약 시간(시, 0=해제) |
|
||||
| 12 | 예약 분(0) |
|
||||
|
||||
### 예약 연동 (룸콘 → ERV → 대시보드)
|
||||
- 예약(시)을 EVENT `[10]=1, [11]=시`로 항상 전송 → **설정·해제(0) 모두 전달**.
|
||||
- ERVSimulator가 수신하여 `ReserveHours / ReserveRemainSec` 반영 → ERVSim UI 표시 + 1초 카운트다운(0 도달 시 전원 OFF).
|
||||
- ERVSim의 HOMENET STATUS(reserve_remain)에 실려 **PCDashBoard / WebDashBoard에도 그대로 반영**.
|
||||
- 모드 전환 시 예약 해제(`[11]=0`) 전송 → ERV·대시보드 함께 해제.
|
||||
|
||||
### VSP 설정 팝업 (테스트 모드)
|
||||
- select 코드(펌웨어 My_RJ2.c) : 1~4 환기1~4 / 5 바이패스 / 6~9 공청1~4 (공청은 EA 미사용)
|
||||
- 환기 조회(RESTART1) / 바이패스·공청 조회(RESTART2)로 현재 preset 읽어 표 갱신
|
||||
- 행별 SA/EA 수정 후 '설정' → 해당 select VSP(0x03) 송신, '테스트 종료' → EXIT(0x04)
|
||||
|
||||
### ID 설정
|
||||
- RS-232 점대점이라 프레임에 ID 필드 없음 → UI 입력값은 표시/로그용(전송 안 함).
|
||||
|
||||
### ERV 응답 표시
|
||||
- 운전모드 / 풍량 / 에러코드 / 실내온도 / 외기온도 (0x01·0x07 응답 파싱), 수신 카운트.
|
||||
|
||||
### 알람 / 에러 표시 (ERV ErrorCode 비트 디코드)
|
||||
ERV 응답의 ErrorCode 바이트(응답 `[7]`)를 비트별로 칩 표시. NORMAL(0x07)/EVENT(0x01) 응답마다 갱신.
|
||||
- **알람**(노랑) : 필터청소(0x01) / 필터교환(0x02)
|
||||
- **에러**(빨강, COLD=시안) : E02 온도센서(0x08) / E09 급기팬(0x80) / E10 배기팬(0x20) / COLD 장비보호(0x10) / E07 내부통신(0x40)
|
||||
- 알람/에러 소스는 ERVSimulator(에러 카드 + 필터청소/교환 토글) → 같은 ErrorCode가 대시보드(STATUS)에도 반영됨.
|
||||
|
||||
| 비트 | 항목 | 구분 |
|
||||
|---|---|---|
|
||||
| 0x01 | 필터청소 | 알람 |
|
||||
| 0x02 | 필터교환 | 알람 |
|
||||
| 0x08 | E02 온도센서 | 에러 |
|
||||
| 0x10 | COLD 장비보호 | 에러 |
|
||||
| 0x20 | E10 배기(EA)팬 | 에러 |
|
||||
| 0x40 | E07 내부통신 | 에러 |
|
||||
| 0x80 | E09 급기(SA)팬 | 에러 |
|
||||
|
||||
### 참고
|
||||
- 본 시뮬레이터는 프로토콜대로 송신하며, ERVSimulator는 모드/풍량/예약/preset 조회를 처리(VSP 설정값 적용은 로그만).
|
||||
Reference in New Issue
Block a user