Embedded Systems / Laboratory
LABORATORY 04

Complete IoT Node: from Sensor to Dashboard

Duration: 4 hours Reference: Chapter 11 Platform: ESP32-C6 + Raspberry Pi 5 as a bridge Book reference chapter RO versiunea română

A sensor that measures perfectly but cannot send its data to anyone is useless. In this lab we connect the node from the second lab to a bridge running on a Raspberry Pi, through the MQTT protocol. Then we do something almost always skipped in IoT courses: we listen to our own traffic and see, with our own eyes, exactly what leaks onto the network.

1Objectives of the lab

  • Understanding the publish-subscribe model and why it replaces request-response in IoT
  • Designing a topic hierarchy and mastering the + and # wildcards
  • Installing and configuring an MQTT broker on a Raspberry Pi
  • Publishing from an ESP32 and consuming the data in a bridge written in Python
  • Using the last-will message and retained messages to detect nodes that have gone down
  • Choosing the quality-of-service level based on the energy cost
  • Practically demonstrating the vulnerabilities of a default configuration and securing it

2System architecture

node 1 ESP32-C6 node 2 ESP32-C6 node 3 … MQTT broker Mosquitto on a Raspberry Pi 5 port 1883 / 8883 Python bridge stores in SQLite updates the LCD web dashboard port 8080 eavesdropper security section publishes subscribes
Fig. 1 - The nodes do not know each other and do not know the consumers. Everyone talks only to the broker. New nodes or applications can be added without changing anything on the existing ones - this is the property that makes the model suited to IoT.

3Materials needed

  • 1 Raspberry Pi 5 or Pi 3 (from Lab 1)
  • 2 ESP32-C6 with a sensor (from Lab 2)
  • 1 16×2 I²C LCD
  • 1 Lab Wi-Fi network you have access to
  • 1 Computer for the web dashboard
The golden rule of this lab Section 10 involves intercepting network traffic. This operation is done exclusively on the lab network, on traffic generated by your own devices, with your own broker. Intercepting someone else's traffic, even on the same network, is illegal. The goal is to understand why the default configuration is not acceptable - not to practice on other people's systems.

Wiring diagram

Raspberry Pi 5/3 40-pin header pin 2 - 5V pin 3 - SDA pin 5 - SCL pin 6 - GND LCD 16×2 PCF8574 · 0x27 VCC SDA SCL GND Wi-Fi · MQTT ESP32-C6 DevKitC-1 3V3 GPIO6 · SDA GPIO7 · SCL GND powered over USB BME280 0x76 VCC SDA SCL GND nod1 and nod2: identical wiring
Fig. 2 - The wiring. Left: the bridge - the LCD on the Raspberry Pi's I²C bus, exactly as in Lab 1 (pin 2 = 5 V, 3 = SDA, 5 = SCL, 6 = GND); the 40-pin header is identical on the Pi 5 and the Pi 3. Right: each node is the Lab 2 setup without the INA219 - the BME280 on GPIO6 (SDA) and GPIO7 (SCL), powered from 3V3. The nodes have no wire to the Pi: they communicate only over Wi-Fi, through the broker.
The LCD's pull-ups The PCF8574 adapter pulls SDA and SCL up to 5 V. It usually works on the Pi, but the correct option is an I²C level shifter or removing the adapter's two pull-up resistors - see Lab 1.

4Why MQTT and not HTTP

In the second lab, the node sent its data with an HTTP request. It worked. Why change anything?

HTTPMQTT
Modelrequest-response: the client asks, the server answerspublish-subscribe: the node announces, whoever wants listens
Minimum headerhundreds of bytes2 bytes
Connectionopens and closes with every messagestays open
Server → nodeimpossible without repeated pollingimmediate, through a subscription
Detecting downed nodesmust be built separatelyincluded: the last-will message
Multiple consumerseach must be contacted separatelythe broker replicates on its own
The two bytes really matter A minimal HTTP header, together with the response, is a few hundred bytes. For a node transmitting a ten-byte temperature value, the header is thirty times bigger than the data. Every extra byte means radio transmission time, and transmission time is the most expensive operation in the energy budget calculated in the second lab. MQTT was designed for satellite links on oil pipelines, where every byte was paid for literally.

The broker's role

