using System; using System.Windows.Threading; using ERVSimulator.Model; using ErvProtocol; // 공용 Crc16 (bunbaegi CRC 도 표준 MODBUS 동일) using RunMode = ERVSimulator.Model.RunMode; // ErvProtocol.RunMode 와 이름 충돌 해소 namespace ERVSimulator.Protocol { // 디퓨저 버스 마스터 (115200) <-> DiffuserSimulator(슬레이브) // 규격 : Protocol/수정_Each_Room_Jushin_protocol_RS485_Rev1.2 (펌웨어 My_Uart.c bunbaegi 미러) // 목적 : DiffuserSimulator 로부터 각실 센서값(PM2.5/PM10/VOC/CO2) 수신 → ErvState → 자동로직 // - 마스터 폴(29B, 0x10): 실/타입(SA/RA)별 전원·모드·풍량·LED·댐퍼 송신 (poll-response 구조상 필수) // - 슬레이브 응답(39B, 0x01): 센서값 수신 // ※ ERVSim 은 각실 댐퍼+LED 를 자체 표시하지 않음(DiffuserSimulator 가 표시). 통신만 수행. public class DiffuserMasterProtocol { readonly SerialChannel _ch; readonly ErvState _state; readonly Dispatcher _dispatcher; readonly DispatcherTimer _pollTimer; int _pollIdx; // (room1 SA),(room1 RA)...(room4 RA) round-robin readonly byte[] _rx = new byte[39]; int _rxPos; DateTime _lastByte = DateTime.MinValue; static readonly TimeSpan FrameGap = TimeSpan.FromMilliseconds(40); public event Action? PacketReceived; public event Action? PacketSent; public bool Verbose { get; set; } = false; // true면 모든 폴 로그 public DiffuserMasterProtocol(SerialChannel ch, ErvState state, Dispatcher dispatcher) { _ch = ch; _state = state; _dispatcher = dispatcher; _ch.ByteReceived += OnByte; _pollTimer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(80) }; _pollTimer.Tick += (_, _) => { if (_ch.IsConnected) PollNext(); }; _pollTimer.Start(); } byte DiffRunMode() => _state.RunMode switch { RunMode.Ventilation => 0x01, RunMode.Auto => 0x02, RunMode.Bypass => 0x04, RunMode.AirClean => 0x08, _ => 0x01, }; void PollNext() { int room = _pollIdx / 2 + 1; // 1~4 byte id1 = (byte)(_pollIdx % 2 == 0 ? 0x01 : 0x02); // SA / RA _pollIdx = (_pollIdx + 1) % 8; var rm = _state.GetRoom(room); var p = new byte[29]; p[0] = 0xAA; p[1] = 0x10; p[2] = id1; p[3] = (byte)room; p[4] = 0x00; p[5] = (byte)(_state.PowerOn ? 1 : 0); p[6] = DiffRunMode(); p[7] = _state.FanMode; p[8] = (byte)rm.LightBright; p[9] = (byte)rm.AirQuality; p[10] = (byte)rm.CurrentSA; p[11] = (byte)rm.CurrentRA; ushort crc = Crc16.Modbus(p, 0, 27); // lo-first : 펌웨어 CRC16()이 표준MODBUS 바이트스왑값 반환 + [27]=icrc>>8 배치 → 와이어는 리틀엔디안 p[27] = (byte)(crc & 0xFF); p[28] = (byte)(crc >> 8); _ch.Send(p, 29); if (Verbose) PacketSent?.Invoke($"Diff TX poll room{room} {(id1 == 1 ? "SA" : "RA")} SA={rm.CurrentSA} RA={rm.CurrentRA} LED={rm.LightBright}"); } void OnByte(byte b) { var now = DateTime.UtcNow; if (now - _lastByte > FrameGap) _rxPos = 0; _lastByte = now; if (_rxPos == 0) { if (b == 0xAA) { _rx[0] = b; _rxPos = 1; } } else if (_rxPos == 1) { if (b == 0x01) { _rx[1] = b; _rxPos = 2; } else _rxPos = (b == 0xAA) ? 1 : 0; } else { _rx[_rxPos++] = b; if (_rxPos >= 39) { var copy = (byte[])_rx.Clone(); _dispatcher.BeginInvoke(new Action(() => HandleResponse(copy))); _rxPos = 0; } } } void HandleResponse(byte[] p) { ushort rxcrc = (ushort)(p[37] | (p[38] << 8)); // lo-first (표준 리틀엔디안) if (Crc16.Modbus(p, 0, 37) != rxcrc) { PacketReceived?.Invoke($"Diff RX CRC오류 {HexFormat.Bytes(p, 39)}"); return; } int id1 = p[2]; // 0x01 SA / 0x02 RA int room = p[3]; // 1~4 if (room < 1 || room > 4) return; // 센서 (응답 39B, 빅엔디안) : LED[8] PM10[12,13] PM2.5[16,17] 습도[20,21] 온도[22,23] VOC[24,25] CO2[28,29] int led = p[8]; // 디퓨저가 echo 한 실제 LED 단수 (수동 제어 시 ERV 명령과 다를 수 있음) int pm10 = (p[12] << 8) | p[13]; int pm25 = (p[16] << 8) | p[17]; int humi = (p[20] << 8) | p[21]; int temp = (p[22] << 8) | p[23]; int voc = (p[24] << 8) | p[25]; int co2 = (p[28] << 8) | p[29]; var rm = _state.GetRoom(room); bool changed = rm.Co2 != co2 || rm.Pm25 != pm25 || rm.Pm10 != pm10 || rm.Voc != voc || rm.Temp != temp || rm.Humi != humi || rm.LedReported != led; rm.Pm10 = pm10; rm.Pm25 = pm25; rm.Voc = voc; rm.Co2 = co2; rm.Temp = temp; rm.Humi = humi; rm.LedReported = led; if (changed || Verbose) PacketReceived?.Invoke($"Diff RX {rm.Name} 센서 CO2={co2} PM2.5={pm25} PM10={pm10} VOC={voc} 온도={temp} 습도={humi} LED={led} (from {(id1 == 1 ? "SA" : "RA")})"); } } }