Skip to content

Latest commit

 

History

History
839 lines (626 loc) · 30.4 KB

File metadata and controls

839 lines (626 loc) · 30.4 KB

Modifying the template

This guide explains how to modify the Asset Tracker Template to fit your use case. It covers common tasks such as adding new zbus events, integrating new sensors, creating your own modules, and replacing the default cloud backend.

Add a new zbus event

This section demonstrates how to add a new event to a module and handle it in another. In this example, you add events to the UART Power Control module to notify the system when VBUS is connected or disconnected on the Thingy:91 X. The main module subscribes to these events and requests specific LED patterns from the LED module in response:

  • When VBUS is connected, the LED blinks white rapidly.
  • When VBUS is disconnected, the LED blinks purple slowly.

Instructions

To add a new zbus event, complete the following procedure:

  1. Create a header for the new channel and message type at app/src/modules/uart_power_control/uart_power_control.h.

    #ifndef UART_POWER_CONTROL_H_
    #define UART_POWER_CONTROL_H_
    
    #include <zephyr/zbus/zbus.h>
    
    enum vbus_msg_type {
        /* VBUS power supply is connected. */
        VBUS_CONNECTED,
    
        /* VBUS power supply is disconnected. */
        VBUS_DISCONNECTED,
    };
    
    struct vbus_msg {
        enum vbus_msg_type type;
    };
    
    ZBUS_CHAN_DECLARE(vbus_chan);
    
    #endif /* UART_POWER_CONTROL_H_ */
  2. Define the channel and publish events from the existing event_callback() function in app/src/modules/uart_power_control/uart_power_control.c. Add the include and channel definition near the top of the file:

    #include "uart_power_control.h"
    #include "app_common.h"
    
    ZBUS_CHAN_DEFINE(vbus_chan,
                     struct vbus_msg,
                     NULL,
                     NULL,
                     ZBUS_OBSERVERS_EMPTY,
                     ZBUS_MSG_INIT(0));

    Then publish the events from inside event_callback():

    if (pins & BIT(NPM13XX_EVENT_VBUS_DETECTED)) {
        LOG_DBG("VBUS detected");
        vbus_present = true;
    
        struct vbus_msg msg = { .type = VBUS_CONNECTED };
        int pub_err = zbus_chan_pub(&vbus_chan, &msg, PUB_TIMEOUT);
    
        if (pub_err) {
            LOG_ERR("zbus_chan_pub, error: %d", pub_err);
            SEND_FATAL_ERROR();
        }
    
        // ... existing code ...
    }
    
    if (pins & BIT(NPM13XX_EVENT_VBUS_REMOVED)) {
        LOG_DBG("VBUS removed");
        vbus_present = false;
    
        struct vbus_msg msg = { .type = VBUS_DISCONNECTED };
        int pub_err = zbus_chan_pub(&vbus_chan, &msg, PUB_TIMEOUT);
    
        if (pub_err) {
            LOG_ERR("zbus_chan_pub, error: %d", pub_err);
            SEND_FATAL_ERROR();
        }
    
        // ... existing code ...
    }
  3. Make sure the channel is observed by the subscriber module. In app/src/main.c, add vbus_chan to the CHANNEL_LIST so it is gated by CONFIG_APP_UART_POWER_CONTROL:

    #include "uart_power_control.h"
    
    #define CHANNEL_LIST(X)                                         \
        X(cloud_chan,       struct cloud_msg)                       \
        X(fota_chan,        struct fota_msg)                        \
        X(network_chan,     struct network_msg)                     \
        X(location_chan,    struct location_msg)                    \
        X(storage_chan,     struct storage_msg)                     \
        X(timer_chan,       struct timer_msg)                       \
        X(priv_main_chan,   struct priv_main_msg)                   \
        IF_ENABLED(CONFIG_APP_BUTTON, (X(button_chan, struct button_msg)))  \
        IF_ENABLED(CONFIG_APP_POWER,  (X(power_chan,  struct power_msg)))   \
        IF_ENABLED(CONFIG_APP_UART_POWER_CONTROL, (X(vbus_chan, struct vbus_msg)))

    If you are subscribing from a different module, add the channel to that module's CHANNEL_LIST instead.

  4. Implement a handler for the new events in the subscriber module. For example, add the following block to the running_run() state handler in app/src/main.c:

    if (state_object->chan == &vbus_chan) {
        const struct vbus_msg *msg =
            (const struct vbus_msg *)state_object->msg_buf;
    
        if (msg->type == VBUS_CONNECTED) {
            LOG_DBG("VBUS connected, request white LED blinking rapidly for 10 seconds");
    
            struct led_msg led_msg = {
                .type = LED_RGB_SET,
                .red = 255,
                .green = 255,
                .blue = 255,
                .duration_on_msec = 300,
                .duration_off_msec = 300,
                .repetitions = 10,
            };
    
            int err = zbus_chan_pub(&led_chan, &led_msg, PUB_TIMEOUT);
    
            if (err) {
                LOG_ERR("zbus_chan_pub, error: %d", err);
                SEND_FATAL_ERROR();
            }
    
            return SMF_EVENT_HANDLED;
        } else if (msg->type == VBUS_DISCONNECTED) {
            LOG_DBG("VBUS disconnected, request purple LED blinking slowly for 10 seconds");
    
            struct led_msg led_msg = {
                .type = LED_RGB_SET,
                .red = 255,
                .green = 0,
                .blue = 255,
                .duration_on_msec = 1000,
                .duration_off_msec = 700,
                .repetitions = 10,
            };
    
            int err = zbus_chan_pub(&led_chan, &led_msg, PUB_TIMEOUT);
    
            if (err) {
                LOG_ERR("zbus_chan_pub, error: %d", err);
                SEND_FATAL_ERROR();
            }
    
            return SMF_EVENT_HANDLED;
        }
    }
  5. Test the implementation by connecting and disconnecting VBUS to verify the LED patterns change as expected.

