Pin Reset after new firmware

I am trying to add a couple tracker features to one of my devices I have set up to monitor a microphone. I originally had an SD card reader for some data collection that I am no longer using. When I flash just the sound monitoring code I have no problems with the device and it does everything I expect. However, when I flash the code below it keeps resetting it self showing a “pin reset”. I cut the power and ground to the SD card reader, but I am still having the pin reset issue. I am not sure where to go from here.

The features I am adding are a GPS sync using the button on the side of the monitor one. I am also not sure if there is a better way to add these feature than starting with the tracker firmware and adding in sound monitoring code.

If anyone has an examples on bringing in the just the button and GNSS features from the tracker.h instead of what I did, I would love to see it.

Thanks in advanced

#include "Particle.h"
#include "FftComplex.h"
#include <complex>
#include <vector>
#include <cmath>

// Tracker Edge (release/v18)
#include "Tracker_Config_User.h"
#include "tracker_config.h"
#include "tracker.h"
#include "location_service.h"

SYSTEM_THREAD(ENABLED);
SYSTEM_MODE(SEMI_AUTOMATIC);

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

// ---------- logging ----------
SerialLogHandler logHandler(LOG_LEVEL_INFO);

// ---------- sampling / timing ----------
constexpr uint32_t SAMPLE_FREQ        = 8000;
constexpr size_t   NUM_SAMPLES        = 1024;
constexpr uint32_t SAMPLE_INTERVAL_US = 1000000UL / SAMPLE_FREQ;
constexpr unsigned long WINDOW_MS     = 2000;

// ---------- publish interval ----------
unsigned long pubIntervalMin = 10UL;
unsigned long pubIntervalMs  = pubIntervalMin * 60000UL;

// ---------- FFT bands ----------
double lowHz1  = 900.0,  highHz1 = 1200.0;
double lowHz2  = 1900.0, highHz2 = 2300.0;

// ---------- analog cleanup ----------
double cleanupHz = 3000.0;
static float lp_prev = 0.0f;
static float lp_alpha = 0.0f;

// ---------- pins ----------
constexpr int MIC_PIN  = A0;
constexpr int GAIN_PIN = A1;

// ---------- buffers & state ----------
static uint16_t rawBuffer[NUM_SAMPLES];
static float    floatBuffer[NUM_SAMPLES];
std::vector<std::complex<double>> fftBuf(NUM_SAMPLES);

double winMax1=0, winFreq1=0, winMax2=0, winFreq2=0;
double pubMax1=0, pubFreq1=0, pubMax2=0, pubFreq2=0;

unsigned long windowStart=0, publishStart=0;
uint32_t lastSampleTime=0;
bool samplingEnabled = true;
int  gainState = 1;

// ---------- LED helpers ----------
static inline void led_take(){ RGB.control(true); RGB.brightness(255); }
static inline void led_free(){ RGB.control(false); }
static inline void led_red(){ RGB.color(255,0,0); }
static inline void led_green(){ RGB.color(0,255,0); }
static inline void led_off(){ RGB.color(0,0,0); }
static inline void led_yellow(){ RGB.color(255,255,0); }

// ---------- GPS state ----------
enum class GpsMode : uint8_t { Idle, Seeking };
GpsMode gpsMode = GpsMode::Idle;

LocationService& GPS = LocationService::instance();
static LocationPoint lastFix;

unsigned long gpsSeekStartMs = 0;
constexpr unsigned long GPS_TIMEOUT_MS = 5UL * 60UL * 1000UL;

// triple-press
uint8_t pressAggCount = 0;
unsigned long pressWindowStartMs = 0;
constexpr unsigned long PRESS_WINDOW_MS = 1500;
constexpr uint8_t      PRESS_TRIGGER    = 3;

// ---------- fwd decl ----------
static inline void computeAlpha();
static inline void peakInBand(const std::vector<std::complex<double>>&, size_t, double, double, double&, size_t&);
void gpsEnterSeek(); void gpsExitSeek(bool hadFix); bool gpsHasFix(); void handleButtonPressAggregation();

// ---------- helpers ----------
static inline void computeAlpha(){
    float dt = 1.0f / SAMPLE_FREQ;
    float RC = 1.0f / (2.0f * M_PI * cleanupHz);
    lp_alpha = dt / (RC + dt);
}
static inline void peakInBand(const std::vector<std::complex<double>>& X, size_t N, double lo, double hi, double& outMag, size_t& outBin){
    size_t s = (size_t)(lo * N / SAMPLE_FREQ);
    size_t e = (size_t)(hi * N / SAMPLE_FREQ);
    if (e >= N) e = N - 1;
    double m=0; size_t mb=s;
    for (size_t k=s; k<=e; ++k){ double a=std::abs(X[k]); if (a>m){m=a; mb=k;} }
    outMag=m; outBin=mb;
}

// ---------- cloud API ----------
int startSampling(const String&){ if (gpsMode==GpsMode::Idle) samplingEnabled=true; return 1; }
int stopSampling (const String&){ samplingEnabled=false; return 1; }
int setB1L(String s){ double v=s.toFloat(); if(v>0 && v<highHz1){lowHz1=v; return 1;} return -1; }
int setB1H(String s){ double v=s.toFloat(); if(v>lowHz1 && v<SAMPLE_FREQ/2){highHz1=v; return 1;} return -1; }
int setB2L(String s){ double v=s.toFloat(); if(v>0 && v<highHz2){lowHz2=v; return 1;} return -1; }
int setB2H(String s){ double v=s.toFloat(); if(v>lowHz2 && v<SAMPLE_FREQ/2){highHz2=v; return 1;} return -1; }
int setCleanup(String s){ double v=s.toFloat(); if(v>100.0 && v<SAMPLE_FREQ/2){cleanupHz=v; computeAlpha(); return 1;} return -1; }
int setPubInterval(String s){ unsigned long v=s.toInt(); if(v>=1 && v<=1440){pubIntervalMin=v; pubIntervalMs=v*60000UL; return 1;} return -1; }
int setGain(String s){ String cmd=s; cmd.toLowerCase(); if(cmd=="high"||s.toInt()==1){digitalWrite(GAIN_PIN,HIGH); gainState=1; return 1;} if(cmd=="low"||s.toInt()==0){digitalWrite(GAIN_PIN,LOW); gainState=0; return 1;} return -1; }
int gpsStartFn(const String&){ gpsEnterSeek(); return 1; }
int gpsStopFn (const String&){ gpsExitSeek(false); return 1; }

// ---------- setup ----------
void setup(){
    Serial.begin(115200);
    waitFor(Serial.isConnected, 2000);

    pinMode(GAIN_PIN, OUTPUT);
    digitalWrite(GAIN_PIN, HIGH);

    Particle.variable("lowHz1", lowHz1);
    Particle.variable("highHz1", highHz1);
    Particle.variable("lowHz2", lowHz2);
    Particle.variable("highHz2", highHz2);
    Particle.variable("cleanupHz", cleanupHz);
    Particle.variable("pubIntervalMin", pubIntervalMin);
    Particle.variable("gainState", gainState);

    Particle.function("start", startSampling);
    Particle.function("stop",  stopSampling);
    Particle.function("setB1L", setB1L);
    Particle.function("setB1H", setB1H);
    Particle.function("setB2L", setB2L);
    Particle.function("setB2H", setB2H);
    Particle.function("setCleanup", setCleanup);
    Particle.function("setPubInterval", setPubInterval);
    Particle.function("setGain", setGain);
    Particle.function("gpsStart", gpsStartFn);
    Particle.function("gpsStop",  gpsStopFn);

    fftBuf.resize(NUM_SAMPLES);
    computeAlpha();

    windowStart  = millis();
    publishStart = millis();

    Particle.connect();
    waitFor(Particle.connected, 15000);

    // Tracker services (repo must be on release/v18 with submodules)
    Tracker::instance().init();
    //GPS.begin();
}

// ---------- loop ----------
void loop(){
    handleButtonPressAggregation();

    // --- GPS seeking state ---
    if (gpsMode == GpsMode::Seeking) {
        led_take(); led_red();
        GPS.start();

        if (gpsHasFix()){
            char payload[160];
            snprintf(payload, sizeof(payload),
            "{\"lat\":%.7f,\"lon\":%.7f,\"alt\":%.1f}",
            lastFix.latitude, lastFix.longitude, lastFix.altitude);
            Particle.publish("gpsLock", payload, PRIVATE);
            led_green(); delay(3000);
            gpsExitSeek(true);
        } else if (millis() - gpsSeekStartMs >= GPS_TIMEOUT_MS) {
            for (int i=0;i<3;i++){ led_yellow(); delay(200); led_off(); delay(200); }
            gpsExitSeek(false);
        }
    }

    // --- FFT pipeline (paused during GPS seeking) ---
    if (samplingEnabled && gpsMode == GpsMode::Idle){
        for (size_t i=0;i<NUM_SAMPLES;i++){
            while ((uint32_t)(micros()-lastSampleTime) < SAMPLE_INTERVAL_US) {}
            lastSampleTime = micros();
            rawBuffer[i] = analogRead(MIC_PIN);
        }

        for (size_t i=0;i<NUM_SAMPLES;i++){
            float centered = (float)rawBuffer[i] - 2048.0f;
            lp_prev = lp_prev + lp_alpha * (centered - lp_prev);
            floatBuffer[i] = lp_prev;
        }

        double sum=0; for(size_t i=0;i<NUM_SAMPLES;i++) sum += floatBuffer[i];
        double mean = sum / NUM_SAMPLES;
        for (size_t i=0;i<NUM_SAMPLES;i++) fftBuf[i] = { (double)floatBuffer[i]-mean, 0.0 };

        Fft::transformRadix2(fftBuf);

        double mag1=0, mag2=0; size_t bin1=0, bin2=0;
        peakInBand(fftBuf, NUM_SAMPLES, lowHz1, highHz1, mag1, bin1);
        peakInBand(fftBuf, NUM_SAMPLES, lowHz2, highHz2, mag2, bin2);
        double freq1 = (double)bin1 * SAMPLE_FREQ / NUM_SAMPLES;
        double freq2 = (double)bin2 * SAMPLE_FREQ / NUM_SAMPLES;

        if (mag1>winMax1){ winMax1=mag1; winFreq1=freq1; }
        if (mag2>winMax2){ winMax2=mag2; winFreq2=freq2; }
    }

    if (millis() - windowStart >= WINDOW_MS){
        if (winMax1>pubMax1){ pubMax1=winMax1; pubFreq1=winFreq1; }
        if (winMax2>pubMax2){ pubMax2=winMax2; pubFreq2=winFreq2; }
        winMax1=winFreq1=0; winMax2=winFreq2=0;
        windowStart = millis();
    }

    if (millis() - publishStart >= pubIntervalMs){
        char payload[192];
        snprintf(payload, sizeof(payload),
            "{\"b1_amp\":%.3f,\"b1_freq\":%.1f,\"b2_amp\":%.3f,\"b2_freq\":%.1f,"
            "\"cleanup\":%.0f,\"bands\":\"%.0f/%.0f|%.0f/%.0f\"}",
            pubMax1, pubFreq1, pubMax2, pubFreq2, cleanupHz,
            lowHz1, highHz1, lowHz2, highHz2);
        Particle.publish("bandPeak10m", payload, PRIVATE);
        pubMax1=pubFreq1=0; pubMax2=pubFreq2=0;
        publishStart = millis();
    }

    Particle.process();
}

