diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 3125f3452..61743feeb 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -475,6 +475,8 @@ jobs: python3 -B test/test_regulator_recovery.py -v python3 -B test/test_regulator_settings.py -v python3 -B test/test_battery_forwarding.py -v + python3 -B test/test_battery_calibration.py -v + python3 -B test/test_battery_calibration_store.py -v python3 -B test/test_power_saving_diagnostics.py -v python3 -B test/test_power_saving_diagnostics_buckets.py -v python3 -B test/test_power_saving_role_diagnostics.py -v diff --git a/docs/cli_commands.md b/docs/cli_commands.md index b1f120221..b6539496c 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -2526,6 +2526,49 @@ sessions. Setting the password to `off` makes it the literal password `off`. --- +#### Calibrate the battery reading against a meter + +**Availability:** Repeater, Room Server, Sensor, Companion and Terminal Chat +firmware on boards with a supported battery ADC. Use an administrative CLI +connection; infrastructure roles also accept authenticated remote commands. + +```text +battery calibrate 4.18 +get adc.multiplier +battery calibrate reset +``` + +Measure the battery voltage with a voltmeter and enter that reading in volts. +The firmware calculates `new multiplier = current multiplier * meter voltage / +reported voltage`, applies it and saves it for restart. A reference is required; +the command does not assume a chemistry or a 4.2 V full battery. Enter the voltage +of the battery input the board actually measures, including pack voltage where +that input supports a pack. For example, `battery calibrate 13.2` supplies a +13.200 V reference; it does not change the board's measurement range. + +Calibration supports the ESP32, nRF52, RP2040 and STM32 boards with implemented +battery-voltage ADC conversions, including the Wio Tracker L2's external ADS1115. +An unsupported board or unavailable/saturated reading returns an error. Digital +PMU/fuel-gauge and battery-module readings retain their hardware scaling; boards +without a battery-voltage input cannot be calibrated through this command. + +The meter reference must be within 50% of the reported voltage. Newly supported +ADCs accept a relative multiplier from `0.500` to `1.500`, where `1.000` retains +their original conversion. Existing board-specific absolute multipliers and +saved values keep their established meaning. The five legacy absolute drivers +are Heltec V4, V4 R8, RC32, Meshnology W12 and ProMicro. + +`battery calibrate reset` restores and saves the board's default conversion. +`set adc.multiplier 0` also resets it. A successful numeric calibration reply shows the effective +multiplier, including any rounding by the board. A failed save reports an error +and attempts to restore the previous runtime setting. + +Calibration corrects voltage reporting and the battery percentages/protection +that use that reading. On nRF52 boards with voltage policy it also applies to +the boot reading and running cutoff. It does not adjust charger hardware or +create a current measurement. This is a single-point scale correction, not an +ADC offset or nonlinear calibration. + #### Fine-tune the battery reading **Usage:** - `get adc.multiplier` @@ -2536,13 +2579,16 @@ sessions. Setting the password to `off` makes it the literal password `off`. **Default:** `0.0` (value defined by board) -**Note:** On nRF52 boards with power management and a boot-voltage setting, +**Note:** On newly supported ADC boards this is a relative `0.500`-`1.500` +correction, with `0` restoring the default. Existing absolute-multiplier boards +retain their coefficient units and accept `0` or a positive value up to `10`. +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. +voltmeter before enabling a low cutoff. Boards without a supported ADC return +unsupported. --- diff --git a/docs/test-results/battery_calibration_2026-10-22.md b/docs/test-results/battery_calibration_2026-10-22.md new file mode 100644 index 000000000..5d606d103 --- /dev/null +++ b/docs/test-results/battery_calibration_2026-10-22.md @@ -0,0 +1,119 @@ + + + + +# Shared battery ADC calibration validation - 2026-10-22 + +Final integration baseline: `94a3b7e568781533510721670af24c162804a5ca` on +`keymindCascade`, plus the calibration changes. Earlier builds and link +failures below used `af106a0ee9edd8ee0113b7b364290e0944245b86`; the final +integration section supersedes those results for the targets it names. +These checks do not constitute an installed-device or voltmeter acceptance test. + +## Behavior and compatibility + +- `battery calibrate ` calculates the correction from the + effective existing multiplier and the reported battery voltage. +- A reference is mandatory, with millivolt precision and no assumed battery + chemistry or full voltage. A single correction must lie within 50% of the + reported reading. Zero and saturated readings are rejected. +- `battery calibrate reset` saves the board's default conversion. +- Real ADC backends across ESP32, nRF52, RP2040 and STM32 opt into calibration. + The Wio Tracker L2's external ADS1115 is included. Boards requiring an external + divider still require that hardware; software does not add an input. +- Existing absolute coefficients for Heltec V4, V4 R8, RC32, Meshnology W12 and + ProMicro retain their units and saved values. Other supported ADCs use a + relative correction of 0.500-1.500, with zero restoring their factory scale. +- The nRF52 boot-voltage policy remains authoritative where available, and is + applied once. Without that policy, ADC commands use the shared correction. +- Infrastructure roles restore their existing calibration preference centrally, + including Repeater. Companion and Terminal use a checked, versioned + `/battery_cal` record without changing their existing preference layouts. +- Failed saves attempt to restore the runtime multiplier. An uncertain + publication returns an error rather than claiming persistence. Volatile + recovery storage is not used for saving calibration. +- PMU/fuel-gauge readings, UART battery-module readings, raw-count test + placeholders and boards with no battery-voltage measurement remain excluded. + +## Host checks + +All following checks passed using `TMPDIR=/var/tmp` (the machine's `/tmp` user +quota prevented compiler temporary-file creation): + +| Check | Coverage | +| --- | --- | +| `test/test_battery_calibration.py` - 3 tests | Production parsing, meter correction, existing absolute coefficients, pack references, effective multiplier, reset, unsupported/zero/saturated readings, bounds, actual MainBoard scaling and nRF52 policy precedence. | +| `test/test_battery_calibration_store.py` - 1 test across four platform fixtures | ESP32, RP2040, nRF52 and STM32 transaction implementations; reboot restore, every single-bit record corruption, truncation, write failure, uncertain publication, reset, missing persistent storage and backup recovery. | +| `test/test_battery_forwarding.py` - 19 tests | Actual infrastructure setter, failed-save rollback, nRF52 policy fallback, and refreshed battery-based forwarding decisions. | +| `test/test_heltec_r8_external_power.py` - 1 test | Existing R8 power estimate still uses the calibrated voltage. | +| `test/test_stm32_companion_profile.py` - 21 tests | Existing ADC conversion and sampling remain exact at default calibration; memory/profile constraints retained. | +| `test/test_rp2040_application_capabilities.py` - 11 tests | Existing upload artifact and capability validation. | + +The production calibration, store and forwarding harnesses use AddressSanitizer +and UndefinedBehaviorSanitizer. `git diff --check` also passed. + +## Earlier firmware builds + +Before integration, PlatformIO processes were run serially. Successful targets: + +- `Heltec_v3_repeater` +- `Heltec_v3_terminal_chat` +- `Heltec_v3_companion_radio_usb` +- `RAK_3401_repeater` +- `RAK_3401_room_server` +- `RAK_3401_sensor` +- `RAK_3401_companion_radio_usb` +- `Xiao_rp2040_companion_radio_usb` +- `wio-e5-mini_companion_radio_usb` + +That earlier Mini Companion occupied 228,832 of 229,376 application bytes: +544 bytes remained. This validates the current build recipe, not every possible version +string, optional feature combination or firmware target. + +## Earlier STM32 flash blockers + +The following targets compile but fail the linker flash limit. Both failures +were reproduced from an isolated, unchanged checkout of the baseline with the +same build recipes and compiler versions: + +| Target | Baseline overflow | Calibration build overflow | Additional bytes | +| --- | ---: | ---: | ---: | +| `wio-e5-mini_repeater` | 1,168 | 2,852 | 1,684 | +| `wio-e5-mini_sensor` | 25,584 | 27,368 | 1,784 | + +The calibration increase consists of code and constants; it did not introduce +additional floating-point runtime helpers in the Repeater. Filesystem and +application boundaries have not been moved, and no unrelated feature was +removed to conceal these build failures. These results apply to the earlier source baseline above, before the Mini +Repeater feature gates in `94a3b7e56`. The integration checks below supersede +that earlier Repeater result when a newer build succeeds. + + +## Final integration checks + +After merging `94a3b7e56`, 69 distinct focused tests passed: 42 calibration, +platform-storage and related regressions, 19 battery-forwarding tests, and +8 Mini Repeater profile checks. The 23 calibration/store/forwarding tests +were rerun successfully after the final reply compaction. Both new calibration +suites are included in the unit-test workflow. + +The first integrated Mini Repeater link exceeded flash by 432 bytes. +Disabling guessed branch probabilities in that target's existing size recipe, +sharing the existing save-failure replies, keeping the command key in constant +storage and shortening calibration replies recovered 808 bytes. Calibration, +routing, logging, 50 neighbours and the existing 16 KiB filesystem are retained. +No extra feature was disabled to make this image fit. + +The final merged-source builds passed their flash and runtime-memory checks: + +| Target | Application bytes used | Application limit | Free flash bytes | +| --- | ---: | ---: | ---: | +| `Heltec_v3_repeater` | 1,890,881 | 3,342,336 | 1,451,455 | +| `RAK_3401_repeater` | 559,616 | 815,104 | 255,488 | +| `wio-e5-mini_repeater` | 245,384 | 245,760 | 376 | +| `wio-e5-mini_companion_radio_usb` | 228,664 | 229,376 | 712 | + +These small Mini margins qualify the stated recipes, not arbitrary optional +features or longer build metadata. The earlier Mini Sensor overflow remains +recorded above; that target was not rebuilt in this final integration campaign. +No physical ADC, voltmeter, charging or installed-device test was performed. diff --git a/docs/test-results/index.md b/docs/test-results/index.md index 7a4502c3e..b35697919 100644 --- a/docs/test-results/index.md +++ b/docs/test-results/index.md @@ -75,6 +75,7 @@ Completed validation reports, experiments, and test guides are collected here. O ## Build, storage, memory, and checklists +- [Shared battery ADC calibration validation, 2026-10-22](battery_calibration_2026-10-22.md) - [Heltec V2 BLE RAM reserve repair, 2026-10-22](heltec_v2_ble_memory_2026-10-22.md) - [ESP32 Bluetooth shutdown and radio2 CPU idle, 2026-10-21](esp32_ble_radio2_sleep_2026-10-21.md) - [RAK3401 all firmware CPU sleep checks, 2026-10-21](rak3401_all_roles_cpu_sleep_2026-10-21.md) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 6564cd307..d029b19fd 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -9,6 +9,7 @@ #include #endif #include +#include #include #include #if defined(MESH_HIL_TX_FREQ_MHZ) @@ -3026,6 +3027,13 @@ void MyMesh::begin(bool has_display, bool radio_available, // load persisted prefs const bool prefs_ready = _store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon); + FILESYSTEM* battery_calibration_fs = _store->getPrimaryFS(); +#if defined(NRF52_PLATFORM) + if (_store->isVolatilePrimaryFS()) battery_calibration_fs = nullptr; +#endif + if (!mesh::power::beginBatteryCalibrationStore(board, battery_calibration_fs)) { + MESH_DEBUG_PRINTLN("Battery calibration could not be restored; using board default"); + } #if defined(ESP32_PLATFORM) && defined(WIFI_SSID) #ifdef ADMIN_PASSWORD const char* management_default = ADMIN_PASSWORD; @@ -10637,6 +10645,8 @@ static bool isCompanionRadioPrefsCommand(const char* command) { // Called only for directly attached USB/BLE/TCP/browser clients. Commands // received over LoRa use handleCommand() with a nonzero sender timestamp. bool MyMesh::handleDirectCommand(const char* command, char* reply, size_t reply_size) { + if (mesh::power::handleStoredBatteryCalibrationCommand( + board, command, reply, reply_size)) return true; #if MESH_ENABLE_LOST_REPLY if (mesh::companion::handleLostReplyCommand( _prefs, command, reply, reply_size, [this]() { return savePrefs(); })) return true; @@ -10834,6 +10844,8 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, && mesh::handleReaderReply(command, reply, reply_capacity)) return true; #endif + if (mesh::power::handleStoredBatteryCalibrationCommand( + board, command, reply, reply_capacity)) return true; #if defined(NRF52_POWER_MANAGEMENT) if (mesh::power::handleVoltagePolicyCommand(command, reply, reply_capacity)) return true; diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 709794060..92ed33085 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -2692,7 +2692,6 @@ void MyMesh::begin(FILESYSTEM *fs) { updateAdvertTimer(); updateFloodAdvertTimer(); - board.setAdcMultiplier(_prefs.adc_multiplier); #if ENV_INCLUDE_GPS == 1 applyGpsPrefs(); diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index dcfde4c42..848137466 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -1,5 +1,6 @@ #include #include +#include #include // needed for PlatformIO #include #if MESH_PACKET_LOGGING @@ -424,6 +425,10 @@ public: mesh::setUsbLoggingEnabled(_prefs.usb_logging_enabled != 0); #endif + if (!mesh::power::beginBatteryCalibrationStore(board, volatile_primary_fs ? nullptr : _fs)) { + MESH_DEBUG_PRINTLN("Battery calibration could not be restored; using board default"); + } + loadContacts(); _public = addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel } @@ -470,6 +475,12 @@ public: } void handleCommand(const char* command) { + char calibration_reply[160]; + if (mesh::power::handleStoredBatteryCalibrationCommand(board, command, + calibration_reply, sizeof(calibration_reply))) { + Serial.println(calibration_reply); + return; + } #if MESH_BATTERY_CHARGE_CONTROL char charge_reply[160]; if (mesh::power::handleBatteryChargeCommand(board, command, charge_reply, diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 8b42f7e3c..4673f0805 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -1316,7 +1316,6 @@ void SensorMesh::begin(FILESYSTEM* fs) { updateAdvertTimer(); updateFloodAdvertTimer(); - board.setAdcMultiplier(_prefs.adc_multiplier); #if ENV_INCLUDE_GPS == 1 applyGpsPrefs(); diff --git a/src/MeshCore.h b/src/MeshCore.h index 96bce2d75..898fba99d 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -5,6 +5,7 @@ #include #include "helpers/UsbLogging.h" #include "helpers/BatteryChargeSupport.h" +#include "helpers/BatteryCalibration.h" #include "helpers/RegulatorMode.h" #include "helpers/PowerSavingDiagnostics.h" #include "helpers/LedPolicy.h" @@ -137,13 +138,28 @@ namespace mesh { class MainBoard { bool _radio_test_active = false; + float _battery_calibration_factor = 1.0f; +protected: + uint16_t calibrateBatteryMillivolts(uint16_t raw_mv) const { + return mesh::power::scaleBatteryMillivolts(raw_mv, _battery_calibration_factor); + } public: void setRadioTestActive(bool active) { _radio_test_active = active; } bool isRadioTestActive() const { return _radio_test_active; } virtual uint16_t getBattMilliVolts() = 0; virtual float getMCUTemperature() { return NAN; } - virtual bool setAdcMultiplier(float multiplier) { return false; }; - virtual float getAdcMultiplier() const { return 0.0f; } + virtual bool supportsBatteryCalibration() const { return false; } + // Relative to the board's built-in ADC conversion; zero restores its scale. + // Boards with an existing absolute ADC coefficient retain their overrides. + virtual bool setAdcMultiplier(float multiplier) { + if (!supportsBatteryCalibration() + || !mesh::power::validRelativeBatteryMultiplier(multiplier)) return false; + _battery_calibration_factor = multiplier == 0.0f ? 1.0f : multiplier; + return true; + } + virtual float getAdcMultiplier() const { + return supportsBatteryCalibration() ? _battery_calibration_factor : 0.0f; + } virtual const char* getManufacturerName() const = 0; virtual void onBeforeTransmit() { } virtual void onAfterTransmit() { } diff --git a/src/helpers/BatteryCalibration.h b/src/helpers/BatteryCalibration.h new file mode 100644 index 000000000..e6559d59a --- /dev/null +++ b/src/helpers/BatteryCalibration.h @@ -0,0 +1,118 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace mesh { namespace power { + +// New ADC backends use a correction relative to their original conversion. +// Existing board-specific absolute multipliers retain their own units. +inline bool validRelativeBatteryMultiplier(float multiplier) { + return isfinite(multiplier) && (multiplier == 0.0f + || (multiplier >= 0.5f && multiplier <= 1.5f)); +} + +inline uint16_t scaleBatteryMillivolts(uint16_t millivolts, float multiplier) { + if (millivolts == 0 || !isfinite(multiplier) || multiplier <= 0.0f) + return millivolts; + const float scaled = millivolts * multiplier; + if (scaled >= 65535.0f) return UINT16_MAX; + return static_cast(scaled + 0.5f); +} + +// Volts with millivolt precision. Require a measured value; neither chemistry +// nor charger regulation voltage supplies a calibration reference. +inline bool parseBatteryReferenceMillivolts(const char* text, uint16_t& result) { + if (!text || *text < '0' || *text > '9') return false; + uint32_t whole = 0, fraction = 0; + while (*text >= '0' && *text <= '9') { + whole = whole * 10 + static_cast(*text++ - '0'); + if (whole > 65) return false; + } + unsigned digits = 0; + if (*text == '.') { + ++text; + while (*text >= '0' && *text <= '9') { + if (++digits > 3) return false; + fraction = fraction * 10 + static_cast(*text++ - '0'); + } + if (digits == 0) return false; + } + while (*text == ' ' || *text == '\t') ++text; + if (*text) return false; + while (digits++ < 3) fraction *= 10; + const uint32_t millivolts = whole * 1000 + fraction; + if (millivolts == 0 || millivolts > UINT16_MAX) return false; + result = static_cast(millivolts); + return true; +} + +// Setter owns application, checked persistence and rollback. It leaves a +// useful error in reply when it fails. This keeps nRF52 boot-voltage policy +// and legacy absolute multiplier storage authoritative in their own roles. +template +bool handleBatteryCalibrationCommand(Board& board, const char* command, + char* reply, size_t capacity, + Setter set_multiplier) { + if (!command || !reply || capacity == 0) return false; + while (*command == ' ' || *command == '\t') ++command; + static const char key[] = "battery calibrate"; + const size_t length = sizeof(key) - 1; + if (strncmp(command, key, length) != 0 + || (command[length] && command[length] != ' ' && command[length] != '\t')) + return false; + const char* value = command + length; + while (*value == ' ' || *value == '\t') ++value; + const float previous = board.getAdcMultiplier(); + if (!isfinite(previous) || previous <= 0.0f) { + snprintf(reply, capacity, "Error: battery ADC unsupported"); + return true; + } + const char* reset_end = value; + if (strncmp(value, "reset", 5) == 0) { + reset_end = value + 5; + while (*reset_end == ' ' || *reset_end == '\t') ++reset_end; + if (*reset_end == 0) { + *reply = 0; + if (set_multiplier(0.0f, reply, capacity)) + snprintf(reply, capacity, "OK - ADC defaults restored (saved)"); + else if (*reply == 0) + snprintf(reply, capacity, "Error: calibration save failed"); + return true; + } + } + uint16_t reference = 0; + if (!parseBatteryReferenceMillivolts(value, reference)) { + snprintf(reply, capacity, "Error: use battery calibrate |reset"); + return true; + } + const uint16_t reported = board.getBattMilliVolts(); + if (reported == 0 || reported == UINT16_MAX) { + snprintf(reply, capacity, "Error: battery voltage unavailable"); + return true; + } + const float correction = static_cast(reference) / reported; + if (!validRelativeBatteryMultiplier(correction)) { + snprintf(reply, capacity, "Error: reference must be 50%%-150%% of reading"); + return true; + } + const float multiplier = previous * correction; + if (!isfinite(multiplier) || multiplier <= 0.0f) { + snprintf(reply, capacity, "Error: invalid calibration correction"); + return true; + } + *reply = 0; + if (set_multiplier(multiplier, reply, capacity)) { + snprintf(reply, capacity, "OK - %u.%03u V; multiplier %.6f (saved)", + static_cast(reference / 1000), + static_cast(reference % 1000), board.getAdcMultiplier()); + } else if (*reply == 0) { + snprintf(reply, capacity, "Error: calibration save failed"); + } + return true; +} + +}} // namespace mesh::power diff --git a/src/helpers/BatteryCalibrationStore.cpp b/src/helpers/BatteryCalibrationStore.cpp new file mode 100644 index 000000000..1a56ab183 --- /dev/null +++ b/src/helpers/BatteryCalibrationStore.cpp @@ -0,0 +1,152 @@ +#include "BatteryCalibrationStore.h" +#include "BatteryCalibration.h" +#include "CheckedSettingsFile.h" +#include "CLICommandUtils.h" +#include "PersistentStoreFormat.h" +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) +#include "AtomicFileWriter.h" +#else +#include "ContactFileTransaction.h" +#endif +#if defined(NRF52_POWER_MANAGEMENT) +#include "NRF52VoltagePolicy.h" +#endif +#include +#include +#include + +namespace mesh { namespace power { +namespace { +FILESYSTEM* calibration_fs = nullptr; +const char* const calibration_path = "/battery_cal"; + +bool usesVoltagePolicy() { +#if defined(NRF52_POWER_MANAGEMENT) + return voltagePolicySupported(); +#else + return false; +#endif +} + +void encode(float multiplier, uint8_t (&data)[12]) { + memcpy(data, "ADC1", 4); + static_assert(sizeof(multiplier) == sizeof(uint32_t), "ADC record needs 32-bit float"); + uint32_t bits; + memcpy(&bits, &multiplier, sizeof(bits)); + storage::writeLE32(data + 4, bits); + storage::writeLE32(data + 8, storage::updateCRC32(0xffffffffUL, data, 8)); +} + +bool readMultiplier(float& multiplier) { + uint8_t data[12]; + if (!storage::readFixedSettingsFile(calibration_fs, calibration_path, data, sizeof(data)) + || memcmp(data, "ADC1", 4) != 0 + || storage::readLE32(data + 8) != storage::updateCRC32(0xffffffffUL, data, 8)) return false; + const uint32_t bits = storage::readLE32(data + 4); + memcpy(&multiplier, &bits, sizeof(bits)); + return isfinite(multiplier) && multiplier >= 0.0f && multiplier <= 10.0f; +} + +bool saveMultiplier(float multiplier) { + if (!calibration_fs) return false; + uint8_t data[12], verified[12]; + encode(multiplier, data); +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + AtomicFileWriter file(calibration_fs, calibration_path); +#else + ContactFileTransaction file(calibration_fs, calibration_path); +#endif + if (!file || file.write(data, sizeof(data)) != sizeof(data) || !file.commit()) return false; + return storage::readFixedSettingsFile(calibration_fs, calibration_path, + verified, sizeof(verified)) + && memcmp(data, verified, sizeof(data)) == 0; +} +} // namespace + +bool beginBatteryCalibrationStore(MainBoard& board, FILESYSTEM* fs) { + calibration_fs = fs; + if (usesVoltagePolicy() || board.getAdcMultiplier() <= 0.0f) return true; +#if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM) + if (!fs || !ContactFileTransaction::recover(fs, calibration_path)) return false; +#endif + if (!fs) return false; + if (!fs->exists(calibration_path)) return true; + float multiplier; + return readMultiplier(multiplier) && board.setAdcMultiplier(multiplier); +} + +bool setStoredAdcMultiplier(MainBoard& board, float multiplier, + char* reply, size_t capacity) { + if (!isfinite(multiplier) || multiplier < 0.0f || multiplier > 10.0f) { + snprintf(reply, capacity, "Error: multiplier must be 0-10"); + return false; + } + if (board.getAdcMultiplier() <= 0.0f) { + snprintf(reply, capacity, "Error: ADC calibration unsupported"); + return false; + } +#if defined(NRF52_POWER_MANAGEMENT) + if (usesVoltagePolicy()) { + char command[48]; + snprintf(command, sizeof(command), "set adc.multiplier %.3f", multiplier); + return handleVoltagePolicyCommand(command, reply, capacity) + && strncmp(reply, "OK", 2) == 0; + } +#endif + const float previous = board.getAdcMultiplier(); + if (!board.setAdcMultiplier(multiplier)) { + snprintf(reply, capacity, "Error: multiplier rejected by board"); + return false; + } + if (!saveMultiplier(multiplier)) { + const bool restored = board.setAdcMultiplier(previous); + snprintf(reply, capacity, "%s", restored + ? "Error: save unconfirmed; reboot may differ" + : "Error: save unconfirmed; runtime restore failed; reboot may differ"); + return false; + } + if (multiplier == 0.0f) snprintf(reply, capacity, "OK - using default board multiplier"); + else snprintf(reply, capacity, "OK - multiplier set to %.3f (saved)", multiplier); + return true; +} + +bool handleStoredAdcMultiplierCommand(MainBoard& board, const char* command, + char* reply, size_t capacity) { + if (!command || !reply || !capacity) return false; + const bool get = strncmp(command, "get ", 4) == 0; + const bool set = strncmp(command, "set ", 4) == 0; + if (!get && !set) return false; + const char* key = command + 4; + if (strncmp(key, "adc.multiplier", 14) != 0 + || (key[14] && key[14] != ' ' && key[14] != '\t')) return false; + if (board.getAdcMultiplier() <= 0.0f) { + snprintf(reply, capacity, "Error: ADC calibration unsupported"); + return true; + } +#if defined(NRF52_POWER_MANAGEMENT) + if (usesVoltagePolicy()) return handleVoltagePolicyCommand(command, reply, capacity); +#endif + const char* value = key + 14; + while (*value == ' ' || *value == '\t') ++value; + if (get) { + if (*value) snprintf(reply, capacity, "Error: usage get adc.multiplier"); + else if (board.getAdcMultiplier() <= 0.0f) snprintf(reply, capacity, "Error: ADC calibration unsupported"); + else snprintf(reply, capacity, "> %.3f", board.getAdcMultiplier()); + } else { + float multiplier; + if (!cli::parseDecimalStrict(value, multiplier)) + snprintf(reply, capacity, "Error: invalid multiplier"); + else setStoredAdcMultiplier(board, multiplier, reply, capacity); + } + return true; +} + +bool handleStoredBatteryCalibrationCommand(MainBoard& board, const char* command, + char* reply, size_t capacity) { + if (handleBatteryCalibrationCommand(board, command, reply, capacity, + [&board](float multiplier, char* result, size_t result_capacity) { + return setStoredAdcMultiplier(board, multiplier, result, result_capacity); + })) return true; + return handleStoredAdcMultiplierCommand(board, command, reply, capacity); +} +}} // namespace mesh::power diff --git a/src/helpers/BatteryCalibrationStore.h b/src/helpers/BatteryCalibrationStore.h new file mode 100644 index 000000000..47fe0b392 --- /dev/null +++ b/src/helpers/BatteryCalibrationStore.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +namespace mesh { namespace power { + +// Companion and Terminal Chat keep calibration outside their published +// preference layouts. Infrastructure roles retain their existing ADC field. +bool beginBatteryCalibrationStore(MainBoard& board, FILESYSTEM* fs); +bool setStoredAdcMultiplier(MainBoard& board, float multiplier, + char* reply, size_t capacity); +bool handleStoredAdcMultiplierCommand(MainBoard& board, const char* command, + char* reply, size_t capacity); +bool handleStoredBatteryCalibrationCommand(MainBoard& board, const char* command, + char* reply, size_t capacity); + +}} // namespace mesh::power diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 2ef9d84a3..308060048 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -22,6 +22,7 @@ #include #endif #include +#include #if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX #include #endif @@ -1069,6 +1070,12 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) { if (loaded) syncOtaConfigFromPrefs(); // persisted OTA policy/keys -> OtaContext (else keep safe defaults) #endif mesh::hilStartupTrace("prefs_apply_begin"); +#if defined(NRF52_POWER_MANAGEMENT) + // The startup voltage policy owns the relative calibration on these boards. + // Its checked record must not be replaced by the legacy infrastructure field. + if (!mesh::power::voltagePolicySupported()) +#endif + _board->setAdcMultiplier(_prefs->adc_multiplier); #if defined(RAK_4631) // Boot-time only. Later CLI saves must not cut a live peripheral/flash rail. _sensors->configureSharedPeripheralMode(_prefs->peripherals_shared != 0); @@ -3018,6 +3025,47 @@ bool CommonCLI::saveCommonPrefs() { return _common_save_result_known && _common_save_succeeded; } +bool CommonCLI::applyAndSaveAdcMultiplier(float multiplier, char* reply, + size_t capacity) { + if (!isfinite(multiplier) || multiplier < 0.0f || multiplier > 10.0f) { + snprintf(reply, capacity, "Error: multiplier must be 0-10"); + return false; + } + if (_board->getAdcMultiplier() <= 0.0f) { + snprintf(reply, capacity, "Error: unsupported"); + return false; + } +#if defined(NRF52_POWER_MANAGEMENT) + if (mesh::power::voltagePolicySupported()) { + char command[48]; + snprintf(command, sizeof(command), "set adc.multiplier %.3f", multiplier); + const bool saved = mesh::power::handleVoltagePolicyCommand(command, reply, capacity) + && strncmp(reply, "OK", 2) == 0; + if (saved) loopBatteryForwarding(true); + return saved; + } +#endif + const float previous = _board->getAdcMultiplier(); + const float previous_pref = _prefs->adc_multiplier; + if (!_board->setAdcMultiplier(multiplier)) { + snprintf(reply, capacity, "Error: multiplier rejected by board"); + return false; + } + _prefs->adc_multiplier = multiplier; + if (!trySavePrefs()) { + _prefs->adc_multiplier = previous_pref; + const bool restored = _board->setAdcMultiplier(previous); + snprintf(reply, capacity, "%s", restored + ? "Error: save unconfirmed; reboot may differ" + : "Error: save unconfirmed; runtime restore failed; reboot may differ"); + return false; + } + loopBatteryForwarding(true); + if (multiplier == 0.0f) snprintf(reply, capacity, "OK - using default board multiplier"); + else snprintf(reply, capacity, "OK - multiplier set to %.3f", multiplier); + return true; +} + bool CommonCLI::savePrimaryRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, uint16_t preamble) { if (!isfinite(freq) || !isfinite(bw) || freq < 150.0f || freq > 2500.0f @@ -3264,6 +3312,10 @@ bool CommonCLI::handleUDPBridgeCommand(uint32_t sender_timestamp, void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) { PrefsSaveReplyGuard save_reply(_prefs_save_failures, reply); mesh::cli::normalizeCommandVerb(command); + if (mesh::power::handleBatteryCalibrationCommand(*_board, command, reply, 160, + [this](float multiplier, char* result, size_t capacity) { + return applyAndSaveAdcMultiplier(multiplier, result, capacity); + })) return; #if MESH_ENABLE_BATTERY_FORWARDING if (handleBatteryForwardingCommand(command, reply)) return; #endif @@ -4229,12 +4281,18 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep return; } #if defined(NRF52_POWER_MANAGEMENT) - if (mesh::power::handleVoltagePolicyCommand(command, reply, 160)) { -#if MESH_ENABLE_BATTERY_FORWARDING - if (strncmp(reply, "OK", 2) == 0) loopBatteryForwarding(true); -#endif + const bool adc_multiplier_command = strncmp(config, "adc.multiplier", 14) == 0 + && (config[14] == 0 || config[14] == ' ' || config[14] == '\t'); + if (adc_multiplier_command && _board->getAdcMultiplier() <= 0.0f) { + strcpy(reply, "Error: unsupported"); return; } + if (!adc_multiplier_command || mesh::power::voltagePolicySupported()) { + if (mesh::power::handleVoltagePolicyCommand(command, reply, 160)) { + if (strncmp(reply, "OK", 2) == 0) loopBatteryForwarding(true); + return; + } + } #endif // Infrastructure roles dispatch here rather than through CommonRadioPrefs. if (strncmp(config, "path.hash.mode", 14) == 0 @@ -5721,19 +5779,8 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep float multiplier = 0.0f; if (!mesh::cli::parseDecimalStrict(&config[15], multiplier)) { strcpy(reply, "Error: invalid multiplier"); - } else if (_board->setAdcMultiplier(multiplier)) { - _prefs->adc_multiplier = multiplier; - savePrefs(); -#if MESH_ENABLE_BATTERY_FORWARDING - loopBatteryForwarding(true); -#endif - if (_prefs->adc_multiplier == 0.0f) { - strcpy(reply, "OK - using default board multiplier"); - } else { - sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier); - } } else { - strcpy(reply, "Error: unsupported"); + applyAndSaveAdcMultiplier(multiplier, reply, 160); }; } else if (memcmp(config, "reboot.interval ", 16) == 0) { int hours = _atoi(&config[16]); @@ -5804,7 +5851,15 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep return; } #if defined(NRF52_POWER_MANAGEMENT) - if (mesh::power::handleVoltagePolicyCommand(command, reply, 160)) return; + const bool adc_multiplier_command = strncmp(config, "adc.multiplier", 14) == 0 + && (config[14] == 0 || config[14] == ' ' || config[14] == '\t'); + if (adc_multiplier_command && _board->getAdcMultiplier() <= 0.0f) { + strcpy(reply, "Error: unsupported"); + return; + } + if (!adc_multiplier_command || mesh::power::voltagePolicySupported()) { + if (mesh::power::handleVoltagePolicyCommand(command, reply, 160)) return; + } #endif if (strcmp(config, "password") == 0) { if (sender_timestamp == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 095b491d7..9212c44ec 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -945,6 +945,7 @@ class CommonCLI { PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common); bool trySavePrefs( PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common); + bool applyAndSaveAdcMultiplier(float multiplier, char* reply, size_t capacity); bool saveObserverPrefs(); void loadPrefsInt(FILESYSTEM* _fs, const char* filename); #ifdef WITH_MQTT_BRIDGE diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index 5a74309c1..d6d17938c 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -518,6 +518,14 @@ public: } #endif + bool supportsBatteryCalibration() const override { +#ifdef PIN_VBAT_READ + return true; +#else + return false; +#endif + } + uint16_t getBattMilliVolts() override { #ifdef PIN_VBAT_READ analogReadResolution(12); @@ -528,7 +536,7 @@ public: } raw = raw / 4; - return (2 * raw); + return calibrateBatteryMillivolts(2 * raw); #else return 0; // not supported #endif diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index aadf055c9..e6192359b 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -293,12 +293,24 @@ void NRF52Board::loop() { #endif } -#ifdef NRF52_POWER_MANAGEMENT uint16_t NRF52Board::calibrateBatteryMillivolts(uint16_t raw_mv) const { - return mesh::power::calibratedMillivolts( - raw_mv, mesh::power::configuredAdcPermille()); -} +#ifdef NRF52_POWER_MANAGEMENT + if (supportsBatteryCalibration() && mesh::power::voltagePolicySupported()) { + return mesh::power::calibratedMillivolts( + raw_mv, mesh::power::configuredAdcPermille()); + } #endif + return mesh::MainBoard::calibrateBatteryMillivolts(raw_mv); +} + +float NRF52Board::getAdcMultiplier() const { +#ifdef NRF52_POWER_MANAGEMENT + if (supportsBatteryCalibration() && mesh::power::voltagePolicySupported()) { + return mesh::power::configuredAdcPermille() / 1000.0f; + } +#endif + return mesh::MainBoard::getAdcMultiplier(); +} #if NRF52_WATCHDOG_TIMEOUT_SECONDS > 0 static void reloadWatchdogChannels() { diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 41ae8562e..1fecbdda1 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -102,10 +102,10 @@ public: bool isUsbDataConnected() override; bool isUsbHostConnected() override; -#ifdef NRF52_POWER_MANAGEMENT - // Relative to each board's built-in ADC conversion; 1.000 is unchanged. + // Power-managed boards use the early-boot voltage policy; other ADC boards + // use the shared relative multiplier persisted with their role settings. uint16_t calibrateBatteryMillivolts(uint16_t raw_mv) const; -#endif + float getAdcMultiplier() const override; void attachDynamicPrefs(KeyValueStore* prefs) { (void)prefs; } // no-op diff --git a/src/helpers/esp32/TBeamBoard.h b/src/helpers/esp32/TBeamBoard.h index b78a867b0..d77afeb13 100644 --- a/src/helpers/esp32/TBeamBoard.h +++ b/src/helpers/esp32/TBeamBoard.h @@ -162,6 +162,8 @@ public: } #endif + bool supportsBatteryCalibration() const override { return false; } + uint16_t getBattMilliVolts(){ if (!charge_pmu_ready) return 0; #if defined(PORTABLE_MQTT_OBSERVER) && !defined(TBEAM_SUPREME_SX1262) diff --git a/test/README.md b/test/README.md index 7a8946d46..1f1ff4db0 100644 --- a/test/README.md +++ b/test/README.md @@ -38,6 +38,8 @@ python3 test/test_companion_identity_startup.py # real saved/missing/unreadable python3 test/test_startup_screen.py # early display ordering and cooperative entropy progress python3 -B test/test_common_prefs_legacy_rxgain.py -v # compact stock gain, appended tails and truncation python3 -B test/test_battery_forwarding.py -v # battery relay thresholds, hysteresis, calibration and checked settings records +python3 -B test/test_battery_calibration.py -v # meter references, ADC correction, reset and invalid-reading rejection +python3 -B test/test_battery_calibration_store.py -v # saved calibration, corruption and write-failure recovery across filesystems python3 test/test_companion_settings_persistence_contract.py # Atomic settings and appended Bluetooth fields python3 test/test_webconfig_ui.py # Web controls, reboot handling and generated-page consistency python3 test/test_webconfig_ui_runtime.py # Real Chromium, including independent stealth toggle diff --git a/test/test_battery_calibration.py b/test/test_battery_calibration.py new file mode 100644 index 000000000..92ef87856 --- /dev/null +++ b/test/test_battery_calibration.py @@ -0,0 +1,228 @@ +"""Exercise production battery calibration parsing, scaling and command behavior.""" +from pathlib import Path +import os +import subprocess +import tempfile +import unittest +from test_replay_reset_integration import extract_braced + +ROOT = Path(__file__).resolve().parents[1] + +HARNESS = r''' +#include +#include +#include +#include +#include +#include + +using namespace mesh::power; +struct Board { + float multiplier = 1.0f; + uint16_t millivolts = 4000; + unsigned reads = 0; + float getAdcMultiplier() const { return multiplier; } + uint16_t getBattMilliVolts() { ++reads; return millivolts; } +}; + +int main() { + assert(validRelativeBatteryMultiplier(0)); + assert(validRelativeBatteryMultiplier(0.5f)); + assert(validRelativeBatteryMultiplier(1.5f)); + assert(!validRelativeBatteryMultiplier(0.499f)); + assert(!validRelativeBatteryMultiplier(1.501f)); + assert(!validRelativeBatteryMultiplier(NAN)); + assert(!validRelativeBatteryMultiplier(INFINITY)); + assert(scaleBatteryMillivolts(4000, 1.05f) == 4200); + assert(scaleBatteryMillivolts(1001, 0.5f) == 501); + assert(scaleBatteryMillivolts(0, 1.5f) == 0); + assert(scaleBatteryMillivolts(60000, 1.5f) == UINT16_MAX); + assert(scaleBatteryMillivolts(4000, NAN) == 4000); + assert(scaleBatteryMillivolts(4000, 0) == 4000); + + uint16_t parsed = 123; + assert(parseBatteryReferenceMillivolts("4.18", parsed) && parsed == 4180); + assert(parseBatteryReferenceMillivolts("13.200", parsed) && parsed == 13200); + assert(parseBatteryReferenceMillivolts("65.535\t ", parsed) && parsed == 65535); + assert(parseBatteryReferenceMillivolts("0.001", parsed) && parsed == 1); + for (const char* bad : {"", "0", "0.000", "-4.2", "+4.2", "nan", "inf", + "4e0", "4.2V", "4.2 extra", "4.", "4.2000", "65.536", "999999999999"}) { + parsed = 123; + assert(!parseBatteryReferenceMillivolts(bad, parsed) && parsed == 123); + } + + Board board; + char reply[160] = {}; + bool save_ok = true; + unsigned saves = 0; + float saved = -1; + auto setter = [&](float value, char* error, size_t capacity) { + ++saves; + if (!save_ok) { + snprintf(error, capacity, "Error: simulated save failure"); + return false; + } + saved = value; + board.multiplier = value == 0 ? 1 : value; + return true; + }; + auto run = [&](const char* command) { + strcpy(reply, "untouched"); + return handleBatteryCalibrationCommand(board, command, reply, sizeof(reply), setter); + }; + assert(!run("battery calibrateX 4.18") && saves == 0); + assert(strcmp(reply, "untouched") == 0); + assert(run("battery calibrate 4.18") && saves == 1); + assert(fabs(saved - 1.045f) < 0.00001f); + assert(strstr(reply, "4.180 V") && strstr(reply, "(saved)")); + // Repeat calibration composes with the effective existing multiplier. + board.millivolts = 4180; + assert(run("battery calibrate 4.18") && saves == 2); + assert(fabs(saved - 1.045f) < 0.00001f); + // Works with an existing absolute board coefficient too. + board.multiplier = 5.42f; + board.millivolts = 4000; + assert(run("battery calibrate 4.18") && saves == 3); + assert(fabs(saved - 5.6639f) < 0.0001f); + // Pack voltages are not limited to single-cell lithium values. + board.multiplier = 1; + board.millivolts = 13000; + assert(run("battery calibrate 13.2") && saves == 4); + assert(fabs(saved - 13200.0f / 13000.0f) < 0.00001f); + const unsigned before = saves; + for (const char* command : {"battery calibrate", "battery calibrate 0", + "battery calibrate 4.2junk", "battery calibrate resetjunk", + "battery calibrate nan", "battery calibrate 4.2", "battery calibrate 65"}) { + assert(run(command) && strncmp(reply, "Error:", 6) == 0 && saves == before); + } + board.millivolts = 0; + assert(run("battery calibrate 13.2") && saves == before); + board.millivolts = UINT16_MAX; + assert(run("battery calibrate 13.2") && saves == before); + // Reset is allowed even while the voltage is unavailable. + assert(run("battery calibrate reset") && saves == before + 1 && saved == 0); + board.millivolts = 4000; + save_ok = false; + const float previous = board.multiplier; + assert(run("battery calibrate 4.18")); + assert(strcmp(reply, "Error: simulated save failure") == 0); + assert(board.multiplier == previous); + board.multiplier = 0; + const unsigned unsupported_saves = saves, unsupported_reads = board.reads; + assert(run("battery calibrate 4.18") && strstr(reply, "unsupported")); + assert(run("battery calibrate reset") && strstr(reply, "unsupported")); + assert(saves == unsupported_saves && board.reads == unsupported_reads); + board.multiplier = NAN; + assert(run("battery calibrate 4.18") && saves == unsupported_saves); + // Replies obey a prefixed Companion console's smaller buffer. + board.multiplier = 1; + char tiny[12]; + memset(tiny, 'x', sizeof(tiny)); + assert(handleBatteryCalibrationCommand(board, "battery calibrate", tiny, 8, setter)); + assert(tiny[7] == 0 && tiny[8] == 'x'); +} +''' + +BOARD_HARNESS = r''' +#include +#include +#include +struct ADCBoard : mesh::MainBoard { + uint16_t raw = 4000; + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { return calibrateBatteryMillivolts(raw); } + const char* getManufacturerName() const override { return "test ADC"; } + void reboot() override {} + uint8_t getStartupReason() const override { return 0; } +}; +struct UnsupportedBoard : ADCBoard { + bool supportsBatteryCalibration() const override { return false; } +}; +int main() { + ADCBoard board; + assert(board.getAdcMultiplier() == 1 && board.getBattMilliVolts() == 4000); + assert(board.setAdcMultiplier(1.05f) && board.getBattMilliVolts() == 4200); + for (float invalid : {NAN, INFINITY, -INFINITY, -1.0f, 0.49f, 1.51f}) { + assert(!board.setAdcMultiplier(invalid)); + assert(board.getBattMilliVolts() == 4200); + } + assert(board.setAdcMultiplier(0) && board.getBattMilliVolts() == 4000); + assert(board.setAdcMultiplier(1.5f)); + board.raw = 60000; + assert(board.getBattMilliVolts() == UINT16_MAX); + board.raw = 0; + assert(board.getBattMilliVolts() == 0); + UnsupportedBoard unsupported; + assert(unsupported.getAdcMultiplier() == 0); + assert(!unsupported.setAdcMultiplier(1.05f)); +} +''' + + +class BatteryCalibrationTests(unittest.TestCase): + def exercise(self, harness): + with tempfile.TemporaryDirectory(prefix="battery-calibration-") as folder: + source, binary = Path(folder) / "main.cpp", Path(folder) / "check" + source.write_text("#include \n" + harness) + build = subprocess.run([ + os.environ.get("CXX", "g++"), "-std=c++17", "-O1", "-g", + "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter", + "-fsanitize=address,undefined", + "-fno-sanitize-recover=all", "-fno-pie", "-no-pie", "-I", + str(ROOT / "src"), str(source), "-o", str(binary) + ], capture_output=True, text=True, timeout=60) + self.assertEqual(build.returncode, 0, build.stdout + build.stderr) + result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_production_calibration_commands(self): + self.exercise(HARNESS) + + def test_actual_board_calibration(self): + self.exercise(BOARD_HARNESS) + + def test_nrf52_boot_policy_takes_priority_without_double_scaling(self): + source = (ROOT / "src/helpers/NRF52Board.cpp").read_text() + methods = "\n".join(extract_braced(source, signature) for signature in ( + "uint16_t NRF52Board::calibrateBatteryMillivolts(", + "float NRF52Board::getAdcMultiplier(")) + self.exercise(r''' +#define NRF52_POWER_MANAGEMENT 1 +#include +#include +#include +#include +namespace mesh { namespace power { +bool policy_ready = false; +bool voltagePolicySupported() { return policy_ready; } +uint16_t configuredAdcPermille() { return 1050; } +}} +class NRF52Board : public mesh::MainBoard { +public: + bool supportsBatteryCalibration() const override { return true; } + uint16_t calibrateBatteryMillivolts(uint16_t) const; + float getAdcMultiplier() const override; + uint16_t getBattMilliVolts() override { return calibrateBatteryMillivolts(4000); } + const char* getManufacturerName() const override { return "fixture"; } + uint8_t getStartupReason() const override { return 0; } + void reboot() override {} +}; +''' + methods + r''' +int main() { + NRF52Board board; + // Early startup has not loaded the boot policy yet. + assert(board.getBattMilliVolts() == 4000 && board.getAdcMultiplier() == 1); + // A board without boot policy uses the common calibration normally. + assert(board.setAdcMultiplier(1.2f) && board.getBattMilliVolts() == 4800); + mesh::power::policy_ready = true; + // A loaded policy is authoritative, rather than stacking two corrections. + assert(board.getBattMilliVolts() == 4200); + assert(fabs(board.getAdcMultiplier() - 1.05f) < 0.00001f); + assert(board.calibrateBatteryMillivolts(0) == 0); + assert(board.calibrateBatteryMillivolts(65000) == UINT16_MAX); +} +''') + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_battery_calibration_store.py b/test/test_battery_calibration_store.py new file mode 100644 index 000000000..80d89cfb1 --- /dev/null +++ b/test/test_battery_calibration_store.py @@ -0,0 +1,168 @@ +"""Exercise the production calibration store and rollback across filesystems.""" +from pathlib import Path +import os +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] + +HARNESS = r''' +#include +#include +#include +#include +#include +#include +#include +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) +#define ATOMIC_FILE_WRITER_IMPLEMENTATION +#include +#endif +#include "store_under_test.cpp" + +struct Board : mesh::MainBoard { + bool capable = true; + uint16_t raw = 4000; + bool supportsBatteryCalibration() const override { return capable; } + uint16_t getBattMilliVolts() override { return calibrateBatteryMillivolts(raw); } + const char* getManufacturerName() const override { return "fixture"; } + uint8_t getStartupReason() const override { return 0; } + void reboot() override {} + void sleep(uint32_t) override {} +}; +int main() { + using namespace mesh::power; + MemoryFS fs; +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + fs.rename_replaces = true; +#endif + Board board; + assert(beginBatteryCalibrationStore(board, &fs)); + assert(fs.files.empty() && board.getAdcMultiplier() == 1); + char reply[160]; + auto run = [&](const char* command) { + strcpy(reply, "untouched"); + return handleStoredBatteryCalibrationCommand(board, command, reply, sizeof(reply)); + }; + assert(run("get adc.multiplier") && !strcmp(reply, "> 1.000")); + assert(!run("get adc.multiplierXYZ") && !strcmp(reply, "untouched")); + assert(run("battery calibrate 4.18") && strstr(reply, "(saved)")); + assert(fabs(board.getAdcMultiplier() - 1.045f) < 0.00001f); + assert(board.getBattMilliVolts() == 4180); + const auto good = fs.files.at("/battery_cal"); + assert(good.size() == 12); + Board rebooted; + assert(beginBatteryCalibrationStore(rebooted, &fs)); + assert(fabs(rebooted.getAdcMultiplier() - 1.045f) < 0.00001f); + fs.fail_write = true; + assert(run("set adc.multiplier 1.1") && strstr(reply, "save unconfirmed")); + assert(fabs(board.getAdcMultiplier() - 1.045f) < 0.00001f); + assert(fs.files.at("/battery_cal") == good); + fs.fail_write = false; + // Publication can succeed before verification fails. Report uncertainty + // and restore runtime without claiming the old disk image survived. + fs.fail_after_publication = true; + assert(run("set adc.multiplier 1.1") && strstr(reply, "save unconfirmed")); + assert(fabs(board.getAdcMultiplier() - 1.045f) < 0.00001f); + assert(fs.files.at("/battery_cal") != good); + fs.fail_after_publication = false; + fs.fail_read_open = false; + Board published; + assert(beginBatteryCalibrationStore(published, &fs)); + assert(fabs(published.getAdcMultiplier() - 1.1f) < 0.00001f); + fs.files["/battery_cal"] = good; + for (const char* command : {"set adc.multiplier nan", "set adc.multiplier 1junk", + "set adc.multiplier -1", "set adc.multiplier 1.6", + "get adc.multiplier junk", "battery calibrate 0"}) { + assert(run(command) && !strncmp(reply, "Error:", 6)); + assert(fs.files.at("/battery_cal") == good); + } + // Complete but corrupted records, truncations and invalid factors cannot + // change the factory conversion on a new instance. + for (size_t bit = 0; bit < good.size() * 8; ++bit) { + fs.files["/battery_cal"] = good; + fs.files["/battery_cal"][bit / 8] ^= uint8_t(1U << (bit % 8)); + Board fresh; + assert(!beginBatteryCalibrationStore(fresh, &fs)); + assert(fresh.getAdcMultiplier() == 1); + } + fs.files["/battery_cal"] = good; + fs.files["/battery_cal"].pop_back(); + Board truncated; + assert(!beginBatteryCalibrationStore(truncated, &fs)); + assert(truncated.getAdcMultiplier() == 1); + fs.files["/battery_cal"] = good; + assert(run("battery calibrate reset") && strstr(reply, "(saved)")); + assert(board.getAdcMultiplier() == 1); + Board reset; + assert(beginBatteryCalibrationStore(reset, &fs) && reset.getAdcMultiplier() == 1); + board.capable = false; + assert(run("get adc.multiplier") && strstr(reply, "unsupported")); + assert(run("battery calibrate 4.18") && strstr(reply, "unsupported")); + assert(run("set adc.multiplier 1.1") && strstr(reply, "unsupported")); + board.capable = true; +#if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM) + // Recover the old record if a reset interrupted the two-rename publication. + fs.files["/battery_cal.bak"] = good; + fs.files.erase("/battery_cal"); + Board recovered; + assert(beginBatteryCalibrationStore(recovered, &fs)); + assert(recovered.getBattMilliVolts() == 4180); +#endif + char bounded[4] = {'X', 'X', 'X', 'Z'}; + assert(handleStoredBatteryCalibrationCommand(board, "get adc.multiplier", bounded, 3)); + assert(bounded[2] == 0 && bounded[3] == 'Z'); + // A failed mount or volatile fallback must not claim reboot persistence. + assert(!beginBatteryCalibrationStore(board, nullptr)); + const float before_missing_fs = board.getAdcMultiplier(); + assert(run("battery calibrate 4.18") && strstr(reply, "save unconfirmed")); + assert(board.getAdcMultiplier() == before_missing_fs); +} +''' + + +class BatteryCalibrationStoreTests(unittest.TestCase): + def test_roundtrip_corruption_write_failure_reset_and_backup_recovery(self): + with tempfile.TemporaryDirectory(prefix="battery-calibration-store-") as temp: + work = Path(temp) + (work / "helpers").mkdir() + mock = ROOT / "test/fixtures/radio_profiles/mocks/helpers/IdentityStore.h" + mock_text = mock.read_text(encoding="ascii") + mock_text = mock_text.replace(" bool fail_read_open = false;", + " bool fail_read_open = false;\n bool fail_after_publication = false;") + mock_text = mock_text.replace(" files[to] = files[from]; files.erase(from); return true;", + " files[to] = files[from]; files.erase(from);\n" + " if (fail_after_publication && !strcmp(to, \"/battery_cal\")) fail_read_open = true;\n" + " return true;") + (work / "helpers/IdentityStore.h").write_text(mock_text, encoding="ascii") + # Redirect only the hardware boundary include. The transaction and + # calibration implementation bodies remain production code. + transaction = (ROOT / "src/helpers/ContactFileTransaction.h").read_text(encoding="ascii") + transaction = transaction.replace('#include "IdentityStore.h"', '#include ') + transaction = transaction.replace('#include "PersistentStoreFormat.h"', '#include ') + (work / "helpers/ContactFileTransaction.h").write_text(transaction, encoding="ascii") + store = (ROOT / "src/helpers/BatteryCalibrationStore.cpp").read_text(encoding="ascii") + for header in ("BatteryCalibrationStore.h", "BatteryCalibration.h", "CheckedSettingsFile.h", + "CLICommandUtils.h", "PersistentStoreFormat.h", "AtomicFileWriter.h", + "ContactFileTransaction.h", "NRF52VoltagePolicy.h"): + store = store.replace(f'#include "{header}"', f'#include ') + (work / "store_under_test.cpp").write_text(store, encoding="ascii") + (work / "test.cpp").write_text(HARNESS, encoding="ascii") + for platform in ("ESP32_PLATFORM", "RP2040_PLATFORM", "NRF52_PLATFORM", "STM32_PLATFORM"): + with self.subTest(platform=platform): + binary = work / platform + built = subprocess.run([ + os.environ.get("CXX", "g++"), "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror", + "-Wno-sign-compare", "-Wno-unused-parameter", "-fsanitize=address,undefined", "-fno-sanitize-recover=all", + "-fno-pie", "-no-pie", "-D" + platform, + "-I" + str(work), "-I" + str(ROOT / "src"), + str(work / "test.cpp"), "-o", str(binary), + ], capture_output=True, text=True, timeout=60) + self.assertEqual(built.returncode, 0, built.stdout + built.stderr) + result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=20) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_battery_forwarding.py b/test/test_battery_forwarding.py index 8fb9ed3e1..192b01135 100644 --- a/test/test_battery_forwarding.py +++ b/test/test_battery_forwarding.py @@ -101,6 +101,7 @@ struct Board { float multiplier = 1.0f; bool adcSupported = true; uint16_t getBattMilliVolts() { ++reads; return uint16_t(float(mv) * multiplier); } + float getAdcMultiplier() const { return adcSupported ? multiplier : 0.0f; } bool setAdcMultiplier(float value) { if (!adcSupported) return false; multiplier = value == 0.0f ? 1.0f : value; @@ -288,6 +289,75 @@ void CommonCLI::dispatchVoltage(char* command, char* reply) { assert(strcmp(reply, "fallthrough") == 0 && board.reads == 5); ''', prefix=prefix, flags=("-DNRF52_PLATFORM", "-DNRF52_POWER_MANAGEMENT")) + def test_actual_adc_get_set_fall_back_when_nrf52_voltage_policy_is_unavailable(self): + source = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="ascii") + setter = extract_braced(source, "void CommonCLI::handleSetCmd(") + getter = extract_braced(source, "void CommonCLI::handleGetCmd(") + def policy_route(method): + start = method.index(" const bool adc_multiplier_command =") + return method[start:method.index("#endif", start)] + set_branch = extract_braced(setter, 'if (memcmp(config, "adc.multiplier ", 15) == 0)') + get_branch = extract_braced(getter, 'if (configKeyEquals(config, "adc.multiplier"))') + prefix = self.runtime_prefix(r''' + struct Prefs { float adc_multiplier = 0.0f; } prefs; + Prefs* _prefs = &prefs; + unsigned prefsSaves = 0; + bool trySavePrefs() { ++prefsSaves; return true; } + bool applyAndSaveAdcMultiplier(float multiplier, char* reply, size_t capacity); + void dispatchSet(char* command, char* reply); + void dispatchGet(char* command, char* reply); +''') + prefix += r''' +#include +#include +namespace mesh { namespace power { +static bool policyAvailable = false; +static unsigned policyCalls = 0; +static bool voltagePolicySupported() { return policyAvailable; } +static bool handleVoltagePolicyCommand(const char*, char* reply, size_t capacity) { + ++policyCalls; + snprintf(reply, capacity, "%s", policyAvailable ? "> policy" : "Error: voltage policy unsupported"); + return true; +} +}} +''' + prefix += extract_braced(source, "static bool configKeyEquals(") + "\n" + prefix += extract_braced(source, "bool CommonCLI::applyAndSaveAdcMultiplier(") + "\n" + prefix += "void CommonCLI::dispatchSet(char* command, char* reply) {\n" + prefix += "const char* config = command + 4;\n" + policy_route(setter) + set_branch + "\n}\n" + prefix += "void CommonCLI::dispatchGet(char* command, char* reply) {\n" + prefix += "const char* config = command + 4;\n" + policy_route(getter) + get_branch + "\n}\n" + self.exercise(r''' + Board board; + CommonCLI cli(board); + char reply[160]; + char get[] = "get adc.multiplier"; + cli.dispatchGet(get, reply); + assert(!strcmp(reply, "> 1.000") && mesh::power::policyCalls == 0); + char set[] = "set adc.multiplier 1.2"; + cli.dispatchSet(set, reply); + assert(!strncmp(reply, "OK", 2) && cli.prefsSaves == 1); + assert(board.multiplier == 1.2f && cli.prefs.adc_multiplier == 1.2f); + assert(mesh::power::policyCalls == 0); + cli.dispatchGet(get, reply); + assert(!strcmp(reply, "> 1.200") && mesh::power::policyCalls == 0); + char profile[] = "set battery.profile lifepo4"; + cli.dispatchSet(profile, reply); + assert(strstr(reply, "voltage policy unsupported") && mesh::power::policyCalls == 1); + char full[] = "get battery.full"; + cli.dispatchGet(full, reply); + assert(strstr(reply, "voltage policy unsupported") && mesh::power::policyCalls == 2); + board.adcSupported = false; + cli.dispatchGet(get, reply); + assert(!strcmp(reply, "Error: unsupported") && mesh::power::policyCalls == 2); + cli.dispatchSet(set, reply); + assert(!strcmp(reply, "Error: unsupported") && mesh::power::policyCalls == 2); + board.adcSupported = true; + mesh::power::policyAvailable = true; + cli.dispatchGet(get, reply); + assert(!strcmp(reply, "> policy") && mesh::power::policyCalls == 3); +''', prefix=prefix, flags=("-DNRF52_PLATFORM", "-DNRF52_POWER_MANAGEMENT")) + def test_actual_set_command_adc_branch_resamples_success_and_skips_rejected_changes(self): source = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="ascii") setter = extract_braced(source, "void CommonCLI::handleSetCmd(") @@ -298,10 +368,13 @@ void CommonCLI::dispatchVoltage(char* command, char* reply) { struct Prefs { float adc_multiplier = 0.0f; } prefs; Prefs* _prefs = &prefs; unsigned prefsSaves = 0; - void savePrefs() { ++prefsSaves; } + bool prefsSaveOk = true; + bool trySavePrefs() { ++prefsSaves; return prefsSaveOk; } + bool applyAndSaveAdcMultiplier(float multiplier, char* reply, size_t capacity); void dispatchAdc(char* command, char* reply); ''') - prefix += "\nnamespace mesh { namespace cli {\n" + parser + "\n}}\n" + prefix += "\n#include \nnamespace mesh { namespace cli {\n" + parser + "\n}}\n" + prefix += extract_braced(source, "bool CommonCLI::applyAndSaveAdcMultiplier(") + "\n" prefix += "void CommonCLI::dispatchAdc(char* command, char* reply) {\n" prefix += "const char* config = &command[4];\n" + branch + "\n}\n" self.exercise(r''' @@ -329,10 +402,17 @@ void CommonCLI::dispatchVoltage(char* command, char* reply) { assert(strcmp(reply, "Error: unsupported") == 0 && cli.prefsSaves == 1 && board.reads == 2); assert(cli.prefs.adc_multiplier == 1.2f && cli.batteryAllowsForward(false)); board.adcSupported = true; + cli.prefsSaveOk = false; + char failed[] = "set adc.multiplier 1.0"; + cli.dispatchAdc(failed, reply); + assert(strstr(reply, "save unconfirmed") && cli.prefsSaves == 2 && board.reads == 2); + assert(cli.prefs.adc_multiplier == 1.2f && board.multiplier == 1.2f); + assert(cli.batteryAllowsForward(false)); + cli.prefsSaveOk = true; char reset[] = "set adc.multiplier 0"; cli.dispatchAdc(reset, reply); assert(strcmp(reply, "OK - using default board multiplier") == 0); - assert(cli.prefsSaves == 2 && board.reads == 3 && cli._battery_forward_state.percentage == 41); + assert(cli.prefsSaves == 3 && board.reads == 3 && cli._battery_forward_state.percentage == 41); assert(!cli.batteryAllowsForward(false)); ''', prefix=prefix, flags=("-DESP32_PLATFORM",)) diff --git a/test/test_heltec_r8_external_power.py b/test/test_heltec_r8_external_power.py index c130083fc..169aadb5b 100644 --- a/test/test_heltec_r8_external_power.py +++ b/test/test_heltec_r8_external_power.py @@ -30,6 +30,7 @@ inline uint32_t analogReadMilliVolts(int) { return 830; } BASE_BOARD = r''' #pragma once #include +#include #include class ESP32Board { protected: @@ -48,6 +49,7 @@ public: virtual uint16_t getBattMilliVolts() { return 0; } virtual bool isExternalPowered() { return false; } virtual bool isUsbHostConnected() { return false; } + virtual bool supportsBatteryCalibration() const { return false; } virtual bool setAdcMultiplier(float) { return false; } virtual float getAdcMultiplier() const { return 0; } virtual const char* getManufacturerName() const { return "test"; } diff --git a/test/test_stm32_companion_profile.py b/test/test_stm32_companion_profile.py index f4b3f082b..146592788 100644 --- a/test/test_stm32_companion_profile.py +++ b/test/test_stm32_companion_profile.py @@ -139,6 +139,7 @@ class RAK3x72BatteryScaleTest(unittest.TestCase): #include #include #include +#include #define PIN_VBAT_READ 7 #define ADC_MULTIPLIER @SCALE@ static unsigned reads, resolutions; @@ -151,7 +152,12 @@ int analogRead(int pin) { remaining -= value; return value; } -class Board { public: @BODY@ }; +class Board { + uint16_t calibrateBatteryMillivolts(uint16_t raw) const { + return mesh::power::scaleBatteryMillivolts(raw, 1.0f); + } +public: @BODY@ +}; static_assert(ADC_MULTIPLIER == 8650, "revisit the fixed-point scale if calibration changes"); static_assert(uint64_t(8) * 4095 * 8650 <= std::numeric_limits::max(), "battery accumulation product must not overflow"); @@ -172,7 +178,8 @@ int main() { cpp.write_text(source) binary = root / "battery" subprocess.run(["g++", "-std=c++17", "-Wall", "-Wextra", "-Werror", - *SANITIZER_FLAGS, str(cpp), "-o", str(binary)], check=True) + *SANITIZER_FLAGS, "-I", str(ROOT / "src"), + str(cpp), "-o", str(binary)], check=True) subprocess.run([str(binary)], check=True) diff --git a/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h b/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h index eba6a7176..827cf9de8 100644 --- a/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h +++ b/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h @@ -17,6 +17,8 @@ public: #define BATTERY_SAMPLES 8 + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); diff --git a/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h b/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h index bf64b19d8..9c93669d0 100644 --- a/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h +++ b/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h @@ -17,6 +17,8 @@ public: #define BATTERY_SAMPLES 8 + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); diff --git a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h index 33f51043b..59202f421 100644 --- a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h +++ b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h @@ -17,6 +17,8 @@ public: #define BATTERY_SAMPLES 8 + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); diff --git a/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.h b/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.h index 31ad7c809..ab82bc89d 100644 --- a/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.h +++ b/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.h @@ -17,6 +17,8 @@ public: #define BATTERY_SAMPLES 8 + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); diff --git a/variants/heltec_ct62/HT-CT62Board.h b/variants/heltec_ct62/HT-CT62Board.h index d26e7b264..91cba55a4 100644 --- a/variants/heltec_ct62/HT-CT62Board.h +++ b/variants/heltec_ct62/HT-CT62Board.h @@ -36,6 +36,14 @@ public: #endif } + bool supportsBatteryCalibration() const override { +#ifdef PIN_VBAT_READ + return true; +#else + return false; +#endif + } + uint16_t getBattMilliVolts() override { #ifdef PIN_VBAT_READ analogReadResolution(12); // ESP32-C3 ADC is 12-bit - 3.3/4096 (ref voltage/max counts) @@ -45,7 +53,7 @@ public: } raw = raw / 8; - return ((6.52 * raw) / 1024.0) * 1000; + return calibrateBatteryMillivolts(((6.52 * raw) / 1024.0) * 1000); #else return 0; // not supported #endif diff --git a/variants/heltec_e213/HeltecE213Board.cpp b/variants/heltec_e213/HeltecE213Board.cpp index e336aaa42..23dd516d7 100644 --- a/variants/heltec_e213/HeltecE213Board.cpp +++ b/variants/heltec_e213/HeltecE213Board.cpp @@ -33,7 +33,7 @@ void HeltecE213Board::begin() { digitalWrite(PIN_ADC_CTRL, LOW); - return (5.42 * (3.3 / 1024.0) * raw) * 1000; + return calibrateBatteryMillivolts((5.42 * (3.3 / 1024.0) * raw) * 1000); } const char* HeltecE213Board::getManufacturerName() const { diff --git a/variants/heltec_e213/HeltecE213Board.h b/variants/heltec_e213/HeltecE213Board.h index fadc038f9..005b59c51 100644 --- a/variants/heltec_e213/HeltecE213Board.h +++ b/variants/heltec_e213/HeltecE213Board.h @@ -12,6 +12,8 @@ public: HeltecE213Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { } void begin(); + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; }; diff --git a/variants/heltec_e290/HeltecE290Board.cpp b/variants/heltec_e290/HeltecE290Board.cpp index 2e491faf1..31c9763b7 100644 --- a/variants/heltec_e290/HeltecE290Board.cpp +++ b/variants/heltec_e290/HeltecE290Board.cpp @@ -33,7 +33,7 @@ void HeltecE290Board::begin() { digitalWrite(PIN_ADC_CTRL, LOW); - return (5.42 * (3.3 / 1024.0) * raw) * 1000; + return calibrateBatteryMillivolts((5.42 * (3.3 / 1024.0) * raw) * 1000); } const char* HeltecE290Board::getManufacturerName() const { diff --git a/variants/heltec_e290/HeltecE290Board.h b/variants/heltec_e290/HeltecE290Board.h index f287227c6..5553b4ec9 100644 --- a/variants/heltec_e290/HeltecE290Board.h +++ b/variants/heltec_e290/HeltecE290Board.h @@ -12,6 +12,8 @@ public: HeltecE290Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } void begin(); + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/heltec_rc32/HeltecRC32Board.cpp b/variants/heltec_rc32/HeltecRC32Board.cpp index 5fe3036cb..e7bd54f18 100644 --- a/variants/heltec_rc32/HeltecRC32Board.cpp +++ b/variants/heltec_rc32/HeltecRC32Board.cpp @@ -79,6 +79,7 @@ uint16_t HeltecRC32Board::getBattMilliVolts() { } bool HeltecRC32Board::setAdcMultiplier(float multiplier) { + if (!isfinite(multiplier) || multiplier < 0.0f || multiplier > 10.0f) return false; adc_mult = multiplier == 0.0f ? ADC_MULTIPLIER : multiplier; return true; } diff --git a/variants/heltec_rc32/HeltecRC32Board.h b/variants/heltec_rc32/HeltecRC32Board.h index 22f88b583..ed9f4ae48 100644 --- a/variants/heltec_rc32/HeltecRC32Board.h +++ b/variants/heltec_rc32/HeltecRC32Board.h @@ -23,6 +23,8 @@ public: void powerOff() override; void onBeforeTransmit() override; void onAfterTransmit() override; + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; bool setAdcMultiplier(float multiplier) override; float getAdcMultiplier() const override { return adc_mult; } diff --git a/variants/heltec_t096/T096Board.h b/variants/heltec_t096/T096Board.h index 1dd40fafd..346df88fe 100644 --- a/variants/heltec_t096/T096Board.h +++ b/variants/heltec_t096/T096Board.h @@ -28,6 +28,8 @@ public: void applyLedPolicy() override; void onBeforeTransmit(void) override; void onAfterTransmit(void) override; + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; void shutdownPeripherals() override; diff --git a/variants/heltec_t1/T1Board.h b/variants/heltec_t1/T1Board.h index 812bc7914..256992cc8 100644 --- a/variants/heltec_t1/T1Board.h +++ b/variants/heltec_t1/T1Board.h @@ -21,6 +21,8 @@ public: void applyLedPolicy() override; void onBeforeTransmit() override; void onAfterTransmit() override; + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override; void shutdownPeripherals() override; diff --git a/variants/heltec_t114/T114Board.h b/variants/heltec_t114/T114Board.h index e398066c3..fec498dcd 100644 --- a/variants/heltec_t114/T114Board.h +++ b/variants/heltec_t114/T114Board.h @@ -33,6 +33,8 @@ public: } #endif + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { int adcvalue = 0; analogReadResolution(12); diff --git a/variants/heltec_t190/HeltecT190Board.cpp b/variants/heltec_t190/HeltecT190Board.cpp index 9d8d46da7..18a775dc0 100644 --- a/variants/heltec_t190/HeltecT190Board.cpp +++ b/variants/heltec_t190/HeltecT190Board.cpp @@ -33,7 +33,7 @@ void HeltecT190Board::begin() { digitalWrite(PIN_ADC_CTRL, LOW); - return (5.42 * (3.3 / 1024.0) * raw) * 1000; + return calibrateBatteryMillivolts((5.42 * (3.3 / 1024.0) * raw) * 1000); } const char* HeltecT190Board::getManufacturerName() const { diff --git a/variants/heltec_t190/HeltecT190Board.h b/variants/heltec_t190/HeltecT190Board.h index 557c070ec..e636c047a 100644 --- a/variants/heltec_t190/HeltecT190Board.h +++ b/variants/heltec_t190/HeltecT190Board.h @@ -12,6 +12,8 @@ public: HeltecT190Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { } void begin(); + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/heltec_tower_v2/HeltecTowerV2Board.h b/variants/heltec_tower_v2/HeltecTowerV2Board.h index 375f1a485..5fe9dc112 100644 --- a/variants/heltec_tower_v2/HeltecTowerV2Board.h +++ b/variants/heltec_tower_v2/HeltecTowerV2Board.h @@ -19,6 +19,8 @@ public: void applyLedPolicy() override; void onBeforeTransmit() override; void onAfterTransmit() override; + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override; void powerOff() override; diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp index c7f0672fc..d422370c6 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp @@ -59,7 +59,7 @@ void HeltecTrackerV2Board::begin() { digitalWrite(PIN_ADC_CTRL, LOW); - return (5.42 * (3.3 / 1024.0) * raw) * 1000; + return calibrateBatteryMillivolts((5.42 * (3.3 / 1024.0) * raw) * 1000); } const char* HeltecTrackerV2Board::getManufacturerName() const { diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h index 7f00b30c5..5fd0fb8a3 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h @@ -21,6 +21,8 @@ public: void onBeforeTransmit(void) override; void onAfterTransmit(void) override; void shutdownPeripherals() override; + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; bool setLoRaFemLnaEnabled(bool enable) override; diff --git a/variants/heltec_v2/HeltecV2Board.h b/variants/heltec_v2/HeltecV2Board.h index 25e8d2f56..9921d2489 100644 --- a/variants/heltec_v2/HeltecV2Board.h +++ b/variants/heltec_v2/HeltecV2Board.h @@ -25,6 +25,8 @@ public: } } + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(10); @@ -34,7 +36,7 @@ public: } raw = raw / 8; - return (1.98 * (2 / 1024.0) * raw) * 1000; + return calibrateBatteryMillivolts((1.98 * (2 / 1024.0) * raw) * 1000); } const char* getManufacturerName() const override { diff --git a/variants/heltec_v3/HeltecV3Board.h b/variants/heltec_v3/HeltecV3Board.h index 17e5708d6..4ac88ac27 100644 --- a/variants/heltec_v3/HeltecV3Board.h +++ b/variants/heltec_v3/HeltecV3Board.h @@ -51,6 +51,8 @@ public: } } + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, adc_active_state); @@ -63,7 +65,7 @@ public: digitalWrite(PIN_ADC_CTRL, !adc_active_state); - return (ADC_MULTIPLIER * (3.3 / 1024.0) * raw) * 1000; + return calibrateBatteryMillivolts((ADC_MULTIPLIER * (3.3 / 1024.0) * raw) * 1000); } const char* getManufacturerName() const override { diff --git a/variants/heltec_v4/HeltecV4Board.h b/variants/heltec_v4/HeltecV4Board.h index 93f909770..9f66af39f 100644 --- a/variants/heltec_v4/HeltecV4Board.h +++ b/variants/heltec_v4/HeltecV4Board.h @@ -30,8 +30,11 @@ public: bool setLoRaFemLnaEnabled(bool enable) override; bool canControlLoRaFemLna() const override; bool isLoRaFemLnaEnabled() const override; + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; bool setAdcMultiplier(float multiplier) override { + if (!isfinite(multiplier) || multiplier < 0.0f || multiplier > 10.0f) return false; if (multiplier == 0.0f) { adc_mult = ADC_MULTIPLIER; } else { diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.h b/variants/heltec_v4_r8/HeltecV4R8Board.h index 555a4a2d9..acb744d13 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.h +++ b/variants/heltec_v4_r8/HeltecV4R8Board.h @@ -32,9 +32,12 @@ public: bool setLoRaFemLnaEnabled(bool enable) override; bool canControlLoRaFemLna() const override; bool isLoRaFemLnaEnabled() const override; + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; bool isExternalPowered() override; bool setAdcMultiplier(float multiplier) override { + if (!isfinite(multiplier) || multiplier < 0.0f || multiplier > 10.0f) return false; if (multiplier == 0.0f) { adc_mult = ADC_MULTIPLIER; } else { diff --git a/variants/ikoka_handheld_nrf/IkokaNrf52Board.h b/variants/ikoka_handheld_nrf/IkokaNrf52Board.h index 53e27b0d9..a97e98b58 100644 --- a/variants/ikoka_handheld_nrf/IkokaNrf52Board.h +++ b/variants/ikoka_handheld_nrf/IkokaNrf52Board.h @@ -25,6 +25,8 @@ public: } #endif + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { // Please read before going further ;) // https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging @@ -38,7 +40,7 @@ public: analogReference(AR_INTERNAL_3_0); delay(10); adcvalue = analogRead(PIN_VBAT); - 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/ikoka_nano_nrf/IkokaNanoNRFBoard.h b/variants/ikoka_nano_nrf/IkokaNanoNRFBoard.h index 92bf74627..ebb303448 100644 --- a/variants/ikoka_nano_nrf/IkokaNanoNRFBoard.h +++ b/variants/ikoka_nano_nrf/IkokaNanoNRFBoard.h @@ -33,6 +33,8 @@ public: } #endif + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { // Please read before going further ;) // https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging @@ -46,7 +48,7 @@ public: analogReference(AR_INTERNAL_3_0); delay(10); adcvalue = analogRead(PIN_VBAT); - 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/ikoka_stick_nrf/IkokaStickNRFBoard.h b/variants/ikoka_stick_nrf/IkokaStickNRFBoard.h index b664861bb..99861a456 100644 --- a/variants/ikoka_stick_nrf/IkokaStickNRFBoard.h +++ b/variants/ikoka_stick_nrf/IkokaStickNRFBoard.h @@ -33,6 +33,8 @@ public: } #endif + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { // Please read before going further ;) // https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging @@ -46,7 +48,7 @@ public: analogReference(AR_INTERNAL_3_0); delay(10); adcvalue = analogRead(PIN_VBAT); - 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/keepteen_lt1/KeepteenLT1Board.h b/variants/keepteen_lt1/KeepteenLT1Board.h index d32dfcd23..84254579c 100644 --- a/variants/keepteen_lt1/KeepteenLT1Board.h +++ b/variants/keepteen_lt1/KeepteenLT1Board.h @@ -14,6 +14,8 @@ public: #define BATTERY_SAMPLES 8 + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); @@ -22,7 +24,7 @@ public: raw += analogRead(PIN_VBAT_READ); } raw = raw / BATTERY_SAMPLES; - return (ADC_MULTIPLIER * raw); + return calibrateBatteryMillivolts((ADC_MULTIPLIER * raw)); } const char* getManufacturerName() const override { diff --git a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h index 136601d06..e8cce7908 100644 --- a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h +++ b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h @@ -26,6 +26,8 @@ public: } #endif + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { // enable battery voltage measurement digitalWrite(BATTERY_MEASUREMENT_CONTROL, HIGH); @@ -45,7 +47,7 @@ public: // disable battery voltage measurement digitalWrite(BATTERY_MEASUREMENT_CONTROL, LOW); - return ((adcvalue * ((3000.0 / 4096.0)))) * 2.0; + return calibrateBatteryMillivolts(((adcvalue * ((3000.0 / 4096.0)))) * 2.0); } const char* getManufacturerName() const override { diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp index cad4f09c3..f9af10307 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp @@ -126,8 +126,8 @@ uint16_t TBeam1WBoard::getBattMilliVolts() { delayMicroseconds(100); } raw /= BATTERY_SENSE_SAMPLES; - return static_cast( - (static_cast(raw) / 4095.0f) * 3300.0f * ADC_MULTIPLIER); + return calibrateBatteryMillivolts(static_cast( + (static_cast(raw) / 4095.0f) * 3300.0f * ADC_MULTIPLIER)); } const char* TBeam1WBoard::getManufacturerName() const { diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.h b/variants/lilygo_tbeam_1w/TBeam1WBoard.h index 9c8db6346..6a0f3a574 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.h +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.h @@ -74,6 +74,8 @@ public: void applyLedPolicy() override; void onBeforeTransmit() override; void onAfterTransmit() override; + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override; void powerOff() override; diff --git a/variants/lilygo_tdeck/TDeckBoard.h b/variants/lilygo_tdeck/TDeckBoard.h index 48e9dfd03..0d80e88ab 100644 --- a/variants/lilygo_tdeck/TDeckBoard.h +++ b/variants/lilygo_tdeck/TDeckBoard.h @@ -26,6 +26,8 @@ public: } #endif + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() { #if defined(PIN_VBAT_READ) && defined(ADC_MULTIPLIER) analogReadResolution(12); @@ -36,7 +38,7 @@ public: } raw = raw / BATTERY_SAMPLES; - return (ADC_MULTIPLIER * raw) / 4096; + return calibrateBatteryMillivolts((ADC_MULTIPLIER * raw) / 4096); #else return 0; #endif diff --git a/variants/lilygo_techo/TechoBoard.cpp b/variants/lilygo_techo/TechoBoard.cpp index 2edbb6a3e..c763aaec8 100644 --- a/variants/lilygo_techo/TechoBoard.cpp +++ b/variants/lilygo_techo/TechoBoard.cpp @@ -26,6 +26,6 @@ uint16_t TechoBoard::getBattMilliVolts() { adcvalue = analogRead(PIN_VBAT_READ); // Convert the raw value to compensated mv, taking the resistor- // divider into account (providing the actual LIPO voltage) - return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB); + return calibrateBatteryMillivolts((uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB)); } #endif diff --git a/variants/lilygo_techo/TechoBoard.h b/variants/lilygo_techo/TechoBoard.h index 867fc24cf..d0ea861ab 100644 --- a/variants/lilygo_techo/TechoBoard.h +++ b/variants/lilygo_techo/TechoBoard.h @@ -17,6 +17,8 @@ class TechoBoard : public NRF52BoardDCDC { public: TechoBoard() : NRF52Board("TECHO_OTA") {} void begin(); + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override { diff --git a/variants/lilygo_techo_card/TechoCardBoard.cpp b/variants/lilygo_techo_card/TechoCardBoard.cpp index 86dbf53db..dc6d440bd 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.cpp +++ b/variants/lilygo_techo_card/TechoCardBoard.cpp @@ -54,7 +54,7 @@ uint16_t TechoCardBoard::getBattMilliVolts() { // Convert the raw value to compensated mv, taking the resistor- // divider into account (providing the actual LIPO voltage) - return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB); + return calibrateBatteryMillivolts((uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB)); } void TechoCardBoard::applyLedPolicy() { diff --git a/variants/lilygo_techo_card/TechoCardBoard.h b/variants/lilygo_techo_card/TechoCardBoard.h index 9fd6935df..f192bd7e5 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.h +++ b/variants/lilygo_techo_card/TechoCardBoard.h @@ -19,6 +19,8 @@ class TechoCardBoard : public NRF52BoardDCDC { public: TechoCardBoard() : NRF52Board("TECHO_OTA") {} void begin(); + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; void applyLedPolicy() override; void onBeforeTransmit(void) override; diff --git a/variants/lilygo_techo_lite/TechoBoard.cpp b/variants/lilygo_techo_lite/TechoBoard.cpp index e0b69f985..714ba5b2d 100644 --- a/variants/lilygo_techo_lite/TechoBoard.cpp +++ b/variants/lilygo_techo_lite/TechoBoard.cpp @@ -49,6 +49,6 @@ uint16_t TechoBoard::getBattMilliVolts() { // = adc * 0.73242188 * 2.0 = adc * 1.46484375 uint16_t millivolts = (uint16_t)((float)adc * (3000.0f / 4096.0f) * 2.0f); - return millivolts; + return calibrateBatteryMillivolts(millivolts); } #endif \ No newline at end of file diff --git a/variants/lilygo_techo_lite/TechoBoard.h b/variants/lilygo_techo_lite/TechoBoard.h index 055beac87..e32635254 100644 --- a/variants/lilygo_techo_lite/TechoBoard.h +++ b/variants/lilygo_techo_lite/TechoBoard.h @@ -15,6 +15,8 @@ class TechoBoard : public NRF52BoardDCDC { public: TechoBoard() : NRF52Board("TECHO_OTA") {} void begin(); + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override { diff --git a/variants/lilygo_tlora_v2_1/LilyGoTLoraBoard.h b/variants/lilygo_tlora_v2_1/LilyGoTLoraBoard.h index f126f0068..5dcdc82d0 100644 --- a/variants/lilygo_tlora_v2_1/LilyGoTLoraBoard.h +++ b/variants/lilygo_tlora_v2_1/LilyGoTLoraBoard.h @@ -10,6 +10,8 @@ public: return "LILYGO T-LoRa V2.1-1.6"; } + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); @@ -19,7 +21,7 @@ public: } raw = raw / 8; - return (2 * raw); + return calibrateBatteryMillivolts((2 * raw)); } uint32_t getIRQGpio() override { diff --git a/variants/mesh_pocket/MeshPocket.h b/variants/mesh_pocket/MeshPocket.h index e215eb776..0d2cf2208 100644 --- a/variants/mesh_pocket/MeshPocket.h +++ b/variants/mesh_pocket/MeshPocket.h @@ -14,6 +14,8 @@ public: HeltecMeshPocket() : NRF52Board("MESH_POCKET_OTA") {} void begin(); + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { int adcvalue = 0; analogReadResolution(12); @@ -26,7 +28,7 @@ public: adcvalue = analogRead(PIN_VBAT_READ); digitalWrite(PIN_BAT_CTL, LOW); - 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/meshadventurer/MeshadventurerBoard.h b/variants/meshadventurer/MeshadventurerBoard.h index a3a78d2fc..7dfecbc00 100644 --- a/variants/meshadventurer/MeshadventurerBoard.h +++ b/variants/meshadventurer/MeshadventurerBoard.h @@ -34,6 +34,8 @@ public: } } + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); @@ -43,7 +45,7 @@ public: } raw = raw / 4; - return (2 * raw); + return calibrateBatteryMillivolts((2 * raw)); } const char* getManufacturerName() const override { diff --git a/variants/meshnology_w12/MeshnologyW12Board.h b/variants/meshnology_w12/MeshnologyW12Board.h index f08acc1e3..4e18b9b4c 100644 --- a/variants/meshnology_w12/MeshnologyW12Board.h +++ b/variants/meshnology_w12/MeshnologyW12Board.h @@ -28,8 +28,11 @@ public: void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); void powerOff() override; bool prepareRadioFrequency(float frequency) override; + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; bool setAdcMultiplier(float multiplier) override { + if (!isfinite(multiplier) || multiplier < 0.0f || multiplier > 10.0f) return false; if (multiplier == 0.0f) { adc_mult = ADC_MULTIPLIER; } else { diff --git a/variants/meshtiny/MeshtinyBoard.h b/variants/meshtiny/MeshtinyBoard.h index e5841e8b5..a0cb28623 100644 --- a/variants/meshtiny/MeshtinyBoard.h +++ b/variants/meshtiny/MeshtinyBoard.h @@ -26,13 +26,15 @@ public: } #endif + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { int adcvalue = 0; analogReadResolution(12); analogReference(AR_INTERNAL_3_0); delay(10); adcvalue = analogRead(PIN_VBAT_READ); - return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; + return calibrateBatteryMillivolts((adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096); } const char *getManufacturerName() const override { return "Meshtiny"; } diff --git a/variants/meshtracker_x1/MeshTrackerX1Board.h b/variants/meshtracker_x1/MeshTrackerX1Board.h index 99ae1a6be..d22848a99 100644 --- a/variants/meshtracker_x1/MeshTrackerX1Board.h +++ b/variants/meshtracker_x1/MeshTrackerX1Board.h @@ -12,6 +12,14 @@ public: MeshTrackerX1Board() : NRF52Board("X1_OTA") {} void begin(); + bool supportsBatteryCalibration() const override { +#ifdef BATTERY_PIN + return true; +#else + return false; +#endif + } + uint16_t getBattMilliVolts() override { #ifdef BATTERY_PIN #ifdef PIN_BAT_ADC_EN @@ -32,7 +40,7 @@ public: if (raw < 0 || raw > 4095) return 0; float volts = (raw * ADC_MULTIPLIER * AREF_VOLTAGE) / 4096; - return volts * 1000; + return calibrateBatteryMillivolts(volts * 1000); #else return 0; #endif diff --git a/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h b/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h index 5ce5cad20..2d9896fc3 100644 --- a/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h +++ b/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h @@ -30,6 +30,8 @@ public: #define BATTERY_SAMPLES 8 + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); @@ -38,7 +40,7 @@ public: raw += analogRead(PIN_VBAT_READ); } raw = raw / BATTERY_SAMPLES; - return (ADC_MULTIPLIER * raw); + return calibrateBatteryMillivolts(ADC_MULTIPLIER * raw); } const char* getManufacturerName() const override { diff --git a/variants/muzi_base/MuziBaseBoard.h b/variants/muzi_base/MuziBaseBoard.h index 02340ce09..e24a64652 100644 --- a/variants/muzi_base/MuziBaseBoard.h +++ b/variants/muzi_base/MuziBaseBoard.h @@ -27,6 +27,8 @@ public: #define BATTERY_SAMPLES 8 + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); analogReference(AR_INTERNAL_3_0); diff --git a/variants/muziworks_r1_neo/R1NeoBoard.h b/variants/muziworks_r1_neo/R1NeoBoard.h index 2e21aa2cb..43ee0a078 100644 --- a/variants/muziworks_r1_neo/R1NeoBoard.h +++ b/variants/muziworks_r1_neo/R1NeoBoard.h @@ -49,6 +49,8 @@ public: #define BATTERY_SAMPLES 8 + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { MESH_DEBUG_PRINTLN("R1Neo: Sampling battery"); analogReadResolution(12); diff --git a/variants/nano_g2_ultra/nano-g2.cpp b/variants/nano_g2_ultra/nano-g2.cpp index 2fced88ff..12eb44128 100644 --- a/variants/nano_g2_ultra/nano-g2.cpp +++ b/variants/nano_g2_ultra/nano-g2.cpp @@ -37,6 +37,6 @@ uint16_t NanoG2Ultra::getBattMilliVolts() adcvalue = analogRead(PIN_VBAT_READ); // Convert the raw value to compensated mv, taking the resistor- // divider into account (providing the actual LIPO voltage) - return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB); + return calibrateBatteryMillivolts((uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB)); } #endif diff --git a/variants/nano_g2_ultra/nano-g2.h b/variants/nano_g2_ultra/nano-g2.h index 4ec530486..0ec6cfeeb 100644 --- a/variants/nano_g2_ultra/nano-g2.h +++ b/variants/nano_g2_ultra/nano-g2.h @@ -39,6 +39,8 @@ class NanoG2Ultra : public NRF52BoardDCDC { public: NanoG2Ultra() : NRF52Board("NANO_G2_OTA") {} void begin(); + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; const char *getManufacturerName() const override { return "Nano G2 Ultra"; } diff --git a/variants/promicro/PromicroBoard.h b/variants/promicro/PromicroBoard.h index ee16c9654..195e662c4 100644 --- a/variants/promicro/PromicroBoard.h +++ b/variants/promicro/PromicroBoard.h @@ -18,6 +18,8 @@ public: #define BATTERY_SAMPLES 8 + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); @@ -30,6 +32,7 @@ public: } bool setAdcMultiplier(float multiplier) override { + if (!isfinite(multiplier) || multiplier < 0.0f || multiplier > 10.0f) return false; if (multiplier == 0.0f) { adc_mult = ADC_MULTIPLIER;} else { diff --git a/variants/rak11310/RAK11310Board.h b/variants/rak11310/RAK11310Board.h index f58d424d4..0df08f291 100644 --- a/variants/rak11310/RAK11310Board.h +++ b/variants/rak11310/RAK11310Board.h @@ -32,6 +32,8 @@ public: void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); } #endif + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { #if defined(PIN_VBAT_READ) && defined(ADC_MULTIPLIER) analogReadResolution(12); @@ -42,7 +44,7 @@ public: } raw = raw / BATTERY_SAMPLES; - return (ADC_MULTIPLIER * raw); + return calibrateBatteryMillivolts((ADC_MULTIPLIER * raw)); #else return 0; #endif diff --git a/variants/rak3112/RAK3112Board.h b/variants/rak3112/RAK3112Board.h index c06aaad71..8d03b4ad8 100644 --- a/variants/rak3112/RAK3112Board.h +++ b/variants/rak3112/RAK3112Board.h @@ -50,6 +50,8 @@ public: } } + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); @@ -59,7 +61,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 1bb4c65c8..74764501a 100644 --- a/variants/rak3401/RAK3401Board.h +++ b/variants/rak3401/RAK3401Board.h @@ -31,6 +31,8 @@ public: #define BATTERY_SAMPLES 8 + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); diff --git a/variants/rak3x72/target.h b/variants/rak3x72/target.h index ed9018022..0839bb266 100644 --- a/variants/rak3x72/target.h +++ b/variants/rak3x72/target.h @@ -23,6 +23,8 @@ public: return "RAK 3x72"; } + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); uint32_t raw = 0; @@ -32,7 +34,7 @@ public: // The eight 12-bit samples sum to at most 32760. This is the exact // previous 8650 / 32768 scale and truncation without double helpers; // the largest product (283374000) fits in uint32_t. - return (uint16_t)((raw * 8650UL) >> 15); + return calibrateBatteryMillivolts((uint16_t)((raw * 8650UL) >> 15)); } void setGpio(uint32_t values) override { diff --git a/variants/rak4631/RAK4631Board.h b/variants/rak4631/RAK4631Board.h index a08ec4298..a18649dc9 100644 --- a/variants/rak4631/RAK4631Board.h +++ b/variants/rak4631/RAK4631Board.h @@ -21,6 +21,8 @@ public: #define BATTERY_SAMPLES 8 + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); diff --git a/variants/rak_wismesh_tag/RAKWismeshTagBoard.h b/variants/rak_wismesh_tag/RAKWismeshTagBoard.h index 97aa0261f..fc308310b 100644 --- a/variants/rak_wismesh_tag/RAKWismeshTagBoard.h +++ b/variants/rak_wismesh_tag/RAKWismeshTagBoard.h @@ -29,6 +29,8 @@ public: #define BATTERY_SAMPLES 8 + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); @@ -38,7 +40,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/rpi_picow/PicoWBoard.h b/variants/rpi_picow/PicoWBoard.h index 72d8d834a..4ebdddd1a 100644 --- a/variants/rpi_picow/PicoWBoard.h +++ b/variants/rpi_picow/PicoWBoard.h @@ -33,6 +33,8 @@ public: #define BATTERY_SAMPLES 8 + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); @@ -42,7 +44,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 b2342b963..28afd3a25 100644 --- a/variants/sensecap_solar/SenseCapSolarBoard.h +++ b/variants/sensecap_solar/SenseCapSolarBoard.h @@ -28,6 +28,8 @@ public: } #endif + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { digitalWrite(VBAT_ENABLE, LOW); int adcvalue = 0; diff --git a/variants/station_g2/StationG2Board.h b/variants/station_g2/StationG2Board.h index d265bbdd0..6804cf5be 100644 --- a/variants/station_g2/StationG2Board.h +++ b/variants/station_g2/StationG2Board.h @@ -21,6 +21,8 @@ public: } } + bool supportsBatteryCalibration() const override { return false; } + uint16_t getBattMilliVolts() override { return 0; } diff --git a/variants/station_g3_esp32/StationG3Board.h b/variants/station_g3_esp32/StationG3Board.h index e84fc4756..f1623eba2 100644 --- a/variants/station_g3_esp32/StationG3Board.h +++ b/variants/station_g3_esp32/StationG3Board.h @@ -76,6 +76,8 @@ public: void shutdownPeripherals() override; + bool supportsBatteryCalibration() const override { return false; } + uint16_t getBattMilliVolts() override { return 0; } diff --git a/variants/t1000-e/T1000eBoard.h b/variants/t1000-e/T1000eBoard.h index f0b9dd7dd..e47786d9c 100644 --- a/variants/t1000-e/T1000eBoard.h +++ b/variants/t1000-e/T1000eBoard.h @@ -12,6 +12,14 @@ public: T1000eBoard() : NRF52Board("T1000E_OTA") {} void begin(); + bool supportsBatteryCalibration() const override { +#ifdef BATTERY_PIN + return true; +#else + return false; +#endif + } + uint16_t getBattMilliVolts() override { #ifdef BATTERY_PIN #ifdef PIN_3V3_EN @@ -28,7 +36,7 @@ public: analogReference(AR_DEFAULT); // put back to default analogReadResolution(10); - return volts * 1000; + return calibrateBatteryMillivolts(volts * 1000); #else return 0; #endif diff --git a/variants/thinknode_m1/ThinkNodeM1Board.cpp b/variants/thinknode_m1/ThinkNodeM1Board.cpp index 45449bafc..97e566397 100644 --- a/variants/thinknode_m1/ThinkNodeM1Board.cpp +++ b/variants/thinknode_m1/ThinkNodeM1Board.cpp @@ -31,6 +31,6 @@ uint16_t ThinkNodeM1Board::getBattMilliVolts() { adcvalue = analogRead(PIN_VBAT_READ); // Convert the raw value to compensated mv, taking the resistor- // divider into account (providing the actual LIPO voltage) - return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB); + return calibrateBatteryMillivolts((uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB)); } #endif diff --git a/variants/thinknode_m1/ThinkNodeM1Board.h b/variants/thinknode_m1/ThinkNodeM1Board.h index f3eb8ed66..6bbd304d3 100644 --- a/variants/thinknode_m1/ThinkNodeM1Board.h +++ b/variants/thinknode_m1/ThinkNodeM1Board.h @@ -17,6 +17,8 @@ class ThinkNodeM1Board : public NRF52Board { public: ThinkNodeM1Board() : NRF52Board("THINKNODE_M1_OTA") {} void begin(); + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; #if defined(P_LORA_TX_LED) diff --git a/variants/thinknode_m2/ThinknodeM2Board.cpp b/variants/thinknode_m2/ThinknodeM2Board.cpp index 8d68006d2..e0d7e1245 100644 --- a/variants/thinknode_m2/ThinknodeM2Board.cpp +++ b/variants/thinknode_m2/ThinknodeM2Board.cpp @@ -22,7 +22,7 @@ uint16_t ThinknodeM2Board::getBattMilliVolts() { mv /= 8; analogReadResolution(10); - return static_cast(mv * ADC_MULTIPLIER); + return calibrateBatteryMillivolts(static_cast(mv * ADC_MULTIPLIER)); } const char *ThinknodeM2Board::getManufacturerName() const { diff --git a/variants/thinknode_m2/ThinknodeM2Board.h b/variants/thinknode_m2/ThinknodeM2Board.h index 025567778..0ef8daa9d 100644 --- a/variants/thinknode_m2/ThinknodeM2Board.h +++ b/variants/thinknode_m2/ThinknodeM2Board.h @@ -8,6 +8,8 @@ class ThinknodeM2Board : public ESP32Board { public: void begin(); + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/thinknode_m3/ThinkNodeM3Board.cpp b/variants/thinknode_m3/ThinkNodeM3Board.cpp index ac513ade5..3f5eb8e7f 100644 --- a/variants/thinknode_m3/ThinkNodeM3Board.cpp +++ b/variants/thinknode_m3/ThinkNodeM3Board.cpp @@ -24,5 +24,5 @@ uint16_t ThinkNodeM3Board::getBattMilliVolts() { adcvalue = analogRead(PIN_VBAT_READ); // Convert the raw value to compensated mv, taking the resistor- // divider into account (providing the actual LIPO voltage) - return (uint16_t)((float)adcvalue * ADC_FACTOR); + return calibrateBatteryMillivolts((uint16_t)((float)adcvalue * ADC_FACTOR)); } diff --git a/variants/thinknode_m3/ThinkNodeM3Board.h b/variants/thinknode_m3/ThinkNodeM3Board.h index 2584fd4cb..87922be82 100644 --- a/variants/thinknode_m3/ThinkNodeM3Board.h +++ b/variants/thinknode_m3/ThinkNodeM3Board.h @@ -16,6 +16,8 @@ protected: public: ThinkNodeM3Board() : NRF52Board("THINKNODE_M3_OTA") {} void begin(); + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; #ifdef P_LORA_TX_LED diff --git a/variants/thinknode_m5/ThinknodeM5Board.cpp b/variants/thinknode_m5/ThinknodeM5Board.cpp index 2cb138e63..640c554a6 100644 --- a/variants/thinknode_m5/ThinknodeM5Board.cpp +++ b/variants/thinknode_m5/ThinknodeM5Board.cpp @@ -31,7 +31,7 @@ void ThinknodeM5Board::begin() { mv /= 8; analogReadResolution(10); - return static_cast(mv * ADC_MULTIPLIER ); + return calibrateBatteryMillivolts(static_cast(mv * ADC_MULTIPLIER )); } const char* ThinknodeM5Board::getManufacturerName() const { diff --git a/variants/thinknode_m5/ThinknodeM5Board.h b/variants/thinknode_m5/ThinknodeM5Board.h index eb2ea3394..fecf796f7 100644 --- a/variants/thinknode_m5/ThinknodeM5Board.h +++ b/variants/thinknode_m5/ThinknodeM5Board.h @@ -12,6 +12,8 @@ class ThinknodeM5Board : public ESP32Board { public: void begin(); + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/thinknode_m6/ThinkNodeM6Board.cpp b/variants/thinknode_m6/ThinkNodeM6Board.cpp index f83f274f9..cfbe7ee11 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.cpp +++ b/variants/thinknode_m6/ThinkNodeM6Board.cpp @@ -34,6 +34,6 @@ uint16_t ThinkNodeM6Board::getBattMilliVolts() { digitalWrite(PIN_ADC_CTRL, LOW); // Convert the raw value to compensated mv, taking the resistor- // divider into account (providing the actual LIPO voltage) - return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB); + return calibrateBatteryMillivolts((uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB)); } #endif diff --git a/variants/thinknode_m6/ThinkNodeM6Board.h b/variants/thinknode_m6/ThinkNodeM6Board.h index b9cbdd003..8c5a0538b 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.h +++ b/variants/thinknode_m6/ThinkNodeM6Board.h @@ -21,6 +21,8 @@ protected: public: ThinkNodeM6Board() : NRF52Board("THINKNODE_M6_OTA") {} void begin(); + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; #if defined(P_LORA_TX_LED) diff --git a/variants/thinknode_m8/ThinkNodeM8Board.cpp b/variants/thinknode_m8/ThinkNodeM8Board.cpp index 2ca393d98..ce576f75d 100644 --- a/variants/thinknode_m8/ThinkNodeM8Board.cpp +++ b/variants/thinknode_m8/ThinkNodeM8Board.cpp @@ -33,6 +33,6 @@ uint16_t ThinkNodeM8Board::getBattMilliVolts() { // Convert the raw value to compensated mv, taking the resistor- // divider into account (providing the actual LIPO voltage) digitalWrite(ADC_EN, LOW); - return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB); + return calibrateBatteryMillivolts((uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB)); } #endif diff --git a/variants/thinknode_m8/ThinkNodeM8Board.h b/variants/thinknode_m8/ThinkNodeM8Board.h index 7f38ba53a..d603587a3 100644 --- a/variants/thinknode_m8/ThinkNodeM8Board.h +++ b/variants/thinknode_m8/ThinkNodeM8Board.h @@ -17,6 +17,8 @@ class ThinkNodeM8Board : public NRF52Board { public: ThinkNodeM8Board() : NRF52Board("THINKNODE_M8_OTA") {} void begin(); + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; #if defined(P_LORA_TX_LED) diff --git a/variants/tiny_relay/target.h b/variants/tiny_relay/target.h index 4e2e4afa7..9e82f3230 100644 --- a/variants/tiny_relay/target.h +++ b/variants/tiny_relay/target.h @@ -23,6 +23,8 @@ class TinyRelayBoard : public STM32Board const char *getManufacturerName() const override { return "Tiny Relay"; } + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); @@ -30,7 +32,7 @@ class TinyRelayBoard : public STM32Board for (int i = 0; i < 8; i++) { raw += analogRead(PIN_VBAT_READ); } - return ((double)raw) * ADC_MULTIPLIER / 8 / 4096; + return calibrateBatteryMillivolts(((double)raw) * ADC_MULTIPLIER / 8 / 4096); } void setGpio(uint32_t values) override diff --git a/variants/waveshare_rp2040_lora/WaveshareBoard.h b/variants/waveshare_rp2040_lora/WaveshareBoard.h index b1bdca56b..e3e77fa38 100644 --- a/variants/waveshare_rp2040_lora/WaveshareBoard.h +++ b/variants/waveshare_rp2040_lora/WaveshareBoard.h @@ -44,6 +44,8 @@ public: void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); } #endif + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { #if defined(PIN_VBAT_READ) && defined(ADC_MULTIPLIER) analogReadResolution(12); @@ -54,7 +56,7 @@ public: } raw = raw / BATTERY_SAMPLES; - return (ADC_MULTIPLIER * raw) / 4096; + return calibrateBatteryMillivolts((ADC_MULTIPLIER * raw) / 4096); #else return 0; #endif diff --git a/variants/wio-e5-mini/platformio.ini b/variants/wio-e5-mini/platformio.ini index 747958315..40c0b4fb6 100644 --- a/variants/wio-e5-mini/platformio.ini +++ b/variants/wio-e5-mini/platformio.ini @@ -51,6 +51,7 @@ build_flags = ${lora_e5_mini.build_flags} -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-schedule-insns2 + -fno-guess-branch-probability ; measured flash saving; retain calibration and routing -Wl,--sort-section=alignment -D RADIO_LIVENESS_SOFT_ONLY=1 ; integrated STM32WL has no separate radio-reset pin -D WIO_E5_MINI_NO_EXTERNAL_SENSORS=1 ; the dedicated sensor env retains BME280 support diff --git a/variants/wio-e5-mini/target.h b/variants/wio-e5-mini/target.h index fe3b5b195..599c1cbff 100644 --- a/variants/wio-e5-mini/target.h +++ b/variants/wio-e5-mini/target.h @@ -34,6 +34,8 @@ public: return "Seeed Wio E5 mini"; } + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { analogReadResolution(12); uint32_t raw = 0; @@ -43,7 +45,7 @@ public: // 1.73 * 5 V over eight 12-bit samples is raw * 8650 / 32768. // Fixed-point math avoids pulling double-precision helpers into this // flash-constrained STM32WL build; the half-divisor rounds to nearest. - return (uint16_t)((raw * 8650UL + 16384UL) >> 15); + return calibrateBatteryMillivolts((uint16_t)((raw * 8650UL + 16384UL) >> 15)); } }; diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h index bb1e314e7..ab45498bb 100644 --- a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h @@ -32,13 +32,15 @@ public: } #endif + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { int adcvalue = 0; analogReadResolution(12); analogReference(AR_INTERNAL); delay(10); adcvalue = analogRead(PIN_VBAT_READ); - 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/wio-tracker-l1/WioTrackerL1Board.h b/variants/wio-tracker-l1/WioTrackerL1Board.h index efa232fa7..a5372bf18 100644 --- a/variants/wio-tracker-l1/WioTrackerL1Board.h +++ b/variants/wio-tracker-l1/WioTrackerL1Board.h @@ -26,13 +26,15 @@ public: } #endif + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { int adcvalue = 0; analogReadResolution(12); analogReference(AR_INTERNAL); delay(10); adcvalue = analogRead(PIN_VBAT_READ); - 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/wio-tracker-l2/WioTrackerL2Board.cpp b/variants/wio-tracker-l2/WioTrackerL2Board.cpp index f521d3150..929faa217 100644 --- a/variants/wio-tracker-l2/WioTrackerL2Board.cpp +++ b/variants/wio-tracker-l2/WioTrackerL2Board.cpp @@ -204,5 +204,5 @@ uint16_t WioTrackerL2Board::getBattMilliVolts() { int16_t raw = adsReadRaw(); if (raw < 0) return 0; - return (uint16_t)(raw * ADS_MV_PER_LSB); + return calibrateBatteryMillivolts((uint16_t)(raw * ADS_MV_PER_LSB)); } diff --git a/variants/wio-tracker-l2/WioTrackerL2Board.h b/variants/wio-tracker-l2/WioTrackerL2Board.h index 3a7f8ecbe..355733501 100644 --- a/variants/wio-tracker-l2/WioTrackerL2Board.h +++ b/variants/wio-tracker-l2/WioTrackerL2Board.h @@ -41,6 +41,8 @@ class WioTrackerL2Board : public ESP32Board { public: void begin(); + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override { diff --git a/variants/wio_wm1110/WioWM1110Board.h b/variants/wio_wm1110/WioWM1110Board.h index 310b72255..b479e31f9 100644 --- a/variants/wio_wm1110/WioWM1110Board.h +++ b/variants/wio_wm1110/WioWM1110Board.h @@ -31,13 +31,15 @@ public: } #endif + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { int adcvalue = 0; analogReadResolution(12); analogReference(AR_INTERNAL_3_0); delay(10); adcvalue = analogRead(BATTERY_PIN); - return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE * 1000.0) / 4096.0; + return calibrateBatteryMillivolts((adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE * 1000.0) / 4096.0); } const char* getManufacturerName() const override { diff --git a/variants/xiao_c3/XiaoC3Board.h b/variants/xiao_c3/XiaoC3Board.h index 67826716a..4ea6264b4 100644 --- a/variants/xiao_c3/XiaoC3Board.h +++ b/variants/xiao_c3/XiaoC3Board.h @@ -59,6 +59,14 @@ public: } #endif + bool supportsBatteryCalibration() const override { +#ifdef PIN_VBAT_READ + return true; +#else + return false; +#endif + } + uint16_t getBattMilliVolts() override { #ifdef PIN_VBAT_READ analogReadResolution(10); @@ -68,7 +76,7 @@ public: } raw = raw / 8; - return ((5.78 * raw) / 1024.0) * 1000; + return calibrateBatteryMillivolts(((5.78 * raw) / 1024.0) * 1000); #else return 0; // not supported #endif diff --git a/variants/xiao_nrf52/XiaoNrf52Board.h b/variants/xiao_nrf52/XiaoNrf52Board.h index d4892fcbf..70d06f393 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.h +++ b/variants/xiao_nrf52/XiaoNrf52Board.h @@ -34,6 +34,8 @@ public: } #endif + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override { diff --git a/variants/xiao_rp2040/XiaoRP2040Board.h b/variants/xiao_rp2040/XiaoRP2040Board.h index 3e4afd5e1..0cde381dc 100644 --- a/variants/xiao_rp2040/XiaoRP2040Board.h +++ b/variants/xiao_rp2040/XiaoRP2040Board.h @@ -43,6 +43,8 @@ public: #endif + bool supportsBatteryCalibration() const override { return true; } + uint16_t getBattMilliVolts() override { #if defined(PIN_VBAT_READ) && defined(ADC_MULTIPLIER) analogReadResolution(12); @@ -53,7 +55,7 @@ public: } raw = raw / BATTERY_SAMPLES; - return (ADC_MULTIPLIER * raw) / 4096; + return calibrateBatteryMillivolts((ADC_MULTIPLIER * raw) / 4096); #else return 0; #endif