Add peak-RSSI GPS tracking and battery monitoring
Track strongest signal per detection to capture GPS at closest approach to surveillance devices, rather than only first-seen position. All exports (JSON, KML, CSV, SPIFFS, dashboard, radar) updated to use best GPS. Also adds battery ADC monitoring framework (disabled by default until voltage divider hardware is wired) and uptime display in stats bar. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
39e4b7ee34
commit
43b4918f26
1 changed files with 125 additions and 27 deletions
152
src/main.cpp
152
src/main.cpp
|
|
@ -66,6 +66,18 @@
|
||||||
#define FY_NEOPIXEL_BRIGHTNESS 50
|
#define FY_NEOPIXEL_BRIGHTNESS 50
|
||||||
#define FY_NEOPIXEL_DETECTION_BRIGHTNESS 200
|
#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
|
// BLE scanning
|
||||||
#define BLE_SCAN_DURATION 2 // seconds per scan
|
#define BLE_SCAN_DURATION 2 // seconds per scan
|
||||||
#define BLE_SCAN_INTERVAL 3000 // ms between scans
|
#define BLE_SCAN_INTERVAL 3000 // ms between scans
|
||||||
|
|
@ -166,11 +178,17 @@ struct FYDetection {
|
||||||
int count;
|
int count;
|
||||||
bool isRaven;
|
bool isRaven;
|
||||||
char ravenFW[16];
|
char ravenFW[16];
|
||||||
// GPS from phone (wardriving)
|
// GPS from phone (wardriving) — first detection position
|
||||||
double gpsLat;
|
double gpsLat;
|
||||||
double gpsLon;
|
double gpsLon;
|
||||||
float gpsAcc;
|
float gpsAcc;
|
||||||
bool hasGPS;
|
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];
|
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 int fyLastSaveCount = 0; // Track changes to avoid unnecessary writes
|
||||||
static bool fySpiffsReady = false;
|
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
|
// 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)
|
// Update GPS on every re-sighting (captures movement)
|
||||||
fyAttachGPS(fyDet[i]);
|
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);
|
xSemaphoreGive(fyMutex);
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
@ -629,14 +680,22 @@ static int fyAddDetection(const char* mac, const char* name, int rssi,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.rssi = rssi;
|
d.rssi = rssi;
|
||||||
|
d.bestRSSI = rssi; // First sighting = initial best
|
||||||
strncpy(d.method, method, sizeof(d.method) - 1);
|
strncpy(d.method, method, sizeof(d.method) - 1);
|
||||||
d.firstSeen = millis();
|
d.firstSeen = millis();
|
||||||
d.lastSeen = millis();
|
d.lastSeen = millis();
|
||||||
d.count = 1;
|
d.count = 1;
|
||||||
d.isRaven = isRaven;
|
d.isRaven = isRaven;
|
||||||
strncpy(d.ravenFW, ravenFW ? ravenFW : "", sizeof(d.ravenFW) - 1);
|
strncpy(d.ravenFW, ravenFW ? ravenFW : "", sizeof(d.ravenFW) - 1);
|
||||||
// Attach GPS from phone
|
// Attach GPS from phone (first-seen position)
|
||||||
fyAttachGPS(d);
|
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++;
|
int idx = fyDetCount++;
|
||||||
xSemaphoreGive(fyMutex);
|
xSemaphoreGive(fyMutex);
|
||||||
return idx;
|
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].mac, fyDet[i].name, fyDet[i].rssi, fyDet[i].method,
|
||||||
fyDet[i].firstSeen, fyDet[i].lastSeen, fyDet[i].count,
|
fyDet[i].firstSeen, fyDet[i].lastSeen, fyDet[i].count,
|
||||||
fyDet[i].isRaven ? "true" : "false", fyDet[i].ravenFW);
|
fyDet[i].isRaven ? "true" : "false", fyDet[i].ravenFW);
|
||||||
// Append GPS if present
|
// Append GPS if present (first-seen position)
|
||||||
if (fyDet[i].hasGPS) {
|
if (fyDet[i].hasGPS) {
|
||||||
resp->printf(",\"gps\":{\"lat\":%.8f,\"lon\":%.8f,\"acc\":%.1f}",
|
resp->printf(",\"gps\":{\"lat\":%.8f,\"lon\":%.8f,\"acc\":%.1f}",
|
||||||
fyDet[i].gpsLat, fyDet[i].gpsLon, fyDet[i].gpsAcc);
|
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("}");
|
resp->print("}");
|
||||||
}
|
}
|
||||||
xSemaphoreGive(fyMutex);
|
xSemaphoreGive(fyMutex);
|
||||||
|
|
@ -801,6 +866,10 @@ static void fySaveSession() {
|
||||||
if (d.hasGPS) {
|
if (d.hasGPS) {
|
||||||
f.printf(",\"gps\":{\"lat\":%.8f,\"lon\":%.8f,\"acc\":%.1f}", d.gpsLat, d.gpsLon, d.gpsAcc);
|
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("}");
|
||||||
}
|
}
|
||||||
f.print("]");
|
f.print("]");
|
||||||
|
|
@ -866,7 +935,11 @@ static void writeDetectionsKML(AsyncResponseStream *resp) {
|
||||||
if (fyMutex && xSemaphoreTake(fyMutex, pdMS_TO_TICKS(300)) == pdTRUE) {
|
if (fyMutex && xSemaphoreTake(fyMutex, pdMS_TO_TICKS(300)) == pdTRUE) {
|
||||||
for (int i = 0; i < fyDetCount; i++) {
|
for (int i = 0; i < fyDetCount; i++) {
|
||||||
FYDetection& d = fyDet[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("<Placemark>\n");
|
resp->print("<Placemark>\n");
|
||||||
resp->printf("<name>%s</name>\n", d.mac);
|
resp->printf("<name>%s</name>\n", d.mac);
|
||||||
resp->printf("<styleUrl>#%s</styleUrl>\n", d.isRaven ? "raven" : "det");
|
resp->printf("<styleUrl>#%s</styleUrl>\n", d.isRaven ? "raven" : "det");
|
||||||
|
|
@ -874,13 +947,14 @@ static void writeDetectionsKML(AsyncResponseStream *resp) {
|
||||||
if (d.name[0]) resp->printf("<b>Name:</b> %s<br/>", d.name);
|
if (d.name[0]) resp->printf("<b>Name:</b> %s<br/>", d.name);
|
||||||
resp->printf("<b>Method:</b> %s<br/>"
|
resp->printf("<b>Method:</b> %s<br/>"
|
||||||
"<b>RSSI:</b> %d dBm<br/>"
|
"<b>RSSI:</b> %d dBm<br/>"
|
||||||
|
"<b>Best RSSI:</b> %d dBm<br/>"
|
||||||
"<b>Count:</b> %d<br/>",
|
"<b>Count:</b> %d<br/>",
|
||||||
d.method, d.rssi, d.count);
|
d.method, d.rssi, d.hasBestGPS ? d.bestRSSI : d.rssi, d.count);
|
||||||
if (d.isRaven) resp->printf("<b>Raven FW:</b> %s<br/>", d.ravenFW);
|
if (d.isRaven) resp->printf("<b>Raven FW:</b> %s<br/>", d.ravenFW);
|
||||||
resp->printf("<b>Accuracy:</b> %.1f m", d.gpsAcc);
|
resp->printf("<b>Accuracy:</b> %.1f m", pinAcc);
|
||||||
resp->print("]]></description>\n");
|
resp->print("]]></description>\n");
|
||||||
resp->printf("<Point><coordinates>%.8f,%.8f,0</coordinates></Point>\n",
|
resp->printf("<Point><coordinates>%.8f,%.8f,0</coordinates></Point>\n",
|
||||||
d.gpsLon, d.gpsLat);
|
pinLon, pinLat);
|
||||||
resp->print("</Placemark>\n");
|
resp->print("</Placemark>\n");
|
||||||
}
|
}
|
||||||
xSemaphoreGive(fyMutex);
|
xSemaphoreGive(fyMutex);
|
||||||
|
|
@ -974,6 +1048,7 @@ h4{color:var(--a1);font-size:14px;margin-bottom:8px}
|
||||||
<div class="sc"><div class="n" id="sR">0</div><div class="l">RAVEN</div></div>
|
<div class="sc"><div class="n" id="sR">0</div><div class="l">RAVEN</div></div>
|
||||||
<div class="sc"><div class="n" id="sB">ON</div><div class="l">BLE+WiFi</div></div>
|
<div class="sc"><div class="n" id="sB">ON</div><div class="l">BLE+WiFi</div></div>
|
||||||
<div class="sc" onclick="reqGPS()" style="cursor:pointer"><div class="n" id="sG" style="font-size:14px">TAP</div><div class="l" id="sGL">GPS</div></div>
|
<div class="sc" onclick="reqGPS()" style="cursor:pointer"><div class="n" id="sG" style="font-size:14px">TAP</div><div class="l" id="sGL">GPS</div></div>
|
||||||
|
<div class="sc"><div class="n" id="sBat" style="font-size:12px">--</div><div class="l" id="sBatL">UPTIME</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tb">
|
<div class="tb">
|
||||||
<button class="a" onclick="tab(0,this)">LIVE</button>
|
<button class="a" onclick="tab(0,this)">LIVE</button>
|
||||||
|
|
@ -1077,7 +1152,7 @@ return '<div class="det t-'+t+'"><div class="mac">'+d.mac+(d.name?'<span class="
|
||||||
+'<span>RSSI: '+d.rssi+'</span><span>'+d.method+'</span>'
|
+'<span>RSSI: '+d.rssi+'</span><span>'+d.method+'</span>'
|
||||||
+'<span style="color:var(--a1);font-weight:bold">×'+d.count+'</span>'
|
+'<span style="color:var(--a1);font-weight:bold">×'+d.count+'</span>'
|
||||||
+(d.raven?'<span class="rv">RAVEN '+d.fw+'</span>':'')
|
+(d.raven?'<span class="rv">RAVEN '+d.fw+'</span>':'')
|
||||||
+(d.gps?'<span style="color:#22c55e">◉ '+d.gps.lat.toFixed(5)+','+d.gps.lon.toFixed(5)+'</span>':'<span style="color:#666">no gps</span>')
|
+(d.best_gps?'<span style="color:#22c55e" title="Peak RSSI: '+d.best_rssi+'dBm">◉ '+d.best_gps.lat.toFixed(5)+','+d.best_gps.lon.toFixed(5)+'</span>':d.gps?'<span style="color:#a3e635">◉ '+d.gps.lat.toFixed(5)+','+d.gps.lon.toFixed(5)+'</span>':'<span style="color:#666">no gps</span>')
|
||||||
+'</div></div>';}
|
+'</div></div>';}
|
||||||
// === STATS ===
|
// === STATS ===
|
||||||
function stats(){document.getElementById('sT').textContent=D.length;
|
function stats(){document.getElementById('sT').textContent=D.length;
|
||||||
|
|
@ -1089,6 +1164,10 @@ else if(s.gps_src==='phone'){g.textContent=s.gps_tagged+'/'+s.total;g.style.colo
|
||||||
else if(s.gps_hw_detected){g.textContent=s.gps_sats+'sat';g.style.color='#facc15';gl.textContent='NO FIX';}
|
else if(s.gps_hw_detected){g.textContent=s.gps_sats+'sat';g.style.color='#facc15';gl.textContent='NO FIX';}
|
||||||
else{g.textContent='TAP';g.style.color='#ef4444';gl.textContent='GPS';}
|
else{g.textContent='TAP';g.style.color='#ef4444';gl.textContent='GPS';}
|
||||||
if(s.device_lat&&s.device_lon)_dGPS={lat:s.device_lat,lon:s.device_lon};else _dGPS=null;
|
if(s.device_lat&&s.device_lon)_dGPS={lat:s.device_lat,lon:s.device_lon};else _dGPS=null;
|
||||||
|
let bt=document.getElementById('sBat'),bl=document.getElementById('sBatL');
|
||||||
|
if(s.batt_pct>=0){bt.textContent=s.batt_pct+'%';bl.textContent=s.batt_v.toFixed(1)+'V';
|
||||||
|
bt.style.color=s.batt_pct>20?'#22c55e':s.batt_pct>10?'#facc15':'#ef4444';}
|
||||||
|
else{bt.textContent=s.uptime;bl.textContent='UPTIME';}
|
||||||
}).catch(()=>{});}
|
}).catch(()=>{});}
|
||||||
// === RADAR ===
|
// === RADAR ===
|
||||||
function togRadar(){_rO=!_rO;
|
function togRadar(){_rO=!_rO;
|
||||||
|
|
@ -1124,8 +1203,8 @@ ctx.fillStyle=grd;ctx.beginPath();ctx.arc(cx,cy,mR,0,Math.PI*2);ctx.fill();
|
||||||
// blips
|
// blips
|
||||||
D.forEach(det=>{
|
D.forEach(det=>{
|
||||||
const rssi=Math.abs(det.rssi),dist=Math.max(0,Math.min(1,(rssi-30)/60))*mR;
|
const rssi=Math.abs(det.rssi),dist=Math.max(0,Math.min(1,(rssi-30)/60))*mR;
|
||||||
let ang;
|
let ang;const dg=det.best_gps||det.gps;
|
||||||
if(_dGPS&&det.gps)ang=bearing(_dGPS.lat,_dGPS.lon,det.gps.lat,det.gps.lon);
|
if(_dGPS&&dg)ang=bearing(_dGPS.lat,_dGPS.lon,dg.lat,dg.lon);
|
||||||
else ang=(macHash(det.mac)%628)/100;
|
else ang=(macHash(det.mac)%628)/100;
|
||||||
const bx=cx+dist*Math.cos(ang),by=cy+dist*Math.sin(ang);
|
const bx=cx+dist*Math.cos(ang),by=cy+dist*Math.sin(ang);
|
||||||
const t=dtype(det);let col=cs.getPropertyValue('--bl-other').trim();
|
const t=dtype(det);let col=cs.getPropertyValue('--bl-other').trim();
|
||||||
|
|
@ -1235,20 +1314,26 @@ static void fySetupServer() {
|
||||||
const char* gpsSrc = "none";
|
const char* gpsSrc = "none";
|
||||||
if (fyGPSIsHardware && fyHWGPSFix) gpsSrc = "hw";
|
if (fyGPSIsHardware && fyHWGPSFix) gpsSrc = "hw";
|
||||||
else if (fyGPSIsFresh()) gpsSrc = "phone";
|
else if (fyGPSIsFresh()) gpsSrc = "phone";
|
||||||
char buf[448];
|
unsigned long upSec = millis() / 1000;
|
||||||
|
int upH = upSec / 3600, upM = (upSec % 3600) / 60;
|
||||||
|
char buf[512];
|
||||||
snprintf(buf, sizeof(buf),
|
snprintf(buf, sizeof(buf),
|
||||||
"{\"total\":%d,\"raven\":%d,\"ble\":\"active\",\"wifi\":\"active\","
|
"{\"total\":%d,\"raven\":%d,\"ble\":\"active\",\"wifi\":\"active\","
|
||||||
"\"wifi_det\":%d,"
|
"\"wifi_det\":%d,"
|
||||||
"\"gps_valid\":%s,\"gps_age\":%lu,\"gps_tagged\":%d,"
|
"\"gps_valid\":%s,\"gps_age\":%lu,\"gps_tagged\":%d,"
|
||||||
"\"gps_src\":\"%s\",\"gps_sats\":%d,\"gps_hw_detected\":%s,"
|
"\"gps_src\":\"%s\",\"gps_sats\":%d,\"gps_hw_detected\":%s,"
|
||||||
"\"device_lat\":%.8f,\"device_lon\":%.8f}",
|
"\"device_lat\":%.8f,\"device_lon\":%.8f,"
|
||||||
|
"\"batt_pct\":%d,\"batt_v\":%.2f,"
|
||||||
|
"\"uptime\":\"%dh%02dm\"}",
|
||||||
fyDetCount, raven, fyWifiDetCount,
|
fyDetCount, raven, fyWifiDetCount,
|
||||||
fyGPSIsFresh() ? "true" : "false",
|
fyGPSIsFresh() ? "true" : "false",
|
||||||
fyGPSValid ? (millis() - fyGPSLastUpdate) : 0UL,
|
fyGPSValid ? (millis() - fyGPSLastUpdate) : 0UL,
|
||||||
withGPS,
|
withGPS,
|
||||||
gpsSrc, fyHWGPSSats,
|
gpsSrc, fyHWGPSSats,
|
||||||
fyHWGPSDetected ? "true" : "false",
|
fyHWGPSDetected ? "true" : "false",
|
||||||
fyGPSLat, fyGPSLon);
|
fyGPSLat, fyGPSLon,
|
||||||
|
fyBatteryPercent, fyBatteryVoltage,
|
||||||
|
upH, upM);
|
||||||
r->send(200, "application/json", buf);
|
r->send(200, "application/json", buf);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1315,21 +1400,23 @@ static void fySetupServer() {
|
||||||
fyServer.on("/api/export/csv", HTTP_GET, [](AsyncWebServerRequest *r) {
|
fyServer.on("/api/export/csv", HTTP_GET, [](AsyncWebServerRequest *r) {
|
||||||
AsyncResponseStream *resp = r->beginResponseStream("text/csv");
|
AsyncResponseStream *resp = r->beginResponseStream("text/csv");
|
||||||
resp->addHeader("Content-Disposition", "attachment; filename=\"dantir_detections.csv\"");
|
resp->addHeader("Content-Disposition", "attachment; filename=\"dantir_detections.csv\"");
|
||||||
resp->println("mac,name,rssi,method,first_seen_ms,last_seen_ms,count,is_raven,raven_fw,latitude,longitude,gps_accuracy");
|
resp->println("mac,name,rssi,method,first_seen_ms,last_seen_ms,count,is_raven,raven_fw,latitude,longitude,gps_accuracy,best_rssi,best_latitude,best_longitude,best_gps_accuracy");
|
||||||
if (fyMutex && xSemaphoreTake(fyMutex, pdMS_TO_TICKS(200)) == pdTRUE) {
|
if (fyMutex && xSemaphoreTake(fyMutex, pdMS_TO_TICKS(200)) == pdTRUE) {
|
||||||
for (int i = 0; i < fyDetCount; i++) {
|
for (int i = 0; i < fyDetCount; i++) {
|
||||||
FYDetection& d = fyDet[i];
|
FYDetection& d = fyDet[i];
|
||||||
|
resp->printf("\"%s\",\"%s\",%d,\"%s\",%lu,%lu,%d,%s,\"%s\"",
|
||||||
|
d.mac, d.name, d.rssi, d.method,
|
||||||
|
d.firstSeen, d.lastSeen, d.count,
|
||||||
|
d.isRaven ? "true" : "false", d.ravenFW);
|
||||||
if (d.hasGPS) {
|
if (d.hasGPS) {
|
||||||
resp->printf("\"%s\",\"%s\",%d,\"%s\",%lu,%lu,%d,%s,\"%s\",%.8f,%.8f,%.1f\n",
|
resp->printf(",%.8f,%.8f,%.1f", d.gpsLat, d.gpsLon, d.gpsAcc);
|
||||||
d.mac, d.name, d.rssi, d.method,
|
|
||||||
d.firstSeen, d.lastSeen, d.count,
|
|
||||||
d.isRaven ? "true" : "false", d.ravenFW,
|
|
||||||
d.gpsLat, d.gpsLon, d.gpsAcc);
|
|
||||||
} else {
|
} else {
|
||||||
resp->printf("\"%s\",\"%s\",%d,\"%s\",%lu,%lu,%d,%s,\"%s\",,,\n",
|
resp->print(",,,");
|
||||||
d.mac, d.name, d.rssi, d.method,
|
}
|
||||||
d.firstSeen, d.lastSeen, d.count,
|
if (d.hasBestGPS) {
|
||||||
d.isRaven ? "true" : "false", d.ravenFW);
|
resp->printf(",%d,%.8f,%.8f,%.1f\n", d.bestRSSI, d.bestGPSLat, d.bestGPSLon, d.bestGPSAcc);
|
||||||
|
} else {
|
||||||
|
resp->print(",,,\n"); // trailing newline included
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
xSemaphoreGive(fyMutex);
|
xSemaphoreGive(fyMutex);
|
||||||
|
|
@ -1395,21 +1482,27 @@ static void fySetupServer() {
|
||||||
if (!err && doc.is<JsonArray>()) {
|
if (!err && doc.is<JsonArray>()) {
|
||||||
int placed = 0;
|
int placed = 0;
|
||||||
for (JsonObject d : doc.as<JsonArray>()) {
|
for (JsonObject d : doc.as<JsonArray>()) {
|
||||||
|
// Prefer best_gps (peak RSSI = closest approach), fall back to first-seen gps
|
||||||
|
JsonObject bgps = d["best_gps"];
|
||||||
JsonObject gps = d["gps"];
|
JsonObject gps = d["gps"];
|
||||||
if (!gps || !gps.containsKey("lat")) continue;
|
JsonObject pinGPS = (bgps && bgps["lat"]) ? bgps : gps;
|
||||||
|
if (!pinGPS || !pinGPS["lat"]) continue;
|
||||||
bool isRaven = d["raven"] | false;
|
bool isRaven = d["raven"] | false;
|
||||||
resp->printf("<Placemark><name>%s</name>\n", d["mac"] | "?");
|
resp->printf("<Placemark><name>%s</name>\n", d["mac"] | "?");
|
||||||
resp->printf("<styleUrl>#%s</styleUrl>\n", isRaven ? "raven" : "det");
|
resp->printf("<styleUrl>#%s</styleUrl>\n", isRaven ? "raven" : "det");
|
||||||
resp->print("<description><![CDATA[");
|
resp->print("<description><![CDATA[");
|
||||||
if (d["name"].is<const char*>() && strlen(d["name"] | "") > 0)
|
if (d["name"].is<const char*>() && strlen(d["name"] | "") > 0)
|
||||||
resp->printf("<b>Name:</b> %s<br/>", d["name"] | "");
|
resp->printf("<b>Name:</b> %s<br/>", d["name"] | "");
|
||||||
resp->printf("<b>Method:</b> %s<br/><b>RSSI:</b> %d<br/><b>Count:</b> %d",
|
resp->printf("<b>Method:</b> %s<br/><b>RSSI:</b> %d<br/>",
|
||||||
d["method"] | "?", d["rssi"] | 0, d["count"] | 1);
|
d["method"] | "?", d["rssi"] | 0);
|
||||||
|
if (d["best_rssi"])
|
||||||
|
resp->printf("<b>Best RSSI:</b> %d<br/>", d["best_rssi"] | 0);
|
||||||
|
resp->printf("<b>Count:</b> %d", d["count"] | 1);
|
||||||
if (isRaven && d["fw"].is<const char*>())
|
if (isRaven && d["fw"].is<const char*>())
|
||||||
resp->printf("<br/><b>Raven FW:</b> %s", d["fw"] | "");
|
resp->printf("<br/><b>Raven FW:</b> %s", d["fw"] | "");
|
||||||
resp->print("]]></description>\n");
|
resp->print("]]></description>\n");
|
||||||
resp->printf("<Point><coordinates>%.8f,%.8f,0</coordinates></Point>\n",
|
resp->printf("<Point><coordinates>%.8f,%.8f,0</coordinates></Point>\n",
|
||||||
(double)(gps["lon"] | 0.0), (double)(gps["lat"] | 0.0));
|
(double)(pinGPS["lon"] | 0.0), (double)(pinGPS["lat"] | 0.0));
|
||||||
resp->print("</Placemark>\n");
|
resp->print("</Placemark>\n");
|
||||||
placed++;
|
placed++;
|
||||||
}
|
}
|
||||||
|
|
@ -1486,10 +1579,14 @@ void setup() {
|
||||||
printf("[DANTIR] SPIFFS init failed - no persistence\n");
|
printf("[DANTIR] SPIFFS init failed - no persistence\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize battery monitoring
|
||||||
|
fyReadBattery();
|
||||||
|
|
||||||
printf("\n========================================\n");
|
printf("\n========================================\n");
|
||||||
printf(" DANTIR Surveillance Counter-Watcher\n");
|
printf(" DANTIR Surveillance Counter-Watcher\n");
|
||||||
printf(" Buzzer: %s\n", fyBuzzerOn ? "ON" : "OFF");
|
printf(" Buzzer: %s\n", fyBuzzerOn ? "ON" : "OFF");
|
||||||
printf(" GPS: auto-detect (L76K on D6/D7)\n");
|
printf(" GPS: auto-detect (L76K on D6/D7)\n");
|
||||||
|
printf(" Battery ADC: %s\n", BATTERY_ADC_PIN >= 0 ? "enabled" : "disabled (no voltage divider)");
|
||||||
printf("========================================\n");
|
printf("========================================\n");
|
||||||
|
|
||||||
// Init BLE scanner FIRST -- start scanning immediately
|
// Init BLE scanner FIRST -- start scanning immediately
|
||||||
|
|
@ -1539,6 +1636,7 @@ void setup() {
|
||||||
void loop() {
|
void loop() {
|
||||||
fyProcessHardwareGPS();
|
fyProcessHardwareGPS();
|
||||||
fyUpdatePixel();
|
fyUpdatePixel();
|
||||||
|
fyReadBattery();
|
||||||
|
|
||||||
// BLE scanning cycle
|
// BLE scanning cycle
|
||||||
if (millis() - fyLastBleScan >= BLE_SCAN_INTERVAL && !fyBLEScan->isScanning()) {
|
if (millis() - fyLastBleScan >= BLE_SCAN_INTERVAL && !fyBLEScan->isScanning()) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue