Zephyr ESP32 Tutorial #5 – Wi-Fi TCP Communication

Introduction

This article is the fifth part of the Zephyr ESP32 series and covers TCP communication. TCP/IP is one of the most widely used communication methods in computer networks.

In the previous UDP communication project, we verified basic network communication by exchanging data between the ESP32 and a PC. This time, we will use TCP to establish a connection between the ESP32 and the PC and exchange data in both directions.

The goal of this project is to connect the ESP32 to a Wi-Fi network, configure it to operate as a TCP client, and communicate with a TCP server running on the PC.

What Is TCP Communication?

TCP (Transmission Control Protocol) is a Protocol for reliably exchanging data between two devices connected to a network.

In the previous project, UDP transmitted data without establishing a connection with the other device in advance. Its structure is simple and data can be transmitted quickly, but it does not guarantee whether the data has actually reached the destination or whether it has arrived in the correct order.

TCP, on the other hand, first establishes a Connection with the other device before transmitting data.

Once the connection has been established, data can be exchanged while checking whether it has been delivered correctly. If data is lost during transmission, it can be retransmitted, and when multiple pieces of data are transmitted, TCP ensures that they are delivered in the original order.

The main characteristics of TCP are as follows.

  • Connection-oriented
    A Connection is established with the other device before data transmission.
  • Reliable transmission
    TCP checks whether data has been delivered correctly and retransmits it if necessary.
  • Ordered delivery
    Data is delivered in the same order in which it was transmitted.
  • Bidirectional communication
    Once a Connection is established, both sides can transmit and receive data.

When establishing a TCP Connection, a process known as the 3-Way Handshake is generally used.

The Client sends a SYN to request a connection, the Server responds with SYN + ACK, and the Client sends an ACK in return. The TCP Connection is then established.

Here, SYN (Synchronize) is a control Flag used to synchronize the Sequence Numbers used for data transmission when starting a TCP Connection. It indicates the beginning of a TCP connection request.

The following figure illustrates the TCP communication process.

The TCP connection management and data transmission described above are handled by the TCP/IP Network Stack. Therefore, the Application can use TCP communication through a Socket API without directly implementing the internal operation of the TCP Protocol.

Project Architecture

In this project, the ESP32 operates as a TCP Client, while the PC operates as a TCP Server. The ESP32 and PC are connected through a Wi-Fi Network, and the ESP32 connects to the Server using the PC’s IP Address and TCP Port.

The following figure shows how the TCP Connection process described above is implemented in this project using Zephyr and Python.

In the ESP32 Application, a TCP Socket is created using Zephyr’s Socket API, and connect() is called.

The 3-Way Handshake is then performed between the Zephyr TCP/IP Network Stack and the PC’s TCP/IP Stack, establishing the TCP Connection.

On the PC, a TCP Server written in Python is executed. The Server waits for a Client connection on a specified Port, and when the ESP32 requests a connection, the Server accepts it and establishes the TCP Connection.

After the TCP Connection is established, the ESP32 exchanges data with the PC using send() and recv(). The PC Server can also use the same Connection to send data to the ESP32 or receive data transmitted from the ESP32.

The overall operation of this project is as follows.

  1. The ESP32 connects to the Wi-Fi AP and obtains an IP Address.
  2. The Python TCP Server is started on the PC and waits for a Client connection.
  3. The ESP32 creates a TCP Socket.
  4. The ESP32 calls connect() to request a connection to the PC Server.
  5. A 3-Way Handshake is performed between the Zephyr TCP/IP Network Stack and the PC’s TCP/IP Stack.
  6. The TCP Connection is established.
  7. The ESP32 and PC exchange data bidirectionally through the established Connection.

Once the TCP Connection is established, multiple data transfers can take place while maintaining a single connection. In this project, the ESP32 transmission and reception operations are handled in separate Threads so that they do not interfere with each other.

Therefore, the general TCP structure described in the previous section as Client → Connection Establishment → Bidirectional Data Transfer is implemented in this project as ESP32/Zephyr → TCP Connection → PC/Python Server.

For more information about the BSD Socket API and TCP/IP Network Programming provided by Zephyr, refer to the official documentation.

Zephyr Documentation – BSD Sockets

Creating the Project and Configuring Files

This TCP communication project is created based on the Wi-Fi UDP project used in the previous exercise.

First, copy the existing skpang_esp32_can_wifiudp project and rename it as follows.

skpang_esp32_can_wifitcp

The process of connecting to a Wi-Fi AP and obtaining an IP Address using DHCP is the same as in the UDP project. Therefore, the existing project configuration is retained, while the UDP transmission and reception components are replaced with TCP Client communication.

The project is organized as follows.

In main.c, the Wi-Fi AP connection and IP Address assignment are handled first. Once the Network is ready, the TCP Client is started.

