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
+180
View File
@@ -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);
}
}
}