Automate pump and fan vibration monitoring with an ESP32 accelerometer, InduQT and Claude

Updated Oct 31, 2026 · InduQT engineering team · Guide, not a customer case study

The short answer

An ESP32 board with an ADXL345 accelerometer, screwed to a pump or fan bearing housing, can report overall vibration (RMS acceleration on three axes, and the peak) every few seconds over MQTT for about US$30. A second sketch on this page runs an FFT on the board and reports velocity in mm/s (the ISO 10816/20816 measure) and the 1×, 2× and 3× running-speed components, so you can tell unbalance from misalignment. It is not a full vibration analyser, but a rising trend in these numbers is the earliest warning most pumps and fans give, and InduQT watches it against the machine’s own normal and similar machines at the site.

  • Overall vibration on three axes, every 5 seconds
  • A warning when vibration rises above this pump’s normal, not a generic limit
  • A trend you can show a vibration specialist before calling them out
Costabout US$30 per point plus a gateway
Time1–2 hours for the first one
SkillWiring and a little code
  1. What to buy
  2. Build and fit the sensor
  3. Load the sketch
  4. Step up: FFT band features (velocity in mm/s, 1x, 2x, 3x)
  5. Set up the MQTT broker (once per plant)
  6. Check the readings with Claude before you connect anything
  7. Wire it to InduQT
  8. What InduQT does with it

What to buy

ESP32-DevKitCabout US$10

Reads the accelerometer and publishes over Wi-Fi/MQTT. Maker: Espressif. Maker’s page

ADXL345 triple-axis accelerometer breakoutabout US$18

Measures acceleration up to ±16 g; read at 800 Hz (overall sketch) or 2,048 Hz (FFT sketch). Maker: Adafruit. Maker’s page

Small IP65 enclosure, a magnet or a stud mount, 5 V USB supplyabout US$10

Keeps it on the bearing housing and powered. Maker: any.

Alternative without code: an industrial wireless vibration sensor with an MQTT gatewayUS$150–500 per point

Use one when you need spectra, hazardous-area ratings or battery power. Maker: several makers.

Raspberry Pi 5 (4 GB) with power supply, case and 32 GB cardabout US$100 for the kit

Runs the MQTT broker and the InduQT bridge (skip it if you already built the gateway). Maker: Raspberry Pi. Maker’s page

Prices are typical street prices in US dollars when we wrote this, not quotes; buy from the maker or a local distributor. InduQT does not sell sensors and is not paid by any maker named here.

Safety. Fit the sensor with the pump stopped and locked out, away from couplings and shaft guards, and route the cable so it cannot reach a rotating part. A DIY sensor is not rated for hazardous (ATEX/IECEx) areas: on oil and gas sites use it only in safe areas such as workshops and utilities.

Build and fit the sensor

  1. Wire the ADXL345 to the ESP32: VIN → 3V3, GND → GND, SDA → GPIO 21, SCL → GPIO 22.
  2. Put both in the enclosure with the ADXL345 board screwed firmly to the enclosure wall that will touch the machine (a loose board measures its own rattle).
  3. Mount the enclosure on the pump’s drive-end bearing housing with a stud or a strong magnet on a flat, bare spot. Note which axis points along the shaft.
  4. Power it from a 5 V USB supply.

Load the sketch

The sketch samples the accelerometer for one second at 800 Hz, removes gravity, and publishes the RMS of each axis and the peak, in g, every 5 s. Install the esp32 boards package (3.x) and the PubSubClient, Adafruit ADXL345 and Adafruit Unified Sensor libraries, set your Wi-Fi and broker details, and upload. Not yet run on a pump: compare the first readings with a handheld vibration meter if you have one.

ESP32 sketch: ADXL345 → factory/pump-3/vibration
// Vibration (RMS and peak acceleration) from an ADXL345 → MQTT every 5 s
// Board: any ESP32 dev board (e.g. Espressif ESP32-DevKitC). Arduino IDE 2.x with the
// "esp32 by Espressif Systems" boards package.
// Libraries (Library Manager): PubSubClient by Nick O'Leary, Adafruit ADXL345 (and Adafruit Unified Sensor)
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_ADXL345_U.h>

