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? PacketReceived; public event Action? 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)}"); } } }