WPF C# - 프로토콜 규격

wsung·2026년 2월 6일
using System;

namespace ICT_WPF.Services
{
    /// <summary>
    /// LDEICT REV03 UDP 프로토콜
    ///
    /// 프레임 구조:
    ///  STX(1)
    ///  CH(2)   : ASCII HEX ("01"~"FF")
    ///  CMD(2)  : ASCII HEX
    ///  LEN(4)  : ASCII HEX (payload byte 수)
    ///  DATA(N) : 바이너리
    ///  CS(2)   : ASCII HEX
    ///  ETX(1)
    ///
    /// 전체 길이 = LEN + 12
    /// </summary>
    public static class IctUdpProtocolLdeict
    {
        public const byte STX = 0x02;
        public const byte ETX = 0x03;

        /// <summary>
        /// CH(0-based) → 보드 IP
        /// </summary>
        public static string GetBoardIp(int ch0Base)
            => $"192.168.0.{ch0Base + 1}";

        /// <summary>
        /// ASCII HEX 2자리 쓰기
        /// </summary>
        private static void WriteHex2(Span<byte> dst, int offset, int value)
        {
            string s = value.ToString("X2");
            dst[offset + 0] = (byte)s[0];
            dst[offset + 1] = (byte)s[1];
        }

        /// <summary>
        /// ASCII HEX 4자리 쓰기
        /// </summary>
        private static void WriteHex4(Span<byte> dst, int offset, int value)
        {
            string s = value.ToString("X4");
            dst[offset + 0] = (byte)s[0];
            dst[offset + 1] = (byte)s[1];
            dst[offset + 2] = (byte)s[2];
            dst[offset + 3] = (byte)s[3];
        }

        /// <summary>
        /// 체크섬 계산
        /// (CH+CMD+LEN+DATA 바이트 합 & 0xFF)
        /// </summary>
        private static byte CalcChecksum(ReadOnlySpan<byte> src, int count)
        {
            int sum = 0;
            for (int i = 0; i < count; i++)
                sum += src[i];

            return (byte)(sum & 0xFF);
        }

        /// <summary>
        /// 요청 프레임 생성
        /// </summary>
        public static byte[] BuildFrame(int ch0Base, byte cmd, byte[] payload)
        {
            payload ??= Array.Empty<byte>();
            int payloadLen = payload.Length;


            int totalLen = payloadLen + 12;
            byte[] frame = new byte[totalLen];

            int o = 0;
            frame[o++] = STX;

            WriteHex2(frame, o, ch0Base + 1); o += 2; // CH
            WriteHex2(frame, o, cmd); o += 2; // CMD
            WriteHex4(frame, o, payloadLen); o += 4; // LEN

            // DATA
            if (payloadLen > 0)
            {
                Buffer.BlockCopy(payload, 0, frame, o, payloadLen);
                o += payloadLen;
            }

            // CS
            byte cs = CalcChecksum(frame.AsSpan(3), payloadLen + 6);
            WriteHex2(frame, o, cs);
            o += 2;

            frame[o] = ETX;

            return frame;
        }
    }
}

파일 이름 IctUdpProtocolLdeict.cs
payload(byte[])를 UDP규격에 맞는 완전한 프레임(byte[])로 만드는 파일
형식(STX~ETX 프레임 규칙)

❌ 스위치 해석 안 함
❌ LED 비트 매핑 안 함
❌ UDP 소켓 안 만짐
❌ UI 모름

STX(1)
CH(2)
CMD(2)
LEN(4)
DATA(N)
CS(2)
ETX(1)
이 규칙을 박아 넣은 파일
buildframe() 함수가 핵심

문서대로 프레임 구조

[STX 1바이트] 
[CH 2바이트 ASCII HEX]
[CMD 2바이트 ASCII HEX]
[LEN 4바이트 ASCII HEX]   // payload 길이
[DATA N바이트 바이너리]   // payload
[CS 2바이트 ASCII HEX]    // 체크섬
[ETX 1바이트]

=> payload를 규격 포멧으로 포장해서 UDP로 바로 보내는 byte[]만들어줌

상수정의

public const byte STX = 0x02;
public const byte ETX = 0x03;
프레임시작과 끝 표시 , 프레임의 경계

GetBoardIp

public static string GetBoardIp(int ch0Base)

  • 채널번호를 0부터 받음
    ex)
    ch0Base=0 → 192.168.0.1
    ch0Base=14 → 192.168.0.15

WriteHex2()

private static void WriteHex2(Span<byte> dst, int offset, int value)

  • ToString("X2") : value를 16진수 2자리 문자열로 만들어라
    ex)
    value = 14 (0x0E)
    string s = value.ToString("X2") --> OE
    dst 에는 '0','E'
    '0' = 0x30
    'E' = 0x45
    => 2자리 ASCII HEX 규칙을 구현한 함수