const char* WIFI_SSID     = "factory-wifi";
const char* WIFI_PASSWORD = "wifi-password";
const char* MQTT_HOST     = "192.168.1.20";   // the Raspberry Pi running Mosquitto
const int   MQTT_PORT     = 1883;
const char* MQTT_USER     = "sensors";
const char* MQTT_PASSWORD = "sensors-password";
const char* TOPIC         = "factory/pump-3/vibration";

WiFiClient net;
PubSubClient mqtt(net);

void connectAll() {
  while (WiFi.status() != WL_CONNECTED) { delay(500); }
  while (!mqtt.connected()) {
    if (!mqtt.connect("pump-3-vibration", MQTT_USER, MQTT_PASSWORD)) delay(2000);
  }
}

Adafruit_ADXL345_Unified accel(12345);

void setup() {
  Serial.begin(115200);
  Wire.begin();                 // SDA 21, SCL 22 on most ESP32 boards
  Wire.setClock(400000);
  if (!accel.begin()) { Serial.println("ADXL345 not found: check wiring"); while (true) delay(1000); }
  accel.setRange(ADXL345_RANGE_8_G);
  accel.setDataRate(ADXL345_DATARATE_800_HZ);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
}

void loop() {
  connectAll();
  // Sample for one second, then report RMS (gravity removed) and peak per axis, in g.
  const int N = 800;
  double sx = 0, sy = 0, sz = 0, qx = 0, qy = 0, qz = 0, peak = 0;
  sensors_event_t e;
  for (int i = 0; i < N; i++) {
    accel.getEvent(&e);
    double x = e.acceleration.x / 9.80665, y = e.acceleration.y / 9.80665, z = e.acceleration.z / 9.80665;
    sx += x; sy += y; sz += z; qx += x * x; qy += y * y; qz += z * z;
    peak = max(peak, max(fabs(x), max(fabs(y), fabs(z))));
    delayMicroseconds(1250);
  }
  double rx = sqrt(max(0.0, qx / N - sq(sx / N)));
  double ry = sqrt(max(0.0, qy / N - sq(sy / N)));
  double rz = sqrt(max(0.0, qz / N - sq(sz / N)));
  char json[160];
  snprintf(json, sizeof json, "{\"vib_rms_x_g\":%.4f,\"vib_rms_y_g\":%.4f,\"vib_rms_z_g\":%.4f,\"vib_peak_g\":%.3f}", rx, ry, rz, peak);
  mqtt.publish(TOPIC, json);
  mqtt.loop();
  delay(4000);
}

Step up: FFT band features (velocity in mm/s, 1x, 2x, 3x)

Vibration specialists judge machines by velocity in mm/s over 10–1,000 Hz (the ISO 10816/20816 measure) and by the amplitude at 1×, 2× and 3× running speed: a high 1× usually means unbalance, a high 2× misalignment or looseness, and rising high-frequency acceleration points at bearings and lubrication. This second sketch samples one radial axis at 2,048 Hz, runs a 1,024-point FFT on the ESP32 and publishes those band features every 5 s as ordinary signals, so InduQT trends and compares them like any other reading.

What the FFT sketch publishes
SignalMeaningTypical use
vel_rms_mm_sVelocity RMS, 10–1,000 HzOverall severity, compared with ISO 10816/20816 zones
v1x_mm_sVelocity peak at running speedUnbalance, bent shaft
v2x_mm_sVelocity peak at twice running speedMisalignment, looseness
v3x_mm_sVelocity peak at three times running speedMisalignment, looseness
acc_hf_rms_gAcceleration RMS, 500–1,000 HzEarly bearing and lubrication problems
acc_rms_g, peak_hzOverall acceleration and the strongest frequencySpotting a new source (a resonance, a fan blade)
ESP32 sketch: FFT band features → factory/pump-3/vibration
ESP32 sketch: FFT band features → factory/pump-3/vibration
// Vibration with FFT band features (ISO-style velocity RMS, 1x/2x/3x, high-frequency band) → MQTT
// Board: ESP32-DevKitC. Arduino IDE 2.x, "esp32 by Espressif Systems" 3.x.
// Libraries: PubSubClient by Nick O'Leary, arduinoFFT by Enrique Condes (2.x)
// Sensor: ADXL345 on I2C (SDA 21, SCL 22), read directly at 400 kHz for a steady 2,048 Hz sample rate.
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <arduinoFFT.h>

