서론
Zephyr ESP 실습의 네 번째 글입니다.
이전 글까지는 ESP32 Wi-Fi Module이 Wi-Fi Network의 AP에 접속하고 IP 주소를 할당받는 과정까지 실습을 했다면 지금부터는 Embedded 장치와 PC가 Wi-Fi Network를 통해 실제로 데이터를 주고받는 것을 실습해 보겠습니다.
Wi-Fi Network를 통한 통신 방법에는 여러 가지가 있지만 가장 먼저 실습해 볼 통신 방식은 UDP입니다. UDP는 TCP에 비해 동작 방식이 단순하기 때문에 비교적 적은 코드만으로도 Wi-Fi Network를 통한 데이터 송수신을 구현해 볼 수 있습니다.
이번 실습에서는 먼저 ESP32에서 PC로 UDP Data를 전송하고, 이어서 PC에서 ESP32로 UDP Data를 전송해 보겠습니다. 이를 통해 ESP32가 Wi-Fi AP에 접속하여 IP 주소를 할당받는 것에서 한 단계 더 나아가 실제 Wi-Fi Network를 통해 양방향으로 데이터를 주고받는 과정을 확인해 보겠습니다.
또한 Zephyr에서 Socket을 생성하고 UDP 통신에 사용하는 과정을 살펴보고, PC에서는 간단한 Python 프로그램을 이용하여 ESP32와 데이터를 주고받아 보겠습니다.
프로젝트 생성 및 구성파일 작성
새 프로젝트를 생성하고 구성 파일을 작성해 보겠습니다.
이 부분은 새 프로젝트를 시작할 때마다 반복적으로 해야 하는 작업입니다.
앞선 글에서도 다루었던 내용을 계속 반복하는 이유는 새 프로젝트마다 달라지는 부분도 있지만, 설령 크게 달라지는 부분이 없더라도 이러한 과정을 반복함으로써 Zephyr 프로젝트를 시작하는 기본적인 작업 순서를 몸에 익히는 의미가 있기 때문입니다.
프로젝트 생성
이전 프로젝트인 skpang_esp32_can_wificonnect를 복사하여 skpang_esp32_can_wifiudp라는 폴더를 만듦니다.
아래는 만들어진 폴더의 모습입니다.

이전 프로젝트와는 달리 tools라는 폴더가 새로 생긴 것을 볼 수 있습니다.
이것은 나중에 만든 것으로 PC에서 UDP 통신을 하기 위한 Server 및 Client 용 Python 프로그램을 저장한 폴더 입니다.
나중에 다시 설명을 하겠습니다.
구성 파일 작성
위 폴더에서 볼 수 있는 CMakeLists.txt, prj.conf, CHANGELOG.md는 새로 생성한 프로젝트에 맞게 수정해야 할 파일들입니다.
CMakeLists.txt

위 그림이 CMakeLists.txt의 내용입니다.
이전 프로젝트와 달라진 점은 target_sources 부분에 src/udp_rx.c가 추가되었다는 점입니다.
프로그램 구현 부분에서 자세히 설명하겠지만, 이번 프로젝트는 UDP 통신을 이용하여 데이터를 송신하고 수신하는 프로그램입니다.
따라서 UDP 데이터 수신은 main.c와 분리하여 udp_rx.c에서 별도의 Thread로 처리하도록 구성했습니다.
prj.config
아래 그림은 prj.conf 파일입니다. 이전 Wi-Fi Connect 프로젝트와 비교하면 크게 두 부분이 달라졌습니다.

①은 UDP 통신을 위해 추가한 Socket 관련 설정입니다.
CONFIG_NET_SOCKETS=y는 Zephyr에서 Socket API를 사용할 수 있도록 하는 설정입니다. 이번 프로젝트에서는 UDP 데이터를 송수신하기 위해 Socket을 생성하므로 반드시 필요한 설정입니다.
CONFIG_NET_UDP=y는 UDP 프로토콜을 사용할 수 있도록 하는 설정입니다. 앞선 Wi-Fi Connect 프로젝트에서는 AP에 접속하고 IP 주소를 할당받는 것까지만 확인했기 때문에 이 설정이 필요하지 않았지만, 이번 프로젝트부터는 실제 UDP 데이터를 송수신하므로 추가하였습니다.
즉, 기존의 Wi-Fi 및 Network 설정 위에 Socket과 UDP 기능을 추가함으로써 ESP32가 단순히 Wi-Fi Network에 접속하는 것을 넘어 실제 데이터를 송수신할 수 있게 됩니다.
②는 생성되는 Zephyr 실행 파일의 이름을 지정하는 부분입니다.
CONFIG_KERNEL_BIN_NAME="skpang_esp32_can_wifiudp"로 설정하면 빌드 과정에서 생성되는 bin, hex, elf 등의 파일 이름이 기본값인 zephyr 대신 skpang_esp32_can_wifiudp로 생성됩니다.
이 설정은 프로그램 동작에 직접 영향을 주는 것은 아니지만, 여러 프로젝트를 관리할 때 생성된 Firmware 파일이 어떤 프로젝트의 결과물인지 쉽게 구분할 수 있게 해 줍니다.
나머지 Wi-Fi, Network Management, IPv4, DHCP 관련 설정은 이전 Wi-Fi Connect 프로젝트에서 사용했던 내용을 그대로 유지하였습니다.
CHANGELOG.md
아래와 같이 CHANGELOG.md파일을 작성 합니다.