WriteHex4()

private static void WriteHex4(Span<byte> dst, int offset, int value)

  • string s = value.ToString("X4") : value를 16진수 4자리 문자열로 바꿔라

ex) payloadLed = 2
-> 0002 => dst에 '0''0''0''2'가 들어감

CalcChecksum()

private static byte CalcChecksum(ReadOnlySpan<byte> src, int count)
하는 일 딱 두개

  • 바이트들을 그대로 더한다
  • 결과의 하위 8비트만 남긴다
    예시프레임 )
  • CH '01'
  • CMD '0E'
  • LEN '0002'
  • DATA {0x03, 0x0A} (resultColde '0', ucDio 0x0A)

    이 값들을 그대로 더한다

0x30 = 48
0x31 = 49
0x30 = 48
0x45 = 69
0x30 = 48
0x30 = 48
0x30 = 48
0x32 = 50
0x30 = 48
0x0A = 10

=> sum = 466
하위 1바이트만 남겨야함 (return (byte)(sum & 0xFF);)

466 & 256 = 210(10진수)
0xD2 (16진수)
-> 체크섬 프레임에 드가는 방식
- 0xD2 = 'D2'
- 'D' = 0x44
- '2' = 0x32

즉, 체크섬은 프레임에 실제로 들어간 바이트(ASCII 포함)를 그대로 다 더해서 하위 8비트만 취한 값임

그래서 왜 이렇게 하냐? (이해 포인트)

  • 구현이 단순
  • MCU에서도 계산 쉬움
  • ASCII 기반 프로토콜에서 흔한 방식
  • CRC보다 가볍지만 “깨졌는지 여부”는 충분히 판단 가능

BuildFrame() — 핵심

public static byte[] BuildFrame(int ch0Base, byte cmd, byte[] payload)

  • ch0Base: 0based채널( 0이면 CH=01)
  • cmd : 명령값 (ex 0x0E)
  • payload : DATA영역에 들어갈 바이너리 (ucDio)

payload null 방지, 길이 계산

payload ??= Array.Empty<byte>();
int payloadLen = payload.Length;
  • payload가 null이면 빈 배열로 바꿔서 NRE방지
  • payload길이는 LEN필드에 들어감

전체 프레임 길이 계산

int totalLen = payloadLen + 12;
byte[] frame = new byte[totalLen];

왜 + 12? : 고정필드합
STX 1
CH 2
CMD 2
LEN 4
CS 2
ETX 1
= 1+2+2+4+2+1 = 12
=> payloadLen + 12

STX 쓰기
frame[o++] = STX;

이 문장을 두 단계로 나눠서 봐야 한다.

(1) frame[o] = STX

o = 0

frame[0]에 STX(0x02) 저장

(2) o++

o를 1 증가

결과:

index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13
frame: [02][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]

o = 1

👉 “STX 썼고, 다음은 index 1부터 써라”

5️⃣ CH 쓰기 (2바이트)
WriteHex2(frame, o, ch0Base + 1);
o += 2;

예: CH = "01" → '0' '1' → 0x30 0x31

WriteHex2 내부에서 일어나는 일
frame[o + 0] = '0'; // frame[1]
frame[o + 1] = '1'; // frame[2]

쓴 뒤 o를 2 증가:

index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13
frame: [02][30][31][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]

o = 3

👉 CH가 2바이트니까 o도 2만큼 점프

6️⃣ CMD 쓰기 (2바이트)
WriteHex2(frame, o, cmd);
o += 2;

예: CMD = 0x0E → "0E" → 0x30 0x45

index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13
frame: [02][30][31][30][45][ ][ ][ ][ ][ ][ ][ ][ ][ ]

o = 5

7️⃣ LEN 쓰기 (4바이트)
WriteHex4(frame, o, payloadLen);
o += 4;

예: payloadLen = 2 → "0002"

index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13
frame: [02][30][31][30][45][30][30][30][32][ ][ ][ ][ ][ ]

o = 9

8️⃣ DATA 쓰기 (payloadLen = 2)
Buffer.BlockCopy(payload, 0, frame, o, payloadLen);
o += payloadLen;

예: payload = { 0x30, 0x0A }

index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13
frame: [02][30][31][30][45][30][30][30][32][30][0A][ ][ ][ ]

o = 11

9️⃣ CS 쓰기 (2바이트)
WriteHex2(frame, o, cs);
o += 2;

예: CS = 0xD2 → "D2"

index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13
frame: [02][30][31][30][45][30][30][30][32][30][0A][44][32][ ]

o = 13

🔟 ETX 쓰기
frame[o] = ETX;

index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13
frame: [02][30][31][30][45][30][30][30][32][30][0A][44][32][03]

profile
0부터 시작하는 백엔드

0개의 댓글