Community project

Wireless Crane Vibration Monitor

ESP32
Photo of Wireless Crane Vibration Monitor
Generated with AI

Gino Maureira

Published September 16, 2026

This wireless crane vibration monitor uses an ESP32 microcontroller paired with a 6-axis accelerometer to continuously track vibration patterns on industrial crane equipment. The device logs acceleration data to an onboard microSD card with precise timestamps from the RTC module, then enters low-power sleep mode to maximize battery life. A 10,000 mAh protected Li-ion cell powers the entire system, charged via the TP4056 module and regulated by a 3.3V buck-boost converter.

This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for mounting the sensor to your crane structure. You'll also receive the complete firmware with I2C communication routines for the accelerometer and RTC, SD card logging functionality, and a web interface for downloading recorded vibration data. A push button allows switching between monitoring and download modes, making it easy to retrieve historical vibration logs for predictive maintenance analysis.

Wiring diagram

Wiring diagram for Wireless Crane Vibration Monitor

Gather all the parts

QtyComponent
1

DFRobot SEN0142 Fermion MPU-6050 6 DOF Sensor Breakout

MPU-6050

DFRobot SEN0142 MPU-6050 breakout with 3-5 V board input, I2C interface, onboard I2C pull-ups, and i2cdevlib Arduino example coverage.

1

MicroSD Card Module

microSD industrial 8 GB o mayor

SPI-based microSD card adapter module for SPI-capable microcontrollers. Uses MOSI, MISO, SCK, and CS plus power and ground. Many low-cost modules include a 3.3 V regulator and level shifting for 5 V MCU boards, while bare breakouts should be powered and signalled at 3.3 V.

1

Pack Li-ion protegido de una celda, 3,7 V 10 000 mAh

3,7 V · 10 000 mAh, con BMS/protección

Batería recargable protegida de gran capacidad para que el registrador permanezca autónomo durante más de un mes.

1

TP4056 Li-Ion/LiPo charger module with protection

TP4056 con protección

TP4056 single-cell Li-Ion/LiPo linear charger module, 5V USB input, 1A charge current (programmable). Common variants ship with DW01 protection. Pair with battery_lipo_storage for the cell.

1

3.3v Buck Boost

3,3 V

TI TPS63030/TPS63031 high-efficiency single-inductor buck-boost converter family with 1A switches. Used to hold a regulated rail when battery voltage crosses above and below the target output.

1

Push Button

pulsador momentáneo

Momentary push button switch

1

PCF85063 RTC

PCF85063 + pila de respaldo

Tiny, low-power I2C real-time clock (RTC) and calendar chip from NXP. Keeps seconds-to-years time plus a programmable alarm, a countdown timer, and a configurable clock/interrupt output. Fixed I2C address 0x51. Operates from 0.9-5.5 V and draws sub-microamp current, so it is commonly used as the onboard RTC of Waveshare ESP32-S3 round-display boards and other compact battery-backed designs. Time is read and set over the I2C bus; pair it with a coin-cell or supercapacitor on the backup supply to retain time across power loss.

Assemble it in 6 steps

1. Prepara una caja firme y seca

Usa una caja cerrada con prensaestopas para que el polvo y el movimiento del puente grúa no tiren de los cables. Fija la caja a una zona rígida de la estructura, no a una cubierta fina que pueda añadir su propia vibración.

  • Marca en la caja las direcciones X, Y y Z del MPU6050 antes de cerrarla; así los ejes del archivo CSV tendrán significado al analizar los datos.
  • No instales este prototipo donde pueda interferir con el movimiento, el freno, los cables de carga ni los sistemas de seguridad del puente grúa.

2. Conecta la batería, cargador y regulador