CHANGELOG.md에는 지금까지 진행한 프로젝트의 변경 이력을 계속해서 기록하고 있습니다. 이전 Wi-Fi Scan 프로젝트와 Wi-Fi Connect 프로젝트의 이력을 그대로 유지하고, 이번에 새로 생성한 skpang_esp32_can_wifiudp 프로젝트를 추가하였습니다.
현재는 프로젝트를 생성한 단계이므로 이번 프로젝트의 변경 이력에는 우선 Start라고만 기록해 둡니다. 이후 UDP 송신과 수신 기능을 구현하고 실험을 진행하면서 주요 변경 사항을 하나씩 추가할 것입니다.
이와 같이 프로젝트를 시작할 때부터 CHANGELOG.md를 작성해 두면 개발 과정에서 어떤 기능이 언제 추가되었는지를 계속해서 기록할 수 있습니다.
UDP 통신이란?
UDP 통신의 개요
UDP(User Datagram Protocol)는 Network를 통해 데이터를 전송하기 위해 사용하는 통신 프로토콜 중 하나입니다.
Network 통신에서 많이 사용되는 전송 프로토콜에는 TCP와 UDP가 있습니다. TCP는 상대방과 연결을 설정한 후 데이터가 정상적으로 전달되었는지를 확인하면서 통신합니다. 따라서 신뢰성 있는 데이터 전송이 가능하지만 연결 설정과 데이터 확인 등의 과정이 필요합니다.
반면 UDP는 상대방과 별도의 연결을 설정하지 않고 데이터를 바로 전송합니다. 전송한 데이터가 상대방에게 정상적으로 도착했는지를 확인하거나, 전송된 데이터의 순서를 보장하는 기능도 기본적으로 제공하지 않습니다.
이러한 특징 때문에 UDP는 TCP에 비해 구조가 단순하고 비교적 적은 코드로 데이터를 송수신할 수 있습니다. 따라서 Wi-Fi Network를 이용한 실제 데이터 통신을 처음 실습해 보기에도 적합합니다.
아래 그림은 UDP 통신의 기본적인 구조를 나타낸 것입니다. Application에서 전송할 Data에 UDP Header가 추가되어 하나의 UDP Datagram이 만들어집니다. Datagram은 UDP에서 한 번에 전송되는 하나의 독립된 데이터 묶음이라고 생각할 수 있습니다.
UDP Header에는 송신 측과 수신 측의 Port 번호, Datagram의 길이, 오류 검사를 위한 Checksum 등이 포함됩니다.

