Fix floor persistence and improve movement detection
- Store floor assignments in SQLite database for persistence - Floor now saves correctly for live-tracked BT devices - Add statistical movement detection (5-sample average + stddev) - Require 1.5m + 2σ deviation to mark device as moving - Reduces false positives from RSSI noise/fluctuations - Add /api/device/floors endpoint for bulk floor retrieval Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,9 +21,72 @@ let liveTrackingEnabled = false;
|
||||
let liveTrackingInterval = null;
|
||||
const LIVE_TRACKING_INTERVAL_MS = 4000; // 4 seconds
|
||||
|
||||
// Track previous distances for movement detection
|
||||
let previousDistances = {};
|
||||
const MOVEMENT_THRESHOLD = 0.5; // meters - consider moving if distance changed by this much
|
||||
// Statistical movement detection
|
||||
const SAMPLE_HISTORY_SIZE = 5; // Number of samples to keep for averaging
|
||||
const MOVEMENT_THRESHOLD = 1.5; // meters - movement must exceed this + stddev margin
|
||||
const MIN_SAMPLES_FOR_MOVEMENT = 3; // Need at least this many samples before detecting movement
|
||||
|
||||
// Store distance history per device: { address: { samples: [], timestamps: [] } }
|
||||
let deviceDistanceHistory = {};
|
||||
|
||||
// Calculate mean of array
|
||||
function mean(arr) {
|
||||
if (arr.length === 0) return 0;
|
||||
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
||||
}
|
||||
|
||||
// Calculate standard deviation
|
||||
function stddev(arr) {
|
||||
if (arr.length < 2) return 0;
|
||||
const avg = mean(arr);
|
||||
const squareDiffs = arr.map(x => Math.pow(x - avg, 2));
|
||||
return Math.sqrt(mean(squareDiffs));
|
||||
}
|
||||
|
||||
// Check if device is moving based on statistical analysis
|
||||
function isDeviceMoving(address, newDistance) {
|
||||
// Initialize history if needed
|
||||
if (!deviceDistanceHistory[address]) {
|
||||
deviceDistanceHistory[address] = { samples: [], timestamps: [] };
|
||||
}
|
||||
|
||||
const history = deviceDistanceHistory[address];
|
||||
const now = Date.now();
|
||||
|
||||
// Add new sample
|
||||
history.samples.push(newDistance);
|
||||
history.timestamps.push(now);
|
||||
|
||||
// Remove old samples (keep last SAMPLE_HISTORY_SIZE)
|
||||
while (history.samples.length > SAMPLE_HISTORY_SIZE) {
|
||||
history.samples.shift();
|
||||
history.timestamps.shift();
|
||||
}
|
||||
|
||||
// Need minimum samples to determine movement
|
||||
if (history.samples.length < MIN_SAMPLES_FOR_MOVEMENT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate statistics from older samples (exclude the newest one)
|
||||
const olderSamples = history.samples.slice(0, -1);
|
||||
const avg = mean(olderSamples);
|
||||
const sd = stddev(olderSamples);
|
||||
|
||||
// Movement threshold = base threshold + 2 standard deviations (95% confidence)
|
||||
const dynamicThreshold = MOVEMENT_THRESHOLD + (2 * sd);
|
||||
|
||||
// Check if current reading deviates significantly from the average
|
||||
const deviation = Math.abs(newDistance - avg);
|
||||
const isMoving = deviation > dynamicThreshold;
|
||||
|
||||
// Debug log for significant movements
|
||||
if (isMoving) {
|
||||
console.log(`[Movement] ${address}: dist=${newDistance.toFixed(2)}m, avg=${avg.toFixed(2)}m, sd=${sd.toFixed(2)}, threshold=${dynamicThreshold.toFixed(2)}m`);
|
||||
}
|
||||
|
||||
return isMoving;
|
||||
}
|
||||
|
||||
// Device positions for hit detection (radar view)
|
||||
let devicePositions = [];
|
||||
@@ -1485,23 +1548,19 @@ async function performLiveBTScan() {
|
||||
const existing = existingBt.find(d => d.address === newDev.address);
|
||||
const newDist = newDev.estimated_distance_m;
|
||||
|
||||
// Check for movement
|
||||
const prevDist = previousDistances[newDev.address];
|
||||
const isMoving = prevDist !== undefined && Math.abs(newDist - prevDist) > MOVEMENT_THRESHOLD;
|
||||
|
||||
// Store current distance for next comparison
|
||||
previousDistances[newDev.address] = newDist;
|
||||
// Check for movement using statistical analysis
|
||||
const moving = isDeviceMoving(newDev.address, newDist);
|
||||
|
||||
if (existing) {
|
||||
// Update RSSI and estimated distance, preserve custom values
|
||||
existing.rssi = newDev.rssi;
|
||||
existing.estimated_distance_m = newDev.estimated_distance_m;
|
||||
existing.signal_quality = newDev.signal_quality;
|
||||
existing.is_moving = isMoving;
|
||||
existing.is_moving = moving;
|
||||
// Preserve floor and custom_distance_m if set
|
||||
} else {
|
||||
// New device, add it
|
||||
newDev.is_moving = isMoving;
|
||||
newDev.is_moving = moving;
|
||||
existingBt.push(newDev);
|
||||
}
|
||||
});
|
||||
@@ -1510,7 +1569,8 @@ async function performLiveBTScan() {
|
||||
} else {
|
||||
// No existing scan data, use BT-only data
|
||||
data.bluetooth_devices.forEach(dev => {
|
||||
previousDistances[dev.address] = dev.estimated_distance_m;
|
||||
// Initialize history with first sample, not moving yet
|
||||
isDeviceMoving(dev.address, dev.estimated_distance_m);
|
||||
dev.is_moving = false;
|
||||
});
|
||||
scanData = {
|
||||
|
||||
Reference in New Issue
Block a user