From f02383d7adeb9867c088f4e8afe7f97d2eda6912 Mon Sep 17 00:00:00 2001 From: Ted Timmons Date: Sat, 23 May 2026 10:53:49 -0700 Subject: [PATCH 1/2] feat: add location privacy / GPS blurring addresses #870 and #1741 by chopping the floating point digits at `(6-blur_digits)`. Tested and running on my Station G2, but should work on all. --- docs/cli_commands.md | 21 ++++++++++ src/helpers/CommonCLI.cpp | 40 +++++++++++++++++-- src/helpers/CommonCLI.h | 1 + src/helpers/SensorManager.h | 3 +- .../sensors/EnvironmentSensorManager.cpp | 17 +++++--- 5 files changed, 72 insertions(+), 10 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 99dced3658..c62114bcae 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -905,6 +905,27 @@ region save --- +#### View or change GPS location blur (privacy) +**Usage:** +- `gps blur` +- `gps blur ` + +**Parameters:** +- `digits`: `0`–`6` decimal places to publish + - `0`: no blur (full precision, ~0.1 m) + - `1`: ~11 km grid + - `2`: ~1.1 km grid + - `3`: ~111 m grid (neighborhood) + - `4`: ~11 m grid + - `5`: ~1.1 m grid + - `6`: same as 0 (full precision) + +**Default:** `0` (off) + +**Note:** Applies to both advertised position and telemetry GPS data. Coordinates are truncated to the nearest grid cell, not rounded. + +--- + #### View or change the GPS advert policy **Usage:** - `gps advert` diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index d495aada5f..870bdfccec 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -88,7 +88,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.read((uint8_t *)&_prefs->gps_blur_digits, sizeof(_prefs->gps_blur_digits)); // 291 + // next: 292 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -118,6 +119,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean + _prefs->gps_blur_digits = constrain(_prefs->gps_blur_digits, 0, 6); + + _sensors->gps_blur_digits = _prefs->gps_blur_digits; file.close(); } @@ -179,7 +183,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.write((uint8_t *)&_prefs->gps_blur_digits, sizeof(_prefs->gps_blur_digits)); // 291 + // next: 292 file.close(); } @@ -194,15 +199,26 @@ void CommonCLI::savePrefs() { _callbacks->savePrefs(); } +static void applyGpsBlur(double& lat, double& lon, uint8_t digits) { + if (digits == 0 || digits > 6) return; + long q = 1; + for (uint8_t i = 0; i < 6 - digits; i++) q *= 10; + lat = (double)(((long)(lat * 1000000.0) / q) * q) / 1000000.0; + lon = (double)(((long)(lon * 1000000.0) / q) * q) / 1000000.0; +} + uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) { if (_prefs->advert_loc_policy == ADVERT_LOC_NONE) { AdvertDataBuilder builder(node_type, _prefs->node_name); return builder.encodeTo(app_data); } else if (_prefs->advert_loc_policy == ADVERT_LOC_SHARE) { + // node_lat/lon already blurred by EnvironmentSensorManager AdvertDataBuilder builder(node_type, _prefs->node_name, _sensors->node_lat, _sensors->node_lon); return builder.encodeTo(app_data); } else { - AdvertDataBuilder builder(node_type, _prefs->node_name, _prefs->node_lat, _prefs->node_lon); + double lat = _prefs->node_lat, lon = _prefs->node_lon; + applyGpsBlur(lat, lon, _prefs->gps_blur_digits); + AdvertDataBuilder builder(node_type, _prefs->node_name, lat, lon); return builder.encodeTo(app_data); } } @@ -376,6 +392,24 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re _prefs->node_lon = _sensors->node_lon; savePrefs(); strcpy(reply, "ok"); + } else if (memcmp(command, "gps blur", 8) == 0) { + if (strlen(command) == 8) { + if (_prefs->gps_blur_digits == 0) { + strcpy(reply, "> off"); + } else { + sprintf(reply, "> %d", _prefs->gps_blur_digits); + } + } else { + uint8_t digits = atoi(command + 9); + if (digits <= 6) { + _prefs->gps_blur_digits = digits; + _sensors->gps_blur_digits = digits; + savePrefs(); + strcpy(reply, "ok"); + } else { + strcpy(reply, "error: value must be 0-6"); + } + } } else if (memcmp(command, "gps advert", 10) == 0) { if (strlen(command) == 10) { switch (_prefs->advert_loc_policy) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index ffdc7c6536..8d5294516d 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -61,6 +61,7 @@ struct NodePrefs { // persisted to file uint8_t rx_boosted_gain; // power settings uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; + uint8_t gps_blur_digits; // 0=off, 1-6: broadcast GPS coords rounded to N decimal places (~111km/11km/1.1km/111m/11m/1m) }; class CommonCLICallbacks { diff --git a/src/helpers/SensorManager.h b/src/helpers/SensorManager.h index 89a174c228..8fb631d694 100644 --- a/src/helpers/SensorManager.h +++ b/src/helpers/SensorManager.h @@ -13,8 +13,9 @@ class SensorManager { public: double node_lat, node_lon; // modify these, if you want to affect Advert location double node_altitude; // altitude in meters + uint8_t gps_blur_digits = 0; // 0=off, 1-6: snap GPS coords to this many decimal places before publishing - SensorManager() { node_lat = 0; node_lon = 0; node_altitude = 0; } + SensorManager() { node_lat = 0; node_lon = 0; node_altitude = 0; gps_blur_digits = 0; } virtual bool begin() { return false; } virtual bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { return false; } virtual void loop() { } diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 19472406d8..068046420b 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -754,6 +754,13 @@ void EnvironmentSensorManager::stop_gps() { #endif } +static double blurCoord(long microdeg, uint8_t digits) { + if (digits == 0 || digits > 6) return microdeg / 1000000.0; + long q = 1; + for (uint8_t i = 0; i < 6 - digits; i++) q *= 10; + return (double)((microdeg / q) * q) / 1000000.0; +} + void EnvironmentSensorManager::loop() { static long next_gps_update = 0; @@ -766,17 +773,15 @@ void EnvironmentSensorManager::loop() { if(gps_active){ #ifdef RAK_WISBLOCK_GPS if ((i2cGPSFlag || serialGPSFlag) && _location->isValid()) { - node_lat = ((double)_location->getLatitude())/1000000.; - node_lon = ((double)_location->getLongitude())/1000000.; - MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon); + node_lat = blurCoord(_location->getLatitude(), gps_blur_digits); + node_lon = blurCoord(_location->getLongitude(), gps_blur_digits); node_altitude = ((double)_location->getAltitude()) / 1000.0; MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude); } #else if (_location->isValid()) { - node_lat = ((double)_location->getLatitude())/1000000.; - node_lon = ((double)_location->getLongitude())/1000000.; - MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon); + node_lat = blurCoord(_location->getLatitude(), gps_blur_digits); + node_lon = blurCoord(_location->getLongitude(), gps_blur_digits); node_altitude = ((double)_location->getAltitude()) / 1000.0; MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude); } From a06ef437d08f4557ba03aa508fab60a170bc90b5 Mon Sep 17 00:00:00 2001 From: Ted Timmons Date: Sat, 23 May 2026 16:18:24 -0700 Subject: [PATCH 2/2] feat: deterministic blur offsets When in "blur mode", using the device pubkey, offset the device from the truncated GPS coordinate. This prevents multiple devices from being at the same point. By using modulo we may not be evenly distributed, but they should have decent spread. Here are some examples from one device: ``` gps -> on, active, fix, 7 sats gps blur -> > 3, offset lat+0.000071 lon-0.000310 gps blur 4 -> ok, offset lat+0.000021 lon+0.000040 gps blur 3 -> ok, offset lat+0.000071 lon-0.000310 gps blur 2 -> ok, offset lat-0.003429 lon-0.004810 ``` --- src/helpers/CommonCLI.cpp | 41 +++++++++++++++---- src/helpers/SensorManager.h | 4 +- .../sensors/EnvironmentSensorManager.cpp | 16 ++++---- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 870bdfccec..2433fcb410 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -123,6 +123,16 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _sensors->gps_blur_digits = _prefs->gps_blur_digits; + // derive per-device fuzz seeds: first half of pub_key → lat, second half → lon + { + const uint8_t* pk = _callbacks->getSelfId().pub_key; + uint32_t lat_seed = 0, lon_seed = 0; + for (int i = 0; i < 16; i++) lat_seed ^= ((uint32_t)pk[i] << ((i % 4) * 8)); + for (int i = 16; i < 32; i++) lon_seed ^= ((uint32_t)pk[i] << ((i % 4) * 8)); + _sensors->gps_fuzz_lat = lat_seed; + _sensors->gps_fuzz_lon = lon_seed; + } + file.close(); } } @@ -199,12 +209,14 @@ void CommonCLI::savePrefs() { _callbacks->savePrefs(); } -static void applyGpsBlur(double& lat, double& lon, uint8_t digits) { +static void applyGpsBlur(double& lat, double& lon, uint8_t digits, uint32_t lat_seed, uint32_t lon_seed) { if (digits == 0 || digits > 6) return; - long q = 1; - for (uint8_t i = 0; i < 6 - digits; i++) q *= 10; - lat = (double)(((long)(lat * 1000000.0) / q) * q) / 1000000.0; - lon = (double)(((long)(lon * 1000000.0) / q) * q) / 1000000.0; + long grid = 1; + for (uint8_t i = 0; i < 6 - digits; i++) grid *= 10; + long lat_mic = (long)(lat * 1000000.0); + long lon_mic = (long)(lon * 1000000.0); + lat = (double)(((lat_mic / grid) * grid) + (long)(lat_seed % (uint32_t)grid) - grid / 2) / 1000000.0; + lon = (double)(((lon_mic / grid) * grid) + (long)(lon_seed % (uint32_t)grid) - grid / 2) / 1000000.0; } uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) { @@ -217,7 +229,7 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) { return builder.encodeTo(app_data); } else { double lat = _prefs->node_lat, lon = _prefs->node_lon; - applyGpsBlur(lat, lon, _prefs->gps_blur_digits); + applyGpsBlur(lat, lon, _prefs->gps_blur_digits, _sensors->gps_fuzz_lat, _sensors->gps_fuzz_lon); AdvertDataBuilder builder(node_type, _prefs->node_name, lat, lon); return builder.encodeTo(app_data); } @@ -397,7 +409,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re if (_prefs->gps_blur_digits == 0) { strcpy(reply, "> off"); } else { - sprintf(reply, "> %d", _prefs->gps_blur_digits); + uint8_t digits = _prefs->gps_blur_digits; + long grid = 1; + for (uint8_t i = 0; i < 6 - digits; i++) grid *= 10; + double lat_off = ((long)(_sensors->gps_fuzz_lat % (uint32_t)grid) - grid / 2) / 1000000.0; + double lon_off = ((long)(_sensors->gps_fuzz_lon % (uint32_t)grid) - grid / 2) / 1000000.0; + sprintf(reply, "> %d, offset lat%+.6f lon%+.6f", digits, lat_off, lon_off); } } else { uint8_t digits = atoi(command + 9); @@ -405,7 +422,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re _prefs->gps_blur_digits = digits; _sensors->gps_blur_digits = digits; savePrefs(); - strcpy(reply, "ok"); + if (digits == 0) { + strcpy(reply, "ok"); + } else { + long grid = 1; + for (uint8_t i = 0; i < 6 - digits; i++) grid *= 10; + double lat_off = ((long)(_sensors->gps_fuzz_lat % (uint32_t)grid) - grid / 2) / 1000000.0; + double lon_off = ((long)(_sensors->gps_fuzz_lon % (uint32_t)grid) - grid / 2) / 1000000.0; + sprintf(reply, "ok, offset lat%+.6f lon%+.6f", lat_off, lon_off); + } } else { strcpy(reply, "error: value must be 0-6"); } diff --git a/src/helpers/SensorManager.h b/src/helpers/SensorManager.h index 8fb631d694..51ca580b40 100644 --- a/src/helpers/SensorManager.h +++ b/src/helpers/SensorManager.h @@ -14,8 +14,10 @@ class SensorManager { double node_lat, node_lon; // modify these, if you want to affect Advert location double node_altitude; // altitude in meters uint8_t gps_blur_digits = 0; // 0=off, 1-6: snap GPS coords to this many decimal places before publishing + uint32_t gps_fuzz_lat = 0; // derived from first half of pub_key; deterministic offset within blur grid cell + uint32_t gps_fuzz_lon = 0; // derived from second half of pub_key - SensorManager() { node_lat = 0; node_lon = 0; node_altitude = 0; gps_blur_digits = 0; } + SensorManager() { node_lat = 0; node_lon = 0; node_altitude = 0; gps_blur_digits = 0; gps_fuzz_lat = 0; gps_fuzz_lon = 0; } virtual bool begin() { return false; } virtual bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { return false; } virtual void loop() { } diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 068046420b..79897a5b9e 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -754,11 +754,11 @@ void EnvironmentSensorManager::stop_gps() { #endif } -static double blurCoord(long microdeg, uint8_t digits) { +static double blurCoord(long microdeg, uint8_t digits, uint32_t fuzz) { if (digits == 0 || digits > 6) return microdeg / 1000000.0; - long q = 1; - for (uint8_t i = 0; i < 6 - digits; i++) q *= 10; - return (double)((microdeg / q) * q) / 1000000.0; + long grid = 1; + for (uint8_t i = 0; i < 6 - digits; i++) grid *= 10; + return (double)(((microdeg / grid) * grid) + (long)(fuzz % (uint32_t)grid) - grid / 2) / 1000000.0; } void EnvironmentSensorManager::loop() { @@ -773,15 +773,15 @@ void EnvironmentSensorManager::loop() { if(gps_active){ #ifdef RAK_WISBLOCK_GPS if ((i2cGPSFlag || serialGPSFlag) && _location->isValid()) { - node_lat = blurCoord(_location->getLatitude(), gps_blur_digits); - node_lon = blurCoord(_location->getLongitude(), gps_blur_digits); + node_lat = blurCoord(_location->getLatitude(), gps_blur_digits, gps_fuzz_lat); + node_lon = blurCoord(_location->getLongitude(), gps_blur_digits, gps_fuzz_lon); node_altitude = ((double)_location->getAltitude()) / 1000.0; MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude); } #else if (_location->isValid()) { - node_lat = blurCoord(_location->getLatitude(), gps_blur_digits); - node_lon = blurCoord(_location->getLongitude(), gps_blur_digits); + node_lat = blurCoord(_location->getLatitude(), gps_blur_digits, gps_fuzz_lat); + node_lon = blurCoord(_location->getLongitude(), gps_blur_digits, gps_fuzz_lon); node_altitude = ((double)_location->getAltitude()) / 1000.0; MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude); }