diff --git a/docs/cli_command_availability.md b/docs/cli_command_availability.md index 997604175..7f78f545a 100644 --- a/docs/cli_command_availability.md +++ b/docs/cli_command_availability.md @@ -212,6 +212,8 @@ should use command `0x42`. See [Companion radio binary protocol](companion_proto | Bridge | [`get/set bridge.secret`](cli_commands.md#set-the-esp-now-secret) | ESP-NOW is ESP32 only | No | No | No | | Board | [`get bootloader.ver`](cli_commands.md#view-the-bootloader-version-nrf52-only) | nRF52 bootloader metadata | Yes | Yes | Yes | | Board | [`get pwrmgt.support`; `get pwrmgt.source`; `get pwrmgt.bootreason`; `get pwrmgt.bootmv`](nrf52_power_management.md#cli-commands) | Board power-management implementation | Feature | Feature | Feature | +| Board | [`get/set pwrmgt.bootlock`; `get/set pwrmgt.cutoff`](cli_commands.md#configure-nrf52-battery-protection) | nRF52 power-management boards with a text CLI | Feature | Feature | Feature | +| Board | [`get/set battery.profile`; `get/set battery.empty`; `get/set battery.full`; `get/set adc.multiplier`](cli_commands.md#configure-nrf52-battery-protection) | nRF52 power-management boards with a text CLI; relative ADC calibration | Feature | Feature | Feature | | Ethernet | [`eth.status`](cli_commands.md#view-ethernet-connection-status) | Ethernet target | Feature | Feature | No | | Browser OTA | [`start ota [ap]`; `stop ota`](cli_commands.md#start-or-stop-an-over-the-air-ota-firmware-update) | ESP32 browser uploader | No | No | No | | WebConfig | [`start webconfig [ap]`; `stop webconfig`; `get/set webui`](cli_commands.md#browser-configuration-portal-esp32-repeater-and-room-server) | ESP32 WebConfig | No | No | No | diff --git a/docs/cli_commands.md b/docs/cli_commands.md index c656364de..cd66480d0 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1836,7 +1836,13 @@ through binary command `0x42`; this command does not report the Bluetooth PIN. **Default:** `0.0` (value defined by board) -**Note:** Returns "Error: unsupported by this board" if hardware doesn't support it +**Note:** On nRF52 boards with power management and a boot-voltage setting, +this is a saved *relative* calibration: `1.000` keeps the board conversion, +`1.050` raises all reported battery millivolts by 5%, and `0` resets to +`1.000`. The allowed range is `0.500`–`1.500`. This affects boot protection, +running cutoff, telemetry, battery alerts, and percentage. Calibrate against a +voltmeter before enabling a low cutoff. Other boards retain their existing +board-specific multiplier behavior, or return unsupported. --- @@ -4790,6 +4796,70 @@ manufacturer images and the boards still requiring untouched factory dumps. --- +#### Configure nRF52 battery protection +**Usage:** `get pwrmgt.bootlock`, `set pwrmgt.bootlock <2500-4200>` + +**Usage:** `get pwrmgt.cutoff`, `set pwrmgt.cutoff ` + +Values are battery millivolts and are saved in internal storage. The boot lock +defaults to each board's compiled threshold and takes effect on the next boot. +Boards whose compiled boot lock is disabled also accept `set pwrmgt.bootlock off`. +The running cutoff +defaults to `off`; when enabled, three valid low readings at 30-second +intervals shut the node down. Both checks are bypassed while externally +powered. A nonzero cutoff cannot exceed the boot lock, and the boot lock +cannot be disabled while a cutoff is active. A fixed 2000 mV pre-check +protects flash on boards with a nonzero compiled boot lock before the saved +setting can be loaded. In the `custom` profile, the lower limit for the boot +lock and running cutoff is 2000 mV. Firmware warns when any saved voltage is +below 2500 mV; that experimental range may brown out the board or interrupt +flash writes. These commands are available on nRF52 boards with +`NRF52_POWER_MANAGEMENT` and a boot-voltage configuration, in roles with a +text CLI. The KISS modem has no text CLI for changing these settings. + +`battery.profile` selects saved voltage-to-percentage endpoints and protection +settings. `liion` uses 3000 mV = 0%, 4200 mV = 100%, the board's original boot +lock, and running cutoff off. `lifepo4` uses 2700 mV = 0%, 3550 mV = 100%, +2900 mV boot lock, and 2700 mV running cutoff. `custom` starts with the current +values and allows endpoint overrides. These percentages are simple voltage +estimates; they are not an accurate state-of-charge measurement under load. + +```text +get battery.profile +set battery.profile lifepo4 +set battery.profile liion +set battery.profile custom +get battery.empty +set battery.empty 2700 +get battery.full +set battery.full 3550 +get adc.multiplier +set adc.multiplier 1.050 +``` + +The custom empty/full endpoints may be 2000–4200 mV and must be at least 100 mV +apart. Setting either endpoint automatically selects `custom`; boot lock and +cutoff remain separately adjustable. `get pwrmgt.bootlock` and +`get pwrmgt.cutoff` show the values in force after selecting a profile. + +For a RAK3401/RAK13302 LiFePO₄ cell: + +```text +set battery.profile lifepo4 +``` + +The 2.7 V cutoff is a cell-protection choice, not a guarantee of 1 W RF +output. The [RAK13302 datasheet](https://docs.rakwireless.com/product-categories/wisblock/rak13302/datasheet-vd/) +specifies 3.0 V minimum at VBAT and lists 5 V / 1 A peak for 30 dBm output. +Use a cutoff at or above 3.0 V if keeping the radio within its published +battery-input range matters; verify the actual voltage under transmit load. + +The shutdown wake comparator has a separate fixed hardware reference. Changing +these millivolt settings does not change that wake threshold; a unit may need +to charge above it or have USB power restored before it wakes. + +--- + ### Ethernet (when Ethernet support is compiled in) Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) Ethernet module. Use the `_ethernet` firmware variants (e.g. `RAK_4631_repeater_ethernet`) to enable this feature. diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md index a747374f7..8f52edb88 100644 --- a/docs/nrf52_power_management.md +++ b/docs/nrf52_power_management.md @@ -190,6 +190,13 @@ Power management status can be queried via the CLI: | `get pwrmgt.bootreason` | Returns reset and shutdown reason strings | | `get pwrmgt.bootmv` | Returns boot voltage in millivolts | +Boards with nRF52 power management and a text CLI also support saved +`pwrmgt.bootlock`, `pwrmgt.cutoff`, `battery.profile`, `battery.empty`, +`battery.full`, and relative `adc.multiplier` settings. See the +[CLI reference](cli_commands.md#configure-nrf52-battery-protection) for their +ranges, board-specific defaults, and wake behavior. KISS builds honor a saved +policy if one exists, but have no text CLI for changing it. + On boards without power management enabled, `get pwrmgt.source` and `get pwrmgt.bootmv` return: ``` @@ -212,11 +219,10 @@ DEBUG: PWRMGT: Boot voltage = 3450 mV (threshold = 3300 mV) DEBUG: PWRMGT: LPCOMP wake configured (AIN7, ref=3/8 VDD) ``` -## Phase 2 (Planned) +## Further power-management work (Planned) -- Runtime voltage monitoring -- Voltage state machine (Normal -> Warning -> Critical -> Shutdown) -- Configurable thresholds +- A voltage state machine (Normal -> Warning -> Critical -> Shutdown) +- Configurable wake comparator thresholds - Load shedding callbacks for power reduction - Deep sleep integration - Scheduled wake-up diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 66fece1fa..2aa0a92f4 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -23,6 +23,9 @@ #include #include #include +#if defined(NRF52_POWER_MANAGEMENT) +#include +#endif #if defined(NRF52_PLATFORM) #include #endif @@ -9316,6 +9319,11 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, while (*command == ' ' || *command == '\t') command++; } +#if defined(NRF52_POWER_MANAGEMENT) + if (mesh::power::handleVoltagePolicyCommand(command, reply, + reply_capacity)) return true; +#endif + if (_radio_profiles.handle(command, reply, reply_capacity, sender_timestamp != 0)) return true; if (!strncmp(command, "set tempradio ", 14)) command += 4; if (!strcmp(command, "get tempradio")) command += 4; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 4090b40f5..5df4cdf3b 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -197,6 +197,9 @@ static bool isNetworkTerminalActive(); #else #if defined(EXTRAFS) #include "ResilientInternalExtraFS.h" + extern "C" struct lfs_config* meshNrf52VoltagePrimaryFsConfig() { + return ResilientInternalExtraFS::primaryConfig(); + } extern "C" uint32_t __flash_arduino_end[]; ResilientInternalExtraFS ExtraFS(0xD4000, 0x19000, 128); DataStore store(InternalFS, ExtraFS, rtc_clock); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index ec8776f55..ae8ff0fc0 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1,5 +1,8 @@ #include "UITask.h" #include +#if defined(NRF52_POWER_MANAGEMENT) +#include +#endif #include #include #include @@ -484,6 +487,10 @@ class HomeScreen : public UIScreen { int batteryPercentage = showBattery ? ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts) : 0; +#if defined(NRF52_POWER_MANAGEMENT) + batteryPercentage = showBattery + ? mesh::power::configuredBatteryPercent(batteryMilliVolts) : 0; +#endif if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0% if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100% diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index b44c38df2..1cb7fab6c 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -4,6 +4,9 @@ #endif #include #include +#if defined(NRF52_POWER_MANAGEMENT) +#include +#endif #include #include #include @@ -285,6 +288,9 @@ void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) { const int minMilliVolts = BATT_MIN_MILLIVOLTS; const int maxMilliVolts = BATT_MAX_MILLIVOLTS; int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts); +#if defined(NRF52_POWER_MANAGEMENT) + batteryPercentage = mesh::power::configuredBatteryPercent(batteryMilliVolts); +#endif if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0% if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100% @@ -495,7 +501,13 @@ void UITask::userLedHandler() { } if (cur_time > next_batt_check) { // battery reads are not free, keep them rare - low_batt = _board->getBattMilliVolts() < LOW_BATT_MILLIVOLTS; + uint16_t low_batt_mv = LOW_BATT_MILLIVOLTS; +#if defined(NRF52_POWER_MANAGEMENT) + low_batt_mv = mesh::power::remapBatteryWarningMillivolts( + LOW_BATT_MILLIVOLTS, BATT_MIN_MILLIVOLTS, BATT_MAX_MILLIVOLTS, + mesh::power::configuredEmpty(), mesh::power::configuredFull()); +#endif + low_batt = _board->getBattMilliVolts() < low_batt_mv; next_batt_check = cur_time + 60000; } diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 8e1a5ba7e..3a87b3a14 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -7,6 +7,9 @@ #include // std::nothrow (heap-allocated flood rule table) #include // for qsort() #include +#if defined(NRF52_POWER_MANAGEMENT) +#include +#endif #include #include #include @@ -455,6 +458,9 @@ static bool verifyFloodSettingsWrite(FILESYSTEM* fs, const char* filename, } static uint8_t batteryPercentFromMilliVolts(uint16_t batt_mv) { +#if defined(NRF52_POWER_MANAGEMENT) + return mesh::power::configuredBatteryPercent(batt_mv); +#else const int min_mv = BATT_MIN_MILLIVOLTS; const int max_mv = BATT_MAX_MILLIVOLTS; if (max_mv <= min_mv) return 100; @@ -463,6 +469,7 @@ static uint8_t batteryPercentFromMilliVolts(uint16_t batt_mv) { if (pct < 0) return 0; if (pct > 100) return 100; return (uint8_t)pct; +#endif } static bool parseBatteryAlertPercent(const char* value, uint8_t min_value, uint8_t max_value, uint8_t& result) { diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index 7b47b6061..fe498e310 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -21,6 +21,9 @@ #include #include #include +#if defined(NRF52_POWER_MANAGEMENT) + #include +#endif #if defined(ESP32_PLATFORM) #include #endif @@ -464,6 +467,14 @@ public: } void handleCommand(const char* command) { +#if defined(NRF52_POWER_MANAGEMENT) + char voltage_reply[160]; + if (mesh::power::handleVoltagePolicyCommand(command, voltage_reply, + sizeof(voltage_reply))) { + Serial.println(voltage_reply); + return; + } +#endif char profile_reply[160]; if (_radio_profiles.handle(command, profile_reply, sizeof(profile_reply))) { Serial.println(profile_reply); return; @@ -785,6 +796,7 @@ void setup() { void loop() { #if defined(NRF52_PLATFORM) board.feedWatchdog(); + board.loop(); #endif the_mesh.loop(); rtc_clock.tick(); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 711fd90c1..c7712b680 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -11,6 +11,9 @@ #include "AdvertDataHelpers.h" #include "AlertReporter.h" // for alertReporterBannedChannelMatch() #include "sensors/EnvironmentI2CConfig.h" +#if defined(NRF52_POWER_MANAGEMENT) +#include "NRF52VoltagePolicy.h" +#endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #include "AtomicFileWriter.h" #elif defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM) @@ -3325,6 +3328,9 @@ bool CommonCLI::handleSdCardGetCmd(const char* config, char* reply) { void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) { const char* config = &command[4]; +#if defined(NRF52_POWER_MANAGEMENT) + if (mesh::power::handleVoltagePolicyCommand(command, reply, 160)) return; +#endif // Infrastructure roles dispatch here rather than through CommonRadioPrefs. if (strncmp(config, "path.hash.mode", 14) == 0 && (config[14] == 0 || config[14] == ' ' || config[14] == '\t')) { @@ -4631,6 +4637,9 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) { const char* config = &command[4]; +#if defined(NRF52_POWER_MANAGEMENT) + if (mesh::power::handleVoltagePolicyCommand(command, reply, 160)) return; +#endif if (strcmp(config, "password") == 0) { if (sender_timestamp == 0) { snprintf(reply, 160, "> %s", _prefs->password); diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index bdd193bbe..c4051febd 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -1,6 +1,7 @@ #if defined(NRF52_PLATFORM) #include "NRF52Board.h" #include "PowerManagementUtils.h" +#include "NRF52VoltagePolicy.h" #include "Nrf52BootloaderVersion.h" #include "nrf52/SoftDeviceState.h" #include @@ -142,6 +143,38 @@ void NRF52Board::begin() { #endif } +void NRF52Board::loop() { +#ifdef NRF52_POWER_MANAGEMENT + if (!mesh::power::voltagePolicySupported()) return; + static uint32_t last_check = 0; + static uint8_t low_count = 0; + static uint16_t last_threshold = 0; + const uint16_t threshold = mesh::power::configuredCutoff(); + const uint32_t now = millis(); + if (threshold != last_threshold) { + last_threshold = threshold; + low_count = 0; + last_check = now; + } + if (threshold == 0 || now - last_check < 30000UL) return; + last_check = now; + if (isExternalPowered()) { + low_count = 0; + return; + } + low_count = mesh::power::nextLowVoltageCount( + low_count, getBattMilliVolts(), threshold, false); + if (low_count >= 3) initiateShutdown(SHUTDOWN_REASON_LOW_VOLTAGE); +#endif +} + +#ifdef NRF52_POWER_MANAGEMENT +uint16_t NRF52Board::calibrateBatteryMillivolts(uint16_t raw_mv) const { + return mesh::power::calibratedMillivolts( + raw_mv, mesh::power::configuredAdcPermille()); +} +#endif + #if NRF52_WATCHDOG_TIMEOUT_SECONDS > 0 static void reloadWatchdogChannels() { // Ordinarily only RR0 is enabled. If a bootloader left the watchdog running @@ -277,20 +310,26 @@ bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) { } boot_voltage_mv = mesh::power::medianVoltage(samples[0], samples[1], samples[2]); - if (config->voltage_bootlock == 0) return true; // Protection disabled - - // Skip check if externally powered - if (isExternalPowered()) { - MESH_DEBUG_PRINTLN("PWRMGT: Boot check skipped (external power)"); - return true; + const bool external = isExternalPowered(); + // Loading the policy still accesses flash. Keep a hard floor ahead of it + // for boards whose compiled policy enables boot protection. + const uint16_t preflight = config->voltage_bootlock == 0 ? 0 + : mesh::power::MIN_CUSTOM_MV; + if (mesh::power::shouldBootLock(boot_voltage_mv, preflight, external)) { + initiateShutdown(SHUTDOWN_REASON_BOOT_PROTECT); + return false; } - MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage = %u mV (threshold = %u mV)", - boot_voltage_mv, config->voltage_bootlock); + mesh::power::loadVoltagePolicyAtBoot(config->voltage_bootlock); + boot_voltage_mv = calibrateBatteryMillivolts(boot_voltage_mv); + const uint16_t threshold = mesh::power::configuredBootlock(); + if (threshold == 0 || external) return true; - // Only trigger shutdown if reading is valid (>1000mV) AND below threshold - // This prevents spurious shutdowns on ADC glitches or uninitialized reads - if (mesh::power::shouldBootLock(boot_voltage_mv, config->voltage_bootlock, false)) { + MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage = %u mV (threshold = %u mV)", + boot_voltage_mv, threshold); + + // Only valid ADC readings (>1000 mV) can initiate protective shutdown. + if (mesh::power::shouldBootLock(boot_voltage_mv, threshold, false)) { MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage too low - entering protective shutdown"); initiateShutdown(SHUTDOWN_REASON_BOOT_PROTECT); diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 91e2b06c0..92cb3ae3e 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -56,6 +56,7 @@ public: #endif {} virtual void begin(); + void loop() override; // Feed the hardware main-loop watchdog. The first call starts it, so normal // setup (including filesystem mount and radio initialization) is not timed. // A loop blocked forever in SoftDevice-backed flash I/O will stop feeding it @@ -78,6 +79,11 @@ public: bool isUsbDataConnected() override; bool isUsbHostConnected() override; +#ifdef NRF52_POWER_MANAGEMENT + // Relative to each board's built-in ADC conversion; 1.000 is unchanged. + uint16_t calibrateBatteryMillivolts(uint16_t raw_mv) const; +#endif + void attachDynamicPrefs(KeyValueStore* prefs) { (void)prefs; } // no-op #ifdef NRF52_POWER_MANAGEMENT diff --git a/src/helpers/NRF52VoltagePolicy.cpp b/src/helpers/NRF52VoltagePolicy.cpp new file mode 100644 index 000000000..77830c153 --- /dev/null +++ b/src/helpers/NRF52VoltagePolicy.cpp @@ -0,0 +1,294 @@ +#include "NRF52VoltagePolicy.h" + +#if defined(NRF52_PLATFORM) && defined(NRF52_POWER_MANAGEMENT) +#include +#include "IdentityStore.h" +#include "AtomicFileWriter.h" +#include "PersistentStoreFormat.h" +#include +#include + +// A Companion using resilient internal ExtraFS has a custom primary LittleFS +// geometry. Its application supplies that config; ordinary roles use default. +extern "C" struct lfs_config* meshNrf52VoltagePrimaryFsConfig() + __attribute__((weak)); + +namespace mesh { +namespace power { + +static const char* POLICY_PATH = "/pwrmgt_v1"; +enum Profile : uint8_t { LI_ION = 0, LIFEPO4 = 1, CUSTOM = 2 }; +static bool supported = false; +static bool boot_off_allowed = false; +static uint16_t board_bootlock = 0; +static uint16_t saved_bootlock = 0; +static uint16_t saved_cutoff = 0; +static uint16_t empty_mv = 3000; +static uint16_t full_mv = 4200; +static uint16_t adc_permille = 1000; +static Profile profile = LI_ION; + +static bool validPercentRange(uint16_t empty, uint16_t full, Profile kind) { + return empty >= (kind == CUSTOM ? MIN_CUSTOM_MV : MIN_CONFIGURED_MV) + && full <= MAX_CONFIGURED_MV + && full >= empty + 100; +} + +static void encode(uint8_t (&data)[20], uint16_t boot, uint16_t low, + uint16_t empty, uint16_t full, uint16_t adc, Profile kind) { + data[0] = 'N'; data[1] = 'V'; data[2] = 'P'; data[3] = '2'; + mesh::storage::writeLE16(data + 4, boot); + mesh::storage::writeLE16(data + 6, low); + mesh::storage::writeLE16(data + 8, empty); + mesh::storage::writeLE16(data + 10, full); + mesh::storage::writeLE16(data + 12, adc); + data[14] = kind; + data[15] = 0; + mesh::storage::writeLE32(data + 16, + mesh::storage::updateCRC32(0xFFFFFFFFUL, data, 16)); +} + +static void readMountedPolicy() { + File file(InternalFS); + if (!file.open(POLICY_PATH, FILE_O_READ)) return; + uint8_t data[20]; + const size_t length = file.size(); + const bool complete = (length == 12 || length == sizeof(data)) + && file.read(data, length) == length; + file.close(); + if (!complete || memcmp(data, "NVP", 3) != 0) return; + const bool old_format = length == 12 && data[3] == '1'; + const bool new_format = length == 20 && data[3] == '2'; + if ((!old_format && !new_format) + || mesh::storage::readLE32(data + length - 4) + != mesh::storage::updateCRC32(0xFFFFFFFFUL, data, length - 4)) return; + const uint16_t boot = mesh::storage::readLE16(data + 4); + const uint16_t low = mesh::storage::readLE16(data + 6); + const Profile kind = new_format ? static_cast(data[14]) : LI_ION; + if (new_format && (data[14] > CUSTOM || data[15] != 0)) return; + if (!validThresholdPair(boot, low, boot_off_allowed, + kind == CUSTOM ? MIN_CUSTOM_MV : MIN_CONFIGURED_MV)) return; + if (new_format) { + const uint16_t empty = mesh::storage::readLE16(data + 8); + const uint16_t full = mesh::storage::readLE16(data + 10); + const uint16_t adc = mesh::storage::readLE16(data + 12); + if (!validPercentRange(empty, full, kind) || adc < 500 || adc > 1500) return; + empty_mv = empty; + full_mv = full; + adc_permille = adc; + profile = static_cast(data[14]); + } + saved_bootlock = boot; + saved_cutoff = low; +} + +void loadVoltagePolicyAtBoot(uint16_t board_default_mv) { + supported = true; + board_bootlock = board_default_mv; + boot_off_allowed = board_default_mv == 0; + saved_bootlock = board_default_mv; + saved_cutoff = 0; + empty_mv = 3000; + full_mv = 4200; + adc_permille = 1000; + profile = LI_ION; + + struct lfs_config* config = meshNrf52VoltagePrimaryFsConfig + ? meshNrf52VoltagePrimaryFsConfig() : nullptr; + const bool mounted = config == nullptr + ? InternalFS.Adafruit_LittleFS::begin() + : InternalFS.Adafruit_LittleFS::begin(config); + if (mounted) readMountedPolicy(); + // Never call InternalFileSystem::begin() here: it auto-formats on failure. + // The role's ordinary primary mount retains its existing recovery behavior. + InternalFS.end(); +} + +bool voltagePolicySupported() { return supported; } +bool bootlockOffAllowed() { return boot_off_allowed; } +uint16_t configuredBootlock() { return saved_bootlock; } +uint16_t configuredCutoff() { return saved_cutoff; } +uint16_t configuredEmpty() { return empty_mv; } +uint16_t configuredFull() { return full_mv; } +uint16_t configuredAdcPermille() { return adc_permille; } +uint8_t configuredBatteryPercent(uint16_t mv) { + return batteryPercent(mv, empty_mv, full_mv); +} + +static bool save(uint16_t boot, uint16_t low, uint16_t empty, uint16_t full, + uint16_t adc, Profile kind) { + if (!supported || !validThresholdPair(boot, low, boot_off_allowed, + kind == CUSTOM ? MIN_CUSTOM_MV : MIN_CONFIGURED_MV) + || !validPercentRange(empty, full, kind) + || adc < 500 || adc > 1500) return false; + uint8_t data[20]; + encode(data, boot, low, empty, full, adc, kind); + mesh::AtomicFileWriter writer(&InternalFS, POLICY_PATH); + if (!writer || writer.write(data, sizeof(data)) != sizeof(data) + || !writer.commit()) return false; + saved_bootlock = boot; + saved_cutoff = low; + empty_mv = empty; + full_mv = full; + adc_permille = adc; + profile = kind; + return true; +} + +bool setConfiguredBootlock(uint16_t mv) { + return save(mv, saved_cutoff, empty_mv, full_mv, adc_permille, profile); +} +bool setConfiguredCutoff(uint16_t mv) { + return save(saved_bootlock, mv, empty_mv, full_mv, adc_permille, profile); +} + +static bool matchKey(const char* command, const char* key, bool set) { + const char* prefix = set ? "set " : "get "; + if (strncmp(command, prefix, 4) != 0) return false; + command += 4; + const size_t len = strlen(key); + return strncmp(command, key, len) == 0 + && (command[len] == 0 || command[len] == ' ' || command[len] == '\t'); +} + +static const char* const KEYS[] = { + "pwrmgt.bootlock", "pwrmgt.cutoff", "battery.profile", + "battery.empty", "battery.full", "adc.multiplier" +}; + +static int commandKey(const char* command, bool set) { + if (command == nullptr) return -1; + for (unsigned i = 0; i < sizeof(KEYS) / sizeof(KEYS[0]); ++i) { + if (matchKey(command, KEYS[i], set)) return (int)i; + } + return -1; +} + +bool isVoltagePolicyCommand(const char* command) { + return commandKey(command, false) >= 0 || commandKey(command, true) >= 0; +} + +// Exact decimal with up to three fractional places. Zero restores factory scale. +static bool parseAdcPermille(const char* value, uint16_t& out) { + if (*value == 0) return false; + uint32_t whole = 0, fraction = 0; + unsigned digits = 0; + bool dot = false; + for (const char* p = value; *p; ++p) { + if (*p == '.') { + if (dot) return false; + dot = true; + } else if (*p >= '0' && *p <= '9') { + if (!dot) { + whole = whole * 10 + (*p - '0'); + if (whole > 1) return false; + } else { + if (++digits > 3) return false; + fraction = fraction * 10 + (*p - '0'); + } + } else return false; + } + if (dot && digits == 0) return false; + while (digits++ < 3) fraction *= 10; + const uint32_t result = whole * 1000 + fraction; + if (result == 0) { out = 1000; return true; } + if (result < 500 || result > 1500) return false; + out = (uint16_t)result; + return true; +} + +bool handleVoltagePolicyCommand(const char* command, char* reply, + size_t reply_capacity) { + if (reply == nullptr || reply_capacity == 0) return false; + const bool set = command != nullptr && strncmp(command, "set ", 4) == 0; + const int key = commandKey(command, set); + if (key < 0) return false; + if (!supported) { + snprintf(reply, reply_capacity, "Error: voltage policy unsupported"); + return true; + } + const char* value = command + 4 + strlen(KEYS[key]); + while (*value == ' ' || *value == '\t') ++value; + + if (!set) { + if (*value != 0) snprintf(reply, reply_capacity, "Error: get %s", KEYS[key]); + else if (key == 0 || key == 1) { + const uint16_t mv = key == 0 ? saved_bootlock : saved_cutoff; + if (mv == 0) snprintf(reply, reply_capacity, "> off"); + else snprintf(reply, reply_capacity, "> %u mV", (unsigned)mv); + } else if (key == 2) { + snprintf(reply, reply_capacity, "> %s", + profile == LIFEPO4 ? "lifepo4" : profile == CUSTOM ? "custom" : "liion"); + } else if (key == 5) { + snprintf(reply, reply_capacity, "> %u.%03u", + (unsigned)(adc_permille / 1000), (unsigned)(adc_permille % 1000)); + } else { + snprintf(reply, reply_capacity, "> %u mV", + (unsigned)(key == 3 ? empty_mv : full_mv)); + } + return true; + } + + if (key == 2) { + uint16_t boot = saved_bootlock, low = saved_cutoff; + uint16_t empty = empty_mv, full = full_mv; + Profile next = profile; + if (strcmp(value, "lifepo4") == 0) { + next = LIFEPO4; boot = 2900; low = 2700; empty = 2700; full = 3550; + } else if (strcmp(value, "liion") == 0) { + next = LI_ION; boot = board_bootlock; low = 0; empty = 3000; full = 4200; + } else if (strcmp(value, "custom") == 0) next = CUSTOM; + else { + snprintf(reply, reply_capacity, "Error: profile liion|lifepo4|custom"); + return true; + } + if (!save(boot, low, empty, full, adc_permille, next)) + snprintf(reply, reply_capacity, "Error: battery profile could not be saved"); + else snprintf(reply, reply_capacity, "OK - %s profile saved; reboot for bootlock", value); + return true; + } + + if (key == 5) { + uint16_t adc; + if (!parseAdcPermille(value, adc)) + snprintf(reply, reply_capacity, "Error: multiplier 0 or 0.500-1.500"); + else if (!save(saved_bootlock, saved_cutoff, empty_mv, full_mv, adc, profile)) + snprintf(reply, reply_capacity, "Error: multiplier could not be saved"); + else snprintf(reply, reply_capacity, "OK - ADC multiplier %u.%03u saved", + (unsigned)(adc / 1000), (unsigned)(adc % 1000)); + return true; + } + + uint16_t mv = 0; + const uint16_t minimum = (profile == CUSTOM || key == 3 || key == 4) + ? MIN_CUSTOM_MV : MIN_CONFIGURED_MV; + if (!parseVoltageMillivolts(value, mv, + key == 1 || (key == 0 && boot_off_allowed), minimum)) { + snprintf(reply, reply_capacity, "Error: set %s %s%u-4200", + KEYS[key], (key == 1 || (key == 0 && boot_off_allowed)) ? "off|" : "", + (unsigned)minimum); + } else if (key == 0 && saved_cutoff != 0 && (mv == 0 || mv < saved_cutoff)) { + snprintf(reply, reply_capacity, "Error: bootlock must be at least cutoff"); + } else if (key == 1 && mv != 0 && (saved_bootlock == 0 || mv > saved_bootlock)) { + snprintf(reply, reply_capacity, "Error: cutoff must not exceed bootlock"); + } else if ((key == 3 && mv + 100 > full_mv) + || (key == 4 && mv < empty_mv + 100)) { + snprintf(reply, reply_capacity, "Error: full must exceed empty by 100 mV"); + } else { + const bool ok = save(key == 0 ? mv : saved_bootlock, + key == 1 ? mv : saved_cutoff, + key == 3 ? mv : empty_mv, + key == 4 ? mv : full_mv, + adc_permille, key >= 3 ? CUSTOM : profile); + if (!ok) snprintf(reply, reply_capacity, "Error: voltage setting could not be saved"); + else snprintf(reply, reply_capacity, "OK - %s %s%s%s", KEYS[key], value, + key == 0 ? "; reboot required" : " (saved)", + mv != 0 && mv < MIN_CONFIGURED_MV + ? "; WARNING: below 2500 mV risks brownout/flash failure" : ""); + } + return true; +} + +} // namespace power +} // namespace mesh +#endif diff --git a/src/helpers/NRF52VoltagePolicy.h b/src/helpers/NRF52VoltagePolicy.h new file mode 100644 index 000000000..a40472422 --- /dev/null +++ b/src/helpers/NRF52VoltagePolicy.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "NRF52VoltageRules.h" + +#if defined(NRF52_PLATFORM) && defined(NRF52_POWER_MANAGEMENT) +namespace mesh { +namespace power { + +// Called by checkBootVoltage(). Uses a single non-formatting mount to read the +// policy, then releases the FS so the application can mount it normally later. +void loadVoltagePolicyAtBoot(uint16_t board_default_mv); +bool voltagePolicySupported(); +bool bootlockOffAllowed(); +uint16_t configuredBootlock(); +uint16_t configuredCutoff(); +uint16_t configuredEmpty(); +uint16_t configuredFull(); +uint16_t configuredAdcPermille(); +uint8_t configuredBatteryPercent(uint16_t mv); +bool setConfiguredBootlock(uint16_t mv); +bool setConfiguredCutoff(uint16_t mv); +bool isVoltagePolicyCommand(const char* command); +bool handleVoltagePolicyCommand(const char* command, char* reply, + size_t reply_capacity); + +} // namespace power +} // namespace mesh +#endif diff --git a/src/helpers/NRF52VoltageRules.h b/src/helpers/NRF52VoltageRules.h new file mode 100644 index 000000000..58c69ef94 --- /dev/null +++ b/src/helpers/NRF52VoltageRules.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include + +namespace mesh { +namespace power { + +static const uint16_t MIN_CONFIGURED_MV = 2500; +static const uint16_t MIN_CUSTOM_MV = 2000; +static const uint16_t MAX_CONFIGURED_MV = 4200; + +inline bool validThresholdPair(uint16_t boot, uint16_t low, + bool boot_off_allowed, + uint16_t minimum_mv = MIN_CONFIGURED_MV) { + return (boot == 0 ? boot_off_allowed + : boot >= minimum_mv && boot <= MAX_CONFIGURED_MV) + && (low == 0 || (low >= minimum_mv && low <= MAX_CONFIGURED_MV)) + && (low == 0 || (boot != 0 && low <= boot)); +} + +inline bool parseVoltageMillivolts(const char* value, uint16_t& mv, + bool allow_off, + uint16_t minimum_mv = MIN_CONFIGURED_MV) { + if (value == nullptr) return false; + if (allow_off && strcmp(value, "off") == 0) { mv = 0; return true; } + if (*value == 0) return false; + uint32_t number = 0; + for (const char* p = value; *p; ++p) { + if (*p < '0' || *p > '9') return false; + number = number * 10 + (*p - '0'); + if (number > MAX_CONFIGURED_MV) return false; + } + if (number < minimum_mv) return false; + mv = (uint16_t)number; + return true; +} + +inline uint8_t nextLowVoltageCount(uint8_t previous, uint16_t mv, + uint16_t cutoff, bool external_power) { + if (cutoff == 0 || external_power || mv <= 1000 || mv >= cutoff) return 0; + return previous < 3 ? previous + 1 : 3; +} + +inline uint8_t batteryPercent(uint16_t mv, uint16_t empty_mv, + uint16_t full_mv) { + if (full_mv <= empty_mv) return 0; + if (mv <= empty_mv) return 0; + if (mv >= full_mv) return 100; + return (uint8_t)(((uint32_t)(mv - empty_mv) * 100U) + / (full_mv - empty_mv)); +} + +// Keep a legacy voltage warning at the same fraction of the displayed battery +// range when the user selects a different chemistry or custom endpoints. +inline uint16_t remapBatteryWarningMillivolts(uint16_t old_warning, + uint16_t old_empty, + uint16_t old_full, + uint16_t new_empty, + uint16_t new_full) { + if (old_full <= old_empty || new_full <= new_empty) return old_warning; + if (old_warning <= old_empty) return new_empty; + if (old_warning >= old_full) return new_full; + const uint32_t old_span = old_full - old_empty; + const uint32_t new_span = new_full - new_empty; + return new_empty + (uint16_t)(((uint32_t)(old_warning - old_empty) + * new_span + old_span / 2U) / old_span); +} + +inline uint16_t calibratedMillivolts(uint16_t raw_mv, + uint16_t multiplier_permille) { + if (raw_mv == 0) return 0; + const uint32_t value = ((uint32_t)raw_mv * multiplier_permille + 500U) / 1000U; + return value > 65535U ? 65535U : (uint16_t)value; +} + +} // namespace power +} // namespace mesh diff --git a/test/README.md b/test/README.md index fadc45232..fa5dc55e5 100644 --- a/test/README.md +++ b/test/README.md @@ -140,6 +140,7 @@ does not reflect the GoogleTest count -- run the built binary directly | `test_packet_manager` | `src/Packet.cpp`, `src/Dispatcher.cpp`, `src/helpers/StaticPoolPacketManager.cpp` | truncated-packet rejection, unavailable-radio behavior, scoped RX-delay replacement, queue/CAD scheduling, and staged radio/TX recovery | | `test_persistent_store_format` | `src/helpers/PersistentStoreFormat.h` | contact-page headers and CRCs, dirty-page state, stable slot allocation, and bounded resumable legacy migration across power loss | | `test_power_management` | `src/helpers/PowerManagementUtils.h` | median filtering of a brownout outlier and valid-reading requirements for the boot lock | +| `test_nrf52_voltage_rules` | `src/helpers/NRF52VoltageRules.h` | board-default threshold bounds, strict CLI values, and consecutive runtime low-voltage readings | | `test_rx_power_saving` | `src/helpers/radiolib/RXPowerSaving.h` | guarded timer/capture bounds across SF5-SF12, bandwidths, TCXO delays and wire preambles; experimental profiles; retuning and strict CLI parsing | | `test_noise_floor_estimator` | `src/helpers/radiolib/NoiseFloorEstimator.h` | spaced low percentile, captured off-SF traffic, weighting, contamination limits, persistent rises, resets and clock rollover | | `test_region_names` | `src/helpers/RegionNameUtils.h` | canonical public-region markers while preserving distinct private and differently named regions | diff --git a/test/test_nrf52_voltage_rules/test_nrf52_voltage_rules.cpp b/test/test_nrf52_voltage_rules/test_nrf52_voltage_rules.cpp new file mode 100644 index 000000000..19e8f8c19 --- /dev/null +++ b/test/test_nrf52_voltage_rules/test_nrf52_voltage_rules.cpp @@ -0,0 +1,75 @@ +#include + +#include + +TEST(NRF52VoltageRules, KeepsBoardDefaultsAndRejectsUnsafePairs) { + EXPECT_TRUE(mesh::power::validThresholdPair(3300, 0, false)); + EXPECT_TRUE(mesh::power::validThresholdPair(3000, 2700, false)); + EXPECT_TRUE(mesh::power::validThresholdPair(0, 0, true)); + EXPECT_FALSE(mesh::power::validThresholdPair(0, 0, false)); + EXPECT_FALSE(mesh::power::validThresholdPair(2499, 0, false)); + EXPECT_FALSE(mesh::power::validThresholdPair(4201, 0, false)); + EXPECT_FALSE(mesh::power::validThresholdPair(3000, 3100, false)); + EXPECT_FALSE(mesh::power::validThresholdPair(0, 2700, true)); + EXPECT_TRUE(mesh::power::validThresholdPair(2000, 2000, false, + mesh::power::MIN_CUSTOM_MV)); + EXPECT_FALSE(mesh::power::validThresholdPair(1999, 0, false, + mesh::power::MIN_CUSTOM_MV)); +} + +TEST(NRF52VoltageRules, ParsesCliMillivoltsStrictly) { + uint16_t mv = 1234; + EXPECT_TRUE(mesh::power::parseVoltageMillivolts("3000", mv, false)); + EXPECT_EQ(mv, 3000); + EXPECT_FALSE(mesh::power::parseVoltageMillivolts("off", mv, false)); + EXPECT_EQ(mv, 3000); + EXPECT_TRUE(mesh::power::parseVoltageMillivolts("off", mv, true)); + EXPECT_EQ(mv, 0); + EXPECT_FALSE(mesh::power::parseVoltageMillivolts("2499", mv, true)); + EXPECT_FALSE(mesh::power::parseVoltageMillivolts("4201", mv, true)); + EXPECT_FALSE(mesh::power::parseVoltageMillivolts("3000mV", mv, true)); + EXPECT_FALSE(mesh::power::parseVoltageMillivolts("3000 ", mv, true)); + EXPECT_FALSE(mesh::power::parseVoltageMillivolts("", mv, true)); + EXPECT_TRUE(mesh::power::parseVoltageMillivolts("2000", mv, false, + mesh::power::MIN_CUSTOM_MV)); + EXPECT_FALSE(mesh::power::parseVoltageMillivolts("1999", mv, false, + mesh::power::MIN_CUSTOM_MV)); +} + +TEST(NRF52VoltageRules, RequiresThreeConsecutiveValidLowReadings) { + using mesh::power::nextLowVoltageCount; + EXPECT_EQ(nextLowVoltageCount(0, 2690, 2700, false), 1); + EXPECT_EQ(nextLowVoltageCount(1, 2650, 2700, false), 2); + EXPECT_EQ(nextLowVoltageCount(2, 2600, 2700, false), 3); + EXPECT_EQ(nextLowVoltageCount(2, 2700, 2700, false), 0); + EXPECT_EQ(nextLowVoltageCount(2, 999, 2700, false), 0); + EXPECT_EQ(nextLowVoltageCount(2, 2600, 2700, true), 0); + EXPECT_EQ(nextLowVoltageCount(2, 2600, 0, false), 0); +} + +TEST(NRF52VoltageRules, LiFePO4EndpointsAndAdcCalibration) { + using mesh::power::batteryPercent; + using mesh::power::calibratedMillivolts; + EXPECT_EQ(batteryPercent(2700, 2700, 3550), 0); + EXPECT_EQ(batteryPercent(3125, 2700, 3550), 50); + EXPECT_EQ(batteryPercent(3550, 2700, 3550), 100); + EXPECT_EQ(batteryPercent(3700, 2700, 3550), 100); + EXPECT_EQ(calibratedMillivolts(3000, 1000), 3000); + EXPECT_EQ(calibratedMillivolts(3000, 1050), 3150); + EXPECT_EQ(calibratedMillivolts(0, 1500), 0); +} + +TEST(NRF52VoltageRules, RemapsCompanionWarningWithBatteryRange) { + using mesh::power::remapBatteryWarningMillivolts; + EXPECT_EQ(remapBatteryWarningMillivolts(3500, 3000, 4200, + 3000, 4200), 3500); + EXPECT_EQ(remapBatteryWarningMillivolts(3500, 3000, 4200, + 2700, 3550), 3054); + EXPECT_EQ(remapBatteryWarningMillivolts(3500, 3000, 4200, + 2000, 3500), 2625); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h b/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h index 82034c7e7..b18ea8afa 100644 --- a/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h +++ b/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h @@ -26,7 +26,7 @@ public: } raw = raw / BATTERY_SAMPLES; - return (ADC_MULTIPLIER * raw) / 4096; + return calibrateBatteryMillivolts((ADC_MULTIPLIER * raw) / 4096); } const char* getManufacturerName() const override { diff --git a/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h b/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h index a3d738cac..0d2b9f472 100644 --- a/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h +++ b/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h @@ -26,7 +26,7 @@ public: } raw = raw / BATTERY_SAMPLES; - return (ADC_MULTIPLIER * raw) / 4096; + return calibrateBatteryMillivolts((ADC_MULTIPLIER * raw) / 4096); } const char* getManufacturerName() const override { diff --git a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h index f179cdc7f..e694b15f4 100644 --- a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h +++ b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h @@ -26,7 +26,7 @@ public: } raw = raw / BATTERY_SAMPLES; - return (ADC_MULTIPLIER * raw) / 4096; + return calibrateBatteryMillivolts((ADC_MULTIPLIER * raw) / 4096); } const char* getManufacturerName() const override { diff --git a/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.h b/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.h index d8fcb2bb7..31ad7c809 100644 --- a/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.h +++ b/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.h @@ -26,7 +26,7 @@ public: } raw = raw / BATTERY_SAMPLES; - return (ADC_MULTIPLIER * raw) / 4096; + return calibrateBatteryMillivolts((ADC_MULTIPLIER * raw) / 4096); } const char* getManufacturerName() const override { diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 2499dcd13..f46481f88 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -76,7 +76,7 @@ uint16_t T096Board::getBattMilliVolts() { adcvalue = analogRead(PIN_VBAT_READ); digitalWrite(PIN_BAT_CTL, 0); - return (uint16_t)((float)adcvalue * MV_LSB * 4.9); + return calibrateBatteryMillivolts((uint16_t)((float)adcvalue * MV_LSB * 4.9)); } void T096Board::variant_shutdown() { nrf_gpio_cfg_default(PIN_VEXT_EN); diff --git a/variants/heltec_t1/T1Board.cpp b/variants/heltec_t1/T1Board.cpp index a54b8eae4..7cfd21249 100644 --- a/variants/heltec_t1/T1Board.cpp +++ b/variants/heltec_t1/T1Board.cpp @@ -77,7 +77,7 @@ uint16_t T1Board::getBattMilliVolts() { int adcvalue = analogRead(PIN_VBAT_READ); digitalWrite(PIN_BAT_CTL, !ADC_CTRL_ENABLED); - return (uint16_t)((float)adcvalue * MV_LSB * ADC_MULTIPLIER); + return calibrateBatteryMillivolts((uint16_t)((float)adcvalue * MV_LSB * ADC_MULTIPLIER)); } void T1Board::variant_shutdown() { diff --git a/variants/heltec_t114/T114Board.h b/variants/heltec_t114/T114Board.h index 6d7664cbd..e3ea410e0 100644 --- a/variants/heltec_t114/T114Board.h +++ b/variants/heltec_t114/T114Board.h @@ -39,7 +39,7 @@ public: adcvalue = analogRead(PIN_VBAT_READ); digitalWrite(6, 0); - return (uint16_t)((float)adcvalue * MV_LSB * 4.9); + return calibrateBatteryMillivolts((uint16_t)((float)adcvalue * MV_LSB * 4.9)); } const char* getManufacturerName() const override { diff --git a/variants/heltec_tower_v2/HeltecTowerV2Board.cpp b/variants/heltec_tower_v2/HeltecTowerV2Board.cpp index 16a7bd94e..61de14c48 100644 --- a/variants/heltec_tower_v2/HeltecTowerV2Board.cpp +++ b/variants/heltec_tower_v2/HeltecTowerV2Board.cpp @@ -81,7 +81,7 @@ uint16_t HeltecTowerV2Board::getBattMilliVolts() { int adcvalue = analogRead(PIN_VBAT_READ); digitalWrite(PIN_BAT_CTL, LOW); - return (uint16_t)((float)adcvalue * MV_LSB * ADC_MULTIPLIER); + return calibrateBatteryMillivolts((uint16_t)((float)adcvalue * MV_LSB * ADC_MULTIPLIER)); } const char* HeltecTowerV2Board::getManufacturerName() const { diff --git a/variants/muzi_base/MuziBaseBoard.h b/variants/muzi_base/MuziBaseBoard.h index a5a4abbe2..02340ce09 100644 --- a/variants/muzi_base/MuziBaseBoard.h +++ b/variants/muzi_base/MuziBaseBoard.h @@ -39,7 +39,7 @@ public: raw = raw / BATTERY_SAMPLES; // ADC_MULTIPLIER is the voltage divider ratio - return (raw * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; + return calibrateBatteryMillivolts((raw * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096); } const char* getManufacturerName() const override { diff --git a/variants/muziworks_r1_neo/R1NeoBoard.h b/variants/muziworks_r1_neo/R1NeoBoard.h index fa49a1843..ecd75b717 100644 --- a/variants/muziworks_r1_neo/R1NeoBoard.h +++ b/variants/muziworks_r1_neo/R1NeoBoard.h @@ -54,7 +54,7 @@ public: } raw = raw / BATTERY_SAMPLES; - return (ADC_MULTIPLIER * raw) / 4096; + return calibrateBatteryMillivolts((ADC_MULTIPLIER * raw) / 4096); } const char* getManufacturerName() const override { diff --git a/variants/rak3401/RAK3401Board.h b/variants/rak3401/RAK3401Board.h index 940a265f4..9c2be9b5f 100644 --- a/variants/rak3401/RAK3401Board.h +++ b/variants/rak3401/RAK3401Board.h @@ -39,7 +39,7 @@ public: } raw = raw / BATTERY_SAMPLES; - return (ADC_MULTIPLIER * raw) / 4096; + return calibrateBatteryMillivolts((ADC_MULTIPLIER * raw) / 4096); } const char* getManufacturerName() const override { diff --git a/variants/rak4631/RAK4631Board.h b/variants/rak4631/RAK4631Board.h index 7e67165b1..6548f913d 100644 --- a/variants/rak4631/RAK4631Board.h +++ b/variants/rak4631/RAK4631Board.h @@ -29,7 +29,7 @@ public: } raw = raw / BATTERY_SAMPLES; - return (ADC_MULTIPLIER * raw) / 4096; + return calibrateBatteryMillivolts((ADC_MULTIPLIER * raw) / 4096); } const char* getManufacturerName() const override { diff --git a/variants/sensecap_solar/SenseCapSolarBoard.h b/variants/sensecap_solar/SenseCapSolarBoard.h index 8bf65c907..0fc2c5e5c 100644 --- a/variants/sensecap_solar/SenseCapSolarBoard.h +++ b/variants/sensecap_solar/SenseCapSolarBoard.h @@ -30,7 +30,7 @@ public: analogReference(AR_INTERNAL_3_0); delay(10); adcvalue = analogRead(BATTERY_PIN); - return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; + return calibrateBatteryMillivolts((adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096); } const char* getManufacturerName() const override { diff --git a/variants/xiao_nrf52/XiaoNrf52Board.cpp b/variants/xiao_nrf52/XiaoNrf52Board.cpp index 42ee6a87f..96c000606 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.cpp +++ b/variants/xiao_nrf52/XiaoNrf52Board.cpp @@ -68,7 +68,7 @@ uint16_t XiaoNrf52Board::getBattMilliVolts() { // VBAT_ENABLE must be LOW to read battery voltage digitalWrite(VBAT_ENABLE, LOW); int adcvalue = analogRead(PIN_VBAT); - return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; + return calibrateBatteryMillivolts((adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096); } -#endif \ No newline at end of file +#endif