Add a new environmental sensor

This section demonstrates how to add support for a new sensor to the environmental module. The environmental module is updated to sample data from the sensor through the Zephyr Sensor API, and the data is forwarded to nRF Cloud together with the existing environmental data.

In this example, support for the Bosch BMM350 magnetometer is added. A similar procedure applies to any sensor that supports the Zephyr Sensor API.

The Thingy:91 X is used as the example board, as it is a supported board in the template with board files defined in the nRF Connect SDK.

Instructions

Before adding a new sensor, make sure the sensor's driver is available in Zephyr RTOS and uses the Zephyr Sensor API. Zephyr includes such a driver for the Bosch BMM350 magnetometer.

  1. Add the sensor to the devicetree and enable it. This step:

    • Instantiates a devicetree node for the sensor.
    • Initializes the driver and the sensor during boot.

    In the case of the Bosch BMM350 magnetometer, the device is already added to the devicetree. The node can be found in the nRF Connect SDK in the nrf/boards/nordic/thingy91x/thingy91x_common.dtsi file.

    To enable the sensor, add the following to the Asset Tracker Template's board-specific devicetree overlay file app/boards/thingy91x_nrf9151_ns.overlay:

    &magnetometer {
        status = "okay";
    };
  2. Update the environmental module's state structure in the app/src/modules/environmental/environmental.c file to include the magnetometer device reference and data fields:

    struct environmental_state_object {
        /* ... existing fields ... */
    
        /* BMM350 sensor device reference */
        const struct device *const bmm350;
    
        /* Magnetic field measurements (X, Y, Z) in Gauss */
        double magnetic_field[3];
    };
  3. In the module's thread function env_module_thread() in the same file, find the initialization of the environmental_state structure and add the reference to the device using the devicetree label:

    struct environmental_state_object environmental_state = {
        .bme680 = DEVICE_DT_GET(DT_NODELABEL(bme680)),
        .bmm350 = DEVICE_DT_GET(DT_NODELABEL(magnetometer)), /* Add this line */
    };
  4. In the same file, update the sensor sampling function signature to include the magnetometer device:

    static void sample_sensors(const struct device *const bme680,
                               const struct device *const bmm350)
  5. In the state_running_run() function in the same file, update the call to sample_sensors():

    sample_sensors(state_object->bme680, state_object->bmm350);
  6. Update the sample_sensors() function in the same file to sample from the new sensor and add the data to the outgoing message (the message struct is extended in the next step):

    static void sample_sensors(const struct device *const bme680,
                               const struct device *const bmm350)
    {
        /* ... existing code, calls to sensor_sample_fetch() and sensor_channel_get() ... */
    
        struct sensor_value magnetic_field[3] = { {0}, {0}, {0} };
    
        err = sensor_sample_fetch(bmm350);
        if (err) {
            LOG_ERR("Failed to fetch magnetometer sample: %d", err);
            SEND_FATAL_ERROR();
            return;
        }
    
        err = sensor_channel_get(bmm350, SENSOR_CHAN_MAGN_XYZ, magnetic_field);
        if (err) {
            LOG_ERR("Failed to get magnetometer data: %d", err);
            SEND_FATAL_ERROR();
            return;
        }
    
        LOG_DBG("Magnetic field: X: %.2f G, Y: %.2f G, Z: %.2f G",
                sensor_value_to_double(&magnetic_field[0]),
                sensor_value_to_double(&magnetic_field[1]),
                sensor_value_to_double(&magnetic_field[2]));
    
        struct environmental_msg msg = {
            /* ... existing fields ... */
            .magnetic_field[0] = sensor_value_to_double(&magnetic_field[0]),
            .magnetic_field[1] = sensor_value_to_double(&magnetic_field[1]),
            .magnetic_field[2] = sensor_value_to_double(&magnetic_field[2]),
        };
    
        /* ... existing code to timestamp and publish the message ... */
    }
  7. Update the environmental_msg structure in app/src/modules/environmental/environmental.h to include the magnetic field data:

    struct environmental_msg {
        /* ... existing fields ... */
    
        /** Magnetic field measurements (X, Y, Z) in Gauss */
        double magnetic_field[3];
    };
  8. Add cloud integration in the cloud_environmental_send() function in app/src/modules/cloud/cloud_environmental.c to send magnetometer data to nRF Cloud. There is no predefined application ID for magnetometer data, so the following code uses a custom "MAGNETIC_FIELD" ID. Update the function as follows:

    int cloud_environmental_send(const struct environmental_msg *env,
                                 int64_t timestamp_ms,
                                 bool confirmable)
    {
        int err;
    
        /* ... existing code to send temperature, pressure and humidity ... */
    
        char mag_message[64];
    
        /* Format magnetometer data as a string with three values */
        snprintk(mag_message, sizeof(mag_message),
                 "%.2f %.2f %.2f",
                 env->magnetic_field[0],
                 env->magnetic_field[1],
                 env->magnetic_field[2]);
    
        err = nrf_cloud_coap_message_send("MAGNETIC_FIELD",
                                          mag_message,
                                          false,
                                          timestamp_ms,
                                          confirmable);
        if (err) {
            LOG_ERR("Failed to send magnetometer data to cloud, error: %d", err);
            return err;
        }
    
        LOG_DBG("Magnetometer data sent to cloud: %s", mag_message);
    
        return 0;
    }
  9. Build and run the modified application.

  10. Confirm that the custom messages appear in the Terminal card in nRF Cloud, as shown below:

    nRF Cloud magnetometer messages

