diff --git a/platformio.ini b/platformio.ini
index a6ad6f873..4c6bc9bfa 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -113,6 +113,7 @@ lib_deps =
https://github.com/boschsensortec/Bosch-BSEC2-Library#v1.5.2400
boschsensortec/BME68x Sensor Library@^1.1.40407
adafruit/Adafruit MCP9808 Library@^2.0.0
+ https://github.com/Tinyu-Zhao/INA3221@^0.0.1
adafruit/Adafruit INA260 Library@^1.5.0
adafruit/Adafruit INA219@^1.2.0
adafruit/Adafruit SHTC3 Library@^1.0.0
diff --git a/src/configuration.h b/src/configuration.h
index 2640b1572..b6b272097 100644
--- a/src/configuration.h
+++ b/src/configuration.h
@@ -111,6 +111,7 @@ along with this program. If not, see .
#define MCP9808_ADDR 0x18
#define INA_ADDR 0x40
#define INA_ADDR_ALTERNATE 0x41
+#define INA3221_ADDR 0x42
#define QMC6310_ADDR 0x1C
#define QMI8658_ADDR 0x6B
#define QMC5883L_ADDR 0x1E
@@ -205,4 +206,4 @@ along with this program. If not, see .
#ifndef HW_VENDOR
#error HW_VENDOR must be defined
-#endif
\ No newline at end of file
+#endif
diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h
index 15d9b1342..2b4b8a735 100644
--- a/src/detect/ScanI2C.h
+++ b/src/detect/ScanI2C.h
@@ -25,6 +25,7 @@ class ScanI2C
BMP_280,
INA260,
INA219,
+ INA3221,
MCP9808,
SHT31,
SHTC3,
diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp
index ced1e34dd..b3873dc91 100644
--- a/src/detect/ScanI2CTwoWire.cpp
+++ b/src/detect/ScanI2CTwoWire.cpp
@@ -251,7 +251,10 @@ void ScanI2CTwoWire::scanPort(I2CPort port)
type = INA219;
}
break;
-
+ case INA3221_ADDR:
+ LOG_INFO("INA3221 sensor found at address 0x%x\n", (uint8_t)addr.address);
+ type = INA3221;
+ break;
case MCP9808_ADDR:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x07), 2);
if (registerValue == 0x0400) {
diff --git a/src/main.cpp b/src/main.cpp
index 9b7d811c4..a18ee4099 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -502,6 +502,7 @@ void setup()
SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::BMP_280, meshtastic_TelemetrySensorType_BMP280)
SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::INA260, meshtastic_TelemetrySensorType_INA260)
SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::INA219, meshtastic_TelemetrySensorType_INA219)
+ SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::INA3221, meshtastic_TelemetrySensorType_INA3221)
SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::MCP9808, meshtastic_TelemetrySensorType_MCP9808)
SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::MCP9808, meshtastic_TelemetrySensorType_MCP9808)
SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::SHT31, meshtastic_TelemetrySensorType_SHT31)
diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp
index 4c3d7eb61..526a1c7d8 100644
--- a/src/modules/Modules.cpp
+++ b/src/modules/Modules.cpp
@@ -24,6 +24,9 @@
#include "modules/Telemetry/AirQualityTelemetry.h"
#include "modules/Telemetry/EnvironmentTelemetry.h"
#endif
+#if HAS_TELEMETRY && !defined(ARCH_PORTDUINO)
+#include "modules/Telemetry/PowerTelemetry.h"
+#endif
#ifdef ARCH_ESP32
#include "modules/esp32/AudioModule.h"
#include "modules/esp32/StoreForwardModule.h"
@@ -92,6 +95,9 @@ void setupModules()
new AirQualityTelemetryModule();
}
#endif
+#if HAS_TELEMETRY && !defined(ARCH_PORTDUINO)
+ new PowerTelemetryModule();
+#endif
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \
!defined(CONFIG_IDF_TARGET_ESP32C3)
new SerialModule();
diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp
new file mode 100644
index 000000000..53e26ee6a
--- /dev/null
+++ b/src/modules/Telemetry/PowerTelemetry.cpp
@@ -0,0 +1,240 @@
+#include "PowerTelemetry.h"
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "MeshService.h"
+#include "NodeDB.h"
+#include "PowerFSM.h"
+#include "RTC.h"
+#include "Router.h"
+#include "configuration.h"
+#include "main.h"
+#include "power.h"
+#include "sleep.h"
+#include "target_specific.h"
+
+#if HAS_TELEMETRY && !defined(ARCH_PORTDUINO)
+#include "Sensor/INA3221Sensor.h"
+INA3221Sensor ina3221Sensor;
+#endif
+
+#define FAILED_STATE_SENSOR_READ_MULTIPLIER 10
+#define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true
+
+#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7735_CS) || defined(ST7789_CS)) && \
+ !defined(DISPLAY_FORCE_SMALL_FONTS)
+
+// The screen is bigger so use bigger fonts
+#define FONT_SMALL ArialMT_Plain_16
+#define FONT_MEDIUM ArialMT_Plain_24
+#define FONT_LARGE ArialMT_Plain_24
+#else
+#define FONT_SMALL ArialMT_Plain_10
+#define FONT_MEDIUM ArialMT_Plain_16
+#define FONT_LARGE ArialMT_Plain_24
+#endif
+
+#define fontHeight(font) ((font)[1] + 1) // height is position 1
+
+#define FONT_HEIGHT_SMALL fontHeight(FONT_SMALL)
+#define FONT_HEIGHT_MEDIUM fontHeight(FONT_MEDIUM)
+
+int32_t PowerTelemetryModule::runOnce()
+{
+ if (sleepOnNextExecution == true) {
+ sleepOnNextExecution = false;
+ uint32_t nightyNightMs = getConfiguredOrDefaultMs(moduleConfig.telemetry.power_update_interval);
+ LOG_DEBUG("Sleeping for %ims, then awaking to send metrics again.\n", nightyNightMs);
+ doDeepSleep(nightyNightMs, true);
+ }
+
+ uint32_t result = UINT32_MAX;
+ /*
+ Uncomment the preferences below if you want to use the module
+ without having to configure it from the PythonAPI or WebUI.
+ */
+
+ // moduleConfig.telemetry.power_measurement_enabled = 1;
+ // moduleConfig.telemetry.power_screen_enabled = 1;
+ // moduleConfig.telemetry.power_update_interval = 45;
+
+ if (!(moduleConfig.telemetry.power_measurement_enabled)) {
+ // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it
+ return disable();
+ }
+
+ if (firstTime) {
+ // This is the first time the OSThread library has called this function, so do some setup
+ firstTime = 0;
+#if HAS_TELEMETRY && !defined(ARCH_PORTDUINO)
+ if (moduleConfig.telemetry.power_measurement_enabled) {
+ LOG_INFO("Power Telemetry: Initializing\n");
+ // it's possible to have this module enabled, only for displaying values on the screen.
+ // therefore, we should only enable the sensor loop if measurement is also enabled
+ if (ina219Sensor.hasSensor() && !ina219Sensor.isInitialized())
+ result = ina219Sensor.runOnce();
+ if (ina260Sensor.hasSensor() && !ina260Sensor.isInitialized())
+ result = ina260Sensor.runOnce();
+ if (ina3221Sensor.hasSensor() && !ina3221Sensor.isInitialized())
+ result = ina3221Sensor.runOnce();
+ }
+ return result;
+#else
+ return disable();
+#endif
+ } else {
+ // if we somehow got to a second run of this module with measurement disabled, then just wait forever
+ if (!moduleConfig.telemetry.power_measurement_enabled)
+ return disable();
+
+ uint32_t now = millis();
+ if (((lastSentToMesh == 0) ||
+ ((now - lastSentToMesh) >= getConfiguredOrDefaultMs(moduleConfig.telemetry.power_update_interval))) &&
+ airTime->isTxAllowedAirUtil()) {
+ sendTelemetry();
+ lastSentToMesh = now;
+ } else if (((lastSentToPhone == 0) || ((now - lastSentToPhone) >= sendToPhoneIntervalMs)) &&
+ (service.isToPhoneQueueEmpty())) {
+ // Just send to phone when it's not our time to send to mesh yet
+ // Only send while queue is empty (phone assumed connected)
+ sendTelemetry(NODENUM_BROADCAST, true);
+ lastSentToPhone = now;
+ }
+ }
+ return min(sendToPhoneIntervalMs, result);
+}
+bool PowerTelemetryModule::wantUIFrame()
+{
+ return moduleConfig.telemetry.power_screen_enabled;
+}
+
+uint32_t GetTimeyWimeySinceMeshPacket(const meshtastic_MeshPacket *mp)
+{
+ uint32_t now = getTime();
+
+ uint32_t last_seen = mp->rx_time;
+ int delta = (int)(now - last_seen);
+ if (delta < 0) // our clock must be slightly off still - not set from GPS yet
+ delta = 0;
+
+ return delta;
+}
+
+void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
+{
+ display->setTextAlignment(TEXT_ALIGN_LEFT);
+ display->setFont(FONT_MEDIUM);
+ display->drawString(x, y, "Power Telemetry");
+ if (lastMeasurementPacket == nullptr) {
+ display->setFont(FONT_SMALL);
+ display->drawString(x, y += fontHeight(FONT_MEDIUM), "No measurement");
+ return;
+ }
+
+ meshtastic_Telemetry lastMeasurement;
+
+ uint32_t agoSecs = GetTimeyWimeySinceMeshPacket(lastMeasurementPacket);
+ const char *lastSender = getSenderShortName(*lastMeasurementPacket);
+
+ auto &p = lastMeasurementPacket->decoded;
+ if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) {
+ display->setFont(FONT_SMALL);
+ display->drawString(x, y += fontHeight(FONT_MEDIUM), "Measurement Error");
+ LOG_ERROR("Unable to decode last packet");
+ return;
+ }
+
+ 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) {
+ 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");
+ 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");
+ 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");
+ }
+}
+
+bool PowerTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
+{
+ if (t->which_variant == meshtastic_Telemetry_power_metrics_tag) {
+#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",
+ 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);
+#endif
+ // release previous packet before occupying a new spot
+ if (lastMeasurementPacket != nullptr)
+ packetPool.release(lastMeasurementPacket);
+
+ lastMeasurementPacket = packetPool.allocCopy(mp);
+ }
+
+ return false; // Let others look at this message also if they want
+}
+
+bool PowerTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
+{
+ meshtastic_Telemetry m;
+ bool valid = false;
+ 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;
+#if HAS_TELEMETRY && !defined(ARCH_PORTDUINO)
+ if (ina219Sensor.hasSensor())
+ valid = ina219Sensor.getMetrics(&m);
+ if (ina260Sensor.hasSensor())
+ valid = ina260Sensor.getMetrics(&m);
+ if (ina3221Sensor.hasSensor())
+ valid = ina3221Sensor.getMetrics(&m);
+#endif
+
+ if (valid) {
+ LOG_INFO("(Sending): ch1_voltage=%f, ch1_current=%f, ch2_voltage=%f, ch2_current=%f, "
+ "ch3_voltage=%f, ch3_current=%f\n",
+ m.variant.power_metrics.ch1_voltage, m.variant.power_metrics.ch1_current, m.variant.power_metrics.ch2_voltage,
+ m.variant.power_metrics.ch2_current, m.variant.power_metrics.ch3_voltage, m.variant.power_metrics.ch3_current);
+
+ sensor_read_error_count = 0;
+
+ meshtastic_MeshPacket *p = allocDataProtobuf(m);
+ p->to = dest;
+ p->decoded.want_response = false;
+ if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR)
+ p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
+ else
+ p->priority = meshtastic_MeshPacket_Priority_MIN;
+ // release previous packet before occupying a new spot
+ if (lastMeasurementPacket != nullptr)
+ packetPool.release(lastMeasurementPacket);
+
+ lastMeasurementPacket = packetPool.allocCopy(*p);
+ if (phoneOnly) {
+ LOG_INFO("Sending packet to phone\n");
+ service.sendToPhone(p);
+ } else {
+ LOG_INFO("Sending packet to mesh\n");
+ service.sendToMesh(p, RX_SRC_LOCAL, true);
+
+ if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) {
+ LOG_DEBUG("Starting next execution in 5 seconds and then going to sleep.\n");
+ sleepOnNextExecution = true;
+ setIntervalFromNow(5000);
+ }
+ }
+ }
+ return valid;
+}
\ No newline at end of file
diff --git a/src/modules/Telemetry/PowerTelemetry.h b/src/modules/Telemetry/PowerTelemetry.h
new file mode 100644
index 000000000..fc5b98875
--- /dev/null
+++ b/src/modules/Telemetry/PowerTelemetry.h
@@ -0,0 +1,43 @@
+#pragma once
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "NodeDB.h"
+#include "ProtobufModule.h"
+#include
+#include
+
+class PowerTelemetryModule : private concurrency::OSThread, public ProtobufModule
+{
+ public:
+ PowerTelemetryModule()
+ : concurrency::OSThread("PowerTelemetryModule"),
+ ProtobufModule("PowerTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg)
+ {
+ lastMeasurementPacket = nullptr;
+ setIntervalFromNow(10 * 1000);
+ }
+ virtual bool wantUIFrame() override;
+#if !HAS_SCREEN
+ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
+#else
+ virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override;
+#endif
+
+ protected:
+ /** Called to handle a particular incoming message
+ @return true if you've guaranteed you've handled this message and no other handlers should be considered for it
+ */
+ virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override;
+ virtual int32_t runOnce() override;
+ /**
+ * Send our Telemetry into the mesh
+ */
+ bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
+
+ private:
+ bool firstTime = 1;
+ meshtastic_MeshPacket *lastMeasurementPacket;
+ uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
+ uint32_t lastSentToMesh = 0;
+ uint32_t lastSentToPhone = 0;
+ uint32_t sensor_read_error_count = 0;
+};
diff --git a/src/modules/Telemetry/Sensor/INA3221Sensor.cpp b/src/modules/Telemetry/Sensor/INA3221Sensor.cpp
new file mode 100644
index 000000000..634f5a5c9
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/INA3221Sensor.cpp
@@ -0,0 +1,43 @@
+#include "INA3221Sensor.h"
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "TelemetrySensor.h"
+#include "configuration.h"
+#include
+
+INA3221Sensor::INA3221Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_INA3221, "INA3221"){};
+
+int32_t INA3221Sensor::runOnce()
+{
+ LOG_INFO("Init sensor: %s\n", sensorName);
+ if (!hasSensor()) {
+ return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
+ }
+ if (!status) {
+ ina3221.setAddr(INA3221_ADDR42_SDA);
+ ina3221.begin();
+ status = true;
+ } else {
+ status = true;
+ }
+ return initI2CSensor();
+};
+
+void INA3221Sensor::setup() {}
+
+bool INA3221Sensor::getMetrics(meshtastic_Telemetry *measurement)
+{
+ measurement->variant.environment_metrics.voltage = ina3221.getVoltage(INA3221_CH1);
+ measurement->variant.environment_metrics.current = ina3221.getCurrent(INA3221_CH1);
+ measurement->variant.power_metrics.ch1_voltage = ina3221.getVoltage(INA3221_CH1);
+ measurement->variant.power_metrics.ch1_current = ina3221.getCurrent(INA3221_CH1);
+ measurement->variant.power_metrics.ch2_voltage = ina3221.getVoltage(INA3221_CH2);
+ measurement->variant.power_metrics.ch2_current = ina3221.getCurrent(INA3221_CH2);
+ measurement->variant.power_metrics.ch3_voltage = ina3221.getVoltage(INA3221_CH3);
+ measurement->variant.power_metrics.ch3_current = ina3221.getCurrent(INA3221_CH3);
+ return true;
+}
+
+uint16_t INA3221Sensor::getBusVoltageMv()
+{
+ return lround(ina3221.getVoltage(INA3221_CH1) * 1000);
+}
\ No newline at end of file
diff --git a/src/modules/Telemetry/Sensor/INA3221Sensor.h b/src/modules/Telemetry/Sensor/INA3221Sensor.h
new file mode 100644
index 000000000..a1c0fb2a7
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/INA3221Sensor.h
@@ -0,0 +1,16 @@
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "TelemetrySensor.h"
+#include
+
+class INA3221Sensor : public TelemetrySensor
+{
+ public:
+ INA3221Sensor();
+ int32_t runOnce() override;
+ void setup() override;
+ bool getMetrics(meshtastic_Telemetry *measurement) override;
+ virtual uint16_t getBusVoltageMv();
+
+ private:
+ INA3221 ina3221 = INA3221(INA3221_ADDR42_SDA);
+};
\ No newline at end of file
diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp
index a9e80c947..29a634922 100644
--- a/src/mqtt/MQTT.cpp
+++ b/src/mqtt/MQTT.cpp
@@ -565,6 +565,13 @@ std::string MQTT::meshPacketToJson(meshtastic_MeshPacket *mp)
msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance);
msgPayload["voltage"] = new JSONValue(decoded->variant.environment_metrics.voltage);
msgPayload["current"] = new JSONValue(decoded->variant.environment_metrics.current);
+ } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
+ msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage);
+ msgPayload["current_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_current);
+ msgPayload["voltage_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_voltage);
+ msgPayload["current_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_current);
+ msgPayload["voltage_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_voltage);
+ msgPayload["current_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_current);
}
jsonObj["payload"] = new JSONValue(msgPayload);
} else {