Ruben Zilzer
Published © CC BY

Mini seismograph

“ESP32 project with MPU6050 & mic that senses booms/shockwaves and logs vibration data online for analysis. ”

IntermediateWork in progress121
Mini seismograph

Things used in this project

Hardware components

ESP-WROOM-32
×1
SparkFun Triple Axis Accelerometer and Gyro Breakout - MPU-6050
SparkFun Triple Axis Accelerometer and Gyro Breakout - MPU-6050
×1
analog microphone
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

seismograph

schem

Code

blast recorder

Arduino
needs a "wifi_secrets.h" with wifi credentials and server settings
#include "wifi_secrets.h"
#include <Wire.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include "time.h"
#include <math.h>


// ====== TIME (GMT+2 with daylight saving) ======
const long gmtOffset_sec      = 2 * 3600;   // Base GMT+2
const int  daylightOffset_sec = 3600;       // +1h when DST applies

// ====== MIC SETTINGS ======
#define MIC_AO_PIN 34
const float micAlpha   = 0.10f;   // balanced smoothing
const float micScale   = 1.0f;    // keep raw values from test

const int   micThreshHigh = 220;  // fire when above this
const int   micThreshLow  = 150;  // re-arm when falling below

// ====== ACCEL (ΔZ) SETTINGS ======
const float baseAlpha        = 0.015f; // baseline adapts a bit faster
const float deltaAlpha       = 0.25f;  // smoothing for deltaZ

const float azThreshHigh = 0.05f;  
const float azThreshLow  = 0.03f;  

const float azAmplify = 15.0f;     
const float azOffset  = -15.0f;

// ====== EVENT CLUSTERING ======
const uint32_t EVENT_WINDOW_MS = 1200;  

const uint16_t MAX_EVENTS_PER_MIN = 60;
uint16_t eventsThisMinute = 0;
uint32_t minuteStartMs = 0;

// ====== QUEUE FOR SENDING ======
struct Spike {
  time_t epoch;
  char   ts[20];
  float  azScaled;
  int    mic;
  uint32_t readyMs;
  uint8_t  attempts;
};

#define QCAP 32
Spike q[QCAP];
int qHead=0, qTail=0;
inline bool qEmpty(){ return qHead==qTail; }
inline bool qFull(){ return ((qHead+1)%QCAP)==qTail; }
bool enqueueSpike(const Spike& s){ if(qFull())return false; q[qHead]=s; qHead=(qHead+1)%QCAP; return true; }
bool peekSpike(Spike& s){ if(qEmpty()) return false; s=q[qTail]; return true; }
void popSpike(){ if(!qEmpty()) qTail=(qTail+1)%QCAP; }

uint32_t nextBackoffMs(uint8_t attempts){ uint32_t ms = 2000UL << (attempts>5?5:attempts); return ms>60000UL?60000UL:ms; }

// ====== GLOBALS ======
Adafruit_MPU6050 mpu;
float micSmooth=0.0f;
float baselineAz=0.0f;
float deltaAz=0.0f;

bool micArmed = true;
bool azArmed  = true;

bool     inEvent         = false;
uint32_t eventStartMs    = 0;
time_t   eventStartEpoch = 0;
char     eventStartTs[20];

float peakAbsDeltaAz = 0.0f;
int   peakMic        = 0;

// ====== HELPERS ======
inline bool timeIsSynced(){ return time(nullptr) > 100000; }
inline void waitForTimeOnce(){ for(int i=0;i<10;i++){ if(timeIsSynced()) return; delay(500);} }

String urlEncode(const String& s){
  const char *hex="0123456789ABCDEF"; String out; out.reserve(s.length()*3);
  for (uint8_t c : s){
    if (('a'<=c&&c<='z')||('A'<=c&&c<='Z')||('0'<=c&&c<='9')||c=='-'||c=='_'||c=='.'||c=='~'||c==',') out+=char(c);
    else if(c==' ') out+="%20";
    else { out+='%'; out+=hex[(c>>4)&0xF]; out+=hex[c&0xF]; }
  } return out;
}

String buildDataParam(const Spike& s){
  String d; d.reserve(48);
  d+=String((unsigned long long)s.epoch); d+=",";
  d+=s.ts; d+=",";
  d+=String(s.azScaled,2); d+=",";
  d+=String(s.mic);
  return d;
}

bool sendSpikeHTTP(const Spike& s){
  String url=(USE_HTTPS?"https://":"http://"); url+=SERVER_HOST; url+=SERVER_PATH; url+="?data="; url+=urlEncode(buildDataParam(s));
  HTTPClient http; http.setTimeout(10000);
  http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
  http.setReuse(false);
  http.addHeader("Connection","close");
  http.addHeader("User-Agent","ESP32Spike/2.0");

  int code=-1;
  if(USE_HTTPS){
    WiFiClientSecure cli; cli.setTimeout(10000); cli.setInsecure();
    if(!http.begin(cli, url)) return false;
    code=http.GET();
  } else {
    WiFiClient cli; cli.setTimeout(10000);
    if(!http.begin(cli, url)) return false;
    code=http.GET();
  }
  http.end();
  return (code>=200 && code<300);
}

void processQueue(){
  if(WiFi.status()!=WL_CONNECTED || qEmpty()) return;
  uint32_t nowMs=millis();
  for(int i=0;i<3;i++){
    Spike s; if(!peekSpike(s)) return;
    if(nowMs < s.readyMs) return;
    if(sendSpikeHTTP(s)){
      Serial.printf("[SEND] OK %llu,%s,%.2f,%d\n",
        (unsigned long long)s.epoch, s.ts, s.azScaled, s.mic);
      popSpike();
    } else {
      Serial.println("[SEND] FAIL (retry later)");
      s.readyMs = nowMs + nextBackoffMs(s.attempts++);
      q[qTail]=s;
      break;
    }
  }
}

