Manage MediaMesh forwarding region with saved radio2 profiles
This commit is contained in:
1 parent
a301820be3
commit
71e42b2e62
21 files changed
+831
-7
No files matched your search
@@ -16,6 +16,7 @@
|
||||
#include "HostKiss.h"
|
||||
#include "PowerSavingDiagnostics.h"
|
||||
#include "HilStartupTrace.h"
|
||||
#include "FilePresence.h"
|
||||
#if defined(ESP32_PLATFORM)
|
||||
#include <helpers/esp32/CpuClockPolicy.h>
|
||||
#include <helpers/esp32/PowerTestControls.h>
|
||||
@@ -166,6 +167,57 @@ static bool isGpioConfig(const char* config) {
|
||||
}
|
||||
#endif
|
||||
|
||||
bool CommonCLI::mediaMeshRegionStoreMissing(FILESYSTEM* fs) {
|
||||
if (!fs) return false;
|
||||
static const char* const paths[] = {"/regions2", "/regions2.tmp", "/regions2.bak"};
|
||||
for (const char* path : paths) {
|
||||
#if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
|
||||
bool present;
|
||||
if (!mesh::filePresence(fs, path, present) || present) return false;
|
||||
#else
|
||||
if (fs->exists(path)) return false;
|
||||
#endif
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CommonCLI::updateMediaMeshScope(void* context, bool enable, bool commit) {
|
||||
auto* cli = static_cast<CommonCLI*>(context);
|
||||
if (!cli || !cli->_region_map || !cli->_callbacks || !cli->_media_regions_ready) return false;
|
||||
auto* region = cli->_region_map->findByName("mediamesh");
|
||||
if (!commit) {
|
||||
return !enable || region || cli->_region_map->getCount() < MAX_REGION_ENTRIES;
|
||||
}
|
||||
if (!region) {
|
||||
if (!enable) return true;
|
||||
region = cli->_region_map->putRegion("mediamesh", 0);
|
||||
if (!region) return false;
|
||||
}
|
||||
if (enable) region->flags &= ~REGION_DENY_FLOOD;
|
||||
else region->flags |= REGION_DENY_FLOOD;
|
||||
// Retry storage even when the RAM flags already match: the previous attempt
|
||||
// may have failed after this mutation. RadioProfileCLI owns paced retries.
|
||||
return cli->_callbacks->saveRegions();
|
||||
}
|
||||
|
||||
void CommonCLI::onRegionsLoaded(FILESYSTEM* fs, bool loaded) {
|
||||
_management_fs = fs;
|
||||
_media_regions_ready = fs && (loaded || mediaMeshRegionStoreMissing(fs));
|
||||
_media_regions_retry_ms = millis() + 1000;
|
||||
_radio_profiles.setMediaMeshScopeHandler(updateMediaMeshScope, this);
|
||||
}
|
||||
|
||||
void CommonCLI::recoverMediaMeshRegions() {
|
||||
if (_media_regions_ready || !_management_fs || !_region_map || !_callbacks
|
||||
|| (int32_t)(millis() - _media_regions_retry_ms) < 0) return;
|
||||
_media_regions_retry_ms = millis() + 1000;
|
||||
const bool loaded = _region_map->load(_management_fs);
|
||||
if (!loaded && !mediaMeshRegionStoreMissing(_management_fs)) return;
|
||||
_media_regions_ready = true;
|
||||
if (loaded) _callbacks->onDefaultRegionChanged(_region_map->getDefaultRegion());
|
||||
_radio_profiles.setMediaMeshScopeHandler(updateMediaMeshScope, this);
|
||||
}
|
||||
|
||||
void CommonCLI::loop() {
|
||||
#if MESH_ENABLE_BATTERY_FORWARDING
|
||||
loopBatteryForwarding();
|
||||
@@ -180,6 +232,7 @@ void CommonCLI::loop() {
|
||||
}
|
||||
#endif
|
||||
if (!mesh::morseTimingActive(_callbacks->getProfileRadio())) {
|
||||
recoverMediaMeshRegions();
|
||||
servicePendingTxPower();
|
||||
servicePendingRxSettings();
|
||||
_radio_profiles.loop();
|
||||
|
||||
@@ -953,6 +953,11 @@ class CommonCLI {
|
||||
#endif
|
||||
SensorManager* _sensors;
|
||||
RegionMap* _region_map;
|
||||
uint32_t _media_regions_retry_ms = 0;
|
||||
bool _media_regions_ready = false;
|
||||
static bool mediaMeshRegionStoreMissing(FILESYSTEM* fs);
|
||||
void recoverMediaMeshRegions();
|
||||
static bool updateMediaMeshScope(void* context, bool enable, bool commit);
|
||||
ClientACL* _acl;
|
||||
char tmp[COMMON_CLI_TMP_LEN];
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
@@ -1072,6 +1077,9 @@ public:
|
||||
uint16_t preamble);
|
||||
mesh::RadioProfileCLI& radioProfiles() { return _radio_profiles; }
|
||||
const mesh::RadioProfileCLI& radioProfiles() const { return _radio_profiles; }
|
||||
// Bind only after the role has loaded its saved regions, so boot-time
|
||||
// radio2 scope reconciliation does not get replaced by the region image.
|
||||
void onRegionsLoaded(FILESYSTEM* fs, bool loaded);
|
||||
mesh::MorseReplyHandoff& morseReply() { return _morse_reply; }
|
||||
const mesh::MorseReplyHandoff& morseReply() const { return _morse_reply; }
|
||||
#if MESH_ENABLE_FLEET_CONTROL
|
||||
|
||||
@@ -92,6 +92,42 @@ bool secondaryOverlaps(uint32_t primary_hz, float secondary_mhz, uint32_t cleara
|
||||
return (primary_hz > secondary_hz ? primary_hz - secondary_hz : secondary_hz - primary_hz)
|
||||
<= clearance_hz;
|
||||
}
|
||||
bool isMediaMeshProfile(const RadioProfileConfig& config) {
|
||||
return config.mode != RadioProfileMode::Off
|
||||
&& config.params.freq >= 911.1495f && config.params.freq <= 911.1505f
|
||||
&& (config.params.bw == 250.0f || config.params.bw == 125.0f)
|
||||
&& config.params.sf == 7 && config.params.cr == 5;
|
||||
}
|
||||
}
|
||||
|
||||
void RadioProfileCLI::setMediaMeshScopeHandler(MediaMeshScopeHandler handler, void* context) {
|
||||
media_scope_handler_ = handler;
|
||||
media_scope_context_ = context;
|
||||
media_scope_pending_ = false;
|
||||
updateMediaMeshScope();
|
||||
}
|
||||
|
||||
bool RadioProfileCLI::mediaMeshScopeReady(const RadioProfileConfig& config) const {
|
||||
return !media_scope_handler_ || !isMediaMeshProfile(config)
|
||||
|| media_scope_handler_(media_scope_context_, true, false);
|
||||
}
|
||||
|
||||
void RadioProfileCLI::updateMediaMeshScope() {
|
||||
// A held/unreadable radio store is unknown intent, not a saved Off choice.
|
||||
if (!media_scope_handler_ || hold_) return;
|
||||
if (isMediaMeshProfile(saved_)) media_scope_enabled_ = true;
|
||||
else if (saved_.mode == RadioProfileMode::Off) media_scope_enabled_ = false;
|
||||
else return; // Custom MediaMesh tuning retains the saved region permission.
|
||||
media_scope_pending_ = true;
|
||||
media_scope_retry_ms_ = 0;
|
||||
retryMediaMeshScope();
|
||||
}
|
||||
|
||||
void RadioProfileCLI::retryMediaMeshScope() {
|
||||
if (!media_scope_pending_ || !media_scope_handler_ || hold_
|
||||
|| (media_scope_retry_ms_ && (int32_t)(millis() - media_scope_retry_ms_) < 0)) return;
|
||||
media_scope_pending_ = !media_scope_handler_(media_scope_context_, media_scope_enabled_, true);
|
||||
media_scope_retry_ms_ = media_scope_pending_ ? millis() + 1000 : 0;
|
||||
}
|
||||
|
||||
RadioProfileCLI::ImageReadResult RadioProfileCLI::readImage(
|
||||
@@ -722,6 +758,7 @@ bool RadioProfileCLI::stageRemoteSchedule(unsigned slot, const Schedule& schedul
|
||||
|
||||
bool RadioProfileCLI::applyReplyMutation() {
|
||||
const auto kind = remote_mutation_;
|
||||
if (kind == RemoteMutation::Saved && !mediaMeshScopeReady(remote_config_)) return false;
|
||||
if ((kind == RemoteMutation::Saved || kind == RemoteMutation::Off)
|
||||
&& !commitSavedImage(ReplyPath)) return false;
|
||||
remote_mutation_ = RemoteMutation::None;
|
||||
@@ -775,6 +812,7 @@ bool RadioProfileCLI::applyReplyMutation() {
|
||||
}
|
||||
// loop() owns publishing, after all completion/failure callbacks have
|
||||
// returned and Dispatcher no longer owns either response packet.
|
||||
if (kind == RemoteMutation::Saved || kind == RemoteMutation::Off) updateMediaMeshScope();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -801,6 +839,7 @@ void RadioProfileCLI::servicePendingTuning() {
|
||||
void RadioProfileCLI::loop() {
|
||||
const uint32_t now = millis();
|
||||
servicePendingTuning();
|
||||
retryMediaMeshScope();
|
||||
if (!radio_ || !radio_->profiles()) return;
|
||||
const uint32_t elapsed = now - last_ms_;
|
||||
last_ms_ = now;
|
||||
@@ -836,13 +875,15 @@ void RadioProfileCLI::loop() {
|
||||
if (s.active && !s.temporary && epoch >= s.start && (!next || s.start < next->start)) next = &s;
|
||||
}
|
||||
if (!next) break;
|
||||
if (!save(next->config, primary_preamble_, cross_, true)) {
|
||||
if (!mediaMeshScopeReady(next->config)
|
||||
|| !save(next->config, primary_preamble_, cross_, true)) {
|
||||
// Keep the requested time and order intact. A failed earlier entry must
|
||||
// not be retried after a later one and overwrite the newer settings.
|
||||
schedule_retry_ms_ = 60000;
|
||||
break;
|
||||
}
|
||||
saved_ = next->config; manual_secondary_ = true; noteSecondaryManualChange(); *next = {};
|
||||
updateMediaMeshScope();
|
||||
}
|
||||
for (auto& s : schedules_) {
|
||||
if (!s.active || !s.temporary || s.started || epoch < s.start || temp_active_ || temp_pending_ || hasReplyMutation()) continue;
|
||||
@@ -893,6 +934,7 @@ void RadioProfileCLI::appendPrimaryChirpWarning(char* reply, size_t capacity, ui
|
||||
void RadioProfileCLI::appendSecondaryWarnings(char* reply, size_t capacity,
|
||||
const RadioProfiles& preview) const {
|
||||
if (!capacity) return;
|
||||
if (media_scope_pending_) appendReply(reply, capacity, "; MediaMesh scope save pending");
|
||||
if (preview.enabled() && RadioProfiles::valid(preview.primary)
|
||||
&& RadioProfiles::valid(preview.secondary.params)) {
|
||||
const uint32_t primary_hz = frequencyHz(preview.primary.freq);
|
||||
@@ -1146,6 +1188,7 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
|
||||
p.recommendationReady() ? " ready" : " pending", (unsigned long)p.switchBudgetUs());
|
||||
appendChirpWarning(reply, capacity, p);
|
||||
}
|
||||
if (media_scope_pending_) appendReply(reply, capacity, "; MediaMesh scope save pending");
|
||||
return true;
|
||||
}
|
||||
if ((base || temporary) && verb == Get && !*args) {
|
||||
@@ -1156,6 +1199,7 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
|
||||
}
|
||||
if (base && saved_.mode == RadioProfileMode::Off) {
|
||||
snprintf(reply, capacity, "> off (%s)", manual_secondary_ ? "manual" : "automatic allowed");
|
||||
if (media_scope_pending_) appendReply(reply, capacity, "; MediaMesh scope save pending");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -1179,9 +1223,11 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
|
||||
}
|
||||
noteSecondaryManualChange();
|
||||
saved_ = {}; manual_secondary_ = false;
|
||||
updateMediaMeshScope();
|
||||
publish_pending_ = true; publish_after_ms_ = millis() + 2000;
|
||||
publish();
|
||||
snprintf(reply, capacity, "OK - automatic radio2 permitted in 2s");
|
||||
if (media_scope_pending_) appendReply(reply, capacity, "; MediaMesh scope save pending");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -1197,11 +1243,15 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
|
||||
return true;
|
||||
}
|
||||
noteSecondaryManualChange();
|
||||
if (base) { saved_ = {}; manual_secondary_ = true; schedule_retry_ms_ = 0; }
|
||||
if (base) {
|
||||
saved_ = {}; manual_secondary_ = true; schedule_retry_ms_ = 0;
|
||||
updateMediaMeshScope();
|
||||
}
|
||||
temp_pending_ = temp_active_ = false; temp_remaining_ms_ = 0;
|
||||
for (auto& s : schedules_) if (base || s.temporary) s = {};
|
||||
publish_pending_ = true; publish_after_ms_ = millis() + 2000;
|
||||
publish(); snprintf(reply, capacity, "OK - %s in 2s", saved_.mode != RadioProfileMode::Off ? "saved radio2 restored" : "single radio");
|
||||
if (media_scope_pending_) appendReply(reply, capacity, "; MediaMesh scope save pending");
|
||||
return true;
|
||||
}
|
||||
if (scheduled && (verb == Get || verb == Delete)) {
|
||||
@@ -1276,6 +1326,9 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
|
||||
if (valid) valid = preview.automaticPreambleFits();
|
||||
if (!valid) { snprintf(reply, capacity, "Error: use %s f,bw,sf,cr,rx|rxtx%s[,preamble|auto]", key,
|
||||
scheduled_temp ? ",start,end (UTC or +minutes)" : scheduled ? ",start (UTC or +minutes)" : temporary ? ",minutes" : ""); return true; }
|
||||
if (base && !mediaMeshScopeReady(config)) {
|
||||
cli::formatError(reply, capacity, "MediaMesh region unavailable or full"); return true;
|
||||
}
|
||||
if (scheduled) {
|
||||
if (!epoch || start <= epoch || (uint64_t)(start-epoch) * 1000 > 0x7fffffffULL
|
||||
|| (scheduled_temp && (end <= start || (uint64_t)(end-epoch) * 1000 > 0x7fffffffULL))) {
|
||||
@@ -1340,6 +1393,7 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
|
||||
noteSecondaryManualChange();
|
||||
saved_ = config;
|
||||
manual_secondary_ = true;
|
||||
updateMediaMeshScope();
|
||||
publish_pending_ = true; publish_after_ms_ = millis() + 2000;
|
||||
publish();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace mesh {
|
||||
// separate versioned image so existing preference offsets stay compatible.
|
||||
class RadioProfileCLI {
|
||||
public:
|
||||
// Infrastructure binds its loaded region map. Check calls cannot mutate it;
|
||||
// commit calls update live permission and report whether it was persisted.
|
||||
using MediaMeshScopeHandler = bool (*)(void* context, bool enable, bool commit);
|
||||
struct Schedule {
|
||||
RadioProfileConfig config;
|
||||
uint32_t start = 0, end = 0;
|
||||
@@ -58,6 +61,13 @@ class RadioProfileCLI {
|
||||
// readable backup. Read/I/O failures and an intact backup remain read-only.
|
||||
bool recoverable_corrupt_store_ = false;
|
||||
bool publish_pending_ = false;
|
||||
MediaMeshScopeHandler media_scope_handler_ = nullptr;
|
||||
void* media_scope_context_ = nullptr;
|
||||
uint32_t media_scope_retry_ms_ = 0;
|
||||
bool media_scope_pending_ = false, media_scope_enabled_ = false;
|
||||
bool mediaMeshScopeReady(const RadioProfileConfig& config) const;
|
||||
void updateMediaMeshScope();
|
||||
void retryMediaMeshScope();
|
||||
#if MESH_RADIO_SWITCH_TUNING
|
||||
RadioSwitchTuning fast_saved_;
|
||||
uint32_t fast_retry_ms_ = 0;
|
||||
@@ -120,6 +130,8 @@ class RadioProfileCLI {
|
||||
static void configureBootTuning(FILESYSTEM*, Radio&) {}
|
||||
#endif
|
||||
void begin(FILESYSTEM* fs, Radio* radio, RTCClock* rtc, bool infrastructure_replies = false);
|
||||
// Bind only after RegionMap::load(), so boot reconciliation is not discarded.
|
||||
void setMediaMeshScopeHandler(MediaMeshScopeHandler handler, void* context);
|
||||
void servicePendingTuning();
|
||||
bool hasPendingTuning() const {
|
||||
#if MESH_RADIO_SWITCH_TUNING
|
||||
|
||||
@@ -188,6 +188,7 @@ bool RegionMap::load(FILESYSTEM* _fs, const char* path) {
|
||||
if (loaded_next == 0xFFFF && loaded_count < MAX_REGION_ENTRIES) success = false;
|
||||
|
||||
if (success) {
|
||||
_store->invalidateCache();
|
||||
memcpy(regions, loaded, loaded_count * sizeof(RegionEntry));
|
||||
if (loaded_count < MAX_REGION_ENTRIES) {
|
||||
memset(®ions[loaded_count], 0, (MAX_REGION_ENTRIES - loaded_count) * sizeof(RegionEntry));
|
||||
|
||||
@@ -19,10 +19,12 @@ class TransportKeyStore {
|
||||
int num_cache;
|
||||
|
||||
void putCache(uint16_t id, const TransportKey& key);
|
||||
void invalidateCache() { num_cache = 0; }
|
||||
|
||||
public:
|
||||
TransportKeyStore() { num_cache = 0; }
|
||||
// Region reloads can assign an existing ID to a different public name.
|
||||
// Discard only derived/cached keys; stored private keys remain untouched.
|
||||
void invalidateCache() { num_cache = 0; }
|
||||
void getAutoKeyFor(uint16_t id, const char* name, TransportKey& dest);
|
||||
int loadKeysFor(uint16_t id, TransportKey keys[], int max_num);
|
||||
bool saveKeysFor(uint16_t id, const TransportKey keys[], int num);
|
||||
|
||||
Reference in new issue
Block a user