From 71b7f9f66c68020cf19ea9425ae172ae40303ee8 Mon Sep 17 00:00:00 2001 From: n30nex Date: Wed, 12 Aug 2026 16:33:35 -0400 Subject: [PATCH] feat(rc52): add battery capacity and ADC calibration --- README.md | 12 +++ examples/companion_radio/MyMesh.cpp | 104 ++++++++++++++++++++++- examples/companion_radio/MyMesh.h | 11 +++ examples/companion_radio/NodePrefs.h | 31 ++++++- variants/heltec_rc52/HeltecRC52Board.cpp | 13 ++- variants/heltec_rc52/HeltecRC52Board.h | 2 + 6 files changed, 169 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index eae22a0e..f6df2a0b 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,18 @@ GitHub Actions is the supported build path. The `RC52 Build` workflow: The artifact is application-only. It must be copied through the RC52 UF2 bootloader. **Never erase or replace the bootloader or SoftDevice.** See [docs/FLASHING.md](docs/FLASHING.md). +## Battery setup and calibration + +The RC52 can distinguish USB VBUS from battery power. After USB is removed it waits three seconds, records a battery-only ADC sample, and preserves that sample across a reset. In early-boot CLI Rescue at 115200 baud: + +```text +set.batterysize 420 +battery +set battery.calibrate 3980 +``` + +`set.batterysize` stores pack capacity metadata (50–20000 mAh, or `0` for unknown). It does **not** calibrate voltage. For voltage calibration, unplug USB, wait at least three seconds, measure the cell with a multimeter, reconnect/reset into CLI Rescue, check `battery`, then pass the measured millivolts to `set battery.calibrate`. The saved multiplier is applied on later boots. Automatic low-voltage shutdown remains disabled until this physical calibration is qualified. + ## 1.1 RC3 qualification The exact release commit and build links are recorded on the 1.1 RC3 release page and in [`docs/releases/1.1-RC3.md`](docs/releases/1.1-RC3.md). GitHub Actions builds the RC52 image, validates its UF2 family and application address, runs upstream unit tests, and regression-builds an existing nRF52 companion target. diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 1711c99e..3b171355 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -973,6 +973,21 @@ void MyMesh::begin(bool has_display) { _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER); _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours +#ifdef HELTEC_RC52 + if (_prefs.battery_capacity_mah != 0 && + (_prefs.battery_capacity_mah < 50 || _prefs.battery_capacity_mah > 20000)) { + _prefs.battery_capacity_mah = 0; + } + if (_prefs.battery_adc_multiplier >= 1.0f && _prefs.battery_adc_multiplier <= 10.0f) { + board.setAdcMultiplier(_prefs.battery_adc_multiplier); + } else { + _prefs.battery_adc_multiplier = 0; + } + if (_prefs.battery_calibration_sample_mv < 2500 || + _prefs.battery_calibration_sample_mv > 4500) { + _prefs.battery_calibration_sample_mv = 0; + } +#endif #ifdef BLE_PIN_CODE // 123456 by default if (_prefs.ble_pin == 0) { @@ -2056,6 +2071,49 @@ void MyMesh::enterCLIRescue() { Serial.println("========= CLI Rescue ========="); } +#ifdef HELTEC_RC52 +void MyMesh::checkBatteryCalibration() { + const uint32_t now = millis(); + if ((int32_t)(now - next_battery_source_check) < 0) return; + next_battery_source_check = now + 500; + + const bool external = board.isExternalPowered(); + if (!battery_source_known) { + battery_source_known = true; + battery_external_power = external; + return; + } + + if (external != battery_external_power) { + battery_external_power = external; + battery_sample_at = external ? 0 : now + 3000; + } + + if (!external && battery_sample_at && (int32_t)(now - battery_sample_at) >= 0) { + battery_sample_at = 0; + const uint16_t sample = board.getBattMilliVolts(); + if (sample >= 2500 && sample <= 4500) { + _prefs.battery_calibration_sample_mv = sample; + savePrefs(); + Serial.printf("Battery learning started: %u mV, %u mAh pack\n", + sample, _prefs.battery_capacity_mah); + } + } +} + +void MyMesh::setBatteryCapacity(const char* value) { + char* end = nullptr; + const unsigned long capacity = strtoul(value, &end, 10); + if (!value[0] || *end || (capacity != 0 && (capacity < 50 || capacity > 20000))) { + Serial.println(" Error: battery size must be 50..20000 mAh, or 0 for unknown"); + return; + } + _prefs.battery_capacity_mah = (uint16_t)capacity; + savePrefs(); + Serial.printf(" > battery size is now %u mAh\n", _prefs.battery_capacity_mah); +} +#endif + void MyMesh::checkCLIRescueCmd() { int len = strlen(cli_command); while (Serial.available() && len < sizeof(cli_command)-1) { @@ -2073,15 +2131,55 @@ void MyMesh::checkCLIRescueCmd() { if (len > 0 && cli_command[len - 1] == '\r') { // received complete line cli_command[len - 1] = 0; // replace newline with C string null terminator - if (memcmp(cli_command, "set ", 4) == 0) { + if (memcmp(cli_command, "set.batterysize ", 16) == 0) { +#ifdef HELTEC_RC52 + setBatteryCapacity(&cli_command[16]); +#else + Serial.println(" Error: unsupported by this board"); +#endif + } else if (memcmp(cli_command, "set ", 4) == 0) { const char* config = &cli_command[4]; if (memcmp(config, "pin ", 4) == 0) { _prefs.ble_pin = atoi(&config[4]); savePrefs(); Serial.printf(" > pin is now %06d\n", _prefs.ble_pin); +#ifdef HELTEC_RC52 + } else if (memcmp(config, "battery.size ", 13) == 0) { + setBatteryCapacity(&config[13]); + } else if (memcmp(config, "battery.calibrate ", 18) == 0) { + char* end = nullptr; + const long actual_mv = strtol(&config[18], &end, 10); + if (!config[18] || *end || actual_mv < 2500 || actual_mv > 4500) { + Serial.println(" Error: measured voltage must be 2500..4500 mV"); + } else if (_prefs.battery_calibration_sample_mv == 0) { + Serial.println(" Error: unplug USB first and wait 3 seconds for a battery sample"); + } else { + const float current = board.getAdcMultiplier(); + const float calibrated = current * (float)actual_mv / + (float)_prefs.battery_calibration_sample_mv; + if (!board.setAdcMultiplier(calibrated)) { + Serial.println(" Error: calculated multiplier is outside 1.0..10.0"); + } else { + _prefs.battery_adc_multiplier = calibrated; + _prefs.battery_calibration_sample_mv = 0; + savePrefs(); + Serial.printf(" > ADC multiplier calibrated to %.3f\n", calibrated); + } + } +#endif } else { Serial.printf(" Error: unknown config: %s\n", config); } +#ifdef HELTEC_RC52 + } else if (strcmp(cli_command, "battery") == 0) { + Serial.printf(" > source=%s voltage=%umV size=%umAh multiplier=%.3f sample=%umV\n", + board.isExternalPowered() ? "USB" : "battery", + board.getBattMilliVolts(), _prefs.battery_capacity_mah, + board.getAdcMultiplier(), _prefs.battery_calibration_sample_mv); + } else if (strcmp(cli_command, "help") == 0) { + Serial.println(" set.batterysize | set battery.size "); + Serial.println(" battery | set battery.calibrate "); +#endif } else if (strcmp(cli_command, "rebuild") == 0) { bool success = _store->formatFileSystem(); if (success) { @@ -2258,6 +2356,10 @@ void MyMesh::checkSerialInterface() { void MyMesh::loop() { BaseChatMesh::loop(); +#ifdef HELTEC_RC52 + checkBatteryCalibration(); +#endif + if (_cli_rescue) { checkCLIRescueCmd(); } else { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index c6b31cfd..ccc09dc5 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -207,6 +207,10 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { void checkCLIRescueCmd(); void checkSerialInterface(); bool isValidClientRepeatFreq(uint32_t f) const; +#ifdef HELTEC_RC52 + void checkBatteryCalibration(); + void setBatteryCapacity(const char* value); +#endif // helpers, short-cuts void saveChannels() { _store->saveChannels(this); } @@ -237,6 +241,13 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { uint32_t sign_data_len; unsigned long dirty_contacts_expiry; +#ifdef HELTEC_RC52 + uint32_t next_battery_source_check = 0; + uint32_t battery_sample_at = 0; + bool battery_source_known = false; + bool battery_external_power = false; +#endif + TransportKey send_scope; uint8_t cmd_frame[MAX_FRAME_SIZE + 1]; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 4ec1803e..e464981c 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -38,6 +38,11 @@ class NodePrefs : public ConfigSerializer { // persisted to file uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) char default_scope_name[31]; uint8_t default_scope_key[16]; +#ifdef HELTEC_RC52 + uint16_t battery_capacity_mah = 0; + float battery_adc_multiplier = 0; + uint16_t battery_calibration_sample_mv = 0; +#endif private: class RadioPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now) @@ -114,6 +119,21 @@ class NodePrefs : public ConfigSerializer { // persisted to file }; CompanionPrefs companion; +#ifdef HELTEC_RC52 + class PowerPrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("capacity_mah", _parent->battery_capacity_mah); + def("adc_mult", _parent->battery_adc_multiplier); + def("cal_sample_mv", _parent->battery_calibration_sample_mv); + } + public: + PowerPrefs(NodePrefs* parent) : _parent(parent) { } + }; + PowerPrefs power; +#endif + protected: void structure() override { def("name", node_name, sizeof(node_name)); @@ -125,9 +145,16 @@ class NodePrefs : public ConfigSerializer { // persisted to file def("gps", gps); def("repeat", repeat); def("comp", companion); +#ifdef HELTEC_RC52 + def("power", power); +#endif } public: - NodePrefs() : radio(this), gps(this), companion(this) { + NodePrefs() : radio(this), gps(this), companion(this) +#ifdef HELTEC_RC52 + , power(this) +#endif + { node_name[0] = 0; default_scope_name[0] = 0; memset(default_scope_key, 0, sizeof(default_scope_key)); @@ -135,4 +162,4 @@ class NodePrefs : public ConfigSerializer { // persisted to file // new accessor methods bool isRepeatEn() const { return repeat.disable_fwd == 0; } void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; } -}; \ No newline at end of file +}; diff --git a/variants/heltec_rc52/HeltecRC52Board.cpp b/variants/heltec_rc52/HeltecRC52Board.cpp index 95faf6fe..1b61f319 100644 --- a/variants/heltec_rc52/HeltecRC52Board.cpp +++ b/variants/heltec_rc52/HeltecRC52Board.cpp @@ -5,6 +5,7 @@ #include "variant.h" static constexpr float RC52_ADC_MV_LSB = 3000.0f / 4096.0f; +static float rc52_adc_multiplier = ADC_MULTIPLIER; #ifdef NRF52_POWER_MANAGEMENT static const PowerMgtConfig rc52_power_config = { @@ -49,7 +50,17 @@ uint16_t HeltecRC52Board::getBattMilliVolts() { delay(10); const int adc = analogRead(PIN_VBAT_READ); digitalWrite(PIN_BAT_CTL, LOW); - return (uint16_t)(adc * RC52_ADC_MV_LSB * ADC_MULTIPLIER); + return (uint16_t)(adc * RC52_ADC_MV_LSB * rc52_adc_multiplier); +} + +bool HeltecRC52Board::setAdcMultiplier(float multiplier) { + if (multiplier < 1.0f || multiplier > 10.0f) return false; + rc52_adc_multiplier = multiplier; + return true; +} + +float HeltecRC52Board::getAdcMultiplier() const { + return rc52_adc_multiplier; } void HeltecRC52Board::powerOff() { diff --git a/variants/heltec_rc52/HeltecRC52Board.h b/variants/heltec_rc52/HeltecRC52Board.h index e6f2209a..1d5ff1aa 100644 --- a/variants/heltec_rc52/HeltecRC52Board.h +++ b/variants/heltec_rc52/HeltecRC52Board.h @@ -14,5 +14,7 @@ class HeltecRC52Board : public NRF52BoardDCDC { void begin() override; void powerOff() override; uint16_t getBattMilliVolts() override; + bool setAdcMultiplier(float multiplier) override; + float getAdcMultiplier() const override; const char* getManufacturerName() const override { return "Heltec RC52"; } };