Architecture

The sample has a modular structure, where each module has a defined scope of responsibility. The communication between modules is handled by the Zephyr bus (zbus) using messages that are passed over channels. If a module has internal state handling, it is implemented using the Zephyr State Machine Framework. The following figure illustrates the relationship between modules, channels, and network stacks in the sample:

Architecture

Architecture

Definitions and payloads of the channel are owned by the system and placed in a common folder that is included by all modules. See the src/common/channel.c and src/common/channel.h files for more details.

Note

The sample does not include a main.c file. To follow the typical flow of the application, see the Sequence diagram section.

Modules

The Module list tables lists all the modules in the sample together with information on each module’s channel handling and general behavior. A common feature for almost all the modules in the sample is that they each have a dedicated thread. The thread is used to initialize functionality specific to each module, and to process incoming messages in case the module is set up as a subscriber.

Subscribers use its thread primarily to monitor and process incoming messages from other modules by continuously polling on zbus_sub_wait(). When a channel that a module subscribes to, is invoked, the subscriber will handle the incoming message depending on its content. The following code snippet shows how a module thread polls for incoming messages on a subscribed channel:

static void sampler_task(void)
{
    const struct zbus_channel *chan;

    while (!zbus_sub_wait(&sampler, &chan, K_FOREVER)) {

            if (&TRIGGER_CHAN == chan) {
                    sample();
            }
    }
}

K_THREAD_DEFINE(sampler_task_id,
                CONFIG_MQTT_SAMPLE_SAMPLER_THREAD_STACK_SIZE,
                sampler_task, NULL, NULL, NULL, 3, 0, 0);

Note

Zbus implements internal message queues for subscribers. In some cases, depending on the use case, it might be necessary to increase the queue size for a particular subscriber. Especially if the module thread can block for some time. To increase the message queue associated with a subscriber, increase the value of the corresponding Kconfig option, CONFIG_MQTT_SAMPLE_<MODULE_NAME>_MESSAGE_QUEUE_SIZE.

Modules that are setup as listeners have dedicated callbacks that are invoked every time there is a change to an observing channel. The difference between a listener and a subscriber is that listeners do not require a dedicated thread to process incoming messages. The callbacks are called in the context of the thread that published the message. The following code snippet shows how a listener is setup in order to listen to changes to the NETWORK channel:

void led_callback(const struct zbus_channel *chan)
{
    int err = 0;
    const enum network_status *status;

    if (&NETWORK_CHAN == chan) {

            /* Get network status from channel. */
            status = zbus_chan_const_msg(chan);

            switch (*status) {
            case NETWORK_CONNECTED:
                    err = led_on(led_device, LED_1_GREEN);
                    if (err) {
                            LOG_ERR("led_on, error: %d", err);
                    }
                    break;
            case NETWORK_DISCONNECTED:
                    err = led_off(led_device, LED_1_GREEN);
                    if (err) {
                            LOG_ERR("led_off, error: %d", err);
                    }
                    break;
            default:
                    LOG_ERR("Unknown event: %d", *status);
                    break;
            }
    }
}

ZBUS_LISTENER_DEFINE(led, led_callback);

A module publishes a message to a channel by calling the zbus_chan_pub() function. The following code snippet shows how this is typically carried out throughout the sample:

int err;
struct payload payload = "Some payload";

err = zbus_chan_pub(&PAYLOAD_CHAN, &payload, K_SECONDS(1));
if (err) {
    LOG_ERR("zbus_chan_pub, error: %d", err);
}

Module name

Observes channel

Subscriber / Listener

Description

Trigger

None

Sends messages on the trigger channel at an interval set by the CONFIG_MQTT_SAMPLE_TRIGGER_TIMEOUT_SECONDS and upon a button press.

Sampler

Trigger

Subscriber

Samples data every time a message is received on the trigger channel. The sampled payload is sent on the payload channel.

Transport

Network Payload

Subscriber

Handles MQTT connection. Will auto connect and keep the MQTT connection alive as long as the network is available. Receives network status messages on the network channel. Publishes messages received on the payload channel to a configured MQTT topic.

Network

None

Auto connects to either Wi-Fi or LTE after boot, depending on the board and the sample configuration. Sends network status messages on the network channel.

LED

Network

Listener

Listens to changes in the network status received on the network channel. Displays LED pattern accordingly. If network is connected, LED 1 on the board will light up. On Thingy:91, the LED turns green

Error

Fatal error

Listener

Listens to messages sent on the fatal error channel. If a message is received on the fatal error channel, the default behavior is to reboot the device.

Channels

Name

Channel payload

Payload description

Trigger channel

None

Network channel

network status

Enumerator. Signifies if the network is connected or not. Can be either NETWORK_CONNECTED or NETWORK_DISCONNECTED

Payload channel

string

String buffer that contains a message that is sent to the MQTT broker.

Fatal error channel

None

States

Currently, only the sample’s transport module implements state handling.

Transport module

The following figure explains the state transitions of the transport module:

Transport module state transitions

Transport module state transitions

Sequence diagram

The following sequence diagram illustrates the most significant chain of events during normal operation of the sample:

Sequence diagram

Sequence diagram