Conecta BAT+ de la batería al terminal B+ del TP4056 y BAT− al B−. Conecta OUT+ del TP4056 a VIN del regulador de 3,3 V; conecta OUT− del cargador y GND del regulador al mismo GND. La salida VOUT del regulador es el cable de 3,3 V que alimenta el ESP32 y los módulos.

  • Antes de conectar el ESP32, mide la salida del regulador: debe ser 3,3 V, no 5 V.
  • No conectes la batería al revés: una batería invertida puede dañar el cargador, el regulador y el ESP32.
  • Carga el pack únicamente por IN+ e IN− del TP4056 con una fuente USB de 5 V; no alimentes el TP4056 desde la red eléctrica directamente.

3. Alimenta el ESP32 y el sensor

Lleva VOUT de 3,3 V del regulador al pin 3V3 del ESP32, al VIN del MPU6050, al VCC del PCF85063 y al VCC de la microSD. Une todos los pines GND: regulador, ESP32, MPU6050, PCF85063 y microSD. Conecta SDA del MPU6050 y SDA del PCF85063 a GPIO21; conecta sus SCL a GPIO22.

  • 3V3 → 3V3/VIN (alimentación); GND → GND (retorno); SDA → GPIO21 (datos); SCL → GPIO22 (reloj). Mantén estos cables cortos y juntos.
  • No alimentes los módulos de datos a 5 V: el ESP32 trabaja a 3,3 V y 5 V en sus pines de datos puede dañarlo.
  • Asegúrate de que el MPU6050 y el PCF85063 tengan direcciones distintas; estos módulos se comparten los mismos dos cables de datos.

4. Conecta la tarjeta de memoria y el botón

En el módulo microSD conecta MISO a GPIO19, MOSI a GPIO23, SCK a GPIO18 y CS a GPIO4. Coloca una pata del pulsador a GPIO27 y la otra a GND. Inserta una tarjeta microSD industrial formateada en FAT32.

  • MISO → GPIO19 (datos hacia el ESP32), MOSI → GPIO23 (datos hacia la tarjeta), SCK → GPIO18 (ritmo de datos), CS → GPIO4 (selecciona la tarjeta), un lado del botón → GPIO27 y el otro → GND (modo descarga).
  • No extraigas la microSD mientras el registrador está alimentado: podrías perder o corromper registros.
  • GPIO4 se usa como selección de la microSD; revisa cuidadosamente que no esté tocando otro cable.

5. Sujeta el sensor al puente grúa

Atornilla o pega el MPU6050 sobre una superficie metálica rígida y limpia del puente grúa, usando fijación mecánica firme. No lo dejes colgando de sus cables. Pon el ESP32, la batería y la tarjeta dentro de la caja y deja accesible el pulsador.

  • Una fijación floja mide el traqueteo de la caja, no la vibración real de la estructura. Anota en qué parte del puente grúa queda instalado.
  • La batería debe quedar inmóvil dentro de la caja: si se desplaza, puede tirar de cables o falsear la vibración medida.

6. Ajusta la fecha y descarga el registro

Para poner la fecha o descargar datos, mantén pulsado el botón mientras alimentas o despiertas el equipo. Conéctate desde un teléfono u ordenador a la red Wi‑Fi CraneVibe-Logger con contraseña vibracion2026. Abre 192.168.4.1 y descarga vibrations.csv. Para fijar la fecha, abre la dirección mostrada en la página principal con los valores de fecha y hora.

  • El modo Wi‑Fi se cierra solo después de diez minutos para ahorrar batería. En funcionamiento normal, no pulses el botón al iniciar.
  • No dejes la red Wi‑Fi abierta durante el mes de medida: consume mucha más batería que el modo de registro.

Review all connections

1. Connections between "imu_1" and "ESP32"

Functionimu_1ESP32
powerVIN3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22

2. Connections between "rtc_1" and "ESP32"

Functionrtc_1ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22

3. Connections between "sd_1" and "ESP32"

Functionsd_1ESP32
powerVCC3V3
groundGNDGND
spiMISOGPIO 19
spiMOSIGPIO 23
spiSCKGPIO 18
spiCSGPIO 4

4. Connections between "button_1" and "ESP32"

Functionbutton_1ESP32
groundGNDGND
digitalSIGNALGPIO 27

