Cellular and WiFi Stack Management on a custom nRF9160 board

Hello!
We have developed a custom board featuring an nRF9160 and an ESP32-C3-MINI-1 co-processor, which is flashed with AT-firmware (version 3.2.0.0). Our current application runs an LWM2M client that connects to an LWM2M server and sends data.

We currently have two firmware builds, each one only compiles in only one network stack (either LTE or WiFi):
1. LTE with nRF9160: Utilizes socket offloading, securely connects to LWM2M with DTLS and COAPS.
2. WiFi with ESP32-C3-MINI-1: Utilizes the Espressif AT Command offloaded WiFi driver, and connects to LWM2M insecurely with COAP (unresolved issues).

Our current objective is to modify our application to support management of both cellular and WiFi network stacks at runtime.
Specifically, the application should be capable of:

- Initializing both network stacks.
- Connecting to both networks.
- Handle switching between networks in case of the disconnection. For example, if WiFi connection becomes unavailable, application should switch to completely using the LTE connection.

Before implementing this in the actual application we created a small sample with shell commands to test the dual stack setup by running both interfaces and sending data to some server.
As a starting point, we took the NCS net/udp sample which can be used on an nRF9160 and nRF7002, but we did not use network management (net_mgmt) to handle initialization and event management of both interfaces.

In this sample the initialization and simultaneous operation of both cellular and WiFi stacks was successful, ESP connected to an access point, nRF9160 connected to LTE.
But when we tried to send data using the socket API we encountered issues when trying to use each interface separately.
Sending only worked over LTE, but not over WiFi. To make sending over WiFi possible we had to:

- enable NET_SHELL
- connect using our wifi commands and
- send the data with the net udp send command,

That way we were able to send a UDP payload through the WiFi interface, with LTE connection still present at that moment.
We were not able to figure out how to switch between the interface, without using the entire net_shell.c logic.
While doing some experimenting, it seemed that either interface wanted to use only one offload implementation.

Main questions:

- Is the simultaneous operation of both interfaces (cellular and WiFi) possible with our current setup?
- Is dynamic switching between network interfaces possible via net_if_* calls?
- Is socket offloading preventing the WiFi interface from using its own implementation?

Development setup:

- OS: Ubuntu 20.04
- Zephyr: 3.5.0
- NCS: 2.5.0

Sample prj.conf:

# Enable console
CONFIG_USE_SEGGER_RTT=y
CONFIG_RTT_CONSOLE=y
CONFIG_SHELL=y
CONFIG_SHELL_BACKEND_RTT=y
CONFIG_SHELL_TAB=y

# Serial
CONFIG_SERIAL=y

# Modem library
CONFIG_MODEM=y
CONFIG_NRF_MODEM_LIB=y
CONFIG_MODEM_INFO=y

# AT monitor library
CONFIG_AT_MONITOR_HEAP_SIZE=4096
CONFIG_AT_MONITOR=y

# PDN
CONFIG_PDN=y

# LTE
CONFIG_LTE_LINK_CONTROL=y # Enables LTE custom commands
CONFIG_LTE_NETWORK_MODE_DEFAULT=y
CONFIG_LTE_NETWORK_USE_FALLBACK=n
CONFIG_LTE_SHELL=n # Disable LTE shell

# Networking
CONFIG_NETWORKING=y

CONFIG_NET_SHELL=y
CONFIG_NET_L2_WIFI_SHELL=n # Enables net wifi shell

CONFIG_NET_NATIVE=y
CONFIG_NET_OFFLOAD=n

CONFIG_NET_SOCKETS=y
CONFIG_NET_SOCKETS_OFFLOAD=y
CONFIG_NET_SOCKETS_POSIX_NAMES=y # Expose Zephyr socket API without zsock_ prefix
CONFIG_NET_SOCKETS_LOG_LEVEL_DBG=y

# Net event management and other logging
CONFIG_NET_LOG=y
CONFIG_NET_MGMT_EVENT_MONITOR=y
CONFIG_NET_OFFLOAD_LOG_LEVEL_DBG=y

# Network buffers
CONFIG_NET_TX_STACK_SIZE=2048
CONFIG_NET_RX_STACK_SIZE=2048
CONFIG_NET_PKT_RX_COUNT=10
CONFIG_NET_PKT_TX_COUNT=10
CONFIG_NET_BUF_RX_COUNT=20
CONFIG_NET_BUF_TX_COUNT=20
CONFIG_NET_MAX_CONTEXTS=10

