diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 9fde4d436e5..05b6c68ae7e 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -69,6 +69,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `epoch_seconds`: Unix epoch time +**Note:** The clock only moves forward. To set it back (e.g. after the clock was pushed into the future by a spoofed GPS time) use `time force `. + --- ### Send a flood advert @@ -1042,6 +1044,19 @@ region save --- +#### GPS anti-spoofing guard (When built with `GPS_GUARD`) +With `GPS_GUARD`, a fix only updates the node position (and telemetry) after it passes plausibility checks: at least `GPS_GUARD_MIN_SATS` satellites, HDOP ≤ `GPS_GUARD_MAX_HDOP10`/10, inside the optional build-time region (`GPS_GUARD_LAT_MIN/MAX`, `GPS_GUARD_LON_MIN/MAX`), within `gps_fence` metres of the node's configured `lat`/`lon` (if set), no jump faster than `GPS_GUARD_MAX_SPEED_KMH`, and `GPS_GUARD_STABLE_FIXES` consistent fixes in a row. + +GPS time is written to the clock only while such a fix is current, never earlier than the firmware build date, and — once the clock is valid — only within `GPS_GUARD_TIME_MAX_STEP` seconds for the first sync and within clock drift (`GPS_GUARD_TIME_DRIFT_BASE` s + `GPS_GUARD_TIME_DRIFT_PPM`) afterwards. Setting the clock manually (`time`, `clock sync`) resets that anchor. + +**Sensor settings** (via `sensor get` / `sensor set`): +- `gps_fence`: geofence radius in metres around `lat`/`lon` (`0` = off). Default `GPS_GUARD_FENCE_M` (1000). +- `gps_ref`: reference position, shown as `lat;lon` (`lat,lon` is also accepted); follows `set lat`, `set lon` and `gps setloc`. +- `gps_guard` (read-only): status, e.g. `ok 7sat time ok +1s`, `rej region time rej nofix`, or `... ALERT gps-clk +3600s 75m` when a trusted fix has disagreed with the clock by the same offset for `GPS_GUARD_ALERT_MIN` minutes. +- `sensor set gps_time_accept 1`: while an alert is active, confirm that the GPS time is right and apply it to the clock. + +--- + ### Sensors (When sensor support is compiled in) #### View the list of sensors on this node diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index dc89325861c..827582d7b67 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -160,6 +160,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0"); + char ref[32]; + snprintf(ref, sizeof(ref), "%.6f,%.6f", _prefs.node_lat, _prefs.node_lon); + sensors.setSettingValue("gps_ref", ref); // anti-spoofing reference (ignored if unsupported) } #endif diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 4dee8bdbf40..9db84ed3b67 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -169,6 +169,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0"); + char ref[32]; + snprintf(ref, sizeof(ref), "%.6f,%.6f", _prefs.node_lat, _prefs.node_lon); + sensors.setSettingValue("gps_ref", ref); // anti-spoofing reference (ignored if unsupported) } #endif diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 590ca0c7657..5141d4648b1 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -174,6 +174,9 @@ class SensorMesh : public mesh::Mesh, public CommonCLICallbacks { #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0"); + char ref[32]; + snprintf(ref, sizeof(ref), "%.6f,%.6f", _prefs.node_lat, _prefs.node_lon); + sensors.setSettingValue("gps_ref", ref); // anti-spoofing reference (ignored if unsupported) } #endif }; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 5d7501d9985..4f474b5834f 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -10,6 +10,14 @@ #endif // Believe it or not, this std C function is busted on some platforms! +// Keeps the GPS anti-spoofing reference in sync with the configured node position +// (ignored by sensor managers that do not support it). +static void updateGpsReference(SensorManager* sensors, double lat, double lon) { + char ref[32]; + snprintf(ref, sizeof(ref), "%.6f,%.6f", lat, lon); + sensors->setSettingValue("gps_ref", ref); +} + static uint32_t _atoi(const char* sp) { uint32_t n = 0; while (*sp && *sp >= '0' && *sp <= '9') { @@ -211,6 +219,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re uint32_t curr = getRTCClock()->getCurrentTime(); if (sender_timestamp > curr) { getRTCClock()->setCurrentTime(sender_timestamp + 1); + _sensors->setSettingValue("gps_time_reset", "1"); // manual clock set: reset GPS time anchor uint32_t now = getRTCClock()->getCurrentTime(); DateTime dt = DateTime(now); sprintf(reply, "OK - clock set: %02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year()); @@ -225,11 +234,22 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re uint32_t now = getRTCClock()->getCurrentTime(); DateTime dt = DateTime(now); sprintf(reply, "%02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year()); + } else if (memcmp(command, "time force ", 11) == 0) { // set time, allowing it to go backwards + uint32_t secs = _atoi(&command[11]); + if (secs > 1700000000UL) { + getRTCClock()->setCurrentTime(secs); + _sensors->setSettingValue("gps_time_reset", "1"); // manual clock set: reset GPS time anchor + DateTime dt = DateTime(getRTCClock()->getCurrentTime()); + sprintf(reply, "OK - clock forced: %02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year()); + } else { + strcpy(reply, "ERR: invalid epoch"); + } } else if (memcmp(command, "time ", 5) == 0) { // set time (to epoch seconds) uint32_t secs = _atoi(&command[5]); uint32_t curr = getRTCClock()->getCurrentTime(); if (secs > curr) { getRTCClock()->setCurrentTime(secs); + _sensors->setSettingValue("gps_time_reset", "1"); // manual clock set: reset GPS time anchor uint32_t now = getRTCClock()->getCurrentTime(); DateTime dt = DateTime(now); sprintf(reply, "OK - clock set: %02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year()); @@ -393,6 +413,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else if (memcmp(command, "gps setloc", 10) == 0) { _prefs->node_lat = _sensors->node_lat; _prefs->node_lon = _sensors->node_lon; + updateGpsReference(_sensors, _prefs->node_lat, _prefs->node_lon); savePrefs(); strcpy(reply, "ok"); } else if (memcmp(command, "gps advert", 10) == 0) { @@ -549,10 +570,12 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); } else if (memcmp(config, "lat ", 4) == 0) { _prefs->node_lat = atof(&config[4]); + updateGpsReference(_sensors, _prefs->node_lat, _prefs->node_lon); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "lon ", 4) == 0) { _prefs->node_lon = atof(&config[4]); + updateGpsReference(_sensors, _prefs->node_lat, _prefs->node_lon); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "flood.max.unscoped ", 19) == 0) { diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 0dc892c3a52..ed46c4f1d31 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -1,6 +1,9 @@ #include "EnvironmentSensorManager.h" #include +#if ENV_INCLUDE_GPS && defined(GPS_GUARD) +#include +#endif #if ENV_PIN_SDA && ENV_PIN_SCL #define TELEM_WIRE &Wire1 // Use Wire1 as the I2C bus for Environment Sensors @@ -684,7 +687,11 @@ bool EnvironmentSensorManager::begin() { bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { next_available_channel = TELEM_CHANNEL_SELF + 1; - if (requester_permissions & TELEM_PERM_LOCATION && gps_active) { + bool share_location = gps_active; + #if ENV_INCLUDE_GPS && defined(GPS_GUARD) + share_location = share_location && _have_trusted_fix; // never report an unverified position + #endif + if (requester_permissions & TELEM_PERM_LOCATION && share_location) { telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); } @@ -693,36 +700,77 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen _active_sensors[i].query(next_available_channel, _active_sensors[i].sub_channel, telemetry); next_available_channel++; } + #if ENV_INCLUDE_GPS && defined(GPS_GUARD) + if (_time_alert) { // GPS-vs-clock offset in hours on its own channel while the alert is active + telemetry.addAnalogInput(next_available_channel++, _time_disagree_offset / 3600.0f); + } + #endif } return true; } +#if ENV_INCLUDE_GPS +#ifdef GPS_GUARD +static uint32_t gpsMinEpoch(); +static uint32_t gpsMaxEpoch(); +#endif + +static const char* const GPS_SETTING_NAMES[] = { + "gps", +#ifdef GPS_GUARD + "gps_fence", // metres from gps_ref, 0 = off + "gps_ref", // reference position "lat;lon", 0;0 = none + "gps_guard", // read-only status +#endif +}; +static const int GPS_SETTING_COUNT = sizeof(GPS_SETTING_NAMES) / sizeof(GPS_SETTING_NAMES[0]); +#endif int EnvironmentSensorManager::getNumSettings() const { int settings = 0; #if ENV_INCLUDE_GPS - if (gps_detected) settings++; // only show GPS setting if GPS is detected + if (gps_detected) settings += GPS_SETTING_COUNT; // only show GPS settings if GPS is detected #endif return settings; } const char* EnvironmentSensorManager::getSettingName(int i) const { - int settings = 0; #if ENV_INCLUDE_GPS - if (gps_detected && i == settings++) { - return "gps"; + if (gps_detected && i >= 0 && i < GPS_SETTING_COUNT) { + return GPS_SETTING_NAMES[i]; } #endif return NULL; } const char* EnvironmentSensorManager::getSettingValue(int i) const { - int settings = 0; #if ENV_INCLUDE_GPS - if (gps_detected && i == settings++) { + const char* name = getSettingName(i); + if (name == NULL) return NULL; + if (strcmp(name, "gps") == 0) { return gps_active ? "1" : "0"; } + #ifdef GPS_GUARD + if (strcmp(name, "gps_fence") == 0) { + snprintf(_setting_buf, sizeof(_setting_buf), "%lu", (unsigned long)_gps_fence_m); + return _setting_buf; + } + if (strcmp(name, "gps_ref") == 0) { + // ";" not ",": companion custom vars are sent as "name:value,name:value" + snprintf(_setting_buf, sizeof(_setting_buf), "%.5f;%.5f", _gps_ref_lat, _gps_ref_lon); + return _setting_buf; + } + if (strcmp(name, "gps_guard") == 0) { + if (_time_alert) { + snprintf(_setting_buf, sizeof(_setting_buf), "%s time %s ALERT gps-clk %+lds %lum", _fix_status, _time_status, + _time_disagree_offset, (unsigned long)((millis() - _time_disagree_since_ms) / 60000)); + } else { + snprintf(_setting_buf, sizeof(_setting_buf), "%s time %s", _fix_status, _time_status); + } + return _setting_buf; + } + #endif #endif return NULL; } @@ -742,6 +790,44 @@ bool EnvironmentSensorManager::setSettingValue(const char* name, const char* val gps_update_interval_sec = interval_seconds > 0 ? interval_seconds : 1; return true; } + #ifdef GPS_GUARD + if (strcmp(name, "gps_fence") == 0) { + _gps_fence_m = atoi(value); + return true; + } + if (strcmp(name, "gps_ref") == 0) { + char* end; + double lat = strtod(value, &end); + if (*end != ',' && *end != ';') return false; // accept "lat,lon" and "lat;lon" + double lon = strtod(end + 1, NULL); + if (lat < -90 || lat > 90 || lon < -180 || lon > 180) return false; + _gps_ref_lat = lat; + _gps_ref_lon = lon; + return true; + } + if (strcmp(name, "gps_time_reset") == 0) { // clock was set manually: drop the GPS drift anchor + _time_accepted = false; + _time_disagree = false; + _time_alert = false; + return true; + } + if (strcmp(name, "gps_time_accept") == 0) { // operator confirms the alerted GPS time is right: apply it now + if (!_time_alert) return false; // only resolves an active alert + if (!_have_trusted_fix || (millis() - _last_trusted_ms) > 5000) return false; + long offset; + if (!_location->getClockOffset(offset)) return false; + if (labs(offset - _time_disagree_offset) > GPS_GUARD_ALERT_TOLERANCE) return false; // not the alerted offset + uint32_t gps_time = _location->getTimestamp(); + if (gps_time < gpsMinEpoch() || gps_time > gpsMaxEpoch()) return false; + if (!_location->applyTimeNow()) return false; + _time_accepted = true; + _last_time_accept_ms = millis(); + _time_disagree = false; + _time_alert = false; + snprintf(_time_status, sizeof(_time_status), "ok manual"); + return true; + } + #endif #endif return false; // not supported } @@ -749,6 +835,10 @@ bool EnvironmentSensorManager::setSettingValue(const char* name, const char* val #if ENV_INCLUDE_GPS void EnvironmentSensorManager::initBasicGPS() { + #ifdef GPS_GUARD + _location->setTimeGuard(timeGuardCallback, this); + #endif + Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX); #ifdef GPS_BAUD_RATE @@ -887,6 +977,160 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ } #endif +#ifdef GPS_GUARD +// Plausible GPS time window: not before this firmware was built, at most 10 years after. +static uint32_t gpsMinEpoch() { + static const uint32_t min_epoch = DateTime(__DATE__, __TIME__).unixtime() - 86400; + return min_epoch; +} +static uint32_t gpsMaxEpoch() { return gpsMinEpoch() + 10UL * 365 * 86400; } + +static double gpsDistanceM(double lat1, double lon1, double lat2, double lon2) { + const double R = 6371000.0, D2R = 0.017453292519943295; + double dlat = (lat2 - lat1) * D2R, dlon = (lon2 - lon1) * D2R; + double a = sin(dlat / 2) * sin(dlat / 2) + + cos(lat1 * D2R) * cos(lat2 * D2R) * sin(dlon / 2) * sin(dlon / 2); + return 2 * R * atan2(sqrt(a), sqrt(1 - a)); +} + +// Called once per second while the GPS is powered: decides whether the current fix is trustworthy. +void EnvironmentSensorManager::evaluateGpsFix() { + if (!_location->isValid()) { + _stable_count = 0; + snprintf(_fix_status, sizeof(_fix_status), "no fix %ldsat", _location->satellitesCount()); + return; + } + double lat = ((double)_location->getLatitude()) / 1000000.; + double lon = ((double)_location->getLongitude()) / 1000000.; + long sats = _location->satellitesCount(); + uint8_t hdop = _location->getHDOP(); + unsigned long now = millis(); + + const char* why = NULL; + double dist = 0; + if (sats < GPS_GUARD_MIN_SATS) { + why = "sats"; + } else if (hdop == 255 || hdop > GPS_GUARD_MAX_HDOP10) { + why = "hdop"; + } + #if defined(GPS_GUARD_LAT_MIN) && defined(GPS_GUARD_LAT_MAX) && defined(GPS_GUARD_LON_MIN) && defined(GPS_GUARD_LON_MAX) + else if (lat < GPS_GUARD_LAT_MIN || lat > GPS_GUARD_LAT_MAX || lon < GPS_GUARD_LON_MIN || lon > GPS_GUARD_LON_MAX) { + why = "region"; + } + #endif + else if (_gps_fence_m > 0 && (_gps_ref_lat != 0 || _gps_ref_lon != 0) && + (dist = gpsDistanceM(_gps_ref_lat, _gps_ref_lon, lat, lon)) > _gps_fence_m) { + why = "fence"; + } else if (_have_trusted_fix) { + dist = gpsDistanceM(_last_trusted_lat, _last_trusted_lon, lat, lon); + double secs = (now - _last_trusted_ms) / 1000.0; + if (secs < 1) secs = 1; + if (dist / secs > GPS_GUARD_MAX_SPEED_KMH / 3.6) why = "jump"; + } + + if (why) { + _stable_count = 0; + if (dist > 0) { + snprintf(_fix_status, sizeof(_fix_status), "rej %s %.1fkm", why, dist / 1000.0); + } else { + snprintf(_fix_status, sizeof(_fix_status), "rej %s", why); + } + MESH_DEBUG_PRINTLN("GPS fix rejected (%s): %f,%f sats=%ld hdop=%u", why, lat, lon, sats, hdop); + return; + } + + // Require several consecutive, mutually consistent fixes. + if (_stable_count > 0 && gpsDistanceM(_cand_lat, _cand_lon, lat, lon) > GPS_GUARD_STABLE_M) { + _stable_count = 0; + } + _cand_lat = lat; + _cand_lon = lon; + if (_stable_count < 255) _stable_count++; + if (_stable_count < GPS_GUARD_STABLE_FIXES) { + snprintf(_fix_status, sizeof(_fix_status), "settle %u/%u", _stable_count, GPS_GUARD_STABLE_FIXES); + return; + } + + node_lat = lat; + node_lon = lon; + node_altitude = ((double)_location->getAltitude()) / 1000.0; + if (!_have_trusted_fix) { + MESH_DEBUG_PRINTLN("GPS fix trusted: %f,%f sats=%ld hdop=%u", lat, lon, sats, hdop); + } + _have_trusted_fix = true; + _last_trusted_ms = now; + _last_trusted_lat = lat; + _last_trusted_lon = lon; + snprintf(_fix_status, sizeof(_fix_status), "ok %ldsat", sats); +} + +// A trusted fix whose time keeps disagreeing with the clock by the same offset is either +// a sustained spoof or a wrong clock; either way the operator should look at it. +void EnvironmentSensorManager::trackTimeDisagreement(long offset) { + unsigned long now = millis(); + if (!_time_disagree || labs(offset - _time_disagree_offset) > GPS_GUARD_ALERT_TOLERANCE) { + _time_disagree = true; + _time_disagree_offset = offset; + _time_disagree_since_ms = now; + _time_alert = false; + return; + } + if (!_time_alert && (now - _time_disagree_since_ms) >= GPS_GUARD_ALERT_MIN * 60000UL) { + _time_alert = true; + MESH_DEBUG_PRINTLN("GPS TIME ALERT: GPS differs from clock by %lds for %u min", offset, GPS_GUARD_ALERT_MIN); + } +} + +bool EnvironmentSensorManager::timeGuardCallback(void* ctx, uint32_t gps_time, uint32_t clock_time) { + return ((EnvironmentSensorManager*)ctx)->checkGpsTime(gps_time, clock_time); +} + +// GPS time is only written to the clock while a trusted fix is current, it is not +// older than this firmware, and (once the clock is valid) it only nudges the clock. +bool EnvironmentSensorManager::checkGpsTime(uint32_t gps_time, uint32_t clock_time) { + const uint32_t min_epoch = gpsMinEpoch(); + const uint32_t max_epoch = gpsMaxEpoch(); + + const char* why = NULL; + long step = 0; + if (!_have_trusted_fix || (millis() - _last_trusted_ms) > 5000) { + why = "nofix"; + } else if (gps_time < min_epoch || gps_time > max_epoch) { + why = "range"; + } else if (clock_time >= min_epoch) { // clock already holds a plausible time + step = (long)gps_time - (long)clock_time; + long limit = GPS_GUARD_TIME_MAX_STEP; + if (_time_accepted) { + // After the first accepted sync only allow corrections the size of real clock + // drift, so a spoofer cannot walk the clock away in many small steps. + uint32_t elapsed_s = (millis() - _last_time_accept_ms) / 1000; + limit = GPS_GUARD_TIME_DRIFT_BASE + (long)((uint64_t)elapsed_s * GPS_GUARD_TIME_DRIFT_PPM / 1000000UL); + if (limit > GPS_GUARD_TIME_MAX_STEP) limit = GPS_GUARD_TIME_MAX_STEP; + } + if (labs(step) > limit) why = "step"; + } + + if (why) { + if (step != 0) { + snprintf(_time_status, sizeof(_time_status), "rej %s %lds", why, step); + trackTimeDisagreement(step); + } else { + snprintf(_time_status, sizeof(_time_status), "rej %s", why); + } + return false; + } + snprintf(_time_status, sizeof(_time_status), "ok %+lds", (long)gps_time - (long)clock_time); + _time_disagree = false; + _time_alert = false; + // Every accepted sync anchors the drift budget (fail closed: no walking the clock in + // repeated MAX_STEP jumps). A wrongly anchored time is recovered with a manual clock + // set, which resets the anchor (gps_time_reset). + _time_accepted = true; + _last_time_accept_ms = millis(); + return true; +} +#endif + void EnvironmentSensorManager::start_gps() { gps_active = true; #ifdef RAK_WISBLOCK_GPS @@ -942,6 +1186,8 @@ void EnvironmentSensorManager::loop() { node_altitude = ((double)_location->getAltitude()) / 1000.0; MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude); } + #elif defined(GPS_GUARD) + // position is updated by evaluateGpsFix() below, only from trusted fixes #else if (_location->isValid()) { node_lat = ((double)_location->getLatitude())/1000000.; @@ -954,6 +1200,14 @@ void EnvironmentSensorManager::loop() { } next_gps_update = millis() + (gps_update_interval_sec * 1000); } + + #if defined(GPS_GUARD) && !defined(RAK_WISBLOCK_GPS) + static unsigned long next_guard_check = 0; + if (gps_active && _location->isEnabled() && (long)(millis() - next_guard_check) > 0) { + next_guard_check = millis() + 1000; + evaluateGpsFix(); + } + #endif #endif #if ENV_INCLUDE_BME680_BSEC if (bsec_active && bsec_iaq.run()) { diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index 17706cbc492..2437a4d290c 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -4,6 +4,43 @@ #include #include +#ifdef GPS_GUARD + // GPS anti-spoofing (opt-in). Optional region box: GPS_GUARD_LAT_MIN/MAX, GPS_GUARD_LON_MIN/MAX. + #ifndef GPS_GUARD_MIN_SATS + #define GPS_GUARD_MIN_SATS 4 + #endif + #ifndef GPS_GUARD_MAX_HDOP10 + #define GPS_GUARD_MAX_HDOP10 40 // HDOP 4.0 + #endif + #ifndef GPS_GUARD_FENCE_M + #define GPS_GUARD_FENCE_M 1000 // max distance from the reference position (0 = off) + #endif + #ifndef GPS_GUARD_STABLE_FIXES + #define GPS_GUARD_STABLE_FIXES 5 // consecutive consistent fixes before trusting + #endif + #ifndef GPS_GUARD_STABLE_M + #define GPS_GUARD_STABLE_M 100 + #endif + #ifndef GPS_GUARD_MAX_SPEED_KMH + #define GPS_GUARD_MAX_SPEED_KMH 250 // jump limit relative to the last trusted fix + #endif + #ifndef GPS_GUARD_TIME_MAX_STEP + #define GPS_GUARD_TIME_MAX_STEP 600 // max GPS-vs-clock correction (s) once the clock is valid + #endif + #ifndef GPS_GUARD_TIME_DRIFT_BASE + #define GPS_GUARD_TIME_DRIFT_BASE 5 // after the first sync: fixed allowance (s) ... + #endif + #ifndef GPS_GUARD_TIME_DRIFT_PPM + #define GPS_GUARD_TIME_DRIFT_PPM 100 // ... plus clock drift since that sync (~8.6 s/day) + #endif + #ifndef GPS_GUARD_ALERT_MIN + #define GPS_GUARD_ALERT_MIN 60 // alert after GPS disagrees with the clock this long ... + #endif + #ifndef GPS_GUARD_ALERT_TOLERANCE + #define GPS_GUARD_ALERT_TOLERANCE 60 // ... by an offset that stays within this many seconds + #endif +#endif + class EnvironmentSensorManager : public SensorManager { protected: static const int MAX_ACTIVE_SENSORS = 16; @@ -32,6 +69,32 @@ class EnvironmentSensorManager : public SensorManager { void rakGPSInit(); bool gpsIsAwake(uint8_t ioPin); #endif + + #ifdef GPS_GUARD + // Only plausible, stable fixes update the position, and GPS time is written to + // the clock only while such a fix is current. + mutable char _setting_buf[72]; + double _gps_ref_lat = 0, _gps_ref_lon = 0; // 0,0 = no reference position + uint32_t _gps_fence_m = GPS_GUARD_FENCE_M; + bool _have_trusted_fix = false; // node_lat/lon hold a trusted position + unsigned long _last_trusted_ms = 0; + double _last_trusted_lat = 0, _last_trusted_lon = 0; + double _cand_lat = 0, _cand_lon = 0; + uint8_t _stable_count = 0; + char _fix_status[24] = "no fix"; + char _time_status[24] = "-"; + bool _time_accepted = false; // a GPS time sync was accepted (drift budget anchor) + unsigned long _last_time_accept_ms = 0; + // Sustained, consistent GPS-vs-clock disagreement (rejected syncs) raises an alert. + bool _time_disagree = false; + long _time_disagree_offset = 0; // GPS minus clock, seconds + unsigned long _time_disagree_since_ms = 0; + bool _time_alert = false; + void evaluateGpsFix(); + bool checkGpsTime(uint32_t gps_time, uint32_t clock_time); + void trackTimeDisagreement(long offset); + static bool timeGuardCallback(void* ctx, uint32_t gps_time, uint32_t clock_time); + #endif #endif public: diff --git a/src/helpers/sensors/LocationProvider.h b/src/helpers/sensors/LocationProvider.h index bc4f1fe7648..09bd4ca14a8 100644 --- a/src/helpers/sensors/LocationProvider.h +++ b/src/helpers/sensors/LocationProvider.h @@ -6,10 +6,24 @@ class LocationProvider { +public: + // Optional veto on GPS time before it is written to the clock (e.g. anti-spoofing). + typedef bool (*TimeGuardFn)(void* ctx, uint32_t gps_time, uint32_t clock_time); + protected: bool _time_sync_needed = true; + TimeGuardFn _time_guard = NULL; + void* _time_guard_ctx = NULL; + + bool timeSyncAllowed(uint32_t gps_time, uint32_t clock_time) { + return _time_guard == NULL || _time_guard(_time_guard_ctx, gps_time, clock_time); + } public: + void setTimeGuard(TimeGuardFn fn, void* ctx) { _time_guard = fn; _time_guard_ctx = ctx; } + virtual uint8_t getHDOP() { return 255; } // in tenths, 255 = unknown + virtual bool getClockOffset(long& offset) { return false; } // GPS time minus clock time + virtual bool applyTimeNow() { return false; } // write GPS time to the clock, bypassing the guard virtual void syncTime() { _time_sync_needed = true; } virtual bool waitingTimeSync() { return _time_sync_needed; } virtual long getLatitude() = 0; diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index 37ef5762c48..2ef5b17f4c8 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -166,6 +166,19 @@ public : return alt; } long satellitesCount() override { return nmea.getNumSatellites(); } + uint8_t getHDOP() override { return nmea.getHDOP(); } + bool getClockOffset(long& offset) override { + if (_clock == NULL || !isValid() || time_valid < 3) return false; + offset = (long)getTimestamp() - (long)_clock->getCurrentTime(); + return true; + } + bool applyTimeNow() override { + if (_clock == NULL || !isValid() || time_valid < 3) return false; + _clock->setCurrentTime(getTimestamp()); + _time_sync_needed = false; + _last_time_sync = millis(); + return true; + } bool isValid() override { return nmea.isValid(); } long getTimestamp() override { @@ -243,8 +256,9 @@ public : _time_sync_needed = true; } if (_time_sync_needed && time_valid > 2) { - if (_clock != NULL) { - _clock->setCurrentTime(getTimestamp()); + uint32_t gps_time = getTimestamp(); + if (_clock != NULL && timeSyncAllowed(gps_time, _clock->getCurrentTime())) { + _clock->setCurrentTime(gps_time); _time_sync_needed = false; _last_time_sync = millis(); }