The node does not send data to someone. It publishes it on a topic, and the broker sends it to everyone subscribed to that topic. The node does not know how many consumers it has - maybe zero, maybe ten. The consumers do not know how many nodes exist. This decoupling is the whole idea: you can add a web dashboard, an alarm and a database without reprogramming a single sensor.

5The topic hierarchy

A topic is a string with levels separated by /, like a file path. A subscription can use wildcards. This is where most misunderstandings are born, so it is worth practicing interactively.

Which subscriptions receive a published message
Try these topics, one at a time
  • lab/si/node1/temperature - the ordinary case
  • lab/si/node1/humidity - watch which subscriptions drop out
  • lab/si/node1 - why does lab/si/# catch it, while lab/si/+/temperature does not?
  • lab/si/node1/sensor/temperature - why doesn't + cover two levels?

How to design a good hierarchy

The rule: from general to specific
lab / si / node1 / temperature
 ↑     ↑      ↑        ↑
place group node  quantity
Place the levels in the order in which you will ever want to filter. With this hierarchy you can ask for "all temperatures" (lab/si/+/temperature) or "everything node 1 says" (lab/si/node1/#). With the reverse order - temperature/node1/si/lab - the second question becomes impossible.
Three frequent mistakes
  • An empty level at the start: /lab/si/node1 starts with an empty level. It is valid, but almost certainly not what you wanted - and lab/si/node1 will not match it.
  • Data inside the topic: lab/si/node1/temperature/23.5 looks clever, but it generates a new topic on every measurement and fills up the broker. The value goes into the message content, not into the topic.
  • Subscribing to # in production: you receive absolutely everything, including the broker's own internal traffic. Useful for debugging, disastrous for a battery-powered node's consumption.

6Installing the broker

  1. Install Mosquitto on the Raspberry Pi
    installation
    sudo apt update
    sudo apt install -y mosquitto mosquitto-clients
    sudo systemctl enable mosquitto
  2. Configure it for the lab

    The default configuration only accepts local connections. We open the port for the network - for now with no security measures at all, precisely so we can see in section 10 what that means.

    create the configuration file
    sudo nano /etc/mosquitto/conf.d/lab.conf
    /etc/mosquitto/conf.d/lab.conf
    # WARNING: insecure configuration, used deliberately for
    # the first part of the lab. Replaced in section 10.
    listener 1883 0.0.0.0
    allow_anonymous true
    
    # Verbose logging, so we can see what is happening.
    log_type all
    log_dest file /var/log/mosquitto/mosquitto.log
    restart and check
    sudo systemctl restart mosquitto
    sudo systemctl status mosquitto --no-pager
  3. Find the broker's address
    network address
    hostname -I

    Note down the address - you will write it into the node's program. Ideally, reserve it a fixed address on the lab router, otherwise it changes on every reboot and the nodes are left orphaned.

  4. Test the broker with itself

    Open two terminal windows. In the first:

    terminal 1 - listening to everything
    mosquitto_sub -h localhost -t 'lab/si/#' -v

    In the second:

    terminal 2 - publishing
    mosquitto_pub -h localhost -t 'lab/si/node1/temperature' -m '23.5'
    mosquitto_pub -h localhost -t 'lab/si/node2/humidity'    -m '61'

    The messages must appear immediately in the first window. If they do, the broker works and you can move on to the node. If not, the problem is local and has nothing to do with the ESP32 - fix it here, it is much easier to debug.

7The publishing node

Install the PubSubClient library from the Arduino library manager, then upload the program below. It is the node from the second lab, with HTTP replaced by MQTT.

node_mqtt.ino
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
#include <esp_sleep.h>

#define uS_PER_S     1000000ULL
#define PERIOD_S     60
#define NODE_ID      "node1"

const char* SSID     = "lab_network";
const char* PASSWORD = "password";
const char* BROKER   = "192.168.1.10";
const uint16_t PORT  = 1883;

// The topics are built once, from the node's identifier.
const char* T_TEMP    = "lab/si/" NODE_ID "/temperature";
const char* T_HUMID   = "lab/si/" NODE_ID "/humidity";
const char* T_BATTERY = "lab/si/" NODE_ID "/battery";
const char* T_STATUS  = "lab/si/" NODE_ID "/status";

RTC_DATA_ATTR uint32_t cycle = 0;

WiFiClient network;
PubSubClient mqtt(network);
Adafruit_BME280 bme;

bool connect_broker() {
  mqtt.setServer(BROKER, PORT);

  // The last-will message: the broker publishes it AUTOMATICALLY if the
  // node disappears without disconnecting politely. It is the only way to
  // tell "the node is quiet because it has nothing to say" apart from
  // "the node has died".
  return mqtt.connect(NODE_ID,          // unique identifier on the network
                      NULL, NULL,       // username and password (none for now)
                      T_STATUS,         // the last-will message's topic
                      1,                // quality of service
                      true,             // retained: whoever subscribes later also sees it
                      "offline");       // the last-will message's content
}

void setup() {
  Serial.begin(115200);
  delay(150);
  cycle++;

  Wire.begin(6, 7);
  if (!bme.begin(0x76)) {
    Serial.println("the sensor is not responding");
  }
  bme.setSampling(Adafruit_BME280::MODE_FORCED,
                  Adafruit_BME280::SAMPLING_X1, Adafruit_BME280::SAMPLING_X1,
                  Adafruit_BME280::SAMPLING_X1, Adafruit_BME280::FILTER_OFF);
  bme.takeForcedMeasurement();
  float temperature = bme.readTemperature();
  float humidity    = bme.readHumidity();

  WiFi.mode(WIFI_STA);
  WiFi.begin(SSID, PASSWORD);
  uint32_t t0 = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - t0 < 10000) delay(10);

  if (WiFi.status() == WL_CONNECTED && connect_broker()) {
    char value[16];

    // RETAINED message: the broker keeps the last value and sends it
    // immediately to anyone who subscribes. A dashboard opened an hour
    // later sees numbers, not a blank screen until the next measurement.
    snprintf(value, sizeof(value), "%.2f", temperature);
    mqtt.publish(T_TEMP, value, true);

    snprintf(value, sizeof(value), "%.1f", humidity);
    mqtt.publish(T_HUMID, value, true);

    snprintf(value, sizeof(value), "%d", (int) (analogRead(0) / 40.95));
    mqtt.publish(T_BATTERY, value, true);

    // Announce that we are alive; replaces the retained last-will message.
    mqtt.publish(T_STATUS, "online", true);

    mqtt.loop();                      // let the library actually send it
    delay(100);
    mqtt.disconnect();                // polite disconnect: does NOT trigger the last will
    Serial.printf("cycle %lu published in %lu ms\n", cycle, millis());
  } else {
    Serial.println("could not publish");
  }

  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
  Serial.flush();
  esp_sleep_enable_timer_wakeup((uint64_t) PERIOD_S * uS_PER_S);
  esp_deep_sleep_start();
}

