Add configurable nRF52 battery profiles with LiFePO4 cutoff
This commit is contained in:
1 parent
5d19a0a731
commit
2d78b8f06c
31 files changed
+692
-32
No files matched your search
@@ -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 |
|
||||
|
||||
+71
-1
@@ -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 <off|2500-4200>`
|
||||
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
#include <helpers/StatsFormatHelper.h>
|
||||
#include <helpers/UsbAsciiBinarySwitch.h>
|
||||
#include <helpers/UsbLogging.h>
|
||||
#if defined(NRF52_POWER_MANAGEMENT)
|
||||
#include <helpers/NRF52VoltagePolicy.h>
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <helpers/nrf52/RamFallbackFileSystem.h>
|
||||
#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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "UITask.h"
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#if defined(NRF52_POWER_MANAGEMENT)
|
||||
#include <helpers/NRF52VoltagePolicy.h>
|
||||
#endif
|
||||
#include <helpers/ui/BluetoothPairingUiPolicy.h>
|
||||
#include <helpers/ui/CompanionHomeLayout.h>
|
||||
#include <helpers/ui/CompanionMessageHistory.h>
|
||||
@@ -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%
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#endif
|
||||
#include <Arduino.h>
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#if defined(NRF52_POWER_MANAGEMENT)
|
||||
#include <helpers/NRF52VoltagePolicy.h>
|
||||
#endif
|
||||
#include <helpers/ui/BluetoothPairingUiPolicy.h>
|
||||
#include <helpers/ui/RadioProfileDisplayPage.h>
|
||||
#include <helpers/ui/RadioProfileSystemStatus.h>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
#include <new> // std::nothrow (heap-allocated flood rule table)
|
||||
#include <stdlib.h> // for qsort()
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#if defined(NRF52_POWER_MANAGEMENT)
|
||||
#include <helpers/NRF52VoltagePolicy.h>
|
||||
#endif
|
||||
#include <helpers/ClientACLCLI.h>
|
||||
#include <helpers/ClockSyncUtils.h>
|
||||
#include <helpers/ClientLoginPersistence.h>
|
||||
@@ -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) {
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
#include <helpers/StaticPoolPacketManager.h>
|
||||
#include <helpers/SimpleMeshTables.h>
|
||||
#include <helpers/IdentityStore.h>
|
||||
#if defined(NRF52_POWER_MANAGEMENT)
|
||||
#include <helpers/NRF52VoltagePolicy.h>
|
||||
#endif
|
||||
#if defined(ESP32_PLATFORM)
|
||||
#include <helpers/ESP32TrueRandom.h>
|
||||
#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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
+50
-11
@@ -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 <target.h>
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
#include "NRF52VoltagePolicy.h"
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(NRF52_POWER_MANAGEMENT)
|
||||
#include <InternalFileSystem.h>
|
||||
#include "IdentityStore.h"
|
||||
#include "AtomicFileWriter.h"
|
||||
#include "PersistentStoreFormat.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
// 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<Profile>(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<Profile>(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
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
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
|
||||
@@ -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 |
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <helpers/NRF52VoltageRules.h>
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
#endif
|
||||
Reference in new issue
Block a user