Skip to content
2 changes: 1 addition & 1 deletion protobufs
62 changes: 57 additions & 5 deletions src/Power.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
#include "power/PowerHAL.h"
#include "sleep.h"

#if HAS_SCREEN && !MESHTASTIC_EXCLUDE_BATTERY_CALIBRATION
#include "modules/BatteryCalibrationModule.h"
#endif

#if defined(ARCH_PORTDUINO)
#include "api/WiFiServerAPI.h"
#include "input/LinuxInputImpl.h"
Expand Down Expand Up @@ -175,6 +179,26 @@ Power *power;

using namespace meshtastic;

// pulls saved OCV array from config
namespace
{
bool copyOcvFromConfig(uint16_t *dest, size_t len)
{
if (config.power.ocv_count == 0) {
return false;
}
if (config.power.ocv_count != len) {
LOG_WARN("Power config OCV array has %u entries, expected %u; using defaults", config.power.ocv_count,
static_cast<unsigned>(len));
return false;
}
for (size_t i = 0; i < len; ++i) {
dest[i] = static_cast<uint16_t>(config.power.ocv[i]);
}
return true;
}
} // namespace

// NRF52 has AREF_VOLTAGE defined in architecture.h but
// make sure it's included. If something is wrong with NRF52
// definition - compilation will fail on missing definition
Expand Down Expand Up @@ -229,10 +253,26 @@ static void battery_adcDisable()
/**
* A simple battery level sensor that assumes the battery voltage is attached
* via a voltage-divider to an analog input
* OCV array is pulled from saved config if available
*/
class AnalogBatteryLevel : public HasBatteryLevel
{
public:
void applyOcvConfig(bool reset_read_value = false)
{
bool ocv_loaded = copyOcvFromConfig(OCV, NUM_OCV_POINTS);
LOG_INFO("OCV load from config: %s (first: %u, last: %u)", ocv_loaded ? "true" : "false", OCV[0],
OCV[NUM_OCV_POINTS - 1]);
if (!ocv_loaded) {
return;
}
chargingVolt = (OCV[0] + 10) * NUM_CELLS;
noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS;
if (reset_read_value || !initial_read_done) {
last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
}
}

/**
* Battery state of charge, from 0 to 100 or -1 for unknown
*/
Expand Down Expand Up @@ -510,9 +550,9 @@ class AnalogBatteryLevel : public HasBatteryLevel

/// For heltecs with no battery connected, the measured voltage is 2204, so
// need to be higher than that, in this case is 2500mV (3000-500)
const uint16_t OCV[NUM_OCV_POINTS] = {OCV_ARRAY};
const float chargingVolt = (OCV[0] + 10) * NUM_CELLS;
const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS;
uint16_t OCV[NUM_OCV_POINTS] = {OCV_ARRAY};
float chargingVolt = (OCV[0] + 10) * NUM_CELLS;
float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS;
// Start value from minimum voltage for the filter to not start from 0
// that could trigger some events.
// This value is over-written by the first ADC reading, it the voltage seems
Expand Down Expand Up @@ -605,7 +645,18 @@ Power::Power() : OSThread("Power")
lastheap = memGet.getFreeHeap();
#endif
}