5. Connections between "battery_1" and "ESP32"

Functionbattery_1ESP32
powerBAT+TP4056 Li-Ion/LiPo charger module with protection B+EXT
groundBAT-TP4056 Li-Ion/LiPo charger module with protection B-EXT

6. Connections between "charger_1" and "ESP32"

Functioncharger_1ESP32
powerIN+Fuente USB de 5 V para cargaEXT
groundIN-Retorno de la fuente USB de 5 V para cargaEXT
powerOUT+3.3v Buck Boost VINEXT
groundOUT-GND

7. Connections between "regulator_1" and "ESP32"

Functionregulator_1ESP32
powerVOUT3V3
groundGNDGND

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <WiFi.h>
#include <WebServer.h>
#include <esp_sleep.h>


struct DateTime {
  uint16_t year;
  uint8_t month, day, hour, minute, second;
};


// Forward declarations
uint8_t bcdToDec(uint8_t value);
uint8_t decToBcd(uint8_t value);
bool readRegisters(uint8_t address, uint8_t reg, uint8_t *data, size_t length);
bool writeRegister(uint8_t address, uint8_t reg, uint8_t value);
bool readRtc(DateTime &dt);
bool setRtc(const DateTime &dt);
String timestamp(const DateTime &dt);
bool initMpu();
bool readAcceleration(float &x, float &y, float &z);
bool appendMeasurement();
String statusJson();
void startDownloadMode();
void goToSleep();

constexpr uint8_t MPU_ADDR = 0x68;
constexpr uint8_t RTC_ADDR = 0x51;
constexpr int I2C_SDA_PIN = 21;
constexpr int I2C_SCL_PIN = 22;
constexpr int SD_CS_PIN = 4;
constexpr int SD_MOSI_PIN = 23;
constexpr int SD_MISO_PIN = 19;
constexpr int SD_SCK_PIN = 18;
constexpr int MODE_BUTTON_PIN = 27;
constexpr uint32_t SAMPLE_WINDOW_MS = 1000;
constexpr uint32_t SAMPLE_INTERVAL_MS = 5;
constexpr uint64_t SLEEP_US = 60ULL * 1000000ULL;
constexpr uint32_t DOWNLOAD_TIMEOUT_MS = 10UL * 60UL * 1000UL;

WebServer server(80);
uint32_t downloadStartedAt = 0;
bool sdReady = false;



uint8_t bcdToDec(uint8_t value) { return ((value >> 4) * 10) + (value & 0x0F); }
uint8_t decToBcd(uint8_t value) { return ((value / 10) << 4) | (value % 10); }

bool readRegisters(uint8_t address, uint8_t reg, uint8_t *data, size_t length) {
  Wire.beginTransmission(address);
  Wire.write(reg);
  if (Wire.endTransmission(false) != 0) return false;
  if (Wire.requestFrom((int)address, (int)length) != (int)length) return false;
  for (size_t i = 0; i < length; ++i) data[i] = Wire.read();
  return true;
}

bool writeRegister(uint8_t address, uint8_t reg, uint8_t value) {
  Wire.beginTransmission(address);
  Wire.write(reg);
  Wire.write(value);
  return Wire.endTransmission() == 0;
}

bool readRtc(DateTime &dt) {
  uint8_t raw[7];
  if (!readRegisters(RTC_ADDR, 0x04, raw, sizeof(raw))) return false;
  dt.second = bcdToDec(raw[0] & 0x7F);
  dt.minute = bcdToDec(raw[1] & 0x7F);
  dt.hour = bcdToDec(raw[2] & 0x3F);
  dt.day = bcdToDec(raw[3] & 0x3F);
  dt.month = bcdToDec(raw[5] & 0x1F);
  dt.year = 2000 + bcdToDec(raw[6]);
  return dt.month >= 1 && dt.month <= 12 && dt.day >= 1 && dt.day <= 31;
}