void loop() {}

Check it right away, from the Raspberry Pi:

listening to the node
mosquitto_sub -h localhost -t 'lab/si/#' -v
The last-will test With the subscription above running, cut the power to the node while it is connected. After the keep-alive timeout expires (by default about 1.5 times the keep-alive interval), the broker publishes lab/si/node1/status → offline on its own. Nobody wrote code for this. Compare that with what implementing the same feature over HTTP would have required.

8The bridge: collecting, storing, displaying

The bridge runs on the Raspberry Pi. It subscribes to everything the nodes publish, saves it to a database, updates the LCD and serves a web dashboard.

setup
source ~/si-lab/bin/activate
pip install paho-mqtt
mkdir -p ~/si-lab/lab04 && cd ~/si-lab/lab04
bridge.py
#!/usr/bin/env python3
"""MQTT bridge: collects the readings, saves them and displays them."""

import json
import sqlite3
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer

import paho.mqtt.client as mqtt

BROKER = "localhost"
PORT = 1883
TOPIC = "lab/si/#"
DATABASE = "readings.db"
WEB_PORT = 8080

# Last value of every topic, for a quick display.
latest = {}
lock = threading.Lock()


def prepare_database():
    con = sqlite3.connect(DATABASE)
    con.execute("""
        CREATE TABLE IF NOT EXISTS readings (
            timestamp REAL,
            node      TEXT,
            quantity  TEXT,
            value     TEXT
        )
    """)
    con.execute("CREATE INDEX IF NOT EXISTS idx_timestamp ON readings(timestamp)")
    con.commit()
    return con


