ESP32 and Arduino

Arduino Nano 33 IoT MQTT with WiFiNINA: Stream IMU Data over SSL

Publish accelerometer and gyroscope readings from the Arduino Nano 33 IoT to an MQTT broker using WiFiNINA, WiFiSSLClient and ArduinoMqttClient, then harden it with credentials and a Last Will.

Arduino Nano 33 IoT board streaming motion sensor data in an Arduino Nano 33 IoT MQTT setup over SSL
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

The Nano 33 IoT packs Wi-Fi, a 6-axis IMU and a secure element into a tiny board, which makes Arduino Nano 33 IoT MQTT projects ideal for motion monitoring. In this guide you will stream accelerometer and gyroscope data over an encrypted SSL connection using WiFiNINA and ArduinoMqttClient. You will also add a Last Will, keep the message rate within broker limits and fix the certificate errors people hit most often.

The Nano 33 IoT Architecture in One Minute

The board combines three chips that matter for this project:

Component Role in an MQTT project
SAMD21 (Arm Cortex-M0+) Runs your sketch
u-blox NINA-W102 Wi-Fi coprocessor; handles TCP and the SSL/TLS handshake
LSM6DS3 3-axis accelerometer and 3-axis gyroscope
ATECC608A Crypto element, useful for advanced certificate-based authentication

Your sketch talks to the NINA module through the WiFiNINA library. The NINA firmware stores a list of root certificates, so WiFiSSLClient can verify most well-known certificate authorities without code changes. That is the main reason this board handles SSL connections more easily than an ESP8266: the heavy lifting happens on the coprocessor.

Prerequisites

  1. Arduino IDE 2.x with Arduino SAMD Boards (32-bits ARM Cortex-M0+) installed from the Boards Manager.
  2. Libraries from the Library Manager: WiFiNINA, ArduinoMqttClient and Arduino_LSM6DS3.
  3. A 2.4 GHz Wi-Fi network.
  4. Mosquitto command-line tools on your computer for testing.
  5. Up-to-date NINA firmware. Upload File → Examples → WiFiNINA → Tools → CheckWiFiNINAFirmware and update with Tools → WiFi101 / WiFiNINA Firmware Updater if needed. Old firmware is the top cause of SSL failures.

Step 1: Read the IMU

Before adding networking, confirm the sensor works:

#include <Arduino_LSM6DS3.h>

void setup() {
  Serial.begin(115200);
  while (!Serial) {}
  if (!IMU.begin()) {
    Serial.println("IMU not found");
    while (true) {}
  }
  Serial.print("Accelerometer sample rate: ");
  Serial.println(IMU.accelerationSampleRate());
}

void loop() {
  float x, y, z;
  if (IMU.accelerationAvailable()) {
    IMU.readAcceleration(x, y, z);
    Serial.print(x); Serial.print('\t');
    Serial.print(y); Serial.print('\t');
    Serial.println(z);
  }
  delay(200);
}

Lying flat on the desk, z should be close to 1.0 (one g). Tilt the board and watch the values move.

Step 2: Arduino Nano 33 IoT MQTT over SSL

You will connect to broker.connectmq.com on port 8883. The broker certificate is issued by Let's Encrypt, whose root is included in current NINA firmware, so no certificate needs to be pasted into the sketch. For the very first test you can use the free public MQTT broker anonymously; it requires only a unique, non-empty client ID. Remember that public topics are readable by anyone.

Create arduino_secrets.h:

#define SECRET_SSID "YOUR_WIFI"
#define SECRET_PASS "YOUR_WIFI_PASSWORD"
// Leave empty for the public broker, fill in for your private ConnectMQ broker
#define SECRET_MQTT_CLIENT_ID "nano33-c7e2a4"
#define SECRET_MQTT_USER ""
#define SECRET_MQTT_PASS ""

The main sketch publishes a compact JSON message with acceleration and rotation five times per second, which stays well within the public broker's limit of 10 messages per second per client:

#include <WiFiNINA.h>
#include <ArduinoMqttClient.h>
#include <Arduino_LSM6DS3.h>
#include "arduino_secrets.h"

const char broker[] = "broker.connectmq.com";
const int  port     = 8883;

const char topicImu[]    = "connectmq/demo/nano33-c7e2a4/imu";
const char topicStatus[] = "connectmq/demo/nano33-c7e2a4/status";
const char topicRate[]   = "connectmq/demo/nano33-c7e2a4/rate/set";

WiFiSSLClient sslClient;
MqttClient mqttClient(sslClient);

unsigned long publishIntervalMs = 200;   // 5 messages per second
unsigned long lastPublish = 0;

void connectWifi() {
  if (WiFi.status() == WL_NO_MODULE) {
    Serial.println("WiFi module not found");
    while (true) {}
  }
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print("Connecting to ");
    Serial.println(SECRET_SSID);
    WiFi.begin(SECRET_SSID, SECRET_PASS);
    delay(5000);
  }
  Serial.print("IP: ");
  Serial.println(WiFi.localIP());
}