The TCP communication functions are separated into tcp_client.c and tcp_client.h. By placing TCP Socket creation, Server connection, and data transmission/reception in separate files, main.c can be kept from becoming unnecessarily complex.

CMakeLists.txt

Add the Source File used to implement the TCP Client to CMakeLists.txt.

This allows main.c and tcp_client.c to be built together.

prj.conf

To use TCP communication, Zephyr’s Network and Socket features must be enabled.

The Wi-Fi, Network, IPv4, and DHCP settings used in the previous Wi-Fi AP connection project are retained. In this project, the TCP feature required for TCP Socket communication is additionally enabled.

CONFIG_NETWORKING enables Zephyr’s Network functionality, while CONFIG_NET_IPV4 enables IPv4 support.

CONFIG_NET_TCP enables the TCP Protocol, and CONFIG_NET_SOCKETS allows the Application to use the Socket API.

With the existing Wi-Fi connection and DHCP settings in place, these Network settings allow the ESP32 Application to create a TCP Socket and communicate with the PC TCP Server.

The basic project configuration is now complete. In the next section, we will examine how the ESP32 TCP Client is actually implemented, focusing on tcp_client.c.

The Network settings used in the previous Wi-Fi project are retained without modification. The important settings for TCP communication in this project are CONFIG_NET_SOCKETS=y and CONFIG_NET_TCP=y, which enable the Socket API and TCP Protocol.

CHANGELOG.md

The CHANGELOG.md file for this project is as follows.

Since the PC operates as the TCP Server, inbound communication on TCP Port 5000 must be allowed in Windows Firewall so that it can accept TCP connections from the ESP32. The procedure for configuring this setting has been added to the Notes section of CHANGELOG.md.

Implementing the ESP32 TCP Client

This project is mainly organized around main.c and tcp_client.c.

main.c

The Wi-Fi connection section of main.c has already been covered in detail in the previous project. However, in this TCP project, the TCP Client starts only after the Wi-Fi connection and IP Address assignment have been completed. Therefore, we will briefly review the overall operation once again.

In the figure above, the left section shows the initial setup process performed in main() and wifi_connect(), while the right section shows the process handled by Callback functions after Network Events occur.

First, main() obtains the default Network Interface and registers Event Callbacks to check the Wi-Fi connection result and IPv4 Address assignment.

iface = net_if_get_default();

When wifi_connect() is called, it uses NET_REQUEST_WIFI_CONNECT internally to request a connection to the Wi-Fi AP.

At this point, the Wi-Fi Connection is not completed immediately. Instead, the connection result is delivered through an Event.

When the Wi-Fi connection is completed, the NET_EVENT_WIFI_CONNECT_RESULT Event occurs, and the previously registered wifi_connect_handler() is called.

The wifi_connect_handler() checks whether the Wi-Fi connection was successful.

After the Wi-Fi connection is successfully established, an IPv4 Address is assigned through DHCP. When the IPv4 Address is added to the Network Interface, the NET_EVENT_IPV4_ADDR_ADD Event occurs, and the corresponding ipv4_addr_handler() is called.

In ipv4_addr_handler(), the IPv4 Address assigned by DHCP is checked and displayed, and then the TCP Client is started.

if (tcp_client_start() == 0) {
    tcp_client_send("Hello from ESP32 TCP\n");
}

First, tcp_client_start() is called to establish a TCP Connection with the PC TCP Server.

If the TCP Connection is successfully established and tcp_client_start() returns 0, tcp_client_send() is then called to send the first Message to the PC Server.

Therefore, the overall execution flow of main.c can be summarized as follows.

Acquire Network Interface → Register Event Callbacks → Request Wi-Fi Connection → Wi-Fi Connection Event → Assign IPv4 Address via DHCP → IPv4 Event → Start TCP Client → Send Initial Data

An important point here is that the Wi-Fi connection, IPv4 Address assignment, and TCP connection do not occur all at once. Each step begins only after the previous step has been completed.

In particular, the TCP Client does not start immediately after connecting to Wi-Fi. It starts only after confirming that an IPv4 Address has been assigned through DHCP.

In this way, main.c manages the process until the Network is ready for TCP communication, while the actual TCP Socket creation, Server Connection, and data transmission/reception are handled in tcp_client.c.

The complete main.c code is shown below.

tcp_client.c

The actual TCP communication functions are implemented in tcp_client.c.

In tcp_client.c, a TCP Socket is created and connected to the PC TCP Server, after which data can be transmitted and received. A separate Receive Thread is also used so that data sent from the PC can be received continuously.

The following figure shows the operation flow of tcp_client.c.

Establishing the TCP Connection and Creating the Receive Thread

As described earlier, when an IPv4 Address is assigned, ipv4_addr_handler() calls tcp_client_start().

In tcp_client_start(), a TCP Socket is first created using zsock_socket(), and the IP Address and Port of the PC TCP Server are configured. Then, zsock_connect() is called to request a Connection to the PC TCP Server.

When zsock_connect() completes successfully, a TCP Connection is established between the ESP32 and the PC.

The TCP Connection establishment process, including the 3-Way Handshake described earlier, is not handled directly by the Application. In this program, the Application simply calls zsock_connect() to request a Connection to the TCP Server.

After the TCP Connection is established, a separate Receive Thread is created using k_thread_create().

The newly created Receive Thread executes tcp_rx_thread_entry(). Inside this function, zsock_recv() waits for data transmitted from the PC.

zsock_recv() waits until data is received. After the received data is processed, zsock_recv() is called again. Therefore, as shown on the left side of the figure, the TCP data reception process is repeated continuously within the Receive Thread.

TCP Transmission

After creating the Receive Thread, the existing program execution flow continues.

When tcp_client_start() completes successfully, it returns 0, and the program returns to ipv4_addr_handler(), which originally called the function.

As shown earlier in main.c, the following code is then executed.

if (tcp_client_start() == 0) {
    tcp_client_send("Hello from ESP32 TCP\n");
}

Therefore, when tcp_client_start() succeeds, tcp_client_send() is called.

Inside tcp_client_send(), zsock_send() is used to transmit data to the PC TCP Server through the already established TCP Connection.

An important point in this structure is that the execution flows for TCP reception and transmission are separated.

While the Receive Thread continuously waits for data from the PC using zsock_recv(), the existing program flow can transmit data using tcp_client_send().

Therefore, the overall flow after the TCP Connection is established can be summarized as follows.

Create TCP Socket → Connect to TCP Server → Create Receive Thread → Wait for Incoming Data

At the same time, the existing execution flow proceeds as follows.

tcp_client_start() completes → Return to ipv4_addr_handler()tcp_client_send() → TCP Data Transmission

By using a separate Receive Thread in this way, the program can handle TCP transmission and reception without blocking other program operations while zsock_recv() waits for incoming data.

The complete tcp_client.c code is shown below.

Implementing the PC TCP Server

The PC TCP Server is implemented as a Python program.

Since the main focus of this project is to implement a TCP Client on the ESP32 using the Zephyr Socket API, we will only briefly examine the overall structure and operation of the Python code running on the PC TCP Server.

The PC Server waits for a connection from the ESP32 Client on TCP Port 5000. Once the connection is established, data can be exchanged bidirectionally with the ESP32.

To prevent data reception and keyboard input for transmission from interfering with each other, receive_thread() and send_thread() are executed as separate Threads. receive_thread() continuously receives data sent from the ESP32, while send_thread() sends Messages entered by the user to the ESP32.

The complete program is shown below.

The basic operation flow of the code is as follows.

socket()bind()listen()accept() → TCP Connection → Data Transmission/Reception

The Server creates a TCP Socket using socket() and binds it to Port 5000 using bind(). It then waits in the listen() state for a Client Connection. When the ESP32 requests a connection, accept() is used to establish the TCP Connection.

After the Connection is established, receive_thread() receives data transmitted from the ESP32, while send_thread() sends data entered by the user to the ESP32.

If the Connection with the ESP32 is lost, the Server returns to accept() and waits for a new Connection. Therefore, even if the ESP32 is reset or the TCP Connection is re-established, the Server can accept the new connection without restarting the Server program.

Build and Test

Now, let’s build the project and verify the actual TCP communication.

As with the previous projects, we will use the build commands recorded in CHANGELOG.md. Run the commands after moving the Terminal’s current Directory to the Project Directory.

The build command is as follows.

## Build
west build -b skpang_esp32_can/esp32/procpu -p always -- -DBOARD_ROOT="D:/Zephyr/workspace/my_boards"

After the build completes successfully, Flash the Firmware to the ESP32.

## Flash
west flash

Before running the ESP32, start the Python TCP Server created earlier on the PC.

python tools/tcp_server_4.py

The PC TCP Server waits for a Connection from the ESP32 on Port 5000.

When the ESP32 starts, it first connects to the Wi-Fi AP and obtains an IPv4 Address through DHCP. It then calls tcp_client_start() to request a Connection to the PC TCP Server.

Once the TCP Connection is successfully established, the ESP32 operates in the following sequence.

Wi-Fi Connection → IPv4 Address Assignment → Connect to TCP Server → TCP Connection Established → Send Initial Message

On the PC Server, you can verify the "Hello from ESP32 TCP" Message sent by the ESP32.

After that, you can enter a Message on the PC and receive it through the ESP32’s Receive Thread. This confirms that bidirectional TCP communication between the PC and ESP32 is operating over a single TCP Connection.

The following screen shows tcp_server_4.py running on the PC.

