096111e983
.claude/ 제외(.gitignore 추가). 기존 초기커밋(5a96a69) 위에 신규·수정·이동분 커밋.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
229 lines
8.9 KiB
C#
229 lines
8.9 KiB
C#
using System.Text;
|
|
|
|
namespace CvnetPacketProgram;
|
|
|
|
// ============================================================================
|
|
// 대림 환기 프로토콜 (20230824 시트) — 구현은 해당 시트 내용만 근거로 함.
|
|
//
|
|
// 공통 프레임 구조
|
|
// Header(0xF7) | Device(0x32) | Sub ID | Cmd | Len | Data[Len] | XOR SUM | ADD SUM
|
|
//
|
|
// 비트 표기: 시트의 "BIT 8 7 6 5 4 3 2 1" 기준 (bit8 = 0x80(MSB), bit1 = 0x01(LSB))
|
|
//
|
|
// 체크섬 (시트: "기존 대림 3.0 기준" — 표준 대림 3.0 방식)
|
|
// XOR SUM = Header ~ 마지막 Data 까지 전체 XOR
|
|
// ADD SUM = (Header ~ XOR SUM 까지 전체 합) & 0xFF
|
|
// ============================================================================
|
|
public static class Cvnet
|
|
{
|
|
public const byte Header = 0xF7;
|
|
public const byte Device = 0x32;
|
|
|
|
// Cmd (시트)
|
|
public const byte CmdStatusQuery = 0x11; // 상태 조회 (요청) Len 0
|
|
public const byte CmdStatusResp = 0x91; // 상태 조회 응답 Len 6
|
|
public const byte CmdCtrlReq = 0x51; // 상세 제어 요구 (요청) Len 3
|
|
public const byte CmdCtrlResp = 0xD1; // 상세 제어 요구 응답 Len 6
|
|
|
|
// 모드 상태 (Data 하위 4bit)
|
|
public static readonly (byte val, string name)[] Modes =
|
|
{
|
|
(0x00, "정지(꺼짐)"),
|
|
(0x01, "자동 - Matrix(환기)"),
|
|
(0x02, "수동 일반(환기)"),
|
|
(0x03, "스케쥴(사용안함)"),
|
|
(0x04, "바이패스"),
|
|
(0x05, "공기청정"),
|
|
(0x06, "히터운전(자동포함)"),
|
|
(0x0A, "자동 - Matrix(공기청정)"),
|
|
};
|
|
|
|
// 풍량 정도 (bit5~7, 3bit)
|
|
public static readonly (byte val, string name)[] Fans =
|
|
{
|
|
(0x00, "꺼짐"),
|
|
(0x01, "약"),
|
|
(0x02, "중"),
|
|
(0x03, "강"),
|
|
};
|
|
|
|
public static string ModeName(int v) => Lookup(Modes, (byte)(v & 0x0F));
|
|
public static string FanName(int v) => Lookup(Fans, (byte)(v & 0x07));
|
|
|
|
private static string Lookup((byte val, string name)[] table, byte v)
|
|
{
|
|
foreach (var t in table) if (t.val == v) return t.name;
|
|
return $"미정의(0x{v:X2})";
|
|
}
|
|
|
|
public static string CmdName(byte cmd) => cmd switch
|
|
{
|
|
CmdStatusQuery => "상태 조회",
|
|
CmdStatusResp => "상태 조회 응답",
|
|
CmdCtrlReq => "상세 제어 요구",
|
|
CmdCtrlResp => "상세 제어 요구 응답",
|
|
_ => $"미정의 Cmd(0x{cmd:X2})",
|
|
};
|
|
|
|
// ---- 체크섬 ----------------------------------------------------------
|
|
public static byte Xor(IReadOnlyList<byte> bytes, int start, int count)
|
|
{
|
|
byte x = 0;
|
|
for (int i = start; i < start + count; i++) x ^= bytes[i];
|
|
return x;
|
|
}
|
|
|
|
public static byte Add(IReadOnlyList<byte> bytes, int start, int count)
|
|
{
|
|
int s = 0;
|
|
for (int i = start; i < start + count; i++) s += bytes[i];
|
|
return (byte)(s & 0xFF);
|
|
}
|
|
|
|
/// <summary>Header~Data 까지 채워진 프레임에 XOR/ADD 2바이트를 덧붙여 완성한다.</summary>
|
|
public static byte[] Finalize(List<byte> body)
|
|
{
|
|
byte xor = Xor(body, 0, body.Count);
|
|
body.Add(xor);
|
|
byte add = Add(body, 0, body.Count); // XOR 포함 합
|
|
body.Add(add);
|
|
return body.ToArray();
|
|
}
|
|
|
|
public static string Hex(IReadOnlyList<byte> b)
|
|
{
|
|
var sb = new StringBuilder(b.Count * 3);
|
|
for (int i = 0; i < b.Count; i++)
|
|
{
|
|
if (i > 0) sb.Append(' ');
|
|
sb.Append(b[i].ToString("X2"));
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
// ====================================================================
|
|
// 프레임 빌더
|
|
// ====================================================================
|
|
|
|
/// <summary>상태 조회 (0x11), Len 0</summary>
|
|
public static byte[] BuildStatusQuery(byte subId)
|
|
{
|
|
var body = new List<byte> { Header, Device, subId, CmdStatusQuery, 0x00 };
|
|
return Finalize(body);
|
|
}
|
|
|
|
/// <summary>상세 제어 요구 (0x51), Len 3</summary>
|
|
public static byte[] BuildCtrlReq(
|
|
byte subId,
|
|
byte mode, bool modeFlag, bool basicMode, bool rangeLink,
|
|
byte fan, bool fanFlag, bool filterTimerReset,
|
|
byte reserveHour, bool reserveFlag)
|
|
{
|
|
// Data0: bit8 기저모드, bit7 렌지연동, bit5 모드Flag, bit1~4 모드상태
|
|
byte d0 = (byte)(mode & 0x0F);
|
|
if (modeFlag) d0 |= 0x10;
|
|
if (rangeLink) d0 |= 0x40;
|
|
if (basicMode) d0 |= 0x80;
|
|
|
|
// Data1: bit8 풍량Flag, bit5~7 풍량정도, bit1 필터타이머리셋
|
|
byte d1 = (byte)((fan & 0x07) << 4);
|
|
if (filterTimerReset) d1 |= 0x01;
|
|
if (fanFlag) d1 |= 0x80;
|
|
|
|
// Data2: bit6 예약Flag, bit1~5 꺼짐예약 설정시간(0x1F=예약끄기/연속)
|
|
byte d2 = (byte)(reserveHour & 0x1F);
|
|
if (reserveFlag) d2 |= 0x20;
|
|
|
|
var body = new List<byte> { Header, Device, subId, CmdCtrlReq, 0x03, d0, d1, d2 };
|
|
return Finalize(body);
|
|
}
|
|
|
|
// 응답(0x91 상태 응답 / 0xD1 제어 응답)은 ERV가 송신하므로 빌더 없음.
|
|
// 수신 프레임은 아래 Decode()에서 해석한다.
|
|
|
|
// ====================================================================
|
|
// 디코더 — 수신 프레임 해석
|
|
// ====================================================================
|
|
public static string Decode(byte[] f)
|
|
{
|
|
if (f.Length < 7) return "(길이 부족 — 최소 7바이트)";
|
|
var sb = new StringBuilder();
|
|
byte subId = f[2];
|
|
byte cmd = f[3];
|
|
byte len = f[4];
|
|
|
|
sb.AppendLine($"Header=0x{f[0]:X2} Device=0x{f[1]:X2} Sub ID=0x{subId:X2} Cmd=0x{cmd:X2} ({CmdName(cmd)}) Len={len}");
|
|
|
|
int dataStart = 5;
|
|
int need = 5 + len + 2;
|
|
bool lenOk = f.Length >= need;
|
|
if (!lenOk)
|
|
{
|
|
sb.AppendLine($" ! Len 기준 필요 길이 {need}B, 실제 {f.Length}B");
|
|
return sb.ToString().TrimEnd();
|
|
}
|
|
|
|
byte rxXor = f[5 + len];
|
|
byte rxAdd = f[5 + len + 1];
|
|
byte calcXor = Xor(f, 0, 5 + len);
|
|
byte calcAdd = Add(f, 0, 5 + len + 1); // XOR 포함
|
|
string xorMark = rxXor == calcXor ? "OK" : $"X (계산 0x{calcXor:X2})";
|
|
string addMark = rxAdd == calcAdd ? "OK" : $"X (계산 0x{calcAdd:X2})";
|
|
|
|
switch (cmd)
|
|
{
|
|
case CmdStatusQuery:
|
|
sb.AppendLine(" [상태 조회 요청]");
|
|
break;
|
|
|
|
case CmdCtrlReq when len >= 3:
|
|
DecodeCtrlReq(sb, f, dataStart);
|
|
break;
|
|
|
|
case CmdStatusResp when len >= 6:
|
|
case CmdCtrlResp when len >= 6:
|
|
DecodeResponse(sb, f, dataStart);
|
|
break;
|
|
|
|
default:
|
|
if (len > 0)
|
|
sb.AppendLine($" Data: {Hex(f[dataStart..(dataStart + len)])}");
|
|
break;
|
|
}
|
|
|
|
sb.Append($" XOR=0x{rxXor:X2} [{xorMark}] ADD=0x{rxAdd:X2} [{addMark}]");
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static void DecodeCtrlReq(StringBuilder sb, byte[] f, int s)
|
|
{
|
|
byte d0 = f[s], d1 = f[s + 1], d2 = f[s + 2];
|
|
sb.AppendLine(" [상세 제어 요구]");
|
|
sb.AppendLine($" Data0=0x{d0:X2} 모드={ModeName(d0 & 0x0F)}"
|
|
+ $" 모드Flag={Bit(d0, 0x10)} 렌지연동={Bit(d0, 0x40)} 기저모드={Bit(d0, 0x80)}");
|
|
sb.AppendLine($" Data1=0x{d1:X2} 풍량={FanName((d1 >> 4) & 0x07)}"
|
|
+ $" 풍량Flag={Bit(d1, 0x80)} 필터타이머리셋={Bit(d1, 0x01)}");
|
|
sb.AppendLine($" Data2=0x{d2:X2} 예약설정시간={ReserveSet(d2 & 0x1F)} 예약Flag={Bit(d2, 0x20)}");
|
|
}
|
|
|
|
private static void DecodeResponse(StringBuilder sb, byte[] f, int s)
|
|
{
|
|
byte d0 = f[s], d1 = f[s + 1], d2 = f[s + 2], d3 = f[s + 3], d4 = f[s + 4], d5 = f[s + 5];
|
|
sb.AppendLine($" Data0=0x{d0:X2} [에러] 장비보호={Bit(d0,0x80)} 미세먼지센서={Bit(d0,0x40)} 배기팬={Bit(d0,0x20)} 급기팬={Bit(d0,0x10)} 내부통신(룸콘)={Bit(d0,0x08)} CO2센서={Bit(d0,0x04)} 소자교환={Bit(d0,0x02)} 온/습도센서={Bit(d0,0x01)}");
|
|
sb.AppendLine($" Data1=0x{d1:X2} 모드={ModeName(d1 & 0x0F)} 기저모드={Bit(d1,0x80)} 렌지연동={Bit(d1,0x40)}");
|
|
sb.AppendLine($" Data2=0x{d2:X2} 풍량={FanName((d2 >> 4) & 0x07)} 필터청소={Bit(d2,0x02)} 필터교환={Bit(d2,0x01)}");
|
|
sb.AppendLine($" Data3=0x{d3:X2} 예약설정시간={ReserveSet(d3 & 0x1F)} 예약상태={Bit(d3,0x20)}");
|
|
sb.AppendLine($" Data4=0x{d4:X2} 남은시간(시)={d4}");
|
|
sb.AppendLine($" Data5=0x{d5:X2} 남은시간(분)={d5}");
|
|
}
|
|
|
|
private static string Bit(byte v, byte mask) => (v & mask) != 0 ? "ON" : "off";
|
|
|
|
private static string ReserveSet(int v) => v switch
|
|
{
|
|
0x00 => "0(없음)",
|
|
0x1F => "예약끄기(연속)",
|
|
_ => $"{v}시간",
|
|
};
|
|
}
|