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:
@@ -0,0 +1,19 @@
|
||||
namespace ErvProtocol
|
||||
{
|
||||
// CRC-16/MODBUS (poly 0xA001 reflected, init 0xFFFF)
|
||||
// PC_ERV_Protocol.md 1장 - CMD~PAYLOAD 구간, 결과 리틀엔디안
|
||||
public static class Crc16
|
||||
{
|
||||
public static ushort Modbus(byte[] data, int start, int length)
|
||||
{
|
||||
ushort crc = 0xFFFF;
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
crc ^= data[start + i];
|
||||
for (int b = 0; b < 8; b++)
|
||||
crc = (crc & 0x0001) != 0 ? (ushort)((crc >> 1) ^ 0xA001) : (ushort)(crc >> 1);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
namespace ErvProtocol
|
||||
{
|
||||
// PC↔ERV 고정패킷 프레임 빌더 (PC_ERV_Protocol.md Rev2.0)
|
||||
// 모든 프레임은 항상 244B 고정 : 0xAA | CMD | DATA[240] | CRC16-MODBUS(LE)
|
||||
// - PC→ERV 제어 : DATA 앞쪽에 명령 인자, 나머지 0 패딩
|
||||
// - ERV→PC : STATUS(0x81) DATA=240B 상태, ACK(0x82) DATA[0]=echoCmd DATA[1]=result
|
||||
public static class CtrlFrame
|
||||
{
|
||||
public const int DataLen = 240; // DATA 고정 크기(=STATUS 페이로드)
|
||||
public const int FrameLen = 1 + 1 + DataLen + 2; // STX+CMD+DATA+CRC = 244
|
||||
|
||||
public const byte CTRL_POWER = 0x01;
|
||||
public const byte CTRL_RUNMODE = 0x02;
|
||||
public const byte CTRL_FAN = 0x03;
|
||||
public const byte CTRL_SUBMODE = 0x04;
|
||||
public const byte CTRL_HOOD = 0x05;
|
||||
public const byte CTRL_HYST_PRESET = 0x06;
|
||||
public const byte CTRL_HYST_VALUE = 0x07;
|
||||
public const byte CTRL_DAMPER = 0x08;
|
||||
public const byte CTRL_LED = 0x09;
|
||||
public const byte REQ_STATUS = 0x0A;
|
||||
public const byte CTRL_RESET = 0x0B;
|
||||
public const byte CTRL_VSP = 0x0C;
|
||||
public const byte CTRL_HYST_THR = 0x0D; // 오염단계 임계 설정 [preset][pollutant][L1~L4]
|
||||
public const byte CTRL_RESERVE = 0x0E; // (꺼짐)예약 [hours 0~8] : N시간 후 전원 OFF (0=해제)
|
||||
|
||||
// cmd + args 로 244B 고정 프레임 생성 (DATA 앞쪽에 args, 나머지 0, CRC 포함)
|
||||
public static byte[] Build(byte cmd, params byte[] args)
|
||||
{
|
||||
int argLen = args?.Length ?? 0;
|
||||
if (argLen > DataLen) argLen = DataLen; // 240B 초과분은 잘림(설계상 발생하지 않음)
|
||||
|
||||
var frame = new byte[FrameLen]; // 전부 0 초기화 = DATA 0 패딩
|
||||
frame[0] = 0xAA;
|
||||
frame[1] = cmd;
|
||||
if (argLen > 0) Array.Copy(args!, 0, frame, 2, argLen); // DATA[0..] = args
|
||||
|
||||
ushort crc = Crc16.Modbus(frame, 1, 1 + DataLen); // CMD + DATA[240] = 241B
|
||||
frame[2 + DataLen] = (byte)(crc & 0xFF); // CRC_L
|
||||
frame[2 + DataLen + 1] = (byte)(crc >> 8); // CRC_H
|
||||
return frame;
|
||||
}
|
||||
|
||||
static byte[] U16BE(int v) => new[] { (byte)((v >> 8) & 0xFF), (byte)(v & 0xFF) };
|
||||
|
||||
public static byte[] Power(int on) => Build(CTRL_POWER, (byte)(on != 0 ? 1 : 0));
|
||||
public static byte[] RunModeCmd(int m) => Build(CTRL_RUNMODE, (byte)m);
|
||||
public static byte[] Fan(int s) => Build(CTRL_FAN, (byte)s);
|
||||
public static byte[] SubMode(int t, int on) => Build(CTRL_SUBMODE, (byte)t, (byte)(on != 0 ? 1 : 0));
|
||||
public static byte[] Hood(int on) => Build(CTRL_HOOD, (byte)(on != 0 ? 1 : 0));
|
||||
public static byte[] Preset(int p) => Build(CTRL_HYST_PRESET, (byte)p);
|
||||
// preset: 0 ECO / 1 NORMAL / 2 TURBO
|
||||
public static byte[] HystValue(int preset, int pm25, int pm10, int voc, int co2)
|
||||
{
|
||||
var p = new List<byte> { (byte)preset };
|
||||
p.AddRange(U16BE(pm25)); p.AddRange(U16BE(pm10)); p.AddRange(U16BE(voc)); p.AddRange(U16BE(co2));
|
||||
return Build(CTRL_HYST_VALUE, p.ToArray());
|
||||
}
|
||||
// type : 0=급기(SA) / 1=배기(EA)
|
||||
public static byte[] Damper(int room, int type, int on) => Build(CTRL_DAMPER, (byte)room, (byte)type, (byte)(on != 0 ? 1 : 0));
|
||||
public static byte[] Led(int room, int dim) => Build(CTRL_LED, (byte)room, (byte)dim);
|
||||
public static byte[] RequestStatus() => Build(REQ_STATUS);
|
||||
public static byte[] Reset(int on) => Build(CTRL_RESET, (byte)(on != 0 ? 1 : 0));
|
||||
public static byte[] Reserve(int hours) => Build(CTRL_RESERVE, (byte)hours); // 0~8시간, 0=해제
|
||||
public static byte[] Vsp(int group, int index, int sa, int ea)
|
||||
{
|
||||
var p = new List<byte> { (byte)group, (byte)index };
|
||||
p.AddRange(U16BE(sa)); p.AddRange(U16BE(ea));
|
||||
return Build(CTRL_VSP, p.ToArray());
|
||||
}
|
||||
// 오염단계 임계 : preset(0 ECO/1 NORMAL/2 TURBO), pollutant(0 CO2/1 PM2.5/2 PM10/3 VOC), L1~L4 상한
|
||||
public static byte[] HystThr(int preset, int pollutant, int l1, int l2, int l3, int l4)
|
||||
{
|
||||
var p = new List<byte> { (byte)preset, (byte)pollutant };
|
||||
p.AddRange(U16BE(l1)); p.AddRange(U16BE(l2)); p.AddRange(U16BE(l3)); p.AddRange(U16BE(l4));
|
||||
return Build(CTRL_HYST_THR, p.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
namespace ErvProtocol
|
||||
{
|
||||
// 펌웨어 없이 UI/파이프라인 검증용 합성 STATUS payload(73B) 생성.
|
||||
public static class DemoStatus
|
||||
{
|
||||
public static byte[] BuildPayload(int tick)
|
||||
{
|
||||
var p = new byte[StatusDecoder.STATUS_LEN];
|
||||
p[0] = 1; // power
|
||||
p[1] = (byte)RunMode.Auto; // runMode
|
||||
p[2] = (byte)((tick / 5) % 2); // autoState 분산/집중
|
||||
p[3] = (byte)(2 + (tick % 3)); // fanMode 2~4
|
||||
p[4] = SubModeBits.SmartSleep; // subMode
|
||||
p[5] = (byte)(tick % 2); // hood
|
||||
p[6] = (byte)HystPreset.Normal; // preset
|
||||
WriteU16(p, 7, 30); WriteU16(p, 9, 50); WriteU16(p, 11, 300); WriteU16(p, 13, 700);
|
||||
WriteU16(p, 15, 0x0000); // errorCode
|
||||
|
||||
for (int r = 0; r < 4; r++)
|
||||
{
|
||||
int o = 17 + r * 14;
|
||||
int seed = tick + r * 13;
|
||||
p[o + 0] = (byte)((seed % 2) | (((seed % 3) == 0) ? 0x02 : 0)); // bit0 급기 / bit1 배기
|
||||
WriteU16(p, o + 1, 10 + (seed * 3) % 60);
|
||||
WriteU16(p, o + 3, 15 + (seed * 5) % 90);
|
||||
WriteU16(p, o + 5, 100 + (seed * 7) % 400);
|
||||
WriteU16(p, o + 7, 450 + (seed * 11) % 700);
|
||||
p[o + 9] = (byte)(1 + (seed % 4));
|
||||
p[o + 10] = (byte)(seed % 10);
|
||||
WriteU16(p, o + 11, (seed * 17) % 100);
|
||||
p[o + 13] = (byte)(seed % 5);
|
||||
}
|
||||
|
||||
p[73] = 0; // reset (토글 off)
|
||||
|
||||
// 풍량 VSP 설정값 (1바이트, 사양서 DL H-ERV VSP 실측표) : 환기1~4, 바이패스, 공청1~4 의 SA/EA
|
||||
int[] sa = { 56, 63, 70, 86, 67, 65, 72, 78, 80 };
|
||||
int[] ea = { 57, 63, 70, 85, 75, 0, 0, 0, 0 };
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
int o = 74 + i * 4;
|
||||
WriteU16(p, o, sa[i]);
|
||||
WriteU16(p, o + 2, ea[i]);
|
||||
}
|
||||
|
||||
// 히스테리시스 데드밴드(하강) (ECO/NORMAL/TURBO 의 PM2.5/PM10/VOC/CO2) - 사양서
|
||||
int[,] hyst = { { 2, 5, 5, 50 }, { 2, 5, 5, 50 }, { 2, 5, 3, 30 } };
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
int o = 110 + i * 8;
|
||||
WriteU16(p, o, hyst[i, 0]);
|
||||
WriteU16(p, o + 2, hyst[i, 1]);
|
||||
WriteU16(p, o + 4, hyst[i, 2]);
|
||||
WriteU16(p, o + 6, hyst[i, 3]);
|
||||
}
|
||||
|
||||
// 모드별 오염단계 임계표 (3프리셋 × [CO2,PM2.5,PM10,VOC] × L1~L4 상한) - 사양서
|
||||
int[][,] thr =
|
||||
{
|
||||
new int[,] { {1000,1300,1600,2000}, {20,38,60,86}, {40,86,126,173}, {171,195,308,438} }, // ECO
|
||||
new int[,] { {800,1100,1400,1700}, {14,29,49,69}, {28,66,102,138}, {120,150,250,350} }, // NORMAL
|
||||
new int[,] { {700,1000,1300,1600}, {12,23,38,52}, {24,53,78,104}, {103,120,192,263} }, // TURBO
|
||||
};
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
int o = StatusDecoder.THR_OFF + i * 32;
|
||||
for (int g = 0; g < 4; g++) // g: 0 CO2,1 PM2.5,2 PM10,3 VOC
|
||||
for (int k = 0; k < 4; k++)
|
||||
WriteU16(p, o + g * 8 + k * 2, thr[i][g, k]);
|
||||
}
|
||||
|
||||
// 각실 온도/습도 (offset 230~, 4실 × [Temp, Humi])
|
||||
for (int r = 0; r < 4; r++)
|
||||
{
|
||||
int o = StatusDecoder.TEMPHUMI_OFF + r * 2;
|
||||
p[o + 0] = (byte)(22 + (tick + r) % 6); // 22~27℃
|
||||
p[o + 1] = (byte)(40 + (tick + r * 7) % 30); // 40~69%
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
static void WriteU16(byte[] p, int off, int v)
|
||||
{
|
||||
p[off] = (byte)((v >> 8) & 0xFF);
|
||||
p[off + 1] = (byte)(v & 0xFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace ErvProtocol
|
||||
{
|
||||
// 운전모드 (PC_ERV_Protocol.md 3.1)
|
||||
public enum RunMode : byte
|
||||
{
|
||||
Off = 0x00,
|
||||
Vent = 0x01, // 환기
|
||||
Auto = 0x02, // 자동
|
||||
AirClean = 0x03, // 공청
|
||||
Bypass = 0x04, // 바이패스
|
||||
}
|
||||
|
||||
// 자동운전 상태 (3.4)
|
||||
public enum AutoState : byte
|
||||
{
|
||||
Distribute = 0x00, // 분산
|
||||
Focus = 0x01, // 집중
|
||||
}
|
||||
|
||||
// 히스테리시스 프리셋
|
||||
public enum HystPreset : byte
|
||||
{
|
||||
Eco = 0x00,
|
||||
Normal = 0x01,
|
||||
Turbo = 0x02,
|
||||
}
|
||||
|
||||
// 공기질 상태 (3.3) - 판단 기준은 협의 예정
|
||||
public enum AirQuality : byte
|
||||
{
|
||||
VeryBad = 0x01, // 매우나쁨 - 빨강
|
||||
Bad = 0x02, // 나쁨 - 주황
|
||||
Normal = 0x03, // 보통 - 초록
|
||||
Good = 0x04, // 좋음 - 파랑
|
||||
}
|
||||
|
||||
// 부가모드 type (CTRL_SUBMODE)
|
||||
public enum SubModeType : byte
|
||||
{
|
||||
SmartSleep = 0x01, // 스마트수면
|
||||
ComfortCook = 0x02, // 쾌적조리
|
||||
ReliefRecover = 0x03, // 안심회복
|
||||
}
|
||||
|
||||
public static class SubModeBits
|
||||
{
|
||||
public const byte SmartSleep = 0x01;
|
||||
public const byte ComfortCook = 0x02;
|
||||
public const byte ReliefRecover = 0x04;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- HuevenEco DL 각실제어 공용 프로토콜 라이브러리 (PC_ERV_Protocol.md 단일 진실원본)
|
||||
PC 대시보드(WPF), 미니PC 수집서버(ErvCollector) 가 공통 참조한다. -->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>ErvProtocol</RootNamespace>
|
||||
<AssemblyName>ErvProtocol</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,69 @@
|
||||
namespace ErvProtocol
|
||||
{
|
||||
// 고정패킷 파서 (PC_ERV_Protocol.md Rev2.0)
|
||||
// 0xAA | CMD | DATA[240] | CRC16-MODBUS(LE) = 244B 고정
|
||||
// 연결(소켓/시리얼)마다 1개 인스턴스. CRC 통과 프레임만 OnFrame 으로 전달.
|
||||
public sealed class FrameParser
|
||||
{
|
||||
public const byte STX = 0xAA;
|
||||
public const int DataLen = CtrlFrame.DataLen; // 240
|
||||
|
||||
public event Action<byte, byte[]>? OnFrame; // (cmd, data[240])
|
||||
public event Action<string>? OnError;
|
||||
|
||||
enum Step { Stx, Cmd, Data, CrcLo, CrcHi }
|
||||
Step _step = Step.Stx;
|
||||
byte _cmd, _crcLo;
|
||||
readonly byte[] _data = new byte[DataLen];
|
||||
int _dataPos;
|
||||
|
||||
public void Feed(ReadOnlySpan<byte> data)
|
||||
{
|
||||
foreach (var b in data) FeedByte(b);
|
||||
}
|
||||
|
||||
public void FeedByte(byte b)
|
||||
{
|
||||
switch (_step)
|
||||
{
|
||||
case Step.Stx:
|
||||
if (b == STX) _step = Step.Cmd;
|
||||
break;
|
||||
case Step.Cmd:
|
||||
_cmd = b; _dataPos = 0; _step = Step.Data;
|
||||
break;
|
||||
case Step.Data:
|
||||
_data[_dataPos++] = b;
|
||||
if (_dataPos >= DataLen) _step = Step.CrcLo;
|
||||
break;
|
||||
case Step.CrcLo:
|
||||
_crcLo = b; _step = Step.CrcHi;
|
||||
break;
|
||||
case Step.CrcHi:
|
||||
ushort rxCrc = (ushort)(_crcLo | (b << 8));
|
||||
_step = Step.Stx;
|
||||
Verify(rxCrc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 프레임 동기 리셋 (시리얼 프레임 갭 발생 시 등)
|
||||
public void Reset() => _step = Step.Stx;
|
||||
|
||||
void Verify(ushort rxCrc)
|
||||
{
|
||||
var buf = new byte[1 + DataLen]; // CMD + DATA[240]
|
||||
buf[0] = _cmd;
|
||||
Array.Copy(_data, 0, buf, 1, DataLen);
|
||||
ushort calc = Crc16.Modbus(buf, 0, buf.Length);
|
||||
if (calc != rxCrc)
|
||||
{
|
||||
OnError?.Invoke($"CRC ERR cmd=0x{_cmd:X2} calc=0x{calc:X4} rx=0x{rxCrc:X4}");
|
||||
return;
|
||||
}
|
||||
var data = new byte[DataLen];
|
||||
Array.Copy(_data, data, DataLen);
|
||||
OnFrame?.Invoke(_cmd, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
namespace ErvProtocol
|
||||
{
|
||||
public static class StatusDecoder
|
||||
{
|
||||
public const byte STATUS = 0x81; // ERV→PC 상태
|
||||
public const byte ACK = 0x82; // ERV→PC 제어 응답
|
||||
public const int STATUS_LEN = 240; // 238 + 2((꺼짐)예약 잔여초 u16)
|
||||
public const int THR_OFF = 134; // 모드별 오염단계 임계표 시작 오프셋
|
||||
public const int TEMPHUMI_OFF = 230; // 각실 온도/습도 블록 시작 (4실 × [Temp,Humi])
|
||||
public const int RESERVE_OFF = 238; // (꺼짐)예약 잔여초 u16
|
||||
|
||||
static int U16(byte[] p, int off) => (p[off] << 8) | p[off + 1]; // 빅엔디안
|
||||
|
||||
// STATUS(0x81) payload(73B) → StatusRecord. 길이 부족 시 null.
|
||||
public static StatusRecord? Decode(byte[] p)
|
||||
{
|
||||
if (p.Length < STATUS_LEN) return null;
|
||||
|
||||
var s = new StatusRecord
|
||||
{
|
||||
Power = p[0],
|
||||
RunMode = p[1],
|
||||
AutoState = p[2],
|
||||
FanMode = p[3],
|
||||
SubMode = p[4],
|
||||
Hood = p[5],
|
||||
HystPreset = p[6],
|
||||
HystPm25 = U16(p, 7),
|
||||
HystPm10 = U16(p, 9),
|
||||
HystVoc = U16(p, 11),
|
||||
HystCo2 = U16(p, 13),
|
||||
ErrorCode = U16(p, 15),
|
||||
};
|
||||
|
||||
const int baseOff = 17;
|
||||
for (int r = 0; r < 4; r++)
|
||||
{
|
||||
int o = baseOff + r * 14;
|
||||
var room = s.Rooms[r];
|
||||
room.Damper = p[o + 0];
|
||||
room.Pm25 = U16(p, o + 1);
|
||||
room.Pm10 = U16(p, o + 3);
|
||||
room.Voc = U16(p, o + 5);
|
||||
room.Co2 = U16(p, o + 7);
|
||||
room.AirQuality = p[o + 9];
|
||||
room.LedDim = p[o + 10];
|
||||
room.LoadScore = U16(p, o + 11);
|
||||
room.FinalVolume = p[o + 13];
|
||||
}
|
||||
|
||||
// ERV 리셋 (offset 73)
|
||||
s.Reset = p[73];
|
||||
|
||||
// 풍량 VSP 블록 (offset 74~, 9엔트리 × u16 SA·EA)
|
||||
const int vspOff = 74;
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
int o = vspOff + i * 4;
|
||||
s.Vsp[i].Sa = U16(p, o);
|
||||
s.Vsp[i].Ea = U16(p, o + 2);
|
||||
}
|
||||
|
||||
// 히스테리시스 프리셋 테이블 (offset 110~, 3프리셋 × PM2.5/PM10/VOC/CO2 u16)
|
||||
const int hystOff = 110;
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
int o = hystOff + i * 8;
|
||||
s.HystTable[i].Pm25 = U16(p, o);
|
||||
s.HystTable[i].Pm10 = U16(p, o + 2);
|
||||
s.HystTable[i].Voc = U16(p, o + 4);
|
||||
s.HystTable[i].Co2 = U16(p, o + 6);
|
||||
}
|
||||
|
||||
// 모드별 오염단계 임계표 (offset 134~, 3프리셋 × [CO2,PM2.5,PM10,VOC] × L1~L4 u16)
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
int o = THR_OFF + i * 32;
|
||||
for (int k = 0; k < 4; k++) s.ThrTable[i].Co2[k] = U16(p, o + 0 + k * 2);
|
||||
for (int k = 0; k < 4; k++) s.ThrTable[i].Pm25[k] = U16(p, o + 8 + k * 2);
|
||||
for (int k = 0; k < 4; k++) s.ThrTable[i].Pm10[k] = U16(p, o + 16 + k * 2);
|
||||
for (int k = 0; k < 4; k++) s.ThrTable[i].Voc[k] = U16(p, o + 24 + k * 2);
|
||||
}
|
||||
|
||||
// 각실 온도/습도 (offset 230~, 4실 × [Temp, Humi])
|
||||
for (int r = 0; r < 4; r++)
|
||||
{
|
||||
int o = TEMPHUMI_OFF + r * 2;
|
||||
s.Rooms[r].Temp = p[o + 0];
|
||||
s.Rooms[r].Humi = p[o + 1];
|
||||
}
|
||||
|
||||
// (꺼짐)예약 잔여초 (offset 238, u16)
|
||||
s.ReserveRemainSec = U16(p, RESERVE_OFF);
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
namespace ErvProtocol
|
||||
{
|
||||
// StatusRecord → STATUS(0x81) 134B payload. StatusDecoder 의 역(逆).
|
||||
// ERV(메인보드/시뮬레이터) 측이 STATUS 를 송신할 때 사용. PC_ERV_Protocol.md 4장.
|
||||
public static class StatusEncoder
|
||||
{
|
||||
static void W16(byte[] p, int off, int v) // 빅엔디안
|
||||
{
|
||||
p[off] = (byte)((v >> 8) & 0xFF);
|
||||
p[off + 1] = (byte)(v & 0xFF);
|
||||
}
|
||||
|
||||
// StatusRecord → 134B payload
|
||||
public static byte[] Encode(StatusRecord s)
|
||||
{
|
||||
var p = new byte[StatusDecoder.STATUS_LEN];
|
||||
|
||||
// 글로벌 0~16
|
||||
p[0] = s.Power;
|
||||
p[1] = s.RunMode;
|
||||
p[2] = s.AutoState;
|
||||
p[3] = s.FanMode;
|
||||
p[4] = s.SubMode;
|
||||
p[5] = s.Hood;
|
||||
p[6] = s.HystPreset;
|
||||
W16(p, 7, s.HystPm25);
|
||||
W16(p, 9, s.HystPm10);
|
||||
W16(p, 11, s.HystVoc);
|
||||
W16(p, 13, s.HystCo2);
|
||||
W16(p, 15, s.ErrorCode);
|
||||
|
||||
// 각실 17~ (14B x 4)
|
||||
const int baseOff = 17;
|
||||
for (int r = 0; r < 4; r++)
|
||||
{
|
||||
int o = baseOff + r * 14;
|
||||
var room = s.Rooms[r];
|
||||
p[o + 0] = room.Damper;
|
||||
W16(p, o + 1, room.Pm25);
|
||||
W16(p, o + 3, room.Pm10);
|
||||
W16(p, o + 5, room.Voc);
|
||||
W16(p, o + 7, room.Co2);
|
||||
p[o + 9] = room.AirQuality;
|
||||
p[o + 10] = room.LedDim;
|
||||
W16(p, o + 11, room.LoadScore);
|
||||
p[o + 13] = room.FinalVolume;
|
||||
}
|
||||
|
||||
// ERV 리셋 (offset 73)
|
||||
p[73] = s.Reset;
|
||||
|
||||
// 풍량 VSP (offset 74~, 9엔트리 × u16 SA·EA)
|
||||
const int vspOff = 74;
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
int o = vspOff + i * 4;
|
||||
W16(p, o, s.Vsp[i].Sa);
|
||||
W16(p, o + 2, s.Vsp[i].Ea);
|
||||
}
|
||||
|
||||
// 히스테리시스 프리셋 테이블 (offset 110~, 3프리셋 × PM2.5/PM10/VOC/CO2 u16)
|
||||
const int hystOff = 110;
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
int o = hystOff + i * 8;
|
||||
W16(p, o, s.HystTable[i].Pm25);
|
||||
W16(p, o + 2, s.HystTable[i].Pm10);
|
||||
W16(p, o + 4, s.HystTable[i].Voc);
|
||||
W16(p, o + 6, s.HystTable[i].Co2);
|
||||
}
|
||||
|
||||
// 모드별 오염단계 임계표 (offset 134~, 3프리셋 × [CO2,PM2.5,PM10,VOC] × L1~L4)
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
int o = StatusDecoder.THR_OFF + i * 32;
|
||||
for (int k = 0; k < 4; k++) W16(p, o + 0 + k * 2, s.ThrTable[i].Co2[k]);
|
||||
for (int k = 0; k < 4; k++) W16(p, o + 8 + k * 2, s.ThrTable[i].Pm25[k]);
|
||||
for (int k = 0; k < 4; k++) W16(p, o + 16 + k * 2, s.ThrTable[i].Pm10[k]);
|
||||
for (int k = 0; k < 4; k++) W16(p, o + 24 + k * 2, s.ThrTable[i].Voc[k]);
|
||||
}
|
||||
|
||||
// 각실 온도/습도 (offset 230~, 4실 × [Temp, Humi])
|
||||
for (int r = 0; r < 4; r++)
|
||||
{
|
||||
int o = StatusDecoder.TEMPHUMI_OFF + r * 2;
|
||||
p[o + 0] = s.Rooms[r].Temp;
|
||||
p[o + 1] = s.Rooms[r].Humi;
|
||||
}
|
||||
|
||||
// (꺼짐)예약 잔여초 (offset 238, u16)
|
||||
W16(p, StatusDecoder.RESERVE_OFF, s.ReserveRemainSec);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
// STATUS(0x81) 완성 프레임 (CRC 포함)
|
||||
public static byte[] BuildStatusFrame(StatusRecord s) => CtrlFrame.Build(StatusDecoder.STATUS, Encode(s));
|
||||
|
||||
// ACK(0x82) 프레임 : [echoCmd][result(0 OK/1 ERR)]
|
||||
public static byte[] BuildAckFrame(byte echoCmd, byte result) => CtrlFrame.Build(StatusDecoder.ACK, echoCmd, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
namespace ErvProtocol
|
||||
{
|
||||
// STATUS(0x81) 1프레임을 디코드한 결과 (PC_ERV_Protocol.md 4장)
|
||||
// 필드는 raw 수치(byte/int). 소비자(WPF)가 필요 시 enum 으로 캐스팅.
|
||||
public sealed class StatusRecord
|
||||
{
|
||||
public byte Power; // 0/1
|
||||
public byte RunMode; // 1환기 2자동 3공청 4바이패스
|
||||
public byte AutoState; // 0분산 1집중
|
||||
public byte FanMode; // 0~4
|
||||
public byte SubMode; // 비트맵 bit0수면 bit1조리 bit2회복
|
||||
public byte Hood; // 비트맵 bit0=연동Enable / bit1=연동운전중 / bit2=후드 통신연결(폴응답 생존)
|
||||
public bool HoodEnable => (Hood & 0x01) != 0; // 후드연동 활성
|
||||
public bool HoodRunning => (Hood & 0x02) != 0; // 후드연동에 의한 운전중
|
||||
public bool HoodConnected => (Hood & 0x04) != 0; // 후드 485 통신 연결됨(ERV가 폴 응답 수신중)
|
||||
public byte HystPreset; // 0 ECO 1 NORMAL 2 TURBO
|
||||
public int HystPm25;
|
||||
public int HystPm10;
|
||||
public int HystVoc;
|
||||
public int HystCo2;
|
||||
public int ErrorCode; // 비트맵
|
||||
public byte Reset; // ERV 리셋 토글 0/1
|
||||
public int ReserveRemainSec; // (꺼짐)예약 잔여 초 (0=예약없음) — STATUS 끝 블록
|
||||
|
||||
public readonly RoomRecord[] Rooms = new RoomRecord[4];
|
||||
|
||||
// 풍량 VSP 9엔트리 (VspInfo.Labels 순서: 환기1~4, 바이패스, 공청1~4)
|
||||
public readonly VspEntry[] Vsp = new VspEntry[9];
|
||||
|
||||
// 히스테리시스 프리셋별 데드밴드(하강) [0]=ECO [1]=NORMAL [2]=TURBO
|
||||
public readonly HystSet[] HystTable = new HystSet[3];
|
||||
|
||||
// 모드(프리셋)별 오염단계 임계 경계표 [0]=ECO [1]=NORMAL [2]=TURBO
|
||||
public readonly ThrSet[] ThrTable = new ThrSet[3];
|
||||
|
||||
public StatusRecord()
|
||||
{
|
||||
for (int i = 0; i < 4; i++) Rooms[i] = new RoomRecord();
|
||||
for (int i = 0; i < 9; i++) Vsp[i] = new VspEntry();
|
||||
for (int i = 0; i < 3; i++) HystTable[i] = new HystSet();
|
||||
for (int i = 0; i < 3; i++) ThrTable[i] = new ThrSet();
|
||||
}
|
||||
}
|
||||
|
||||
// 히스테리시스 한 프리셋의 데드밴드(하강폭)
|
||||
public sealed class HystSet
|
||||
{
|
||||
public int Pm25;
|
||||
public int Pm10;
|
||||
public int Voc;
|
||||
public int Co2;
|
||||
}
|
||||
|
||||
// 한 프리셋의 오염단계 임계(각 오염원별 레벨 1~4 상한 경계)
|
||||
public sealed class ThrSet
|
||||
{
|
||||
public int[] Co2 = new int[4]; // L1~L4 상한
|
||||
public int[] Pm25 = new int[4];
|
||||
public int[] Pm10 = new int[4];
|
||||
public int[] Voc = new int[4];
|
||||
}
|
||||
|
||||
public sealed class VspEntry
|
||||
{
|
||||
public int Sa; // 급기 풍량 설정값
|
||||
public int Ea; // 배기 풍량 설정값
|
||||
}
|
||||
|
||||
// 풍량 VSP 엔트리 인덱스 ↔ 라벨/group/index 매핑 (PC_ERV_Protocol.md 3.5)
|
||||
public static class VspInfo
|
||||
{
|
||||
public static readonly string[] Labels =
|
||||
{ "환기1", "환기2", "환기3", "환기4", "바이패스", "공청1", "공청2", "공청3", "공청4" };
|
||||
|
||||
// InfluxDB 태그/JSON 용 ASCII 키
|
||||
public static readonly string[] Keys =
|
||||
{ "vent1", "vent2", "vent3", "vent4", "bypass", "air1", "air2", "air3", "air4" };
|
||||
|
||||
// group: 0 Vent / 1 Bypass / 2 AirClean
|
||||
public static readonly int[] Group = { 0, 0, 0, 0, 1, 2, 2, 2, 2 };
|
||||
// index: Vent/AirClean 1~4, Bypass 1
|
||||
public static readonly int[] Index = { 1, 2, 3, 4, 1, 1, 2, 3, 4 };
|
||||
public const int Count = 9;
|
||||
}
|
||||
|
||||
public sealed class RoomRecord
|
||||
{
|
||||
public byte Damper; // 비트맵 : bit0=급기(SA) 열림 / bit1=배기(EA) 열림
|
||||
public bool DamperSa => (Damper & 0x01) != 0; // 급기 댐퍼 열림
|
||||
public bool DamperEa => (Damper & 0x02) != 0; // 배기 댐퍼 열림
|
||||
public int Pm25;
|
||||
public int Pm10;
|
||||
public int Voc;
|
||||
public int Co2;
|
||||
public byte AirQuality; // 1매우나쁨~4좋음
|
||||
public byte LedDim; // 0~9
|
||||
public int LoadScore; // 부하점수
|
||||
public byte FinalVolume; // 최종풍량
|
||||
public byte Temp; // 온도(℃) — STATUS 끝 블록(offset 230~)
|
||||
public byte Humi; // 습도(%) — STATUS 끝 블록(offset 230~)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user