Add your own module

This section walks through creating a minimal "dummy" module. The dummy module illustrates the template's module architecture and can be used as a starting point for your own modules.

Instructions

To add your own module, complete the following steps:

  1. Create the module directory structure:

    mkdir -p app/src/modules/dummy
  2. Create the following files in the app/src/modules/dummy directory:

    • app/src/modules/dummy/dummy.h - Module interface definitions.
    • app/src/modules/dummy/dummy.c - Module implementation.
    • app/src/modules/dummy/Kconfig.dummy - Module configuration options.
    • app/src/modules/dummy/CMakeLists.txt - Build system configuration.

    An optional dummy_shell.c file is added later in the Add shell support section.

  3. In app/src/modules/dummy/dummy.h, define the module's interface:

    #ifndef _DUMMY_H_
    #define _DUMMY_H_
    
    #include <zephyr/kernel.h>
    #include <zephyr/zbus/zbus.h>
    
    #ifdef __cplusplus
    extern "C" {
    #endif
    
    /* Module's zbus channel */
    ZBUS_CHAN_DECLARE(dummy_chan);
    
    /* Module message types */
    enum dummy_msg_type {
        /* Output message types */
        DUMMY_SAMPLE_RESPONSE = 0x1,
    
        /* Input message types */
        DUMMY_SAMPLE_REQUEST,
    };
    
    /* Module message structure */
    struct dummy_msg {
        enum dummy_msg_type type;
        int32_t value;
    };
    
    #ifdef __cplusplus
    }
    #endif
    
    #endif /* _DUMMY_H_ */
  4. In app/src/modules/dummy/dummy.c, implement the module's functionality:

    #include <zephyr/kernel.h>
    #include <zephyr/logging/log.h>
    #include <zephyr/zbus/zbus.h>
    #include <zephyr/task_wdt/task_wdt.h>
    #include <zephyr/smf.h>
    
    #include "app_common.h"
    #include "dummy.h"
    
    /* Register log module */
    LOG_MODULE_REGISTER(dummy_module, CONFIG_APP_DUMMY_LOG_LEVEL);
    
    /* Define module's zbus channel */
    ZBUS_CHAN_DEFINE(dummy_chan,
                     struct dummy_msg,
                     NULL,
                     NULL,
                     ZBUS_OBSERVERS_EMPTY,
                     ZBUS_MSG_INIT(0)
    );
    
    /* Register zbus subscriber */
    ZBUS_MSG_SUBSCRIBER_DEFINE(dummy);
    
    /* Add subscriber to channel */
    ZBUS_CHAN_ADD_OBS(dummy_chan, dummy, 0);
    
    #define MAX_MSG_SIZE sizeof(struct dummy_msg)
    
    BUILD_ASSERT(CONFIG_APP_DUMMY_WATCHDOG_TIMEOUT_SECONDS >
                 CONFIG_APP_DUMMY_MSG_PROCESSING_TIMEOUT_SECONDS,
                 "Watchdog timeout must be greater than maximum message processing time");
    
    /* State machine states */
    enum dummy_module_state {
        STATE_RUNNING,
    };
    
    /* Module state structure */
    struct dummy_state {
        /* State machine context (must be first) */
        struct smf_ctx ctx;
    
        /* Last received zbus channel */
        const struct zbus_channel *chan;
    
        /* Message buffer */
        uint8_t msg_buf[MAX_MSG_SIZE];
    
        /* Current counter value */
        int32_t current_value;
    };
    
    /* Forward declarations */
    static enum smf_state_result state_running_run(void *o);
    
    /* State machine definition */
    static const struct smf_state states[] = {
        [STATE_RUNNING] = SMF_CREATE_STATE(NULL, state_running_run, NULL, NULL, NULL),
    };
    
    /* Watchdog callback */
    static void task_wdt_callback(int channel_id, void *user_data)
    {
        LOG_ERR("Watchdog expired, Channel: %d, Thread: %s",
                channel_id, k_thread_name_get((k_tid_t)user_data));
    
        SEND_FATAL_ERROR_WATCHDOG_TIMEOUT();
    }
    
    /* State machine handlers */
    static enum smf_state_result state_running_run(void *obj)
    {
        struct dummy_state *state_object = obj;
    
        if (&dummy_chan == state_object->chan) {
            const struct dummy_msg *msg =
                (const struct dummy_msg *)state_object->msg_buf;
    
            if (msg->type == DUMMY_SAMPLE_REQUEST) {
                LOG_DBG("Received sample request");
                state_object->current_value++;
    
                struct dummy_msg response = {
                    .type = DUMMY_SAMPLE_RESPONSE,
                    .value = state_object->current_value,
                };
    
                int err = zbus_chan_pub(&dummy_chan, &response, PUB_TIMEOUT);
                if (err) {
                    LOG_ERR("Failed to publish response: %d", err);
                    SEND_FATAL_ERROR();
                    return SMF_EVENT_HANDLED;
                }
    
                return SMF_EVENT_HANDLED;
            }
        }
    
        return SMF_EVENT_PROPAGATE;
    }
    
    /* Module task function */
    static void dummy_task(void)
    {
        int err;
        int task_wdt_id;
        const uint32_t wdt_timeout_ms =
            (CONFIG_APP_DUMMY_WATCHDOG_TIMEOUT_SECONDS * MSEC_PER_SEC);
        const uint32_t execution_time_ms =
            (CONFIG_APP_DUMMY_MSG_PROCESSING_TIMEOUT_SECONDS * MSEC_PER_SEC);
        const k_timeout_t zbus_wait_ms = K_MSEC(wdt_timeout_ms - execution_time_ms);
        struct dummy_state dummy_state = {
            .current_value = 0
        };
    
        LOG_DBG("Starting dummy module task");
    
        task_wdt_id = task_wdt_add(wdt_timeout_ms, task_wdt_callback, (void *)k_current_get());
    
        smf_set_initial(SMF_CTX(&dummy_state), &states[STATE_RUNNING]);
    
        while (true) {
            err = task_wdt_feed(task_wdt_id);
            if (err) {
                LOG_ERR("Failed to feed watchdog: %d", err);
                SEND_FATAL_ERROR();
                return;
            }
    
            err = zbus_sub_wait_msg(&dummy,
                                   &dummy_state.chan,
                                   dummy_state.msg_buf,
                                   zbus_wait_ms);
            if (err == -ENOMSG) {
                continue;
            } else if (err) {
                LOG_ERR("Failed to wait for message: %d", err);
                SEND_FATAL_ERROR();
                return;
            }
    
            err = smf_run_state(SMF_CTX(&dummy_state));
            if (err) {
                LOG_ERR("Failed to run state machine: %d", err);
                SEND_FATAL_ERROR();
                return;
            }
        }
    }
    
    /* Define module thread */
    K_THREAD_DEFINE(dummy_task_id,
                    CONFIG_APP_DUMMY_THREAD_STACK_SIZE,
                    dummy_task, NULL, NULL, NULL,
                    K_LOWEST_APPLICATION_THREAD_PRIO, 0, 0);
  5. In app/src/modules/dummy/Kconfig.dummy, define module configuration options:

    menuconfig APP_DUMMY
        bool "Dummy module"
        default y
        help
            Enable the dummy module.
    
    if APP_DUMMY
    
    config APP_DUMMY_THREAD_STACK_SIZE
        int "Dummy module thread stack size"
        default 2048
        help
            Stack size for the dummy module thread.
    
    config APP_DUMMY_WATCHDOG_TIMEOUT_SECONDS
        int "Dummy module watchdog timeout in seconds"
        default 30
        help
            Watchdog timeout for the dummy module.
    
    config APP_DUMMY_MSG_PROCESSING_TIMEOUT_SECONDS
        int "Dummy module message processing timeout in seconds"
        default 5
        help
            Maximum time allowed for processing a single message in the dummy module.
    
    module = APP_DUMMY
    module-str = DUMMY
    source "subsys/logging/Kconfig.template.log_config"
    
    endif # APP_DUMMY
    
  6. In app/src/modules/dummy/CMakeLists.txt, configure the build system to include the source files of the module:

    target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/dummy.c)
    target_include_directories(app PRIVATE .)
  7. Register the module directory in the app/CMakeLists.txt file. Use add_subdirectory_ifdef so the module is only compiled when CONFIG_APP_DUMMY is enabled, matching the pattern used by the other optional modules:

    add_subdirectory_ifdef(CONFIG_APP_DUMMY src/modules/dummy)
  8. Add the module's Kconfig file to the app/Kconfig file:

    rsource "src/modules/dummy/Kconfig.dummy"
    
  9. Increase the value of the CONFIG_TASK_WDT_CHANNELS Kconfig option in the app/prj.conf file by 1 to accommodate for the new module's task watchdog integration.