void onMqttMessage(int size) {
  char buf[16];
  int i = 0;
  while (mqttClient.available() && i < (int)sizeof(buf) - 1) {
    buf[i++] = (char)mqttClient.read();
  }
  buf[i] = '\0';
  unsigned long ms = strtoul(buf, nullptr, 10);
  if (ms >= 100 && ms <= 60000) {          // never faster than 10 msg/s
    publishIntervalMs = ms;
    Serial.print("New interval: ");
    Serial.println(publishIntervalMs);
  }
}

bool connectMqtt() {
  mqttClient.setId(SECRET_MQTT_CLIENT_ID);
  if (strlen(SECRET_MQTT_USER) > 0) {
    mqttClient.setUsernamePassword(SECRET_MQTT_USER, SECRET_MQTT_PASS);
  }
  mqttClient.setKeepAliveInterval(30 * 1000L);
  mqttClient.setConnectionTimeout(10 * 1000L);

  mqttClient.beginWill(topicStatus, true, 1);
  mqttClient.print("offline");
  mqttClient.endWill();

  Serial.print("MQTT over SSL... ");
  if (!mqttClient.connect(broker, port)) {
    Serial.print("failed, error ");
    Serial.println(mqttClient.connectError());
    return false;
  }
  Serial.println("connected");

  mqttClient.onMessage(onMqttMessage);
  mqttClient.subscribe(topicRate, 1);

  mqttClient.beginMessage(topicStatus, true, 1);
  mqttClient.print("online");
  mqttClient.endMessage();
  return true;
}

void setup() {
  Serial.begin(115200);
  unsigned long start = millis();
  while (!Serial && millis() - start < 3000) {}

  if (!IMU.begin()) {
    Serial.println("IMU init failed");
    while (true) {}
  }
  connectWifi();
  while (!connectMqtt()) delay(5000);
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) connectWifi();
  if (!mqttClient.connected()) {
    delay(3000);
    connectMqtt();
    return;
  }
  mqttClient.poll();

  if (millis() - lastPublish >= publishIntervalMs &&
      IMU.accelerationAvailable() && IMU.gyroscopeAvailable()) {
    lastPublish = millis();
    float ax, ay, az, gx, gy, gz;
    IMU.readAcceleration(ax, ay, az);   // g
    IMU.readGyroscope(gx, gy, gz);      // degrees per second

    // Stream the JSON directly; print(float, digits) avoids printf float support issues
    mqttClient.beginMessage(topicImu);
    mqttClient.print("{\"ax\":"); mqttClient.print(ax, 3);
    mqttClient.print(",\"ay\":"); mqttClient.print(ay, 3);
    mqttClient.print(",\"az\":"); mqttClient.print(az, 3);
    mqttClient.print(",\"gx\":"); mqttClient.print(gx, 1);
    mqttClient.print(",\"gy\":"); mqttClient.print(gy, 1);
    mqttClient.print(",\"gz\":"); mqttClient.print(gz, 1);
    mqttClient.print("}");
    mqttClient.endMessage();
  }
}

Watch the stream and slow it down from your computer:

mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-c7e2a4 -t "connectmq/demo/nano33-c7e2a4/#" -v
mosquitto_pub -h broker.connectmq.com -p 1883 -i ctl-c7e2a4 -t "connectmq/demo/nano33-c7e2a4/rate/set" -m 1000

Notice that your laptop uses plain port 1883 while the board uses SSL on 8883. Both reach the same topics on the same broker.

The payload is streamed with print(value, digits) instead of snprintf. Float support in printf-style functions varies between Arduino cores, while print() formats floats the same way everywhere, and streaming avoids holding the whole message in RAM.

Step 3: Move to Private Credentials

The public area is fine for a demo, but motion data from real machines or people should not be public. On a managed MQTT broker such as ConnectMQ you create a broker in the portal and receive a generated username, password and client ID. Put them in arduino_secrets.h, keep the connection on 8883, and remove the connectmq/demo/ prefix in favour of your own topic structure. Client IDs are bound to the issued credentials, so the client ID must match exactly.

For guidance on topic design, credential rotation and least privilege, read MQTT security best practices.

Choosing QoS and Rate for IMU Data

High-rate sensor data and command messages have different needs:

  • Telemetry at 5 Hz or more: QoS 0. Losing one sample out of hundreds does not matter, and QoS 0 avoids extra acknowledgement traffic over the NINA link.
  • Rate changes and other commands: subscribe with QoS 1 so the broker retries delivery.
  • Status: retained QoS 1 with a matching Last Will, so dashboards always know whether the board is online.