bool rateLimited(){
  if (MAX_EVENTS_PER_MIN==0) return false;
  uint32_t now=millis();
  if (now - minuteStartMs >= 60000UL){ minuteStartMs = now; eventsThisMinute = 0; }
  if (eventsThisMinute >= MAX_EVENTS_PER_MIN) return true;
  return false;
}
void noteEventSent(){ if(MAX_EVENTS_PER_MIN) eventsThisMinute++; }

// ====== SETUP ======
void setup(){
  Serial.begin(115200);
  pinMode(MIC_AO_PIN, INPUT);

  WiFi.mode(WIFI_STA);

  // >>> Add your known networks <<<
  wifiMulti.addAP(WIFI1_SSID, WIFI1_PASS);
  wifiMulti.addAP(WIFI2_SSID, WIFI2_PASS);
  wifiMulti.addAP(WIFI3_SSID, WIFI3_PASS);

  Serial.println("Connecting to known WiFi networks...");
  unsigned long t0 = millis();
  while (wifiMulti.run() != WL_CONNECTED && millis() - t0 < 20000) {
    delay(300);
    Serial.print(".");
  }
  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("\nWiFi OK → SSID: %s  IP: %s\n",
                  WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
  } else {
    Serial.println("\nWiFi FAILED (will keep trying in background)");
  }

  WiFi.setAutoReconnect(true);
  WiFi.persistent(false);

  configTime(gmtOffset_sec, daylightOffset_sec, "pool.ntp.org");
  waitForTimeOnce();

  mpu.begin();
  mpu.setAccelerometerRange(MPU6050_RANGE_2_G);
  mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);

  minuteStartMs = millis();
  Serial.println("Ready.");
}

// ====== LOOP ======
void loop(){
  sensors_event_t a,g,temp; mpu.getEvent(&a,&g,&temp);

  int micRaw = analogRead(MIC_AO_PIN);
  micSmooth  = micAlpha*micRaw + (1.0f - micAlpha)*micSmooth;
  int micLvl = int(micSmooth * micScale);

  float az = a.acceleration.z;
  baselineAz = baseAlpha*az + (1.0f - baseAlpha)*baselineAz;
  float rawDelta = az - baselineAz;
  deltaAz = deltaAlpha*rawDelta + (1.0f - deltaAlpha)*deltaAz;

  // Debug print
  Serial.printf("micRaw=%d micSmooth=%.1f micLvl=%d | ΔZ=%.4f\n",
                micRaw, micSmooth, micLvl, deltaAz);

  // ---- Edge triggers ----
  bool micFire = false;
  if (micArmed){
    if (micLvl >= micThreshHigh){ micFire=true; micArmed=false; }
  } else {
    if (micLvl <= micThreshLow) micArmed=true;
  }

  float absDZ = fabsf(deltaAz);
  bool azFire = false;
  if (azArmed){
    if (absDZ >= azThreshHigh){ azFire=true; azArmed=false; }
  } else {
    if (absDZ <= azThreshLow) azArmed=true;
  }

  uint32_t nowMs = millis();

  // ---- Event clustering ----
  if (!inEvent){
    if ((micFire || azFire) && timeIsSynced() && !rateLimited()){
      inEvent = true;
      eventStartMs = nowMs;
      time_t epoch = time(nullptr);
      eventStartEpoch = epoch;
      struct tm tm_; localtime_r(&epoch,&tm_);
      strftime(eventStartTs, sizeof(eventStartTs), "%Y-%m-%d %H:%M:%S", &tm_);
      peakAbsDeltaAz = absDZ;
      peakMic        = micLvl;
      Serial.printf("[EVENT] start @ %s (mic=%d, |dZ|=%.3f)\n",
                    eventStartTs, micLvl, absDZ);
    }
  } else {
    if (absDZ > peakAbsDeltaAz) peakAbsDeltaAz = absDZ;
    if (micLvl > peakMic)       peakMic = micLvl;

    if (nowMs - eventStartMs >= EVENT_WINDOW_MS){
      Spike s;
      s.epoch = eventStartEpoch;
      strncpy(s.ts, eventStartTs, sizeof(s.ts)); s.ts[sizeof(s.ts)-1]='\0';

      // === NEW LOGIC: always report peaks, filter tiny noise ===
      s.mic = (peakMic > micThreshLow) ? peakMic : 0;
      s.azScaled = (peakAbsDeltaAz > azThreshLow)
                     ? peakAbsDeltaAz * azAmplify + azOffset
                     : 0.0f;

      s.readyMs  = nowMs + 1000;
      s.attempts = 0;

      if (enqueueSpike(s)){
        noteEventSent();
        Serial.printf("[EVENT] send mic=%d, |dZ|=%.3f @ %s\n",
                      s.mic, peakAbsDeltaAz, eventStartTs);

        // >>> Zero row to reset baseline
        Spike z;
        z.epoch = eventStartEpoch;
        strncpy(z.ts, eventStartTs, sizeof(z.ts));
        z.ts[sizeof(z.ts)-1]='\0';
        z.azScaled = 0.0f;
        z.mic      = 0;
        z.readyMs  = nowMs + 1500;
        z.attempts = 0;
        enqueueSpike(z);

      } else {
        Serial.println("[QUEUE] Full, dropping event");
      }
      inEvent = false;
    }
  }

  processQueue();
  delay(50); // ~20 Hz
}

Credits

Ruben Zilzer
7 projects • 7 followers
For nearly four decades I developed, supported, and taught digital media Now retired, I keep creating, experimenting and sharing projects.

Comments