The dummy module is now ready to use. It provides the following functionality:

  • Initializes with a counter value of 0.
  • Increments the counter on each sample request.
  • Responds with the current counter value over zbus.
  • Includes error handling and watchdog support.
  • Follows the state machine pattern used by the other modules.

To trigger the module from C code, publish a DUMMY_SAMPLE_REQUEST to dummy_chan:

struct dummy_msg req = { .type = DUMMY_SAMPLE_REQUEST };

err = zbus_chan_pub(&dummy_chan, &req, PUB_TIMEOUT);
if (err) {
    LOG_ERR("Failed to request dummy sample: %d", err);
}

For an interactive way to drive the module during development, add a shell command as described in the next section.

Add shell support

A shell command provides an easy way to publish requests and inspect responses without rebuilding or reflashing. The template uses this pattern in several modules (for example power_shell.c, network_shell.c, cloud_shell.c) and you can reuse it for your own modules.

The recipe has three parts:

  • A <module>_shell.c file that registers a SHELL_CMD_REGISTER root command and a zbus listener that prints responses.
  • A Kconfig option (CONFIG_APP_<MODULE>_SHELL, default y if SHELL) that gates the shell file so it is compiled only when the shell subsystem is enabled.
  • A target_sources_ifdef() line in the module's CMakeLists.txt so the shell file is built only when the option is set.

Apply the recipe to the dummy module as follows:

  1. Create app/src/modules/dummy/dummy_shell.c:

    #include <zephyr/shell/shell.h>
    #include <zephyr/zbus/zbus.h>
    #include <zephyr/kernel.h>
    #include <zephyr/logging/log.h>
    #include <errno.h>
    
    #include "app_common.h"
    #include "dummy.h"
    
    LOG_MODULE_DECLARE(dummy_module, CONFIG_APP_DUMMY_LOG_LEVEL);
    
    static bool sample_requested;
    
    static void dummy_shell_listener_callback(const struct zbus_channel *chan)
    {
        if (!sample_requested) {
            return;
        }
    
        const struct dummy_msg *msg = zbus_chan_const_msg(chan);
    
        if (msg->type == DUMMY_SAMPLE_RESPONSE) {
            LOG_INF("Dummy sample response: %d", msg->value);
            sample_requested = false;
        }
    }
    
    ZBUS_LISTENER_DEFINE(dummy_shell_listener, dummy_shell_listener_callback);
    ZBUS_CHAN_ADD_OBS(dummy_chan, dummy_shell_listener, 0);
    
    static int cmd_dummy_sample(const struct shell *sh, size_t argc, char **argv)
    {
        ARG_UNUSED(argc);
        ARG_UNUSED(argv);
    
        int err;
        struct dummy_msg msg = {
            .type = DUMMY_SAMPLE_REQUEST,
        };
    
        err = zbus_chan_pub(&dummy_chan, &msg, PUB_TIMEOUT);
        if (err) {
            shell_print(sh, "Failed to send request: %d", err);
            return err;
        }
    
        sample_requested = true;
        shell_print(sh, "Requesting dummy sample...");
        return 0;
    }
    
    SHELL_STATIC_SUBCMD_SET_CREATE(
        sub_cmds,
        SHELL_CMD(sample,
                  NULL,
                  "Request a dummy sample (publishes DUMMY_SAMPLE_REQUEST and prints the response)",
                  cmd_dummy_sample),
        SHELL_SUBCMD_SET_END);
    
    SHELL_CMD_REGISTER(att_dummy,
                       &sub_cmds,
                       "Asset Tracker Template Dummy module commands",
                       NULL);

    The listener checks a local sample_requested flag so it logs only the responses triggered by the most recent shell command, rather than every DUMMY_SAMPLE_RESPONSE published on the channel.

  2. Add the Kconfig option to app/src/modules/dummy/Kconfig.dummy, inside the existing if APP_DUMMY block:

    config APP_DUMMY_SHELL
        bool "Dummy module shell commands"
        default y if SHELL
        help
            Enable shell commands for the dummy module. Adds the att_dummy sample
            command, which publishes a DUMMY_SAMPLE_REQUEST and logs the response.
    
  3. Append the conditional source to app/src/modules/dummy/CMakeLists.txt:

    target_sources_ifdef(CONFIG_APP_DUMMY_SHELL app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/dummy_shell.c)