bool setRtc(const DateTime &dt) {
  if (dt.year < 2000 || dt.year > 2099 || dt.month < 1 || dt.month > 12 || dt.day < 1 || dt.day > 31 || dt.hour > 23 || dt.minute > 59 || dt.second > 59) return false;
  uint8_t raw[7] = {decToBcd(dt.second), decToBcd(dt.minute), decToBcd(dt.hour), decToBcd(dt.day), 1, decToBcd(dt.month), decToBcd(dt.year - 2000)};
  Wire.beginTransmission(RTC_ADDR);
  Wire.write(0x04);
  Wire.write(raw, sizeof(raw));
  return Wire.endTransmission() == 0;
}

String timestamp(const DateTime &dt) {
  char buffer[24];
  snprintf(buffer, sizeof(buffer), "%04u-%02u-%02uT%02u:%02u:%02u", dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second);
  return String(buffer);
}

bool initMpu() {
  if (!writeRegister(MPU_ADDR, 0x6B, 0x00)) return false; // wake
  delay(50);
  if (!writeRegister(MPU_ADDR, 0x1A, 0x03)) return false; // 44 Hz low-pass filter
  if (!writeRegister(MPU_ADDR, 0x1C, 0x10)) return false; // accelerometer +/-8 g
  return true;
}

bool readAcceleration(float &x, float &y, float &z) {
  uint8_t raw[6];
  if (!readRegisters(MPU_ADDR, 0x3B, raw, sizeof(raw))) return false;
  int16_t ax = (int16_t)((raw[0] << 8) | raw[1]);
  int16_t ay = (int16_t)((raw[2] << 8) | raw[3]);
  int16_t az = (int16_t)((raw[4] << 8) | raw[5]);
  x = ax / 4096.0f;
  y = ay / 4096.0f;
  z = az / 4096.0f;
  return true;
}

bool appendMeasurement() {
  if (!initMpu()) return false;
  float sumX = 0, sumY = 0, sumZ = 0;
  float sumSqX = 0, sumSqY = 0, sumSqZ = 0;
  float peakX = 0, peakY = 0, peakZ = 0;
  uint16_t count = 0;
  uint32_t started = millis();
  while (millis() - started < SAMPLE_WINDOW_MS) {
    float x, y, z;
    if (readAcceleration(x, y, z)) {
      sumX += x; sumY += y; sumZ += z;
      sumSqX += x * x; sumSqY += y * y; sumSqZ += z * z;
      ++count;
    }
    delay(SAMPLE_INTERVAL_MS);
  }
  if (count < 20) return false;
  float meanX = sumX / count, meanY = sumY / count, meanZ = sumZ / count;
  // RMS after removing the static acceleration; this represents vibration rather than mounting angle.
  float rmsX = sqrtf(fmaxf(0, sumSqX / count - meanX * meanX));
  float rmsY = sqrtf(fmaxf(0, sumSqY / count - meanY * meanY));
  float rmsZ = sqrtf(fmaxf(0, sumSqZ / count - meanZ * meanZ));
  // A second short pass captures the peak relative to the measured static value.
  started = millis();
  while (millis() - started < SAMPLE_WINDOW_MS) {
    float x, y, z;
    if (readAcceleration(x, y, z)) {
      peakX = fmaxf(peakX, fabsf(x - meanX));
      peakY = fmaxf(peakY, fabsf(y - meanY));
      peakZ = fmaxf(peakZ, fabsf(z - meanZ));
    }
    delay(SAMPLE_INTERVAL_MS);
  }
  DateTime dt{};
  String when = readRtc(dt) ? timestamp(dt) : "TIME_NOT_SET";
  // On the ESP32 SD library, FILE_WRITE appends when the file already exists.
  File file = SD.open("/vibrations.csv", FILE_WRITE);
  if (!file) return false;
  if (file.size() == 0) file.println("timestamp,samples,rms_x_g,rms_y_g,rms_z_g,peak_x_g,peak_y_g,peak_z_g");
  file.printf("%s,%u,%.5f,%.5f,%.5f,%.5f,%.5f,%.5f\n", when.c_str(), count, rmsX, rmsY, rmsZ, peakX, peakY, peakZ);
  file.close();
  return true;
}