If you need features like vibration analysis, consider computing summary values on the device (for example RMS acceleration over one second) and publishing those, rather than raw samples. It reduces bandwidth and stays well inside any plan's message limits. MQTT QoS levels explained covers these trade-offs in more depth.

Computing Vibration Summaries on the Board

Raw IMU samples are useful for experiments, but for monitoring a fan, pump or washing machine you usually care about how much the device is shaking over time. Computing a root mean square (RMS) of the acceleration magnitude over one second turns dozens of samples into a single meaningful number. Removing the 1 g gravity component first makes the value close to zero when the machine is still.

float sumSq = 0;
int samples = 0;
unsigned long windowStart = 0;

void sampleVibration() {
  float x, y, z;
  if (IMU.accelerationAvailable()) {
    IMU.readAcceleration(x, y, z);
    float mag = sqrt(x * x + y * y + z * z) - 1.0;   // remove gravity
    sumSq += mag * mag;
    samples++;
  }
  if (millis() - windowStart >= 1000 && samples > 0) {
    float rms = sqrt(sumSq / samples);
    mqttClient.beginMessage("connectmq/demo/nano33-c7e2a4/vibration");
    mqttClient.print("{\"rms_g\":");
    mqttClient.print(rms, 4);
    mqttClient.print(",\"n\":");
    mqttClient.print(samples);
    mqttClient.print("}");
    mqttClient.endMessage();
    sumSq = 0;
    samples = 0;
    windowStart = millis();
  }
}

Call sampleVibration() from loop() alongside mqttClient.poll(). The result is one message per second instead of a stream, and the n field tells you how many samples the value is based on. On the receiving side, a threshold on rms_g is a simple and effective way to flag abnormal shaking. Calibrate it by recording a few minutes of normal operation first, because every machine and mounting position behaves differently.

Keeping the Connection Healthy

A motion logger often runs unattended for days, so plan for network hiccups. The reconnect logic in the sketch retries every few seconds, but a router reboot can take longer, so consider increasing the delay after repeated failures to avoid hammering the network. Publish the retained status topic on every successful reconnect, and log WiFi.RSSI() together with your data: a weak signal is the most common reason a board in a metal enclosure keeps dropping off. If readings stop while the status still says online, check that poll() is called on every pass through loop().

Troubleshooting Checklist

  1. connect() fails on 8883 but works on 1883 with WiFiClient: update NINA firmware. If you use a broker with an uncommon CA, upload its root certificate with the firmware updater's certificate tool.
  2. "WiFi module not found": wrong board selected, or the WiFiNINA library is outdated.
  3. Error 2 (identifier rejected): empty client ID.
  4. Error 4 or 5: credentials or client ID do not match what the portal issued.
  5. Disconnects after minutes: something in loop() blocks poll(), or two boards share a client ID.
  6. IMU values frozen: check IMU.begin() succeeded and you read only when accelerationAvailable() is true.

The ArduinoMqttClient library guide goes deeper into the API used here, and the Arduino UNO R4 WiFi MQTT tutorial shows the same pattern on the WiFiS3 stack. For a broader view of embedded MQTT projects, see the ESP32 IoT tutorial.

Stream Your Nano 33 IoT Data to ConnectMQ

ConnectMQ is an MQTT broker in the cloud with SSL on port 8883, a Let's Encrypt certificate that the NINA module trusts out of the box, and a portal with a live topic explorer and threshold alert rules, which suits motion and vibration alerts well. Create a free Serverless broker at app.connectmq.com, no credit card needed.

Frequently Asked Questions

Does the Arduino Nano 33 IoT support MQTT over SSL?

Yes. The NINA-W102 Wi-Fi module performs the SSL/TLS handshake, and WiFiSSLClient combined with ArduinoMqttClient connects to port 8883 with very little code.

Do I need to add a root certificate for ConnectMQ?

Normally no. ConnectMQ uses a Let's Encrypt certificate, and its root is part of current NINA firmware. Update the firmware if verification fails.

How fast can I publish IMU data?

Keep the device within your broker's limits. On the public broker that is 10 messages per second per client; for higher sample rates, batch several readings into one message or publish computed summaries.

Can I use the ATECC608A for MQTT authentication?

The crypto element can store keys for certificate-based authentication with brokers that support client certificates. For the setup in this guide, you authenticate with the username, password and client ID issued in the portal.

Related articles

ESP-IDF MQTT Example: Build an esp-mqtt Client from Scratch

Build an ESP-IDF project from an empty folder with the esp-mqtt component: Wi-Fi station setup, the MQTT event handler, publish and subscribe, MQTT 5 properties and TLS with the certificate bundle.

8 min read

Using esp-mqtt MQTT 5 Features on ESP32 with ESP-IDF

A hands-on ESP-IDF project that enables MQTT 5.0 in esp-mqtt and uses user properties, message expiry, topic aliases, session expiry and response topics against a real MQTT 5 broker.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.