const char* WIFI_SSID     = "factory-wifi";
const char* WIFI_PASSWORD = "wifi-password";
const char* MQTT_HOST     = "192.168.1.20";   // the Raspberry Pi running Mosquitto
const int   MQTT_PORT     = 1883;
const char* MQTT_USER     = "sensors";
const char* MQTT_PASSWORD = "sensors-password";
const char* TOPIC         = "factory/pump-3/vibration";
const double RUNNING_HZ   = 24.75;  // shaft speed in Hz (rpm / 60); 0 if unknown
const uint8_t AXIS        = 0;      // 0 = X, 1 = Y, 2 = Z: the axis that points radially at the bearing

WiFiClient net;
PubSubClient mqtt(net);

void connectAll() {
  while (WiFi.status() != WL_CONNECTED) { delay(500); }
  while (!mqtt.connected()) {
    if (!mqtt.connect("pump-3-vibration", MQTT_USER, MQTT_PASSWORD)) delay(2000);
  }
}

const int    N  = 1024;             // 0.5 s of data, 2 Hz per bin
const double FS = 2048.0;           // samples per second (Nyquist 1,024 Hz)
const double G  = 9806.65;          // mm/s² per g
double vReal[N], vImag[N];
ArduinoFFT<double> fft(vReal, vImag, N, FS);

void adxlWrite(uint8_t reg, uint8_t v) { Wire.beginTransmission(0x53); Wire.write(reg); Wire.write(v); Wire.endTransmission(); }

double readAxisG() {
  Wire.beginTransmission(0x53); Wire.write(0x32 + 2 * AXIS); Wire.endTransmission(false);
  Wire.requestFrom(0x53, 2);
  int16_t raw = Wire.read() | (Wire.read() << 8);
  return raw * 0.0039;              // full resolution: 3.9 mg per count
}

void setup() {
  Serial.begin(115200);
  Wire.begin(); Wire.setClock(400000);
  adxlWrite(0x31, 0x0B);            // DATA_FORMAT: full resolution, ±16 g
  adxlWrite(0x2C, 0x0F);            // BW_RATE: 3,200 Hz output data rate
  adxlWrite(0x2D, 0x08);            // POWER_CTL: measure
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setBufferSize(512);
}

// Mean-square (g²) of one bin, scaled so the bins of a band add up to that band's mean square.
double binMs(int k, double s2) { return 2.0 * vReal[k] * vReal[k] / (N * s2); }

// Peak velocity (mm/s) of a component near f: the energy of ±3 bins, so off-bin speeds still read right.
double bandPeakV(double f, double s2) {
  int k = (int)round(f / (FS / N)); double ms = 0;
  for (int j = max(1, k - 3); j <= min(N / 2 - 1, k + 3); j++) { double fj = j * FS / N; ms += binMs(j, s2) * sq(G / (2 * PI * fj)); }
  return sqrt(ms) * sqrt(2.0);
}

