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>
144 lines
6.0 KiB
C#
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}" : "")}");
|
|
}
|
|
}
|
|
}
|