Add MAX17048 lipo fuel gauge (#4851)

* Initial commit

* Update MAX17048Sensor.cpp

* Update EnvironmentTelemetry.cpp

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
This commit is contained in:
David
2024-09-25 20:34:53 +10:00
committed by GitHub
parent 1129c92974
commit 40b3dbaa70
11 changed files with 449 additions and 19 deletions

View File

@@ -83,7 +83,7 @@ int32_t EnvironmentTelemetryModule::runOnce()
*/
// moduleConfig.telemetry.environment_measurement_enabled = 1;
// moduleConfig.telemetry.environment_screen_enabled = 1;
// moduleConfig.telemetry.environment_screen_enabled = 1;
// moduleConfig.telemetry.environment_update_interval = 15;
if (!(moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) {
@@ -144,6 +144,8 @@ int32_t EnvironmentTelemetryModule::runOnce()
result = mlx90632Sensor.runOnce();
if (nau7802Sensor.hasSensor())
result = nau7802Sensor.runOnce();
if (max17048Sensor.hasSensor())
result = max17048Sensor.runOnce();
#endif
}
return result;
@@ -156,6 +158,7 @@ int32_t EnvironmentTelemetryModule::runOnce()
result = bme680Sensor.runTrigger();
}
uint32_t now = millis();
if (((lastSentToMesh == 0) ||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.environment_update_interval,
@@ -397,6 +400,10 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m
m->variant.environment_metrics.relative_humidity = m_ahtx.variant.environment_metrics.relative_humidity;
}
}
if (max17048Sensor.hasSensor()) {
valid = valid && max17048Sensor.getMetrics(m);
hasSensor = true;
}
#endif
return valid && hasSensor;
@@ -587,6 +594,11 @@ AdminMessageHandleResult EnvironmentTelemetryModule::handleAdminMessageForModule
if (result != AdminMessageHandleResult::NOT_HANDLED)
return result;
}
if (max17048Sensor.hasSensor()) {
result = max17048Sensor.handleAdminMessage(mp, request, response);
if (result != AdminMessageHandleResult::NOT_HANDLED)
return result;
}
return result;
}

View File