# IPv
CONFIG_NET_IPV4=y
CONFIG_NET_IF_MAX_IPV4_COUNT=5
CONFIG_NET_IPV6=y
CONFIG_NET_IF_MAX_IPV6_COUNT=5

# Protocols
CONFIG_NET_UDP=y

# ESP wifi configs
CONFIG_WIFI=y
CONFIG_WIFI_DRIVER=y # Custom WiFi driver with helper functions
CONFIG_WIFI_LOG_LEVEL_DBG=y

# AT Command offloaded WiFi driver.
CONFIG_WIFI_ESP_AT=y
CONFIG_WIFI_ESP_AT_PASSIVE_MODE=y


Cellular commands:

#include <zephyr/net/socket.h>
#include <zephyr/shell/shell.h>
#include <modem/lte_lc.h>

LOG_MODULE_REGISTER(lte_commands, LOG_LEVEL_DBG);

static int client_fd;
static struct sockaddr_storage host_addr;

static void prv_lte_handler(const struct lte_lc_evt *const evt)
{
    switch (evt->type) {
    case LTE_LC_EVT_NW_REG_STATUS:
        if ((evt->nw_reg_status != LTE_LC_NW_REG_REGISTERED_HOME) &&
            (evt->nw_reg_status != LTE_LC_NW_REG_REGISTERED_ROAMING)) {
            break;
        }

        printk("Network registration status: %s\n",
               evt->nw_reg_status == LTE_LC_NW_REG_REGISTERED_HOME ? "Connected - home"
                                       : "Connected - roaming");
        break;
    case LTE_LC_EVT_PSM_UPDATE:
        printk("PSM parameter update: TAU: %d s, Active time: %d s\n", evt->psm_cfg.tau,
               evt->psm_cfg.active_time);
        break;
    case LTE_LC_EVT_EDRX_UPDATE:
        printf("eDRX parameter update: eDRX: %.2f s, PTW: %.2f s\n", evt->edrx_cfg.edrx,
               evt->edrx_cfg.ptw);
        break;
    case LTE_LC_EVT_RRC_UPDATE:
        printk("RRC mode: %s\n",
               evt->rrc_mode == LTE_LC_RRC_MODE_CONNECTED ? "Connected" : "Idle\n");
        break;
    case LTE_LC_EVT_CELL_UPDATE:
        printk("LTE cell changed: Cell ID: %d, Tracking area: %d\n", evt->cell.id,
               evt->cell.tac);
        break;
    default:
        break;
    }
}

/**
 * @brief Connect to LTE network.
 *
 * This function connects to the LTE network. The function will return
 * immediately and the connection process will be handled by the event handler.
 *
 * @param[in] sh        Pointer to the shell instance.
 * @param[in] argc        Number of arguments.
 * @param[in] argv        Arguments.
 *
 * @retval 0 If the command was successful.
 * @retval -EINVAL If no event handler was registered.
 * @retval -EFAULT If the command was successful.
 */
static int cmd_lte_init_and_connect(const struct shell *sh, size_t argc, char **argv)
{
    int err;

    err = lte_lc_init();
    if (err) {
        shell_error(sh, "lte_lc_init, err: %d", err);
        return err;
    }

    /* Reister handler */
    lte_lc_register_handler(prv_lte_handler);

    err = lte_lc_connect_async(NULL);
    if (err) {
        shell_error(sh, "lte_lc_connect_async, err: %d", err);
        return err;
    }

    return 0;
}

/**
 * @brief Disconnect from LTE network.
 *
 * @param[in] sh         Pointer to the shell instance.
 * @param[in] argc        Number of arguments.
 * @param[in] argv        Arguments.
 *
 * @retval 0 If the command was successful.
 * @retval -EFAULT If functional mode could not be configured.
 */
static int cmd_lte_offline(const struct shell *sh, size_t argc, char **argv)
{
    ARG_UNUSED(argc);
    ARG_UNUSED(argv);

    int err;

    err = lte_lc_offline();
    if (err) {
        shell_error(sh, "Error: lte_lc_offline() returned %d", err);
        return err;
    }

    return 0;
}

/**
 * @brief Power off LTE modem.
 *
 * @param[in] sh         Pointer to the shell instance.
 * @param[in] argc         Number of arguments.
 * @param[in] argv         Arguments.
 *
 * @retval 0 If the command was successful.
 * @retval -EFAULT If functional mode could not be configured.
 */
