I have an issue with my monitor one using the expansion board with a microphone attached. I have a Max9814 connected using A5 as out and A4 for gain. The issue I have is after doing a GPS sync and restarting the device the voltage on A5 stays at 2.8 volts. Prior to the GPS sync it is 1.25 which is where it should be. The only way I can get the voltage to go back to 1.25 is to flash new software. I have tried various reset and hibernate options and the it stays at 2.8 volts most likely due to SPI. Any ideas?
Thanks in advance. I put half the code in here since it was too long.
Copy
// =============================== main.cpp ===============================
#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>
// DMA ADC on Gen3
#include "ADCDMAGen3_RK.h"
#include "AB1805_RK.h"
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
using namespace std;
using namespace std::chrono;
SYSTEM_MODE(SEMI_AUTOMATIC);
SYSTEM_THREAD(ENABLED);
// AB1805 RTC/Watchdog - used to trigger hardware reset after GPS
// which cycles the PMIC power rails and resets the MAX9814 AGC
AB1805 ab1805(Wire); // Tracker SoM AB1805 is on Wire (I2C0, D0/D1)
// ================= Feature toggles =================
#define ENABLE_FFT 1
#define ENABLE_PUBLISH 1
#define LOG_CELL_EVERY_MS 0
// ---- diagnostic & stability toggles ----
#define USE_HW_WATCHDOG 1
#define SAFE_BACKOFF_MAX 10
#define SAFE_BACKOFF_MS 120000
// ================= 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
#ifndef MONITORONE_USER_BUTTON
#define MONITORONE_USER_BUTTON D2
#endif
// ================== Logging ==================
SerialLogHandler logHandler(115200, LOG_LEVEL_WARN, {
{"app", LOG_LEVEL_INFO},
{"app.gps.nmea", LOG_LEVEL_NONE},
{"app.gps.ubx", LOG_LEVEL_NONE},
{"system", LOG_LEVEL_WARN},
{"system.nm", LOG_LEVEL_NONE},
{"comm.dtls", LOG_LEVEL_NONE},
{"comm.protocol", LOG_LEVEL_NONE},
{"net", LOG_LEVEL_NONE}
});
// =================== Watchdog ======================
static const uint32_t WD_TIMEOUT_MS = 300000;
// =================== Reset reason helpers ======================
static const char* resetReasonName(int rr) {
switch (rr) {
case 10: return "POWER";
case 20: return "PIN";
case 50: return "BROWNOUT";
case 60: return "RTC_WATCHDOG";
case 70: return "INTERNAL_WATCHDOG";
case 80: return "UPDATE";
case 90: return "SOFT";
default: return "?";
}
}
static const char* resetReasonHint(int rr) {
switch (rr) {
case 20: return "pin_reset_possible_device_or_noise";
case 60: return "ext_watchdog_ab1805";
case 70: return "mcu_watchdog_internal";
default: return "";
}
}
// =================== Breadcrumbs ===================
struct Crumb { uint32_t magic, stage, ms, rr, rrData; };
static constexpr uint32_t CRUMB_MAGIC = 0xA5A5A5A5;
retained Crumb bc_prev = {0};
Crumb bc_last = {0};
Crumb bc_curr = {0};
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 {
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();
Log.info("Reset reason=%lu (%s) data=0x%08lx",
(unsigned long)bc_curr.rr, resetReasonName((int)bc_curr.rr),
(unsigned long)bc_curr.rrData);
}
// ============= Retained reboot window =============
struct RetainedDiag { uint32_t magic, bootCount, lastBootMs, lastBootMillisStamp; };
retained RetainedDiag rdiag = {0};
static void updateRebootWindow() {
uint32_t now = millis();
if (rdiag.magic != 0xBEEFCAFE) {
rdiag.magic = 0xBEEFCAFE;
rdiag.bootCount = 0;
rdiag.lastBootMs = now;
rdiag.lastBootMillisStamp = now;
}
if ((now - rdiag.lastBootMillisStamp) <= SAFE_BACKOFF_MS)
rdiag.bootCount++;
else
rdiag.bootCount = 1;
rdiag.lastBootMillisStamp = now;
}
static bool inSafeBackoff() { return (rdiag.bootCount >= SAFE_BACKOFF_MAX); }
// ============= Retained GPS lock status =============
struct RetainedGPS { uint32_t magic; uint32_t lastPublishEpoch; };
static constexpr uint32_t GPS_MAGIC = 0xC0FFEE01;
retained RetainedGPS gpsStatus = {0};
static inline bool hasEverHadGPSLock() {
return (gpsStatus.magic == GPS_MAGIC && gpsStatus.lastPublishEpoch > 0);
}
// Retained hibernate wake counter - tracks how many hibernate cycles
// have occurred since GPS session ended. MAX9814 AGC needs multiple
// full power cycles to fully reset after SPI1 drives A5 to 3.3V.
struct RetainedHibernate { uint32_t magic; uint32_t wakeCount; };
static constexpr uint32_t HIB_MAGIC = 0xDECAFBAD;
retained RetainedHibernate hibStatus = {0};
static const uint32_t HIB_CYCLES_NEEDED = 3; // number of power cycles to reset MAX9814
// ================= 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;
// ================= GPS state =================
// GPS starts at boot automatically. Triple-tap or timeout shuts it down.
// No hibernate, no Particle.disconnect() - same stable architecture as
// the version that ran for months without PIN reset issues.
bool g_gpsActive = false;
bool g_gpsShutdownDone = false;
bool g_gpsLockAchieved = false;
bool g_forceGPSShutdown = false;
bool g_startGPSRequested = false; // set by triple-tap to start GPS on demand
bool g_flushFFTBuffers = false; // set after GPS shutdown to discard stale ADC data
unsigned long g_gpsStartMs = 0;
unsigned long g_gpsFirstLockMs = 0;
const unsigned long GPS_TIMEOUT_MS = 5UL * 60UL * 1000UL;
const unsigned long GPS_DWELL_MS = 60000;
int g_reqId = 1;
// ================= LED state machine =================
// Red = FFT running, no GPS lock ever recorded - needs sync
// Green = FFT running, GPS lock confirmed at least once
// Orange flash = GPS active, searching
// Green solid = GPS locked during dwell
inline void ledNoGPS() {
RGB.control(true);
RGB.color(255, 0, 0);
}
inline void ledFFTWithGPS() {
RGB.control(true);
RGB.color(0, 255, 0);
}
inline void ledSearching() {
static unsigned long lastToggle = 0;
static bool ledOn = false;
if (millis() - lastToggle >= 500) {
ledOn = !ledOn;
RGB.control(true);
RGB.color(ledOn ? 255 : 0, ledOn ? 165 : 0, 0);
lastToggle = millis();
}
}
inline void ledLocked() {
RGB.control(true);
RGB.color(0, 255, 0);
}
inline void ledFFTActive() {
if (hasEverHadGPSLock())
ledFFTWithGPS();
else
ledNoGPS();
}
// ================= FFT config & state =================
constexpr uint32_t SAMPLE_FREQ = 8000;
constexpr size_t NUM_SAMPLES = 1024;
constexpr unsigned long WINDOW_MS = 2000;
unsigned long publishIntervalMs = 10UL * 60000UL;
constexpr int FFT_SKIP_FACTOR = 1;
static int fftSkipCounter = 0;
double lowHz1 = 900.0, highHz1 = 1200.0;
double lowHz2 = 1700.0, highHz2 = 2200.0;
double hitThreshold1 = 800.0;
double hitThreshold2 = 900.0;
int hitCount1 = 0;
int hitCount2 = 0;
double cleanupHz = 3500.0;
static float lp_prev = 0.0f;
static float lp_alpha = 0.0f;
vector<complex<float>> fftBuf(NUM_SAMPLES);
static float floatBuffer[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;
static const unsigned long FFT_LOG_PERIOD_MS = 2000;
static unsigned long lastFftLog = 0;
bool fftEnabled = false; // enabled after GPS shuts down
int gainState = 1;
// ================= Cellular cooldown =================
// Simple timer-based suppression of FFT data during/after RF activity.
// No modem state polling - avoids interference with the nRF52840.
static unsigned long cellCooldownUntil = 0;
static const unsigned long CELL_COOLDOWN_MS = 5000;
static inline bool cellIdle() { return (millis() > cellCooldownUntil); }
static inline void setCellCooldown(){ cellCooldownUntil = millis() + CELL_COOLDOWN_MS; }
// Restore A5 (SPI1 SCK = MIC_PIN) to SAADC-compatible analog input mode.
// Particle OS cellular/publish operations reset A5 pin config to digital
// input (INPUT=Connect) which loads down the analog signal and kills ADC.
// nRF52840 SAADC requires INPUT=Disconnect on the sampled pin.
static inline void restoreA5ForADC() {
// P0.31 = A5: Fully release then set for SAADC
// Step 1 removes any residual output driver state left by cellular/SPI ops.
// Step 2 sets INPUT=Disconnect required by nRF52840 SAADC.
NRF_P0->PIN_CNF[31] = 0x00000000UL; // input, driver off, no pull
NRF_P0->PIN_CNF[31] = 0x00000002UL; // INPUT=Disconnect for SAADC
}
// ================= FFT diagnostic state =================
static float maxEverSeen = 0;
static int frameCount = 0;
// ================= DMA ADC (ping-pong) =================
ADCDMAGen3 adc;
const size_t SAMPLES_IN_BUFFER = NUM_SAMPLES;
static nrf_saadc_value_t buffer0[SAMPLES_IN_BUFFER];
static nrf_saadc_value_t buffer1[SAMPLES_IN_BUFFER];
static volatile const nrf_saadc_value_t *g_readyBuf = nullptr;
static volatile size_t g_readyCount = 0;
static volatile uint32_t g_dmaOverruns= 0;
void adcBufferCallback(nrf_saadc_value_t *buf, size_t size) {
if (g_readyBuf != nullptr) g_dmaOverruns++;
g_readyBuf = buf;
g_readyCount = size;
}
// ================= Helpers =================
static inline void computeAlpha() {
const double nyq = SAMPLE_FREQ / 2.0;
const double maxLP = nyq * 0.8;
if (cleanupHz > maxLP) cleanupHz = maxLP;
const float dt = 1.0f / SAMPLE_FREQ;
const float RC = 1.0f / (2.0f * M_PI * cleanupHz);
lp_alpha = dt / (RC + dt);
}
static inline void peakInBand(
const vector<complex<float>>& X, size_t N,
float lo, float hi,
float& outMag, size_t& outBin)
{
size_t s = (size_t)(lo * N / SAMPLE_FREQ);
size_t e = (size_t)(hi * N / SAMPLE_FREQ);
float mSq = 0.0f; size_t mb = s;
for (size_t k = s; k <= e && k < N; ++k) {
float re = X[k].real(), im = X[k].imag();
float aSq = re*re + im*im;
if (aSq > mSq) { mSq = aSq; mb = k; }
}
outMag = sqrtf(mSq);
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() { return System.batteryCharge(); }
inline void safeDelay(unsigned long ms) {
unsigned long start = millis();
while (millis() - start < ms) {
if (USE_HW_WATCHDOG) Watchdog.refresh();
delay(10);
}
}
// ================= 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;
if (USE_HW_WATCHDOG) Watchdog.refresh();
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();
if (USE_HW_WATCHDOG) Watchdog.refresh();
}
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);
}
// Shut down GPS cleanly - no hibernate, no modem changes
// Mirrors the stable approach from old version that ran for months
void shutdownGPS() {
if (!g_gpsActive) return;
if (gps && gps->isOn()) {
gps->off();
Log.info("GPS powered OFF");
}
// Release SPI1's ownership of A5 (SPI SCK = MIC_PIN), then immediately
gps->off();
delete gps;
gps = nullptr;
// Use AB1805 hardware watchdog to trigger a hardware RESET pulse.
// The AB1805 nRST pin is wired to the nRF52840 RESET line - same as
// the expansion card reset button. This causes the PMIC to do a full
// power-on sequence which cycles the 3V3 rail and resets the MAX9814 AGC.
// System.reset() and hibernate do NOT do this - only a hardware RESET pulse works.
Log.info("GPS session complete - triggering AB1805 hardware reset to recover MAX9814");
if (USE_HW_WATCHDOG) Watchdog.stop();
delay(500); // let log flush
// Set AB1805 WDT to minimum (4s) and stop servicing it - it will fire and
// pull nRST low via the hardware RESET line, cycling the PMIC rails
// ab1805 already initialized in setup() - just set WDT directly
ab1805.setWDT(4); // 4 second watchdog - minimum value
// Do NOT service the watchdog - let it expire and pull RESET low
// This does not return normally - device resets via hardware watchdog
unsigned long wdtStart = millis();
while (true) {
// Deliberately NOT calling Watchdog.refresh() or ab1805 service
delay(100);
if (millis() - wdtStart > 30000) {
// Fallback - if AB1805 WDT didn't fire after 30s, force system reset
Log.info("AB1805 WDT fallback - forcing System.reset()");
System.reset();
}
}
}
// Start GPS on demand - called from loop when triple-tap received
void startGPS() {
if (g_gpsActive) return; // already running
Log.info("Starting GPS session on demand...");
// Initialize SPI1 here - not at boot - to avoid driving A5 (MIC_PIN) as
// SCK output before the mic session begins
SPI1.begin();
gps = new ubloxGPS(SPI1, gpsSpiSelect, gpsPowerControl, GPS_INT_PIN, GPS_INT_GPS_PIN);
if (gps) {
digitalWrite(GPS_RST_PIN, LOW); safeDelay(10);
digitalWrite(GPS_RST_PIN, HIGH); safeDelay(20);
gps->on();
Log.info("GPS powered ON");
g_gpsActive = true;
g_gpsShutdownDone = false;
g_gpsLockAchieved = false;
g_gpsFirstLockMs = 0;
g_gpsStartMs = millis();
fftEnabled = false; // pause FFT during GPS
setStage(ST_GPS_ON); rollCrumbForNextBoot();
ledSearching();
} else {
Log.error("Failed to create GPS object");
}
}
// ================= Location publish =================
static unsigned long lastPublishAt = 0;
bool publishTrackerJSON() {
#if !ENABLE_PUBLISH
return true;
#endif
setStage(ST_PUBLISH); rollCrumbForNextBoot();
if (!Particle.connected()) {
Particle.connect();
unsigned long t0 = millis();
while (!Particle.connected() && millis() - t0 < 60000) {
if (USE_HW_WATCHDOG) Watchdog.refresh();
delay(25);
}
}
if (!Particle.connected()) {
Log.warn("publishTrackerJSON: not connected, skipping");
return false;
}
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\":[\"button\"],\"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 (USE_HW_WATCHDOG) Watchdog.refresh();
Log.info("Publishing location...");
bool ok = Particle.publish("loc", buf, PRIVATE);
lastPublishAt = millis();
safeDelay(3000); // let publish transmit before GPS shutdown begins
restoreA5ForADC(); // cellular ops reset A5 pin config - restore for SAADC
if (USE_HW_WATCHDOG) Watchdog.refresh();
Log.info("Location publish %s", ok ? "queued" : "failed");
if (ok) {
gpsStatus.magic = GPS_MAGIC;
gpsStatus.lastPublishEpoch = (uint32_t)Time.now();
Log.info("GPS lock recorded in retained memory");
}
return ok;
}
// ================= Cloud API =================
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) {
if (!g_gpsActive) {
g_startGPSRequested = true;
Log.info("simulateTriple: GPS start requested via cloud");
} else {
g_forceGPSShutdown = true;
Log.info("simulateTriple: GPS shutdown requested via cloud");
}
return 1;
}
int fn_setHitThresh1(String s) {
double v=s.toFloat();
if(v>0&&v<100000){ hitThreshold1=v; Log.info("Hit threshold 1: %.1f", hitThreshold1); return 1;}
return -1;
}
int fn_setHitThresh2(String s) {
double v=s.toFloat();
if(v>0&&v<100000){ hitThreshold2=v; Log.info("Hit threshold 2: %.1f", hitThreshold2); return 1;}
return -1;
}
int fn_enterShippingMode(String s) {
Log.info("Entering shipping mode");
if (Particle.connected()) { Particle.publish("status","entering_shipping_mode",PRIVATE); delay(1000); }
if (g_gpsActive) shutdownGPS();
adc.stop();
RGB.control(false);
SystemSleepConfiguration config;
config.mode(SystemSleepMode::HIBERNATE).gpio(MONITORONE_USER_BUTTON, FALLING);
delay(100);
System.sleep(config);
return 1;
}
// ================= Button =================
#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) {
if (!g_gpsActive && !g_gpsShutdownDone) {
g_startGPSRequested = true;
Log.info("Triple-tap: starting GPS session");
} else if (g_gpsActive) {
g_forceGPSShutdown = true;
Log.info("Triple-tap: forcing GPS shutdown");
} else {
Log.info("Triple-tap: GPS already completed this boot");
}
}
if (taps == 5) {
Log.info("5-tap: entering shipping mode");
fn_enterShippingMode("");
}
}
}
#endif
// ================= Setup =================
void setup() {
Serial.begin(115200);
waitFor(Serial.isConnected, 8000);
capturePrevAndInitCurr();
updateRebootWindow();
if (USE_HW_WATCHDOG) {
particle::WatchdogConfiguration cfg;
cfg.timeout(milliseconds(WD_TIMEOUT_MS));
Watchdog.init(cfg);
Watchdog.start();
Log.info("Watchdog started (timeout=%lums)", (unsigned long)WD_TIMEOUT_MS);
} else {
Watchdog.stop();
Log.warn("Watchdog DISABLED for test");
}
// Initialize AB1805 RTC/watchdog - used to trigger hardware reset after GPS
ab1805.setup();
ab1805.resetConfig();
Particle.keepAlive(30);
#if ENABLE_PUBLISH
Particle.connect();
#endif
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 is initialized on demand in startGPS() - not at boot.
// Calling SPI1.begin() here configures A5 as SCK output immediately,
// which drives the MAX9814 mic output to 3.3V and damages the capsule.
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.variable("hitThresh1",hitThreshold1);
Particle.variable("hitThresh2",hitThreshold2);
Particle.variable("hits1", hitCount1);
Particle.variable("hits2", hitCount2);
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);
Particle.function("setHitTh1", fn_setHitThresh1);
Particle.function("setHitTh2", fn_setHitThresh2);
Particle.function("shipMode", fn_enterShippingMode);
#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
pinMode(GAIN_PIN, OUTPUT);
digitalWrite(GAIN_PIN, HIGH);
gainState = 1;
computeAlpha();
lp_prev = 0.0f;
windowStart = millis();
publishStart = millis();
setStage(ST_SETUP); rollCrumbForNextBoot();
bool safeMode = inSafeBackoff();
if (safeMode) {
Log.error("SAFE MODE: too many reboots (%lu in %lu ms). Skipping GPS and ADCDMA.",
(unsigned long)rdiag.bootCount, (unsigned long)SAFE_BACKOFF_MS);
ledFFTActive();
return;
}
// GPS is ON-DEMAND only - starts when user triple-taps button outdoors.
// Boot goes straight to FFT. This prevents SPI1 activity from stressing
// the MAX9814 mic capsule on every power cycle.
fftEnabled = true;
ledFFTActive();
safeDelay(2000);
// Start ADC - runs continuously through GPS and FFT phases
ret_code_t err = adc
.withSampleFreqHz(SAMPLE_FREQ)
.withDoubleBuffer(SAMPLES_IN_BUFFER, buffer0, buffer1)
.withSamplePin(MIC_PIN)
.withBufferCallback(adcBufferCallback)
.init();
if (err == NRF_SUCCESS) {
adc.start();
Log.info("ADCDMA started @ %lu Hz", (unsigned long)SAMPLE_FREQ);
} else {
Log.error("ADCDMA init failed err=%lu", (unsigned long)err);
}
if (USE_HW_WATCHDOG) Watchdog.refresh();
}
// ================= Loop =================
void loop() {
if (USE_HW_WATCHDOG) Watchdog.refresh();
#if ENABLE_PUBLISH
if (!Particle.connected()) Particle.connect();
#endif
// ---------- Boot diag publish (once) ----------
#if ENABLE_PUBLISH
static bool bootDiagSent = false;
if (Particle.connected() && !bootDiagSent) {
if (USE_HW_WATCHDOG) Watchdog.refresh();
char j[256];
if (bc_last.magic == CRUMB_MAGIC) {
snprintf(j, sizeof(j),
"{\"rr\":%d,\"rr_name\":\"%s\",\"rrd\":%lu,"
"\"hint\":\"%s\",\"boots_in_%lus\":%lu,"
"\"prev_stage\":%lu,\"prev_stage_name\":\"%s\",\"prev_ms\":%lu,"
"\"free\":%lu}",
(int)System.resetReason(), resetReasonName((int)System.resetReason()),
(unsigned long)System.resetReasonData(),
resetReasonHint((int)System.resetReason()),
(unsigned long)(SAFE_BACKOFF_MS/1000), (unsigned long)rdiag.bootCount,
(unsigned long)bc_last.stage, stageName(bc_last.stage),
(unsigned long)bc_last.ms, (unsigned long)System.freeMemory());
} else {
snprintf(j, sizeof(j),
"{\"rr\":%d,\"rr_name\":\"%s\",\"rrd\":%lu,"
"\"hint\":\"%s\",\"boots_in_%lus\":%lu,\"free\":%lu}",
(int)System.resetReason(), resetReasonName((int)System.resetReason()),
(unsigned long)System.resetReasonData(),
resetReasonHint((int)System.resetReason()),
(unsigned long)(SAFE_BACKOFF_MS/1000), (unsigned long)rdiag.bootCount,
(unsigned long)System.freeMemory());
}
Particle.publish("boot_diag", j, PRIVATE);
// Only restore A5 after GPS is done - writing PIN_CNF while SPI1 is
// actively clocking the GPS module causes a SCK glitch and PIN reset
if (!g_gpsActive) restoreA5ForADC();
bootDiagSent = true;
if (USE_HW_WATCHDOG) Watchdog.refresh();
}
#endif
// ================= GPS start on demand =================
if (g_startGPSRequested) {
g_startGPSRequested = false;
startGPS();
}
// ================= GPS phase =================
if (g_gpsActive) {
setStage(ST_GPS_LOOP); rollCrumbForNextBoot();
if (USE_HW_WATCHDOG) Watchdog.refresh();
updateGPSData();
ledSearching(); // flashing orange until lock
static unsigned long lastPrintGPS = 0;
static bool lastLock = false;
bool nowLock = gpsData.locked;
if (nowLock != lastLock || millis() - lastPrintGPS >= 30000UL) {
printGPSData();
lastLock = nowLock;
lastPrintGPS = millis();
}
if (!g_gpsLockAchieved && isGoodFix(gpsData)) {
g_gpsLockAchieved = true;
g_gpsFirstLockMs = millis();
ledLocked(); // solid green
Log.info("GPS lock achieved! Waiting %lus for accuracy refinement...",
(unsigned long)(GPS_DWELL_MS / 1000));
}
if (g_gpsFirstLockMs > 0) {
static unsigned long lastAccLog = 0;
if (millis() - lastAccLog >= 10000) {
Log.info("GPS refining: HAcc=%.1fm, sats=%d, time since lock=%lus",
gpsData.horizontalAccuracy, gpsData.satellites,
(unsigned long)((millis() - g_gpsFirstLockMs) / 1000));
lastAccLog = millis();
}
}
bool timeout = (millis() - g_gpsStartMs >= GPS_TIMEOUT_MS);
bool dwellDone = (g_gpsFirstLockMs > 0 &&
millis() - g_gpsFirstLockMs >= GPS_DWELL_MS);
bool forceShutdown = g_forceGPSShutdown;
if (timeout || dwellDone || forceShutdown) {
g_forceGPSShutdown = false;
if (timeout) Log.warn("GPS timeout after %lus", GPS_TIMEOUT_MS/1000);
if (dwellDone) Log.info("GPS dwell complete. HAcc=%.1fm", gpsData.horizontalAccuracy);
if (forceShutdown) Log.info("GPS force shutdown (triple-tap)");
// Publish location if we have a good fix
if (isGoodFix(gpsData)) {
#if ENABLE_PUBLISH
publishTrackerJSON();
#endif
} else {
Log.warn("GPS shutdown without good fix - no loc publish");
}
shutdownGPS();
}
}