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.
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.
To add a new zbus event, complete the following procedure:
-
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_ */
-
Define the channel and publish events from the existing
event_callback()function inapp/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 ... }
-
Make sure the channel is observed by the subscriber module. In
app/src/main.c, addvbus_chanto theCHANNEL_LISTso it is gated byCONFIG_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_LISTinstead. -
Implement a handler for the new events in the subscriber module. For example, add the following block to the
running_run()state handler inapp/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; } }
-
Test the implementation by connecting and disconnecting VBUS to verify the LED patterns change as expected.
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.
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.
-
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.dtsifile.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"; };
-
Update the environmental module's state structure in the
app/src/modules/environmental/environmental.cfile 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]; };
-
In the module's thread function
env_module_thread()in the same file, find the initialization of theenvironmental_statestructure 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 */ };
-
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)
-
In the
state_running_run()function in the same file, update the call tosample_sensors():sample_sensors(state_object->bme680, state_object->bmm350);
-
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 ... */ }
-
Update the
environmental_msgstructure inapp/src/modules/environmental/environmental.hto include the magnetic field data:struct environmental_msg { /* ... existing fields ... */ /** Magnetic field measurements (X, Y, Z) in Gauss */ double magnetic_field[3]; };
-
Add cloud integration in the
cloud_environmental_send()function inapp/src/modules/cloud/cloud_environmental.cto 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; }
-
Build and run the modified application.
-
Confirm that the custom messages appear in the Terminal card in nRF Cloud, as shown below:
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.
To add your own module, complete the following steps:
-
Create the module directory structure:
mkdir -p app/src/modules/dummy
-
Create the following files in the
app/src/modules/dummydirectory: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.cfile is added later in the Add shell support section. -
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_ */
-
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);
-
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 -
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 .)
-
Register the module directory in the
app/CMakeLists.txtfile. Useadd_subdirectory_ifdefso the module is only compiled whenCONFIG_APP_DUMMYis enabled, matching the pattern used by the other optional modules:add_subdirectory_ifdef(CONFIG_APP_DUMMY src/modules/dummy)
-
Add the module's Kconfig file to the
app/Kconfigfile:rsource "src/modules/dummy/Kconfig.dummy" -
Increase the value of the
CONFIG_TASK_WDT_CHANNELSKconfig option in theapp/prj.conffile by1to 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.
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.cfile that registers aSHELL_CMD_REGISTERroot command and a zbus listener that prints responses. - A Kconfig option (
CONFIG_APP_<MODULE>_SHELL, defaulty 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'sCMakeLists.txtso the shell file is built only when the option is set.
Apply the recipe to the dummy module as follows:
-
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_requestedflag so it logs only the responses triggered by the most recent shell command, rather than everyDUMMY_SAMPLE_RESPONSEpublished on the channel. -
Add the Kconfig option to
app/src/modules/dummy/Kconfig.dummy, inside the existingif APP_DUMMYblock: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. -
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.
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.
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 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_MQTTCONFIG_APP_CLOUD_MQTT_PROVISION_CREDENTIALSCONFIG_APP_CLOUD_MQTT_HOSTNAMECONFIG_APP_CLOUD_MQTT_CLIENT_IDCONFIG_APP_CLOUD_MQTT_CLIENT_ID_BUFFER_SIZECONFIG_APP_CLOUD_MQTT_TOPIC_SIZE_MAXCONFIG_APP_CLOUD_MQTT_PUB_TOPICCONFIG_APP_CLOUD_MQTT_SUB_TOPICCONFIG_APP_CLOUD_MQTT_SHELLCONFIG_APP_CLOUD_MQTT_PAYLOAD_BUFFER_MAX_SIZECONFIG_APP_CLOUD_MQTT_SHADOW_RESPONSE_BUFFER_MAX_SIZECONFIG_APP_CLOUD_MQTT_BACKOFF_INITIAL_SECONDSCONFIG_APP_CLOUD_MQTT_BACKOFF_TYPE_LINEARCONFIG_APP_CLOUD_MQTT_BACKOFF_TYPE_EXPONENTIALCONFIG_APP_CLOUD_MQTT_BACKOFF_TYPE_NONECONFIG_APP_CLOUD_MQTT_BACKOFF_LINEAR_INCREMENT_SECONDSCONFIG_APP_CLOUD_MQTT_BACKOFF_MAX_SECONDSCONFIG_APP_CLOUD_MQTT_THREAD_STACK_SIZECONFIG_APP_CLOUD_MQTT_MESSAGE_QUEUE_SIZECONFIG_APP_CLOUD_MQTT_WATCHDOG_TIMEOUT_SECONDSCONFIG_APP_CLOUD_MQTT_MSG_PROCESSING_TIMEOUT_SECONDS
-
Build and flash the application with the MQTT overlay.
In the template's
appfolder, run:west build -p -b thingy91x/nrf9151/ns -- -DEXTRA_CONF_FILE="$(pwd)/../examples/modules/cloud/overlay-mqtt.conf" && west flash --erase --skip-rebuild
-
Observe that the device connects to the broker.
-
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
The cloud MQTT module implements an internal state machine to manage the connection and reconnection logic.
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_mqttshell 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.