chore: HERV 통합 저장소 재초기화 커밋
손상된 .git 히스토리(missing tree)로 재초기화 후 작업트리 전체 커밋. .claude/ 만 제외(로컬 에이전트 설정). 구 저장소 백업(.git_corrupt_backup/) 포함. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
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<string>? PacketReceived;
|
||||
public event Action<string>? 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")})");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user