void loop() {
  connectAll();
  // 1. Sample at a fixed rate.
  unsigned long next = micros();
  for (int i = 0; i < N; i++) {
    while ((long)(micros() - next) < 0) {}
    vReal[i] = readAxisG(); vImag[i] = 0;
    next += 488;                    // 1e6 / 2048 µs, rounded
  }
  // 2. Remove the mean (gravity), apply a Hann window and keep its sums for scaling.
  double mean = 0; for (int i = 0; i < N; i++) mean += vReal[i]; mean /= N;
  double s2 = 0;
  for (int i = 0; i < N; i++) { double w = 0.5 - 0.5 * cos(2 * PI * i / (N - 1)); vReal[i] = (vReal[i] - mean) * w; s2 += w * w; }
  // 3. FFT → magnitudes in vReal[0..N/2].
  fft.compute(FFTDirection::Forward);
  fft.complexToMagnitude();
  // 4. Band features.
  double velMs = 0, accMs = 0, hfMs = 0, peakV = 0, peakHz = 0;
  for (int k = 1; k < N / 2; k++) {
    double f = k * FS / N, a = binMs(k, s2);
    if (f >= 10 && f <= 1000) {
      double v = a * sq(G / (2 * PI * f));
      velMs += v; accMs += a;
      if (v > peakV) { peakV = v; peakHz = f; }
    }
    if (f >= 500 && f <= 1000) hfMs += a;
  }
  char json[300];
  int n = snprintf(json, sizeof json, "{\"vel_rms_mm_s\":%.3f,\"acc_rms_g\":%.4f,\"acc_hf_rms_g\":%.4f,\"peak_hz\":%.1f",
                   sqrt(velMs), sqrt(accMs), sqrt(hfMs), peakHz);
  if (RUNNING_HZ > 0) {
    n += snprintf(json + n, sizeof json - n, ",\"v1x_mm_s\":%.3f,\"v2x_mm_s\":%.3f,\"v3x_mm_s\":%.3f",
                  bandPeakV(RUNNING_HZ, s2), bandPeakV(2 * RUNNING_HZ, s2), bandPeakV(3 * RUNNING_HZ, s2));
  }
  snprintf(json + n, sizeof json - n, "}");
  mqtt.publish(TOPIC, json);
  mqtt.loop();
  delay(4000);
}

How we checked it: the windowing, scaling and band maths were run in Python against synthetic signals on 2026-10-31. Velocity RMS came out within about 1 % and the 1× and 2× amplitudes within about 1 %, including running speeds that fall between FFT bins. The sketch itself has not yet been run on a machine. Set RUNNING_HZ to the shaft speed (rpm ÷ 60) and AXIS to the radial axis.

Commonly quoted ISO 10816-3 zone boundaries, velocity RMS in mm/s (check the standard for your machine group and mounting)
MachineA/B (good → acceptable)B/C (acceptable → act soon)C/D (damage likely)
15–300 kW, rigid foundation1.42.84.5
15–300 kW, flexible foundation2.34.57.1
300 kW–50 MW, rigid foundation2.34.57.1
300 kW–50 MW, flexible foundation3.57.111.0
InduQT stores these band features, not raw waveforms or full spectra. For spectrum and waveform analysis on critical machines, use a dedicated analyser or add InduQT’s expert monitoring.

Set up the MQTT broker (once per plant)

The sensors publish their readings to a broker: a small program that receives MQTT messages and hands them to whoever subscribes. Mosquitto is free and runs on a Raspberry Pi, an old PC or a small industrial box on the plant network. You need one per plant, not one per machine. If you already have a broker (Node-RED, Home Assistant, an IoT gateway), use it and skip this step.

Install Mosquitto
# Raspberry Pi OS, Debian or Ubuntu
sudo apt update
sudo apt install -y mosquitto mosquitto-clients

# One user for the sensors and one read-only user for testing
sudo mosquitto_passwd -c /etc/mosquitto/passwd sensors
sudo mosquitto_passwd /etc/mosquitto/passwd viewer
Require a password
# /etc/mosquitto/conf.d/factory.conf
listener 1883
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/acl
Who can publish and who can only read
# /etc/mosquitto/acl
user sensors
topic readwrite factory/#

user viewer
topic read factory/#
Start it and watch messages arrive
# Mosquitto 2 wants these files readable only by its own user
sudo chown mosquitto: /etc/mosquitto/passwd /etc/mosquitto/acl
sudo chmod 0700 /etc/mosquitto/passwd /etc/mosquitto/acl

sudo systemctl restart mosquitto
sudo systemctl enable mosquitto

# Watch everything that arrives (Ctrl+C to stop)
mosquitto_sub -h localhost -u viewer -P 'viewer-password' -t 'factory/#' -v
Give the Raspberry Pi a fixed IP address (a DHCP reservation on the router) so the sensors can always find it. Every example below uses 192.168.1.20. Keep the broker on the plant network; it does not need to be reachable from the internet.

