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? MasterPacketReceived; // mode, fan, en, run public event Action? ResponseSent; // 송신한 9바이트 응답 public event Action? LogMessage; public event Action? 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); } } }