Merge branch 'master' into esp32-c6

This commit is contained in:
Thomas Göttgens
2024-09-26 00:20:00 +02:00
committed by GitHub
134 changed files with 2895 additions and 1171 deletions

View File

@@ -34,8 +34,8 @@
#include "modules/PositionModule.h"
#endif
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "AccelerometerThread.h"
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
#include "motion/AccelerometerThread.h"
#endif
AdminModule *adminModule;

View File

@@ -27,6 +27,7 @@
#endif
#include "graphics/ScreenFonts.h"
#include <Throttle.h>
// Remove Canned message screen if no action is taken for some milliseconds
#define INACTIVATE_AFTER_MS 20000
@@ -275,6 +276,13 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
showTemporaryMessage("Node Info \nUpdate Sent");
}
break;
case INPUT_BROKER_MSG_DISMISS_FRAME: // fn+del: dismiss screen frames like text or waypoint
// Avoid opening the canned message screen frame
// We're only handling the keypress here by convention, this has nothing to do with canned messages
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
// Attempt to close whatever frame is currently shown on display
screen->dismissCurrentFrame();
return 0;
default:
// pass the pressed key
// LOG_DEBUG("Canned message ANYKEY (%x)\n", event->kbchar);
@@ -422,7 +430,7 @@ int32_t CannedMessageModule::runOnce()
this->notifyObservers(&e);
} else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) &&
((millis() - this->lastTouchMillis) > INACTIVATE_AFTER_MS)) {
!Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) {
// Reset module
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen
this->currentMessageIndex = -1;
@@ -934,6 +942,19 @@ void CannedMessageModule::drawEnterIcon(OLEDDisplay *display, int x, int y, floa
#endif
// Indicate to screen class that module is handling keyboard input specially (at certain times)
// This prevents the left & right keys being used for nav. between screen frames during text entry.
bool CannedMessageModule::interceptingKeyboardInput()
{
switch (runState) {
case CANNED_MESSAGE_RUN_STATE_DISABLED:
case CANNED_MESSAGE_RUN_STATE_INACTIVE:
return false;
default:
return true;
}
}
void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
char buffer[50];

View File

@@ -114,6 +114,7 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
virtual bool wantUIFrame() override { return this->shouldDraw(); }
virtual Observable<const UIFrameEvent *> *getUIFrameObservable() override { return this; }
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override;
virtual bool interceptingKeyboardInput() override;
virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response) override;

View File

@@ -5,11 +5,47 @@
#include "PowerFSM.h"
#include "configuration.h"
#include "main.h"
#include <Throttle.h>
DetectionSensorModule *detectionSensorModule;
#define GPIO_POLLING_INTERVAL 100
#define DELAYED_INTERVAL 1000
typedef enum {
DetectionSensorVerdictDetected,
DetectionSensorVerdictSendState,
DetectionSensorVerdictNoop,
} DetectionSensorTriggerVerdict;
typedef DetectionSensorTriggerVerdict (*DetectionSensorTriggerHandler)(bool prev, bool current);
static DetectionSensorTriggerVerdict detection_trigger_logic_level(bool prev, bool current)
{
return current ? DetectionSensorVerdictDetected : DetectionSensorVerdictNoop;
}
static DetectionSensorTriggerVerdict detection_trigger_single_edge(bool prev, bool current)
{
return (!prev && current) ? DetectionSensorVerdictDetected : DetectionSensorVerdictNoop;
}
static DetectionSensorTriggerVerdict detection_trigger_either_edge(bool prev, bool current)
{
if (prev == current) {
return DetectionSensorVerdictNoop;
}
return current ? DetectionSensorVerdictDetected : DetectionSensorVerdictSendState;
}
const static DetectionSensorTriggerHandler handlers[_meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MAX + 1] = {
[meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_LOW] = detection_trigger_logic_level,
[meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH] = detection_trigger_logic_level,
[meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_FALLING_EDGE] = detection_trigger_single_edge,
[meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_RISING_EDGE] = detection_trigger_single_edge,
[meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_EITHER_EDGE_ACTIVE_LOW] = detection_trigger_either_edge,
[meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_EITHER_EDGE_ACTIVE_HIGH] = detection_trigger_either_edge,
};
int32_t DetectionSensorModule::runOnce()
{
/*
@@ -21,7 +57,8 @@ int32_t DetectionSensorModule::runOnce()
// moduleConfig.detection_sensor.monitor_pin = 21; // WisBlock RAK12013 Radar IO6
// moduleConfig.detection_sensor.minimum_broadcast_secs = 30;
// moduleConfig.detection_sensor.state_broadcast_secs = 120;
// moduleConfig.detection_sensor.detection_triggered_high = true;
// moduleConfig.detection_sensor.detection_trigger_type =
// meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH;
// strcpy(moduleConfig.detection_sensor.name, "Motion");
if (moduleConfig.detection_sensor.enabled == false)
@@ -49,18 +86,31 @@ int32_t DetectionSensorModule::runOnce()
// LOG_DEBUG("Detection Sensor Module: Current pin state: %i\n", digitalRead(moduleConfig.detection_sensor.monitor_pin));
if ((millis() - lastSentToMesh) >= Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.minimum_broadcast_secs) &&
hasDetectionEvent()) {
sendDetectionMessage();
return DELAYED_INTERVAL;
if (!Throttle::isWithinTimespanMs(lastSentToMesh,
Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.minimum_broadcast_secs))) {
bool isDetected = hasDetectionEvent();
DetectionSensorTriggerVerdict verdict =
handlers[moduleConfig.detection_sensor.detection_trigger_type](wasDetected, isDetected);
wasDetected = isDetected;
switch (verdict) {
case DetectionSensorVerdictDetected:
sendDetectionMessage();
return DELAYED_INTERVAL;
case DetectionSensorVerdictSendState:
sendCurrentStateMessage(isDetected);
return DELAYED_INTERVAL;
case DetectionSensorVerdictNoop:
break;
}
}
// Even if we haven't detected an event, broadcast our current state to the mesh on the scheduled interval as a sort
// of heartbeat. We only do this if the minimum broadcast interval is greater than zero, otherwise we'll only broadcast state
// change detections.
else if (moduleConfig.detection_sensor.state_broadcast_secs > 0 &&
(millis() - lastSentToMesh) >= Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.state_broadcast_secs,
default_telemetry_broadcast_interval_secs)) {
sendCurrentStateMessage();
if (moduleConfig.detection_sensor.state_broadcast_secs > 0 &&
!Throttle::isWithinTimespanMs(lastSentToMesh,
Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.state_broadcast_secs,
default_telemetry_broadcast_interval_secs))) {
sendCurrentStateMessage(hasDetectionEvent());
return DELAYED_INTERVAL;
}
return GPIO_POLLING_INTERVAL;
@@ -86,10 +136,10 @@ void DetectionSensorModule::sendDetectionMessage()
delete[] message;
}
void DetectionSensorModule::sendCurrentStateMessage()
void DetectionSensorModule::sendCurrentStateMessage(bool state)
{
char *message = new char[40];
sprintf(message, "%s state: %i", moduleConfig.detection_sensor.name, hasDetectionEvent());
sprintf(message, "%s state: %i", moduleConfig.detection_sensor.name, state);
meshtastic_MeshPacket *p = allocDataPacket();
p->want_ack = false;
@@ -105,5 +155,5 @@ bool DetectionSensorModule::hasDetectionEvent()
{
bool currentState = digitalRead(moduleConfig.detection_sensor.monitor_pin);
// LOG_DEBUG("Detection Sensor Module: Current state: %i\n", currentState);
return moduleConfig.detection_sensor.detection_triggered_high ? currentState : !currentState;
return (moduleConfig.detection_sensor.detection_trigger_type & 1) ? currentState : !currentState;
}

View File

@@ -15,8 +15,9 @@ class DetectionSensorModule : public SinglePortModule, private concurrency::OSTh
private:
bool firstTime = true;
uint32_t lastSentToMesh = 0;
bool wasDetected = false;
void sendDetectionMessage();
void sendCurrentStateMessage();
void sendCurrentStateMessage(bool state);
bool hasDetectionEvent();
};

View File

@@ -3,6 +3,7 @@
#include "MeshService.h"
#include "NodeDB.h"
#include "RTC.h"
#include <Throttle.h>
NeighborInfoModule *neighborInfoModule;
@@ -83,11 +84,11 @@ uint32_t NeighborInfoModule::collectNeighborInfo(meshtastic_NeighborInfo *neighb
*/
void NeighborInfoModule::cleanUpNeighbors()
{
uint32_t now = getTime();
NodeNum my_node_id = nodeDB->getNodeNum();
for (auto it = neighbors.rbegin(); it != neighbors.rend();) {
// We will remove a neighbor if we haven't heard from them in twice the broadcast interval
if ((now - it->last_rx_time > it->node_broadcast_interval_secs * 2) && (it->node_id != my_node_id)) {
if (!Throttle::isWithinTimespanMs(it->last_rx_time, it->node_broadcast_interval_secs * 2) &&
(it->node_id != my_node_id)) {
LOG_DEBUG("Removing neighbor with node ID 0x%x\n", it->node_id);
it = std::vector<meshtastic_Neighbor>::reverse_iterator(
neighbors.erase(std::next(it).base())); // Erase the element and update the iterator

View File

@@ -6,6 +6,7 @@
#include "Router.h"
#include "configuration.h"
#include "main.h"
#include <Throttle.h>
NodeInfoModule *nodeInfoModule;
@@ -67,13 +68,12 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply()
LOG_DEBUG("Skip sending NodeInfo due to > 40 percent channel util.\n");
return NULL;
}
uint32_t now = millis();
// If we sent our NodeInfo less than 5 min. ago, don't send it again as it may be still underway.
if (!shorterTimeout && lastSentToMesh && (now - lastSentToMesh) < (5 * 60 * 1000)) {
if (!shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 5 * 60 * 1000)) {
LOG_DEBUG("Skip sending NodeInfo since we just sent it less than 5 minutes ago.\n");
ignoreRequest = true; // Mark it as ignored for MeshModule
return NULL;
} else if (shorterTimeout && lastSentToMesh && (now - lastSentToMesh) < (60 * 1000)) {
} else if (shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 60 * 1000)) {
LOG_DEBUG("Skip sending actively requested NodeInfo since we just sent it less than 60 seconds ago.\n");
ignoreRequest = true; // Mark it as ignored for MeshModule
return NULL;
@@ -82,7 +82,7 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply()
meshtastic_User &u = owner;
LOG_INFO("sending owner %s/%s/%s\n", u.id, u.long_name, u.short_name);
lastSentToMesh = now;
lastSentToMesh = millis();
return allocDataProtobuf(u);
}
}

View File

@@ -12,6 +12,7 @@
#include "gps/GeoCoord.h"
#include "main.h"
#include "mesh/compression/unishox2.h"
#include "meshUtils.h"
#include "meshtastic/atak.pb.h"
#include "sleep.h"
#include "target_specific.h"
@@ -145,7 +146,7 @@ void PositionModule::trySetRtc(meshtastic_Position p, bool isLocal, bool forceUp
bool PositionModule::hasQualityTimesource()
{
bool setFromPhoneOrNtpToday =
lastSetFromPhoneNtpOrGps == 0 ? false : (millis() - lastSetFromPhoneNtpOrGps) <= (SEC_PER_DAY * 1000UL);
lastSetFromPhoneNtpOrGps == 0 ? false : Throttle::isWithinTimespanMs(lastSetFromPhoneNtpOrGps, SEC_PER_DAY * 1000UL);
#if MESHTASTIC_EXCLUDE_GPS
bool hasGpsOrRtc = (rtc_found.address != ScanI2C::ADDRESS_NONE.address);
#else
@@ -347,8 +348,8 @@ void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t cha
service->sendToMesh(p, RX_SRC_LOCAL, true);
if ((config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER ||
config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) &&
if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER,
meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) &&
config.power.is_power_saving) {
LOG_DEBUG("Starting next execution in 5 seconds and then going to sleep.\n");
sleepOnNextExecution = true;

View File

@@ -9,6 +9,7 @@
#include "main.h"
#include "sleep.h"
#include "target_specific.h"
#include <Throttle.h>
extern void printInfo();
@@ -114,7 +115,7 @@ int32_t PowerStressModule::runOnce()
break;
case meshtastic_PowerStressMessage_Opcode_CPU_FULLON: {
uint32_t start_msec = millis();
while ((millis() - start_msec) < (uint32_t)sleep_msec)
while (Throttle::isWithinTimespanMs(start_msec, sleep_msec))
; // Don't let CPU idle at all
sleep_msec = 0; // we already slept
break;

View File

@@ -19,6 +19,7 @@
#include "configuration.h"
#include "gps/GeoCoord.h"
#include <Arduino.h>
#include <Throttle.h>
RangeTestModule *rangeTestModule;
RangeTestModuleRadio *rangeTestModuleRadio;
@@ -79,7 +80,7 @@ int32_t RangeTestModule::runOnce()
}
// If we have been running for more than 8 hours, turn module back off
if (millis() - started > 28800000) {
if (!Throttle::isWithinTimespanMs(started, 28800000)) {
LOG_INFO("Range Test Module - Disabling after 8 hours\n");
return disable();
} else {
@@ -114,7 +115,7 @@ void RangeTestModuleRadio::sendPayload(NodeNum dest, bool wantReplies)
packetSequence++;
static char heartbeatString[MAX_RHPACKETLEN];
static char heartbeatString[MAX_LORA_PAYLOAD_LEN + 1];
snprintf(heartbeatString, sizeof(heartbeatString), "seq %u", packetSequence);
p->decoded.payload.size = strlen(heartbeatString); // You must specify how many bytes are in the reply

View File

@@ -5,6 +5,7 @@
#include "Router.h"
#include "configuration.h"
#include "main.h"
#include <Throttle.h>
#define NUM_GPIOS 64
@@ -118,11 +119,10 @@ bool RemoteHardwareModule::handleReceivedProtobuf(const meshtastic_MeshPacket &r
int32_t RemoteHardwareModule::runOnce()
{
if (moduleConfig.remote_hardware.enabled && watchGpios) {
uint32_t now = millis();
if (now - lastWatchMsec >= WATCH_INTERVAL_MSEC) {
if (!Throttle::isWithinTimespanMs(lastWatchMsec, WATCH_INTERVAL_MSEC)) {
uint64_t curVal = digitalReads(watchGpios);
lastWatchMsec = now;
lastWatchMsec = millis();
if (curVal != previousWatch) {
previousWatch = curVal;

View File

@@ -7,6 +7,7 @@
#include "Router.h"
#include "configuration.h"
#include <Arduino.h>
#include <Throttle.h>
/*
SerialModule
@@ -100,8 +101,7 @@ SerialModuleRadio::SerialModuleRadio() : MeshModule("SerialModuleRadio")
*/
bool SerialModule::checkIsConnected()
{
uint32_t now = millis();
return (now - lastContactMsec) < SERIAL_CONNECTION_TIMEOUT;
return Throttle::isWithinTimespanMs(lastContactMsec, SERIAL_CONNECTION_TIMEOUT);
}
int32_t SerialModule::runOnce()
@@ -194,13 +194,13 @@ int32_t SerialModule::runOnce()
return runOncePart();
} else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA) && HAS_GPS) {
// in NMEA mode send out GGA every 2 seconds, Don't read from Port
if (millis() - lastNmeaTime > 2000) {
if (!Throttle::isWithinTimespanMs(lastNmeaTime, 2000)) {
lastNmeaTime = millis();
printGGA(outbuf, sizeof(outbuf), localPosition);
serialPrint->printf("%s", outbuf);
}
} else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) && HAS_GPS) {
if (millis() - lastNmeaTime > 10000) {
if (!Throttle::isWithinTimespanMs(lastNmeaTime, 10000)) {
lastNmeaTime = millis();
uint32_t readIndex = 0;
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
@@ -517,7 +517,7 @@ void SerialModule::processWXSerial()
LOG_INFO("WS85 : %i %.1fg%.1f %.1fv %.1fv\n", atoi(windDir), strtof(windVel, nullptr), strtof(windGust, nullptr),
batVoltageF, capVoltageF);
}
if (gotwind && millis() - lastAveraged > averageIntervalMillis) {
if (gotwind && !Throttle::isWithinTimespanMs(lastAveraged, averageIntervalMillis)) {
// calulate averages and send to the mesh
float velAvg = 1.0 * velSum / velCount;

View File

@@ -12,6 +12,7 @@
#include "Router.h"
#include "detect/ScanI2CTwoWire.h"
#include "main.h"
#include <Throttle.h>
int32_t AirQualityTelemetryModule::runOnce()
{
@@ -60,15 +61,14 @@ int32_t AirQualityTelemetryModule::runOnce()
if (!moduleConfig.telemetry.air_quality_enabled)
return disable();
uint32_t now = millis();
if (((lastSentToMesh == 0) ||
((now - lastSentToMesh) >= Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs,
numOnlineNodes))) &&
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
airTime->isTxAllowedAirUtil()) {
sendTelemetry();
lastSentToMesh = now;
lastSentToMesh = millis();
} else if (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)

View File

@@ -11,14 +11,15 @@
#include "main.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>
#include <meshUtils.h>
#define MAGIC_USB_BATTERY_LEVEL 101
int32_t DeviceTelemetryModule::runOnce()
{
refreshUptime();
bool isImpoliteRole = config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR ||
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER;
bool isImpoliteRole =
IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_ROUTER);
if (((lastSentToMesh == 0) ||
((uptimeLastMs - lastSentToMesh) >=
Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval,

View File

@@ -64,6 +64,7 @@ T1000xSensor t1000xSensor;
#define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true
#include "graphics/ScreenFonts.h"
#include <Throttle.h>
int32_t EnvironmentTelemetryModule::runOnce()
{
@@ -82,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)) {
@@ -143,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;
@@ -155,21 +158,20 @@ int32_t EnvironmentTelemetryModule::runOnce()
result = bme680Sensor.runTrigger();
}
uint32_t now = millis();
if (((lastSentToMesh == 0) ||
((now - lastSentToMesh) >=
Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.environment_update_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.environment_update_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
airTime->isTxAllowedAirUtil()) {
sendTelemetry();
lastSentToMesh = now;
} else if (((lastSentToPhone == 0) || ((now - lastSentToPhone) >= sendToPhoneIntervalMs)) &&
lastSentToMesh = millis();
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(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;
lastSentToPhone = millis();
}
}
return min(sendToPhoneIntervalMs, result);
@@ -397,6 +399,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 +593,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

@@ -19,6 +19,7 @@
#define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true
#include "graphics/ScreenFonts.h"
#include <Throttle.h>
int32_t PowerTelemetryModule::runOnce()
{
@@ -59,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
@@ -69,20 +72,19 @@ int32_t PowerTelemetryModule::runOnce()
if (!moduleConfig.telemetry.power_measurement_enabled)
return disable();
uint32_t now = millis();
if (((lastSentToMesh == 0) ||
((now - lastSentToMesh) >= Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.power_update_interval,
default_telemetry_broadcast_interval_secs,
numOnlineNodes))) &&
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.power_update_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
airTime->isTxAllowedAirUtil()) {
sendTelemetry();
lastSentToMesh = now;
} else if (((lastSentToPhone == 0) || ((now - lastSentToPhone) >= sendToPhoneIntervalMs)) &&
lastSentToMesh = millis();
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(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;
lastSentToPhone = millis();
}
}
return min(sendToPhoneIntervalMs, result);
@@ -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

View File

@@ -7,6 +7,7 @@
#include "NAU7802Sensor.h"
#include "SafeFile.h"
#include "TelemetrySensor.h"
#include <Throttle.h>
#include <pb_decode.h>
#include <pb_encode.h>
@@ -40,7 +41,7 @@ bool NAU7802Sensor::getMetrics(meshtastic_Telemetry *measurement)
uint32_t start = millis();
while (!nau7802.available()) {
delay(100);
if (millis() - start > 1000) {
if (!Throttle::isWithinTimespanMs(start, 1000)) {
nau7802.powerDown();
return false;
}

View File

@@ -17,6 +17,7 @@
#include "NodeDB.h"
#include "RTC.h"
#include "Router.h"
#include "Throttle.h"
#include "airtime.h"
#include "configuration.h"
#include "memGet.h"
@@ -29,6 +30,9 @@
StoreForwardModule *storeForwardModule;
uint32_t lastHeartbeat = 0;
uint32_t heartbeatInterval = 60; // Default to 60 seconds, adjust as needed
int32_t StoreForwardModule::runOnce()
{
#ifdef ARCH_ESP32
@@ -42,7 +46,7 @@ int32_t StoreForwardModule::runOnce()
this->busy = false;
}
}
} else if (this->heartbeat && (millis() - lastHeartbeat > (heartbeatInterval * 1000)) &&
} else if (this->heartbeat && (!Throttle::isWithinTimespanMs(lastHeartbeat, heartbeatInterval * 1000)) &&
airTime->isTxAllowedChannelUtil(true)) {
lastHeartbeat = millis();
LOG_INFO("*** Sending heartbeat\n");
@@ -127,7 +131,7 @@ uint32_t StoreForwardModule::getNumAvailablePackets(NodeNum dest, uint32_t last_
{
uint32_t count = 0;
if (lastRequest.find(dest) == lastRequest.end()) {
lastRequest[dest] = 0;
lastRequest.emplace(dest, 0);
}
for (uint32_t i = lastRequest[dest]; i < this->packetHistoryTotalCount; i++) {
if (this->packetHistory[i].time && (this->packetHistory[i].time > last_time)) {