Check the readings with Claude before you connect anything

Before the data goes anywhere, make sure the sensor is telling the truth. Record two minutes of messages to a file and ask Claude (or ChatGPT) to check them. This catches the usual first-day problems (a clamp on the wrong cable, a probe reading the room instead of the pipe, a sensor that only reports when something changes) while you are still standing at the pump.

Record two minutes of messages (Raspberry Pi, Mac or Linux)
# Record 2 minutes of messages to a file you can give Claude or ChatGPT
mosquitto_sub -h 192.168.1.20 -u viewer -P 'viewer-password' \
  -t 'factory/#' -v -W 120 | while read -r line; do
  echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) $line"
done > sensor-sample.txt
The same on Windows
# Windows PowerShell, with Mosquitto installed from mosquitto.org
& "C:\Program Files\mosquitto\mosquitto_sub.exe" -h 192.168.1.20 -u viewer -P viewer-password `
  -t "factory/#" -v -W 120 | ForEach-Object { "$(Get-Date -Format o) $_" } | Out-File sensor-sample.txt
Prompt
Paste into Claude or ChatGPT with sensor-sample.txt attached
I've attached sensor-sample.txt: two minutes of MQTT messages from a sensor I just fitted to the pump.
Each line is: time, topic, payload.

Please check it for me:
1. List each topic and every numeric field in it, with the min, max and typical value.
2. Tell me whether the values are physically plausible for the pump. With the pump stopped the RMS values should be close to zero (under about 0.01 g). Running, a healthy small pump is typically a few hundredths to a few tenths of a g RMS. Tell me the stopped and running values for each axis and whether any axis is much higher than the others.
3. Point out gaps longer than 30 seconds, repeated identical values, or fields that are always 0.
4. Say which fields are worth sending to InduQT as signals and which I can ignore.
Answer in a short table, then a list of anything I should fix before I wire it up.

Claude Code or Claude desktop with a terminal can run the mosquitto_sub command for you: ask it to "subscribe to factory/# on 192.168.1.20 as viewer for two minutes and check the readings". Either way the readings stay on your laptop: nothing is stored in InduQT until you wire it up in the next step.

Using a cloud broker (HiveMQ Cloud, EMQX Serverless or your own server with TLS) instead of one on the plant network? Add the free InduQT connector to Claude (https://claude.ai/directory/connectors/induqt) and Claude can listen to the broker for a few seconds itself:

Prompt
Claude with the InduQT connector
Use InduQT to preview my MQTT broker before I connect it:
url mqtts://<your-cluster>.hivemq.cloud:8883, topic factory/#,
username viewer, password <viewer password>. Listen for 10 seconds.
Then tell me which topic level names the machine and which fields are numeric.

Wire it to InduQT

Once the readings look right, send them to InduQT. The simplest way is a small bridge on the same Raspberry Pi: it listens to your broker and forwards every number to InduQT over HTTPS. It works on every InduQT plan, including the free one (up to 3 machines), and nothing on the plant network has to be opened to the internet.

  1. In Claude with the InduQT connector, paste the prompt below. Claude creates the site, the machine and a key that can only send readings to that site. (No Claude? Do the same in the InduQT app: Add machine with the same externalId, then Settings → API keys → a key with the ingest scope, limited to the site.)
  2. Copy the key Claude shows you. It is shown once.
  3. Start the bridge on the Raspberry Pi with that key.
  4. Ask Claude for the latest values, and set the normal bands.
Prompt
1 · Claude creates the machine and the key
Set up InduQT for this sensor:
1. Create a site called "Plant 1" (skip if it already exists).
2. Create a machine named "pump-3" with externalId "pump-3" and family "pumps".
3. Create an ingest key for the site called "Raspberry Pi bridge" and show it to me once.
After I start the bridge, I'll ask you to check the latest values. The signals will be vib_rms_x_g, vib_rms_y_g, vib_rms_z_g and vib_peak_g.
2 · Start the bridge
python3 -m venv ~/induqt && source ~/induqt/bin/activate
pip install "paho-mqtt>=2.0" requests

export MQTT_HOST=localhost MQTT_USER=viewer MQTT_PASSWORD='viewer-password'
export MQTT_TOPICS='factory/#'
export INDUQT_API_KEY='iq_live_…'   # the site key Claude or the app created for you
python3 induqt_bridge.py
induqt_bridge.py (the whole bridge, about 120 lines)
induqt_bridge.py (the whole bridge, about 120 lines)
#!/usr/bin/env python3
"""
induqt_bridge.py — forward MQTT sensor messages to InduQT.

Subscribes to your local broker, turns each message into numbers, and posts them
to the InduQT ingest API in batches. Works on every InduQT plan, including the free one,
and your broker never has to be reachable from the internet.

  factory/<machine>/<anything>        ->  machine = <machine> (matched to the machine's externalId)
  factory/pump-3/vibration {"vib_rms_x_g": 0.12}  ->  signal vib_rms_x_g
  factory/comp-1/status/em:0 {"a_current": 12.4}  ->  signal em_0.a_current
      (two or more levels after the machine: the last level names the group, so
       two probes or two channels never overwrite each other)
  {"temperature": {"tC": 41.2}}       ->  signal temperature.tC
  factory/furnace-1/temp  21.7        ->  signal temp (a bare number takes the last level)

pip install "paho-mqtt>=2.0" requests
"""
import json
import os
import re
import threading
import time

import paho.mqtt.client as mqtt
import requests

MQTT_HOST = os.environ.get("MQTT_HOST", "localhost")
MQTT_PORT = int(os.environ.get("MQTT_PORT", "1883"))
MQTT_USER = os.environ.get("MQTT_USER")
MQTT_PASSWORD = os.environ.get("MQTT_PASSWORD")
TOPICS = [t.strip() for t in os.environ.get("MQTT_TOPICS", "factory/#").split(",") if t.strip()]
MACHINE_LEVEL = int(os.environ.get("MACHINE_LEVEL", "1"))  # factory/<machine>/... is level 1
SEND_EVERY = float(os.environ.get("SEND_EVERY", "10"))     # seconds between posts
IGNORE = set(os.environ.get("IGNORE_FIELDS", "id,ts,time,timestamp,unixtime,Time").split(","))
INGEST_URL = os.environ.get("INDUQT_URL", "https://api.induqt.ai/v1/ingest")
API_KEY = os.environ["INDUQT_API_KEY"]  # a site key from InduQT (Settings -> API keys)

BAD = re.compile(r"[^A-Za-z0-9_.\-]")
buffer, lock = [], threading.Lock()


def key(name):
    return BAD.sub("_", name)[:64]


def numbers(prefix, value, out):
    """Collect every number in a JSON value; true/false become 1/0."""
    if isinstance(value, bool):
        out[key(prefix)] = 1 if value else 0
    elif isinstance(value, (int, float)):
        out[key(prefix)] = value
    elif isinstance(value, str):
        text = value.strip().lower()
        if text in ("on", "true", "online"):
            out[key(prefix)] = 1
        elif text in ("off", "false", "offline"):
            out[key(prefix)] = 0
        else:
            try:
                out[key(prefix)] = float(text)
            except ValueError:
                pass
    elif isinstance(value, dict):
        for k, v in value.items():
            if k not in IGNORE:
                numbers(f"{prefix}.{k}" if prefix else k, v, out)


def on_connect(client, userdata, flags, reason_code, properties):
    print(f"MQTT connected ({reason_code}); subscribing to {TOPICS}")
    for t in TOPICS:
        client.subscribe(t, qos=1)


def on_message(client, userdata, msg):
    levels = msg.topic.split("/")
    if len(levels) <= MACHINE_LEVEL:
        return
    machine = levels[MACHINE_LEVEL]
    try:
        payload = json.loads(msg.payload.decode("utf-8", "replace"))
    except ValueError:
        payload = msg.payload.decode("utf-8", "replace")
    values = {}
    group = levels[-1] if len(levels) - MACHINE_LEVEL > 2 else ""
    if isinstance(payload, dict):
        numbers(group, payload, values)
    else:
        numbers(levels[-1], payload, values)
    if values:
        with lock:
            buffer.append({"machine": machine, "ts": int(time.time() * 1000), "values": values})


def sender():
    while True:
        time.sleep(SEND_EVERY)
        with lock:
            batch = buffer[:500]
        if not batch:
            continue
        try:
            r = requests.post(
                INGEST_URL,
                headers={"Authorization": f"Bearer {API_KEY}"},
                json={"readings": batch, "autoCreate": True},
                timeout=15,
            )
            if r.status_code < 300:
                with lock:
                    del buffer[: len(batch)]
                print(f"sent {len(batch)} readings: {r.json()}")
            elif r.status_code in (400, 401, 403):
                print(f"InduQT refused the batch ({r.status_code}): {r.text}")
                with lock:
                    del buffer[: len(batch)]  # fix the key or mapping; retrying will not help
            else:
                print(f"InduQT returned {r.status_code}; will retry")
        except requests.RequestException as e:
            print(f"network error, will retry: {e}")
        with lock:
            del buffer[:-5000]  # keep at most ~5,000 readings while offline


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="induqt-bridge")
if MQTT_USER:
    client.username_pw_set(MQTT_USER, MQTT_PASSWORD)
client.on_connect = on_connect
client.on_message = on_message
threading.Thread(target=sender, daemon=True).start()
client.connect(MQTT_HOST, MQTT_PORT, keepalive=60)
client.loop_forever()
Keep it running after a reboot (systemd)
Keep it running after a reboot (systemd)
# /etc/systemd/system/induqt-bridge.service  — keeps the bridge running after a reboot
[Unit]
Description=MQTT to InduQT bridge
After=network-online.target mosquitto.service

[Service]
User=pi
EnvironmentFile=/home/pi/induqt-bridge.env
ExecStart=/home/pi/induqt/bin/python3 /home/pi/induqt_bridge.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

# then:
# sudo systemctl daemon-reload && sudo systemctl enable --now induqt-bridge
# journalctl -u induqt-bridge -f

The bridge treats the topic level after factory/ as the machine (it matches the machine's externalId and creates it if needed), turns every number in the message into a signal (nested fields become names like temperature.tC; when the topic has a group after the machine, such as Shelly's status/em:0, the group goes in front, as em_0.a_current, so two channels never overwrite each other; true/false and on/off become 1/0) and sends a batch every 10 seconds. It was run end to end on 2026-10-31 against Mosquitto 2 with Shelly Gen2, Theengs and plain-number messages. Prefer Node-RED? The same bridge as a Node-RED flow is on the gateway guide.

Prompt
3 · Check it arrived and set what normal looks like
Show me the latest values InduQT has for pump-3.
Then set these normal bands so InduQT knows what "normal" looks like: vib_rms_x_g, vib_rms_y_g and vib_rms_z_g up to twice their running values from the first day.

On the Plant Monitor plan InduQT can also subscribe to a broker directly. Point your sensors (or a Mosquitto bridge) at a cloud broker with TLS, then ask Claude to "create an MQTT connector for this broker, machine from topic level 1", or add it in the app under the site → Connect data → MQTT broker.

What InduQT does with it

  • Learns this pump’s normal running vibration (and separates stopped from running by itself).
  • Spots slow rises over weeks with trend and change-point tests, and forecasts when vibration will cross the band you set.
  • Compares pumps of the same kind at the site (peer comparison), so the one that is different stands out.
  • Add the pump’s current (CNC guide clamp) and InduQT can tell a hydraulic problem from a mechanical one more often.
Prompt
Ask Claude
In InduQT, which of my pumps has the highest vibration compared with its own normal this week, and is it getting worse?

Frequently asked questions

Is an ADXL345 good enough for vibration monitoring?

For overall vibration trends, yes. For bearing fault frequencies, no: you need a sensor with a flat response to several kHz, and spectra. Start with the trend; call in a vibration specialist when InduQT shows a rise.

Should I report velocity (mm/s) instead of acceleration?

ISO 10816/20816 limits are in velocity. Acceleration RMS rises with the same faults and is simpler to measure; InduQT compares each pump with its own normal, so the unit matters less than consistency.

Related