Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,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 RC4 qualification

The exact release commit and build links are recorded on the [1.1 RC4 release page](https://github.com/n30nex/NeonPocketMC-RC52/releases/tag/v1.1.0-rc.4). GitHub Actions builds both RC52 images, validates their UF2 family and application address, runs upstream unit tests, and regression-builds an existing nRF52 companion target.
Expand Down
104 changes: 103 additions & 1 deletion examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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 <mAh> | set battery.size <mAh>");
Serial.println(" battery | set battery.calibrate <multimeter-mV>");
#endif
} else if (strcmp(cli_command, "rebuild") == 0) {
bool success = _store->formatFileSystem();
if (success) {
Expand Down Expand Up @@ -2258,6 +2356,10 @@ void MyMesh::checkSerialInterface() {
void MyMesh::loop() {
BaseChatMesh::loop();

#ifdef HELTEC_RC52
checkBatteryCalibration();
#endif

if (_cli_rescue) {
checkCLIRescueCmd();
} else {
Expand Down
11 changes: 11 additions & 0 deletions examples/companion_radio/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -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); }
Expand Down Expand Up @@ -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];
Expand Down
31 changes: 29 additions & 2 deletions examples/companion_radio/NodePrefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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));
Expand All @@ -125,14 +145,21 @@ 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));
}
// new accessor methods
bool isRepeatEn() const { return repeat.disable_fwd == 0; }
void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; }
};
};
13 changes: 12 additions & 1 deletion variants/heltec_rc52/HeltecRC52Board.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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() {
Expand Down
2 changes: 2 additions & 0 deletions variants/heltec_rc52/HeltecRC52Board.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"; }
};
Loading