UDP를 이용한 firmware 기능

wsung·2026년 2월 10일

using System;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ICT_WPF.Services.UDP
{
    public sealed class IctFirmwareUpdateSession : IDisposable
    {
        // MFC 상수
        private const int CH_BOARD_RESPONSE_TIMEOUT_MS = 500;
        private const int F0_RETRY_PERIOD_MS = 200;
        private const int F0_TIMEOUT_MS = 10_000; // 50*200ms
        private const int F1_ACK_TIMEOUT_MS = 2500; // CH_BOARD_FW_UPDATE_ACK_TIMEOUT
        private const int F2_TIMEOUT_MS = 10_000;

        private readonly IctUdpService _udp;
        private readonly int _ch0Base;            // 0 => CH01
        private readonly string _boardIp;         // "192.168.0.x"

        private byte _waitCmd;
        private TaskCompletionSource<byte[]?>? _waitRx; // cmd별 단순 대기

        public IctFirmwareUpdateSession(IctUdpService udp, int ch0Base)
        {
            _udp = udp;
            _ch0Base = ch0Base;
            _boardIp = $"192.168.0.{ch0Base + 1}";
            _udp.DatagramReceived += OnDatagram;
        }

        public void Dispose()
        {
            _udp.DatagramReceived -= OnDatagram;
        }

        // RX 프레임 파싱 후 "payload만" 돌려줌 (DATA 부분)
        private static bool TryParseFrame(byte[] datagram, out byte cmd, out byte[] payload)
        {
            cmd = 0;
            payload = Array.Empty<byte>();

            if (datagram == null || datagram.Length < 12) return false;
            if (datagram[0] != IctUdpProtocolLdeict.STX) return false;
            if (datagram[^1] != IctUdpProtocolLdeict.ETX) return false;

            try
            {
                cmd = Convert.ToByte(Encoding.ASCII.GetString(datagram, 3, 2), 16);
                int len = Convert.ToInt32(Encoding.ASCII.GetString(datagram, 5, 4), 16);
                if (len < 0) return false;
                if (datagram.Length < 9 + len + 3) return false;

                payload = new byte[len];
                Buffer.BlockCopy(datagram, 9, payload, 0, len);
                return true;
            }
            catch
            {
                return false;
            }
        }

        private void OnDatagram(byte[] datagram, IPEndPoint remote)
        {
            if (remote.Address.ToString() != _boardIp) return;
            if (!TryParseFrame(datagram, out var cmd, out var payload)) return;

            var tcs = _waitRx;
            if (tcs != null && cmd == _waitCmd)
            {
                if (ReferenceEquals(_waitRx, tcs))
                    _waitRx = null;

                tcs.TrySetResult(payload);
            }
        }

        private async Task<byte[]?> SendAndWaitAsync(byte cmd, byte[] payload, int timeoutMs, CancellationToken ct)
        {
            _waitCmd = cmd;

            var tcs = new TaskCompletionSource<byte[]?>(TaskCreationOptions.RunContinuationsAsynchronously);
            _waitRx = tcs;

            var frame = IctUdpProtocolLdeict.BuildFrame(_ch0Base, cmd, payload);
            await _udp.SendAsync(frame, _boardIp);

            var done = await Task.WhenAny(tcs.Task, Task.Delay(timeoutMs, ct));
            if (done != tcs.Task)
            {
                // timeout이면 대기 해제
                if (ReferenceEquals(_waitRx, tcs))
                    _waitRx = null;
                return null;
            }

            return tcs.Task.Result;
        }


        private static bool IsOkAck(byte[] payload)
        {
            // ResultCode가 ASCII '0' 이면 OK
            return payload != null && payload.Length >= 1 && payload[0] == (byte)'0';
        }

        // === 외부에서 호출 ===
        public async Task<string?> ReadBoardVersionAsync(CancellationToken ct)
        {
            var payload = await SendAndWaitAsync(IctCmd01Mapper.Cmd, Array.Empty<byte>(), CH_BOARD_RESPONSE_TIMEOUT_MS, ct);
            if (payload == null) return null;

            if (!IctCmd01Mapper.TryParseRx(payload, out var maj, out var min)) return null;
            return IctCmd01Mapper.ToVersionString(maj, min);
        }

        public async Task<bool> EnterBootloaderByF0Async(CancellationToken ct)
        {
            var start = Environment.TickCount64;

            while (!ct.IsCancellationRequested)
            {
                var payload = await SendAndWaitAsync(0xF0, Array.Empty<byte>(), F0_RETRY_PERIOD_MS, ct);
                if (payload != null && IsOkAck(payload)) return true;

                if (Environment.TickCount64 - start > F0_TIMEOUT_MS)
                    return false;
            }
            return false;
        }

        public async Task<bool> SendFirmwareAsync(
            byte[] fileBytes,
            Action<long, long>? onProgress,
            CancellationToken ct)
        {
            if (fileBytes == null || fileBytes.Length == 0) return false;

            long offset = 0;
            long total = fileBytes.Length;

            while (offset < total)
            {
                ct.ThrowIfCancellationRequested();

                int size = (int)Math.Min(1024, total - offset);

                // MFC: dwStartAddress = offset (베이스 없음)
                uint addr = (uint)offset;

                var payload = IctCmdF1Mapper.BuildTxPayload(addr, fileBytes, (int)offset, size);

                // ✅ 이제 rx는 payload만 옴 (cmd 붙어있지 않음)
                var ack = await SendAndWaitAsync(0xF1, payload, F1_ACK_TIMEOUT_MS, ct);
                if (ack == null || !IsOkAck(ack)) return false;

                offset += size;
                onProgress?.Invoke(offset, total);
            }

            // F2 Complete (payload only)
            var ack2 = await SendAndWaitAsync(0xF2, Array.Empty<byte>(), F2_TIMEOUT_MS, ct);
            if (ack2 == null || !IsOkAck(ack2)) return false;

            return true;
        }
    }
}

