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
2 changes: 2 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ lib_deps =
#For ADS1115 sensor uncomment following
; adafruit/Adafruit BusIO @ 1.13.2
; adafruit/Adafruit ADS1X15 @ 2.4.0
#For INA219 sensor uncomment following (requires maintainer approval before enabling globally)
; adafruit/Adafruit INA219 @ 1.2.1

extra_scripts = ${scripts_defaults.extra_scripts}

Expand Down
222 changes: 222 additions & 0 deletions usermods/INA219_v2/usermod_ina219.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// #warning **** Included USERMOD_INA219 ****

#pragma once

#include <Arduino.h> // WLEDMM: make sure that I2C drivers have the "right" Wire Object
#include "wled.h"
#include <Adafruit_INA219.h>

/*
* Usermod for the INA219 I2C current/power sensor.
*
* Displays the following values in the Info tab:
* - Bus Voltage (V)
* - Load Voltage (V)
* - Current (mA)
* - Power (mW)
*
* Configurable parameters (via WLED Settings > Usermods):
* - enabled : enable/disable the usermod
* - i2cAddress : I2C address (0x40, 0x41, 0x44, 0x45)
* - readInterval-ms : measurement interval in milliseconds
* - shuntResistor-mOhm : shunt resistor value in milli-Ohm (default: 100 = 0.1 Ohm)
* - maxCurrentRange-A : maximum expected current (0.4, 1.0, or 2.0 A) — selects PGA gain
* - busVoltageRange-V : bus voltage range (16 or 32 V)
*
* Current and power are calculated directly from the shunt voltage:
* I = V_shunt / R_shunt (independent of internal INA219 calibration)
*
* Requires: adafruit/Adafruit INA219 @ 1.2.1 (uncomment in platformio.ini)
*/