// ---------- GPS helpers ----------
bool gpsHasFix() {
    LocationPoint loc;
    // release/v18: one-arg overload
    if (!GPS.getLocation(loc)) return false;

    // Optional: sanity checks instead of isValid()
    if (!std::isfinite(loc.latitude) || !std::isfinite(loc.longitude)) return false;

    lastFix = loc;
    return true;
}
void gpsEnterSeek(){
    if (gpsMode == GpsMode::Seeking) return;
    samplingEnabled = false;
    gpsSeekStartMs = millis();
    gpsMode = GpsMode::Seeking;
}
void gpsExitSeek(bool){
    GPS.stop();
    led_off(); led_free();
    gpsMode = GpsMode::Idle;
    samplingEnabled = true;
}

// ---------- triple-press ----------
void handleButtonPressAggregation(){
    int presses = System.buttonPushed();
    if (presses > 0){
        unsigned long now = millis();
        if (pressAggCount == 0) pressWindowStartMs = now;
        pressAggCount += presses;
        if (pressAggCount >= PRESS_TRIGGER && (now - pressWindowStartMs) <= PRESS_WINDOW_MS){
            gpsEnterSeek();
            pressAggCount = 0; pressWindowStartMs = 0;
            return;
        }
    }
    if (pressAggCount > 0 && (millis() - pressWindowStartMs) > PRESS_WINDOW_MS){
        pressAggCount = 0; pressWindowStartMs = 0;
    }
}

Here is what the serial monitor showing. It is getting hung up somewhere. I even removed the board and it is still happening.