IP 주소와 Port
UDP를 이용하여 데이터를 전송하려면 데이터를 받을 장치의 IP 주소와 Port 번호를 알아야 합니다.
IP 주소는 Network에 연결된 장치를 구분하기 위한 주소이고, Port 번호는 그 장치에서 데이터를 받을 프로그램 또는 통신 서비스를 구분하기 위한 번호라고 생각할 수 있습니다.
예를 들어 PC의 IP 주소가 다음과 같다고 가정하고,
192.168.0.10
PC에서 실행하는 UDP Server가 Port 5000을 사용한다면 ESP32는 다음 두 가지 정보를 이용하여 데이터를 전송합니다.
- Destination IP Address :
192.168.0.10 - Destination Port :
5000
즉, IP 주소를 이용하여 어느 장치로 보낼 것인지를 결정하고, Port 번호를 이용하여 그 장치의 어느 프로그램으로 전달할 것인지를 결정합니다.
UDP의 Server와 Client
UDP는 TCP와 달리 통신 전에 Server와 Client 사이에 연결을 설정하는 과정이 없습니다. 따라서 UDP에서 Server와 Client라는 구분은 TCP만큼 엄격한 의미를 가지지는 않습니다.
일반적으로 특정 Port를 열어 두고 데이터를 기다리는 쪽을 UDP Server라고 하고, 그 IP 주소와 Port로 데이터를 전송하는 쪽을 UDP Client라고 부를 수 있습니다.
이번 실습에서는 ESP32와 PC가 서로 UDP 데이터를 주고받도록 구성합니다.
먼저 ESP32에서 PC의 UDP Server로 데이터를 전송하여 ESP32 → PC 통신을 확인합니다. 이어서 PC에서 ESP32로 데이터를 전송하여 PC → ESP32 통신도 확인합니다.
이를 통해 최종적으로 다음과 같은 양방향 UDP 통신을 실습해 보겠습니다.
ESP32 ↔ PC 양방향 UDP 통신
실제 UDP 통신 프로그램에서는 이러한 동작을 구현하기 위해 Socket을 사용합니다. 다음 장에서는 Zephyr의 Socket API를 이용하여 ESP32에서 UDP 데이터를 송신하고 수신하는 프로그램을 구현해 보겠습니다.
ESP32 프로그램 구현
개요
이제 ESP32에서 UDP 통신을 수행하는 프로그램을 구현해 보겠습니다.
이번 프로젝트에서는 앞서 구현한 Wi-Fi Connect 기능을 그대로 사용하여 먼저 ESP32를 Wi-Fi AP에 접속시키고 IP 주소를 할당받습니다. 그 이후 UDP Socket을 생성하여 PC와 데이터를 송수신합니다.
프로그램은 크게 두 부분으로 구성하였습니다.
main.c에서는 Wi-Fi AP 접속과 IP 주소 할당을 확인한 후 UDP Socket을 생성하고 PC로 데이터를 전송합니다. UDP 데이터 수신은 udp_rx.c에서 별도의 Thread로 처리하도록 하였습니다.
아래는 main.c, udp_rx.c, udp_rx.h가 포함된 src 폴더의 구조입니다.

프로그램이 시작되면 먼저 net_if_get_default()를 이용하여 기본 Network Interface를 얻고, Wi-Fi 연결 결과와 IPv4 주소 할당을 확인하기 위한 Event Callback을 등록합니다.
이후 NET_REQUEST_WIFI_CONNECT 요청을 통해 지정한 SSID의 Wi-Fi AP에 접속합니다.
Wi-Fi 연결이 완료되면 NET_EVENT_WIFI_CONNECT_RESULT Event가 발생하고, DHCP를 통해 IPv4 주소가 할당되면 NET_EVENT_IPV4_ADDR_ADD Event가 발생합니다.
이 과정은 이전 Wi-Fi Connect 실습에서 자세히 살펴보았으므로 이번 글에서는 반복되는 부분에 대한 상세한 설명은 생략하겠습니다.
다만 UDP 통신을 시작하기 위해서는 ESP32가 먼저 Wi-Fi Network에 정상적으로 접속하고 IP 주소를 할당받아야 합니다. 따라서 이번 프로그램에서도 UDP Socket을 생성하기 전에 이 과정을 먼저 수행합니다.
전체적인 프로그램의 흐름은 다음과 같습니다.
Wi-Fi AP 접속 → IPv4 주소 할당 → UDP Socket 생성 → PC로 UDP Data 송신
그리고 UDP Data 수신은 별도의 Thread에서 처리합니다.
UDP Socket 생성 → Port Bind → Data 수신 대기 → UDP Data 수신
아래는 main.c의 전체 프로그램입니다;