def on_connect(client, userdata, flags, rc, properties=None):
    if rc == 0:
        print("connected to the broker")
        client.subscribe(TOPIC, qos=1)
    else:
        print("connection failed, code", rc)


def on_message(client, userdata, msg):
    text = msg.payload.decode("utf-8", errors="replace")
    levels = msg.topic.split("/")

    # We expect the form lab/si/<node>/<quantity>; ignore the rest.
    if len(levels) != 4:
        return
    _, _, node, quantity = levels

    with lock:
        latest[(node, quantity)] = (text, time.time())

    con = userdata["con"]
    con.execute("INSERT INTO readings VALUES (?,?,?,?)",
                (time.time(), node, quantity, text))
    con.commit()

    tag = " (retained)" if msg.retain else ""
    print("%-34s %s%s" % (msg.topic, text, tag))


def update_lcd(panel):
    """Rotates through the nodes on the LCD, one every 3 seconds."""
    while True:
        with lock:
            nodes = sorted({n for (n, _) in latest})
            snapshot = dict(latest)
        if not nodes:
            panel.write("waiting for nodes...", "")
        for node in nodes:
            t = snapshot.get((node, "temperature"), ("--", 0))[0]
            h = snapshot.get((node, "humidity"), ("--", 0))[0]
            status, moment = snapshot.get((node, "status"), ("?", 0))
            age = int(time.time() - moment) if moment else 999
            panel.write("%-6s %5s C" % (node, t),
                        "%3s%% %s %ds" % (h, status[:3], min(age, 999)))
            time.sleep(3)


PAGE = """<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
<meta http-equiv="refresh" content="5"><title>Nodes</title>
<style>body{font-family:monospace;background:#123526;color:#efede7;padding:24px}
table{border-collapse:collapse}td,th{border:1px solid #2a513d;padding:6px 12px;text-align:left}
th{color:#a0dfba}.old{color:#ff9d80}</style></head><body>
<h2>Active nodes</h2>%s</body></html>"""


class Server(BaseHTTPRequestHandler):
    def do_GET(self):
        with lock:
            snapshot = dict(latest)

        rows = ["<tr><th>node</th><th>quantity</th><th>value</th><th>age</th></tr>"]
        for (node, quantity), (value, moment) in sorted(snapshot.items()):
            age = time.time() - moment
            css_class = ' class="old"' if age > 180 else ""
            rows.append("<tr%s><td>%s</td><td>%s</td><td>%s</td><td>%d s</td></tr>"
                           % (css_class, node, quantity, value, age))

        body = (PAGE % ("<table>" + "".join(rows) + "</table>")).encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, *args):
        pass          # do not flood the console with the web server's log


def main():
    from display import Panel          # reuse the class from Lab 1

    con = prepare_database()
    panel = Panel()

    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                         client_id="bridge", userdata={"con": con})
    client.on_connect = on_connect
    client.on_message = on_message
    client.connect(BROKER, PORT, keepalive=60)

    threading.Thread(target=update_lcd, args=(panel,), daemon=True).start()
    threading.Thread(target=HTTPServer(("", WEB_PORT), Server).serve_forever,
                     daemon=True).start()

    print("bridge active - web dashboard on port %d" % WEB_PORT)
    try:
        client.loop_forever()
    except KeyboardInterrupt:
        panel.close()
        con.close()
        print("\nstopped")


if __name__ == "__main__":
    main()
run it
python3 bridge.py
# then open in a browser  http://<pi-address>:8080
Why SQLite and not a text file The database accepts queries. You can ask directly "what was the average temperature of each node in the last hour" without writing any code:
query
sqlite3 readings.db "SELECT node, ROUND(AVG(CAST(value AS REAL)),2), COUNT(*)
  FROM readings WHERE quantity='temperature' AND timestamp > strftime('%s','now')-3600
  GROUP BY node;"
A CSV file would have required a separate program for every new question.

9Quality of service and its cost

MQTT offers three levels of delivery guarantee. The choice is not free: every extra level means more radio frames, so more transmission time, so more battery consumed.

