#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
}
Comments