// Allows overwriting defaults with values loaded from config independent of boot sequence
void Power::loadOcvFromConfig()
{
copyOcvFromConfig(OCV, NUM_OCV_POINTS);
}
bool Power::reloadOcvFromConfig()
{
bool loaded = copyOcvFromConfig(OCV, NUM_OCV_POINTS);
analogLevel.applyOcvConfig(true);
LOG_INFO("Power OCV reload %s (first=%u last=%u)", loaded ? "ok" : "failed", OCV[0], OCV[NUM_OCV_POINTS - 1]);
return loaded;
}
bool Power::analogInit()
{
#ifdef EXT_PWR_DETECT
Expand Down Expand Up @@ -672,7 +723,7 @@ bool Power::analogInit()
#ifndef ARCH_ESP32
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
#endif

analogLevel.applyOcvConfig();
batteryLevel = &analogLevel;
return true;
#else
Expand All @@ -688,6 +739,7 @@ bool Power::analogInit()
bool Power::setup()
{
bool found = false;
analogLevel.applyOcvConfig();
if (axpChipInit()) {
found = true;
} else if (lipoInit()) {
Expand Down
9 changes: 9 additions & 0 deletions src/PowerFSM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
#include "main.h"
#include "sleep.h"
#include "target_specific.h"
#if HAS_SCREEN && !MESHTASTIC_EXCLUDE_BATTERY_CALIBRATION
#include "modules/BatteryCalibrationModule.h"
#endif

#if HAS_WIFI && !defined(ARCH_PORTDUINO) || defined(MESHTASTIC_EXCLUDE_WIFI)
#include "mesh/wifi/WiFiAPClient.h"
Expand Down Expand Up @@ -65,6 +68,12 @@ static void sdsEnter()
static void lowBattSDSEnter()
{
LOG_POWERFSM("State: Lower batt SDS");
// Save OCV array to persistent memory if in battery calibration
#if HAS_SCREEN && !MESHTASTIC_EXCLUDE_BATTERY_CALIBRATION
if (batteryCalibrationModule && batteryCalibrationModule->persistCalibrationOcv()) {
nodeDB->saveToDisk(SEGMENT_CONFIG);
}
#endif
doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, true);
}
extern Power *power;
Expand Down
14 changes: 12 additions & 2 deletions src/graphics/Screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "draw/NodeListRenderer.h"
#include "draw/NotificationRenderer.h"
#include "draw/UIRenderer.h"
#include "modules/BatteryCalibrationModule.h"
#include "modules/CannedMessageModule.h"

#if !MESHTASTIC_EXCLUDE_GPS
Expand Down Expand Up @@ -155,6 +156,7 @@ void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
#ifdef USE_EINK
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
#endif
NotificationRenderer::bannerGeneration++; // bugfix for external modules
// Store the message and set the expiration timestamp
strncpy(NotificationRenderer::alertBannerMessage, banner_overlay_options.message, 255);
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
Expand All @@ -166,7 +168,8 @@ void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
NotificationRenderer::alertBannerCallback = banner_overlay_options.bannerCallback;
NotificationRenderer::curSelected = banner_overlay_options.InitialSelected;
NotificationRenderer::pauseBanner = false;
NotificationRenderer::current_notification_type = notificationTypeEnum::selection_picker;
NotificationRenderer::current_notification_type = banner_overlay_options.notificationType;
NotificationRenderer::inEvent.inputEvent = INPUT_BROKER_NONE;
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
ui->setTargetFPS(60);
Expand All @@ -179,6 +182,7 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
#ifdef USE_EINK
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
#endif
NotificationRenderer::bannerGeneration++;
nodeDB->pause_sort(true);
// Store the message and set the expiration timestamp
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
Expand All @@ -202,6 +206,7 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
#ifdef USE_EINK
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
#endif
NotificationRenderer::bannerGeneration++;
// Store the message and set the expiration timestamp
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
Expand All @@ -224,6 +229,8 @@ void Screen::showTextInput(const char *header, const char *initialText, uint32_t
{
LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs);

NotificationRenderer::bannerGeneration++;

// Start OnScreenKeyboardModule session (non-touch variant)
OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback);
NotificationRenderer::textInputCallback = textCallback;
Expand Down Expand Up @@ -1761,7 +1768,10 @@ int Screen::handleInputEvent(const InputEvent *event)
this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
if (batteryCalibrationModule && this->ui->getUiState()->currentFrame < moduleFrames.size() &&
moduleFrames.at(this->ui->getUiState()->currentFrame) == batteryCalibrationModule) {
menuHandler::batteryCalibrationMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
menuHandler::homeBaseMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) {
menuHandler::systemBaseMenu();
Expand Down
94 changes: 94 additions & 0 deletions src/graphics/draw/MenuHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
#include "mesh/Default.h"
#include "mesh/MeshTypes.h"
#include "modules/AdminModule.h"
#include "modules/BatteryCalibrationModule.h"
#include "modules/BatteryCalibrationSampler.h"
#include "modules/CannedMessageModule.h"
#include "modules/ExternalNotificationModule.h"
#include "modules/KeyVerificationModule.h"
Expand Down Expand Up @@ -2396,6 +2398,92 @@ void menuHandler::powerMenu()
screen->showOverlayBanner(bannerOptions);
}

void menuHandler::batteryCalibrationMenu()
{

static const char *optionsArrayIdle[] = {"Back", "Begin Calibration", "Reset OCV Array"};
static const char *optionsArrayActive[] = {"Back", "Stop Calibration", "Reset OCV Array", "Save OCV & End"};

enum optionsNumbers { Back = 0, Start = 1, Reset = 2, Apply = 3 };
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Battery Calibration Action";
const bool calibrationActive = batteryCalibrationModule && batteryCalibrationModule->isCalibrationActive();
bannerOptions.optionsArrayPtr = calibrationActive ? optionsArrayActive : optionsArrayIdle;
bannerOptions.optionsCount = calibrationActive ? 4 : 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Start) {
if (batteryCalibrationModule && batteryCalibrationModule->isCalibrationActive()) {
batteryCalibrationModule->stopCalibration();
IF_SCREEN(screen->showSimpleBanner("Calibration stopped.", 2000));
} else {
menuHandler::menuQueue = menuHandler::battery_calibration_confirm_menu;
screen->runNow();
}
} else if (selected == Reset) {
if (batteryCalibrationSampler) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we call a batteryCalibrationModule->stopCalibration(); here in order to preempt any issues?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed 356a4e1

batteryCalibrationSampler->resetSamples();
batteryCalibrationModule->stopCalibration();
}
config.power.ocv_count = 0;
for (size_t i = 0; i < NUM_OCV_POINTS; ++i) {
config.power.ocv[i] = 0;
}
if (nodeDB) {
nodeDB->saveToDisk(SEGMENT_CONFIG);
}
IF_SCREEN(screen->showSimpleBanner("OCV array reset.\nRebooting...", 2000));
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
screen->runNow();
} else if (selected == Apply) {
if (batteryCalibrationModule && batteryCalibrationModule->isCalibrationActive()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the node restarted due to low battery / brownout, will isCalibrationActive still be true?

I was trying to trace this in the code but can't confirm.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theres nothing in persistent memory/protobufs right now for saving current state, so brownout/low battery shutdown would reset back to false (See BatteryCalibrationModule.h line 27)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do have future ideas about saving the calibration data to memory periodically to resume after unintentional rebooting though. In testing, its a pain to have to start calibration from scratch if you accidentally hit the reset button several days in with a battery that lasts a week+.

if (batteryCalibrationModule->persistCalibrationOcv()) {
if (nodeDB) {
nodeDB->saveToDisk(SEGMENT_CONFIG);
} else {
}
batteryCalibrationModule->stopCalibration();
IF_SCREEN(screen->showSimpleBanner("OCV saved.\nCalibration ended.", 2000));
} else {
IF_SCREEN(screen->showSimpleBanner("OCV not ready yet.", 2000));
}
} else {
}
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
}

void menuHandler::batteryCalibrationConfirmMenu()
{
static const char *optionsArray[] = {"Back", "Start Calibration"};
enum optionsNumbers { Back = 0, Start = 1 };

BannerOverlayOptions bannerOptions;
bannerOptions.message = "Confirm Battery Calibration\n"
"1) Fully charge battery\n"
"2) Remove charger\n"
"3) Start calibration";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Start) {
if (batteryCalibrationModule) {
batteryCalibrationModule->startCalibration();
IF_SCREEN(screen->showSimpleBanner(

"Calibration started.\nUse device as normal.\nDo not charge until battery dies.", 5000));
} else if (batteryCalibrationSampler) {
batteryCalibrationSampler->resetSamples();
}
} else {
menuHandler::menuQueue = menuHandler::battery_calibration_menu;
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
}

void menuHandler::keyVerificationInitMenu()
{
screen->showNodePicker("Node to Verify", 30000,
Expand Down Expand Up @@ -2725,6 +2813,12 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case power_menu:
powerMenu();
break;
case battery_calibration_menu:
batteryCalibrationMenu();
break;
case battery_calibration_confirm_menu:
batteryCalibrationConfirmMenu();
break;
case FrameToggles:
FrameToggles_menu();
break;
Expand Down
4 changes: 4 additions & 0 deletions src/graphics/draw/MenuHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class menuHandler
bluetooth_toggle_menu,
screen_options_menu,
power_menu,
battery_calibration_menu,
battery_calibration_confirm_menu,
system_base_menu,
key_verification_init,
key_verification_final_prompt,
Expand Down Expand Up @@ -105,6 +107,8 @@ class menuHandler
static void wifiToggleMenu();
static void screenOptionsMenu();
static void powerMenu();
static void batteryCalibrationMenu();
static void batteryCalibrationConfirmMenu();
static void nodeNameLengthMenu();
static void FrameToggles_menu();
static void DisplayUnits_menu();
Expand Down
22 changes: 19 additions & 3 deletions src/graphics/draw/NotificationRenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelct
const char **NotificationRenderer::optionsArrayPtr = nullptr;
const int *NotificationRenderer::optionsEnumPtr = nullptr;
std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL;
uint32_t NotificationRenderer::bannerGeneration = 0;
bool NotificationRenderer::pauseBanner = false;
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
uint32_t NotificationRenderer::numDigits = 0;
Expand Down Expand Up @@ -204,8 +205,13 @@ void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiS
return;
}
if (curSelected == static_cast<int8_t>(numDigits)) {
uint32_t generation = bannerGeneration;
alertBannerCallback(currentNumber);
resetBanner();
if (bannerGeneration == generation) {
resetBanner();
} else {
inEvent.inputEvent = INPUT_BROKER_NONE;
}
return;
}

Expand Down Expand Up @@ -270,8 +276,13 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
uint32_t generation = bannerGeneration;
alertBannerCallback(selectedNodenum);
resetBanner();
if (bannerGeneration == generation) {
resetBanner();
} else {
inEvent.inputEvent = INPUT_BROKER_NONE;
}
return;
} else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) &&
alertBannerUntil != 0) {
Expand Down Expand Up @@ -387,13 +398,18 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
uint32_t generation = bannerGeneration;
if (optionsEnumPtr != nullptr) {
alertBannerCallback(optionsEnumPtr[curSelected]);
optionsEnumPtr = nullptr;
} else {
alertBannerCallback(curSelected);
}
resetBanner();
if (bannerGeneration == generation) {
resetBanner();
} else {
inEvent.inputEvent = INPUT_BROKER_NONE;
}
return;
} else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) &&
alertBannerUntil != 0) {
Expand Down
Loading
Loading