a502322188
손상된 .git 히스토리(missing tree)로 재초기화 후 작업트리 전체 커밋. .claude/ 만 제외(로컬 에이전트 설정). 구 저장소 백업(.git_corrupt_backup/) 포함. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
181 lines
6.8 KiB
C#
181 lines
6.8 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|