Target signed fleet commands by region and GPS radius

This commit is contained in:
mikecarper committed 2026-10-09 01:32:12 -07:00
1 parent 8b3655d76a
commit 4d446c2cd0
24 files changed
+2447 -107

No files matched your search

+2 -2
View File
@@ -248,7 +248,7 @@ void CommonCLI::beginManagement(mesh::Mesh& mesh, FILESYSTEM* fs) {
#if MESH_ENABLE_FLEET_CONTROL
if (_callbacks->supportsFleetControl()
&& (fs->exists("/fleet_channel") || fs->exists("/fleet_channel.bak"))) {
_fleet_channel = new (std::nothrow) mesh::FleetChannel(fs, _callbacks);
_fleet_channel = new (std::nothrow) mesh::FleetChannel(fs, _callbacks, _region_map);
}
#endif
if (!_data_route) {
@@ -279,7 +279,7 @@ bool CommonCLI::handleFleetCommand(const char* command, char* reply) {
if (!_callbacks->supportsFleetControl()) {
strcpy(reply, "Err - fleet control is available on infrastructure roles"); return true;
}
if (!_fleet_channel) _fleet_channel = new (std::nothrow) mesh::FleetChannel(_management_fs, _callbacks);
if (!_fleet_channel) _fleet_channel = new (std::nothrow) mesh::FleetChannel(_management_fs, _callbacks, _region_map);
if (!_fleet_channel) {
strcpy(reply, "Err - fleet control memory unavailable"); return true;
}
+2 -1
View File
@@ -213,7 +213,8 @@ FleetChannel::DecodeResult FleetChannel::decode(Mesh& mesh, FleetCommand::Decode
&& storage::readLE32(envelope + 4) > last_sequence_
&& (!assembled || storage::readLE32(envelope + 4) == assembly_sequence_)
&& FleetCommand::decode(controller_, channel_.secret, envelope, envelope_length,
mesh.getRTCClock()->getCurrentTime(), mesh.self_id.pub_key, command);
mesh.getRTCClock()->getCurrentTime(), mesh.self_id.pub_key, command,
matchesFleetRegion, regions_, matchesFleetLocation, hooks_);
if (assembled || (valid && assembly_parts_ && command.sequence >= assembly_sequence_)) clearAssembly();
if (!valid) return DecodeResult::Rejected;
broadcast_ = command.broadcast;
+48 -2
View File
@@ -6,6 +6,8 @@
#include <helpers/TempRadioReplyBarrier.h>
#include <helpers/FleetCommand.h>
#include <helpers/TransportKeyStore.h>
#include <helpers/RegionMap.h>
#include <helpers/RegionNameUtils.h>
namespace mesh {
@@ -16,8 +18,19 @@ struct FleetReplyHooks {
virtual void endFleetCommand() {}
virtual bool hasFleetReplyMutation() const { return false; }
virtual void finishFleetReplyMutation(bool) {}
virtual bool getFleetLocation(int32_t&, int32_t&) const { return false; }
};
inline bool matchesFleetLocation(void* context, int32_t latitude_e6,
int32_t longitude_e6, uint32_t radius_meters) {
auto* hooks = static_cast<FleetReplyHooks*>(context);
int32_t local_latitude_e6 = 0, local_longitude_e6 = 0;
if (!hooks || !hooks->getFleetLocation(local_latitude_e6, local_longitude_e6)
|| (!local_latitude_e6 && !local_longitude_e6)) return false;
return FleetCommand::withinRadius(latitude_e6, longitude_e6, radius_meters,
local_latitude_e6, local_longitude_e6);
}
// Scope codes authenticate the original packet's payload. A new reply needs
// fresh codes from the exact matching region key, including rotated keys.
template <typename Regions, typename Region>
@@ -35,12 +48,44 @@ bool captureFleetReplyScope(Regions& regions, const Region* region,
return false;
}
// Fleet selectors read the current region map without changing routing.
// A configured-list selector includes denied regions too; home selectors
// follow only the selected home's parent chain, never the default TX scope.
inline bool matchesFleetRegion(void* context, FleetCommand::RegionTarget kind,
const char* name, size_t length) {
auto* regions = static_cast<RegionMap*>(context);
if (!regions || !name || !length || length > FleetCommand::MaxRegionNameLength) return false;
const auto matches = [name, length](const RegionEntry* entry) {
if (!entry || entry->isWildcard()) return false;
const char* canonical = RegionNameUtils::canonical(entry->name);
const size_t available = sizeof(entry->name) - size_t(canonical - entry->name);
const size_t actual = strnlen(canonical, available);
return actual < available && actual == length && !memcmp(canonical, name, length);
};
const int count = regions->getCount();
if (count < 0 || count > MAX_REGION_ENTRIES) return false;
if (kind == FleetCommand::RegionTarget::Configured) {
for (int i = 0; i < count; ++i) {
if (matches(regions->getByIdx(i))) return true;
}
} else if (kind == FleetCommand::RegionTarget::Home) {
const RegionEntry* entry = regions->getHomeRegion();
// Bound traversal even if a damaged in-memory map has a parent cycle.
for (int depth = 0; entry && !entry->isWildcard() && depth < count; ++depth) {
if (matches(entry)) return true;
entry = regions->findById(entry->parent);
}
}
return false;
}
// Optional infrastructure-only receiver. The publisher's private key is never
// enrolled here. The ordinary channel key provides privacy; signatures grant
// the deliberately narrower fleet capability.
class FleetChannel {
FILESYSTEM* fs_;
FleetReplyHooks* hooks_;
RegionMap* regions_;
GroupChannel channel_{};
Identity controller_;
uint32_t last_sequence_ = 0;
@@ -77,8 +122,9 @@ class FleetChannel {
void serviceDeadline(Mesh& mesh, RadioProfileCLI& profiles);
public:
explicit FleetChannel(FILESYSTEM* fs, FleetReplyHooks* hooks = nullptr)
: fs_(fs), hooks_(hooks) { healthy_ = load(); }
explicit FleetChannel(FILESYSTEM* fs, FleetReplyHooks* hooks = nullptr,
RegionMap* regions = nullptr)
: fs_(fs), hooks_(hooks), regions_(regions) { healthy_ = load(); }
bool handleConfig(const char* command, char* reply, size_t capacity);
void receive(Packet* packet, Mesh& mesh, const TransportKey* scope = nullptr);
void complete(Packet* packet, RadioProfileCLI& profiles);
+188 -19
View File
@@ -1,5 +1,6 @@
#include "FleetCommand.h"
#include <SHA256.h>
#include <math.h>
#include <string.h>
namespace mesh {
@@ -49,11 +50,93 @@ void targetHash(const uint8_t* full_key, uint8_t* target) {
hash.finalize(target, FleetCommand::TargetSize);
}
bool regionNameAllowed(const char* name, size_t length) {
if (!length || length > FleetCommand::MaxRegionNameLength) return false;
for (size_t i = 0; i < length; ++i) {
const unsigned char value = name[i];
// Match RegionMap's name characters within printable ASCII only.
if (value != '-' && value != '$' && value != '#'
&& !(value >= '0' && value <= '9')
&& !(value >= 'A' && value <= '~')) return false;
}
return true;
}
bool coordinatesAllowed(int32_t latitude_e6, int32_t longitude_e6) {
return latitude_e6 >= -90000000 && latitude_e6 <= 90000000
&& longitude_e6 >= -180000000 && longitude_e6 <= 180000000;
}
bool geoAllowed(int32_t latitude_e6, int32_t longitude_e6, uint32_t radius) {
return coordinatesAllowed(latitude_e6, longitude_e6)
&& radius >= FleetCommand::MinRadiusMeters && radius <= FleetCommand::MaxRadiusMeters;
}
// Convert bounded decimal text to fixed point without floating-point rounding,
// exponent aliases, silently dropped precision, or intermediate overflow.
bool scaledDecimal(const char* text, size_t length, unsigned precision,
uint32_t maximum, bool signed_value, int32_t& result) {
if (!length) return false;
size_t cursor = 0;
bool negative = false;
if (text[cursor] == '+' || text[cursor] == '-') {
if (!signed_value || ++cursor == length) return false;
negative = text[0] == '-';
}
if (text[cursor] < '0' || text[cursor] > '9') return false;
if (text[cursor] == '0' && cursor + 1 < length
&& text[cursor + 1] >= '0' && text[cursor + 1] <= '9') return false;
uint32_t scale = 1;
for (unsigned i = 0; i < precision; ++i) scale *= 10;
const uint32_t maximum_whole = maximum / scale;
uint32_t whole = 0;
while (cursor < length && text[cursor] >= '0' && text[cursor] <= '9') {
const uint32_t digit = unsigned(text[cursor++] - '0');
if (digit > maximum_whole || whole > (maximum_whole - digit) / 10) return false;
whole = whole * 10 + digit;
}
uint32_t fraction = 0;
unsigned digits = 0;
if (cursor < length) {
if (text[cursor++] != '.' || cursor == length) return false;
while (cursor < length) {
if (text[cursor] < '0' || text[cursor] > '9' || digits == precision) return false;
fraction = fraction * 10 + unsigned(text[cursor++] - '0');
++digits;
}
}
while (digits++ < precision) fraction *= 10;
const uint32_t magnitude = whole * scale + fraction;
if (magnitude > maximum) return false;
result = negative ? -int32_t(magnitude) : int32_t(magnitude);
return true;
}
bool parseGeo(const char* token, size_t length, int32_t& latitude_e6,
int32_t& longitude_e6, uint32_t& radius) {
size_t cursor = 4; // gps:
int32_t values[3];
for (unsigned i = 0; i < 3; ++i) {
const size_t start = cursor;
const char separator = i == 0 ? ',' : ':';
while (cursor < length && token[cursor] != separator) ++cursor;
if ((i < 2 && cursor == length) || (i == 2 && cursor != length)
|| !scaledDecimal(token + start, cursor - start, i == 2 ? 3 : 6,
i == 0 ? 90000000 : i == 1 ? 180000000 : FleetCommand::MaxRadiusMeters,
i != 2, values[i])) return false;
if (cursor < length) ++cursor;
}
latitude_e6 = values[0]; longitude_e6 = values[1]; radius = uint32_t(values[2]);
return geoAllowed(latitude_e6, longitude_e6, radius);
}
// Validate every record, including records after one that matches this node.
// An absent self key is used by encode to validate externally supplied lists.
bool targetRecords(const uint8_t* data, size_t available, uint8_t count,
const uint8_t* self, size_t& consumed, bool& matched,
bool& broadcast) {
bool& broadcast, FleetCommand::RegionMatcher matcher = nullptr,
void* context = nullptr, FleetCommand::GeoMatcher geo_matcher = nullptr,
void* geo_context = nullptr) {
consumed = 0;
matched = count == 0;
broadcast = count == 0 || count > 1;
@@ -63,6 +146,30 @@ bool targetRecords(const uint8_t* data, size_t available, uint8_t count,
for (unsigned i = 0; i < count; ++i) {
if (consumed >= available) return false;
const uint8_t type = data[consumed++];
if (type == FleetCommand::GeoRecordType) {
constexpr size_t fields = FleetCommand::GeoRecordLength - 1;
if (fields > available - consumed || consumed + fields > FleetCommand::MaxTargetBytes) return false;
const int32_t latitude_e6 = int32_t(read32(data + consumed));
const int32_t longitude_e6 = int32_t(read32(data + consumed + 4));
const uint32_t radius = read32(data + consumed + 8);
if (!geoAllowed(latitude_e6, longitude_e6, radius)) return false;
broadcast = true;
if (self && geo_matcher && geo_matcher(geo_context, latitude_e6, longitude_e6, radius)) matched = true;
consumed += fields;
continue;
}
if (type == FleetCommand::RegionRecordType || type == FleetCommand::HomeRecordType) {
if (consumed >= available) return false;
const uint8_t length = data[consumed++];
if (length > available - consumed || consumed + length > FleetCommand::MaxTargetBytes
|| !regionNameAllowed(reinterpret_cast<const char*>(data + consumed), length)) return false;
broadcast = true;
if (self && matcher && matcher(context, type == FleetCommand::RegionRecordType
? FleetCommand::RegionTarget::Configured : FleetCommand::RegionTarget::Home,
reinterpret_cast<const char*>(data + consumed), length)) matched = true;
consumed += length;
continue;
}
if (type != 4 && type != 6 && type != FleetCommand::TargetSize) return false;
if (type > available - consumed
|| consumed + type > FleetCommand::MaxTargetBytes) return false;
@@ -265,30 +372,89 @@ bool FleetCommand::parseTargets(const char* text, size_t length, Targets& target
size_t cursor = 0;
while (cursor < length) {
const size_t start = cursor;
while (cursor < length && text[cursor] != ',') ++cursor;
// A semicolon always terminates a target. GPS owns its one coordinate
// comma, so malformed coordinates cannot consume another target.
while (cursor < length && text[cursor] != ';') ++cursor;
const size_t token_length = cursor - start;
if (token_length != 8 && token_length != 12 && token_length != PUB_KEY_SIZE * 2) return fail();
const size_t type = token_length == PUB_KEY_SIZE * 2 ? TargetSize : token_length / 2;
if (targets.count == MaxTargetCount || targets.length + 1 + type > MaxTargetBytes) return fail();
uint8_t full_key[PUB_KEY_SIZE];
const size_t byte_length = token_length / 2;
for (size_t i = 0; i < byte_length; ++i) {
const int upper = hexDigit(text[start + i * 2]);
const int lower = hexDigit(text[start + i * 2 + 1]);
if (upper < 0 || lower < 0) return fail();
full_key[i] = uint8_t((upper << 4) | lower);
if (!token_length || targets.count == MaxTargetCount) return fail();
const char* token = text + start;
const bool gps = token_length >= 4 && !memcmp(token, "gps:", 4);
const bool region = token_length >= 7 && !memcmp(token, "region:", 7);
const bool home = token_length >= 5 && !memcmp(token, "home:", 5);
const bool explicit_name = region || home;
const bool key = !explicit_name && !gps
&& (token_length == 8 || token_length == 12 || token_length == PUB_KEY_SIZE * 2);
if (gps) {
int32_t latitude_e6, longitude_e6;
uint32_t radius;
if (targets.length + GeoRecordLength > MaxTargetBytes
|| !parseGeo(token, token_length, latitude_e6, longitude_e6, radius)) return fail();
targets.data[targets.length++] = GeoRecordType;
write32(targets.data + targets.length, uint32_t(latitude_e6));
write32(targets.data + targets.length + 4, uint32_t(longitude_e6));
write32(targets.data + targets.length + 8, radius);
targets.length += GeoRecordLength - 1;
} else if (key) {
const size_t type = token_length == PUB_KEY_SIZE * 2 ? TargetSize : token_length / 2;
if (targets.length + 1 + type > MaxTargetBytes) return fail();
uint8_t full_key[PUB_KEY_SIZE];
const size_t byte_length = token_length / 2;
for (size_t i = 0; i < byte_length; ++i) {
const int upper = hexDigit(token[i * 2]);
const int lower = hexDigit(token[i * 2 + 1]);
if (upper < 0 || lower < 0) return fail();
full_key[i] = uint8_t((upper << 4) | lower);
}
if (type == TargetSize && zero(full_key, PUB_KEY_SIZE)) return fail();
targets.data[targets.length++] = uint8_t(type);
if (type == TargetSize) targetHash(full_key, targets.data + targets.length);
else memcpy(targets.data + targets.length, full_key, type);
targets.length += uint8_t(type);
} else {
if (!explicit_name) {
if (keyEquals(token, token_length, "all") || keyEquals(token, token_length, "get")
|| keyEquals(token, token_length, "set") || keyEquals(token, token_length, "del")
|| keyEquals(token, token_length, "time") || keyEquals(token, token_length, "clock")) return fail();
bool all_hex = true;
for (size_t i = 0; i < token_length; ++i) all_hex = all_hex && hexDigit(token[i]) >= 0;
if (all_hex) return fail();
}
const size_t prefix = region ? 7 : home ? 5 : 0;
const char* name = token + prefix;
size_t name_length = token_length - prefix;
if (name_length && *name == '#') { ++name; --name_length; }
if (!regionNameAllowed(name, name_length)
|| targets.length + 2 + name_length > MaxTargetBytes) return fail();
targets.data[targets.length++] = home ? HomeRecordType : RegionRecordType;
targets.data[targets.length++] = uint8_t(name_length);
memcpy(targets.data + targets.length, name, name_length);
targets.length += uint8_t(name_length);
}
if (type == TargetSize && zero(full_key, PUB_KEY_SIZE)) return fail();
targets.data[targets.length++] = uint8_t(type);
if (type == TargetSize) targetHash(full_key, targets.data + targets.length);
else memcpy(targets.data + targets.length, full_key, type);
targets.length += uint8_t(type);
++targets.count;
if (cursor < length && ++cursor == length) return fail();
}
return true;
}
bool FleetCommand::withinRadius(int32_t center_latitude_e6, int32_t center_longitude_e6,
uint32_t radius_meters, int32_t local_latitude_e6,
int32_t local_longitude_e6) {
if (!geoAllowed(center_latitude_e6, center_longitude_e6, radius_meters)
|| !coordinatesAllowed(local_latitude_e6, local_longitude_e6)) return false;
constexpr double radians_per_e6_degree = 0.000001 * 0.017453292519943295769;
const double center_latitude = double(center_latitude_e6) * radians_per_e6_degree;
const double local_latitude = double(local_latitude_e6) * radians_per_e6_degree;
const double latitude_half = (double(local_latitude_e6) - center_latitude_e6) * radians_per_e6_degree / 2;
const double longitude_half = (double(local_longitude_e6) - center_longitude_e6) * radians_per_e6_degree / 2;
const double latitude_sine = sin(latitude_half), longitude_sine = sin(longitude_half);
double haversine = latitude_sine * latitude_sine
+ cos(center_latitude) * cos(local_latitude) * longitude_sine * longitude_sine;
if (haversine < 0) haversine = 0;
else if (haversine > 1) haversine = 1;
const double distance = 2 * 6371008.8 * atan2(sqrt(haversine), sqrt(1 - haversine));
return distance <= double(radius_meters);
}
bool FleetCommand::commandAllowed(const char* command) {
if (!command) return false;
size_t length = 0;
@@ -411,7 +577,9 @@ size_t FleetCommand::encode(const LocalIdentity& publisher, const uint8_t* key,
bool FleetCommand::decode(const Identity& publisher, const uint8_t* key,
const uint8_t* payload, size_t length, uint32_t now,
const uint8_t* self_public_key, Decoded& output) {
const uint8_t* self_public_key, Decoded& output,
RegionMatcher matcher, void* context,
GeoMatcher geo_matcher, void* geo_context) {
memset(&output, 0, sizeof(output));
if (!privateKeyAllowed(key) || !payload || !self_public_key
|| zero(publisher.pub_key, PUB_KEY_SIZE) || zero(self_public_key, PUB_KEY_SIZE)
@@ -433,7 +601,8 @@ bool FleetCommand::decode(const Identity& publisher, const uint8_t* key,
size_t consumed;
bool matched;
if (!targetRecords(payload + 13, unsigned_length - 13, payload[12],
self_public_key, consumed, matched, broadcast)
self_public_key, consumed, matched, broadcast, matcher, context,
geo_matcher, geo_context)
|| !matched) return false;
command_offset = MinHeaderSize + consumed;
if (command_offset > unsigned_length) return false;
+28 -3
View File
@@ -27,6 +27,13 @@ public:
static constexpr size_t FixedTargetMaxCommandLength = MaxEnvelopeLength - HeaderSize - SignatureSize;
static constexpr size_t MaxTargetBytes = 86;
static constexpr size_t MaxTargetCount = 17;
static constexpr uint8_t RegionRecordType = 0x20;
static constexpr uint8_t HomeRecordType = 0x21;
static constexpr uint8_t GeoRecordType = 0x22;
static constexpr size_t GeoRecordLength = 13;
static constexpr size_t MaxRegionNameLength = 30;
static constexpr uint32_t MinRadiusMeters = 1;
static constexpr uint32_t MaxRadiusMeters = 20050000;
static constexpr uint32_t MinEpoch = 1735689600UL; // 2025-01-01 UTC
static constexpr uint32_t MaxLifetime = 600;
static constexpr uint32_t MaxClockLead = 60;
@@ -43,9 +50,20 @@ public:
uint8_t count;
// Each record is a length/type byte (4, 6, or 16) followed by that many
// bytes. Prefixes are raw public-key bytes; complete keys are SHA-256/128.
// Region/home records are type 0x20/0x21, name length, then canonical name.
// GPS records are type 0x22, signed latitude/longitude E6, radius metres LE32.
uint8_t data[MaxTargetBytes];
};
enum class RegionTarget { Configured, Home };
// Names are length-delimited and case-sensitive, with one leading '#' alias
// removed by the encoder. The role matches against its existing RegionMap.
// This bounded, read-only callback must not execute or mutate any state.
using RegionMatcher = bool (*)(void* context, RegionTarget target,
const char* name, size_t length);
using GeoMatcher = bool (*)(void* context, int32_t latitude_e6,
int32_t longitude_e6, uint32_t radius_meters);
struct Fragment {
uint32_t sequence;
uint16_t total_length;
@@ -58,10 +76,15 @@ public:
// "all" becomes the reserved zero target; otherwise require one complete
// public key and derive the first 16 bytes of SHA-256, not a short node ID.
static bool parseTarget(const char* text, uint8_t* target);
// Parse one bounded token containing comma-separated 8-, 12-, or 64-digit
// public keys. "all" is accepted only alone and produces zero records.
// Parse one bounded token containing semicolon-separated public keys and named
// regions. Bare key-length tokens never fall back to region names; explicit
// region:/home: selectors permit ambiguous names. GPS circles use
// gps:latitude,longitude:radius-km. "all" is accepted alone.
static bool parseTargets(const char* text, size_t length, Targets& targets);
static bool commandAllowed(const char* command);
static bool withinRadius(int32_t center_latitude_e6, int32_t center_longitude_e6,
uint32_t radius_meters, int32_t local_latitude_e6,
int32_t local_longitude_e6);
// Return the exact envelope size, or zero on invalid input. The signature
// binds the domain, private channel key, complete header, and command bytes.
@@ -83,7 +106,9 @@ public:
// any mutation, and defer execution until the receive stack has unwound.
static bool decode(const Identity& publisher, const uint8_t* key,
const uint8_t* payload, size_t length, uint32_t now,
const uint8_t* self_public_key, Decoded& output);
const uint8_t* self_public_key, Decoded& output,
RegionMatcher matcher = nullptr, void* context = nullptr,
GeoMatcher geo_matcher = nullptr, void* geo_context = nullptr);
// Envelopes <= MaxPayloadLength travel unchanged. Longer envelopes use
// exactly two FMP1 frames, each bounded by the group payload limit.
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <math.h>
#include <stdint.h>
namespace mesh {
// Private fleet matching uses a configured fixed position first. Only the
// default/unset pair falls back to an existing GPS cache; never wake GPS here.
template<class Prefs, class Sensors>
bool readFleetLocation(const Prefs& prefs, const Sensors& sensors,
int32_t& latitude_e6, int32_t& longitude_e6) {
latitude_e6 = longitude_e6 = 0;
double latitude = prefs.node_lat;
double longitude = prefs.node_lon;
if (latitude == 0 && longitude == 0
&& !sensors.getCachedGpsPosition(latitude, longitude)) return false;
if (!isfinite(latitude) || !isfinite(longitude)
|| latitude < -90.0 || latitude > 90.0
|| longitude < -180.0 || longitude > 180.0) return false;
const int32_t lat = static_cast<int32_t>(lround(latitude * 1000000.0));
const int32_t lon = static_cast<int32_t>(lround(longitude * 1000000.0));
if (lat == 0 && lon == 0) return false;
latitude_e6 = lat;
longitude_e6 = lon;
return true;
}
} // namespace mesh
+21 -1
View File
@@ -1,4 +1,21 @@
#include "SensorManager.h"
#include <math.h>
bool SensorManager::getCachedGpsPosition(double& latitude, double& longitude) const {
latitude = longitude = 0;
#if ENV_INCLUDE_GPS
if (!gpsTelemetryCacheFresh(millis())
|| !isfinite(gps_cache_lat) || !isfinite(gps_cache_lon)
|| gps_cache_lat < -90.0f || gps_cache_lat > 90.0f
|| gps_cache_lon < -180.0f || gps_cache_lon > 180.0f
|| (gps_cache_lat == 0 && gps_cache_lon == 0)) return false;
latitude = gps_cache_lat;
longitude = gps_cache_lon;
return true;
#else
return false;
#endif
}
#if ENV_INCLUDE_GPS
#ifndef GPS_TELEMETRY_CACHE_INTERVAL_SEC
@@ -37,7 +54,7 @@ bool SensorManager::gpsTelemetryHoldActive(unsigned long now) const {
bool SensorManager::gpsTelemetryCacheFresh(unsigned long now) const {
return gps_cache_valid &&
(unsigned long)(now - gps_cache_updated_at) <= GPS_TELEMETRY_MAX_STALE_SEC * 1000UL;
static_cast<uint32_t>(now - gps_cache_updated_at) <= GPS_TELEMETRY_MAX_STALE_SEC * 1000UL;
}
void SensorManager::updateGpsTelemetryCache(float lat, float lon, float altitude, unsigned long now) {
@@ -176,6 +193,9 @@ void SensorManager::processGpsTelemetryFix(float lat, float lon, float altitude,
}
void SensorManager::loopGpsTelemetry(unsigned long now) {
// Expire the validity bit even while a bridge owns the UART. A stale fix
// must not become fresh again when the 32-bit millis counter cycles.
if (gps_cache_valid && !gpsTelemetryCacheFresh(now)) gps_cache_valid = false;
if (!gps_transport_available) return;
if (!gps_user_enabled && !gpsTelemetryHoldActive(now) && !gps_acquiring) {
maybeStopGpsForTelemetry(now);
+3
View File
@@ -109,6 +109,9 @@ public:
}
virtual void loop() { }
virtual void setTelemetryLocationAccessAvailable(bool available);
// Read the existing fresh cache without acquiring GPS, changing power, or
// taking a UART. Sleeping/off receivers may retain a recent valid fix.
bool getCachedGpsPosition(double& latitude, double& longitude) const;
bool requestGpsTelemetryTimeSync(uint64_t min_interval_secs) {
#if ENV_INCLUDE_GPS
// A denied request must not consume the provider's throttle or reclaim a