To apply the same pattern to your own module, replace dummy with your module name in the file, Kconfig symbol, CMake option, channel, and message type. The root command (att_dummy here) is conventionally att_<module> so all template commands share a common prefix.

Test the module using the shell

Build and flash the application, then open a serial terminal at 115200 baud and request a sample:

uart:~$ att_dummy sample
Requesting dummy sample...
[00:00:43.080,932] <inf> dummy_module: Dummy sample response: 1
uart:~$ att_dummy sample
Requesting dummy sample...
[00:05:40.587,219] <inf> dummy_module: Dummy sample response: 2
uart:~$

Each invocation publishes a DUMMY_SAMPLE_REQUEST to dummy_chan. The module's state machine handles it, increments its internal counter, and publishes a DUMMY_SAMPLE_RESPONSE. The shell's listener prints the counter value, confirming the round trip works end to end.

You can extend this dummy module by adding new message types, state variables, and processing logic to fit your specific use case.

Enable support for MQTT

To connect to a generic MQTT server using the Asset Tracker Template, you can use the example cloud module provided under examples/modules/cloud. This module replaces the default nRF Cloud CoAP cloud integration with a flexible MQTT client implementation.

  • MQTT module default configuration:

    • Broker hostname: mqtt.nordicsemi.academy
    • Device/Client ID: IMEI (International Mobile Equipment Identity)
    • Port: 8883
    • TLS: Yes
    • Authentication: Server only
    • CA certificate: examples/modules/cloud/creds/mqtt.nordicsemi.academy.pem
    • Publish topic: <IMEI>/att-pub-topic
    • Subscribe topic: <IMEI>/att-sub-topic