해당 클래스는
UDP위에서 요청 -> 해당 응답 하나만 골라서 기다리는 구조

핵심변수 2개

  • private byte _waitCmd; : 지금 나는 CMD 응답을 기다리고 있다.
  • private TaskCompletionSource<byte[]?>? _waitRx; : 그 응답이 오면 나를 깨워라

SendAndWaitAsync

var ack = await SendAndWaitAsync(0xF1, payload, 2500, ct);

  • f1 명령을 보내고,
  • f1응답이 올때까지 2.5초 기다렸다가
  • 그 응답 payload 나에게 줘 라는 뜻
  1. _waitCmd = cmd // 이 CMD 기다릴게
  2. _waitRx = new TaskCompletionSource<byte[]>(); // 응답이 오면 나 깨워
  3. await _udp.SendAsync() // 패킷 보냄, await(기다림=> 응답 or 타임아웃)

OnDatagram : 문지기

if (tcs != null && cmd == _waitCmd)
{
tcs.TrySetResult(payload);
}

  • 기다리던 cmd가 왔다 -> 깨워라는 뜻

SendAndWaitAsync
1. send
2. 기다림

  1. Ondatagram이 깨움

낯선 변수/멤버/함수/클래스 정리
1. _udp

  • 타입: IctUdpService
  • 의미: UDP 소켓(송수신) 담당 서비스
  • 하는 일 :
    - SnedAsync()로 보냄
    • 내부 ReceiveLoop가 패킷 받으면 DatagramReceived 이벤트 발생
  1. _ch0Base
  • 타입: int
  • 의미: 채널 번호를 0부터 쓰기 위한 값
  • 예:
    - 0이면 CH01
    • 1이면 CH02
  • BuildFrame(_ch0Base, cmd, payload)에서 CH 계산에 쓰임
  1. _boardIp
  • 타입: string
  • 의미: 대상 보드 IP
  • 설정:
    _boardIp = $"192.168.0.{ch0Base + 1}";
    • ch0Base=0 -> 192.168.0.1
  1. _waitCmd
  • 타입: byte
  • 의미: 지금 기다리는 응답의 CMD
  • 예:
    - F1 보내고 응답 기다릴 때 _waitCmd=0xF1
  1. _waitRx
  • 타입: TaskCompletionSource<byte[]?>?
  • 의미: 응답이 오면 깨워주는 스위치
  • 핵심:
    - sendAndWaitAsync()가 _waitRx를 만들어 놓고 기다림
    • OnDatagram()이 응답을 받으면 _waitRx.TrySetResult(payload)로 깨움
  • 이게 코드에서 제일 어려운 부분, 한줄로 만들면 UDP에는 요청응답 기능이 없어서 우리가 직접 요청 응답처럼 보이게 만드는 장치임.
  1. TaskCompletionSource<>
  • 미래에 완료될 Task를 내가 직접 완료시키는 도구
  • 쉽게
    - awit로 기다리는 쪽
    • 실제 응답이 왔을 때 완료시키는 쪽
    • 둘을 연결해주는 전화 벨 같은것
  1. DatagramReceived 이벤트
  • _udp.ReceiveLoop() 에서 패킷 받으면 자동 호출
  • 이 클래스에서는 생성자에서 구독
    - _udp.DatagramReceived += OnDatagram;
  1. TryParseFrame(...)
  • UDP 프레임(STX~ETX)을 분석해서 cmd랑 payload(DATA)만 뽑아주는 함수
  • cmd: 0x0E, 0xF1 같은 것
  • payload: 그 CMD에 딸린 데이터(바이트 배열)
  1. Encoding.ASCII.GetString(datagram, 3, 2)
  • datagram[3], datagram[4] 두 바이트를 ASCII 문자열로 만든다는 뜻
  • 예: 'F','1' → "F1"
  • 그걸 Convert.ToByte(...,16) 해서 0xF1로 바꿈
  1. Task.WhenAny(A, B)
  • A와 B 중 먼저 끝나는 것을 기다림
  • 여기서는:
  • A = “응답이 오는 Task”
  • B = “timeout 시간 지나면 끝나는 Task”
  • 둘 중 timeout이 먼저면 null 반환
  1. ReferenceEquals(_waitRx, tcs)
  • “지금 _waitRx가 여전히 내가 만든 tcs인가?” 확인
  • 비동기 환경에서 다른 요청이 들어와 _waitRx가 바뀌었을 수도 있어서 안전장치로 넣는 것
profile
0부터 시작하는 백엔드

0개의 댓글