diff --git a/src/main.cpp b/src/main.cpp
index d8e0072..a764851 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -66,6 +66,18 @@
#define FY_NEOPIXEL_BRIGHTNESS 50
#define FY_NEOPIXEL_DETECTION_BRIGHTNESS 200
+// Battery monitoring
+// XIAO ESP32-S3 has no dedicated battery ADC pin. To enable battery monitoring:
+// 1. Wire a voltage divider (2x 200K resistors) from BAT+ to an ADC pin
+// 2. Set BATTERY_ADC_PIN to that GPIO number (e.g. 1 for A0/D0)
+// 3. The code reads voltage, applies divider ratio, maps to 0-100%
+// Set to -1 to disable (no hardware voltage divider connected)
+#define BATTERY_ADC_PIN -1 // -1 = disabled, set to GPIO with voltage divider
+#define BATTERY_SAMPLES 16 // ADC samples to average (noise reduction)
+#define BATTERY_FULL_V 4.2f // LiPo full charge voltage
+#define BATTERY_EMPTY_V 3.0f // LiPo cutoff voltage
+#define BATTERY_DIVIDER 2.0f // Voltage divider ratio (2x 200K = /2)
+
// BLE scanning
#define BLE_SCAN_DURATION 2 // seconds per scan
#define BLE_SCAN_INTERVAL 3000 // ms between scans
@@ -166,11 +178,17 @@ struct FYDetection {
int count;
bool isRaven;
char ravenFW[16];
- // GPS from phone (wardriving)
+ // GPS from phone (wardriving) — first detection position
double gpsLat;
double gpsLon;
float gpsAcc;
bool hasGPS;
+ // Peak-RSSI GPS — position at strongest signal (closest approach to device)
+ int bestRSSI; // Strongest (least negative) RSSI seen
+ double bestGPSLat;
+ double bestGPSLon;
+ float bestGPSAcc;
+ bool hasBestGPS;
};
static FYDetection fyDet[MAX_DETECTIONS];
@@ -221,6 +239,31 @@ static unsigned long fyLastSave = 0;
static int fyLastSaveCount = 0; // Track changes to avoid unnecessary writes
static bool fySpiffsReady = false;
+// Battery monitoring
+static int fyBatteryPercent = -1; // -1 = no ADC available
+static float fyBatteryVoltage = 0;
+static unsigned long fyLastBattRead = 0;
+#define BATTERY_READ_INTERVAL 10000 // Read every 10 seconds
+
+static void fyReadBattery() {
+#if BATTERY_ADC_PIN >= 0
+ if (millis() - fyLastBattRead < BATTERY_READ_INTERVAL && fyLastBattRead > 0) return;
+ fyLastBattRead = millis();
+ uint32_t raw = 0;
+ for (int i = 0; i < BATTERY_SAMPLES; i++) {
+ raw += analogReadMilliVolts(BATTERY_ADC_PIN);
+ }
+ float mv = (float)raw / BATTERY_SAMPLES;
+ fyBatteryVoltage = mv * BATTERY_DIVIDER / 1000.0f;
+ int pct = (int)((fyBatteryVoltage - BATTERY_EMPTY_V) / (BATTERY_FULL_V - BATTERY_EMPTY_V) * 100.0f);
+ if (pct > 100) pct = 100;
+ if (pct < 0) pct = 0;
+ fyBatteryPercent = pct;
+#else
+ fyBatteryPercent = -1;
+#endif
+}
+
// ============================================================================
// AUDIO SYSTEM
// ============================================================================
@@ -612,6 +655,14 @@ static int fyAddDetection(const char* mac, const char* name, int rssi,
}
// Update GPS on every re-sighting (captures movement)
fyAttachGPS(fyDet[i]);
+ // Track peak RSSI — strongest signal = closest approach to device
+ if (rssi > fyDet[i].bestRSSI && fyGPSIsFresh()) {
+ fyDet[i].bestRSSI = rssi;
+ fyDet[i].bestGPSLat = fyGPSLat;
+ fyDet[i].bestGPSLon = fyGPSLon;
+ fyDet[i].bestGPSAcc = fyGPSAcc;
+ fyDet[i].hasBestGPS = true;
+ }
xSemaphoreGive(fyMutex);
return i;
}
@@ -629,14 +680,22 @@ static int fyAddDetection(const char* mac, const char* name, int rssi,
}
}
d.rssi = rssi;
+ d.bestRSSI = rssi; // First sighting = initial best
strncpy(d.method, method, sizeof(d.method) - 1);
d.firstSeen = millis();
d.lastSeen = millis();
d.count = 1;
d.isRaven = isRaven;
strncpy(d.ravenFW, ravenFW ? ravenFW : "", sizeof(d.ravenFW) - 1);
- // Attach GPS from phone
+ // Attach GPS from phone (first-seen position)
fyAttachGPS(d);
+ // Initialize best GPS to first-seen GPS
+ if (d.hasGPS) {
+ d.bestGPSLat = d.gpsLat;
+ d.bestGPSLon = d.gpsLon;
+ d.bestGPSAcc = d.gpsAcc;
+ d.hasBestGPS = true;
+ }
int idx = fyDetCount++;
xSemaphoreGive(fyMutex);
return idx;
@@ -765,11 +824,17 @@ static void writeDetectionsJSON(AsyncResponseStream *resp) {
fyDet[i].mac, fyDet[i].name, fyDet[i].rssi, fyDet[i].method,
fyDet[i].firstSeen, fyDet[i].lastSeen, fyDet[i].count,
fyDet[i].isRaven ? "true" : "false", fyDet[i].ravenFW);
- // Append GPS if present
+ // Append GPS if present (first-seen position)
if (fyDet[i].hasGPS) {
resp->printf(",\"gps\":{\"lat\":%.8f,\"lon\":%.8f,\"acc\":%.1f}",
fyDet[i].gpsLat, fyDet[i].gpsLon, fyDet[i].gpsAcc);
}
+ // Append best GPS (peak-RSSI = closest approach position)
+ if (fyDet[i].hasBestGPS) {
+ resp->printf(",\"best_rssi\":%d,\"best_gps\":{\"lat\":%.8f,\"lon\":%.8f,\"acc\":%.1f}",
+ fyDet[i].bestRSSI,
+ fyDet[i].bestGPSLat, fyDet[i].bestGPSLon, fyDet[i].bestGPSAcc);
+ }
resp->print("}");
}
xSemaphoreGive(fyMutex);
@@ -801,6 +866,10 @@ static void fySaveSession() {
if (d.hasGPS) {
f.printf(",\"gps\":{\"lat\":%.8f,\"lon\":%.8f,\"acc\":%.1f}", d.gpsLat, d.gpsLon, d.gpsAcc);
}
+ if (d.hasBestGPS) {
+ f.printf(",\"best_rssi\":%d,\"best_gps\":{\"lat\":%.8f,\"lon\":%.8f,\"acc\":%.1f}",
+ d.bestRSSI, d.bestGPSLat, d.bestGPSLon, d.bestGPSAcc);
+ }
f.print("}");
}
f.print("]");
@@ -866,7 +935,11 @@ static void writeDetectionsKML(AsyncResponseStream *resp) {
if (fyMutex && xSemaphoreTake(fyMutex, pdMS_TO_TICKS(300)) == pdTRUE) {
for (int i = 0; i < fyDetCount; i++) {
FYDetection& d = fyDet[i];
- if (!d.hasGPS) continue; // Skip detections without GPS
+ if (!d.hasGPS && !d.hasBestGPS) continue; // Skip detections without any GPS
+ // Use best GPS (closest approach) if available, else first-seen GPS
+ double pinLat = d.hasBestGPS ? d.bestGPSLat : d.gpsLat;
+ double pinLon = d.hasBestGPS ? d.bestGPSLon : d.gpsLon;
+ float pinAcc = d.hasBestGPS ? d.bestGPSAcc : d.gpsAcc;
resp->print("
", d.name);
resp->printf("Method: %s
"
"RSSI: %d dBm
"
+ "Best RSSI: %d dBm
"
"Count: %d
",
- d.method, d.rssi, d.count);
+ d.method, d.rssi, d.hasBestGPS ? d.bestRSSI : d.rssi, d.count);
if (d.isRaven) resp->printf("Raven FW: %s
", d.ravenFW);
- resp->printf("Accuracy: %.1f m", d.gpsAcc);
+ resp->printf("Accuracy: %.1f m", pinAcc);
resp->print("]]>\n");
resp->printf("