mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-11-04 12:07:42 +00:00
Allow Auto, SSD1306 or SH1106 through display.controller on display-enabled RAK4631 and RAK3401 roles. Persist the choice before restart without changing the active screen policy, and let saved Auto override legacy build defaults. Share checked settings I/O and the existing atomic writer to keep Full Companion within its flash boundary while retaining features and existing record formats. Cover save failures, backup recovery, startup selection and ambiguous OLED replies. Validation: 35 targeted host tests passed. RAK4631 Full Companion and RAK3401 unified LoRa OTA repeater build and package checks passed with release-length version strings.
322 lines
13 KiB
C++
322 lines
13 KiB
C++
#include "RegulatorSettings.h"
|
|
#include "RegulatorPlatform.h"
|
|
#include "NRF52VoltageRules.h"
|
|
#include "NRF52VoltagePolicy.h"
|
|
#include "PersistentStoreFormat.h"
|
|
#include "CheckedSettingsFile.h"
|
|
#if defined(NRF52_PLATFORM)
|
|
#include "AtomicFileWriter.h"
|
|
#endif
|
|
#include <Arduino.h>
|
|
#include <Dispatcher.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
// Current STM32WL boards expose neither MCU nor radio regulator selection.
|
|
// Their radio and MCU share a supply, so adding that support requires a
|
|
// coordinated implementation rather than the independent controls used by
|
|
// nRF52 plus an external radio. Do not retain the unreachable persistence and
|
|
// recovery machinery on these flash-constrained targets. Match this exact
|
|
// driver: an STM32 target with a different radio keeps the ordinary path.
|
|
#if MESH_REGULATOR_FIXED_SUPPLY
|
|
namespace mesh { namespace power {
|
|
bool beginRegulatorSettings(MainBoard&, Radio&, FILESYSTEM*) { return true; }
|
|
void serviceRegulatorSettings() {}
|
|
const char* regulatorModeName(RegulatorMode mode) {
|
|
return mode == RegulatorMode::Dcdc ? "dcdc" : mode == RegulatorMode::Ldo ? "ldo" : "auto";
|
|
}
|
|
uint8_t configuredRegulatorModes(bool) { return 0; }
|
|
RegulatorMode requestedRegulatorMode(bool) { return RegulatorMode::Default; }
|
|
RegulatorMode activeRegulatorMode(bool) { return RegulatorMode::Default; }
|
|
const char* activeRegulatorModeName(bool) { return "unknown"; }
|
|
bool regulatorFallbackActive(bool) { return false; }
|
|
bool regulatorRecoveryAvailable() { return false; }
|
|
// No regulator command exists on this driver. Let the ordinary CLI dispatch
|
|
// report its existing unknown-command/setting error, avoiding a second parser
|
|
// and error formatter solely for a capability that these targets lack.
|
|
bool handleRegulatorCommand(const char*, char*, size_t) { return false; }
|
|
void markRegulatorCleanReset() {}
|
|
void resumeRegulatorRecovery() {}
|
|
}} // namespace mesh::power
|
|
#else
|
|
#include "MorseLoopPolicy.h"
|
|
namespace mesh { namespace power {
|
|
namespace {
|
|
MainBoard* main_board = nullptr;
|
|
Radio* main_radio = nullptr;
|
|
FILESYSTEM* settings_fs = nullptr;
|
|
RegulatorMode requested[2] = {RegulatorMode::Default, RegulatorMode::Default};
|
|
uint8_t saved_fallback = 0;
|
|
bool initialized = false;
|
|
bool fallback_saved = true;
|
|
uint32_t last_service = 0, last_battery = 0, last_save = 0;
|
|
RecoveryPolicy& recovery() {
|
|
static RecoveryPolicy policy(platformRecoveryRecord());
|
|
return policy;
|
|
}
|
|
const char* const path = "/regulator_v1";
|
|
const char* const temporary = "/regulator_v1.tmp";
|
|
const char* const backup = "/regulator_v1.bak";
|
|
|
|
void encode(RegulatorMode mcu, RegulatorMode radio, uint8_t fallback, uint8_t (&data)[12]) {
|
|
memcpy(data, "REG1", 4);
|
|
data[4] = uint8_t(mcu); data[5] = uint8_t(radio);
|
|
data[6] = fallback; data[7] = 0;
|
|
storage::writeLE32(data + 8, storage::updateCRC32(0xffffffffUL, data, 8));
|
|
}
|
|
bool readFile(const char* name, uint8_t (&data)[12]) {
|
|
const bool complete = storage::readFixedSettingsFile(settings_fs, name, data, sizeof(data));
|
|
return complete && memcmp(data, "REG1", 4) == 0
|
|
&& data[4] <= 2 && data[5] <= 2 && data[6] <= RECOVERY_DOMAINS
|
|
&& data[7] == 0 && storage::readLE32(data + 8)
|
|
== storage::updateCRC32(0xffffffffUL, data, 8);
|
|
}
|
|
bool save(RegulatorMode mcu, RegulatorMode radio, uint8_t fallback) {
|
|
if (!settings_fs) return false;
|
|
#if defined(NRF52_PLATFORM)
|
|
uint8_t data[12];
|
|
if (uint8_t(mcu) > 2 || uint8_t(radio) > 2 || fallback > RECOVERY_DOMAINS) return false;
|
|
encode(mcu, radio, fallback, data);
|
|
// The shared LittleFS writer already stages, flushes, checks size/CRC32C,
|
|
// atomically publishes and verifies the public file. Reuse it rather than
|
|
// linking a second replacement transaction into flash-constrained targets.
|
|
if (storage::writeCheckedSettingsFile(settings_fs, path, data, sizeof(data))
|
|
!= mesh::AtomicFileWriter::Outcome::Committed) return false;
|
|
// The checked writer compares the complete encoded record after commit.
|
|
// Retire legacy backups only after this verified publication.
|
|
settings_fs->remove(backup);
|
|
saved_fallback = fallback;
|
|
return true;
|
|
#else
|
|
settings_fs->remove(temporary);
|
|
if (settings_fs->exists(temporary)) return false;
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
File file(*settings_fs);
|
|
if (!file.open(temporary, FILE_O_WRITE)) return false;
|
|
#elif defined(RP2040_PLATFORM)
|
|
File file = settings_fs->open(temporary, "w");
|
|
if (!file) return false;
|
|
#else
|
|
File file = settings_fs->open(temporary, "w", true);
|
|
if (!file) return false;
|
|
#endif
|
|
uint8_t data[12], checked[12];
|
|
encode(mcu, radio, fallback, data);
|
|
const bool written = file.write(data, sizeof(data)) == sizeof(data);
|
|
file.close();
|
|
if (!written || !readFile(temporary, checked)
|
|
|| memcmp(data, checked, sizeof(data)) != 0) return false;
|
|
if (settings_fs->exists(backup) && !settings_fs->remove(backup)) return false;
|
|
if (settings_fs->exists(path) && !settings_fs->rename(path, backup)) return false;
|
|
if (!settings_fs->rename(temporary, path)) {
|
|
if (settings_fs->exists(backup)) settings_fs->rename(backup, path);
|
|
return false;
|
|
}
|
|
settings_fs->remove(backup);
|
|
saved_fallback = fallback;
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
uint8_t readBatteryPercent(bool& valid) {
|
|
uint16_t samples[3];
|
|
for (unsigned i = 0; i < 3; ++i) {
|
|
samples[i] = main_board->getBattMilliVolts();
|
|
if (i != 2) delay(5);
|
|
}
|
|
if (samples[0] > samples[1]) { auto v = samples[0]; samples[0] = samples[1]; samples[1] = v; }
|
|
if (samples[1] > samples[2]) { auto v = samples[1]; samples[1] = samples[2]; samples[2] = v; }
|
|
if (samples[0] > samples[1]) samples[1] = samples[0];
|
|
const uint16_t mv = samples[1];
|
|
valid = mv >= 2000 && mv <= 5500;
|
|
if (!valid) return 0;
|
|
#if defined(NRF52_PLATFORM) && defined(NRF52_POWER_MANAGEMENT)
|
|
// Board ADC methods already return calibrated millivolts.
|
|
return configuredBatteryPercent(mv);
|
|
#else
|
|
return batteryPercent(mv, 3000, 4200);
|
|
#endif
|
|
}
|
|
RegulatorMode effective(bool mcu) {
|
|
RegulatorMode mode = requested[mcu ? 0 : 1];
|
|
if (mode == RegulatorMode::Default) mode = mcu
|
|
? main_board->defaultMcuRegulatorMode() : main_radio->defaultRegulatorMode();
|
|
const uint8_t bit = mcu ? RECOVERY_MCU : RECOVERY_RADIO;
|
|
if (mode == RegulatorMode::Dcdc && (recovery().fallbackMask() & bit)) mode = RegulatorMode::Ldo;
|
|
return mode;
|
|
}
|
|
}
|
|
|
|
const char* regulatorModeName(RegulatorMode mode) {
|
|
return mode == RegulatorMode::Dcdc ? "dcdc" : mode == RegulatorMode::Ldo ? "ldo" : "auto";
|
|
}
|
|
uint8_t configuredRegulatorModes(bool mcu) {
|
|
if (!main_board || !main_radio) return 0;
|
|
return mcu ? main_board->mcuRegulatorModes() : main_radio->regulatorModes();
|
|
}
|
|
RegulatorMode requestedRegulatorMode(bool mcu) { return requested[mcu ? 0 : 1]; }
|
|
RegulatorMode activeRegulatorMode(bool mcu) {
|
|
if (!main_board || !main_radio) return RegulatorMode::Default;
|
|
return mcu ? main_board->activeMcuRegulatorMode() : main_radio->activeRegulatorMode();
|
|
}
|
|
const char* activeRegulatorModeName(bool mcu) {
|
|
const auto mode = activeRegulatorMode(mcu);
|
|
return mode == RegulatorMode::Default ? "unknown" : regulatorModeName(mode);
|
|
}
|
|
bool regulatorFallbackActive(bool mcu) {
|
|
return initialized && (recovery().fallbackMask() & (mcu ? RECOVERY_MCU : RECOVERY_RADIO));
|
|
}
|
|
bool regulatorRecoveryAvailable() { return initialized && platformRecoverySupported(); }
|
|
|
|
bool beginRegulatorSettings(MainBoard& board, Radio& radio, FILESYSTEM* fs) {
|
|
if (initialized) return true;
|
|
main_board = &board; main_radio = &radio; settings_fs = fs;
|
|
if (!board.mcuRegulatorModes() && !radio.regulatorModes()) return true;
|
|
uint8_t data[12];
|
|
bool loaded = readFile(path, data);
|
|
if (!loaded && readFile(backup, data)) {
|
|
loaded = true;
|
|
settings_fs->remove(path);
|
|
settings_fs->rename(backup, path);
|
|
}
|
|
if (loaded) {
|
|
requested[0] = RegulatorMode(data[4]); requested[1] = RegulatorMode(data[5]);
|
|
saved_fallback = data[6];
|
|
}
|
|
recovery().begin(platformRecoverySupported() ? platformResetKind() : ResetKind::Unknown,
|
|
saved_fallback);
|
|
initialized = true;
|
|
uint8_t active_dcdc = 0;
|
|
bool valid = false;
|
|
const uint8_t percent = readBatteryPercent(valid);
|
|
const uint32_t now = millis();
|
|
const auto cpu_mode = effective(true), radio_mode = effective(false);
|
|
if (configuredRegulatorModes(true) && cpu_mode == RegulatorMode::Dcdc) active_dcdc |= RECOVERY_MCU;
|
|
if (configuredRegulatorModes(false) && radio_mode == RegulatorMode::Dcdc) active_dcdc |= RECOVERY_RADIO;
|
|
recovery().arm(active_dcdc, now, percent, valid);
|
|
fallback_saved = recovery().fallbackMask() == saved_fallback
|
|
|| save(requested[0], requested[1], recovery().fallbackMask());
|
|
bool okay = true;
|
|
if (configuredRegulatorModes(true)) {
|
|
if (!board.applyMcuRegulator(cpu_mode)) {
|
|
active_dcdc &= ~RECOVERY_MCU;
|
|
recovery().arm(active_dcdc, now, percent, valid);
|
|
okay = false;
|
|
}
|
|
}
|
|
if (configuredRegulatorModes(false) && !radio.configureBootRegulator(radio_mode)) {
|
|
active_dcdc &= ~RECOVERY_RADIO;
|
|
recovery().updateActiveDomains(active_dcdc);
|
|
okay = false;
|
|
}
|
|
last_service = last_battery = last_save = now;
|
|
return okay;
|
|
}
|
|
|
|
void serviceRegulatorSettings() {
|
|
if (!initialized) return;
|
|
const uint32_t now = millis();
|
|
if (uint32_t(now - last_service) < 1000) return;
|
|
last_service = now;
|
|
const uint8_t active_dcdc =
|
|
(activeRegulatorMode(true) == RegulatorMode::Dcdc ? RECOVERY_MCU : 0)
|
|
| (activeRegulatorMode(false) == RegulatorMode::Dcdc ? RECOVERY_RADIO : 0);
|
|
recovery().updateActiveDomains(active_dcdc);
|
|
// ADC sampling deliberately spaces three readings, and persistence can
|
|
// block on flash. Keep both out of keyed Morse TX/RX timing; heartbeats
|
|
// remain live without refreshing a deferred sample or retry deadline.
|
|
const bool defer_io = mesh::morseTimingActive(main_radio);
|
|
if (!defer_io && uint32_t(now - last_battery) >= 30000) {
|
|
bool valid = false;
|
|
const uint8_t percent = readBatteryPercent(valid);
|
|
last_battery = millis();
|
|
recovery().updateBattery(last_battery, percent, valid);
|
|
}
|
|
recovery().heartbeat(millis());
|
|
if (!defer_io && !fallback_saved && uint32_t(now - last_save) >= 30000) {
|
|
last_save = now;
|
|
fallback_saved = save(requested[0], requested[1], recovery().fallbackMask());
|
|
}
|
|
}
|
|
|
|
bool handleRegulatorCommand(const char* command, char* reply, size_t capacity) {
|
|
const bool get = strncmp(command, "get ", 4) == 0;
|
|
const bool set = strncmp(command, "set ", 4) == 0;
|
|
if (!get && !set) return false;
|
|
const char* key = command + 4;
|
|
bool mcu;
|
|
size_t length;
|
|
if (strncmp(key, "mcu.regulator", 13) == 0) { mcu = true; length = 13; }
|
|
else if (strncmp(key, "radio.regulator", 15) == 0) { mcu = false; length = 15; }
|
|
else return false;
|
|
if (key[length] != 0 && key[length] != ' ') return false;
|
|
if (!initialized || !configuredRegulatorModes(mcu)) {
|
|
snprintf(reply, capacity, "Error: %s regulator selection unsupported", mcu ? "MCU" : "radio");
|
|
return true;
|
|
}
|
|
const char* value = key + length;
|
|
if (get) {
|
|
if (*value) snprintf(reply, capacity, "Error: unexpected argument");
|
|
else snprintf(reply, capacity, "> %s; active %s; fallback %s; crashes %u; recovery %s%s",
|
|
regulatorModeName(requestedRegulatorMode(mcu)), activeRegulatorModeName(mcu),
|
|
regulatorFallbackActive(mcu) ? "ldo" : "off",
|
|
unsigned(recovery().crashCount(mcu ? RECOVERY_MCU : RECOVERY_RADIO)),
|
|
regulatorRecoveryAvailable() ? "on" : "unsupported",
|
|
fallback_saved ? "" : "; fallback not saved");
|
|
return true;
|
|
}
|
|
while (*value == ' ') ++value;
|
|
RegulatorMode mode;
|
|
if (strcmp(value, "auto") == 0) mode = RegulatorMode::Default;
|
|
else if (strcmp(value, "dcdc") == 0) mode = RegulatorMode::Dcdc;
|
|
else if (strcmp(value, "ldo") == 0) mode = RegulatorMode::Ldo;
|
|
else { snprintf(reply, capacity, "Error: use set %s.regulator auto|dcdc|ldo", mcu ? "mcu" : "radio"); return true; }
|
|
if (!regulatorModeSupported(configuredRegulatorModes(mcu), mode)) {
|
|
snprintf(reply, capacity, "Error: regulator mode unsupported by this hardware");
|
|
return true;
|
|
}
|
|
const uint8_t domain = mcu ? RECOVERY_MCU : RECOVERY_RADIO;
|
|
const uint8_t fallback = recovery().fallbackMask() & ~domain;
|
|
const auto cpu = mcu ? mode : requested[0], rf = mcu ? requested[1] : mode;
|
|
if (!save(cpu, rf, fallback)) {
|
|
snprintf(reply, capacity, "Error: regulator setting save failed");
|
|
return true;
|
|
}
|
|
requested[mcu ? 0 : 1] = mode;
|
|
recovery().clearDomain(domain);
|
|
fallback_saved = true;
|
|
snprintf(reply, capacity, "OK - %s regulator %s saved; reboot required", mcu ? "MCU" : "radio", regulatorModeName(mode));
|
|
return true;
|
|
}
|
|
|
|
void markRegulatorCleanReset() {
|
|
// Early resets still initialize the retained record, using the same policy
|
|
// object as normal startup instead of constructing another on the stack.
|
|
if (!initialized) recovery().begin(ResetKind::Intentional, 0);
|
|
recovery().markCleanReset();
|
|
}
|
|
|
|
void resumeRegulatorRecovery() {
|
|
if (!initialized) return;
|
|
bool valid = false;
|
|
const uint8_t percent = readBatteryPercent(valid);
|
|
const uint32_t now = millis();
|
|
const uint8_t active_dcdc =
|
|
(activeRegulatorMode(true) == RegulatorMode::Dcdc ? RECOVERY_MCU : 0)
|
|
| (activeRegulatorMode(false) == RegulatorMode::Dcdc ? RECOVERY_RADIO : 0);
|
|
recovery().arm(active_dcdc, now, percent, valid);
|
|
last_service = last_battery = now;
|
|
}
|
|
|
|
}} // namespace mesh::power
|
|
#endif
|
|
|
|
extern "C" void meshRegulatorCleanReset() {
|
|
// Intentional resets can happen before the settings/filesystem is ready.
|
|
mesh::power::markRegulatorCleanReset();
|
|
}
|
|
|
|
extern "C" void meshRegulatorResumeRecovery() {
|
|
mesh::power::resumeRegulatorRecovery();
|
|
}
|