@@ -60,6 +60,8 @@ int32_t PowerTelemetryModule::runOnce()
result = ina260Sensor.runOnce();
if (ina3221Sensor.hasSensor() && !ina3221Sensor.isInitialized())
result = ina3221Sensor.runOnce();
if (max17048Sensor.hasSensor() && !max17048Sensor.isInitialized())
result = max17048Sensor.runOnce();
}
return result;
#else
@@ -128,19 +130,23 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
return;
}
// Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags
display->setFont(FONT_SMALL);
String last_temp = String(lastMeasurement.variant.environment_metrics.temperature, 0) + "°C";
display->drawString(x, y += _fontHeight(FONT_MEDIUM) - 2, "From: " + String(lastSender) + "(" + String(agoSecs) + "s)");
if (lastMeasurement.variant.power_metrics.ch1_voltage != 0) {
if (lastMeasurement.variant.power_metrics.has_ch1_voltage || lastMeasurement.variant.power_metrics.has_ch1_current) {
display->drawString(x, y += _fontHeight(FONT_SMALL),
"Ch 1 Volt/Cur: " + String(lastMeasurement.variant.power_metrics.ch1_voltage, 0) + "V / " +
String(lastMeasurement.variant.power_metrics.ch1_current, 0) + "mA");
"Ch1 Volt: " + String(lastMeasurement.variant.power_metrics.ch1_voltage, 2) +
"V / Curr: " + String(lastMeasurement.variant.power_metrics.ch1_current, 0) + "mA");
}
if (lastMeasurement.variant.power_metrics.has_ch2_voltage || lastMeasurement.variant.power_metrics.has_ch2_current) {
display->drawString(x, y += _fontHeight(FONT_SMALL),
"Ch 2 Volt/Cur: " + String(lastMeasurement.variant.power_metrics.ch2_voltage, 0) + "V / " +
String(lastMeasurement.variant.power_metrics.ch2_current, 0) + "mA");
"Ch2 Volt: " + String(lastMeasurement.variant.power_metrics.ch2_voltage, 2) +
"V / Curr: " + String(lastMeasurement.variant.power_metrics.ch2_current, 0) + "mA");
}
if (lastMeasurement.variant.power_metrics.has_ch3_voltage || lastMeasurement.variant.power_metrics.has_ch3_current) {
display->drawString(x, y += _fontHeight(FONT_SMALL),
"Ch 3 Volt/Cur: " + String(lastMeasurement.variant.power_metrics.ch3_voltage, 0) + "V / " +
String(lastMeasurement.variant.power_metrics.ch3_current, 0) + "mA");
"Ch3 Volt: " + String(lastMeasurement.variant.power_metrics.ch3_voltage, 2) +
"V / Curr: " + String(lastMeasurement.variant.power_metrics.ch3_current, 0) + "mA");
}
}
@@ -150,8 +156,8 @@ bool PowerTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &m
#ifdef DEBUG_PORT
const char *sender = getSenderShortName(mp);
LOG_INFO("(Received from %s): ch1_voltage=%f, ch1_current=%f, ch2_voltage=%f, ch2_current=%f, "
"ch3_voltage=%f, ch3_current=%f\n",
LOG_INFO("(Received from %s): ch1_voltage=%.1f, ch1_current=%.1f, ch2_voltage=%.1f, ch2_current=%.1f, "
"ch3_voltage=%.1f, ch3_current=%.1f\n",
sender, t->variant.power_metrics.ch1_voltage, t->variant.power_metrics.ch1_current,
t->variant.power_metrics.ch2_voltage, t->variant.power_metrics.ch2_current, t->variant.power_metrics.ch3_voltage,
t->variant.power_metrics.ch3_current);
@@ -172,12 +178,7 @@ bool PowerTelemetryModule::getPowerTelemetry(meshtastic_Telemetry *m)
m->time = getTime();
m->which_variant = meshtastic_Telemetry_power_metrics_tag;
m->variant.power_metrics.ch1_voltage = 0;
m->variant.power_metrics.ch1_current = 0;
m->variant.power_metrics.ch2_voltage = 0;
m->variant.power_metrics.ch2_current = 0;
m->variant.power_metrics.ch3_voltage = 0;
m->variant.power_metrics.ch3_current = 0;
m->variant.power_metrics = meshtastic_PowerMetrics_init_zero;
#if HAS_TELEMETRY && !defined(ARCH_PORTDUINO)
if (ina219Sensor.hasSensor())
valid = ina219Sensor.getMetrics(m);
@@ -185,6 +186,8 @@ bool PowerTelemetryModule::getPowerTelemetry(meshtastic_Telemetry *m)
valid = ina260Sensor.getMetrics(m);
if (ina3221Sensor.hasSensor())
valid = ina3221Sensor.getMetrics(m);
if (max17048Sensor.hasSensor())
valid = max17048Sensor.getMetrics(m);
#endif
return valid;

View File

@@ -0,0 +1,176 @@
#include "MAX17048Sensor.h"
#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
MAX17048Singleton *MAX17048Singleton::GetInstance()
{
if (pinstance == nullptr) {
pinstance = new MAX17048Singleton();
}
return pinstance;
}
MAX17048Singleton::MAX17048Singleton() {}
MAX17048Singleton::~MAX17048Singleton() {}
MAX17048Singleton *MAX17048Singleton::pinstance{nullptr};
bool MAX17048Singleton::runOnce(TwoWire *theWire)
{
initialized = begin(theWire);
LOG_DEBUG("MAX17048Sensor::runOnce %s\n", initialized ? "began ok" : "begin failed");
return initialized;
}
bool MAX17048Singleton::isBatteryCharging()
{
float volts = cellVoltage();
if (isnan(volts)) {
LOG_DEBUG("MAX17048Sensor::isBatteryCharging is not connected\n");
return 0;
}
MAX17048ChargeSample sample;
sample.chargeRate = chargeRate(); // charge/discharge rate in percent/hr
sample.cellPercent = cellPercent(); // state of charge in percent 0 to 100
chargeSamples.push(sample); // save a sample into a fifo buffer
// Keep the fifo buffer trimmed
while (chargeSamples.size() > MAX17048_CHARGING_SAMPLES)
chargeSamples.pop();
// Based on the past n samples, is the lipo charging, discharging or idle
if (chargeSamples.front().chargeRate > MAX17048_CHARGING_MINIMUM_RATE &&
chargeSamples.back().chargeRate > MAX17048_CHARGING_MINIMUM_RATE) {
if (chargeSamples.front().cellPercent > chargeSamples.back().cellPercent)
chargeState = MAX17048ChargeState::EXPORT;
else if (chargeSamples.front().cellPercent < chargeSamples.back().cellPercent)
chargeState = MAX17048ChargeState::IMPORT;
else
chargeState = MAX17048ChargeState::IDLE;
} else {
chargeState = MAX17048ChargeState::IDLE;
}
LOG_DEBUG("MAX17048Sensor::isBatteryCharging %s volts: %.3f soc: %.3f rate: %.3f\n", chargeLabels[chargeState], volts,
sample.cellPercent, sample.chargeRate);
return chargeState == MAX17048ChargeState::IMPORT;
}
uint16_t MAX17048Singleton::getBusVoltageMv()
{
float volts = cellVoltage();
if (isnan(volts)) {
LOG_DEBUG("MAX17048Sensor::getBusVoltageMv is not connected\n");
return 0;
}
LOG_DEBUG("MAX17048Sensor::getBusVoltageMv %.3fmV\n", volts);
return (uint16_t)(volts * 1000.0f);
}
uint8_t MAX17048Singleton::getBusBatteryPercent()
{
float soc = cellPercent();
LOG_DEBUG("MAX17048Sensor::getBusBatteryPercent %.1f%%\n", soc);
return clamp(static_cast<uint8_t>(round(soc)), static_cast<uint8_t>(0), static_cast<uint8_t>(100));
}
uint16_t MAX17048Singleton::getTimeToGoSecs()
{
float rate = chargeRate(); // charge/discharge rate in percent/hr
float soc = cellPercent(); // state of charge in percent 0 to 100
soc = clamp(soc, 0.0f, 100.0f); // clamp soc between 0 and 100%
float ttg = ((100.0f - soc) / rate) * 3600.0f; // calculate seconds to charge/discharge
LOG_DEBUG("MAX17048Sensor::getTimeToGoSecs %.0f seconds\n", ttg);
return (uint16_t)ttg;
}
bool MAX17048Singleton::isBatteryConnected()
{
float volts = cellVoltage();
if (isnan(volts)) {
LOG_DEBUG("MAX17048Sensor::isBatteryConnected is not connected\n");
return false;
}
// if a valid voltage is returned, then battery must be connected
return true;
}
bool MAX17048Singleton::isExternallyPowered()
{
float volts = cellVoltage();
if (isnan(volts)) {
// if the battery is not connected then there must be external power
LOG_DEBUG("MAX17048Sensor::isExternallyPowered battery is\n");
return true;
}
// if the bus voltage is over MAX17048_BUS_POWER_VOLTS, then the external power
// is assumed to be connected
LOG_DEBUG("MAX17048Sensor::isExternallyPowered %s connected\n", volts >= MAX17048_BUS_POWER_VOLTS ? "is" : "is not");
return volts >= MAX17048_BUS_POWER_VOLTS;
}
#if (HAS_TELEMETRY && (!MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_POWER_TELEMETRY))
MAX17048Sensor::MAX17048Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MAX17048, "MAX17048") {}
int32_t MAX17048Sensor::runOnce()
{
if (isInitialized()) {
LOG_INFO("Init sensor: %s is already initialised\n", sensorName);
return true;
}
LOG_INFO("Init sensor: %s\n", sensorName);
if (!hasSensor()) {
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
// Get a singleton instance and initialise the max17048
if (max17048 == nullptr) {
max17048 = MAX17048Singleton::GetInstance();
}
status = max17048->runOnce(nodeTelemetrySensorsMap[sensorType].second);
return initI2CSensor();
}
void MAX17048Sensor::setup() {}
bool MAX17048Sensor::getMetrics(meshtastic_Telemetry *measurement)
{
LOG_DEBUG("MAX17048Sensor::getMetrics id: %i\n", measurement->which_variant);
float volts = max17048->cellVoltage();
if (isnan(volts)) {
LOG_DEBUG("MAX17048Sensor::getMetrics battery is not connected\n");
return false;
}
float rate = max17048->chargeRate(); // charge/discharge rate in percent/hr
float soc = max17048->cellPercent(); // state of charge in percent 0 to 100
soc = clamp(soc, 0.0f, 100.0f); // clamp soc between 0 and 100%
float ttg = (100.0f - soc) / rate; // calculate hours to charge/discharge
LOG_DEBUG("MAX17048Sensor::getMetrics volts: %.3fV soc: %.1f%% ttg: %.1f hours\n", volts, soc, ttg);
if ((int)measurement->which_variant == meshtastic_Telemetry_power_metrics_tag) {
measurement->variant.power_metrics.has_ch1_voltage = true;
measurement->variant.power_metrics.ch1_voltage = volts;
} else if ((int)measurement->which_variant == meshtastic_Telemetry_device_metrics_tag) {
measurement->variant.device_metrics.has_battery_level = true;
measurement->variant.device_metrics.has_voltage = true;
measurement->variant.device_metrics.battery_level = static_cast<uint32_t>(round(soc));
measurement->variant.device_metrics.voltage = volts;
}
return true;
}
uint16_t MAX17048Sensor::getBusVoltageMv()
{
return max17048->getBusVoltageMv();
};
#endif
#endif

View File

@@ -0,0 +1,110 @@
#pragma once
#ifndef MAX17048_SENSOR_H
#define MAX17048_SENSOR_H
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
// Samples to store in a buffer to determine if the battery is charging or discharging
#define MAX17048_CHARGING_SAMPLES 3
// Threshold to determine if the battery is on charge, in percent/hour
#define MAX17048_CHARGING_MINIMUM_RATE 1.0f
// Threshold to determine if the board has bus power
#define MAX17048_BUS_POWER_VOLTS 4.195f
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "TelemetrySensor.h"
#include "VoltageSensor.h"
#include "meshUtils.h"
#include <Adafruit_MAX1704X.h>
#include <queue>
struct MAX17048ChargeSample {
float cellPercent;
float chargeRate;
};
enum MAX17048ChargeState { IDLE, EXPORT, IMPORT };
// Singleton wrapper for the Adafruit_MAX17048 class
class MAX17048Singleton : public Adafruit_MAX17048
{
private:
static MAX17048Singleton *pinstance;
bool initialized = false;
std::queue<MAX17048ChargeSample> chargeSamples;
MAX17048ChargeState chargeState = IDLE;
const String chargeLabels[3] = {F("idle"), F("export"), F("import")};
protected:
MAX17048Singleton();
~MAX17048Singleton();
public:
// Create a singleton instance (not thread safe)
static MAX17048Singleton *GetInstance();
// Singletons should not be cloneable.
MAX17048Singleton(MAX17048Singleton &other) = delete;
// Singletons should not be assignable.
void operator=(const MAX17048Singleton &) = delete;
// Initialise the sensor (not thread safe)
virtual bool runOnce(TwoWire *theWire = &Wire);
// Get the current bus voltage
uint16_t getBusVoltageMv();
// Get the state of charge in percent 0 to 100
uint8_t getBusBatteryPercent();
// Calculate the seconds to charge/discharge
uint16_t getTimeToGoSecs();
// Returns true if the battery sensor has started
inline virtual bool isInitialised() { return initialized; };
// Returns true if the battery is currently on charge (not thread safe)
bool isBatteryCharging();
// Returns true if a battery is actually connected
bool isBatteryConnected();
// Returns true if there is bus or external power connected
bool isExternallyPowered();
};
#if (HAS_TELEMETRY && (!MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_POWER_TELEMETRY))
class MAX17048Sensor : public TelemetrySensor, VoltageSensor
{
private:
MAX17048Singleton *max17048 = nullptr;
protected:
virtual void setup() override;
public:
MAX17048Sensor();
// Initialise the sensor
virtual int32_t runOnce() override;
// Get the current bus voltage and state of charge
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
// Get the current bus voltage
virtual uint16_t getBusVoltageMv() override;
};
#endif
#endif
#endif