Configuration

Configuration for the MQTT stack is set through the examples/modules/cloud/overlay-mqtt.conf file and the Kconfig options defined in examples/modules/cloud/Kconfig.cloud_mqtt. The following are the main options for controlling the MQTT module:

  • CONFIG_APP_CLOUD_MQTT
  • CONFIG_APP_CLOUD_MQTT_PROVISION_CREDENTIALS
  • CONFIG_APP_CLOUD_MQTT_HOSTNAME
  • CONFIG_APP_CLOUD_MQTT_CLIENT_ID
  • CONFIG_APP_CLOUD_MQTT_CLIENT_ID_BUFFER_SIZE
  • CONFIG_APP_CLOUD_MQTT_TOPIC_SIZE_MAX
  • CONFIG_APP_CLOUD_MQTT_PUB_TOPIC
  • CONFIG_APP_CLOUD_MQTT_SUB_TOPIC
  • CONFIG_APP_CLOUD_MQTT_SHELL
  • CONFIG_APP_CLOUD_MQTT_PAYLOAD_BUFFER_MAX_SIZE
  • CONFIG_APP_CLOUD_MQTT_SHADOW_RESPONSE_BUFFER_MAX_SIZE
  • CONFIG_APP_CLOUD_MQTT_BACKOFF_INITIAL_SECONDS
  • CONFIG_APP_CLOUD_MQTT_BACKOFF_TYPE_LINEAR
  • CONFIG_APP_CLOUD_MQTT_BACKOFF_TYPE_EXPONENTIAL
  • CONFIG_APP_CLOUD_MQTT_BACKOFF_TYPE_NONE
  • CONFIG_APP_CLOUD_MQTT_BACKOFF_LINEAR_INCREMENT_SECONDS
  • CONFIG_APP_CLOUD_MQTT_BACKOFF_MAX_SECONDS
  • CONFIG_APP_CLOUD_MQTT_THREAD_STACK_SIZE
  • CONFIG_APP_CLOUD_MQTT_MESSAGE_QUEUE_SIZE
  • CONFIG_APP_CLOUD_MQTT_WATCHDOG_TIMEOUT_SECONDS
  • CONFIG_APP_CLOUD_MQTT_MSG_PROCESSING_TIMEOUT_SECONDS