0000001244 [system.nm] INFO: State changed: DISABLED -> IFACE_DOWN
0000001246 [system.nm] INFO: State changed: IFACE_DOWN -> IFACE_REQUEST_UP
0000001322 [mux] INFO: Starting GSM07.10 muxer
0000001323 [mux] INFO: GSM07.10 muxer thread started
0000001323 [mux] INFO: Opening mux channel 0
0000001324 [system.nm] INFO: State changed: IFACE_REQUEST_UP -> IFACE_UP
0000001374 [mux] INFO: Opening mux channel 1
0000001378 [mux] INFO: Mux channel 1 already opened
0000001427 [mux] INFO: Opening mux channel 2
0000002620 [system.nm] INFO: State changed: IFACE_UP -> IFACE_LINK_UP
0000002623 [system.nm] INFO: State changed: IFACE_LINK_UP -> IP_CONFIGURED
0000002628 [system] INFO: Cloud: connecting
0000002633 [system] INFO: Cloud socket connected
0000002634 [comm.protocol.handshake] INFO: Establish secure connection
0000002640 [comm.dtls] INFO: session has 0 uses
0000002658 [comm.dtls] INFO: (CMPL,RENEG,NO_SESS,ERR) restoreStatus=0
0000002659 [comm.dtls] INFO: out_ctr 0,1,0,0,0,0,0,217, next_coap_id=bd
0000002660 [comm.dtls] INFO: restored session from persisted session data. next_msg_id=189
0000002662 [comm.dtls] INFO: session cmd (CLS,DIS,MOV,LOD,SAV): 2
0000004694 [comm.dtls] INFO: session cmd (CLS,DIS,MOV,LOD,SAV): 4
0000004695 [comm.dtls] INFO: session cmd (CLS,DIS,MOV,LOD,SAV): 3
0000004696 [comm.protocol.handshake] INFO: Sending HELLO message
0000009738 [comm.dtls] INFO: session cmd (CLS,DIS,MOV,LOD,SAV): 4
0000009739 [comm.protocol.handshake] INFO: Handshake completed
0000009740 [comm.dtls] INFO: session cmd (CLS,DIS,MOV,LOD,SAV): 4
0000009742 [comm.dtls] INFO: session cmd (CLS,DIS,MOV,LOD,SAV): 3
0000011794 [comm.protocol] INFO: Sending subscriptions
0000012443 [comm.protocol] INFO: Received TIME response: 1756594796
0000012473 [comm.dtls] INFO: session cmd (CLS,DIS,MOV,LOD,SAV): 4
0000012474 [comm.dtls] INFO: session cmd (CLS,DIS,MOV,LOD,SAV): 3
0000012495 [comm.dtls] INFO: session cmd (CLS,DIS,MOV,LOD,SAV): 4
0000012496 [comm.dtls] INFO: session cmd (CLS,DIS,MOV,LOD,SAV): 3
0000016144 [app] INFO: Tracker model = 0003, variant = 0003
0000016356 [app] INFO: loading config sleep: {"version":1,"hash":"2A39C096BB63BEB687E5FC8D0B4BE8C2","sleep":{"mode":"enable","exe_min":10,"conn_max":90}}
0000016367 [app] INFO: loading config tracker: {"version":1,"hash":"F53736A0E3B271B9273988F4273988F4","tracker":{"usb_cmd":true}}
0000016629 [app] INFO: loading config location: {"version":1,"hash":"A2727250BC9CE43FE693AC9A66D09E1C","location":{"radius":0.0000000000,"interval_min":900,"interval_max":3600,"min_~
0000016644 [app] INFO: loading config geofence: {"version":1,"hash":"AF0C5CD528BF5B21C07381B23E6FE552","geofence":{"interval":0,"zone1":{"enable":false,"lat":0.0000000000,"lon":0.00~
0000016669 [app] INFO: loading config imu_trig: {"version":1,"hash":"5E5488D40858A17D5C214BA3319647FD","imu_trig":{"motion":"disable","high_g":"disable"}}
0000016691 [app] INFO: loading config store: {"version":1,"hash":"98B78836A21601240044846E79B8304A","store":{"enable":false,"quota":64,"policy":"drop_old"}}

It looks like you are using tracker-edge as the base, but it looks like your device is a Monitor One. You should use the monitor-edge firmware as the base.

I believe that the problem is that since you call Tracker::instance().init(); the Tracker functions are enabled. The reason you are getting a pin reset is because you don't call Tracker::instance().loop() from loop. This prevents the hardware watchdog from being serviced, which causes the device to reset using pin reset from the external hardware watchdog (AM1805).

The actual functions you should use are Edge::instance().init() and Edge::instance().loop() on the Monitor One.

The Edge firmware is only necessary if you want to use the Edge features like cloud configuration and initialization of the built-in peripherals. If you want to use parts of the hardware, the best solution is to base your firmware on the monitor-edge firmware so you get all of the libraries, then don't call Edge::instance().init() or Edge::instance().loop(). Instead, look inside of those functions and directly initialize the libraries for the hardware you want to use instead of the entire library.

1 Like

Hey Rick. I gave it my college best but I am still running into issues using the monitor one firmware for my project. So I pulled in the monitor one firmware from Git and added my own .cpp. What I am attempting to do is once the device is powered on, the user hits the external button 3 times and it attempts to get a GPS lock. Then once the GPS lock is done, then I would like the device to start my microphone code. Since I am using the expansion board, I know I need to stop SPI or Wire depending on which GPIO’s I want to steal. Here are issues/error’s I am getting that have stumped me all week.

  1. Device Resets even without a microphone attached and shows this error in the log. “[MonitorOne] ERROR: This is not the card you are looking for”. I assume I have the {"EXP1_IO_BASIC_485CAN"} that I see in the monitor_edge_io expansion.h file.
  2. I can struggling to find a way to bring in "edge_gnss_abstraction.h" for the GPS detection and leave the rest of edge.h out of it. I am pretty sure when I include Edge that all my GPIO’s are now tied up. Which would explain the extremely high amplitudes I was reading.
  3. Is there a way to free up any two GPIO’s when calling Edge.h? I was using A0,A1 before. Or what can I delete from the monitor one package that will give me what I need.

I appreciate any idea’s or examples you can share with me. I am stuck at this point. My code is below.
Thanks in advance,

Jon

// Bolt_Monitor_GPS.cpp
// Idle at boot. Triple-press (D2) starts GNSS seek.
// On valid lock: publish gpsLock once, then start FFT + periodic bandPeak publishes.

// --- Tell Monitor Edge which IO expansion we have (prevents "not the card") ---
//#define MONITOREDGE_IOEX_SKU "EXP1_IO_BASIC_485CAN"
// Optional extra guard. Harmless if the card is present.
//#define MONITORONE_DISABLE_CARD_DETECT 1

#include "Particle.h"
#include "FftComplex.h"
#include <complex>
#include <vector>
#include <cmath>

// --- Tracker/Monitor Edge ---
#include "edge.h"
#include "edge_sleep.h"
#include "edge_gnss_abstraction.h"

// Block STS3x auto-probe so no I2C service starts accidentally on Monitor One
#undef  MONITORONE_STS3X_I2C_ADDR
#define MONITORONE_STS3X_I2C_ADDR 0x00

SYSTEM_THREAD(ENABLED);
SYSTEM_MODE(SEMI_AUTOMATIC);

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

SerialLogHandler logHandler(LOG_LEVEL_INFO);

// ---------- BUTTON: D2 active-LOW ----------
constexpr pin_t BUTTON_PIN = D2;
static const unsigned long DEBOUNCE_MS     = 30;
static const unsigned long PRESS_WINDOW_MS = 1500;
static const uint8_t      PRESS_TRIGGER    = 3;

static int           btnPrev            = HIGH;
static unsigned long btnLastChangeMs    = 0;
static unsigned long clickWindowStartMs = 0;
static uint8_t       clickCount         = 0;
volatile bool        g_btnTripleRequested = false;

static inline void pollButton() {
    int raw = digitalRead(BUTTON_PIN);
    unsigned long now = millis();

    if (raw != btnPrev && (now - btnLastChangeMs) < DEBOUNCE_MS) return;

    if (raw != btnPrev) {
        btnLastChangeMs = now;

        // count on release (LOW->HIGH)
        if (btnPrev == LOW && raw == HIGH) {
            if (clickCount == 0) {
                clickWindowStartMs = now;
                Serial.println("[BTN] window start");
            }
            clickCount++;
            Serial.printf("[BTN] click #%u (window %lu ms)\n",
                          clickCount, (unsigned long)(now - clickWindowStartMs));
            if (clickCount >= PRESS_TRIGGER &&
                (now - clickWindowStartMs) <= PRESS_WINDOW_MS) {
                Serial.println("[BTN] triple-press detected!");
                g_btnTripleRequested = true;
                clickCount = 0;
                clickWindowStartMs = 0;
            }
        }
        btnPrev = raw;
    }

    if (clickCount > 0 && (now - clickWindowStartMs) > PRESS_WINDOW_MS) {
        Serial.printf("[BTN] window expired (count=%u)\n", clickCount);
        clickCount = 0;
        clickWindowStartMs = 0;
    }
}

// ---------- sampling / timing ----------
constexpr uint32_t SAMPLE_FREQ        = 8000;
constexpr size_t   NUM_SAMPLES        = 1024;
constexpr uint32_t SAMPLE_INTERVAL_US = 1000000UL / SAMPLE_FREQ;
constexpr unsigned long WINDOW_MS     = 2000;

// ---------- publish interval ----------
unsigned long pubIntervalMin = 10UL;                 // minutes
unsigned long pubIntervalMs  = 10UL * 60000UL;

// ---------- FFT bands ----------
double lowHz1  = 900.0,  highHz1 = 1200.0;
double lowHz2  = 1900.0, highHz2 = 2300.0;

// ---------- analog cleanup (classic LPF path) ----------
double cleanupHz = 3000.0;   // cutoff for the 1-pole LPF
static float lp_prev  = 0.0f;
static float lp_alpha = 0.0f;

// ---------- pins (avoid A6/A7/D8/D9 used by the IO card) ----------
constexpr int MIC_PIN  = A4;   // free analog
constexpr int GAIN_PIN = D6;   // digital, not used by IO card

// ---------- buffers & state ----------
static uint16_t rawBuffer[NUM_SAMPLES];
static float    floatBuffer[NUM_SAMPLES];
std::vector<std::complex<double>> fftBuf(NUM_SAMPLES);

// --- publish/sampling gate ---
static bool          pubInFlight = false;
static unsigned long samplingResumeAt = 0;

// --- maxima tracking ---
double winMax1=0, winFreq1=0, winMax2=0, winFreq2=0;
double pubMax1=0, pubFreq1=0, pubMax2=0, pubFreq2=0;

unsigned long windowStart=0, publishStart=0;
// start disabled so FFT does NOT run at boot
bool samplingEnabled = false;
int  gainState = 1;

// ---------- sampler scheduler ----------
static uint32_t lastSampleTime = 0;

// ---------- GNSS seek state ----------
static bool          gnssSeekActive = false;
static unsigned long seekStartMs    = 0;
static unsigned long nextGnssLog    = 0;
static const unsigned long GNSS_TIMEOUT_MS = 5UL * 60UL * 1000UL; // 5 min
static const unsigned long GNSS_WARMUP_MS  = 8000;                // ignore first few seconds

// ---------- helpers ----------
static inline void computeAlpha(){
    const float dt = 1.0f / float(SAMPLE_FREQ);
    const float RC = 1.0f / (2.0f * float(M_PI) * float(cleanupHz));
    lp_alpha = dt / (RC + dt);
}

// “field units” amplitude: (2/N) * |X[k]| on single-sided spectrum
static inline void peakInBand(const std::vector<std::complex<double>>& X, size_t N,
                              double lo, double hi, double& outAmp, size_t& outBin)
{
    const size_t kMin = 1;
    const size_t kMax = (N/2) - 1;

    size_t s = (size_t)floor(lo * N / SAMPLE_FREQ);
    size_t e = (size_t)ceil (hi * N / SAMPLE_FREQ);
    if (s < kMin) s = kMin;
    if (e > kMax) e = kMax;

    double m = 0.0; size_t mb = s;
    for (size_t k = s; k <= e; ++k) {
        double a = (2.0 / (double)N) * std::abs(X[k]);
        if (!std::isfinite(a)) a = 0.0;
        if (a > m) { m = a; mb = k; }
    }
    outAmp = m;
    outBin = mb;
}

static inline bool isValidFix(const LocationPoint& pt) {
    return (pt.locked != 0) &&
           (pt.horizontalAccuracy > 0.0 && pt.horizontalAccuracy <= 100.0) &&
           (pt.satsInUse >= 4);
}

// ---------- GNSS control ----------
static void startGnssSeek() {
    if (gnssSeekActive) return;

    samplingEnabled = false;                // ensure FFT is paused

    // Initialize core Edge once (safe to call again)
    Edge::instance().init();

    // Hard-disable sleep policy so nothing reboots us
    EdgeSleep::instance().pauseSleep();

    // Start GNSS driver
    EdgeGnssAbstraction::instance().start();

    seekStartMs = millis();
    nextGnssLog = 0;
    gnssSeekActive = true;

    RGB.control(true); RGB.brightness(255);
    RGB.color(255, 255, 0); // yellow = seeking
    Serial.println("[GNSS] seek START");
}

static void stopGnssSeek(bool hadFix) {
    // Turn GNSS off cleanly
    Log.info("Turning GNSS off");
    EdgeGnssAbstraction::instance().stop();

    gnssSeekActive = false;

    if (hadFix) {
        Serial.println("[GNSS] seek STOP (FIX acquired)");
        RGB.color(0, 255, 0);
        delay(200);
        samplingEnabled = true;   // enable FFT after a good fix
    } else {
        Serial.println("[GNSS] seek STOP (TIMEOUT / no fix)");
        RGB.color(255, 128, 0);
        delay(200);
        samplingEnabled = false;  // remain idle
    }
    RGB.color(0, 0, 255);
}

// 1 Hz GNSS poll + sat prints (LocationPoint only; no LocationStatus)
static void pollGnssOnce() {
    if (millis() < nextGnssLog) return;
    nextGnssLog = millis() + 1000;

    unsigned long elapsed = millis() - seekStartMs;

    LocationPoint pt{};
    int rcLoc = EdgeGnssAbstraction::instance().getLocation(pt);

    if (rcLoc == SYSTEM_ERROR_NONE) {
        if (isValidFix(pt) && elapsed >= GNSS_WARMUP_MS) {
            Serial.printf("[GNSS] LOCKED lat=%.7f lon=%.7f alt=%.1f "
                          "hAcc=%.1f vAcc=%.1f satsUse=%u satsView=%u\n",
                          pt.latitude, pt.longitude, pt.altitude,
                          pt.horizontalAccuracy, pt.verticalAccuracy,
                          pt.satsInUse, pt.satsInView);

            // One-time publish on lock (if cloud is up)
            if (Particle.connected()) {
                char payload[256];
                snprintf(payload, sizeof(payload),
                         "{\"lat\":%.7f,\"lon\":%.7f,\"alt\":%.1f,"
                         "\"hacc\":%.1f,\"vacc\":%.1f,"
                         "\"satsUse\":%u,\"satsView\":%u}",
                         pt.latitude, pt.longitude, pt.altitude,
                         pt.horizontalAccuracy, pt.verticalAccuracy,
                         pt.satsInUse, pt.satsInView);
                Particle.publish("gpsLock", payload, PRIVATE);
            }

            stopGnssSeek(true);
            return;
        } else {
            Serial.printf("[GNSS] seeking… t=%lus hAcc=%.1f vAcc=%.1f satsUse=%u satsView=%u\n",
                          (unsigned long)(elapsed/1000),
                          pt.horizontalAccuracy, pt.verticalAccuracy,
                          pt.satsInUse, pt.satsInView);
        }
    } else {
        Serial.printf("[GNSS] seeking… t=%lus (getLocation rc=%d)\n",
                      (unsigned long)(elapsed/1000), rcLoc);
    }

    if (elapsed >= GNSS_TIMEOUT_MS) {
        stopGnssSeek(false);
    }
}

// ---------- cloud API ----------
int startSamplingFn(const String&){ if (!gnssSeekActive) samplingEnabled=true; return 1; }
int stopSamplingFn (const String&){ samplingEnabled=false; return 1; }
int setB1L(String s){ double v=s.toFloat(); if(v>0 && v<highHz1){lowHz1=v; return 1;} return -1; }
int setB1H(String s){ double v=s.toFloat(); if(v>lowHz1 && v<SAMPLE_FREQ/2){highHz1=v; return 1;} return -1; }
int setB2L(String s){ double v=s.toFloat(); if(v>0 && v<highHz2){lowHz2=v; return 1;} return -1; }
int setB2H(String s){ double v=s.toFloat(); if(v>lowHz2 && v<SAMPLE_FREQ/2){highHz2=v; return 1;} return -1; }
int setCleanup(String s){ double v=s.toFloat(); if(v>100.0 && v<SAMPLE_FREQ/2){cleanupHz=v; computeAlpha(); return 1;} return -1; }
int setPubInterval(String s){ unsigned long v=s.toInt(); if(v>=1 && v<=1440){pubIntervalMin=v; pubIntervalMs=v*60000UL; return 1;} return -1; }
int setGain(String s){
    String cmd=s; cmd.toLowerCase();
    if(cmd=="high"||s.toInt()==1){digitalWrite(GAIN_PIN,HIGH); gainState=1; return 1;}
    if(cmd=="low" ||s.toInt()==0){digitalWrite(GAIN_PIN,LOW ); gainState=0; return 1;}
    return -1;
}
int gpsStartFn(const String&){ startGnssSeek(); return 1; }
int gpsStopFn (const String&){ stopGnssSeek(false); return 1; }

// ---------- setup ----------
static const char* rrStr() {
    switch (System.resetReason()) {
        case RESET_REASON_UNKNOWN:        return "UNKNOWN";
        case RESET_REASON_POWER_DOWN:     return "POWER_DOWN";
        case RESET_REASON_PIN_RESET:      return "PIN_RESET";
        case RESET_REASON_WATCHDOG:       return "WATCHDOG";
        case RESET_REASON_CONFIG_UPDATE:  return "SOFTWARE";
        case RESET_REASON_POWER_BROWNOUT: return "BROWNOUT";
        default:                          return "OTHER";
    }
}

void setup(){
    Serial.begin(115200);
    waitFor(Serial.isConnected, 2000);
    Serial.printf("[BOOT] resetReason=%s (%d)\n", rrStr(), (int)System.resetReason());

    // Button
    pinMode(BUTTON_PIN, INPUT_PULLUP);
    btnPrev         = digitalRead(BUTTON_PIN);
    btnLastChangeMs = millis();

    // RGB
    RGB.control(true);
    RGB.brightness(255);
    RGB.color(0,0,255); // idle blue

    // Pins
    pinMode(MIC_PIN, INPUT);
    pinMode(GAIN_PIN, OUTPUT);
    digitalWrite(GAIN_PIN, HIGH);
    gainState = 1;

    // Filters & buffers
    computeAlpha();
    fftBuf.resize(NUM_SAMPLES);

    // Timers
    windowStart    = millis();
    publishStart   = millis();
    lastSampleTime = micros();

    SPI.end(); // keep shared buses quiet

    // Cloud vars / funcs (safe to expose even when offline)
    Particle.variable("lowHz1", lowHz1);
    Particle.variable("highHz1", highHz1);
    Particle.variable("lowHz2", lowHz2);
    Particle.variable("highHz2", highHz2);
    Particle.variable("cleanupHz", cleanupHz);
    Particle.variable("pubIntervalMin", pubIntervalMin);
    Particle.variable("gainState", gainState);

    Particle.function("start", startSamplingFn);
    Particle.function("stop",  stopSamplingFn);
    Particle.function("setB1L", setB1L);
    Particle.function("setB1H", setB1H);
    Particle.function("setB2L", setB2L);
    Particle.function("setB2H", setB2H);
    Particle.function("setCleanup", setCleanup);
    Particle.function("setPubInterval", setPubInterval);
    Particle.function("setGain", setGain);
    Particle.function("gpsStart", gpsStartFn);
    Particle.function("gpsStop",  gpsStopFn);

    // Bring up modem/cloud at boot if you like; or wait until after GNSS lock.
    Cellular.on();
    Cellular.connect();
    waitFor(Cellular.ready, 120000);
    Particle.keepAlive(25 * 60);
    Particle.connect();
    waitFor(Particle.connected, 60000);

    // Initialize Edge and **pause sleep** so policy cannot reboot the device.
    Edge::instance().init();
    EdgeSleep::instance().pauseSleep();

    // Ensure GNSS idle at boot
    EdgeGnssAbstraction::instance().stop();

    samplingEnabled = false; // wait for GNSS lock
    Serial.println("[BOOT] ready; triple-press D2 to start GNSS seek");
}

// ---------- loop ----------
void loop() {
    // Keep sleep disabled (belt + suspenders)
    static unsigned long lastSleepKill = 0;
    if (millis() - lastSleepKill > 2000) {
        lastSleepKill = millis();
        EdgeSleep::instance().pauseSleep();
    }

    // Button
    pollButton();
    if (g_btnTripleRequested && !gnssSeekActive) {
        g_btnTripleRequested = false;
        startGnssSeek();
    }

    // GNSS (must run Edge loop while seeking)
    if (gnssSeekActive) {
        Edge::instance().loop();
        pollGnssOnce();
    }

    // FFT (runs only if samplingEnabled == true)
    if (samplingEnabled) {
        // 1) Sample @ uniform intervals
        for (size_t i = 0; i < NUM_SAMPLES; i++) {
            if ((int32_t)(micros() - lastSampleTime) > (int32_t)(2 * SAMPLE_INTERVAL_US)) {
                lastSampleTime = micros();
            }
            while (micros() - lastSampleTime < SAMPLE_INTERVAL_US) { Particle.process(); }
            lastSampleTime = micros();
            rawBuffer[i]   = analogRead(MIC_PIN);
        }

        // 2) One-pole LPF cleanup
        for (size_t i = 0; i < NUM_SAMPLES; i++) {
            float centered = (float)rawBuffer[i] - 2048.0f;
            float y = (lp_alpha > 0.0f && lp_alpha < 1.0f)
                        ? (lp_prev + lp_alpha * (centered - lp_prev))
                        : centered;
            lp_prev = y;
            floatBuffer[i] = y;
        }

        // 3) Remove DC & build complex FFT vector
        double sum = 0;
        for (size_t i = 0; i < NUM_SAMPLES; i++) sum += floatBuffer[i];
        const double mean = sum / NUM_SAMPLES;
        for (size_t i = 0; i < NUM_SAMPLES; i++) {
            fftBuf[i] = std::complex<double>(floatBuffer[i] - mean, 0.0);
        }

        // 4) FFT
        Fft::transformRadix2(fftBuf);

        // 5) Peaks
        double mag1=0, mag2=0; size_t bin1=0, bin2=0;
        peakInBand(fftBuf, NUM_SAMPLES, lowHz1, highHz1, mag1, bin1);
        peakInBand(fftBuf, NUM_SAMPLES, lowHz2, highHz2, mag2, bin2);
        const double freq1 = (double)bin1 * SAMPLE_FREQ / NUM_SAMPLES;
        const double freq2 = (double)bin2 * SAMPLE_FREQ / NUM_SAMPLES;

        // 6) rolling maxima over 2 s
        if (mag1 > winMax1){ winMax1 = mag1; winFreq1 = freq1; }
        if (mag2 > winMax2){ winMax2 = mag2; winFreq2 = freq2; }

        // debug every ~2 s
        static unsigned long lastDbg = 0;
        if (millis() - lastDbg >= 2000) {
            Serial.printf("[DBG] b1_amp=%.3f @ %.1f Hz | b2_amp=%.3f @ %.1f Hz\r\n",
                          mag1, freq1, mag2, freq2);
            lastDbg = millis();
        }
    }

    // 2 s window folding only when sampling is active
    if (samplingEnabled && (millis() - windowStart >= WINDOW_MS)) {
        if (winMax1 > pubMax1){ pubMax1 = winMax1; pubFreq1 = winFreq1; }
        if (winMax2 > pubMax2){ pubMax2 = winMax2; pubFreq2 = winFreq2; }
        winMax1 = winFreq1 = 0.0;
        winMax2 = winFreq2 = 0.0;
        windowStart = millis();
    }

    // Publish once per interval — only if sampling active and not seeking
    if (samplingEnabled && !gnssSeekActive && (millis() - publishStart >= pubIntervalMs)) {
        publishStart = millis();

        if (Particle.connected()) {
            pubInFlight = true;
            samplingEnabled = false;

            char payload[192];
            snprintf(payload, sizeof(payload),
                "{\"b1_amp\":%.3f,\"b1_freq\":%.1f,"
                "\"b2_amp\":%.3f,\"b2_freq\":%.1f,"
                "\"cleanup\":%.0f,"
                "\"bands\":\"%.0f/%.0f|%.0f/%.0f\"}",
                pubMax1, pubFreq1,
                pubMax2, pubFreq2,
                cleanupHz,
                lowHz1, highHz1, lowHz2, highHz2);

            Particle.publish("bandPeak10m", payload, PRIVATE);

            pubMax1 = pubFreq1 = 0.0;
            pubMax2 = pubFreq2 = 0.0;

            samplingResumeAt = millis() + 2500;
        }
    }

    // Resume sampling after publish window
    if (pubInFlight && samplingResumeAt && millis() >= samplingResumeAt) {
        samplingResumeAt = 0;
        pubInFlight = false;
        if (!gnssSeekActive) samplingEnabled = true;
    }

    SPI.end();
    Particle.process();
}

I am getting closer but the pin_reset is still happening. I did go back to the Proto_typing card and am using A4 and A5 for the Mic pin’s. Eric helped me with pulling in libraries I need, but I am stuck on the resets. Any suggestions on what I should do next?

#include "Particle.h"
#include <math.h>          // isnan()
#include "ubloxGPS.h"
#include "tracker_config.h"        // GPS_* pin macros for Monitor One
// ---- Optional button (set USE_USER_BUTTON 1 if you have it) ----
#define USE_USER_BUTTON 1
#if USE_USER_BUTTON
  #include "DebounceSwitchRK.h"
#endif

// ------- FFT stuff -------
#include "FftComplex.h"
#include <complex>
#include <vector>
#include <cmath>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

using namespace std;

SYSTEM_MODE(SEMI_AUTOMATIC);
SYSTEM_THREAD(ENABLED);

// ================== Pins / Hardware ==================
#define GPS_CS_PIN   (GPS_CS)
#define GPS_PWR_PIN  (GPS_PWR)
#define GPS_RST_PIN  (GPS_RST)
#define GPS_INT_PIN  (GPS_INT)
#define GPS_INT_GPS_PIN (14)

// ================== Breadcrumbs / reset reason ==================
retained struct { uint32_t magic, stage, ms; } bc;
enum { ST_BOOT=1, ST_SETUP, ST_GPS_ON, ST_GPS_LOOP, ST_PUBLISH, ST_FFT_SAMPLE, ST_FFT_PUBLISH };
static inline void setStage(uint32_t s){ bc.stage=s; bc.ms=millis(); }
static inline void logReset() {
  int rr = (int)System.resetReason();
  Log.info("Reset reason=%d data=0x%08lx", rr, (unsigned long)System.resetReasonData());
  if (bc.magic==0xA5A5A5A5)
    Log.info("Last boot breadcrumbs: stage=%lu ms=%lu", bc.stage, bc.ms);
  bc.magic = 0xA5A5A5A5;
}

// ----- CAN kept OFF permanently -----
#define USE_CAN 0
#if USE_CAN
  #include "mcp_can.h"
  #define CAN_CS_PIN   (CAN_CS)
  #define CAN_INT_PIN  (CAN_INT)
  #define CAN_RST_PIN  (CAN_RST)
  #define CAN_PWR_PIN  (CAN_PWR)
  #define CAN_STBY_PIN (CAN_STBY)
#endif

// ---- User button fallback / optional ----
#ifndef MONITORONE_USER_BUTTON
  #define MONITORONE_USER_BUTTON D2   // fallback if not provided by headers
#endif
#if USE_USER_BUTTON
  #define USER_BUTTON_PIN (MONITORONE_USER_BUTTON)
#endif

#define MIC_PIN   A5
#define GAIN_PIN  A4  // MAX9814 gain (HIGH=high, LOW=low)

// ================== Logging ==================
// Quiet the GPS driver logs to reduce chatter/back-pressure
SerialLogHandler logHandler(115200, LOG_LEVEL_INFO, {
    {"app.gps.nmea", LOG_LEVEL_WARN},
    {"app.gps.ubx",  LOG_LEVEL_WARN},
});

// ================== GPS model ==================
struct GPSData {
    double latitude = 0.0;
    double longitude = 0.0;
    double altitude = 0.0;
    double speed = 0.0;
    double heading = 0.0;
    bool   locked = false;
    bool   stable = false;
    uint8_t satellites = 0;
    double hdop = 0.0;
    double vdop = 0.0;
    double horizontalAccuracy = 0.0;
    double verticalAccuracy = 0.0;
    time_t utcTime = 0;
};

ubloxGPS *gps = nullptr;
GPSData   gpsData;

// ================== State / LEDs ==================
volatile bool g_requestPublish  = false;  // triple-tap request (or simulateTriple)
bool         g_ledGreenSet      = false;
bool         g_shutdownDone     = false;
bool         g_autoLocPublished = false;

unsigned long g_lastCloudKick = 0;
int           g_reqId = 1;                // increments per Tracker-style publish

inline void ledSearching() { RGB.control(true); RGB.color(255,165,0); } // orange
inline void ledLocked()    { RGB.control(true); RGB.color(0,255,0);   } // green
inline void ledDone()      { RGB.control(true); RGB.color(0,0,255);   } // blue
inline void ledCyan()      { RGB.control(true); RGB.color(0,255,255); } // cyan
inline void ledMagenta()   { RGB.control(true); RGB.color(255,0,255); } // magenta

// ================== FFT config & state ==================
constexpr uint32_t SAMPLE_FREQ        = 8000;        // Hz
constexpr size_t   NUM_SAMPLES        = 1024;        // power-of-2
constexpr uint32_t SAMPLE_INTERVAL_US = 1000000UL / SAMPLE_FREQ;
constexpr unsigned long WINDOW_MS     = 2000;        // roll maxima every 2s
unsigned long      publishIntervalMs  = 10UL * 60000UL; // every 10 minutes

double lowHz1  = 900.0,  highHz1 = 1200.0;          // band 1
double lowHz2  = 1900.0, highHz2 = 2300.0;          // band 2

double cleanupHz = 3000.0;                          // simple one-pole LPF
static float lp_prev = 0.0f;
static float lp_alpha = 0.0f;

static uint16_t rawBuffer[NUM_SAMPLES];
static float    floatBuffer[NUM_SAMPLES];
vector<complex<double>> fftBuf(NUM_SAMPLES);

double winMax1=0, winFreq1=0, winMax2=0, winFreq2=0;
double pubMax1=0, pubFreq1=0, pubMax2=0, pubFreq2=0;

unsigned long windowStart = 0, publishStart = 0;
uint32_t lastSampleTime = 0;

bool fftEnabled = false;  // becomes true after publish + GPS shutdown (or cloud fn)
int  gainState  = 1;      // 1=HIGH, 0=LOW

// ====== GNSS acquire timeout ======
const unsigned long GNSS_TIMEOUT_MS = 3UL * 60UL * 1000UL; // 3 minutes
unsigned long gnssStartMs = 0;

// ================== Helpers ==================
static inline void computeAlpha() {
    float dt = 1.0f / SAMPLE_FREQ;
    float RC = 1.0f / (2.0f * M_PI * cleanupHz);
    lp_alpha = dt / (RC + dt);
}

static inline void peakInBand(
    const vector<complex<double>>& X, size_t N,
    double lo, double hi,
    double& outMag, size_t& outBin)
{
    size_t s = (size_t)(lo * N / SAMPLE_FREQ);
    size_t e = (size_t)(hi * N / SAMPLE_FREQ);
    double m = 0.0; size_t mb = s;
    for (size_t k = s; k <= e && k < N; ++k) {
        double a = abs(X[k]);
        if (a > m) { m = a; mb = k; }
    }
    outMag = m; outBin = mb;
}

bool isGoodFix(const GPSData& d) {
    bool vdopValid = (d.vdop > 0.0 && d.vdop < 50.0);
    bool dopOk = (d.hdop > 0.0 && d.hdop <= 2.5) && (!vdopValid || d.vdop <= 3.0);
    bool accOk = (d.horizontalAccuracy > 0 && d.horizontalAccuracy <= 25.0);
    return d.locked && (dopOk || accOk);
}

float readCellStrength() {
#if HAL_PLATFORM_CELLULAR
    CellularSignal sig = Cellular.RSSI();
    if (sig.isValid()) return sig.getStrength();
#endif
    return NAN;
}
float readBatteryPct() {
    float pct = System.batteryCharge(); // may be NAN
    return pct;
}

// ================== Button (optional) ==================
#if USE_USER_BUTTON
void buttonHandler(DebounceSwitchState *s, void*) {
    if (s->getPressState() == DebouncePressState::TAP) {
        int taps = s->getTapCount();
        Log.info("Button: %d taps", taps);
        if (taps == 3) {
            g_requestPublish = true;
            Log.info("Triple-tap: will publish location, shutdown GPS, then start FFT");
        }
    }
}
#endif

// ================== GPS helpers ==================
bool gpsPowerControl(bool en) { digitalWrite(GPS_PWR_PIN, en ? HIGH : LOW); return true; }
bool gpsSpiSelect  (bool sel){ digitalWrite(GPS_CS_PIN,  sel ? LOW : HIGH); return true; }

void updateGPSData() {
    if (!gps) return;
    gps->lock();
    gpsData.locked = gps->getLock();
    gpsData.stable = gps->isLockStable();
    gpsData.satellites = gps->getSatellites();
    gpsData.hdop = gps->getHDOP();
    gpsData.vdop = gps->getVDOP();
    gpsData.horizontalAccuracy = gps->getHorizontalAccuracy();
    gpsData.verticalAccuracy = gps->getVerticalAccuracy();
    gpsData.utcTime = (time_t)gps->getUTCTime();
    if (gpsData.locked) {
        gpsData.latitude = gps->getLatitude();
        gpsData.longitude = gps->getLongitude();
        gpsData.altitude  = gps->getAltitude();
        gpsData.speed     = gps->getSpeed(GPS_SPEED_UNIT_MPS);
        gpsData.heading   = gps->getHeading();
    }
    gps->unlock();
}

void printGPSData() {
    Log.info("=== GPS === lock:%s sats:%d HDOP:%.2f HAcc:%.2f m",
        gpsData.locked ? "YES":"NO", gpsData.satellites, gpsData.hdop, gpsData.horizontalAccuracy);
    if (gpsData.locked) {
        Log.info("Lat:%.6f Lon:%.6f Alt:%.2f", gpsData.latitude, gpsData.longitude, gpsData.altitude);
    }
}

// ================== Tracker-style JSON publish ==================
bool publishTrackerJSON() {
    setStage(ST_PUBLISH);
    if (!Particle.connected()) {
        Particle.keepAlive(30);
        Particle.connect();
        waitFor(Particle.connected, 60000);
    }
    float cell = readCellStrength();
    float batt = readBatteryPct();

    char buf[384];
    snprintf(buf, sizeof(buf),
        "{"
          "\"cmd\":\"loc\","
          "\"time\":%lu,"
          "\"loc\":{"
              "\"lck\":%d,"
              "\"time\":%lu,"
              "\"lat\":%.6f,"
              "\"lon\":%.6f,"
              "\"alt\":%.2f,"
              "\"hd\":%.2f,"
              "\"h_acc\":%.2f,"
              "\"v_acc\":%.2f,"
              "\"cell\":%s,"
              "\"batt\":%s"
          "},"
          "\"trig\":[\"time\"],"
          "\"req_id\":%d"
        "}",
        (unsigned long)Time.now(),
        gpsData.locked ? 1 : 0,
        (unsigned long)(gpsData.utcTime ? gpsData.utcTime : Time.now()),
        gpsData.latitude, gpsData.longitude, gpsData.altitude,
        gpsData.heading,
        gpsData.horizontalAccuracy, gpsData.verticalAccuracy,
        (isnan(cell) ? "null" : String(cell, 1).c_str()),
        (isnan(batt) ? "null" : String(batt, 1).c_str()),
        g_reqId++
    );

    Log.info("Publishing Tracker-style location...");
    bool ok = Particle.publish("loc", buf, PRIVATE); // no WITH_ACK to avoid long stall
    Log.info("Location publish %s", ok ? "queued" : "failed");
    return ok;
}

// ================== Cloud API (Particle.function) ==================
int fn_startFFT (String){ fftEnabled = true;  Log.info("FFT enabled");  return 1; }
int fn_stopFFT  (String){ fftEnabled = false; Log.info("FFT disabled"); return 1; }

int fn_setB1L(String s){ double v=s.toFloat(); if(v>0 && v<highHz1){ lowHz1=v; return 1;} return -1; }
int fn_setB1H(String s){ double v=s.toFloat(); if(v>lowHz1 && v<SAMPLE_FREQ/2){ highHz1=v; return 1;} return -1; }
int fn_setB2L(String s){ double v=s.toFloat(); if(v>0 && v<highHz2){ lowHz2=v; return 1;} return -1; }
int fn_setB2H(String s){ double v=s.toFloat(); if(v>lowHz2 && v<SAMPLE_FREQ/2){ highHz2=v; return 1;} return -1; }

int fn_setCleanup(String s){
    double v=s.toFloat();
    if (v>100.0 && v<SAMPLE_FREQ/2){ cleanupHz=v; computeAlpha(); return 1; }
    return -1;
}

int fn_setPubInterval(String s){
    unsigned long v=s.toInt();
    if (v>=1 && v<=1440){ publishIntervalMs=v*60000UL; return 1; }
    return -1;
}

int fn_setGain(String s){
    String cmd=s; cmd.toLowerCase();
    if (cmd=="high" || s.toInt()==1){ digitalWrite(GAIN_PIN,HIGH); gainState=1; return 1; }
    if (cmd=="low"  || s.toInt()==0){ digitalWrite(GAIN_PIN,LOW);  gainState=0; return 1; }
    return -1;
}

int fn_simulateTriple(String) {
    g_requestPublish = true;
    Log.info("simulateTriple: triple-tap requested via cloud");
    return 1;
}

// ================== Setup ==================
void setup() {
    logReset();                  // print previous cause
    setStage(ST_SETUP);

    Serial.begin(115200);
    waitFor(Serial.isConnected, 3000);

    // GPS & SPI pins
    pinMode(GPS_CS_PIN,  OUTPUT);
    pinMode(GPS_PWR_PIN, OUTPUT);
    pinMode(GPS_RST_PIN, OUTPUT);
    pinMode(GPS_INT_PIN, INPUT);
    digitalWrite(GPS_CS_PIN, HIGH);
    digitalWrite(GPS_PWR_PIN, LOW);
    digitalWrite(GPS_RST_PIN, HIGH);

    SPI1.begin(); // used by GPS

    // ---- Cloud API ----
    Particle.variable("b1L", lowHz1);
    Particle.variable("b1H", highHz1);
    Particle.variable("b2L", lowHz2);
    Particle.variable("b2H", highHz2);
    Particle.variable("cleanupHz", cleanupHz);
    Particle.variable("pubMin", publishIntervalMs); // ms
    Particle.variable("gainState", gainState);

    Particle.function("fftStart",  fn_startFFT);
    Particle.function("fftStop",   fn_stopFFT);
    Particle.function("setB1L",    fn_setB1L);
    Particle.function("setB1H",    fn_setB1H);
    Particle.function("setB2L",    fn_setB2L);
    Particle.function("setB2H",    fn_setB2H);
    Particle.function("setCleanup",fn_setCleanup);
    Particle.function("setPubMin", fn_setPubInterval);
    Particle.function("setGain",   fn_setGain);
    Particle.function("simulateTriple", fn_simulateTriple);

    // Button + debounce (optional)
#if USE_USER_BUTTON
    pinMode(USER_BUTTON_PIN, INPUT_PULLUP);
    DebounceSwitch::getInstance()->setup();
    DebounceSwitch::getInstance()->addSwitch(
        USER_BUTTON_PIN,
        DebounceSwitchStyle::PRESS_LOW_PULLUP,
        buttonHandler,
        nullptr
    );
#endif

    // LED searching
    ledSearching();

    // GPS
    gps = new ubloxGPS(SPI1, gpsSpiSelect, gpsPowerControl, GPS_INT_PIN, GPS_INT_GPS_PIN);
    if (gps) {
        digitalWrite(GPS_RST_PIN, LOW); delay(10);
        digitalWrite(GPS_RST_PIN, HIGH); delay(20);
        gps->on();
        Log.info("GPS: ON");
        setStage(ST_GPS_ON);
        gnssStartMs = millis();           // start the acquire timeout window
    }

    // MIC
    pinMode(MIC_PIN, INPUT);
    pinMode(GAIN_PIN, OUTPUT);
    digitalWrite(GAIN_PIN, HIGH);
    gainState=1;

    // CAN stays OFF (do nothing)

    // Cloud up + keep alive
    Particle.keepAlive(30);
    ledCyan();
    Particle.connect();
    waitFor(Particle.connected, 60000);

    // FFT prep
    computeAlpha();
    windowStart  = millis();
    publishStart = millis();
}

// ================== Loop ==================
void loop() {
    // Cloud liveness / reconnection
    if (millis() - g_lastCloudKick >= 10000) {
        if (!Particle.connected()) {
            Log.warn("Cloud disconnected; reconnecting...");
            Particle.connect();
        }
        g_lastCloudKick = millis();
    }

    if (gps && gps->isOn()) {
        setStage(ST_GPS_LOOP);
        updateGPSData();

        // LED green on good fix
        if (!g_ledGreenSet && isGoodFix(gpsData)) {
            ledLocked();
            g_ledGreenSet = true;
            Log.info("LED GREEN: good GPS lock/accuracy");
        } else if (!g_ledGreenSet) {
            ledSearching();
        }

        // Auto location publish once on first good lock
        if (!g_autoLocPublished && g_ledGreenSet && Particle.connected()) {
            publishTrackerJSON();
            g_autoLocPublished = true;
        }

        // Enforce GNSS acquire timeout -> shutdown GPS/SPI -> start FFT
        if (!g_shutdownDone && (millis() - gnssStartMs) >= GNSS_TIMEOUT_MS) {
            Log.warn("GNSS: timeout; shutting down GPS and starting FFT.");
            if (gps->isOn()) gps->off();
            SPI1.end();                 // free SPI1 bus completely
            g_shutdownDone = true;
            ledDone();
            fftEnabled = true;
        }

        // Triple-tap flow: publish, then shutdown GPS/SPI, then start FFT
        if (g_requestPublish && !g_shutdownDone) {
            g_requestPublish = false;
            bool ok = publishTrackerJSON();

            if (gps->isOn()) gps->off();
            SPI1.end();                 // free SPI1 bus entirely
            g_shutdownDone = true;
            ok ? ledDone() : ledMagenta();
            Log.info("GPS off, SPI1 ended. FFT will start.");
            fftEnabled = true;
        }

        // Print GPS every 5s
        static unsigned long lastPrintGPS = 0;
        if (millis() - lastPrintGPS >= 5000) {
            printGPSData();
            lastPrintGPS = millis();
        }
    }

    // ================== FFT mode ==================
    if (fftEnabled) {
        // 1) Sample at 8 kHz
        setStage(ST_FFT_SAMPLE);
        for (size_t i = 0; i < NUM_SAMPLES; i++) {
            while (micros() - lastSampleTime < SAMPLE_INTERVAL_US) { /* spin */ }
            lastSampleTime = micros();
            rawBuffer[i]   = analogRead(MIC_PIN);
        }

        // 2) One-pole LPF cleanup (center around midcode ~2048)
        for (size_t i = 0; i < NUM_SAMPLES; i++) {
            float centered = (float)rawBuffer[i] - 2048.0f;
            lp_prev = lp_prev + lp_alpha * (centered - lp_prev);
            floatBuffer[i] = lp_prev;
        }

        // 3) Remove DC & build complex vector
        double sum = 0;
        for (size_t i = 0; i < NUM_SAMPLES; i++) sum += floatBuffer[i];
        double mean = sum / NUM_SAMPLES;
        for (size_t i = 0; i < NUM_SAMPLES; i++) {
            fftBuf[i] = complex<double>(floatBuffer[i] - mean, 0.0);
        }

        // 4) FFT
        Fft::transformRadix2(fftBuf);

        // 5) Peak search in bands
        double mag1=0, mag2=0; size_t bin1=0, bin2=0;
        peakInBand(fftBuf, NUM_SAMPLES, lowHz1, highHz1, mag1, bin1);
        peakInBand(fftBuf, NUM_SAMPLES, lowHz2, highHz2, mag2, bin2);
        double freq1 = (double)bin1 * SAMPLE_FREQ / NUM_SAMPLES;
        double freq2 = (double)bin2 * SAMPLE_FREQ / NUM_SAMPLES;

        if (mag1 > winMax1){ winMax1 = mag1; winFreq1 = freq1; }
        if (mag2 > winMax2){ winMax2 = mag2; winFreq2 = freq2; }

        // Log every 2 seconds and fold into publish maxima
        if (millis() - windowStart >= WINDOW_MS) {
            Log.info("FFT bands: B1 amp=%.3f @ %.1f Hz | B2 amp=%.3f @ %.1f Hz",
                     winMax1, winFreq1, winMax2, winFreq2);

            if (winMax1 > pubMax1){ pubMax1 = winMax1; pubFreq1 = winFreq1; }
            if (winMax2 > pubMax2){ pubMax2 = winMax2; pubFreq2 = winFreq2; }
            winMax1 = winFreq1 = 0.0;
            winMax2 = winFreq2 = 0.0;
            windowStart = millis();
        }

        // Publish every 10 minutes
        if (millis() - publishStart >= publishIntervalMs) {
            setStage(ST_FFT_PUBLISH);
            char payload[192];
            snprintf(payload, sizeof(payload),
                "{\"b1_amp\":%.3f,\"b1_freq\":%.1f,"
                 "\"b2_amp\":%.3f,\"b2_freq\":%.1f,"
                 "\"cleanup\":%.0f,"
                 "\"bands\":\"%.0f/%.0f|%.0f/%.0f\"}",
                pubMax1, pubFreq1,
                pubMax2, pubFreq2,
                cleanupHz,
                lowHz1, highHz1, lowHz2, highHz2);

            if (!Particle.connected()) { Particle.connect(); waitFor(Particle.connected, 60000); }
            Log.info("[FFT] publish: %s", payload);
            Particle.publish("bandPeak10m", payload, PRIVATE);

            pubMax1 = pubFreq1 = 0.0;
            pubMax2 = pubFreq2 = 0.0;
            publishStart = millis();
        }
    }

    // Let background tasks run
    delay(50);
}

If you call Edge::instance().init() from setup you must also call Edge::instance::loop() from loop().

You either need to not init() and manually initialize anything you need, or call the loop function from loop.

Also you shouldn't delay the loop by 50ms on every loop. It's not why it's failing, but it's also not necessary or helpful.

I am not calling Edge::Instance anywhere in the code anymore. Is there something I am doing when I turn GPS off that could effect the watchdog? The code is not failing the same way, so I am still stumped. I will also remove the delay. Thank you for that.

Sorry, when I scrolled up to double-check the source I looked at the wrong source block. The GPS should not affect the watchdog. Also I'm not 100% sure it's the AB1805 hardware watchdog that's causing the pin reset, though it probably is.

Did you remove all power, including the battery, after disabling the Edge init? Leave it unplugged for at least 30 seconds.

Since you also removed the AB1805 initialization code, it probably is maintaining the previous configuration. The watchdog should have been disabled before reset, but since loop was also not called before, it's unclear what state it's in now.

@rickkas7 , you are the best. I left the power off the device and now it is no longer tripping Pin_reset. Also, my other issue of “card not detected” was due to me corrupted EEPROM when I applied a signal to A0,A1 with the microphone. I wanted to let you and other know that is what was most likely the reason.

1 Like

@rickkas7, it ran for awhile but started the Pin_reset issue again after I flashed new firmware. Should I be pulling in the AB1805 library and doing either a stop or reset on it?

It's not at all clear to me why this is happening. The AB1805 watchdog defaults to not running so unless something is enabling it, it should be disabled on cold boot.

There is still a possibility that something else is causing a pin reset. Are you using a HAT on the expansion connector? Which one?

I switched to using the basic IO card and have my microphone using GND, 3V3, A4 and A5. Nothing else to plugged in. And the resets are not consistent, it will run for 15 mintues and then maybe only run for 5 minutes. I have the battery connected and even an external power source to try and rule out power fluctuations. I also tried another basic IO card with nothing attached and it is doing similar things. I did attempt to pull in the AN1805_RK.h code to do a reset and stop but that didn’t seem to fix the issue. I think one or both of two things are contributing to the issue and I would to hear what you think or what I should try. My code is currently pulling in the GPS_Ublox, GPS-nmea-parser, DebounceSwitchRK, and Can mcp25x.

  1. Since I am using the basic proto-typing card I have a conflict with the Hardware and software. I am not using Can, so I have it disabled in my code.
  2. Since I am not calling Edge, is there conflict with the basic proto-typing card and the TSOM with respect to SPI or Wire.

Let me me know what you think.

Thanks,

Jon

I would try adding a 10K pull-up resistor on the reset line on the prototyping card.

It looks like neither the proto card nor the Muon carrier board have a hardware pull-up on the reset line and rely on the MCU pull-up which is weak.

Since the time is random my next guess is that EMI from the circuitry on the proto card is triggering the reset. There's a trace that runs half the length of the card that may be acting like an antenna.

I added in the 10K resistors to both proto-cards. One with the Mic and one without. Both ran for about an hour and it was starting to rain so I brought them inside. 1 minute later they both reset on Pin_reset. Is there a timing issue or something with particle.connect that I am doing wrong. Here is my updated code with the reset reasons and what not. The resets happen during the FFT which makes me think I am running out of Ram, but the fact I brought both in and they both reset doesn’t add up. Any thoughts?

#include "Particle.h"
#include <math.h>
#include "ubloxGPS.h"
#include "tracker_config.h"
#define USE_USER_BUTTON 1
#if USE_USER_BUTTON
  #include "DebounceSwitchRK.h"
#endif

#include "FftComplex.h"
#include <complex>
#include <vector>
#include <cmath>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

using namespace std;

SYSTEM_MODE(SEMI_AUTOMATIC);
SYSTEM_THREAD(ENABLED);

// ======= Quick A/B feature toggles =======
#define ENABLE_FFT        1     // set 0 to disable FFT entirely
#define ENABLE_PUBLISH    1     // set 0 to disable cloud publishes
#define LOG_CELL_EVERY_MS 10000 // set 0 to disable periodic cell logging

// ======= Pins / Hardware =======
#define GPS_CS_PIN   (GPS_CS)
#define GPS_PWR_PIN  (GPS_PWR)
#define GPS_RST_PIN  (GPS_RST)
#define GPS_INT_PIN  (GPS_INT)
#define GPS_INT_GPS_PIN (14)

#define MIC_PIN   A5
#define GAIN_PIN  A4  // MAX9814 gain (HIGH=high, LOW=low)

// ======= Breadcrumbs / reset reason (refactored) =======
struct Crumb { uint32_t magic, stage, ms, rr, rrData; };
static constexpr uint32_t CRUMB_MAGIC = 0xA5A5A5A5;

// Avoid collision with std::prev
retained Crumb bc_prev = {0}; // retained for next boot to read
Crumb bc_last = {0};          // snapshot of previous boot (publish this boot)
Crumb bc_curr = {0};          // current boot

enum { ST_BOOT=1, ST_SETUP, ST_GPS_ON, ST_GPS_LOOP, ST_PUBLISH, ST_FFT_SAMPLE, ST_FFT_PUBLISH };

static inline const char* stageName(uint32_t s){
  switch (s){
    case ST_BOOT:         return "BOOT";
    case ST_SETUP:        return "SETUP";
    case ST_GPS_ON:       return "GPS_ON";
    case ST_GPS_LOOP:     return "GPS_LOOP";
    case ST_PUBLISH:      return "PUBLISH";
    case ST_FFT_SAMPLE:   return "FFT_SAMPLE";
    case ST_FFT_PUBLISH:  return "FFT_PUBLISH";
    default:              return "?";
  }
}
static inline void setStage(uint32_t s){ bc_curr.stage = s; bc_curr.ms = millis(); }
static inline void rollCrumbForNextBoot(){ bc_prev = bc_curr; }

static void capturePrevAndInitCurr() {
  if (bc_prev.magic == CRUMB_MAGIC) {
    bc_last = bc_prev;
    Log.info("Last boot: stage=%lu (%s) ms=%lu rr=%lu",
      (unsigned long)bc_last.stage, stageName(bc_last.stage),
      (unsigned long)bc_last.ms, (unsigned long)bc_last.rr);
  } else {
    bc_last = {0};
    Log.info("No previous crumb");
  }
  bc_curr.magic = CRUMB_MAGIC;
  bc_curr.rr    = (uint32_t)System.resetReason();
  bc_curr.rrData= (uint32_t)System.resetReasonData();
  setStage(ST_BOOT);
  rollCrumbForNextBoot();
}
static inline void logResetNow() {
  Log.info("Reset reason=%d data=0x%08lx", (int)bc_curr.rr, (unsigned long)bc_curr.rrData);
}

// ======= Logging =======
SerialLogHandler logHandler(115200, LOG_LEVEL_INFO, {
    {"app.gps.nmea", LOG_LEVEL_WARN},
    {"app.gps.ubx",  LOG_LEVEL_WARN},
});

// ======= GPS model =======
struct GPSData {
    double latitude = 0.0, longitude = 0.0, altitude = 0.0;
    double speed = 0.0, heading = 0.0;
    bool   locked = false, stable = false;
    uint8_t satellites = 0;
    double hdop = 0.0, vdop = 0.0;
    double horizontalAccuracy = 0.0, verticalAccuracy = 0.0;
    time_t utcTime = 0;
};
ubloxGPS *gps = nullptr;
GPSData   gpsData;

// ======= State / LEDs =======
volatile bool g_requestPublish  = false;
bool         g_ledGreenSet      = false;
bool         g_shutdownDone     = false;
bool         g_autoLocPublished = false;

unsigned long g_lastCloudKick = 0;
int           g_reqId = 1;

inline void ledSearching() { RGB.control(true);  RGB.color(255,165,0); }
inline void ledLocked()    { RGB.control(true);  RGB.color(0,255,0);   }
inline void ledDone()      { RGB.control(true);  RGB.color(0,0,255);   }
inline void ledCyan()      { RGB.control(true);  RGB.color(0,255,255); }
inline void ledMagenta()   { RGB.control(true);  RGB.color(255,0,255); }

// ======= FFT config & state =======
constexpr uint32_t SAMPLE_FREQ        = 8000;
constexpr size_t   NUM_SAMPLES        = 1024;
constexpr uint32_t SAMPLE_INTERVAL_US = 1000000UL / SAMPLE_FREQ;
constexpr unsigned long WINDOW_MS     = 2000;
unsigned long      publishIntervalMs  = 10UL * 60000UL;

double lowHz1  = 900.0,  highHz1 = 1200.0;
double lowHz2  = 1900.0, highHz2 = 2300.0;

double cleanupHz = 3000.0;
static float lp_prev = 0.0f;
static float lp_alpha = 0.0f;

static uint16_t rawBuffer[NUM_SAMPLES];
static float    floatBuffer[NUM_SAMPLES];
vector<complex<double>> fftBuf(NUM_SAMPLES);

double winMax1=0, winFreq1=0, winMax2=0, winFreq2=0;
double pubMax1=0, pubFreq1=0, pubMax2=0, pubFreq2=0;

unsigned long windowStart = 0, publishStart = 0;
uint32_t lastSampleTime = 0;

bool fftEnabled = false;
int  gainState  = 1;

const unsigned long GNSS_TIMEOUT_MS = 3UL * 60UL * 1000UL;
unsigned long gnssStartMs = 0;

// ======= Helpers =======
static inline void computeAlpha() {
    float dt = 1.0f / SAMPLE_FREQ;
    float RC = 1.0f / (2.0f * M_PI * cleanupHz);
    lp_alpha = dt / (RC + dt);
}
static inline void peakInBand(
    const vector<complex<double>>& X, size_t N,
    double lo, double hi,
    double& outMag, size_t& outBin)
{
    size_t s = (size_t)(lo * N / SAMPLE_FREQ);
    size_t e = (size_t)(hi * N / SAMPLE_FREQ);
    double m = 0.0; size_t mb = s;
    for (size_t k = s; k <= e && k < N; ++k) {
        double a = abs(X[k]);
        if (a > m) { m = a; mb = k; }
    }
    outMag = m; outBin = mb;
}
bool isGoodFix(const GPSData& d) {
    bool vdopValid = (d.vdop > 0.0 && d.vdop < 50.0);
    bool dopOk = (d.hdop > 0.0 && d.hdop <= 2.5) && (!vdopValid || d.vdop <= 3.0);
    bool accOk = (d.horizontalAccuracy > 0 && d.horizontalAccuracy <= 25.0);
    return d.locked && (dopOk || accOk);
}
float readCellStrength() {
#if HAL_PLATFORM_CELLULAR
    CellularSignal sig = Cellular.RSSI();
    if (sig.isValid()) return sig.getStrength();
#endif
    return NAN;
}
float readBatteryPct() {
    float pct = System.batteryCharge();
    return pct;
}
static void logCellSignal(const char* tag = "cell") {
#if HAL_PLATFORM_CELLULAR
    CellularSignal s = Cellular.RSSI();
    if (s.isValid()) {
        // strength/quality are % (0–100); RAT is enum -> cast to int for logging
        Log.info("[%s] strength=%d%% quality=%d%% rat=%d",
                 tag, s.getStrength(), s.getQuality(), (int)s.getAccessTechnology());
    } else {
        Log.info("[%s] signal invalid", tag);
    }
#else
    (void)tag;
#endif
}

// ======= Button (optional) =======
#if USE_USER_BUTTON
void buttonHandler(DebounceSwitchState *s, void*) {
    if (s->getPressState() == DebouncePressState::TAP) {
        int taps = s->getTapCount();
        Log.info("Button: %d taps", taps);
        if (taps == 3) {
            g_requestPublish = true;
            Log.info("Triple-tap: will publish location, shutdown GPS, then start FFT");
        }
    }
}
#endif

// ======= GPS helpers =======
bool gpsPowerControl(bool en) { digitalWrite(GPS_PWR_PIN, en ? HIGH : LOW); return true; }
bool gpsSpiSelect  (bool sel){ digitalWrite(GPS_CS_PIN,  sel ? LOW : HIGH); return true; }
void updateGPSData() {
    if (!gps) return;
    gps->lock();
    gpsData.locked = gps->getLock();
    gpsData.stable = gps->isLockStable();
    gpsData.satellites = gps->getSatellites();
    gpsData.hdop = gps->getHDOP();
    gpsData.vdop = gps->getVDOP();
    gpsData.horizontalAccuracy = gps->getHorizontalAccuracy();
    gpsData.verticalAccuracy = gps->getVerticalAccuracy();
    gpsData.utcTime = (time_t)gps->getUTCTime();
    if (gpsData.locked) {
        gpsData.latitude = gps->getLatitude();
        gpsData.longitude = gps->getLongitude();
        gpsData.altitude  = gps->getAltitude();
        gpsData.speed     = gps->getSpeed(GPS_SPEED_UNIT_MPS);
        gpsData.heading   = gps->getHeading();
    }
    gps->unlock();
}
void printGPSData() {
    Log.info("=== GPS === lock:%s sats:%d HDOP:%.2f HAcc:%.2f m",
        gpsData.locked ? "YES":"NO", gpsData.satellites, gpsData.hdop, gpsData.horizontalAccuracy);
    if (gpsData.locked) {
        Log.info("Lat:%.6f Lon:%.6f Alt:%.2f", gpsData.latitude, gpsData.longitude, gpsData.altitude);
    }
}

// ======= Tracker-style JSON publish =======
static unsigned long lastPublishAt = 0;  // enforce spacing between publishes
static inline bool shouldPublishNow() {
#if ENABLE_PUBLISH
    const unsigned long MIN_PUB_SPACING_MS = 5000; // ≥5s between publishes
    return Particle.connected() && (millis() - lastPublishAt >= MIN_PUB_SPACING_MS);
#else
    return false;
#endif
}

bool publishTrackerJSON() {
#if !ENABLE_PUBLISH
    return true;
#endif
    setStage(ST_PUBLISH); rollCrumbForNextBoot();

    if (!Particle.connected()) {
        Particle.keepAlive(30);
        Particle.connect();
        waitFor(Particle.connected, 60000);
    }

    logCellSignal("pre-publish"); // correlate resets with weak signal

    float cell = readCellStrength();
    float batt = readBatteryPct();

    char buf[384];
    snprintf(buf, sizeof(buf),
        "{"
          "\"cmd\":\"loc\",\"time\":%lu,"
          "\"loc\":{"
              "\"lck\":%d,\"time\":%lu,"
              "\"lat\":%.6f,\"lon\":%.6f,\"alt\":%.2f,"
              "\"hd\":%.2f,\"h_acc\":%.2f,\"v_acc\":%.2f,"
              "\"cell\":%s,\"batt\":%s"
          "},\"trig\":[\"time\"],\"req_id\":%d}",
        (unsigned long)Time.now(),
        gpsData.locked ? 1 : 0,
        (unsigned long)(gpsData.utcTime ? gpsData.utcTime : Time.now()),
        gpsData.latitude, gpsData.longitude, gpsData.altitude,
        gpsData.heading,
        gpsData.horizontalAccuracy, gpsData.verticalAccuracy,
        (isnan(cell) ? "null" : String(cell, 1).c_str()),
        (isnan(batt) ? "null" : String(batt, 1).c_str()),
        g_reqId++
    );

    if (!shouldPublishNow()) return false;

    Log.info("Publishing Tracker-style location...");
    delay(15); // tiny breather before the TX burst
    bool ok = Particle.publish("loc", buf, PRIVATE);
    lastPublishAt = millis();
    Log.info("Location publish %s", ok ? "queued" : "failed");

    logCellSignal("post-publish");
    return ok;
}

// ======= Cloud API (Particle.function) =======
int fn_startFFT (String){ fftEnabled = true;  Log.info("FFT enabled");  return 1; }
int fn_stopFFT  (String){ fftEnabled = false; Log.info("FFT disabled"); return 1; }
int fn_setB1L(String s){ double v=s.toFloat(); if(v>0 && v<highHz1){ lowHz1=v; return 1;} return -1; }
int fn_setB1H(String s){ double v=s.toFloat(); if(v>lowHz1 && v<SAMPLE_FREQ/2){ highHz1=v; return 1;} return -1; }
int fn_setB2L(String s){ double v=s.toFloat(); if(v>0 && v<highHz2){ lowHz2=v; return 1;} return -1; }
int fn_setB2H(String s){ double v=s.toFloat(); if(v>lowHz2 && v<SAMPLE_FREQ/2){ highHz2=v; return 1;} return -1; }
int fn_setCleanup(String s){
    double v=s.toFloat();
    if (v>100.0 && v<SAMPLE_FREQ/2){ cleanupHz=v; computeAlpha(); return 1; }
    return -1;
}
int fn_setPubInterval(String s){
    unsigned long v=s.toInt();
    if (v>=1 && v<=1440){ publishIntervalMs=v*60000UL; return 1; }
    return -1;
}
int fn_setGain(String s){
    String cmd=s; cmd.toLowerCase();
    if (cmd=="high" || s.toInt()==1){ digitalWrite(GAIN_PIN,HIGH); gainState=1; return 1; }
    if (cmd=="low"  || s.toInt()==0){ digitalWrite(GAIN_PIN,LOW);  gainState=0; return 1; }
    return -1;
}
int fn_simulateTriple(String) {
    g_requestPublish = true;
    Log.info("simulateTriple: triple-tap requested via cloud");
    return 1;
}

// ======= Setup =======
void setup() {
    Serial.begin(115200);
    waitFor(Serial.isConnected, 10000);
    delay(5000);

    capturePrevAndInitCurr();
    logResetNow();

    // GPS & SPI pins
    pinMode(GPS_CS_PIN,  OUTPUT);
    pinMode(GPS_PWR_PIN, OUTPUT);
    pinMode(GPS_RST_PIN, OUTPUT);
    pinMode(GPS_INT_PIN, INPUT);
    digitalWrite(GPS_CS_PIN, HIGH);
    digitalWrite(GPS_PWR_PIN, LOW);
    digitalWrite(GPS_RST_PIN, HIGH);

    SPI1.begin();

    // Cloud API
    Particle.variable("b1L", lowHz1);
    Particle.variable("b1H", highHz1);
    Particle.variable("b2L", lowHz2);
    Particle.variable("b2H", highHz2);
    Particle.variable("cleanupHz", cleanupHz);
    Particle.variable("pubMin", publishIntervalMs);
    Particle.variable("gainState", gainState);

    Particle.function("fftStart",  fn_startFFT);
    Particle.function("fftStop",   fn_stopFFT);
    Particle.function("setB1L",    fn_setB1L);
    Particle.function("setB1H",    fn_setB1H);
    Particle.function("setB2L",    fn_setB2L);
    Particle.function("setB2H",    fn_setB2H);
    Particle.function("setCleanup",fn_setCleanup);
    Particle.function("setPubMin", fn_setPubInterval);
    Particle.function("setGain",   fn_setGain);
    Particle.function("simulateTriple", fn_simulateTriple);

#if USE_USER_BUTTON
    pinMode(MONITORONE_USER_BUTTON, INPUT_PULLUP);
    DebounceSwitch::getInstance()->setup();
    DebounceSwitch::getInstance()->addSwitch(
        MONITORONE_USER_BUTTON,
        DebounceSwitchStyle::PRESS_LOW_PULLUP,
        buttonHandler,
        nullptr
    );
#endif

    ledSearching();

    // GPS
    gps = new ubloxGPS(SPI1, gpsSpiSelect, gpsPowerControl, GPS_INT_PIN, GPS_INT_GPS_PIN);
    if (gps) {
        digitalWrite(GPS_RST_PIN, LOW); delay(10);
        digitalWrite(GPS_RST_PIN, HIGH); delay(20);
        gps->on();
        Log.info("GPS: ON");
        setStage(ST_GPS_ON); rollCrumbForNextBoot();
        gnssStartMs = millis();
    }

    // MIC
    pinMode(MIC_PIN, INPUT);
    pinMode(GAIN_PIN, OUTPUT);
    digitalWrite(GAIN_PIN, HIGH);
    gainState=1;

    // Cloud up + keep alive
    Particle.keepAlive(30);
    ledCyan();
#if ENABLE_PUBLISH
    Particle.connect();
    waitFor(Particle.connected, 60000);
#endif

    computeAlpha();
    windowStart  = millis();
    publishStart = millis();

    setStage(ST_SETUP); rollCrumbForNextBoot();
}

// ======= Loop =======
void loop() {
    // Publish the PREVIOUS boot once
#if ENABLE_PUBLISH
    static bool bootCrumbPublished = false;
    if (Particle.connected() && !bootCrumbPublished) {
        char j[180];
        if (bc_last.magic == CRUMB_MAGIC) {
            snprintf(j, sizeof(j),
                "{\"prev_stage\":%lu,\"prev_stage_name\":\"%s\","
                 "\"prev_ms\":%lu,\"prev_rr\":%lu,\"prev_rrd\":%lu}",
                (unsigned long)bc_last.stage, stageName(bc_last.stage),
                (unsigned long)bc_last.ms,
                (unsigned long)bc_last.rr, (unsigned long)bc_last.rrData);
        } else {
            snprintf(j, sizeof(j), "{\"prev\":\"none\"}");
        }
        Particle.publish("boot_diag", j, PRIVATE);
        lastPublishAt = millis();
        bootCrumbPublished = true;
    }
#endif

    // Periodic cell diarization (helps correlate indoor resets)
#if LOG_CELL_EVERY_MS > 0 && HAL_PLATFORM_CELLULAR
    static unsigned long lastCellLog = 0;
    if (millis() - lastCellLog >= LOG_CELL_EVERY_MS) {
        logCellSignal("periodic");
        lastCellLog = millis();
    }
#endif

    // Cloud liveness / reconnection
#if ENABLE_PUBLISH
    if (millis() - g_lastCloudKick >= 10000) {
        if (!Particle.connected()) {
            Log.warn("Cloud disconnected; reconnecting...");
            Particle.connect();
        }
        g_lastCloudKick = millis();
    }
#endif

    if (gps && gps->isOn()) {
        setStage(ST_GPS_LOOP); rollCrumbForNextBoot();
        updateGPSData();

        if (!g_ledGreenSet && isGoodFix(gpsData)) {
            ledLocked();
            g_ledGreenSet = true;
            Log.info("LED GREEN: good GPS lock/accuracy");
        } else if (!g_ledGreenSet) {
            ledSearching();
        }

#if ENABLE_PUBLISH
        if (!g_autoLocPublished && g_ledGreenSet && Particle.connected()) {
            publishTrackerJSON();
            g_autoLocPublished = true;
        }
#endif

        if (!g_shutdownDone && (millis() - gnssStartMs) >= GNSS_TIMEOUT_MS) {
            Log.warn("GNSS: timeout; shutting down GPS and starting FFT.");
            if (gps->isOn()) gps->off();
            SPI1.end();
            g_shutdownDone = true;
            ledDone();
#if ENABLE_FFT
            fftEnabled = true;
#endif
        }

        if (g_requestPublish && !g_shutdownDone) {
            g_requestPublish = false;
#if ENABLE_PUBLISH
            bool ok = publishTrackerJSON();
#else
            bool ok = true;
#endif
            if (gps->isOn()) gps->off();
            SPI1.end();
            g_shutdownDone = true;
            ok ? ledDone() : ledMagenta();
            Log.info("GPS off, SPI1 ended. FFT will start.");
#if ENABLE_FFT
            fftEnabled = true;
#endif
        }

        static unsigned long lastPrintGPS = 0;
        if (millis() - lastPrintGPS >= 5000) {
            printGPSData();
            lastPrintGPS = millis();
        }
    }

#if ENABLE_FFT
    if (fftEnabled) {
        setStage(ST_FFT_SAMPLE); rollCrumbForNextBoot();
        for (size_t i = 0; i < NUM_SAMPLES; i++) {
            while (micros() - lastSampleTime < SAMPLE_INTERVAL_US) { /* spin */ }
            lastSampleTime = micros();
            rawBuffer[i]   = analogRead(MIC_PIN);
        }

        for (size_t i = 0; i < NUM_SAMPLES; i++) {
            float centered = (float)rawBuffer[i] - 2048.0f;
            lp_prev = lp_prev + lp_alpha * (centered - lp_prev);
            floatBuffer[i] = lp_prev;
        }

        double sum = 0;
        for (size_t i = 0; i < NUM_SAMPLES; i++) sum += floatBuffer[i];
        double mean = sum / NUM_SAMPLES;
        for (size_t i = 0; i < NUM_SAMPLES; i++) {
            fftBuf[i] = complex<double>(floatBuffer[i] - mean, 0.0);
        }

        Fft::transformRadix2(fftBuf);

        double mag1=0, mag2=0; size_t bin1=0, bin2=0;
        peakInBand(fftBuf, NUM_SAMPLES, lowHz1, highHz1, mag1, bin1);
        peakInBand(fftBuf, NUM_SAMPLES, lowHz2, highHz2, mag2, bin2);
        double freq1 = (double)bin1 * SAMPLE_FREQ / NUM_SAMPLES;
        double freq2 = (double)bin2 * SAMPLE_FREQ / NUM_SAMPLES;

        if (mag1 > winMax1){ winMax1 = mag1; winFreq1 = freq1; }
        if (mag2 > winMax2){ winMax2 = mag2; winFreq2 = freq2; }

        if (millis() - windowStart >= WINDOW_MS) {
            Log.info("FFT bands: B1 amp=%.3f @ %.1f Hz | B2 amp=%.3f @ %.1f Hz",
                     winMax1, winFreq1, winMax2, winFreq2);

            if (winMax1 > pubMax1){ pubMax1 = winMax1; pubFreq1 = winFreq1; }
            if (winMax2 > pubMax2){ pubMax2 = winMax2; pubFreq2 = winFreq2; }
            winMax1 = winFreq1 = 0.0;
            winMax2 = winFreq2 = 0.0;
            windowStart = millis();
        }

#if ENABLE_PUBLISH
        if (millis() - publishStart >= publishIntervalMs && shouldPublishNow()) {
            setStage(ST_FFT_PUBLISH); rollCrumbForNextBoot();
            char payload[192];
            snprintf(payload, sizeof(payload),
                "{\"b1_amp\":%.3f,\"b1_freq\":%.1f,"
                 "\"b2_amp\":%.3f,\"b2_freq\":%.1f,"
                 "\"cleanup\":%.0f,"
                 "\"bands\":\"%.0f/%.0f|%.0f/%.0f\"}",
                pubMax1, pubFreq1, pubMax2, pubFreq2,
                cleanupHz, lowHz1, highHz1, lowHz2, highHz2);

            Log.info("[FFT] publish: %s", payload);
            delay(15); // breather before TX burst
            Particle.publish("bandPeak10m", payload, PRIVATE);
            lastPublishAt = millis();

            pubMax1 = pubFreq1 = 0.0;
            pubMax2 = pubFreq2 = 0.0;
            publishStart = millis();
            logCellSignal("post-fft-publish");
        }
#endif
    }
#endif
}