The following is the ESP32 Console screen viewed using Docklight.

First, when tcp_server_4.py is started on the PC, the TCP Server waits for a connection from the ESP32 Client on Port 5000.

When the ESP32 starts, it connects to the Wi-Fi AP and obtains an IPv4 Address through DHCP. It then requests a TCP Connection to the PC TCP Server using the PC Server’s IP Address and Port 5000.

On the PC Server screen, you can confirm that the ESP32 Client has connected as follows.

Client connected: ('192.168.0.9', 45929)

Here, 192.168.0.9 is the IPv4 Address assigned to the ESP32, and 45929 is the Source Port used by the ESP32 for this TCP Connection.

Once the TCP Connection is established, tcp_client_start() completes successfully, and tcp_client_send() is then executed to send the initial Message to the PC.

As a result, the following Message is received by the PC Server.

Received: Hello from ESP32 TCP

The same process can also be confirmed on the ESP32 Console.

Wi-Fi connected
IPv4 address: 192.168.0.9
Connecting to TCP server 192.168.0.6:5000...
TCP connected
TCP TX: Hello from ESP32 TCP

This output shows that the sequence of Wi-Fi Connection → IPv4 Address Assignment → PC TCP Server Connection → TCP Connection Establishment → Data Transmission from ESP32 to PC was completed successfully.

You can also see that the Source Port assigned to the ESP32 changed from 45929 to 34640 after the ESP32 was reset. When a new TCP Connection is created, the Client-side Source Port may change, while the Server continues to wait for connections on the fixed Port 5000.

From the results so far, we have confirmed that the ESP32 TCP Client successfully connects to the PC TCP Server and TCP data is transmitted from the ESP32 to the PC.

Next, we will send Messages from the PC Server and verify that zsock_recv() running in the ESP32’s separate Receive Thread receives the data correctly.

In the following test, the PC Server sends the Messages "Hi! from PC 1", "Hi! from PC 2", and "Hi! from PC 3".

The following screen shows the data received by the ESP32.

Messages entered in the PC Server’s send_thread() are transmitted to the ESP32 through the already established TCP Connection.

On the ESP32, the separate Receive Thread created earlier executes zsock_recv() and waits for incoming data. When a Message is sent from the PC, zsock_recv() receives it and displays it on the Console.

On the ESP32 Console, you can confirm that all three Messages sent from the PC were received as follows.

TCP RX: Hi! from PC 1
TCP RX: Hi! from PC 2
TCP RX: Hi! from PC 3

Even after receiving one Message, the Receive Thread does not terminate. Instead, it calls zsock_recv() again and waits for the next Data. Therefore, even if multiple Messages are sent from the PC, the ESP32 can continue receiving them.

This confirms that, through a single TCP Connection, not only ESP32 → PC transmission but also PC → ESP32 reception operates correctly.

In other words, the bidirectional TCP communication between the ESP32 and PC, which was the goal of this project, has been successfully verified.

UDP vs. TCP

In both the previous UDP project and this TCP project, data was transmitted and received between the ESP32 and PC through a Wi-Fi Network.

With UDP, data can be transmitted immediately by specifying the destination IP Address and Port without establishing a separate Connection. With TCP, however, a Connection must first be established between the Client and Server before data can be transmitted or received.

In this project, the ESP32 established a Connection with the PC TCP Server using zsock_connect(), and then transmitted and received data using zsock_send() and zsock_recv().

The main differences between the two Protocols can be summarized as follows.

Item UDP TCP
Communication method Connectionless Connection-oriented
Connection establishment Not required Required
Data format Datagram Byte Stream
Main send/receive API sendto(), recvfrom() send(), recv()
Data delivery Delivery and order not guaranteed Reliable and ordered delivery
Communication structure Relatively simple Relatively complex
Communication overhead Relatively low Relatively high

UDP has a simple structure and low Overhead, while TCP maintains a Connection and provides reliable, ordered data delivery.

Therefore, rather than asking which Protocol is better, it is important to choose UDP or TCP according to the communication requirements of the Application.

Through these two projects, we were able to examine the differences between UDP and TCP not only in theory, but also directly through the Socket API and the actual program structure.

Conclusion

In this project, we configured the ESP32 as a TCP Client and the PC as a TCP Server and verified bidirectional TCP communication over a Wi-Fi Network.

We examined the overall flow from Wi-Fi connection and IPv4 Address assignment to starting the TCP Client, and learned how to establish a TCP Connection using the Zephyr Socket API. We also confirmed how a separate Receive Thread can be used to handle data reception.

Through the previous UDP project and this TCP project, we were able to see how Network communication is implemented on the ESP32 and compare the differences between the two Protocols through actual programs.

In the next project, we will build on this Network communication and use the MQTT Protocol to exchange Messages between the ESP32 and an MQTT Broker.

댓글 남기기