How to use the MQTT cloud example

  1. Build and flash the application with the MQTT overlay.

    In the template's app folder, run:

    west build -p -b thingy91x/nrf9151/ns -- -DEXTRA_CONF_FILE="$(pwd)/../examples/modules/cloud/overlay-mqtt.conf" && west flash --erase --skip-rebuild
  2. Observe that the device connects to the broker.

  3. Test using shell commands:

    uart:~$ att_cloud_publish_mqtt test-payload
    Sending on payload channel: "data":"test-payload","ts":1746534066186 (40 bytes)
    [00:00:18.607,421] <dbg> cloud: on_cloud_payload_json: MQTT Publish Details:
    [00:00:18.607,482] <dbg> cloud: on_cloud_payload_json:  -Payload: "data":"test-payload","ts":1746534066186
    [00:00:18.607,513] <dbg> cloud: on_cloud_payload_json:  -Payload Length: 40
    [00:00:18.607,543] <dbg> cloud: on_cloud_payload_json:  -Topic: 359404230261381/att-pub-topic
    [00:00:18.607,574] <dbg> cloud: on_cloud_payload_json:  -Topic Size: 29
    [00:00:18.607,635] <dbg> cloud: on_cloud_payload_json:  -QoS: 1
    [00:00:18.607,635] <dbg> cloud: on_cloud_payload_json:  -Message ID: 1
    [00:00:18.607,696] <dbg> mqtt_helper: mqtt_helper_publish: Publishing to topic: 359404230261381/att-pub-topic
    [00:00:19.141,235] <dbg> mqtt_helper: mqtt_evt_handler: MQTT_EVT_PUBACK: id = 1 result = 0
    [00:00:19.141,265] <dbg> cloud: on_mqtt_puback: Publish acknowledgment received, message id: 1
    [00:00:19.141,296] <dbg> mqtt_helper: mqtt_helper_poll_loop: Polling on socket fd: 0
    [00:00:48.653,503] <dbg> mqtt_helper: mqtt_helper_poll_loop: Polling on socket fd: 0
    [00:00:49.587,463] <dbg> mqtt_helper: mqtt_evt_handler: MQTT_EVT_PINGRESP
    [00:00:49.587,493] <dbg> mqtt_helper: mqtt_helper_poll_loop: Polling on socket fd: 0
    [00:01:18.697,692] <dbg> mqtt_helper: mqtt_helper_poll_loop: Polling on socket fd: 0
    [00:01:19.350,921] <dbg> mqtt_helper: mqtt_evt_handler: MQTT_EVT_PINGRESP

Module state machine

The cloud MQTT module implements an internal state machine to manage the connection and reconnection logic.

Cloud MQTT state diagram

Limitations

The MQTT cloud module is intended as a demonstration of how to replace the template's default nRF Cloud CoAP cloud module with an MQTT-based implementation. It is not a fully featured solution and has the following limitations:

  • Sensor and location support: The MQTT module does not encode or send sensor or location data to the broker. You can send test payloads using the att_cloud_publish_mqtt shell command.

  • FOTA support: The MQTT module does not support firmware over-the-air (FOTA) updates, as these rely on nRF Cloud CoAP functionality, which is a dependency of the FOTA module.

  • Stub channel for FOTA: To prevent build errors, the MQTT module includes a placeholder (stub) channel declaration for FOTA. If your application needs FOTA, implement a custom module tailored to your chosen cloud and FOTA service.

For production use, we recommend the default nRF Cloud CoAP cloud module, which provides comprehensive support for FOTA and other advanced features.