Target signed fleet commands by region and GPS radius
This commit is contained in:
1 parent
8b3655d76a
commit
4d446c2cd0
24 files changed
+2447
-107
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user