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,7 @@
|
||||
<Application x:Class="CvnetPacketProgram.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,7 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace CvnetPacketProgram;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<AssemblyName>CvnetPacketProgram</AssemblyName>
|
||||
<RootNamespace>CvnetPacketProgram</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<!-- 단일 exe 배포 옵션 (publish 시 적용) -->
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IO.Ports" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,56 @@
|
||||
namespace CvnetPacketProgram;
|
||||
|
||||
/// <summary>
|
||||
/// 수신 바이트 스트림에서 0xF7 0x32 로 시작하는 완전한 프레임을 추출한다.
|
||||
/// 프레임 길이 = 5(Header~Len) + Len + 2(XOR,ADD).
|
||||
/// </summary>
|
||||
public sealed class FrameParser
|
||||
{
|
||||
private readonly List<byte> _buf = new();
|
||||
|
||||
public void Append(byte[] data, int len)
|
||||
{
|
||||
for (int i = 0; i < len; i++) _buf.Add(data[i]);
|
||||
}
|
||||
|
||||
/// <summary>버퍼에서 추출 가능한 모든 완전 프레임을 반환한다.</summary>
|
||||
public List<byte[]> Extract()
|
||||
{
|
||||
var result = new List<byte[]>();
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Header(0xF7) + Device(0x32) 동기화
|
||||
int sync = FindSync();
|
||||
if (sync < 0)
|
||||
{
|
||||
// 동기 패턴 없음 — 마지막 1바이트(0xF7 가능성)만 남기고 버림
|
||||
if (_buf.Count > 1) _buf.RemoveRange(0, _buf.Count - 1);
|
||||
break;
|
||||
}
|
||||
if (sync > 0) _buf.RemoveRange(0, sync); // 앞쪽 쓰레기 제거
|
||||
|
||||
if (_buf.Count < 5) break; // Len 까지 못 받음
|
||||
int len = _buf[4];
|
||||
int frameLen = 5 + len + 2;
|
||||
if (_buf.Count < frameLen) break; // 프레임 미완성
|
||||
|
||||
var frame = _buf.GetRange(0, frameLen).ToArray();
|
||||
_buf.RemoveRange(0, frameLen);
|
||||
result.Add(frame);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private int FindSync()
|
||||
{
|
||||
for (int i = 0; i + 1 < _buf.Count; i++)
|
||||
if (_buf[i] == Cvnet.Header && _buf[i + 1] == Cvnet.Device)
|
||||
return i;
|
||||
// 마지막 바이트가 Header 면 다음 바이트 대기
|
||||
if (_buf.Count > 0 && _buf[^1] == Cvnet.Header) return _buf.Count - 1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void Clear() => _buf.Clear();
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<Window x:Class="CvnetPacketProgram.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="CVNET DL 사양 패킷 통신프로그램 (프로토콜 230824 기준)"
|
||||
Height="860" Width="1320" MinHeight="600" MinWidth="1000"
|
||||
FontFamily="Segoe UI" FontSize="13" Background="#F4F5F7">
|
||||
<Window.Resources>
|
||||
<Style TargetType="GroupBox">
|
||||
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||
<Setter Property="Padding" Value="8"/>
|
||||
</Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Padding" Value="10,4"/>
|
||||
<Setter Property="Margin" Value="0,0,6,0"/>
|
||||
<Setter Property="MinWidth" Value="72"/>
|
||||
</Style>
|
||||
<Style x:Key="Mono" TargetType="TextBox">
|
||||
<Setter Property="FontFamily" Value="Consolas"/>
|
||||
<Setter Property="FontSize" Value="12.5"/>
|
||||
<Setter Property="IsReadOnly" Value="True"/>
|
||||
<Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
|
||||
<Setter Property="HorizontalScrollBarVisibility" Value="Auto"/>
|
||||
<Setter Property="TextWrapping" Value="NoWrap"/>
|
||||
<Setter Property="Background" Value="#1E1E1E"/>
|
||||
<Setter Property="Foreground" Value="#DDE6EE"/>
|
||||
<Setter Property="Padding" Value="6"/>
|
||||
</Style>
|
||||
<Style x:Key="Lbl" TargetType="TextBlock">
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="Margin" Value="0,0,6,0"/>
|
||||
<Setter Property="MinWidth" Value="78"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<DockPanel Margin="8">
|
||||
|
||||
<!-- ===== 상단 연결 바 ===== -->
|
||||
<Border DockPanel.Dock="Top" Background="White" BorderBrush="#D0D5DD" BorderThickness="1"
|
||||
CornerRadius="6" Padding="10,8" Margin="0,0,0,8">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="COM" Style="{StaticResource Lbl}" MinWidth="34"/>
|
||||
<ComboBox x:Name="CmbPort" Width="100" VerticalAlignment="Center"/>
|
||||
<Button Content="↻" Click="OnRefreshPorts" Margin="4,0,12,0" MinWidth="30"/>
|
||||
<TextBlock Text="Baud" Style="{StaticResource Lbl}" MinWidth="40"/>
|
||||
<ComboBox x:Name="CmbBaud" Width="90" VerticalAlignment="Center"/>
|
||||
<Button x:Name="BtnOpen" Content="열기" Click="OnToggleOpen" Margin="12,0,6,0" MinWidth="80"/>
|
||||
<Ellipse x:Name="LedConn" Width="14" Height="14" Fill="#C0392B" VerticalAlignment="Center" Margin="6,0"/>
|
||||
<TextBlock x:Name="TxtConn" Text="닫힘" VerticalAlignment="Center" Foreground="#475467"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="410"/>
|
||||
<ColumnDefinition Width="6"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- ===== 좌측 : 패킷 빌더 ===== -->
|
||||
<Border Grid.Column="0" Background="White" BorderBrush="#D0D5DD" BorderThickness="1" CornerRadius="6">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="10">
|
||||
<StackPanel>
|
||||
<TextBlock Text="패킷 빌더" FontWeight="Bold" FontSize="15" Margin="0,0,0,8"/>
|
||||
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="Cmd" Style="{StaticResource Lbl}"/>
|
||||
<ComboBox x:Name="CmbCmd" Grid.Column="1" SelectionChanged="OnCmdChanged"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,0,0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="Sub ID(hex)" Style="{StaticResource Lbl}"/>
|
||||
<TextBox x:Name="TxtSubId" Grid.Column="1" Text="01" MaxLength="2"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 모드 / 풍량 / 예약 (제어·응답 공통) -->
|
||||
<GroupBox x:Name="GrpFields" Header="필드">
|
||||
<StackPanel>
|
||||
<Grid Margin="0,0,0,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/><ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="모드" Style="{StaticResource Lbl}"/>
|
||||
<ComboBox x:Name="CmbMode" Grid.Column="1"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,0,0,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/><ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="풍량" Style="{StaticResource Lbl}"/>
|
||||
<ComboBox x:Name="CmbFan" Grid.Column="1"/>
|
||||
</Grid>
|
||||
|
||||
<WrapPanel Margin="0,2,0,4">
|
||||
<CheckBox x:Name="ChkBasic" Content="기저모드" Margin="0,2,12,2"/>
|
||||
<CheckBox x:Name="ChkRange" Content="렌지연동" Margin="0,2,12,2"/>
|
||||
</WrapPanel>
|
||||
|
||||
<!-- 제어 요구(0x51) 전용 Flag -->
|
||||
<StackPanel x:Name="PnlCtrlFlags">
|
||||
<TextBlock Text="제어 Flag (요청 항목만 ON)" FontWeight="SemiBold" Margin="0,4,0,2"/>
|
||||
<WrapPanel>
|
||||
<CheckBox x:Name="ChkModeFlag" Content="모드Flag" IsChecked="True" Margin="0,2,12,2"/>
|
||||
<CheckBox x:Name="ChkFanFlag" Content="풍량Flag" IsChecked="True" Margin="0,2,12,2"/>
|
||||
<CheckBox x:Name="ChkRsvFlag" Content="예약Flag" Margin="0,2,12,2"/>
|
||||
<CheckBox x:Name="ChkFilterReset" Content="필터타이머리셋" Margin="0,2,12,2"/>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Grid Margin="0,4,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/><ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="예약시간(0~24)" Style="{StaticResource Lbl}" MinWidth="100"/>
|
||||
<TextBox x:Name="TxtReserve" Grid.Column="1" Text="0"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<TextBlock Text="미리보기 (HEX)" FontWeight="SemiBold" Margin="0,4,0,2"/>
|
||||
<TextBox x:Name="TxtPreview" Style="{StaticResource Mono}" Height="48"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
|
||||
<Button Content="빌드" Click="OnBuild"/>
|
||||
<Button x:Name="BtnSend" Content="전송" Click="OnSendBuilt" Background="#1D6FE0" Foreground="White"/>
|
||||
</StackPanel>
|
||||
|
||||
<Separator Margin="0,12"/>
|
||||
|
||||
<TextBlock Text="직접 HEX 전송" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
||||
<TextBox x:Name="TxtRawHex" Height="44" FontFamily="Consolas"
|
||||
TextWrapping="Wrap" AcceptsReturn="True"
|
||||
Text="F7 32 01 11 00"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<CheckBox x:Name="ChkAutoSum" Content="XOR/ADD 자동추가" IsChecked="True" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||
<Button Content="HEX 전송" Click="OnSendRaw"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<GridSplitter Grid.Column="1" Width="6" HorizontalAlignment="Stretch" Background="Transparent"/>
|
||||
|
||||
<!-- ===== 우측 : 송신/수신 로그 ===== -->
|
||||
<Grid Grid.Column="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="6"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 송신 로그 -->
|
||||
<Border Grid.Row="0" Background="White" BorderBrush="#D0D5DD" BorderThickness="1" CornerRadius="6">
|
||||
<DockPanel>
|
||||
<Border DockPanel.Dock="Top" Background="#0B3D91" CornerRadius="6,6,0,0" Padding="10,6">
|
||||
<DockPanel>
|
||||
<TextBlock Text="▶ 송신 (TX)" Foreground="White" FontWeight="Bold" FontSize="14"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" DockPanel.Dock="Right">
|
||||
<CheckBox x:Name="ChkTxAutoScroll" Content="자동스크롤" IsChecked="True" Foreground="White" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||
<Button Content="지우기" Click="OnClearTx" MinWidth="60"/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<TextBox x:Name="TxtTxLog" Style="{StaticResource Mono}" Background="#10243F" BorderThickness="0"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<GridSplitter Grid.Row="1" Height="6" HorizontalAlignment="Stretch" Background="Transparent"/>
|
||||
|
||||
<!-- 수신 로그 -->
|
||||
<Border Grid.Row="2" Background="White" BorderBrush="#D0D5DD" BorderThickness="1" CornerRadius="6">
|
||||
<DockPanel>
|
||||
<Border DockPanel.Dock="Top" Background="#0A6B3B" CornerRadius="6,6,0,0" Padding="10,6">
|
||||
<DockPanel>
|
||||
<TextBlock Text="◀ 수신 (RX)" Foreground="White" FontWeight="Bold" FontSize="14"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" DockPanel.Dock="Right">
|
||||
<CheckBox x:Name="ChkRxAutoScroll" Content="자동스크롤" IsChecked="True" Foreground="White" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||
<Button Content="지우기" Click="OnClearRx" MinWidth="60"/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<TextBox x:Name="TxtRxLog" Style="{StaticResource Mono}" Background="#0E2418" BorderThickness="0"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,286 @@
|
||||
using System.IO.Ports;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace CvnetPacketProgram;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private SerialPort? _port;
|
||||
private readonly FrameParser _parser = new();
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitUi();
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 초기화
|
||||
// ====================================================================
|
||||
private void InitUi()
|
||||
{
|
||||
foreach (var b in new[] { 9600, 19200, 38400, 57600, 115200, 4800, 2400 })
|
||||
CmbBaud.Items.Add(b);
|
||||
CmbBaud.SelectedIndex = 0; // 9600 기본
|
||||
|
||||
// 송신은 마스터(월패드) 측 요청만 — 응답(0x91/0xD1)은 ERV가 보내며 RX 로그에서 디코딩한다.
|
||||
CmbCmd.Items.Add(new CmdItem(Cvnet.CmdStatusQuery, "상태 조회 (0x11)"));
|
||||
CmbCmd.Items.Add(new CmdItem(Cvnet.CmdCtrlReq, "상세 제어 요구 (0x51)"));
|
||||
|
||||
foreach (var m in Cvnet.Modes) CmbMode.Items.Add(new ByteItem(m.val, $"0x{m.val:X2} {m.name}"));
|
||||
foreach (var fobj in Cvnet.Fans) CmbFan.Items.Add(new ByteItem(fobj.val, $"0x{fobj.val:X2} {fobj.name}"));
|
||||
CmbMode.SelectedIndex = 2; // 수동 일반
|
||||
CmbFan.SelectedIndex = 1; // 약
|
||||
|
||||
RefreshPorts();
|
||||
CmbCmd.SelectedIndex = 1; // 상세 제어 요구 기본
|
||||
}
|
||||
|
||||
private void RefreshPorts()
|
||||
{
|
||||
string? cur = CmbPort.SelectedItem as string;
|
||||
CmbPort.Items.Clear();
|
||||
foreach (var p in SerialPort.GetPortNames().OrderBy(NaturalKey))
|
||||
CmbPort.Items.Add(p);
|
||||
if (cur != null && CmbPort.Items.Contains(cur)) CmbPort.SelectedItem = cur;
|
||||
else if (CmbPort.Items.Count > 0) CmbPort.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private static int NaturalKey(string s)
|
||||
=> int.TryParse(new string(s.Where(char.IsDigit).ToArray()), out int n) ? n : 0;
|
||||
|
||||
private void OnRefreshPorts(object sender, RoutedEventArgs e) => RefreshPorts();
|
||||
|
||||
// ====================================================================
|
||||
// 연결
|
||||
// ====================================================================
|
||||
private void OnToggleOpen(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_port is { IsOpen: true }) { ClosePort(); return; }
|
||||
|
||||
if (CmbPort.SelectedItem is not string portName)
|
||||
{
|
||||
MessageBox.Show("COM 포트를 선택하세요.", "포트", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
_port = new SerialPort(portName, (int)CmbBaud.SelectedItem!, Parity.None, 8, StopBits.One)
|
||||
{
|
||||
ReadTimeout = 500,
|
||||
WriteTimeout = 500,
|
||||
};
|
||||
_port.DataReceived += OnDataReceived;
|
||||
_parser.Clear();
|
||||
_port.Open();
|
||||
SetConnUi(true, portName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_port = null;
|
||||
MessageBox.Show($"포트 열기 실패:\n{ex.Message}", "오류", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClosePort()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_port != null)
|
||||
{
|
||||
_port.DataReceived -= OnDataReceived;
|
||||
if (_port.IsOpen) _port.Close();
|
||||
_port.Dispose();
|
||||
}
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
_port = null;
|
||||
SetConnUi(false, null);
|
||||
}
|
||||
|
||||
private void SetConnUi(bool open, string? port)
|
||||
{
|
||||
LedConn.Fill = open
|
||||
? System.Windows.Media.Brushes.LimeGreen
|
||||
: new System.Windows.Media.SolidColorBrush(
|
||||
System.Windows.Media.Color.FromRgb(0xC0, 0x39, 0x2B));
|
||||
TxtConn.Text = open ? $"{port} 연결됨" : "닫힘";
|
||||
BtnOpen.Content = open ? "닫기" : "열기";
|
||||
CmbPort.IsEnabled = !open;
|
||||
CmbBaud.IsEnabled = !open;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 수신
|
||||
// ====================================================================
|
||||
private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sp = _port;
|
||||
if (sp is not { IsOpen: true }) return;
|
||||
int n = sp.BytesToRead;
|
||||
if (n <= 0) return;
|
||||
var buf = new byte[n];
|
||||
int read = sp.Read(buf, 0, n);
|
||||
_parser.Append(buf, read);
|
||||
|
||||
var frames = _parser.Extract();
|
||||
if (frames.Count == 0) return;
|
||||
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
foreach (var f in frames)
|
||||
AppendLog(TxtRxLog, ChkRxAutoScroll, "RX", f, Cvnet.Decode(f));
|
||||
});
|
||||
}
|
||||
catch { /* 수신 중 포트 닫힘 등 무시 */ }
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Cmd 변경 → 입력 패널 토글
|
||||
// ====================================================================
|
||||
private void OnCmdChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (CmbCmd.SelectedItem is not CmdItem ci) return;
|
||||
bool isQuery = ci.Cmd == Cvnet.CmdStatusQuery;
|
||||
bool isCtrl = ci.Cmd == Cvnet.CmdCtrlReq;
|
||||
|
||||
if (GrpFields != null) GrpFields.Visibility = isQuery ? Visibility.Collapsed : Visibility.Visible;
|
||||
if (PnlCtrlFlags != null) PnlCtrlFlags.Visibility = isCtrl ? Visibility.Visible : Visibility.Collapsed;
|
||||
OnBuild(sender, e);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 빌드 / 전송
|
||||
// ====================================================================
|
||||
private byte[]? BuildPacket()
|
||||
{
|
||||
if (CmbCmd.SelectedItem is not CmdItem ci) return null;
|
||||
byte subId = ParseHexByte(TxtSubId.Text, 0x01);
|
||||
|
||||
byte mode = (CmbMode.SelectedItem as ByteItem)?.Val ?? 0x00;
|
||||
byte fan = (CmbFan.SelectedItem as ByteItem)?.Val ?? 0x00;
|
||||
byte reserve = ParseDecByte(TxtReserve.Text, 0);
|
||||
|
||||
switch (ci.Cmd)
|
||||
{
|
||||
case Cvnet.CmdStatusQuery:
|
||||
return Cvnet.BuildStatusQuery(subId);
|
||||
|
||||
case Cvnet.CmdCtrlReq:
|
||||
return Cvnet.BuildCtrlReq(
|
||||
subId,
|
||||
mode, ChkModeFlag.IsChecked == true, ChkBasic.IsChecked == true, ChkRange.IsChecked == true,
|
||||
fan, ChkFanFlag.IsChecked == true, ChkFilterReset.IsChecked == true,
|
||||
reserve, ChkRsvFlag.IsChecked == true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void OnBuild(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var pkt = BuildPacket();
|
||||
if (pkt != null) TxtPreview.Text = Cvnet.Hex(pkt);
|
||||
}
|
||||
|
||||
private void OnSendBuilt(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var pkt = BuildPacket();
|
||||
if (pkt == null) return;
|
||||
TxtPreview.Text = Cvnet.Hex(pkt);
|
||||
SendBytes(pkt);
|
||||
}
|
||||
|
||||
private void OnSendRaw(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var bytes = ParseHexString(TxtRawHex.Text);
|
||||
if (bytes.Count == 0)
|
||||
{
|
||||
MessageBox.Show("유효한 HEX 바이트가 없습니다.", "HEX", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
if (ChkAutoSum.IsChecked == true)
|
||||
bytes = Cvnet.Finalize(bytes).ToList();
|
||||
SendBytes(bytes.ToArray());
|
||||
}
|
||||
|
||||
private void SendBytes(byte[] pkt)
|
||||
{
|
||||
if (_port is not { IsOpen: true })
|
||||
{
|
||||
// 포트 미연결이어도 로그에는 남겨 패킷을 확인할 수 있게 한다.
|
||||
AppendLog(TxtTxLog, ChkTxAutoScroll, "TX(미연결)", pkt, Cvnet.Decode(pkt));
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
_port.Write(pkt, 0, pkt.Length);
|
||||
AppendLog(TxtTxLog, ChkTxAutoScroll, "TX", pkt, Cvnet.Decode(pkt));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"전송 실패:\n{ex.Message}", "오류", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 로그
|
||||
// ====================================================================
|
||||
private static void AppendLog(TextBox box, CheckBox autoScroll, string tag, byte[] frame, string decode)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var sb = new StringBuilder();
|
||||
sb.Append('[').Append(now.ToString("HH:mm:ss.fff")).Append("] ")
|
||||
.Append(tag).Append(" (").Append(frame.Length).Append("B)\n");
|
||||
sb.Append(" HEX: ").Append(Cvnet.Hex(frame)).Append('\n');
|
||||
sb.Append(IndentLines(decode)).Append("\n\n");
|
||||
|
||||
box.AppendText(sb.ToString());
|
||||
if (autoScroll.IsChecked == true) box.ScrollToEnd();
|
||||
}
|
||||
|
||||
private static string IndentLines(string s)
|
||||
{
|
||||
var lines = s.Replace("\r", "").Split('\n');
|
||||
return string.Join('\n', lines.Select(l => " " + l));
|
||||
}
|
||||
|
||||
private void OnClearTx(object sender, RoutedEventArgs e) => TxtTxLog.Clear();
|
||||
private void OnClearRx(object sender, RoutedEventArgs e) => TxtRxLog.Clear();
|
||||
|
||||
// ====================================================================
|
||||
// 파서 유틸
|
||||
// ====================================================================
|
||||
private static byte ParseHexByte(string s, byte def)
|
||||
=> byte.TryParse(s?.Trim().Replace("0x", "", StringComparison.OrdinalIgnoreCase),
|
||||
System.Globalization.NumberStyles.HexNumber, null, out var v) ? v : def;
|
||||
|
||||
private static byte ParseDecByte(string s, byte def)
|
||||
=> byte.TryParse(s?.Trim(), out var v) ? v : def;
|
||||
|
||||
private static List<byte> ParseHexString(string s)
|
||||
{
|
||||
var list = new List<byte>();
|
||||
if (string.IsNullOrWhiteSpace(s)) return list;
|
||||
var tokens = s.Replace("0x", " ", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(",", " ").Replace("\r", " ").Replace("\n", " ").Replace("\t", " ")
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var t in tokens)
|
||||
if (byte.TryParse(t, System.Globalization.NumberStyles.HexNumber, null, out var v))
|
||||
list.Add(v);
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
ClosePort();
|
||||
base.OnClosed(e);
|
||||
}
|
||||
|
||||
// 콤보 항목 ----------------------------------------------------------
|
||||
private sealed record CmdItem(byte Cmd, string Text) { public override string ToString() => Text; }
|
||||
private sealed record ByteItem(byte Val, string Text) { public override string ToString() => Text; }
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
using System.Text;
|
||||
|
||||
namespace CvnetPacketProgram;
|
||||
|
||||
// ============================================================================
|
||||
// 대림 환기 프로토콜 (20230824 시트) — 구현은 해당 시트 내용만 근거로 함.
|
||||
//
|
||||
// 공통 프레임 구조
|
||||
// Header(0xF7) | Device(0x32) | Sub ID | Cmd | Len | Data[Len] | XOR SUM | ADD SUM
|
||||
//
|
||||
// 비트 표기: 시트의 "BIT 8 7 6 5 4 3 2 1" 기준 (bit8 = 0x80(MSB), bit1 = 0x01(LSB))
|
||||
//
|
||||
// 체크섬 (시트: "기존 대림 3.0 기준" — 표준 대림 3.0 방식)
|
||||
// XOR SUM = Header ~ 마지막 Data 까지 전체 XOR
|
||||
// ADD SUM = (Header ~ XOR SUM 까지 전체 합) & 0xFF
|
||||
// ============================================================================
|
||||
public static class Cvnet
|
||||
{
|
||||
public const byte Header = 0xF7;
|
||||
public const byte Device = 0x32;
|
||||
|
||||
// Cmd (시트)
|
||||
public const byte CmdStatusQuery = 0x11; // 상태 조회 (요청) Len 0
|
||||
public const byte CmdStatusResp = 0x91; // 상태 조회 응답 Len 6
|
||||
public const byte CmdCtrlReq = 0x51; // 상세 제어 요구 (요청) Len 3
|
||||
public const byte CmdCtrlResp = 0xD1; // 상세 제어 요구 응답 Len 6
|
||||
|
||||
// 모드 상태 (Data 하위 4bit)
|
||||
public static readonly (byte val, string name)[] Modes =
|
||||
{
|
||||
(0x00, "정지(꺼짐)"),
|
||||
(0x01, "자동 - Matrix(환기)"),
|
||||
(0x02, "수동 일반(환기)"),
|
||||
(0x03, "스케쥴(사용안함)"),
|
||||
(0x04, "바이패스"),
|
||||
(0x05, "공기청정"),
|
||||
(0x06, "히터운전(자동포함)"),
|
||||
(0x0A, "자동 - Matrix(공기청정)"),
|
||||
};
|
||||
|
||||
// 풍량 정도 (bit5~7, 3bit)
|
||||
public static readonly (byte val, string name)[] Fans =
|
||||
{
|
||||
(0x00, "꺼짐"),
|
||||
(0x01, "약"),
|
||||
(0x02, "중"),
|
||||
(0x03, "강"),
|
||||
};
|
||||
|
||||
public static string ModeName(int v) => Lookup(Modes, (byte)(v & 0x0F));
|
||||
public static string FanName(int v) => Lookup(Fans, (byte)(v & 0x07));
|
||||
|
||||
private static string Lookup((byte val, string name)[] table, byte v)
|
||||
{
|
||||
foreach (var t in table) if (t.val == v) return t.name;
|
||||
return $"미정의(0x{v:X2})";
|
||||
}
|
||||
|
||||
public static string CmdName(byte cmd) => cmd switch
|
||||
{
|
||||
CmdStatusQuery => "상태 조회",
|
||||
CmdStatusResp => "상태 조회 응답",
|
||||
CmdCtrlReq => "상세 제어 요구",
|
||||
CmdCtrlResp => "상세 제어 요구 응답",
|
||||
_ => $"미정의 Cmd(0x{cmd:X2})",
|
||||
};
|
||||
|
||||
// ---- 체크섬 ----------------------------------------------------------
|
||||
public static byte Xor(IReadOnlyList<byte> bytes, int start, int count)
|
||||
{
|
||||
byte x = 0;
|
||||
for (int i = start; i < start + count; i++) x ^= bytes[i];
|
||||
return x;
|
||||
}
|
||||
|
||||
public static byte Add(IReadOnlyList<byte> bytes, int start, int count)
|
||||
{
|
||||
int s = 0;
|
||||
for (int i = start; i < start + count; i++) s += bytes[i];
|
||||
return (byte)(s & 0xFF);
|
||||
}
|
||||
|
||||
/// <summary>Header~Data 까지 채워진 프레임에 XOR/ADD 2바이트를 덧붙여 완성한다.</summary>
|
||||
public static byte[] Finalize(List<byte> body)
|
||||
{
|
||||
byte xor = Xor(body, 0, body.Count);
|
||||
body.Add(xor);
|
||||
byte add = Add(body, 0, body.Count); // XOR 포함 합
|
||||
body.Add(add);
|
||||
return body.ToArray();
|
||||
}
|
||||
|
||||
public static string Hex(IReadOnlyList<byte> b)
|
||||
{
|
||||
var sb = new StringBuilder(b.Count * 3);
|
||||
for (int i = 0; i < b.Count; i++)
|
||||
{
|
||||
if (i > 0) sb.Append(' ');
|
||||
sb.Append(b[i].ToString("X2"));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 프레임 빌더
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>상태 조회 (0x11), Len 0</summary>
|
||||
public static byte[] BuildStatusQuery(byte subId)
|
||||
{
|
||||
var body = new List<byte> { Header, Device, subId, CmdStatusQuery, 0x00 };
|
||||
return Finalize(body);
|
||||
}
|
||||
|
||||
/// <summary>상세 제어 요구 (0x51), Len 3</summary>
|
||||
public static byte[] BuildCtrlReq(
|
||||
byte subId,
|
||||
byte mode, bool modeFlag, bool basicMode, bool rangeLink,
|
||||
byte fan, bool fanFlag, bool filterTimerReset,
|
||||
byte reserveHour, bool reserveFlag)
|
||||
{
|
||||
// Data0: bit8 기저모드, bit7 렌지연동, bit5 모드Flag, bit1~4 모드상태
|
||||
byte d0 = (byte)(mode & 0x0F);
|
||||
if (modeFlag) d0 |= 0x10;
|
||||
if (rangeLink) d0 |= 0x40;
|
||||
if (basicMode) d0 |= 0x80;
|
||||
|
||||
// Data1: bit8 풍량Flag, bit5~7 풍량정도, bit1 필터타이머리셋
|
||||
byte d1 = (byte)((fan & 0x07) << 4);
|
||||
if (filterTimerReset) d1 |= 0x01;
|
||||
if (fanFlag) d1 |= 0x80;
|
||||
|
||||
// Data2: bit6 예약Flag, bit1~5 꺼짐예약 설정시간(0x1F=예약끄기/연속)
|
||||
byte d2 = (byte)(reserveHour & 0x1F);
|
||||
if (reserveFlag) d2 |= 0x20;
|
||||
|
||||
var body = new List<byte> { Header, Device, subId, CmdCtrlReq, 0x03, d0, d1, d2 };
|
||||
return Finalize(body);
|
||||
}
|
||||
|
||||
// 응답(0x91 상태 응답 / 0xD1 제어 응답)은 ERV가 송신하므로 빌더 없음.
|
||||
// 수신 프레임은 아래 Decode()에서 해석한다.
|
||||
|
||||
// ====================================================================
|
||||
// 디코더 — 수신 프레임 해석
|
||||
// ====================================================================
|
||||
public static string Decode(byte[] f)
|
||||
{
|
||||
if (f.Length < 7) return "(길이 부족 — 최소 7바이트)";
|
||||
var sb = new StringBuilder();
|
||||
byte subId = f[2];
|
||||
byte cmd = f[3];
|
||||
byte len = f[4];
|
||||
|
||||
sb.AppendLine($"Header=0x{f[0]:X2} Device=0x{f[1]:X2} Sub ID=0x{subId:X2} Cmd=0x{cmd:X2} ({CmdName(cmd)}) Len={len}");
|
||||
|
||||
int dataStart = 5;
|
||||
int need = 5 + len + 2;
|
||||
bool lenOk = f.Length >= need;
|
||||
if (!lenOk)
|
||||
{
|
||||
sb.AppendLine($" ! Len 기준 필요 길이 {need}B, 실제 {f.Length}B");
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
byte rxXor = f[5 + len];
|
||||
byte rxAdd = f[5 + len + 1];
|
||||
byte calcXor = Xor(f, 0, 5 + len);
|
||||
byte calcAdd = Add(f, 0, 5 + len + 1); // XOR 포함
|
||||
string xorMark = rxXor == calcXor ? "OK" : $"X (계산 0x{calcXor:X2})";
|
||||
string addMark = rxAdd == calcAdd ? "OK" : $"X (계산 0x{calcAdd:X2})";
|
||||
|
||||
switch (cmd)
|
||||
{
|
||||
case CmdStatusQuery:
|
||||
sb.AppendLine(" [상태 조회 요청]");
|
||||
break;
|
||||
|
||||
case CmdCtrlReq when len >= 3:
|
||||
DecodeCtrlReq(sb, f, dataStart);
|
||||
break;
|
||||
|
||||
case CmdStatusResp when len >= 6:
|
||||
case CmdCtrlResp when len >= 6:
|
||||
DecodeResponse(sb, f, dataStart);
|
||||
break;
|
||||
|
||||
default:
|
||||
if (len > 0)
|
||||
sb.AppendLine($" Data: {Hex(f[dataStart..(dataStart + len)])}");
|
||||
break;
|
||||
}
|
||||
|
||||
sb.Append($" XOR=0x{rxXor:X2} [{xorMark}] ADD=0x{rxAdd:X2} [{addMark}]");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void DecodeCtrlReq(StringBuilder sb, byte[] f, int s)
|
||||
{
|
||||
byte d0 = f[s], d1 = f[s + 1], d2 = f[s + 2];
|
||||
sb.AppendLine(" [상세 제어 요구]");
|
||||
sb.AppendLine($" Data0=0x{d0:X2} 모드={ModeName(d0 & 0x0F)}"
|
||||
+ $" 모드Flag={Bit(d0, 0x10)} 렌지연동={Bit(d0, 0x40)} 기저모드={Bit(d0, 0x80)}");
|
||||
sb.AppendLine($" Data1=0x{d1:X2} 풍량={FanName((d1 >> 4) & 0x07)}"
|
||||
+ $" 풍량Flag={Bit(d1, 0x80)} 필터타이머리셋={Bit(d1, 0x01)}");
|
||||
sb.AppendLine($" Data2=0x{d2:X2} 예약설정시간={ReserveSet(d2 & 0x1F)} 예약Flag={Bit(d2, 0x20)}");
|
||||
}
|
||||
|
||||
private static void DecodeResponse(StringBuilder sb, byte[] f, int s)
|
||||
{
|
||||
byte d0 = f[s], d1 = f[s + 1], d2 = f[s + 2], d3 = f[s + 3], d4 = f[s + 4], d5 = f[s + 5];
|
||||
sb.AppendLine($" Data0=0x{d0:X2} [에러] 장비보호={Bit(d0,0x80)} 미세먼지센서={Bit(d0,0x40)} 배기팬={Bit(d0,0x20)} 급기팬={Bit(d0,0x10)} 내부통신(룸콘)={Bit(d0,0x08)} CO2센서={Bit(d0,0x04)} 소자교환={Bit(d0,0x02)} 온/습도센서={Bit(d0,0x01)}");
|
||||
sb.AppendLine($" Data1=0x{d1:X2} 모드={ModeName(d1 & 0x0F)} 기저모드={Bit(d1,0x80)} 렌지연동={Bit(d1,0x40)}");
|
||||
sb.AppendLine($" Data2=0x{d2:X2} 풍량={FanName((d2 >> 4) & 0x07)} 필터청소={Bit(d2,0x02)} 필터교환={Bit(d2,0x01)}");
|
||||
sb.AppendLine($" Data3=0x{d3:X2} 예약설정시간={ReserveSet(d3 & 0x1F)} 예약상태={Bit(d3,0x20)}");
|
||||
sb.AppendLine($" Data4=0x{d4:X2} 남은시간(시)={d4}");
|
||||
sb.AppendLine($" Data5=0x{d5:X2} 남은시간(분)={d5}");
|
||||
}
|
||||
|
||||
private static string Bit(byte v, byte mask) => (v & mask) != 0 ? "ON" : "off";
|
||||
|
||||
private static string ReserveSet(int v) => v switch
|
||||
{
|
||||
0x00 => "0(없음)",
|
||||
0x1F => "예약끄기(연속)",
|
||||
_ => $"{v}시간",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="CvnetPacketProgram.app" />
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
Binary file not shown.
@@ -1,489 +0,0 @@
|
||||
# 각실제어 내부 통신 프로토콜 (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 코드·필드 세부값을 상호 합의하여 확정한다.
|
||||
@@ -1,546 +0,0 @@
|
||||
# 각실제어 내부 통신 프로토콜 (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.
Reference in New Issue
Block a user