String statusJson() {
  DateTime dt{};
  String now = readRtc(dt) ? timestamp(dt) : "TIME_NOT_SET";
  File file = SD.open("/vibrations.csv", FILE_READ);
  size_t bytes = file ? file.size() : 0;
  if (file) file.close();
  return String("{\"rtc\":\"") + now + "\",\"sd_ready\":" + (sdReady ? "true" : "false") + ",\"log_bytes\":" + String(bytes) + "}";
}

void startDownloadMode() {
  WiFi.mode(WIFI_AP);
  WiFi.softAP("CraneVibe-Logger", "vibracion2026");
  server.on("/", HTTP_GET, []() {
    const char *page = "<!doctype html><html><body><h2>Crane vibration logger</h2><p><a href='/download'>Download vibrations.csv</a></p><p><a href='/status'>Status</a></p><p>Set clock: /set-time?year=2026&month=1&day=1&hour=12&minute=0&second=0</p></body></html>";
    server.send(200, "text/html", page);
  });
  server.on("/status", HTTP_GET, []() { server.send(200, "application/json", statusJson()); });
  server.on("/download", HTTP_GET, []() {
    if (!sdReady || !SD.exists("/vibrations.csv")) { server.send(404, "text/plain", "No log file"); return; }
    File file = SD.open("/vibrations.csv", FILE_READ);
    server.sendHeader("Content-Disposition", "attachment; filename=vibrations.csv");
    server.setContentLength(file.size());
    server.send(200, "text/csv", "");
    char buffer[513];
    size_t count = 0;
    while (file.available()) {
      int nextByte = file.read();
      if (nextByte < 0) break;
      buffer[count++] = static_cast<char>(nextByte);
      if (count == sizeof(buffer) - 1) {
        buffer[count] = '\0';
        server.sendContent(buffer);
        count = 0;
      }
    }
    if (count > 0) {
      buffer[count] = '\0';
      server.sendContent(buffer);
    }
    file.close();
  });
  server.on("/set-time", HTTP_GET, []() {
    if (!(server.hasArg("year") && server.hasArg("month") && server.hasArg("day") && server.hasArg("hour") && server.hasArg("minute") && server.hasArg("second"))) {
      server.send(400, "text/plain", "Missing date fields"); return;
    }
    DateTime dt{(uint16_t)server.arg("year").toInt(), (uint8_t)server.arg("month").toInt(), (uint8_t)server.arg("day").toInt(), (uint8_t)server.arg("hour").toInt(), (uint8_t)server.arg("minute").toInt(), (uint8_t)server.arg("second").toInt()};
    bool clockSet = setRtc(dt);
    server.send(clockSet ? 200 : 400, "text/plain", clockSet ? "Clock set" : "Invalid clock value");
  });
  server.begin();
  downloadStartedAt = millis();
}

void goToSleep() {
  Wire.end();
  // Holding the button low wakes the ESP32 directly into the download mode.
#if defined(ARDUINO_ARCH_ESP32)
  esp_sleep_enable_ext0_wakeup(static_cast<gpio_num_t>(MODE_BUTTON_PIN), 0);
#endif
  esp_sleep_enable_timer_wakeup(SLEEP_US);
  esp_deep_sleep_start();
}

void setup() {
  pinMode(MODE_BUTTON_PIN, INPUT_PULLUP);
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  SPI.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);
  sdReady = SD.begin(SD_CS_PIN, SPI, 4000000);
  bool downloadMode = digitalRead(MODE_BUTTON_PIN) == LOW;
  if (downloadMode) {
    startDownloadMode();
    return;
  }
  if (sdReady) appendMeasurement();
  goToSleep();
}

void loop() {
  server.handleClient();
  if (millis() - downloadStartedAt > DOWNLOAD_TIMEOUT_MS) {
    WiFi.softAPdisconnect(true);
    goToSleep();
  }
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik