Zephyr tutorial 202 – Socket programming in Zephyr

Zephyr tutorial 202 – Socket programming in Zephyr

Javad Rahimipetroudi (Mind embedded software consultant)
Javad Rahamipetroudi
09/10/2024

Introduction

In the last tutorial about Zephyr networking, we saw what network programming looks like in Zephyr. In this tutorial, we are going to directly use socket APIs in Zephyr. Zephyr offers an implementation of a subset of BSD sockets. This great feature allows us to easily port existing socket-based applications and libraries to Zephyr.

Socket APIs are enabled by setting CONFIG_NET_SOCKETS=y in a project config file. However, to avoid conflicts between other POSIX functions, all socket-related functions are prefixed with zsock_. For example, socket will be called zsock_socket in Zephyr. To be able to reuse libraries or application code that uses BSD sockets without modificatoin, we should enable the CONFIG_POSIX_API option in the project.

In this tutorial, we are going to implement a remote controller system based on a TCP protocol, which enables/disables an actuator (in our case a simple LED) from the host. The microcontroller acts as a TCP server that receives the commands from the client (PC, another MCU, etc…) and applies the commands to the actuators/sensors.

Application Architecture

Socket server application architectureThe firmware is a multithreaded application that comprises several building blocks. Firstly, the server thread waits for incoming connections from clients. Once a connection is established, clients send commands to the server in a specific format. These commands are parsed and validated by the command parser and then put into a command queue.

Secondly, the controller thread is responsible for processing the command queue. Whenever a new command is added to the queue, the controller passes it to the corresponding function to execute. To make the application simple, only the LED section is implemented. However, other actuators also follows the same rule.

Project configuration file

As usual, let’s start with the project configuration file. As we are going to use TCP sockets the following configuration should be enabled for this project.

# Required for LED, FAN, etc...
CONFIG_GPIO=y
# Networking configuration
CONFIG_NETWORKING=y
CONFIG_NET_TCP=y
CONFIG_NET_IPV4=y
CONFIG_NET_IPV6=n
# We are going to use BSD sockets
CONFIG_NET_SOCKETS=y
CONFIG_POSIX_API=y
CONFIG_POSIX_MAX_FDS=6
# When enabled, this will start the connection manager that will
# listen to network interface and IP events in order to verify
# whether an interface is connected or not. It will then raise
# L4 events "connected" or "disconnected" depending on the result.
CONFIG_NET_CONNECTION_MANAGER=y

# Logging
CONFIG_NET_LOG=y
CONFIG_LOG=y
CONFIG_NET_STATISTICS=y
CONFIG_PRINTK=y

# Network buffers
# How many packet receives can be pending at the same time
CONFIG_NET_PKT_RX_COUNT=16
CONFIG_NET_PKT_TX_COUNT=16

# How many network buffers are allocated for receiving data
CONFIG_NET_BUF_RX_COUNT=64
CONFIG_NET_BUF_TX_COUNT=64

# Network application options and configuration
CONFIG_NET_CONFIG_SETTINGS=y
CONFIG_NET_CONFIG_NEED_IPV4=y
CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.168.1.19"
CONFIG_NET_CONFIG_PEER_IPV4_ADDR="192.168.1.100"

Some configurations were discussed in the last tutorial, so we omitted the description for them. The important configuration variables added here are CONFIG_NET_TCP, CONFIG_NET_SOCKETS, and CONFIG_NET_SOCKETS which allow us to write our socket application.

Thread definition

As the first step, the network and controller threads are defined as follows. K_THREAD_DEFINE is a Zephyr macro that statically defines a thread (instead of creating it dynamically from the main function).

#if defined(CONFIG_NET_IPV4)

#define MAX_CLIENT_QUEUE 1 
#define CONFIG_NET_SAMPLE_NUM_HANDLERS 1
#define STACK_SIZE      4096
#define THREAD_PRIORITY K_PRIO_PREEMPT(8)
static int process_tcp(void *data);
static int controller(void *data);

K_THREAD_DEFINE(tcp4_thread_id, STACK_SIZE, process_tcp, 
                NULL, NULL, NULL, THREAD_PRIORITY, 0, 0);

K_THREAD_DEFINE(cnt_thread_id, STACK_SIZE, controller,
                NULL, NULL, NULL, THREAD_PRIORITY, 0, 0);
#endif

Server thread

