5a96a696b1
- 펌웨어(program), C# 대시보드(TestProgram), 시뮬레이터(Simulator), 프로토콜/문서(Protocol, doc) 전체를 단일 저장소로 통합 - program 폴더의 별도 git 저장소를 제거하고 통합 저장소에 흡수 - 빌드 산출물(program/build, bin/obj, *.o/.elf/.bin/.hex 등) .gitignore 처리 - 사내 Synology NAS Git 원격 연결 예정 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
215 lines
8.2 KiB
C#
215 lines
8.2 KiB
C#
using System;
|
|
using ERVSimulator.Model;
|
|
|
|
namespace ERVSimulator.Protocol
|
|
{
|
|
// 룸콘 프로토콜 (UART2/SC0)
|
|
// 패킷: AA | Cmd | D[2..12] | XOR_SUM[13] | EE (15 byte)
|
|
// 펌웨어 [My_RJ2.c] rx_roomcon_check() + roomcon_parsing()
|
|
public class RoomConProtocol
|
|
{
|
|
public const byte HEADER = 0xAA;
|
|
public const byte TAIL = 0xEE;
|
|
public const int PACKET_LEN = 15;
|
|
|
|
// Cmd (Rx_roomcon232_buffer[1])
|
|
public const byte CMD_NORMAL = 0x00; // 상태 폴링
|
|
public const byte CMD_EVENT = 0x01; // 모드/팬 변경 이벤트
|
|
public const byte CMD_RESTART1 = 0x02; // 환기단 preset 요청
|
|
public const byte CMD_RESTART2 = 0x12; // bypass/air preset 요청
|
|
public const byte CMD_VSP = 0x03; // 테스트모드 진입
|
|
public const byte CMD_EXIT = 0x04; // 테스트모드 종료
|
|
public const byte CMD_HOOD_INFO = 0x0A; // ERV→룸콘 후드 연동 통지 (힘펠 V3.7 RX_DATA_HOOD_INFO)
|
|
|
|
readonly SerialChannel _ch;
|
|
readonly ErvState _state;
|
|
readonly DamperSequencer _seq;
|
|
readonly System.Windows.Threading.Dispatcher _dispatcher;
|
|
|
|
readonly byte[] _rx = new byte[PACKET_LEN];
|
|
int _rxPos;
|
|
bool _hoodLinkReported; // 마지막으로 룸콘에 통지한 후드 연동 상태(변화 시에만 0x0A 송신)
|
|
DateTime _lastByte = DateTime.MinValue;
|
|
static readonly TimeSpan FrameGap = TimeSpan.FromMilliseconds(50);
|
|
|
|
public event Action<string>? PacketReceived;
|
|
public event Action<string>? PacketSent;
|
|
|
|
public RoomConProtocol(SerialChannel ch, ErvState state, DamperSequencer seq,
|
|
System.Windows.Threading.Dispatcher dispatcher)
|
|
{
|
|
_ch = ch; _state = state; _seq = seq; _dispatcher = dispatcher;
|
|
_ch.ByteReceived += OnByte;
|
|
}
|
|
|
|
void OnByte(byte b)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
if (now - _lastByte > FrameGap) _rxPos = 0;
|
|
_lastByte = now;
|
|
|
|
// 펌웨어와 동일한 byte 파서 (My_RJ2.c:37)
|
|
if (_rxPos == 0)
|
|
{
|
|
if (b != HEADER) return;
|
|
_rx[_rxPos++] = b;
|
|
return;
|
|
}
|
|
if (_rxPos >= 1 && _rxPos <= 12)
|
|
{
|
|
_rx[_rxPos++] = b;
|
|
return;
|
|
}
|
|
if (_rxPos == 13)
|
|
{
|
|
byte cksum = ChecksumHelper.Xor(_rx, 0, 13);
|
|
if (cksum != b) { _rxPos = 0; return; }
|
|
_rx[_rxPos++] = b;
|
|
return;
|
|
}
|
|
if (_rxPos == 14)
|
|
{
|
|
_rxPos = 0;
|
|
if (b != TAIL) return;
|
|
byte[] copy = (byte[])_rx.Clone();
|
|
_dispatcher.BeginInvoke(new Action(() => HandlePacket(copy)));
|
|
}
|
|
}
|
|
|
|
void HandlePacket(byte[] p)
|
|
{
|
|
PacketReceived?.Invoke($"RoomCon RX: {HexFormat.Bytes(p, 14)} EE");
|
|
|
|
byte cmd = p[1];
|
|
switch (cmd)
|
|
{
|
|
case CMD_EVENT: HandleEvent(p); break;
|
|
case CMD_NORMAL: HandleNormal(p); break;
|
|
case CMD_RESTART1: HandleRestart1(); break;
|
|
case CMD_RESTART2: HandleRestart2(); break;
|
|
case CMD_VSP: HandleVsp(p); break;
|
|
case CMD_EXIT: HandleExit(); break;
|
|
default:
|
|
PacketReceived?.Invoke($" (unknown RoomCon cmd 0x{cmd:X2})");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// [My_RJ2.c:387] RX_DATA_MODE_EVENT - 운전 모드/팬 변경
|
|
void HandleEvent(byte[] p)
|
|
{
|
|
byte runMode = p[2];
|
|
byte fanMode = p[3];
|
|
_state.RunMode = (RunMode)runMode;
|
|
_state.SetRunMode = (RunMode)runMode;
|
|
_state.FanMode = fanMode;
|
|
_state.SetFanMode = fanMode;
|
|
// VENT && fan=0 ⇒ Power OFF 진입
|
|
_state.PowerOn = !(runMode == 0 && fanMode == 0);
|
|
|
|
// 예약 (룸콘 EVENT [10]=flag / [11]=시 / [12]=분). HOMENET STATUS(reserve)로도 전달 → 대시보드 반영
|
|
if (p[10] == 1)
|
|
{
|
|
int hours = p[11];
|
|
_state.ReserveHours = hours; // 0이면 해제
|
|
_state.ReserveRemainSec = hours * 3600 + p[12] * 60; // 카운트다운/전원OFF는 ReserveTick(1s) 처리
|
|
}
|
|
|
|
_seq.NotifyCommandChanged();
|
|
|
|
// 응답: AA 01 RunMode FanMode 00 misc... XOR EE (펌웨어 [My_RJ2.c:489])
|
|
var tx = NewPacket();
|
|
tx[1] = 0x01;
|
|
tx[2] = runMode;
|
|
tx[3] = fanMode;
|
|
tx[5] = 0; // Heater/UV/Kijer
|
|
tx[7] = _state.ErrorCode; // ErrorCode (E02/E07/E09/E10/COLD 비트맵)
|
|
tx[8] = 0; // Out_Temperature sign
|
|
tx[9] = 20 + 25; // Out_Temperature = 25
|
|
tx[10] = 20 + 22; // In_Temperature = 22
|
|
FinalizeAndSend(tx);
|
|
}
|
|
|
|
// [My_RJ2.c:327] RX_DATA_MODE_NORMAL - 상태 폴링 응답
|
|
void HandleNormal(byte[] p)
|
|
{
|
|
// 후드 연동 상태가 바뀌면 HOOD_INFO(0x0A)로 통지 (힘펠 V3.7, 펌웨어 Hood_info_command).
|
|
// HoodStatus = 연동운전중(후드 가동 → 메이크업 에어). 후드 OFF로 ERV 복귀 시 0x80(OFF) 전송.
|
|
if (_state.HoodStatus != _hoodLinkReported)
|
|
{
|
|
_hoodLinkReported = _state.HoodStatus;
|
|
var hi = NewPacket();
|
|
hi[1] = CMD_HOOD_INFO;
|
|
hi[2] = _state.HoodStatus ? (byte)RunMode.Ventilation : (byte)_state.SetRunMode; // 연동 시 환기
|
|
hi[3] = _state.HoodStatus ? (byte)1 : _state.SetFanMode;
|
|
hi[6] = _state.HoodStatus ? (byte)0x81 : (byte)0x80; // 후드 연동 ON / OFF
|
|
FinalizeAndSend(hi);
|
|
return;
|
|
}
|
|
|
|
var tx = NewPacket();
|
|
tx[1] = 0x07; // COMMAND_CONTROLL
|
|
tx[2] = (byte)_state.SetRunMode;
|
|
tx[3] = _state.SetFanMode;
|
|
tx[4] = 0; // Auto_Mode
|
|
tx[5] = 0;
|
|
tx[7] = _state.ErrorCode; // ErrorCode 도 동봉
|
|
FinalizeAndSend(tx);
|
|
}
|
|
|
|
// [My_RJ2.c:522] RX_DATA_MODE_RESTART1 - 환기 1~4단 preset
|
|
void HandleRestart1()
|
|
{
|
|
var tx = NewPacket();
|
|
tx[1] = 0x02;
|
|
tx[4] = 0x10;
|
|
tx[5] = (byte)_state.FanSAPreset_Vent[1]; tx[6] = (byte)_state.FanEAPreset_Vent[1];
|
|
tx[7] = (byte)_state.FanSAPreset_Vent[2]; tx[8] = (byte)_state.FanEAPreset_Vent[2];
|
|
tx[9] = (byte)_state.FanSAPreset_Vent[3]; tx[10] = (byte)_state.FanEAPreset_Vent[3];
|
|
tx[11] = (byte)_state.FanSAPreset_Vent[4]; tx[12] = (byte)_state.FanEAPreset_Vent[4];
|
|
FinalizeAndSend(tx);
|
|
}
|
|
|
|
// [My_RJ2.c:556] RX_DATA_MODE_RESTART2 - bypass/air preset
|
|
void HandleRestart2()
|
|
{
|
|
var tx = NewPacket();
|
|
tx[1] = 0x12;
|
|
tx[4] = 0x10;
|
|
tx[5] = (byte)_state.FanSAPreset_Bypass[1]; tx[6] = (byte)_state.FanEAPreset_Bypass[1];
|
|
tx[7] = (byte)_state.FanSAPreset_Air[1];
|
|
tx[8] = (byte)_state.FanSAPreset_Air[2];
|
|
tx[9] = (byte)_state.FanSAPreset_Air[3];
|
|
tx[10] = (byte)_state.FanSAPreset_Air[4];
|
|
FinalizeAndSend(tx);
|
|
}
|
|
|
|
// [My_RJ2.c:579] RX_DATA_MODE_VSP - 테스트 모드 진입 (preset 갱신)
|
|
void HandleVsp(byte[] p)
|
|
{
|
|
// 본 시뮬레이터에선 RX만 기록, preset 변경은 생략
|
|
PacketReceived?.Invoke($" VSP select={p[3]} sa={p[4]} ea={p[5]}");
|
|
}
|
|
|
|
void HandleExit()
|
|
{
|
|
PacketReceived?.Invoke(" VSP exit");
|
|
}
|
|
|
|
byte[] NewPacket()
|
|
{
|
|
var tx = new byte[PACKET_LEN];
|
|
tx[0] = HEADER;
|
|
return tx;
|
|
}
|
|
|
|
void FinalizeAndSend(byte[] tx)
|
|
{
|
|
tx[13] = ChecksumHelper.Xor(tx, 0, 13);
|
|
tx[14] = TAIL;
|
|
if (_ch.Send(tx, PACKET_LEN))
|
|
PacketSent?.Invoke($"RoomCon TX: {HexFormat.Bytes(tx, 15)}");
|
|
}
|
|
}
|
|
}
|