class UsermodINA219 : public Usermod {
private:
Adafruit_INA219 *ina219 = nullptr;

float shuntVoltage_mV = 0.0f;
float busVoltage_V = 0.0f;
float current_mA = 0.0f;
float power_mW = 0.0f;
float loadVoltage_V = 0.0f;

bool sensorFound = false;
unsigned long lastMeasure = 0;

// Configurable settings
uint32_t readInterval = 5000; // ms between measurements
uint8_t i2cAddress = 0x40; // INA219 I2C address
float shuntResistor_mOhm = 100.0f; // shunt resistor in milli-Ohm (100 mOhm = 0.1 Ohm)
float maxCurrentRange_A = 2.0f; // max expected current: 0.4, 1.0, or 2.0 A
uint8_t busVoltageRange_V = 32; // bus voltage range: 16 or 32 V

// PROGMEM string keys for config
static const char _readInterval[];
static const char _i2cAddress[];
static const char _shuntResistor[];
static const char _maxCurrentRange[];
static const char _busVoltageRange[];

// Select Adafruit calibration preset matching the configured voltage/current range.
// This sets the correct PGA gain register in the INA219 Config register (BRNG + PG bits).
// Current and power are still computed manually from the shunt voltage for accuracy.
// Note: the Adafruit library has no 16V preset beyond 400 mA; for 16V + higher current
// setCalibration_16V_400mA() is the only available 16V option and is used for all 16V cases.
void applyCalibration() {
if (!ina219) return;
if (busVoltageRange_V <= 16) {
ina219->setCalibration_16V_400mA();
} else {
if (maxCurrentRange_A <= 1.0f)
ina219->setCalibration_32V_1A();
else
ina219->setCalibration_32V_2A();
}
}

public:
UsermodINA219(const char *name, bool enabled) : Usermod(name, enabled) {}

void setup() override {
if (!enabled) return;

if (!pinManager.joinWire()) { // WLEDMM: allocates global I2C pins and starts Wire
USER_PRINTLN(F("[INA219]: failed to join I2C bus."));
sensorFound = false;
initDone = true;
return;
}

// Re-create sensor object with (potentially updated) I2C address
if (ina219) { delete ina219; ina219 = nullptr; }
ina219 = new Adafruit_INA219(i2cAddress);

if (!ina219->begin()) {
USER_PRINTLN(F("[INA219]: sensor not found."));
delete ina219;
ina219 = nullptr;
sensorFound = false;
initDone = true;
return;
}

applyCalibration();
sensorFound = true;
USER_PRINTLN(F("[INA219]: sensor found."));
initDone = true;
}

void loop() override {
if (!enabled || !sensorFound || !initDone || !ina219) return;
if (strip.isUpdating()) return;

unsigned long now = millis();
if (now - lastMeasure < readInterval) return;
lastMeasure = now;

// Read raw voltages directly from the sensor
shuntVoltage_mV = ina219->getShuntVoltage_mV();
busVoltage_V = ina219->getBusVoltage_V();

// Calculate load voltage, current and power manually using the configured shunt value.
// This gives correct results for any shunt resistor, independent of the INA219 calibration.
loadVoltage_V = busVoltage_V + (shuntVoltage_mV / 1000.0f);
if (shuntResistor_mOhm < 1.0f) {
// Guard against division by zero / near-zero shunt value (misconfigured)
USER_PRINTLN(F("[INA219]: shuntResistor-mOhm is invalid (<1). Skipping current/power calculation."));
current_mA = 0.0f;
power_mW = 0.0f;
} else {
current_mA = shuntVoltage_mV / (shuntResistor_mOhm / 1000.0f); // I = U / R
power_mW = current_mA * loadVoltage_V;
}
}

void addToJsonInfo(JsonObject &root) override {
if (!enabled) return;

JsonObject user = root[F("u")];
if (user.isNull()) user = root.createNestedObject(F("u"));

if (!initDone) {
JsonArray arr = user.createNestedArray(F("INA219"));
arr.add(F("Initializing..."));
return;
}

if (!sensorFound) {
JsonArray arr = user.createNestedArray(F("INA219"));
arr.add(F("Not found"));
return;
}

JsonArray busV = user.createNestedArray(F("INA219 Bus Voltage"));
busV.add(busVoltage_V);
busV.add(F(" V"));

JsonArray loadV = user.createNestedArray(F("INA219 Load Voltage"));
loadV.add(loadVoltage_V);
loadV.add(F(" V"));

JsonArray curr = user.createNestedArray(F("INA219 Current"));
curr.add(current_mA);
curr.add(F(" mA"));

JsonArray pwr = user.createNestedArray(F("INA219 Power"));
pwr.add(power_mW);
pwr.add(F(" mW"));
}

void addToConfig(JsonObject &root) override {
JsonObject top = root.createNestedObject(FPSTR(_name));
top[F("enabled")] = enabled;
top[FPSTR(_readInterval)] = readInterval;
top[FPSTR(_i2cAddress)] = i2cAddress;
top[FPSTR(_shuntResistor)] = shuntResistor_mOhm;
top[FPSTR(_maxCurrentRange)] = maxCurrentRange_A;
top[FPSTR(_busVoltageRange)] = busVoltageRange_V;
DEBUG_PRINTLN(F("[INA219] config saved."));
}

bool readFromConfig(JsonObject &root) override {
JsonObject top = root[FPSTR(_name)];
if (top.isNull()) {
DEBUG_PRINTLN(F("[INA219]: No config found. (Using defaults.)"));
return false;
}
bool configComplete = !top.isNull();

uint8_t oldAddress = i2cAddress;

configComplete &= getJsonValue(top[F("enabled")], enabled, false);
configComplete &= getJsonValue(top[FPSTR(_readInterval)], readInterval, (uint32_t)5000);
configComplete &= getJsonValue(top[FPSTR(_i2cAddress)], i2cAddress, (uint8_t)0x40);
configComplete &= getJsonValue(top[FPSTR(_shuntResistor)], shuntResistor_mOhm, 100.0f);
if (shuntResistor_mOhm < 1.0f) {
USER_PRINTLN(F("[INA219]: shuntResistor-mOhm clamped to minimum 1 mOhm."));
shuntResistor_mOhm = 1.0f;
}
configComplete &= getJsonValue(top[FPSTR(_maxCurrentRange)], maxCurrentRange_A, 2.0f);
configComplete &= getJsonValue(top[FPSTR(_busVoltageRange)], busVoltageRange_V, (uint8_t)32);
Comment on lines +190 to +199
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate i2cAddress and readInterval.

No bounds checks on user-supplied config:

  • i2cAddress: the INA219 only supports 0x40/0x41/0x44/0x45 (and combinations). Arbitrary addresses will just cause begin() to silently fail on reload.
  • readInterval: a user-entered 0 makes now - lastMeasure < 0 always false and triggers a read every loop() call, hammering the I²C bus.
  • busVoltageRange_V: any value other than 16/32 will fall into the 32V branch at Line 68; either document or clamp.
🛡️ Proposed validation
     configComplete &= getJsonValue(top[FPSTR(_readInterval)],    readInterval,        (uint32_t)5000);
+    if (readInterval < 50) { readInterval = 50; configComplete = false; }
     configComplete &= getJsonValue(top[FPSTR(_i2cAddress)],      i2cAddress,          (uint8_t)0x40);
+    if (i2cAddress != 0x40 && i2cAddress != 0x41 && i2cAddress != 0x44 && i2cAddress != 0x45) {
+      USER_PRINTLN(F("[INA219]: invalid i2cAddress, reset to 0x40."));
+      i2cAddress = 0x40;
+      configComplete = false;
+    }
     configComplete &= getJsonValue(top[FPSTR(_shuntResistor)],   shuntResistor_mOhm,  100.0f);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
configComplete &= getJsonValue(top[F("enabled")], enabled, false);
configComplete &= getJsonValue(top[FPSTR(_readInterval)], readInterval, (uint32_t)5000);
configComplete &= getJsonValue(top[FPSTR(_i2cAddress)], i2cAddress, (uint8_t)0x40);
configComplete &= getJsonValue(top[FPSTR(_shuntResistor)], shuntResistor_mOhm, 100.0f);
if (shuntResistor_mOhm < 1.0f) {
USER_PRINTLN(F("[INA219]: shuntResistor-mOhm clamped to minimum 1 mOhm."));
shuntResistor_mOhm = 1.0f;
}
configComplete &= getJsonValue(top[FPSTR(_maxCurrentRange)], maxCurrentRange_A, 2.0f);
configComplete &= getJsonValue(top[FPSTR(_busVoltageRange)], busVoltageRange_V, (uint8_t)32);
configComplete &= getJsonValue(top[F("enabled")], enabled, false);
configComplete &= getJsonValue(top[FPSTR(_readInterval)], readInterval, (uint32_t)5000);
if (readInterval < 50) { readInterval = 50; configComplete = false; }
configComplete &= getJsonValue(top[FPSTR(_i2cAddress)], i2cAddress, (uint8_t)0x40);
if (i2cAddress != 0x40 && i2cAddress != 0x41 && i2cAddress != 0x44 && i2cAddress != 0x45) {
USER_PRINTLN(F("[INA219]: invalid i2cAddress, reset to 0x40."));
i2cAddress = 0x40;
configComplete = false;
}
configComplete &= getJsonValue(top[FPSTR(_shuntResistor)], shuntResistor_mOhm, 100.0f);
if (shuntResistor_mOhm < 1.0f) {
USER_PRINTLN(F("[INA219]: shuntResistor-mOhm clamped to minimum 1 mOhm."));
shuntResistor_mOhm = 1.0f;
}
configComplete &= getJsonValue(top[FPSTR(_maxCurrentRange)], maxCurrentRange_A, 2.0f);
configComplete &= getJsonValue(top[FPSTR(_busVoltageRange)], busVoltageRange_V, (uint8_t)32);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/INA219_v2/usermod_ina219.h` around lines 190 - 199, After reading
JSON into i2cAddress, readInterval and busVoltageRange_V via getJsonValue, add
validation and clamping: validate i2cAddress against the supported INA219
addresses (0x40,0x41,0x44,0x45) and if not one of those set a safe default (e.g.
0x40) and log the correction; ensure readInterval is >= 1 (or a chosen minimum)
and if zero or too small set it to the minimum and log; for busVoltageRange_V
only accept 16 or 32 (clamp or default to 32) and log adjustments. Place these
checks immediately after the getJsonValue lines (near variables i2cAddress,
readInterval, busVoltageRange_V) so begin() will receive valid values. Ensure
configComplete remains accurate if values were corrected and keep the existing
shuntResistor_mOhm min clamp logic.


if (!initDone) {
DEBUG_PRINTLN(F("[INA219] config loaded."));
} else {
DEBUG_PRINTLN(F("[INA219] config (re)loaded."));
if (oldAddress != i2cAddress) {
setup(); // reinitialize sensor with new I2C address
} else {
applyCalibration(); // update PGA gain for new voltage/current range
}
}
Comment on lines +201 to +210
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Re-entering setup() from readFromConfig() — verify behavior when disabled.

At Line 206, setup() is invoked on I²C-address change. If the user simultaneously toggled enabled to false, setup() short-circuits at Line 80, but sensorFound/ina219 keep their previous values. That's benign, but toggling enabled from falsetrue via config edit won't re-run setup() at all (no address change path) — the sensor will stay uninitialized until reboot. Consider forcing re-init when enabled transitions to true as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/INA219_v2/usermod_ina219.h` around lines 201 - 210, readFromConfig()
currently only calls setup() when the I2C address changes, which misses the case
where enabled transitions from false→true; update readFromConfig() to track the
previous enabled state (e.g. oldEnabled) and if oldEnabled != enabled and
enabled == true call setup() to force sensor initialization (use same setup()
that handles I2C init and sets sensorFound/ina219), and when enabled transitions
to false clear or reset sensorFound and ina219 to avoid stale state; keep the
existing address-change logic but ensure both paths handle the enabled flag
consistently.

return configComplete;
}

uint16_t getId() override { return USERMOD_ID_INA219; }
};

