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:
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.IO.Ports;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace HoodSimulator
|
||||
{
|
||||
// 후드메인(Slave) 시뮬레이터 프로토콜
|
||||
// 규격 : Protocol/HOOD/주신전자_protocol_hood_전열교환기_Rev1.3_20241125.xlsx
|
||||
// - 9바이트 고정, 115200 8N1, 폴링주기 100~500ms, 응답 50ms 이내
|
||||
// - CS = Preamble~CS직전(byte0~7) 전체 XOR
|
||||
// Master(전열교환기) → Slave(후드) : AA 21 ID MODE FAN 연동EN 연동운전중 ERROR CS
|
||||
// Slave(후드) → Master(전열교환기) : AA 11 ID FANSTATUS LIGHTSTATUS 00 연동CMD ERROR CS
|
||||
// 시뮬레이터는 Slave 역할 — 마스터 폴 수신 시 현재 후드 상태로 응답.
|
||||
public class HoodProtocol : IDisposable
|
||||
{
|
||||
public const byte PREAMBLE = 0xAA;
|
||||
public const byte MS_MASTER = 0x21;
|
||||
public const byte MS_SLAVE = 0x11;
|
||||
public const byte HOOD_ID = 0x01;
|
||||
public const int FRAME_LEN = 9;
|
||||
|
||||
SerialPort? _port;
|
||||
CancellationTokenSource? _cts;
|
||||
readonly object _lock = new();
|
||||
bool _disposed;
|
||||
bool _responding;
|
||||
|
||||
// ---- 후드 상태 (UI 제어) ----
|
||||
public bool PowerOn; // 전원 on/off
|
||||
public byte FanStage; // 풍량 0(꺼짐)~5
|
||||
public bool Light; // 조명 on/off
|
||||
public byte ErrorCode; // ERROR : 0 정상 / 1 FAN 에러 / 2 기타 에러
|
||||
|
||||
public event Action<byte, byte, byte, byte>? MasterPacketReceived; // mode, fan, en, run
|
||||
public event Action<byte[]>? ResponseSent; // 송신한 9바이트 응답
|
||||
public event Action<string>? LogMessage;
|
||||
public event Action<bool>? ConnectionChanged;
|
||||
|
||||
public bool IsConnected => _port?.IsOpen == true;
|
||||
public bool IsResponding => _responding;
|
||||
|
||||
public static byte Xor(byte[] d, int start, int len)
|
||||
{
|
||||
byte x = 0;
|
||||
for (int i = 0; i < len; i++) x ^= d[start + i];
|
||||
return x;
|
||||
}
|
||||
|
||||
public string[] GetAvailablePorts() => SerialPort.GetPortNames();
|
||||
|
||||
public bool Connect(string portName)
|
||||
{
|
||||
try
|
||||
{
|
||||
Disconnect();
|
||||
_port = new SerialPort(portName)
|
||||
{
|
||||
BaudRate = 115200, DataBits = 8,
|
||||
StopBits = StopBits.One, Parity = Parity.None,
|
||||
ReadTimeout = 100, WriteTimeout = 500
|
||||
};
|
||||
_port.Open();
|
||||
Log($"[연결] {portName} (115200, 8N1)");
|
||||
ConnectionChanged?.Invoke(true);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"[오류] 연결 실패: {ex.Message}");
|
||||
ConnectionChanged?.Invoke(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
StopResponding();
|
||||
try { if (_port?.IsOpen == true) { _port.Close(); Log("[연결 해제]"); } } catch { }
|
||||
_port?.Dispose();
|
||||
_port = null;
|
||||
ConnectionChanged?.Invoke(false);
|
||||
}
|
||||
|
||||
public void StartResponding()
|
||||
{
|
||||
StopResponding();
|
||||
_responding = true;
|
||||
_cts = new CancellationTokenSource();
|
||||
var token = _cts.Token;
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_port?.IsOpen != true) { Thread.Sleep(50); continue; }
|
||||
if (_port.BytesToRead < 1) { Thread.Sleep(3); continue; }
|
||||
|
||||
byte b = (byte)_port.ReadByte();
|
||||
if (b != PREAMBLE) continue;
|
||||
|
||||
byte[] rx = new byte[FRAME_LEN];
|
||||
rx[0] = PREAMBLE;
|
||||
int total = 1, remain = FRAME_LEN - 1, retries = 100;
|
||||
while (remain > 0 && retries-- > 0)
|
||||
{
|
||||
if (_port.BytesToRead > 0)
|
||||
{ int r = _port.Read(rx, total, remain); total += r; remain -= r; }
|
||||
else Thread.Sleep(2);
|
||||
}
|
||||
if (total < FRAME_LEN) continue;
|
||||
|
||||
// 마스터 프레임만 처리
|
||||
if (rx[1] != MS_MASTER) continue;
|
||||
if (rx[2] != HOOD_ID) continue;
|
||||
byte cs = Xor(rx, 0, 8);
|
||||
if (cs != rx[8]) { Log($"[CS오류] 수신:0x{rx[8]:X2} 계산:0x{cs:X2} {BitConverter.ToString(rx)}"); continue; }
|
||||
|
||||
byte mode = rx[3], fan = rx[4], en = rx[5], run = rx[6];
|
||||
Log($"[RX] {BitConverter.ToString(rx)} MODE={mode} FAN={fan} 연동EN={en} 연동운전={run}");
|
||||
MasterPacketReceived?.Invoke(mode, fan, en, run);
|
||||
|
||||
// 응답 전송 (현재 후드 상태)
|
||||
byte[] tx = BuildResponse();
|
||||
lock (_lock) { _port?.Write(tx, 0, tx.Length); }
|
||||
Log($"[TX 응답] {BitConverter.ToString(tx)}");
|
||||
ResponseSent?.Invoke(tx);
|
||||
}
|
||||
catch (TimeoutException) { }
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!token.IsCancellationRequested) Log($"[오류] {ex.Message}");
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
}, token);
|
||||
Log("[통신 시작] 마스터 응답 모드");
|
||||
}
|
||||
|
||||
public void StopResponding()
|
||||
{
|
||||
_responding = false;
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
public byte[] BuildResponse()
|
||||
{
|
||||
byte fanStatus = PowerOn ? FanStage : (byte)0; // 후드 FAN STATUS 0~5
|
||||
byte lightStatus = (byte)((PowerOn && Light) ? 1 : 0); // 후드 LIGHT STATUS
|
||||
byte cmd = (byte)(PowerOn ? 1 : 0); // 연동 CMD : 0 꺼짐 / 1 켜짐
|
||||
|
||||
byte[] p = new byte[FRAME_LEN];
|
||||
p[0] = PREAMBLE;
|
||||
p[1] = MS_SLAVE;
|
||||
p[2] = HOOD_ID;
|
||||
p[3] = fanStatus;
|
||||
p[4] = lightStatus;
|
||||
p[5] = 0x00;
|
||||
p[6] = cmd;
|
||||
p[7] = ErrorCode; // ERROR : 0 정상 / 1 FAN / 2 기타
|
||||
p[8] = Xor(p, 0, 8); // CS
|
||||
return p;
|
||||
}
|
||||
|
||||
void Log(string msg) => LogMessage?.Invoke($"[{DateTime.Now:HH:mm:ss.fff}] {msg}");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
Disconnect();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user