이제 Wi-Fi Network 연결이 완료된 이후 수행되는 UDP 송신과 수신 부분을 자세히 살펴보겠습니다.
UDP 송신 프로그램
UDP 송신 프로그램은 main.c의 함수중 udp_send() 입니다. 위 코드에서 확인 할 수 있습니다.
상세한 설명은 다음과 같습니다.
ESP32에서 PC로 UDP 데이터를 전송하려면 먼저 데이터를 받을 PC의 IP 주소와 Port 번호를 지정해야 합니다.
이를 위해 sockaddr_in 구조체를 사용하여 Destination Address를 설정합니다. 이 구조체에는 사용할 Address Family, Destination Port, Destination IP Address 등의 정보가 저장됩니다.
UDP Socket은 socket() 함수를 이용하여 생성합니다.
sock = zsock_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
AF_INET은 IPv4를 사용한다는 의미이며, SOCK_DGRAM은 Datagram 방식의 Socket, 즉 UDP Socket을 생성한다는 의미입니다. IPPROTO_UDP는 사용할 Protocol을 UDP로 지정합니다.
Socket이 정상적으로 생성되면 PC의 IP 주소와 Port를 설정합니다.
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(UDP_SERVER_PORT);
ret = zsock_inet_pton(AF_INET,
UDP_SERVER_IP,
&dest_addr.sin_addr);
sin_family에는 IPv4를 의미하는 AF_INET을 지정하고, sin_port에는 PC의 UDP Server가 사용하는 Port 번호를 지정합니다.
htons()는 CPU에서 사용하는 Port 번호 값을 Network에서 사용하는 Byte Order로 변환하는 함수입니다.
Network Byte Order는 Big Endian을 사용하므로, htons()는 16bit Port 번호를 Host Byte Order에서 Network Byte Order로 변환합니다.
여기서 htons는 Host TO Network Short를 의미합니다.
zsock_inet_pton()은 "192.168.x.x"와 같이 문자열로 표현된 IP 주소를 Network에서 사용할 수 있는 Binary 형식으로 변환하여 dest_addr.sin_addr에 저장합니다.
여기서 pton은 Presentation TO Network 을 의미합니다.
Destination Address의 설정이 완료되면 zsock_sendto() 함수를 이용하여 UDP Data를 전송할 수 있습니다.
ret = zsock_sendto(sock,
message,
strlen(message),
0,
(struct sockaddr *)&dest_addr,
sizeof(dest_addr));
여기서
sock → UDP 통신에 사용할 Socketmessage → 전송할 Datastrlen(message) → 전송할 Data의 길이0 → 별도의 Flag를 사용하지 않음dest_addr → Destination IP Address와 Port 정보sizeof(dest_addr) → Destination Address 구조체의 크기
를 의미합니다.
zsock_sendto()를 호출할 때 전송할 Data와 함께 Destination IP Address와 Port가 저장된 dest_addr를 전달합니다.
UDP는 TCP와 달리 상대방과 미리 Connection을 설정하지 않기 때문에 별도의 연결 과정 없이 zsock_sendto()를 이용하여 바로 데이터를 전송할 수 있습니다.
이때 한 번의 zsock_sendto() 호출을 통해 전달되는 데이터는 하나의 UDP Datagram이 됩니다.
UDP 수신 프로그램
UDP Data 수신은 main.c와 분리하여 udp_rx.c에서 별도의 Thread로 처리하도록 하였습니다.
UDP 송신은 필요한 시점에 zsock_sendto()를 호출하여 데이터를 전송하면 되지만, 수신 프로그램은 PC에서 언제 데이터가 전송될지 알 수 없기 때문에 계속해서 수신을 기다려야 합니다.
따라서 이번 프로젝트에서는 UDP 수신을 위한 별도의 Thread를 만들고, 이 Thread에서 UDP Data가 들어올 때까지 대기하도록 구성하였습니다.
이를 위해 src 폴더에 다음 두 개의 파일을 추가하였습니다.
udp_rx.c: UDP 수신 Thread와 실제 수신 기능 구현udp_rx.h: UDP 수신 기능을main.c에서 사용할 수 있도록 필요한 선언을 정의
UDP 수신 프로그램의 기본적인 동작 순서는 다음과 같습니다.
UDP Socket 생성 → Local IP/Port 설정 → Socket Bind → Data 수신 대기 → UDP Data 수신
송신 프로그램에서는 zsock_sendto()를 호출할 때마다 Destination IP Address와 Port를 지정하였습니다.
반면 수신 프로그램에서는 ESP32가 어느 Port로 들어오는 UDP Data를 받을 것인지를 먼저 지정해야 합니다. 이를 위해 Local Address와 Port를 설정하고 생성한 Socket에 zsock_bind()를 사용하여 Bind합니다.
Bind가 완료되면 zsock_recvfrom()을 이용하여 PC에서 전송되는 UDP Datagram을 기다립니다.
이제 udp_rx.h와 udp_rx.c의 내용을 차례로 살펴보겠습니다.
아래는 udp_rx.h의 코드 입니다.

udp_rx.h는 UDP 수신 기능을 다른 소스 파일에서 사용할 수 있도록 필요한 함수의 Prototype을 선언한 Header 파일입니다.
이번 프로그램에서는 udp_rx.c에 구현된 UDP 수신 기능을 시작하기 위해 다음 함수를 선언하였습니다.
void udp_rx_start(void);
main.c에서는 udp_rx.h를 Include한 후 udp_rx_start()를 호출하여 UDP 수신 Thread를 시작할 수 있습니다.
다음은 udp_rx.c에 대한 설명입니다.
아래는 선언부 입니다.

먼저 UDP Socket과 Thread를 사용하기 위해 필요한 Header 파일을 Include합니다.
#include <zephyr/kernel.h>
#include <zephyr/net/socket.h>
#include <zephyr/sys/printk.h>
#include <errno.h>
#include <string.h>
#include "udp_rx.h"
zephyr/kernel.h는 Thread와 같은 Zephyr Kernel 기능을 사용하기 위해 필요하며, zephyr/net/socket.h는 UDP Socket 관련 API를 사용하기 위해 필요합니다.
다음은 UDP 수신 Port와 Thread에 필요한 값을 정의한 부분입니다.
#define UDP_LOCAL_PORT 5001
#define UDP_RX_STACK_SIZE 2048
#define UDP_RX_PRIORITY 5
UDP_LOCAL_PORT는 ESP32가 UDP Data를 수신할 Port 번호입니다. 이번 프로그램에서는 Port 5001을 사용합니다.
따라서 PC에서 ESP32로 UDP Data를 전송할 때는 ESP32의 IP Address와 함께 Destination Port를 5001로 지정해야 합니다.
UDP_RX_STACK_SIZE는 UDP 수신 Thread에서 사용할 Stack의 크기이고, UDP_RX_PRIORITY는 Thread의 Priority를 지정합니다.
다음은 UDP 수신 Thread를 생성하기 위해 필요한 Stack과 Thread 관련 변수를 선언한 부분입니다.
K_THREAD_STACK_DEFINE(udp_rx_stack, UDP_RX_STACK_SIZE);
static struct k_thread udp_rx_thread_data;
static k_tid_t udp_rx_tid;
K_THREAD_STACK_DEFINE()을 이용하여 UDP 수신 Thread에서 사용할 Stack 영역을 생성합니다.
udp_rx_thread_data에는 생성되는 Thread의 제어 정보가 저장되며, udp_rx_tid에는 생성된 Thread의 ID가 저장됩니다.
이와 같이 UDP 수신에 사용할 Port와 Thread 실행에 필요한 항목을 먼저 준비합니다.
다음은 실제로 수신을 처리할 함수인 udp_rx_thread()입니다.


