chore: HERV 통합 저장소 초기 커밋

- 펌웨어(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>
This commit is contained in:
2026-06-15 21:44:23 +09:00
commit 5a96a696b1
265 changed files with 76458 additions and 0 deletions
@@ -0,0 +1,214 @@
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)}");
}
}
}