Files
HECO2/Simulator/ERVSimulator/Program/Protocol/HoodMasterProtocol.cs
T
jeon a502322188 chore: HERV 통합 저장소 재초기화 커밋
손상된 .git 히스토리(missing tree)로 재초기화 후 작업트리 전체 커밋.
.claude/ 만 제외(로컬 에이전트 설정). 구 저장소 백업(.git_corrupt_backup/) 포함.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 09:32:17 +09:00

144 lines
6.0 KiB
C#

using System;
using System.Windows.Threading;
using ERVSimulator.Model;
using RunMode = ERVSimulator.Model.RunMode;
namespace ERVSimulator.Protocol
{
// 후드 버스 마스터 (115200) <-> 후드메인(슬레이브)
// 규격 : Protocol/HOOD/주신전자_protocol_hood_전열교환기_Rev1.3_20241125.xlsx
// - 9바이트 고정, 폴링주기 100~500ms, 응답 50ms 이내, CS = Preamble~CS직전 전체 XOR
// 목적 : ERV(Master) 가 후드메인(Slave) 을 폴 → 후드 FAN/LIGHT/연동CMD 수신 → ErvState 반영
// 마스터 폴(9B) : Preamble | M/S(0x21) | ID | MODE | FAN | 연동EN | 연동운전중 | ERROR | CS
// 슬레이브 응답(9B) : Preamble | M/S(0x11) | ID | FAN STATUS | LIGHT STATUS | 0x00 | 연동CMD | ERROR | CS
public class HoodMasterProtocol
{
const byte PREAMBLE = 0xAA;
const byte MS_MASTER = 0x21;
const byte MS_SLAVE = 0x11;
const byte HOOD_ID = 0x01;
const int FRAME_LEN = 9;
readonly SerialChannel _ch;
readonly ErvState _state;
readonly Dispatcher _dispatcher;
readonly DispatcherTimer _pollTimer;
readonly byte[] _rx = new byte[FRAME_LEN];
int _rxPos;
DateTime _lastByte = DateTime.MinValue;
static readonly TimeSpan FrameGap = TimeSpan.FromMilliseconds(40);
public event Action<string>? PacketReceived;
public event Action<string>? PacketSent;
public bool Verbose { get; set; } = false; // true면 모든 폴 로그
// 후드 통신 생존 표시용 — 마지막으로 유효한 응답을 받은 시각(UTC)
public DateTime LastRxUtc { get; private set; } = DateTime.MinValue;
// 폴(200ms) 기준 이 시간 내 응답이 없으면 미연결로 판정 (몇 회 누락 허용)
static readonly TimeSpan ConnTimeout = TimeSpan.FromMilliseconds(1000);
public HoodMasterProtocol(SerialChannel ch, ErvState state, Dispatcher dispatcher)
{
_ch = ch; _state = state; _dispatcher = dispatcher;
_ch.ByteReceived += OnByte;
// 폴링주기 200ms (사양 100~500ms 범위 내)
_pollTimer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(200) };
_pollTimer.Tick += (_, _) =>
{
if (_ch.IsConnected) Poll();
// 폴 주기마다 통신 생존 갱신 : 채널 연결 && 최근 응답 수신 → 연결됨
_state.HoodConnected = _ch.IsConnected && (DateTime.UtcNow - LastRxUtc) < ConnTimeout;
};
_pollTimer.Start();
}
// MODE : 전원 OFF시 0, ON시 1 환기 / 2 자동 / 3 공청 / 4 바이패스 / 5 기타
byte HoodMode()
{
if (!_state.PowerOn) return 0;
return _state.RunMode switch
{
RunMode.Ventilation => 1,
RunMode.Auto => 2,
RunMode.AirClean => 3,
RunMode.Bypass => 4,
RunMode.Off => 0,
_ => 5,
};
}
void Poll()
{
var p = new byte[FRAME_LEN];
p[0] = PREAMBLE;
p[1] = MS_MASTER;
p[2] = HOOD_ID;
p[3] = HoodMode();
p[4] = _state.FanMode; // 전열교환기 FAN 0 OFF, 1~5단
p[5] = (byte)(_state.HoodEnable ? 0x01 : 0x00); // 연동 Enable/Disable
p[6] = (byte)(_state.HoodStatus ? 0x01 : 0x00); // 연동 운전중(후드 연동에 의한 환기장치 동작중)
p[7] = 0x00; // ERROR
p[8] = ChecksumHelper.Xor(p, 0, 8); // CS = Preamble~CS직전 XOR
_ch.Send(p, FRAME_LEN);
if (Verbose)
PacketSent?.Invoke($"Hood TX poll MODE={p[3]} FAN={p[4]} EN={p[5]} 연동운전={p[6]}");
}
void OnByte(byte b)
{
var now = DateTime.UtcNow;
if (now - _lastByte > FrameGap) _rxPos = 0;
_lastByte = now;
if (_rxPos == 0)
{
if (b == PREAMBLE) { _rx[0] = b; _rxPos = 1; }
}
else if (_rxPos == 1)
{
if (b == MS_SLAVE) { _rx[1] = b; _rxPos = 2; }
else _rxPos = (b == PREAMBLE) ? 1 : 0;
}
else
{
_rx[_rxPos++] = b;
if (_rxPos >= FRAME_LEN)
{
var copy = (byte[])_rx.Clone();
_dispatcher.BeginInvoke(new Action(() => HandleResponse(copy)));
_rxPos = 0;
}
}
}
void HandleResponse(byte[] p)
{
byte cs = ChecksumHelper.Xor(p, 0, 8);
if (cs != p[8])
{
PacketReceived?.Invoke($"Hood RX CS오류 {HexFormat.Bytes(p, FRAME_LEN)}");
return;
}
if (p[2] != HOOD_ID) return;
LastRxUtc = DateTime.UtcNow; // 유효 응답 수신 → 통신 생존
_state.HoodConnected = true; // 응답 받았으므로 즉시 연결 표시
int fan = p[3]; // 후드 FAN STATUS : 0 OFF, 1~5단
bool light = p[4] != 0; // 후드 LIGHT STATUS : 0 OFF, 1 ON
bool cmd = p[6] != 0; // 연동 CMD : 0 후드 꺼짐 / 1 후드 켜짐
int err = p[7];
bool changed = _state.HoodFan != fan || _state.HoodLight != light || _state.HoodCmd != cmd || _state.HoodError != err;
_state.HoodFan = fan;
_state.HoodLight = light;
_state.HoodCmd = cmd;
_state.HoodError = err;
// 연동운전중(HoodStatus)은 AutoLogic 이 메이크업 에어 상태(롤백 유지 포함)로 소유.
if (changed || Verbose)
PacketReceived?.Invoke($"Hood RX FAN={fan} LIGHT={(light ? "ON" : "OFF")} 연동CMD={(cmd ? "ON" : "OFF")}{(err != 0 ? $" ERR={err}" : "")}");
}
}
}