LevelGuaranteeFrames exchangedRelative energy cost
0 - at most oncenone; the message can disappear1reference
1 - at least oncearrives for sure, but may arrive more than once2roughly double
2 - exactly oncearrives for sure, exactly once4roughly quadruple
How to choose, in practice
  • Level 0 for periodic measurements. If you lose one temperature reading, the next one arrives in a minute. This is the right choice for most battery-powered sensors.
  • Level 1 for commands and events: "the door opened", "turn on the light". It must not be lost, and if it arrives twice that is not a tragedy - provided the operation is idempotent.
  • Level 2 almost never in IoT. Use it only when a repeat would cause real damage: "add 10 lei to the account". It costs four times as much and, usually, the problem is solved more cheaply by putting a unique identifier in the message.

Retained messages

A message published with the "retained" flag stays at the broker. Anyone who subscribes later gets it immediately, without waiting for the next measurement. For a node that wakes up once an hour, the difference is between a working dashboard and a blank screen.

experiment with retained messages
# publish a retained message and exit
mosquitto_pub -h localhost -t 'lab/si/test/value' -m '42' -r

# subscribe ONLY NOW - and we still get the message
mosquitto_sub -h localhost -t 'lab/si/test/value' -v

# deleting a retained message is done by publishing EMPTY content
mosquitto_pub -h localhost -t 'lab/si/test/value' -m '' -r
Retained messages do not expire A decommissioned node leaves its last value at the broker forever, and the dashboard will display it indefinitely as if it were current. That is why the bridge from section 8 marks values older than three minutes with a different color - the age of a message is information just as important as its value.

10What an eavesdropper sees

So far we have run the broker with allow_anonymous true, exactly like most tutorials online. Now we see what that means.

Step 1 - anyone can subscribe to everything

from any computer on the network
mosquitto_sub -h 192.168.1.10 -t '#' -v

No password, no certificate. You see every measurement from every node. And if anything in the system reacted to commands, it could also receive commands:

and can publish anything
mosquitto_pub -h 192.168.1.10 -t 'lab/si/node1/command' -m 'reset'

Step 2 - the traffic is plain text

On the Raspberry Pi, while the nodes are publishing:

listening to our own traffic
sudo tcpdump -i any -A -n port 1883

In the output you will make out, in the clear: the topic names, the measured values, the node identifier - and, had you configured authentication, the username and password, just as readable.

The conclusion to draw Authentication without encryption does not solve much: the password travels in the clear and anyone can read it. The two measures must be taken together. This is exactly the mistake in countless real IoT installations, which stop at adding a password and consider the problem closed.

Step 3 - authentication

creating the users
sudo mosquitto_passwd -c /etc/mosquitto/passwd node1
sudo mosquitto_passwd    /etc/mosquitto/passwd node2
sudo mosquitto_passwd    /etc/mosquitto/passwd bridge

We also add access control: a sensor must be able to publish on its own topics, but has no reason to read the traffic of the entire network.

/etc/mosquitto/acl
# The bridge reads everything, but publishes nothing.
user bridge
topic read lab/si/#

# Each node publishes ONLY on its own branch.
user node1
topic write lab/si/node1/#

user node2
topic write lab/si/node2/#
/etc/mosquitto/conf.d/lab.conf - variant with authentication
listener 1883 0.0.0.0
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/acl
apply and verify
sudo systemctl restart mosquitto

# without credentials - this must now fail
mosquitto_sub -h localhost -t '#' -v

# with credentials - this works
mosquitto_sub -h localhost -u bridge -P bridge_password -t 'lab/si/#' -v

# node 1 tries to publish on node 2's branch - must be rejected
mosquitto_pub -h localhost -u node1 -P node1_password -t 'lab/si/node2/temp' -m '99'

Step 4 - encryption

We generate our own certificate authority and a certificate for the broker. On a closed lab network, this is the correct solution; on a public system a publicly issued certificate would be used instead.

generating the certificates
mkdir -p ~/certs && cd ~/certs

# 1. our own certificate authority
openssl req -new -x509 -days 3650 -extensions v3_ca \
  -keyout ca.key -out ca.crt -nodes -subj "/CN=SI Lab CA"

# 2. the key and request for the broker (CN = the broker's address!)
openssl req -new -nodes -keyout broker.key -out broker.csr \
  -subj "/CN=192.168.1.10"

