Skip to content

Commit

Permalink
INA3221 / Power Telemetry Payload Variant Implementation (#2916)
Browse files Browse the repository at this point in the history
* INA3221 / Power Telemetry Variant Implementation
	modified:   platformio.ini
	modified:   src/configuration.h
	modified:   src/detect/ScanI2C.h
	modified:   src/detect/ScanI2CTwoWire.cpp
	modified:   src/main.cpp
	modified:   src/modules/Modules.cpp
	new file:   src/modules/Telemetry/PowerTelemetry.cpp
	new file:   src/modules/Telemetry/PowerTelemetry.h
	new file:   src/modules/Telemetry/Sensor/INA3221Sensor.cpp
	new file:   src/modules/Telemetry/Sensor/INA3221Sensor.h
	modified:   src/mqtt/MQTT.cpp

* ifdef for portduino / linux native
	modified:   src/modules/Telemetry/PowerTelemetry.cpp

* try #2
	modified:   src/modules/Modules.cpp
	modified:   src/modules/Telemetry/PowerTelemetry.cpp
	deleted:    variants/xiao_ble/1.0.0/libraries/SPI/SPI.cpp

* try #3
	modified:   src/modules/Modules.cpp

* try #4
	modified:   src/modules/Telemetry/PowerTelemetry.cpp

* try #5?
	modified:   src/modules/Telemetry/PowerTelemetry.cpp

* try #6
	modified:   src/modules/Telemetry/PowerTelemetry.cpp

---------

Co-authored-by: Ben Meadors <[email protected]>
  • Loading branch information
RicInNewMexico and thebentern authored Nov 5, 2023
1 parent 4a6cc8f commit f570204
Show file tree
Hide file tree
Showing 11 changed files with 364 additions and 2 deletions.
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#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
Expand Down Expand Up @@ -205,4 +206,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.

#ifndef HW_VENDOR
#error HW_VENDOR must be defined
#endif
#endif
1 change: 1 addition & 0 deletions src/detect/ScanI2C.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class ScanI2C
BMP_280,
INA260,
INA219,
INA3221,
MCP9808,
SHT31,
SHTC3,
Expand Down
5 changes: 4 additions & 1 deletion src/detect/ScanI2CTwoWire.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions src/modules/Modules.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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();
Expand Down
240 changes: 240 additions & 0 deletions src/modules/Telemetry/PowerTelemetry.cpp
Original file line number Diff line number Diff line change
@@ -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;
}
43 changes: 43 additions & 0 deletions src/modules/Telemetry/PowerTelemetry.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#pragma once
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "NodeDB.h"
#include "ProtobufModule.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>

class PowerTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
{
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;
};
Loading

0 comments on commit f570204

Please sign in to comment.