Let’s begin by discussing the server. Since socket programming in Zephyr is very similar to BSD socket programming, we will not go into detail about socket programming concepts.

The process_tcp is the server thread’s main function. It waits for a connection from the client side. Afterward, it reads commands from the socket, parses then and sends them to the controller thread. A couple of LOG lines are added to the code to provide further clarification.

In the first line, we have defined a queue for passing the commands from the server thread to the controller thread. K_FIFO_DEFINE is a Zephyr macro that defines and initializes a FIFO queue statically.

K_FIFO_DEFINE(cmd_queue);

static int process_tcp(void *data)
{
    int ret = 0;
    int opt, optlen = sizeof(int);
    struct addrinfo *res;
    int sck, st;
    struct sockaddr_in servaddr, cli;

    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(1989);
        
    sck = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    ret = getsockopt(sck, IPPROTO_IP, SOL_SOCKET, &opt, &optlen);
    
    while(1)
    {
        ret = setsockopt(sck, IPPROTO_IP, SOL_SOCKET, &opt, &optlen);

        // Binding newly created socket to given IP and verification 
        if ((bind(sck, (struct sockaddr *)&servaddr, sizeof(servaddr))) != 0) {
            LOG_ERR("socket bind failed...");
            exit(0);
        }

        if ((listen(sck, CONFIG_NET_SAMPLE_NUM_HANDLERS)) != 0)
        {
            LOG_ERR("Listen failed...");
            exit(0);
        }
        else
            LOG_INF("Server listening...");

        socklen_t len = sizeof(cli);
        int counter = 0;

        while(1)
        {
            char addr_str[32];

            // Accept the data packet from client and verification 
            int cli_con = accept(sck,(struct sockaddr *) &cli, &len);
            if (cli_con < 0) {
                LOG_ERR("server accept failed...");
                continue;
            }
            else
                LOG_DBG("server accept the client...");

            inet_ntop(cli.sin_family, &cli.sin_addr,
                      addr_str, sizeof(addr_str)); //convert IPv4 and IPv6 addresses from binary to text form
            LOG_INF("Connection #%d from %s", counter++, addr_str);
            while(1)
            {
                int rx_len = 0;
                char rx_buf[64] = {0};
                struct cmd_fifo cmd;


                rx_len = recv(cli_con, rx_buf, sizeof(rx_buf), 0);
                if(rx_len <0)
                {
                    LOG_ERR("RX Error: %d", rx_len);
                }
                else
                {
                    LOG_INF("Received: %s", rx_buf);
                    /* Allocate memory to command data strucure */
                    size_t size = sizeof(struct cmd_fifo);
                    char *mem_ptr = k_malloc(size);
                    __ASSERT_NO_MSG(mem_ptr != 0);
                    
                    /* Validate and parse the received command */
                    cmd_parser(rx_buf, &cmd);
                    memcpy(mem_ptr, &cmd, size);
                    k_fifo_put(&cmd_queue, mem_ptr);
                }
            }
            LOG_INF("Close client...");
            close(cli_con);
        }
        close(sck);
    }
    return ret;
}

Command parser

A simple command parser has been added to the application to transform human-readable commands into command data structures.

The command has the following structure:

  

For example, if the user wants to turn on FAN for 10 seconds, the following command should be sent:

FAN ON 10

On the server side, the received commands are parsed and stored in command data structures:

/* Command typedefs */
typedef enum _DEV_TYPES {
    DEV_FAN = 0x00,
    DEV_LED = 0x01,
} dev_type_t;

typedef enum _FAN_CMDS_ {
    DEV_OFF = 0x00,
    DEV_ON = 0x01
} fan_cmd_t;
/* Main command data structure */
struct cmd_fifo {
    void *fifo_reserved; /* 1st word reserved for use by fifo */
    dev_type_t _dev;
    fan_cmd_t  _cmd;
    uint8_t _val;
};

The command_parser function traverses the received buffer and puts each section into the corresponding variable:

int cmd_parser(char *raw_cmd, struct cmd_fifo* out_cmd)
{
    int ret = 0;
    char * pch;
    cmd_seq_t cmd_idx = 0;
    if(raw_cmd == NULL)
            ret = -1;
    pch = strtok(raw_cmd, " ");

    while (pch != NULL)
    {
        LOG_DBG("CMD IDX: %d, Parsed: %s", cmd_idx, pch);
        switch(cmd_idx)
        {
        case CMD_DEV:
            if(0 == strncmp("FAN", pch,3))
            {
                LOG_DBG("FAN CMD");
                out_cmd->_dev = DEV_FAN;
            }
            else if(0 == strncmp("LED",pch,3))
            {
                LOG_DBG("LED CMD");
                out_cmd->_dev = DEV_LED;
            }
            break;
        case CMD_TYPE:
            if(0 == strncmp("ON", pch,2))
            {
                LOG_DBG("ON Detected");
                out_cmd->_cmd = DEV_ON;
            }
            else if(0 == strncmp("OFF",pch,3))
            {
                LOG_DBG("OFF Detected");
                out_cmd->_cmd = DEV_OFF;
            }
            break;
        case CMD_VALUE:
            out_cmd->_val = atoi(pch);
            break;
        }
        cmd_idx++;
    }
}

Note that this code assumes there’s only a single command in the buffer. Since TCP is a stream, in general there may be several commands in the buffer, or the command may not be completely in the buffer yet. Zephyr’s ring buffer should be used instead of a simple linear buffer. For simplicity of the example, this was left out here.

Controller thread

Finally, when the commands are parsed, the validated commands are sent to the controller thread which applies them to the actuators. To make it simple, only the LED section is implemented as a demo:

/* Controller callback function */
int controller(void *data)
{
    int ret;
    bool led_state = true;

    if (!gpio_is_ready_dt(&led)) {
        return 0;
    }

    ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
    if (ret < 0) { return 0; } while (1) { struct cmd_fifo *rx_data = k_fifo_get(&cmd_queue, K_FOREVER); LOG_INF("Device %d; cmd=%d val: %d", rx_data->_dev, rx_data->_cmd, rx_data->_val);

        if(rx_data->_dev == DEV_LED)
        {
            switch (rx_data->_cmd)
            {
            case DEV_OFF:
                gpio_pin_set_dt(&led, DEV_OFF);
                break;
            case DEV_ON:
                gpio_pin_set_dt(&led, DEV_ON);
                break;
            default:
                LOG_ERR("Illegal LED command");
                break;
            }

            k_msleep(SLEEP_TIME_MS*rx_data->_val);
            ret = gpio_pin_toggle_dt(&led);
            k_free(rx_data);
        }
    }
    return 0;
}

Test

To test our controller, telnet is used as a client. However, any other TCP socket application could also be used. First, build and flash the firmware inside the NUCLEO board:

west build -p always -b nucleo_h723zg workspace/IoTHub_server/

After flashing the MCU, it should start to set up the network stack:

The server is listening for incoming connections on port 1989. So we can connect to the server by following the command:

$ telnet 192.168.1.19 1989

If the connection is successful, the following message is shown:

Now we can send commands to the server. For example, to turn on the LED for 5 seconds, we send the following command:

LED ON 5

On the server side, we see the following logs:

and the LED will be on for 5 seconds.

Conclusion

In this tutorial, we created a useful application that utilizes socket features to solve a common problem. The Zephyr’s power of abstraction is highlighted in this application, as it allows for network-based application development without having to worry about the underlying microcontroller. However, the BSD socket API is still a pretty low-level interface to the networking stack. In practice, you’ll probably be using one of the higher-level protocols offered by Zephyr: CoAP, HTTP Server and Client, MQTT, Websocket client, etc.

In this tutorial, we only covered how to program with BSD sockets. The networking still needs to be configured to be usable. In the following tutorial, we will dive deeper down the networking stack and look at how to configure a WiFi interface.

Presentations

Drop the docs and embrace the model with Gaphor Fosdem '24 - Frank Van Bever 20 March, 2024 Read more
How to update your Yocto layer for embedded systems? ER '23 -Charles-Antoine Couret 28 September, 2023 Read more
Tracking vulnerabilities with Buildroot & Yocto EOSS23 conference - Arnout Vandecapelle 12 July, 2023 Read more
Lua for the lazy C developer Fosdem '23 - Frank Van Bever 5 February, 2023 Read more
Exploring a Swedish smart home hub Fosdem '23 - Hannah Kiekens 4 February, 2023 Read more
prplMesh An Open-source Implementation of the Wi-Fi Alliance® Multi-AP (Arnout Vandecappelle) 25 October, 2018 Read more

 

News