# 3. signing the request with our own authority
openssl x509 -req -in broker.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out broker.crt -days 3650

sudo cp ca.crt broker.crt broker.key /etc/mosquitto/certs/
sudo chown mosquitto:mosquitto /etc/mosquitto/certs/broker.key
sudo chmod 600 /etc/mosquitto/certs/broker.key
/etc/mosquitto/conf.d/lab.conf - final version
# The insecure port stays closed for good.
listener 8883 0.0.0.0
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/acl

cafile   /etc/mosquitto/certs/ca.crt
certfile /etc/mosquitto/certs/broker.crt
keyfile  /etc/mosquitto/certs/broker.key
tls_version tlsv1.2
checking the encryption
sudo systemctl restart mosquitto

# listen to the traffic again, now on the secure port
sudo tcpdump -i any -A -n port 8883
Compare the two captures Put the tcpdump output from port 1883 side by side with the one from port 8883. In the first you can read the topics and the values; in the second nothing is distinguishable except the packet sizes and their timing. This pair of captures is the most convincing figure you can put in your report.
What remains visible even with encryption An observer can still see when and how often each node communicates, as well as the size of the messages. Surprisingly much can be inferred from the temporal pattern - for example whether a building is occupied. This is called traffic analysis and is a real problem that encryption does not solve.
node_mqtt_secure.ino - the modified part
#include <WiFiClientSecure.h>

// Our own authority's certificate, copied from ca.crt.
// This is how the node verifies it is talking to OUR broker, not an impostor.
const char* CA_CERT = R"EOF(
-----BEGIN CERTIFICATE-----
... the contents of the ca.crt file ...
-----END CERTIFICATE-----
)EOF";

WiFiClientSecure network;
PubSubClient mqtt(network);

void setup() {
  // ... Wi-Fi connection, as before ...

  network.setCACert(CA_CERT);         // do NOT use setInsecure() in a real system
  mqtt.setServer(BROKER, 8883);

  mqtt.connect(NODE_ID, "node1", "node1_password",
               T_STATUS, 1, true, "offline");
  // ... the rest of the program is unchanged ...
}
The energy cost of encryption The TLS handshake adds 200-600 ms to every connection and needs more memory. For a node that wakes up once a minute, this is a significant increase in the active phase. Measure it - it is exactly the kind of trade-off an embedded systems engineer must be able to quantify, not just intuit. The usual solution is not giving up encryption, but transmitting less often and grouping several measurements into a single message.

11Assignments

  1. Install the broker and verify it with mosquitto_pub and mosquitto_sub. Include a screenshot with the two terminals in your report.
  2. Using the widget from section 5, fill in a table with eight published topics and, for each, which of the eight subscriptions receive it. Explain the two cases that surprised you.
  3. Program two nodes with different identifiers and verify that the bridge displays both of them, alternating, on the LCD.
  4. Demonstrate the last-will message working: cut the power to a connected node and show the moment the broker publishes the offline status on its own.
  5. Compare quality levels 0 and 1: measure the duration of the node's active phase in both cases and calculate the autonomy difference with the widget from the second lab.
  6. Capture the traffic on port 1883 and identify the topic and value of a measurement in it. Then switch to the secure port and repeat the capture.
  7. Configure the access-control list and demonstrate that node 1 cannot publish on node 2's branch.

12Deeper-dive challenge

The bridge that watches and decides Extend the bridge so it does not just record, but also reacts:
  • Detecting silent nodes: if a node has not published for three times its usual interval, publish lab/si/alarms/silent_node with its identifier. Infer the interval from the data, without hard-coding it in the program.
  • Detecting impossible values: a temperature that jumps by 20 °C between two consecutive readings almost certainly means a faulty sensor, not a real change. Flag it separately from a high value.
  • Commands in the other direction: subscribe the nodes to lab/si/<node>/command and implement the interval <seconds> command, which changes the sleep period. Careful: the node is asleep and cannot receive commands. How do you solve this? (Hint: a retained message, read on wakeup.)
The last point is the most instructive part. A battery-powered node cannot be contacted - it contacts. Any real IoT system must be designed around this asymmetry, and retained messages are the elegant answer to it.

13Self-check questions

14Resources