static int cmd_lte_off(const struct shell *sh, size_t argc, char **argv)
{
    ARG_UNUSED(argc);
    ARG_UNUSED(argv);

    int err;

    err = lte_lc_power_off();
    if (err) {
        shell_error(sh, "Error: lte_lc_power_off() returned %d", err);
        return err;
    }

    return 0;
}

/**
 * @brief Put LTE modem into normal mode.
 *
 * @param[in] sh         Pointer to the shell instance.
 * @param[in] argc         Number of arguments.
 * @param[in] argv         Arguments.
 *
 * @retval 0 If the command was successful.
 * @retval -EFAULT If functional mode could not be configured.
 */
static int cmd_lte_normal(const struct shell *sh, size_t argc, char **argv)
{
    ARG_UNUSED(argc);
    ARG_UNUSED(argv);

    int err;

    err = lte_lc_normal();
    if (err) {
        shell_error(sh, "Error: lte_lc_normal() returned %d", err);
        return -ENOEXEC;
    }
    return 0;
}

/**
 * @brief Deinitialize LTE library.
 *
 * @param[in] sh        Pointer to the shell instance.
 * @param[in] argc        Number of arguments.
 * @param[in] argv        Arguments.
 *
 * @retval 0 If the command was successful.
 * @retval -EFAULT If the command failed.
 */
static int cmd_lte_deinit(const struct shell *sh, size_t argc, char **argv)
{
    ARG_UNUSED(argc);
    ARG_UNUSED(argv);

    int err;
    err = lte_lc_deinit();
    if (err) {
        shell_error(sh, "Error: lte_lc_deinit() returned %d", err);
        return -EFAULT;
    }

    return 0;
}

static void prv_socket_send(void)
{
    int err;
    char buffer[10] = {"hello"};

    printk("Transmitting UDP payload");

    err = send(client_fd, buffer, sizeof(buffer), 0);
    if (err < 0) {
        printk("Failed to transmit UDP packet, error: %d\n", errno);
    }
}

static int prv_socket_connect(void)
{
    int err;

    struct sockaddr_in *server4 = ((struct sockaddr_in *)&host_addr);

    server4->sin_family = AF_INET;
    server4->sin_port = htons(PORT);
    inet_pton(AF_INET, server_ip, &server4->sin_addr);

    client_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (client_fd < 0) {
        printk("Failed to create UDP socket, error: %d\n", errno);
        err = -errno;
        return err;
    }

    err = connect(client_fd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr_in));
    if (err < 0) {
        printk("Failed to connect socket, error: %d\n", errno);
        close(client_fd);
        return err;
    }

    return 0;
}

/**
 * @brief Send some something with UDP.
 *
 * @param[in] sh        Pointer to the shell instance.
 * @param[in] argc        Number of arguments.
 * @param[in] argv        Arguments.
 *
 * @retval 0 If the command was successful.
 * @retval -EFAULT If the command failed.
 */
static int cmd_lte_send(const struct shell *sh, size_t argc, char **argv)
{
    ARG_UNUSED(argc);
    ARG_UNUSED(argv);

    int err = prv_socket_connect();
    if (err) {
        shell_error(sh, "Error: prv_socket_connect() returned %d", err);
        return -EFAULT;
    }

    prv_socket_send();

    return 0;
}

SHELL_STATIC_SUBCMD_SET_CREATE(
    lte,
    SHELL_CMD_ARG(conn, NULL, "Init and connect to LTE network\n", cmd_lte_init_and_connect, 1,
              0),
    SHELL_CMD_ARG(dis, NULL, "Disconnect from LTE network if connected\n", cmd_lte_offline, 1,
              0),
    SHELL_CMD_ARG(power_off, NULL, "Power off LTE modem\n", cmd_lte_off, 1, 0),
    SHELL_CMD_ARG(normal, NULL, "Power off LTE modem\n", cmd_lte_normal, 1, 0),
    SHELL_CMD_ARG(deinit, NULL, "Power off LTE modem\n", cmd_lte_deinit, 1, 0),
    SHELL_CMD_ARG(send, NULL, "Power off LTE modem\n", cmd_lte_send, 1, 0),
    SHELL_SUBCMD_SET_END);

SHELL_CMD_REGISTER(lte, &lte, "Custom LTE commands", NULL);


WiFi commands:

#include <zephyr/net/socket.h>
#include <zephyr/shell/shell.h>
#include <wifi_driver.h>

static int client_fd;
static struct sockaddr_storage host_addr;

/**
 * @brief Scan for WiFi access points.
 *
 * @param[in] sh         Pointer to the shell instance.
 * @param[in] argc         Number of arguments.
 * @param[in] argv         Arguments.
 * @return
 */
static int cmd_wifi_scan(const struct shell *sh, size_t argc, char **argv)
{
    ARG_UNUSED(argc);
    ARG_UNUSED(argv);

    int err;

    err = wifi_scan();
    if (err) {
        shell_error(sh, "Failed to scan WiFi");
        return -1;
    }
    return 0;
}

/**
 * @brief Connect to WiFi access point.
 *
 * @param[in] sh        Pointer to the shell instance.
 * @param[in] argc         Number of arguments.
 * @param[in] argv         Arguments.
 * @return
 */
static int cmd_wifi_connect(const struct shell *sh, size_t argc, char **argv)
{
    ARG_UNUSED(argc);
    ARG_UNUSED(argv);

    int err;

    err = wifi_connect(WIFI_AP, WIFI_PSK);
    if (err) {
        shell_error(sh, "Failed to connect to WiFi");
        return -1;
    }
    return 0;
}

/**
 * @brief Disconnect from a WiFi access point.
 *
 * @param[in] sh        Pointer to the shell instance.
 * @param[in] argc         Number of arguments.
 * @param[in] argv         Arguments.
 * @return
 */
static int cmd_wifi_disconnect(const struct shell *sh, size_t argc, char **argv)
{
    ARG_UNUSED(argc);
    ARG_UNUSED(argv);

    int err;

    err = wifi_disconnect();
    if (err) {
        shell_error(sh, "Failed to disconnect from WiFi");
        return -1;
    }
    return 0;
}

static void prv_socket_send(void)
{
    int err;
    char buffer[10] = {"hello"};

    printk("Transmitting UDP payload");

    err = send(client_fd, buffer, sizeof(buffer), 0);
    if (err < 0) {
        printk("Failed to transmit UDP packet, error: %d\n", errno);
    }
}

static int prv_socket_connect(void)
{
    int err;

    struct sockaddr_in *server4 = ((struct sockaddr_in *)&host_addr);

    server4->sin_family = AF_INET;
    server4->sin_port = htons(PORT);
    inet_pton(AF_INET, server_ip, &server4->sin_addr);

    client_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (client_fd < 0) {
        printk("Failed to create UDP socket, error: %d\n", errno);
        err = -errno;
        return err;
    }

    err = connect(client_fd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr_in));
    if (err < 0) {
        printk("Failed to connect socket, error: %d\n", errno);
        close(client_fd);
        return err;
    }

    return 0;
}

/**
 * @brief Send some something with UDP.
 *
 * @param[in] sh        Pointer to the shell instance.
 * @param[in] argc        Number of arguments.
 * @param[in] argv        Arguments.
 *
 * @retval 0 If the command was successful.
 * @retval -EFAULT If the command failed.
 */
static int cmd_wifi_send_udp(const struct shell *sh, size_t argc, char **argv)
{
    ARG_UNUSED(argc);
    ARG_UNUSED(argv);

    int err = prv_socket_connect();
    if (err) {
        shell_error(sh, "Error: prv_socket_connect() returned %d", err);
        return -EFAULT;
    }

    prv_socket_send();

    return 0;
}

SHELL_STATIC_SUBCMD_SET_CREATE(wifi,
                   SHELL_CMD_ARG(scan, NULL,
                         "Scan WiFi for access points\n"
                         "usage: scan",
                         cmd_wifi_scan, 1, 0),
                   SHELL_CMD_ARG(conn, NULL,
                         "Connect to WiFi access point (pre-set ssid and psk)\n"
                         "usage: connect",
                         cmd_wifi_connect, 1, 0),
                   SHELL_CMD_ARG(dis, NULL,
                         "Disconnect from WiFi access point if connected\n"
                         "usage: disconnect",
                         cmd_wifi_disconnect, 1, 0),
                   SHELL_CMD_ARG(send, NULL,
                         "Send 'hello' to nodered\n"
                         "usage: send",
                         cmd_wifi_send_udp, 1, 0),
                   SHELL_SUBCMD_SET_END);


Thanks for your help!

Parents Reply Children
No Data
Related