UDP 수신에 필요한 변수
먼저 UDP Data를 수신하기 위해 필요한 변수들을 선언합니다.
int sock;
int ret;
struct sockaddr_in local_addr;
struct sockaddr_in remote_addr;
socklen_t remote_addr_len;
char rx_buffer[128];
sock은 생성된 UDP Socket을 식별하기 위한 Socket Descriptor이고, ret은 각 Socket 함수의 실행 결과를 확인하기 위해 사용합니다.
여기서는 두 개의 sockaddr_in 구조체가 사용됩니다.
local_addr는 ESP32 자신의 UDP 수신 주소 정보를 설정하기 위한 구조체입니다. 특히 ESP32가 어느 Port에서 UDP Data를 받을 것인지를 지정할 때 사용합니다.
반면 remote_addr는 UDP Data를 보낸 상대방의 주소 정보를 저장하기 위한 구조체입니다. 이후 zsock_recvfrom()을 호출하면 송신자의 IP Address와 Port 정보가 이 구조체에 저장됩니다.
remote_addr_len은 remote_addr 구조체의 크기를 전달하기 위해 사용하고, rx_buffer는 실제로 수신한 UDP Data를 저장하는 Buffer입니다.
Thread 함수에는 p1, p2, p3 세 개의 Parameter가 전달될 수 있지만 이번 프로그램에서는 사용하지 않으므로 다음과 같이 처리하였습니다.
ARG_UNUSED(p1);
ARG_UNUSED(p2);
ARG_UNUSED(p3);
UDP Socket 생성
수신에서도 송신과 마찬가지로 먼저 UDP Socket을 생성합니다.
sock = zsock_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
AF_INET은 IPv4, SOCK_DGRAM은 Datagram 방식의 Socket, IPPROTO_UDP는 UDP Protocol을 사용한다는 의미입니다.
Socket 생성에 실패하면 음수 값이 반환되므로 이를 확인하여 Thread를 종료합니다.
if (sock < 0) {
printk("UDP RX socket creation failed: %d\n", errno);
return;
}
Local Address와 Port 설정
다음은 ESP32가 UDP Data를 받을 Local Address를 설정하는 부분입니다.
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_port = htons(UDP_LOCAL_PORT);
local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
먼저 memset()을 이용하여 local_addr 구조체를 0으로 초기화합니다.
sin_family에는 IPv4를 의미하는 AF_INET을 지정하고, sin_port에는 앞에서 정의한 UDP_LOCAL_PORT, 즉 Port 5001을 지정합니다.
여기서 INADDR_ANY는 특정 Local IP Address 하나를 직접 지정하지 않고 사용 가능한 Local IPv4 Address에서 들어오는 UDP Data를 받겠다는 의미입니다.
따라서 이 부분에서는 아직 Socket과 Port가 결합된 것은 아닙니다. ESP32가 사용할 Local Address 정보를 local_addr에 준비한 것입니다.
Socket과 Local Port의 Bind
준비한 local_addr를 실제 UDP Socket에 연결하는 것이 zsock_bind()입니다.
ret = zsock_bind(sock,
(struct sockaddr *)&local_addr,
sizeof(local_addr));
이 부분은 송신에서 사용했던 zsock_sendto()와 비교하면 이해하기 쉽습니다.
송신에서는 zsock_sendto()를 호출할 때 Socket과 Destination Address를 함께 지정하여 Datagram을 전송했습니다.
수신에서는 먼저 zsock_bind()를 이용하여 생성한 Socket에 Local Address와 Port를 Bind합니다.
즉,
zsock_socket()
↓
UDP Socket ─────┐
├── zsock_bind()
Local Address ────┘
└─ Port 5001
↓
Port 5001로 들어오는 UDP Data를
이 Socket에서 수신
이제 ESP32의 Port 5001로 들어오는 UDP Datagram을 이 Socket을 통해 받을 준비가 된 것입니다.
UDP Data 수신
zsock_bind()가 완료되면 ESP32는 지정한 Port로 들어오는 UDP Data를 수신할 준비가 됩니다.
다음 while (1) 문에서는 UDP Data를 계속해서 수신할 수 있도록 반복하여 zsock_recvfrom()을 호출합니다.
먼저 다음 코드에서 remote_addr_len은 remote_addr 구조체의 크기로 설정합니다.
remote_addr_len = sizeof(remote_addr);
remote_addr는 앞에서 설명한 것처럼 UDP Data를 보낸 상대방의 주소 정보를 저장하기 위한 구조체입니다.
이후 zsock_recvfrom()을 호출합니다.
ret = zsock_recvfrom(sock,
rx_buffer,
sizeof(rx_buffer) - 1,
0,
(struct sockaddr *)&remote_addr,
&remote_addr_len);
각 인자의 의미는 다음과 같습니다.
sock→ 앞에서 생성하고 Port 5001에 Bind한 UDP Socketrx_buffer→ 수신한 Data를 저장할 Buffersizeof(rx_buffer) - 1→ 한 번에 수신할 수 있는 최대 Data 크기0→ 별도의 Flag를 사용하지 않음remote_addr→ Data를 보낸 상대방의 IP Address와 Port 정보를 저장remote_addr_len→ 상대방 주소 구조체의 크기
UDP Datagram이 도착하지 않은 상태에서는 zsock_recvfrom()이 Data가 들어올 때까지 기다립니다. PC에서 ESP32의 Port 5001로 UDP Datagram을 전송하면 zsock_recvfrom()이 Data를 수신하여 rx_buffer에 저장하고, 수신한 Data의 Byte 수를 ret으로 반환합니다.
따라서 예를 들어 PC에서 “Hello from PC” 라는 13 Byte의 문자열을 전송하였다면 정상적으로 수신했을 때 ret은 13이 됩니다.
수신 오류 확인
다음 부분에서는 zsock_recvfrom()의 실행 결과를 확인합니다.
if (ret < 0) {
printk("UDP RX receive failed: %d\n", errno);
continue;
}
ret이 음수이면 수신 과정에서 오류가 발생한 것입니다.
오류가 발생하더라도 Thread를 종료하지 않고 continue를 이용하여 다시 while (1)의 처음으로 돌아가 다음 UDP Data를 기다립니다.
수신 Data를 문자열로 완성
정상적으로 Data를 수신하면 다음 코드를 실행합니다.
rx_buffer[ret] = '\0';
zsock_recvfrom()은 수신한 Byte 수를 ret으로 반환하지만, 수신한 Data 뒤에 C 문자열의 끝을 나타내는 '\0'을 자동으로 추가해 주는 것은 아닙니다.
따라서 수신한 Data의 바로 다음 위치에 '\0'을 넣어 rx_buffer를 정상적인 C 문자열로 만들어 줍니다.
앞에서 수신 크기를
sizeof(rx_buffer) - 1
로 지정한 것도 바로 이 '\0'을 저장할 한 Byte의 공간을 남겨두기 위해서입니다.
마지막으로 수신한 문자열을 출력합니다.
printk("UDP RX received: %s\n", rx_buffer);
따라서 PC에서 "Hello from PC"를 전송하면 ESP32의 Serial Terminal에서는 다음과 같은 내용을 확인할 수 있습니다.
UDP RX received: Hello from PC
이 과정이 끝나면 while (1)의 처음으로 돌아가 다시 zsock_recvfrom()을 호출하므로 UDP 수신 Thread는 Port 5001로 들어오는 다음 UDP Datagram을 계속해서 기다리게 됩니다.
전체 흐름은 다음과 같이 정리할 수 있습니다.
while (1)
↓
zsock_recvfrom()
↓
UDP Datagram이 올 때까지 대기
↓
UDP Data 수신
↓
rx_buffer에 저장
↓
'\0' 추가
↓
수신 Data 출력
↓
다시 zsock_recvfrom()
여기서 특히 중요한 것은 zsock_recvfrom()이 Data뿐만 아니라 누가 보냈는지에 대한 remote_addr 정보도 함께 얻을 수 있다는 점입니다. 현재 프로그램에서는 수신 문자열만 출력하고 있지만, 나중에는 remote_addr를 이용하여 송신자의 IP Address와 Port도 확인할 수 있습니다.
udp_rx.c의 마지막 부분은 이 Thread를 시작 시키는 함수 입니다.
아래와 같습니다.

