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
@@ -0,0 +1,131 @@
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);
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));
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")})");
}
}
}