// PROGMEM string definitions
const char UsermodINA219::_readInterval[] PROGMEM = "readInterval-ms";
const char UsermodINA219::_i2cAddress[] PROGMEM = "i2cAddress";
const char UsermodINA219::_shuntResistor[] PROGMEM = "shuntResistor-mOhm";
const char UsermodINA219::_maxCurrentRange[] PROGMEM = "maxCurrentRange-A";
const char UsermodINA219::_busVoltageRange[] PROGMEM = "busVoltageRange-V";
1 change: 1 addition & 0 deletions wled00/const.h
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
#define USERMOD_ID_WIREGUARD 41 //Usermod "wireguard.h"
#define USERMOD_ID_INTERNAL_TEMPERATURE 42 //Usermod "usermod_internal_temperature.h"
#define USERMOD_ID_LDR_DUSK_DAWN 43 //Usermod "usermod_LDR_Dusk_Dawn_v2.h"
#define USERMOD_ID_INA219 44 //Usermod "usermod_ina219.h"
//WLEDMM
#define USERMOD_ID_MCUTEMP 89 //Usermod "usermod_v2_artifx.h"
#define USERMOD_ID_ARTIFX 90 //Usermod "usermod_v2_artifx.h"
Expand Down
8 changes: 8 additions & 0 deletions wled00/usermods_list.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@
#include "../usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h"
#endif

#ifdef USERMOD_INA219
#include "../usermods/INA219_v2/usermod_ina219.h"
#endif

//WLEDMM ARTIFX
#ifdef USERMOD_ARTIFX
#include "../usermods/artifx/usermod_v2_artifx.h"
Expand Down Expand Up @@ -383,6 +387,10 @@ void registerUsermods()
usermods.add(new LDR_Dusk_Dawn_v2());
#endif

#ifdef USERMOD_INA219
usermods.add(new UsermodINA219("INA219", false));
#endif


// WLEDMM ARTIFX
#ifdef USERMOD_ARTIFX
Expand Down
Loading