Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
adafruit/Adafruit INA219 @ 1.2.1
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

extra_scripts = ${scripts_defaults.extra_scripts}

Expand Down
207 changes: 207 additions & 0 deletions usermods/INA219_v2/usermod_ina219.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// #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.
void applyCalibration() {
if (!ina219) return;
if (busVoltageRange_V <= 16 && maxCurrentRange_A <= 0.4f)
ina219->setCalibration_16V_400mA();
else if (maxCurrentRange_A <= 1.0f)
ina219->setCalibration_32V_1A();
else
ina219->setCalibration_32V_2A();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

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);
current_mA = shuntVoltage_mV / (shuntResistor_mOhm / 1000.0f); // I = U / R
power_mW = current_mA * loadVoltage_V;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

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);
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