UNPKG

bt-sensors-plugin-sk

Version:

Bluetooth Sensors for Signalk - see https://www.npmjs.com/package/bt-sensors-plugin-sk#supported-sensors for a list of supported sensors

249 lines (209 loc) 9.34 kB
const { LRUCache } = require('lru-cache') class DistanceManager { static METHOD_AVG = 1; static METHOD_WEIGHTED_AVG = 2; static METHOD_LAST_FEW_SAMPLES = 3; Constant = { DISTANCE_FIND_LAST_FEW_SAMPLE_TIME_FRAME_MILLIS: 5000, // Example value, adjust as needed DISTANCE_FIND_TIME_FRAME_MILLIS: 10000, // Example value, adjust as needed LAST_FEW_SAMPLE_COUNT: 5 // Example value, adjust as needed }; #beaconRssiSampleMap = new LRUCache({ttl:1000*60*5, ttlAutopurge: true}); // Using LRUCache with a ttl of 5m for beaconRssiSampleMap #timeFormatter = new Intl.DateTimeFormat('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false // Use 24-hour format }); constructor(constants) { Object.assign(this.Constant,constants) // Constructor is empty as initialization is done with property declarations } addSample(macAddress, rssi) { // In Node.js (JavaScript), maps and objects are not inherently synchronized like // Java's synchronized blocks. If this were a multi-threaded Node.js environment // (e.g., Worker Threads), you'd need explicit locking mechanisms or message passing. // For typical single-threaded Node.js, direct access is usually fine, // but if concurrency is a concern, consider a mutex implementation. let samples = this.#beaconRssiSampleMap.get(macAddress); if (!samples) { samples = new Map(); // Using Map instead of LinkedHashMap this.#beaconRssiSampleMap.set(macAddress, samples); } samples.set(Date.now(), rssi); } getDistance(macAddress, txPower, method, debugLog) { const samples = this.#beaconRssiSampleMap.get(macAddress); if (!samples) { return 0; } // Create a new Map to avoid modifying the original during filtering/processing const currentSamples = new Map(samples); const fromTimestamp = Date.now() - (method === DistanceManager.METHOD_LAST_FEW_SAMPLES ? this.Constant.DISTANCE_FIND_LAST_FEW_SAMPLE_TIME_FRAME_MILLIS : this.Constant.DISTANCE_FIND_TIME_FRAME_MILLIS); const toTimestamp = Date.now(); const filteredRssi = this.#filterRssiSamplesWithinTimeFrame(currentSamples, fromTimestamp, toTimestamp); const smoothRssi = this.#reduceNoiseFromRSSI(filteredRssi); let rssi = 0; let distance; switch (method) { case DistanceManager.METHOD_AVG: rssi = this.#calculateAverage(smoothRssi); break; case DistanceManager.METHOD_WEIGHTED_AVG: rssi = this.#calculateWeightedAverageOfRssiSamples(smoothRssi); break; case DistanceManager.METHOD_LAST_FEW_SAMPLES: if (samples.size >= this.Constant.LAST_FEW_SAMPLE_COUNT) { rssi = this.#calculateAverage(smoothRssi); } else { return -1; } break; default: // Handle unknown method or provide a default console.warn(`Unknown method: ${method}`); return 0; } distance = this.#calculateAccuracy(txPower, rssi); if (debugLog) { this.#logSamples(smoothRssi, fromTimestamp, toTimestamp, rssi, distance); } return distance; } #removeOutliers(filteredRssi, avgOutlierRssi, outlierConstant) { const outlierRemoveRssi = new Map(); const minRssi = Math.floor(avgOutlierRssi) - outlierConstant; const maxRssi = Math.floor(avgOutlierRssi) + outlierConstant; for (const [timestamp, value] of filteredRssi.entries()) { if (value >= minRssi && value <= maxRssi) { outlierRemoveRssi.set(timestamp, value); } } return outlierRemoveRssi; } #reduceNoiseFromRSSI(filteredRssi) { const smoothRssi = new Map(); const rssiEntries = Array.from(filteredRssi.entries()); const totalRssi = rssiEntries.length; for (let i = 0; i < totalRssi; i++) { const [currentTimestamp, currentRssi] = rssiEntries[i]; const nextRssi = rssiEntries[i + 1] ? rssiEntries[i + 1][1] : currentRssi; // If no next, use current const avgRssi = Math.floor((currentRssi + nextRssi) / 2); smoothRssi.set(currentTimestamp, avgRssi); } return smoothRssi; } #filterRssiSamplesWithinTimeFrame(rssiSamples, fromTimestamp, toTimestamp) { const filteredSamples = new Map(); for (const [timestamp, rssi] of rssiSamples.entries()) { if (fromTimestamp === 0) { if (timestamp <= toTimestamp) { filteredSamples.set(timestamp, rssi); } } else if (timestamp > fromTimestamp && timestamp <= toTimestamp) { filteredSamples.set(timestamp, rssi); } } return filteredSamples; } #calculateAverage(filteredRssi) { let sum = 0; if (filteredRssi.size === 0) { return 0; // Avoid division by zero } for (const rssi of filteredRssi.values()) { sum += rssi; } return sum / filteredRssi.size; } #calculateWeightedAverageOfRssiSamples(filteredRssi) { // 1. Find count const uniqueRssiCountMap = new Map(); for (const rssi of filteredRssi.values()) { uniqueRssiCountMap.set(rssi, (uniqueRssiCountMap.get(rssi) || 0) + 1); } // 2. Find weight of each rssi const uniqueRssiWeightMap = new Map(); const totalSamples = filteredRssi.size; if (totalSamples === 0) { return 0; // Avoid division by zero } for (const [rssi, count] of uniqueRssiCountMap.entries()) { const weight = count / totalSamples; uniqueRssiWeightMap.set(rssi, weight); } // 3. Calculate weighted average let sum = 0; for (const [rssi, weight] of uniqueRssiWeightMap.entries()) { sum += rssi * weight; } return sum; } #calculateAccuracy(txPower, rssi) { if (rssi === 0) { return -1.0; } const ratio = rssi * 1.0 / txPower; if (ratio < 1.0) { return Math.pow(ratio, 10); } else { return ((0.42093) * Math.pow(ratio, 6.9476)) + 0.54992; // Nexus 5 formula } } #logSamples(samples, fromTimestamp, toTimestamp, rssiWeightedAvg, distance) { const object = {}; const array = []; try { for (const [timestamp, rssi] of samples.entries()) { const jsonSample = { rssi: rssi, timestamp: this.#getTimeString(timestamp) }; array.push(jsonSample); } object.calc_rssi = rssiWeightedAvg; object.distance = distance; object.start_time = this.#getTimeString(fromTimestamp); object.end_time = this.#getTimeString(toTimestamp); object.samples = array; // In Node.js, 'console.log' is used instead of 'Log.d' //console.log("SampleData", JSON.stringify(object, null, 2)); // Prettify output } catch (error) { console.error("Error logging samples:", error); // Use console.error for errors } } #getTimeString(millis) { const d = new Date(millis); return this.#timeFormatter.format(d); } } // Example Usage (for demonstration purposes) /* // You might need to define or import Constant with your specific values const Constant = { DISTANCE_FIND_LAST_FEW_SAMPLE_TIME_FRAME_MILLIS: 5000, DISTANCE_FIND_TIME_FRAME_MILLIS: 10000, LAST_FEW_SAMPLE_COUNT: 5 }; const distanceManager = new DistanceManager(); // Simulate adding some samples distanceManager.addSample("AA:BB:CC:DD:EE:FF", -70); setTimeout(() => distanceManager.addSample("AA:BB:CC:DD:EE:FF", -72), 500); setTimeout(() => distanceManager.addSample("AA:BB:CC:DD:EE:FF", -68), 1000); setTimeout(() => distanceManager.addSample("AA:BB:CC:DD:EE:FF", -75), 1500); setTimeout(() => distanceManager.addSample("AA:BB:CC:DD:EE:FF", -71), 2000); setTimeout(() => distanceManager.addSample("AA:BB:CC:DD:EE:FF", -69), 2500); setTimeout(() => distanceManager.addSample("AA:BB:CC:DD:EE:FF", -73), 3000); // Get distance after some time setTimeout(() => { const txPower = -59; // Example TxPower let distance = distanceManager.getDistance("AA:BB:CC:DD:EE:FF", txPower, DistanceManager.METHOD_AVG, true); console.log(`Calculated Distance (AVG): ${distance.toFixed(2)} meters`); distance = distanceManager.getDistance("AA:BB:CC:DD:EE:FF", txPower, DistanceManager.METHOD_WEIGHTED_AVG, true); console.log(`Calculated Distance (WEIGHTED_AVG): ${distance.toFixed(2)} meters`); distance = distanceManager.getDistance("AA:BB:CC:DD:EE:FF", txPower, DistanceManager.METHOD_LAST_FEW_SAMPLES, true); console.log(`Calculated Distance (LAST_FEW_SAMPLES): ${distance.toFixed(2)} meters`); }, 4000); */ module.exports=DistanceManager