이 부분은 Zeplhyr Thread 글에서 충분히 설명을 하였기 때문에 자세한 설명은 하지 않겠습니다.
이제 ESP32에서 UDP 통신을 하기 위한 송신 및 수신 프로그램이 모두 준비되었습니다. 다음 장에서는 ESP32와 실제로 데이터를 주고받기 위한 PC의 UDP Server와 Client 프로그램을 준비해 보겠습니다.
PC UDP Server 및 Client 준비
ESP32와 UDP Data를 송수신하기 위해 PC에서는 간단한 Python 프로그램을 사용하였습니다.
ESP32에서 PC로 전송되는 UDP Data를 수신하기 위한 UDP Server 프로그램과, 반대로 PC에서 ESP32로 UDP Data를 전송하기 위한 UDP Client 프로그램을 각각 준비하였습니다.
두 프로그램은 프로젝트의 tools 폴더에 저장하였습니다.
UDP Server
아래는 UDP Server의 Python 코드 입니다.

프로그램 내용은 다음과 같습니다.
- PC의 지정된 Port를 열고 대기
- ESP32에서 전송한 UDP Data 수신
- 수신한 Data를 화면에 출력
UDP Client
아래는 UDP Client의 Python 코드 입니다.

프로그램 내용은 다음과 같습니다.
- ESP32의 IP Address와 Port
5001지정 "Hello from PC"와 같은 UDP Data 전송
PC 프로그램은 Python의 socket 모듈을 이용한 간단한 프로그램이며, 이번 글의 목적은 ESP32에서 Zephyr를 이용하여 UDP 통신을 구현하는 것이므로 Python 코드에 대한 자세한 설명은 생략하겠습니다.
Windows 방화벽 설정
ESP32에서 PC로 UDP Data를 전송하기 위해서는 PC의 UDP Server가 사용하는 Port로 들어오는 Network Traffic을 Windows 방화벽에서 허용해야 합니다.
이번 실습에서는 PC의 UDP Server가 Port 5000을 사용하므로 Windows Defender Firewall에서 UDP 5000번 Port의 Inbound Traffic을 허용하도록 설정하였습니다.
Windows PowerShell을 관리자 권한으로 실행한 후 다음 명령을 입력합니다.
New-NetFirewallRule -DisplayName "UDP 5000" -Direction Inbound -Protocol UDP -LocalPort 5000 -Action Allow
이 명령은 Windows Defender Firewall에 UDP 5000이라는 Inbound Rule을 추가하여 UDP Port 5000으로 들어오는 Data를 허용합니다.
이번 실습의 통신 방향과 함께 보면 다음과 같습니다.
ESP32
│
│ UDP Datagram
│ Destination Port : 5000
▼
Windows Firewall
│
│ Inbound UDP 5000 Allow
▼
udp_server.py
│
▼
UDP Data 수신
따라서 ESP32의 UDP 프로그램이 정상적으로 동작하더라도 PC에서 Data가 수신되지 않는다면 UDP Server의 Port 번호뿐만 아니라 Windows 방화벽의 Inbound Rule도 확인할 필요가 있습니다.
이로써 모든 준비가 완료 되었으므로 다음 장에서는 이 두 프로그램을 이용하여 실제로 ESP32 → PC, PC → ESP32 방향으로 UDP Data가 정상적으로 송수신되는지 확인해 보겠습니다.
빌드 및 실험 결과 확인
모든 프로그램이 준비 되었으므로 Build를 해 보겠습니다.
마찬가지로 CHANGELOG.md의 Build 부분에 명기한 명령어를 복사하여 프로젝트 디렉토리에서 실행을 합니다.
아래와 같은 메세지가 출력되면서 Build가 성공적으로 진행된 것을 확인할 수 있습니다.

west flash 명령어로 target 보드에 다운로드하여 실행 해 보겠습니다.
아래와 같은 메세지가 출력되면서 다운로드가 성공하였습니다.

Docklight 프로그램으로 console 출력 메세지를 확인 하였습니다.

Console 출력 결과 ESP32가 Wi-Fi AP에 정상적으로 접속하고 IPv4 주소를 할당받은 것을 확인할 수 있습니다.
또한 UDP Socket이 정상적으로 생성되어 "Hello from Zephyr ESP32" Data를 송신하였으며, UDP 수신 Thread도 시작되어 Port 5001에서 UDP Data를 기다리고 있는 것을 확인할 수 있습니다.
이제 실제로 ESP32에서 전송한 UDP Data가 PC에서 수신되는지 확인해 보겠습니다.
먼저 udp_server.py Python 프로그램을 실행 시켜서 ESP32에서 보낸 메세지가 PC에서 수신이 되는 지 확인 해 보겠습니다.
ESP32 송신 프로그램은 Booting 시 한번 보내므로 Docklight에서 통신을 close 한 후 open하면 ESP32가 Reset되어 데이터를 송신하게 됩니다. 물론 보드의 Reset 버튼이 있으면 그것을 눌러도 됩니다만 우리 보드는 Reset 버튼이 장착되어 있지 않아서 Docklight UART로 리셋을 실행 하였습니다.
PC에서 udp_server.py Python 프로그램을 실행 시키면 아래와 같은 메세지가 출력되고 ESP32로 부터 오는 데이터가 오기를 기다립니다.

보드가 리셋되고 다시 Booting되면서 PC에서는 아래와 같이 메세지가 수신 되었습니다.

수신 결과의 From: ('192.168.0.8', 40905)에서는 UDP Data를 전송한 ESP32의 IP Address와 Source Port도 함께 확인할 수 있습니다. 여기서 40905는 ESP32의 UDP 송신 Socket에 동적으로 할당된 Source Port입니다.
ESP32에서 UDP를 통한 데이터 전송이 성공하였습니다.
다음은 PC에서 Data를 보내 보겠습니다.
PC에서 udp_client.py Python 프로그램을 실행 시킵니다.
ESP32에서는 Data가 수신되면 console로 출력하므로 Docklight에서 확인 합니다.
아래는 세 번을 실행 시킨 화면입니다.

Docklight화면에도 메세지가 세 번 출력된 것이 확인 되었습니다.

실험 결과 ESP와 PC 사이에 Wi-Fi Network의 UDP 통신을 이용한 양방향 Data 전송이 성공 하였습니다.
결론
이번 실습에서는 앞서 구현한 Wi-Fi Connect 프로젝트를 기반으로 ESP32와 PC 사이에서 UDP를 이용하여 실제 Data를 송수신해 보았습니다.
이전 실습에서는 ESP32가 Wi-Fi AP에 접속하고 DHCP를 통해 IP 주소를 할당받는 것까지 확인하였습니다. 이번에는 여기서 한 단계 더 나아가 할당받은 IP 주소를 이용하여 실제 Network Data 통신을 구현하였습니다.
ESP32에서는 Zephyr의 Socket API를 이용하여 UDP Socket을 생성하고, zsock_sendto()를 이용하여 PC로 UDP Datagram을 전송하였습니다. 또한 별도의 Thread에서 수신 Socket을 생성하고 Local Port에 Bind한 후 zsock_recvfrom()을 이용하여 PC에서 전송되는 UDP Data를 수신하였습니다.
PC에서는 간단한 Python UDP Server와 Client 프로그램을 이용하여 ESP32 → PC, PC → ESP32 두 방향의 Data 전송을 각각 확인하였습니다.
이번 실습을 통해 UDP의 Connectionless 특성과 Datagram의 의미뿐만 아니라 IP Address와 Port, Socket, Bind, 송신과 수신의 관계도 실제 프로그램을 통해 살펴볼 수 있었습니다.
특히 Wi-Fi AP에 접속하고 IP 주소를 할당받는 것만으로 Network 통신이 완성되는 것은 아니며, 그 위에서 UDP와 같은 전송 Protocol을 이용해야 실제 Application 사이의 Data 통신이 이루어진다는 것도 확인할 수 있었습니다.
이것으로 ESP32와 PC 사이의 기본적인 UDP 통신이 완성되었습니다. 다음 실습에서는 UDP와 다른 특성을 가지는 TCP 통신을 구현하여 두 Protocol의 차이를 실제 프로그램을 통해 살펴보겠습니다.