From 7f042a011af7d4cbb9e66451fb46d20d239ab0c3 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 1 Jul 2025 17:23:52 +0200 Subject: [PATCH 01/52] Move PMSA003I to separate class and update AQ telemetry --- src/modules/Telemetry/AirQualityTelemetry.cpp | 303 ++++++++++++------ src/modules/Telemetry/AirQualityTelemetry.h | 37 ++- .../Telemetry/EnvironmentTelemetry.cpp | 2 - src/modules/Telemetry/EnvironmentTelemetry.h | 1 - .../Telemetry/Sensor/PMSA003ISensor.cpp | 89 +++++ src/modules/Telemetry/Sensor/PMSA003ISensor.h | 52 +++ 6 files changed, 362 insertions(+), 122 deletions(-) create mode 100644 src/modules/Telemetry/Sensor/PMSA003ISensor.cpp create mode 100644 src/modules/Telemetry/Sensor/PMSA003ISensor.h diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 2472b95b144..8b7ab1b24d9 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -1,36 +1,54 @@ #include "configuration.h" -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "AirQualityTelemetry.h" #include "Default.h" +#include "AirQualityTelemetry.h" #include "MeshService.h" #include "NodeDB.h" #include "PowerFSM.h" #include "RTC.h" #include "Router.h" -#include "detect/ScanI2CTwoWire.h" +#include "UnitConversions.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" #include "main.h" +#include "sleep.h" #include -#ifndef PMSA003I_WARMUP_MS -// from the PMSA003I datasheet: -// "Stable data should be got at least 30 seconds after the sensor wakeup -// from the sleep mode because of the fan’s performance." -#define PMSA003I_WARMUP_MS 30000 +#if __has_include() +#include "Sensor/PMSA003ISensor.h" +PMSA003ISensor pmsa003iSensor; +#else +NullSensor pmsa003iSensor; #endif int32_t AirQualityTelemetryModule::runOnce() { + if (sleepOnNextExecution == true) { + sleepOnNextExecution = false; + // uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, + uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs); + LOG_DEBUG("Sleeping for %ims, then awaking to send metrics again.", nightyNightMs); + doDeepSleep(nightyNightMs, true, false); + } + + 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.air_quality_enabled = 1; + // TODO there is no config in module_config.proto for air_quality_screen_enabled. Reusing environment one, although it should have its own + // moduleConfig.telemetry.environment_screen_enabled = 1; + // moduleConfig.telemetry.air_quality_interval = 15; - if (!(moduleConfig.telemetry.air_quality_enabled)) { + if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.environment_screen_enabled || + AIR_QUALITY_TELEMETRY_MODULE_ENABLE)) { // 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(); } @@ -42,79 +60,141 @@ int32_t AirQualityTelemetryModule::runOnce() if (moduleConfig.telemetry.air_quality_enabled) { LOG_INFO("Air quality Telemetry: init"); -#ifdef PMSA003I_ENABLE_PIN - // put the sensor to sleep on startup - pinMode(PMSA003I_ENABLE_PIN, OUTPUT); - digitalWrite(PMSA003I_ENABLE_PIN, LOW); -#endif /* PMSA003I_ENABLE_PIN */ - - if (!aqi.begin_I2C()) { -#ifndef I2C_NO_RESCAN - LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan"); - // rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty. - uint8_t i2caddr_scan[] = {PMSA0031_ADDR}; - uint8_t i2caddr_asize = 1; - auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); -#if defined(I2C_SDA1) - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); -#endif - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); - auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031); - if (found.type != ScanI2C::DeviceType::NONE) { - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address; - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second = - i2cScanner->fetchI2CBus(found.address); - return setStartDelay(); - } -#endif - return disable(); - } - return setStartDelay(); + if (pmsa003iSensor.hasSensor()) + result = pmsa003iSensor.runOnce(); } - return disable(); + + // 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 + return result == UINT32_MAX ? disable() : setStartDelay(); } else { // if we somehow got to a second run of this module with measurement disabled, then just wait forever - if (!moduleConfig.telemetry.air_quality_enabled) + if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) { return disable(); + } - switch (state) { + // Wake up the sensors that need it #ifdef PMSA003I_ENABLE_PIN - case State::IDLE: - // sensor is in standby; fire it up and sleep - LOG_DEBUG("runOnce(): state = idle"); - digitalWrite(PMSA003I_ENABLE_PIN, HIGH); - state = State::ACTIVE; - - return PMSA003I_WARMUP_MS; + if (pmsa003iSensor.hasSensor() && pmsa003iSensor.state == pmsa003iSensor::State::IDLE) + return pmsa003iSensor.wakeUp(); #endif /* PMSA003I_ENABLE_PIN */ - case State::ACTIVE: - // sensor is already warmed up; grab telemetry and send it - LOG_DEBUG("runOnce(): state = active"); - - if (((lastSentToMesh == 0) || - !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 = 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) - sendTelemetry(NODENUM_BROADCAST, true); - } + + if (((lastSentToMesh == 0) || + !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 = 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 = millis(); + } #ifdef PMSA003I_ENABLE_PIN - // put sensor back to sleep - digitalWrite(PMSA003I_ENABLE_PIN, LOW); - state = State::IDLE; + pmsa003iSensor.sleep(); #endif /* PMSA003I_ENABLE_PIN */ - return sendToPhoneIntervalMs; - default: - return disable(); + } + return min(sendToPhoneIntervalMs, result); +} + +bool AirQualityTelemetryModule::wantUIFrame() +{ + return moduleConfig.telemetry.environment_screen_enabled; +} + +void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // === Setup display === + display->clear(); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + int line = 1; + + // === Set Title + const char *titleStr = (graphics::isHighResolution) ? "Environment" : "Env."; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + // === Row spacing setup === + const int rowHeight = FONT_HEIGHT_SMALL - 4; + int currentY = graphics::getTextPositions(display)[line++]; + + // === Show "No Telemetry" if no data available === + if (!lastMeasurementPacket) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + // Decode the telemetry message from the latest received packet + const meshtastic_Data &p = lastMeasurementPacket->decoded; + meshtastic_Telemetry telemetry; + if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + const auto &m = telemetry.variant.air_quality_metrics; + + // Check if any telemetry field has valid data + bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_pm10_environmental || m.has_pm25_environmental || + m.has_pm100_environmental; + + if (!hasAny) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + // === First line: Show sender name + time since received (left), and first metric (right) === + const char *sender = getSenderShortName(*lastMeasurementPacket); + uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); + String agoStr = (agoSecs > 864000) ? "?" + : (agoSecs > 3600) ? String(agoSecs / 3600) + "h" + : (agoSecs > 60) ? String(agoSecs / 60) + "m" + : String(agoSecs) + "s"; + + String leftStr = String(sender) + " (" + agoStr + ")"; + display->drawString(x, currentY, leftStr); // Left side: who and when + + // === Collect sensor readings as label strings (no icons) === + std::vector entries; + + if (m.has_pm10_standard) + entries.push_back("PM1.0: " + String(m.pm10_standard, 0) + "ug/m3"); + if (m.has_pm25_standard) + entries.push_back("PM2.5: " + String(m.pm25_standard, 0) + "ug/m3"); + if (m.has_pm100_standard) + entries.push_back("PM10.0: " + String(m.pm100_standard, 0) + "ug/m3"); + + // === Show first available metric on top-right of first line === + if (!entries.empty()) { + String valueStr = entries.front(); + int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr); + display->drawString(rightX, currentY, valueStr); + entries.erase(entries.begin()); // Remove from queue + } + + // === Advance to next line for remaining telemetry entries === + currentY += rowHeight; + + // === Draw remaining entries in 2-column format (left and right) === + for (size_t i = 0; i < entries.size(); i += 2) { + // Left column + display->drawString(x, currentY, entries[i]); + + // Right column if it exists + if (i + 1 < entries.size()) { + int rightX = SCREEN_WIDTH / 2; + display->drawString(rightX, currentY, entries[i + 1]); } + + currentY += rowHeight; } } @@ -142,37 +222,23 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack return false; // Let others look at this message also if they want } +// CHECKED bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) { - if (!aqi.read(&data)) { - LOG_WARN("Skip send measurements. Could not read AQIn"); - return false; - } - + bool valid = true; + bool hasSensor = false; m->time = getTime(); m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; - m->variant.air_quality_metrics.has_pm10_standard = true; - m->variant.air_quality_metrics.pm10_standard = data.pm10_standard; - m->variant.air_quality_metrics.has_pm25_standard = true; - m->variant.air_quality_metrics.pm25_standard = data.pm25_standard; - m->variant.air_quality_metrics.has_pm100_standard = true; - m->variant.air_quality_metrics.pm100_standard = data.pm100_standard; - - m->variant.air_quality_metrics.has_pm10_environmental = true; - m->variant.air_quality_metrics.pm10_environmental = data.pm10_env; - m->variant.air_quality_metrics.has_pm25_environmental = true; - m->variant.air_quality_metrics.pm25_environmental = data.pm25_env; - m->variant.air_quality_metrics.has_pm100_environmental = true; - m->variant.air_quality_metrics.pm100_environmental = data.pm100_env; - - LOG_INFO("Send: PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i", m->variant.air_quality_metrics.pm10_standard, - m->variant.air_quality_metrics.pm25_standard, m->variant.air_quality_metrics.pm100_standard); - - LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", - m->variant.air_quality_metrics.pm10_environmental, m->variant.air_quality_metrics.pm25_environmental, - m->variant.air_quality_metrics.pm100_environmental); - - return true; + m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero; + + if (pmsa003iSensor.hasSensor()) { + // TODO - Should we check for sensor state here? + // If a sensor is sleeping, we should know and check to wake it up + valid = valid && pmsa003iSensor.getMetrics(m); + hasSensor = true; + } + + return valid && hasSensor; } meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() @@ -206,7 +272,14 @@ meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag; + m.time = getTime(); if (getAirQualityTelemetry(&m)) { + LOG_INFO("Send: pm10_standard=%f, pm25_standard=%f, pm100_standard=%f, pm10_environmental=%f, pm100_environmental=%f", + m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, + m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, + m.variant.air_quality_metrics.pm100_environmental); + meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; p->decoded.want_response = false; @@ -221,16 +294,46 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) lastMeasurementPacket = packetPool.allocCopy(*p); if (phoneOnly) { - LOG_INFO("Send packet to phone"); + LOG_INFO("Sending packet to phone"); service->sendToPhone(p); } else { - LOG_INFO("Send packet to mesh"); + LOG_INFO("Sending packet to mesh"); service->sendToMesh(p, RX_SRC_LOCAL, true); + + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { + meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); + notification->level = meshtastic_LogRecord_Level_INFO; + notification->time = getValidTime(RTCQualityFromNet); + sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", + Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs) / + 1000U); + service->sendClientNotification(notification); + sleepOnNextExecution = true; + LOG_DEBUG("Start next execution in 5s, then sleep"); + setIntervalFromNow(FIVE_SECONDS_MS); + } } return true; } - return false; } +AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL + if (pmsa003iSensor.hasSensor()) { + result = pmsa003iSensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + + +#endif + return result; +} + #endif \ No newline at end of file diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index 0142ee68641..8314c54bc28 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -1,12 +1,18 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #pragma once + +#ifndef AIR_QUALITY_TELEMETRY_MODULE_ENABLE +#define AIR_QUALITY_TELEMETRY_MODULE_ENABLE 0 +#endif + #include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "Adafruit_PM25AQI.h" #include "NodeDB.h" #include "ProtobufModule.h" +#include +#include class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule { @@ -20,18 +26,15 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { lastMeasurementPacket = nullptr; - setIntervalFromNow(10 * 1000); - aqi = Adafruit_PM25AQI(); nodeStatusObserver.observe(&nodeStatus->onNewStatus); - -#ifdef PMSA003I_ENABLE_PIN - // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking - // a reading - state = State::IDLE; + setIntervalFromNow(10 * 1000); + } + virtual bool wantUIFrame() override; +#if !HAS_SCREEN + void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #else - state = State::ACTIVE; + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif - } protected: /** Called to handle a particular incoming message @@ -49,19 +52,15 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf */ bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; private: - enum State { - IDLE = 0, - ACTIVE = 1, - }; - - State state; - Adafruit_PM25AQI aqi; - PM25_AQI_Data data = {0}; bool firstTime = true; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t lastSentToMesh = 0; + uint32_t lastSentToPhone = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index d1b10fa8273..2d6a8a0cbb9 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -743,8 +743,6 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature, m.variant.environment_metrics.soil_moisture); - sensor_read_error_count = 0; - meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; p->decoded.want_response = false; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.h b/src/modules/Telemetry/EnvironmentTelemetry.h index d70c063fc66..ffbb229f0fe 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.h +++ b/src/modules/Telemetry/EnvironmentTelemetry.h @@ -62,7 +62,6 @@ class EnvironmentTelemetryModule : private concurrency::OSThread, public Protobu 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; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp new file mode 100644 index 00000000000..dacdf5ff465 --- /dev/null +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -0,0 +1,89 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "PMSA003ISensor.h" +#include "TelemetrySensor.h" +#include "detect/ScanI2CTwoWire.h" +#include + +PMSA003ISensor::PMSA003ISensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") {} + +int32_t PMSA003ISensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + +#ifdef PMSA003I_ENABLE_PIN +// TODO not sure why this was like this + sleep(); +#endif /* PMSA003I_ENABLE_PIN */ + + if (!pmsa003i.begin_I2C()){ +#ifndef I2C_NO_RESCAN + LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan"); + // rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty. + uint8_t i2caddr_scan[] = {PMSA0031_ADDR}; + uint8_t i2caddr_asize = 1; + auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); +#if defined(I2C_SDA1) + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); +#endif + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); + auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031); + if (found.type != ScanI2C::DeviceType::NONE) { + nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address; + nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second = + i2cScanner->fetchI2CBus(found.address); + return initI2CSensor(); + } +#endif + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + return initI2CSensor(); +} + +void PMSA003ISensor::setup() +{ +} + +#ifdef PMSA003I_ENABLE_PIN +void sleep() { + digitalWrite(PMSA003I_ENABLE_PIN, LOW); + state = State::IDLE; +} + +uint32_t wakeUp() { + digitalWrite(PMSA003I_ENABLE_PIN, HIGH); + state = State::ACTIVE; +} +#endif /* PMSA003I_ENABLE_PIN */ + +bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) +{ + if (!pmsa003i.read(&pmsa003iData)) { + LOG_WARN("Skip send measurements. Could not read AQIn"); + return false; + } + + measurement->variant.air_quality_metrics.has_pm10_standard = true; + measurement->variant.air_quality_metrics.pm10_standard = pmsa003iData.pm10_standard; + measurement->variant.air_quality_metrics.has_pm25_standard = true; + measurement->variant.air_quality_metrics.pm25_standard = pmsa003iData.pm25_standard; + measurement->variant.air_quality_metrics.has_pm100_standard = true; + measurement->variant.air_quality_metrics.pm100_standard = pmsa003iData.pm100_standard; + + measurement->variant.air_quality_metrics.has_pm10_environmental = true; + measurement->variant.air_quality_metrics.pm10_environmental = pmsa003iData.pm10_env; + measurement->variant.air_quality_metrics.has_pm25_environmental = true; + measurement->variant.air_quality_metrics.pm25_environmental = pmsa003iData.pm25_env; + measurement->variant.air_quality_metrics.has_pm100_environmental = true; + measurement->variant.air_quality_metrics.pm100_environmental = pmsa003iData.pm100_env; + + return true; +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h new file mode 100644 index 00000000000..01b04368ed1 --- /dev/null +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -0,0 +1,52 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include "detect/ScanI2CTwoWire.h" +#include + +#ifndef PMSA003I_WARMUP_MS +// from the PMSA003I datasheet: +// "Stable data should be got at least 30 seconds after the sensor wakeup +// from the sleep mode because of the fan’s performance." +#define PMSA003I_WARMUP_MS 30000 +#endif + +class PMSA003ISensor : public TelemetrySensor +{ + private: + Adafruit_PM25AQI pmsa003i = Adafruit_PM25AQI(); + PM25_AQI_Data pmsa003iData = {0}; + +#ifdef PMSA003I_ENABLE_PIN + void sleep(); + uint32_t wakeUp(); +#endif + + protected: + virtual void setup() override; + + public: + enum State { + IDLE = 0, + ACTIVE = 1, + }; + +#ifdef PMSA003I_ENABLE_PIN + // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking + // a reading + // put the sensor to sleep on startup + pinMode(PMSA003I_ENABLE_PIN, OUTPUT); + State state = State::IDLE; +#else + State state = State::ACTIVE; +#endif + + PMSA003ISensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +}; + +#endif \ No newline at end of file From 835adb2eac8de9de76a03f875439e2490a6f6625 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 1 Jul 2025 22:17:38 +0200 Subject: [PATCH 02/52] AirQualityTelemetry module not depend on PM sensor presence --- src/modules/Modules.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 3528f57f57b..2ff5a345ab0 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -221,11 +221,7 @@ void setupModules() // TODO: How to improve this? #if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR new EnvironmentTelemetryModule(); -#if __has_include("Adafruit_PM25AQI.h") - if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) { - new AirQualityTelemetryModule(); - } -#endif + new AirQualityTelemetryModule(); #if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 || nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) { From 3b470b7f3b38bdd4f25fa911f9f8daefc3ef066e Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 2 Jul 2025 11:29:02 +0200 Subject: [PATCH 03/52] Remove commented line --- src/modules/Telemetry/AirQualityTelemetry.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 8b7ab1b24d9..75058f849e0 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -28,7 +28,6 @@ int32_t AirQualityTelemetryModule::runOnce() { if (sleepOnNextExecution == true) { sleepOnNextExecution = false; - // uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, default_telemetry_broadcast_interval_secs); LOG_DEBUG("Sleeping for %ims, then awaking to send metrics again.", nightyNightMs); @@ -222,7 +221,6 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack return false; // Let others look at this message also if they want } -// CHECKED bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) { bool valid = true; From 2f68458a83076f84909fc03a20dd279c0f913745 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 2 Jul 2025 13:12:07 +0200 Subject: [PATCH 04/52] Fixes on PMS class --- src/modules/Telemetry/AirQualityTelemetry.cpp | 2 +- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 13 ++++++++++--- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 75058f849e0..7689802ea2b 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -74,7 +74,7 @@ int32_t AirQualityTelemetryModule::runOnce() // Wake up the sensors that need it #ifdef PMSA003I_ENABLE_PIN - if (pmsa003iSensor.hasSensor() && pmsa003iSensor.state == pmsa003iSensor::State::IDLE) + if (pmsa003iSensor.hasSensor() && !pmsa003iSensor.isActive()) return pmsa003iSensor.wakeUp(); #endif /* PMSA003I_ENABLE_PIN */ diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index dacdf5ff465..8567d7e7014 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -48,24 +48,31 @@ int32_t PMSA003ISensor::runOnce() void PMSA003ISensor::setup() { +#ifdef PMSA003I_ENABLE_PIN + pinMode(PMSA003I_ENABLE_PIN, OUTPUT); +#endif /* PMSA003I_ENABLE_PIN */ } #ifdef PMSA003I_ENABLE_PIN -void sleep() { +void PMSA003ISensor::sleep() { digitalWrite(PMSA003I_ENABLE_PIN, LOW); state = State::IDLE; } -uint32_t wakeUp() { +uint32_t PMSA003ISensor::wakeUp() { digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; } #endif /* PMSA003I_ENABLE_PIN */ +bool PMSA003ISensor::isActive() { + return state == State::ACTIVE; +} + bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) { if (!pmsa003i.read(&pmsa003iData)) { - LOG_WARN("Skip send measurements. Could not read AQIn"); + LOG_WARN("Skip send measurements. Could not read AQI"); return false; } diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 01b04368ed1..db7c9aaa92d 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -38,13 +38,13 @@ class PMSA003ISensor : public TelemetrySensor // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking // a reading // put the sensor to sleep on startup - pinMode(PMSA003I_ENABLE_PIN, OUTPUT); State state = State::IDLE; #else State state = State::ACTIVE; #endif PMSA003ISensor(); + bool isActive(); virtual int32_t runOnce() override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override; }; From e4903eb43070a554caa866c7937ba0380a3208fc Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 2 Jul 2025 14:41:44 +0200 Subject: [PATCH 05/52] Add missing warmup period to wakeUp function --- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 8567d7e7014..67f00574ece 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -62,6 +62,7 @@ void PMSA003ISensor::sleep() { uint32_t PMSA003ISensor::wakeUp() { digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; + return PMSA003I_WARMUP_MS } #endif /* PMSA003I_ENABLE_PIN */ From 9111f88f02489bd9446615f39c4b2f86bc91d0c5 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sun, 6 Jul 2025 08:31:57 +0200 Subject: [PATCH 06/52] Fixes on compilation for different variants --- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 2 +- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 67f00574ece..2b165cd6da3 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -62,7 +62,7 @@ void PMSA003ISensor::sleep() { uint32_t PMSA003ISensor::wakeUp() { digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; - return PMSA003I_WARMUP_MS + return PMSA003I_WARMUP_MS; } #endif /* PMSA003I_ENABLE_PIN */ diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index db7c9aaa92d..7e460ce3303 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -20,11 +20,6 @@ class PMSA003ISensor : public TelemetrySensor Adafruit_PM25AQI pmsa003i = Adafruit_PM25AQI(); PM25_AQI_Data pmsa003iData = {0}; -#ifdef PMSA003I_ENABLE_PIN - void sleep(); - uint32_t wakeUp(); -#endif - protected: virtual void setup() override; @@ -35,6 +30,8 @@ class PMSA003ISensor : public TelemetrySensor }; #ifdef PMSA003I_ENABLE_PIN + void sleep(); + uint32_t wakeUp(); // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking // a reading // put the sensor to sleep on startup From 40af7b82c4bc63a2e63eda9355a5677e39212611 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 11 Jul 2025 14:39:26 +0200 Subject: [PATCH 07/52] Add functions to check for I2C bus speed and set it --- src/detect/ScanI2CTwoWire.cpp | 69 +++++++++++++++++++++++++++++++++++ src/detect/ScanI2CTwoWire.h | 3 ++ src/main.cpp | 21 +++++++++++ 3 files changed, 93 insertions(+) diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 652d50d5122..215efc1d0b0 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -109,6 +109,75 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation return value; } +bool ScanI2CTwoWire::setClockSpeed(I2CPort port, uint32_t speed) { + + DeviceAddress addr(port, 0x00); + TwoWire *i2cBus; + +#if WIRE_INTERFACES_COUNT == 2 + if (port == I2CPort::WIRE1) { + i2cBus = &Wire1; + } else { +#endif + i2cBus = &Wire; +#if WIRE_INTERFACES_COUNT == 2 + } +#endif + + return i2cBus->setClock(speed); +} + +uint32_t ScanI2CTwoWire::getClockSpeed(I2CPort port) { + + DeviceAddress addr(port, 0x00); + TwoWire *i2cBus; + +#if WIRE_INTERFACES_COUNT == 2 + if (port == I2CPort::WIRE1) { + i2cBus = &Wire1; + } else { +#endif + i2cBus = &Wire; +#if WIRE_INTERFACES_COUNT == 2 + } +#endif + + return i2cBus->getClock(); +} + +/// for SEN5X detection +String readSEN5xProductName(TwoWire* i2cBus, uint8_t address) { + uint8_t cmd[] = { 0xD0, 0x14 }; + uint8_t response[48] = {0}; + + i2cBus->beginTransmission(address); + i2cBus->write(cmd, 2); + if (i2cBus->endTransmission() != 0) return ""; + + delay(20); + if (i2cBus->requestFrom(address, (uint8_t)48) != 48) return ""; + + for (int i = 0; i < 48 && i2cBus->available(); ++i) { + response[i] = i2cBus->read(); + } + + char productName[33] = {0}; + int j = 0; + for (int i = 0; i < 48 && j < 32; i += 3) { + if (response[i] >= 32 && response[i] <= 126) + productName[j++] = response[i]; + else + break; + + if (response[i + 1] >= 32 && response[i + 1] <= 126) + productName[j++] = response[i + 1]; + else + break; + } + + return String(productName); +} + #define SCAN_SIMPLE_CASE(ADDR, T, ...) \ case ADDR: \ logFoundDevice(__VA_ARGS__); \ diff --git a/src/detect/ScanI2CTwoWire.h b/src/detect/ScanI2CTwoWire.h index 6988091ad3b..28b073a1745 100644 --- a/src/detect/ScanI2CTwoWire.h +++ b/src/detect/ScanI2CTwoWire.h @@ -29,6 +29,9 @@ class ScanI2CTwoWire : public ScanI2C size_t countDevices() const override; + bool setClockSpeed(ScanI2C::I2CPort, uint32_t); + uint32_t getClockSpeed(ScanI2C::I2CPort); + protected: FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override; diff --git a/src/main.cpp b/src/main.cpp index 1868d98c7c4..132dab9e27e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -480,6 +480,7 @@ void setup() Wire.setSCL(I2C_SCL); Wire.begin(); #elif defined(I2C_SDA) && !defined(ARCH_RP2040) + LOG_INFO("Starting Bus with (SDA) %d and (SCL) %d: ", I2C_SDA, I2C_SCL); Wire.begin(I2C_SDA, I2C_SCL); #elif defined(ARCH_PORTDUINO) if (settingsStrings[i2cdev] != "") { @@ -538,6 +539,26 @@ void setup() i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); #endif +#ifdef I2C_CLOCK_SPEED + uint32_t currentClock; + currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); + LOG_INFO("Clock speed: %uHz on WIRE", currentClock); + LOG_DEBUG("Setting Wire with defined clock speed, %uHz...", I2C_CLOCK_SPEED); + if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, I2C_CLOCK_SPEED)) { + LOG_ERROR("Unable to set clock speed on WIRE"); + } else { + + currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); + LOG_INFO("Set clock speed: %uHz on WIRE", currentClock); + } + // LOG_DEBUG("Starting Wire with defined clock speed, %d...", I2C_CLOCK_SPEED); + // if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE1, I2C_CLOCK_SPEED)) { + // LOG_ERROR("Unable to set clock speed on WIRE1"); + // } else { + // LOG_INFO("Set clock speed: %d on WIRE1", I2C_CLOCK_SPEED); + // } +#endif + auto i2cCount = i2cScanner->countDevices(); if (i2cCount == 0) { LOG_INFO("No I2C devices found"); From ff8691dc136555c997db5b6c23c69b1e99ea1d90 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sat, 12 Jul 2025 22:12:31 +0200 Subject: [PATCH 08/52] Add ScreenFonts.h Co-authored-by: Hannes Fuchs --- src/modules/Telemetry/AirQualityTelemetry.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 7689802ea2b..1c8e95a828f 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -23,6 +23,7 @@ PMSA003ISensor pmsa003iSensor; #else NullSensor pmsa003iSensor; #endif +#include "graphics/ScreenFonts.h" int32_t AirQualityTelemetryModule::runOnce() { From 0dda175d97e8ea004c7d26c9af27abaf2e90ea83 Mon Sep 17 00:00:00 2001 From: Nashui-Yan Date: Tue, 22 Jul 2025 16:55:09 +0100 Subject: [PATCH 09/52] PMSA003I 1st round test --- src/modules/Telemetry/AirQualityTelemetry.cpp | 8 +- .../Telemetry/Sensor/PMSA003ISensor.cpp | 192 ++++++++++++------ src/modules/Telemetry/Sensor/PMSA003ISensor.h | 59 +++--- 3 files changed, 163 insertions(+), 96 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 1c8e95a828f..9bc41bfa576 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -166,11 +166,11 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta std::vector entries; if (m.has_pm10_standard) - entries.push_back("PM1.0: " + String(m.pm10_standard, 0) + "ug/m3"); + entries.push_back("PM1.0: " + String(m.pm10_standard) + "ug/m3"); if (m.has_pm25_standard) - entries.push_back("PM2.5: " + String(m.pm25_standard, 0) + "ug/m3"); + entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3"); if (m.has_pm100_standard) - entries.push_back("PM10.0: " + String(m.pm100_standard, 0) + "ug/m3"); + entries.push_back("PM10.0: " + String(m.pm100_standard) + "ug/m3"); // === Show first available metric on top-right of first line === if (!entries.empty()) { @@ -274,7 +274,7 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m.time = getTime(); if (getAirQualityTelemetry(&m)) { - LOG_INFO("Send: pm10_standard=%f, pm25_standard=%f, pm100_standard=%f, pm10_environmental=%f, pm100_environmental=%f", + LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, pm10_environmental=%u, pm100_environmental=%u", m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, m.variant.air_quality_metrics.pm100_environmental); diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 2b165cd6da3..39f8269a2c7 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -1,97 +1,175 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "PMSA003ISensor.h" #include "TelemetrySensor.h" -#include "detect/ScanI2CTwoWire.h" -#include -PMSA003ISensor::PMSA003ISensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") {} +#include + +PMSA003ISensor::PMSA003ISensor() + : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") +{ +} + +void PMSA003ISensor::setup() +{ +#ifdef PMSA003I_ENABLE_PIN + pinMode(PMSA003I_ENABLE_PIN, OUTPUT); +#endif +} + +bool PMSA003ISensor::restoreClock(uint32_t currentClock){ +#ifdef PMSA003I_I2C_CLOCK_SPEED + if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ + // LOG_DEBUG("Restoring I2C clock to %uHz", currentClock); + return bus->setClock(currentClock); + } + return true; +#endif +} int32_t PMSA003ISensor::runOnce() { LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } -#ifdef PMSA003I_ENABLE_PIN -// TODO not sure why this was like this - sleep(); -#endif /* PMSA003I_ENABLE_PIN */ - - if (!pmsa003i.begin_I2C()){ -#ifndef I2C_NO_RESCAN - LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan"); - // rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty. - uint8_t i2caddr_scan[] = {PMSA0031_ADDR}; - uint8_t i2caddr_asize = 1; - auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); -#if defined(I2C_SDA1) - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); -#endif - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); - auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031); - if (found.type != ScanI2C::DeviceType::NONE) { - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address; - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second = - i2cScanner->fetchI2CBus(found.address); - return initI2CSensor(); - } + bus = nodeTelemetrySensorsMap[sensorType].second; + address = (uint8_t)nodeTelemetrySensorsMap[sensorType].first; + +#ifdef PMSA003I_I2C_CLOCK_SPEED + uint32_t currentClock; + currentClock = bus->getClock(); + if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ + // LOG_DEBUG("Changing I2C clock to %u", PMSA003I_I2C_CLOCK_SPEED); + bus->setClock(PMSA003I_I2C_CLOCK_SPEED); + } #endif + + bus->beginTransmission(address); + if (bus->endTransmission() != 0) { + LOG_WARN("PMSA003I not found on I2C at 0x12"); return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } + + restoreClock(currentClock); + + status = 1; + LOG_INFO("PMSA003I Enabled"); + return initI2CSensor(); } -void PMSA003ISensor::setup() +bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) { -#ifdef PMSA003I_ENABLE_PIN - pinMode(PMSA003I_ENABLE_PIN, OUTPUT); -#endif /* PMSA003I_ENABLE_PIN */ -} + if(!isActive()){ + LOG_WARN("PMSA003I is not active"); + return false; + } -#ifdef PMSA003I_ENABLE_PIN -void PMSA003ISensor::sleep() { - digitalWrite(PMSA003I_ENABLE_PIN, LOW); - state = State::IDLE; -} +#ifdef PMSA003I_I2C_CLOCK_SPEED + uint32_t currentClock; + currentClock = bus->getClock(); + if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ + // LOG_DEBUG("Changing I2C clock to %u", PMSA003I_I2C_CLOCK_SPEED); + bus->setClock(PMSA003I_I2C_CLOCK_SPEED); + } +#endif -uint32_t PMSA003ISensor::wakeUp() { - digitalWrite(PMSA003I_ENABLE_PIN, HIGH); - state = State::ACTIVE; - return PMSA003I_WARMUP_MS; -} -#endif /* PMSA003I_ENABLE_PIN */ + bus->requestFrom(address, PMSA003I_FRAME_LENGTH); + if (bus->available() < PMSA003I_FRAME_LENGTH) { + LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", bus->available()); + return false; + } -bool PMSA003ISensor::isActive() { - return state == State::ACTIVE; -} + restoreClock(currentClock); -bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) -{ - if (!pmsa003i.read(&pmsa003iData)) { - LOG_WARN("Skip send measurements. Could not read AQI"); + for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { + buffer[i] = bus->read(); + } + + if (buffer[0] != 0x42 || buffer[1] != 0x4D) { + LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]); + return false; + } + + auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { + return (data[idx] << 8) | data[idx + 1]; + }; + + computedChecksum = 0; + + for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH - 2; i++) { + computedChecksum += buffer[i]; + } + receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2); + + if (computedChecksum != receivedChecksum) { + LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum); return false; } measurement->variant.air_quality_metrics.has_pm10_standard = true; - measurement->variant.air_quality_metrics.pm10_standard = pmsa003iData.pm10_standard; + measurement->variant.air_quality_metrics.pm10_standard = read16(buffer, 4); + measurement->variant.air_quality_metrics.has_pm25_standard = true; - measurement->variant.air_quality_metrics.pm25_standard = pmsa003iData.pm25_standard; + measurement->variant.air_quality_metrics.pm25_standard = read16(buffer, 6); + measurement->variant.air_quality_metrics.has_pm100_standard = true; - measurement->variant.air_quality_metrics.pm100_standard = pmsa003iData.pm100_standard; + measurement->variant.air_quality_metrics.pm100_standard = read16(buffer, 8); measurement->variant.air_quality_metrics.has_pm10_environmental = true; - measurement->variant.air_quality_metrics.pm10_environmental = pmsa003iData.pm10_env; + measurement->variant.air_quality_metrics.pm10_environmental = read16(buffer, 10); + measurement->variant.air_quality_metrics.has_pm25_environmental = true; - measurement->variant.air_quality_metrics.pm25_environmental = pmsa003iData.pm25_env; + measurement->variant.air_quality_metrics.pm25_environmental = read16(buffer, 12); + measurement->variant.air_quality_metrics.has_pm100_environmental = true; - measurement->variant.air_quality_metrics.pm100_environmental = pmsa003iData.pm100_env; + measurement->variant.air_quality_metrics.pm100_environmental = read16(buffer, 14); + measurement->variant.air_quality_metrics.has_particles_03um = true; + measurement->variant.air_quality_metrics.particles_03um = read16(buffer, 16); + + measurement->variant.air_quality_metrics.has_particles_05um = true; + measurement->variant.air_quality_metrics.particles_05um = read16(buffer, 18); + + measurement->variant.air_quality_metrics.has_particles_10um = true; + measurement->variant.air_quality_metrics.particles_10um = read16(buffer, 20); + + measurement->variant.air_quality_metrics.has_particles_25um = true; + measurement->variant.air_quality_metrics.particles_25um = read16(buffer, 22); + + measurement->variant.air_quality_metrics.has_particles_50um = true; + measurement->variant.air_quality_metrics.particles_50um = read16(buffer, 24); + + measurement->variant.air_quality_metrics.has_particles_100um = true; + measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26); + return true; } -#endif \ No newline at end of file +bool PMSA003ISensor::isActive() +{ + return state == State::ACTIVE; +} + +#ifdef PMSA003I_ENABLE_PIN +void PMSA003ISensor::sleep() +{ + digitalWrite(PMSA003I_ENABLE_PIN, LOW); + state = State::IDLE; +} + +uint32_t PMSA003ISensor::wakeUp() +{ + digitalWrite(PMSA003I_ENABLE_PIN, HIGH); + state = State::ACTIVE; + return PMSA003I_WARMUP_MS; +} +#endif + +#endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 7e460ce3303..d6f12dfbb8a 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -1,49 +1,38 @@ -#include "configuration.h" +#pragma once -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() - -#include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" -#include "detect/ScanI2CTwoWire.h" -#include - -#ifndef PMSA003I_WARMUP_MS -// from the PMSA003I datasheet: -// "Stable data should be got at least 30 seconds after the sensor wakeup -// from the sleep mode because of the fan’s performance." -#define PMSA003I_WARMUP_MS 30000 + +#ifndef PMSA003I_I2C_CLOCK_SPEED +#define PMSA003I_I2C_CLOCK_SPEED 100000 +#endif + +#ifndef PMSA003I_ENABLE_PIN +#define PMSA003I_FRAME_LENGTH 32 #endif class PMSA003ISensor : public TelemetrySensor { - private: - Adafruit_PM25AQI pmsa003i = Adafruit_PM25AQI(); - PM25_AQI_Data pmsa003iData = {0}; - - protected: +public: + PMSA003ISensor(); virtual void setup() override; - - public: - enum State { - IDLE = 0, - ACTIVE = 1, - }; + virtual int32_t runOnce() override; + virtual bool restoreClock(uint32_t currentClock); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool isActive(); #ifdef PMSA003I_ENABLE_PIN void sleep(); uint32_t wakeUp(); - // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking - // a reading - // put the sensor to sleep on startup - State state = State::IDLE; -#else - State state = State::ACTIVE; #endif - PMSA003ISensor(); - bool isActive(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; -}; +private: + enum class State { IDLE, ACTIVE }; + State state = State::ACTIVE; + TwoWire * bus; + uint8_t address; -#endif \ No newline at end of file + uint16_t computedChecksum = 0; + uint16_t receivedChecksum = 0; + + uint8_t buffer[PMSA003I_FRAME_LENGTH]; +}; From 14eaa3e097e33fa11c6739470249045fbfd0ff2a Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 23 Jul 2025 13:42:34 +0200 Subject: [PATCH 10/52] Fix I2C scan speed --- src/main.cpp | 53 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 132dab9e27e..deda7f1076d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -524,6 +524,39 @@ void setup() LOG_INFO("Scan for i2c devices"); #endif +// Scan I2C port at desired speed +#ifdef SCAN_I2C_CLOCK_SPEED + uint32_t currentClock; + currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); + LOG_INFO("Clock speed: %uHz on WIRE", currentClock); + LOG_DEBUG("Setting Wire with defined clock speed, %uHz...", SCAN_I2C_CLOCK_SPEED); + + if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, SCAN_I2C_CLOCK_SPEED)) { + LOG_ERROR("Unable to set clock speed on WIRE"); + } else { + currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); + LOG_INFO("Set clock speed: %uHz on WIRE", currentClock); + } + + // TODO Check if necessary + // LOG_DEBUG("Starting Wire with defined clock speed, %d...", SCAN_I2C_CLOCK_SPEED); + // if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE1, SCAN_I2C_CLOCK_SPEED)) { + // LOG_ERROR("Unable to set clock speed on WIRE1"); + // } else { + // LOG_INFO("Set clock speed: %d on WIRE1", SCAN_I2C_CLOCK_SPEED); + // } + + // Restore clock speed + if (currentClock != SCAN_I2C_CLOCK_SPEED) { + if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, currentClock)) { + LOG_ERROR("Unable to restore clock speed on WIRE"); + } else { + currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); + LOG_INFO("Set clock speed restored to: %uHz on WIRE", currentClock); + } + } +#endif + #if defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2)) i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1); #endif @@ -539,26 +572,6 @@ void setup() i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); #endif -#ifdef I2C_CLOCK_SPEED - uint32_t currentClock; - currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); - LOG_INFO("Clock speed: %uHz on WIRE", currentClock); - LOG_DEBUG("Setting Wire with defined clock speed, %uHz...", I2C_CLOCK_SPEED); - if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, I2C_CLOCK_SPEED)) { - LOG_ERROR("Unable to set clock speed on WIRE"); - } else { - - currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); - LOG_INFO("Set clock speed: %uHz on WIRE", currentClock); - } - // LOG_DEBUG("Starting Wire with defined clock speed, %d...", I2C_CLOCK_SPEED); - // if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE1, I2C_CLOCK_SPEED)) { - // LOG_ERROR("Unable to set clock speed on WIRE1"); - // } else { - // LOG_INFO("Set clock speed: %d on WIRE1", I2C_CLOCK_SPEED); - // } -#endif - auto i2cCount = i2cScanner->countDevices(); if (i2cCount == 0) { LOG_INFO("No I2C devices found"); From 8a811b209db77bab3c9ff6b31b3009a86dadf89f Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 23 Jul 2025 17:23:04 +0200 Subject: [PATCH 11/52] Fix minor issues and bring back I2C SPEED def --- src/modules/Telemetry/AirQualityTelemetry.cpp | 9 +++++---- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 7 ++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 9bc41bfa576..431473e05b3 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -274,10 +274,11 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m.time = getTime(); if (getAirQualityTelemetry(&m)) { - LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, pm10_environmental=%u, pm100_environmental=%u", - m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, - m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, - m.variant.air_quality_metrics.pm100_environmental); + LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \ + pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", \ + m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, \ + m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, \ + m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental); meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index d6f12dfbb8a..ee3258ab1ce 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -2,13 +2,10 @@ #include "TelemetrySensor.h" -#ifndef PMSA003I_I2C_CLOCK_SPEED #define PMSA003I_I2C_CLOCK_SPEED 100000 -#endif - -#ifndef PMSA003I_ENABLE_PIN #define PMSA003I_FRAME_LENGTH 32 -#endif +#define PMSA003I_WARMUP_MS 30000 + class PMSA003ISensor : public TelemetrySensor { From ec5a752078c6e651ab171bfcf226b19bb438f281 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 25 Jul 2025 12:29:40 +0200 Subject: [PATCH 12/52] Remove PMSA003I library as its no longer needed --- platformio.ini | 2 -- src/modules/Telemetry/AirQualityTelemetry.cpp | 12 +++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/platformio.ini b/platformio.ini index 8bf56cf5bfc..b5f08dd729d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -133,8 +133,6 @@ lib_deps = adafruit/Adafruit INA260 Library@1.5.3 # renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219 adafruit/Adafruit INA219@1.2.3 - # renovate: datasource=custom.pio depName=Adafruit PM25 AQI Sensor packageName=adafruit/library/Adafruit PM25 AQI Sensor - adafruit/Adafruit PM25 AQI Sensor@2.0.0 # renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050 adafruit/Adafruit MPU6050@2.2.6 # renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 431473e05b3..ff5bd22efbb 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -16,13 +16,12 @@ #include "main.h" #include "sleep.h" #include - -#if __has_include() +// Sensor includes #include "Sensor/PMSA003ISensor.h" + +// Sensors PMSA003ISensor pmsa003iSensor; -#else -NullSensor pmsa003iSensor; -#endif + #include "graphics/ScreenFonts.h" int32_t AirQualityTelemetryModule::runOnce() @@ -326,6 +325,9 @@ AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule( AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL if (pmsa003iSensor.hasSensor()) { + // TODO - Potentially implement an admin message to choose between pm_standard + // and pm_environmental. This could be configurable as it doesn't make sense so + // have both result = pmsa003iSensor.handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) return result; From a129441533a72a52726ffb67f87ee76b2c01eb2a Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 6 Aug 2025 10:40:57 +0200 Subject: [PATCH 13/52] Remove unused I2C speed functions and cleanup * Cleanup of SEN5X specific code added from switching branches * Remove SCAN_I2C_CLOCK_SPEED block as its not needed * Remove associated functions for setting I2C speed --- src/detect/ScanI2CTwoWire.cpp | 69 ------------------- src/detect/ScanI2CTwoWire.h | 3 - src/main.cpp | 33 --------- .../Telemetry/Sensor/PMSA003ISensor.cpp | 16 +++-- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 1 - 5 files changed, 10 insertions(+), 112 deletions(-) diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 23ccdd1ee5f..8b3670cd989 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -109,75 +109,6 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation return value; } -bool ScanI2CTwoWire::setClockSpeed(I2CPort port, uint32_t speed) { - - DeviceAddress addr(port, 0x00); - TwoWire *i2cBus; - -#if WIRE_INTERFACES_COUNT == 2 - if (port == I2CPort::WIRE1) { - i2cBus = &Wire1; - } else { -#endif - i2cBus = &Wire; -#if WIRE_INTERFACES_COUNT == 2 - } -#endif - - return i2cBus->setClock(speed); -} - -uint32_t ScanI2CTwoWire::getClockSpeed(I2CPort port) { - - DeviceAddress addr(port, 0x00); - TwoWire *i2cBus; - -#if WIRE_INTERFACES_COUNT == 2 - if (port == I2CPort::WIRE1) { - i2cBus = &Wire1; - } else { -#endif - i2cBus = &Wire; -#if WIRE_INTERFACES_COUNT == 2 - } -#endif - - return i2cBus->getClock(); -} - -/// for SEN5X detection -String readSEN5xProductName(TwoWire* i2cBus, uint8_t address) { - uint8_t cmd[] = { 0xD0, 0x14 }; - uint8_t response[48] = {0}; - - i2cBus->beginTransmission(address); - i2cBus->write(cmd, 2); - if (i2cBus->endTransmission() != 0) return ""; - - delay(20); - if (i2cBus->requestFrom(address, (uint8_t)48) != 48) return ""; - - for (int i = 0; i < 48 && i2cBus->available(); ++i) { - response[i] = i2cBus->read(); - } - - char productName[33] = {0}; - int j = 0; - for (int i = 0; i < 48 && j < 32; i += 3) { - if (response[i] >= 32 && response[i] <= 126) - productName[j++] = response[i]; - else - break; - - if (response[i + 1] >= 32 && response[i + 1] <= 126) - productName[j++] = response[i + 1]; - else - break; - } - - return String(productName); -} - #define SCAN_SIMPLE_CASE(ADDR, T, ...) \ case ADDR: \ logFoundDevice(__VA_ARGS__); \ diff --git a/src/detect/ScanI2CTwoWire.h b/src/detect/ScanI2CTwoWire.h index 28b073a1745..6988091ad3b 100644 --- a/src/detect/ScanI2CTwoWire.h +++ b/src/detect/ScanI2CTwoWire.h @@ -29,9 +29,6 @@ class ScanI2CTwoWire : public ScanI2C size_t countDevices() const override; - bool setClockSpeed(ScanI2C::I2CPort, uint32_t); - uint32_t getClockSpeed(ScanI2C::I2CPort); - protected: FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override; diff --git a/src/main.cpp b/src/main.cpp index 4e01f4409b0..c0276d6a662 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -524,39 +524,6 @@ void setup() LOG_INFO("Scan for i2c devices"); #endif -// Scan I2C port at desired speed -#ifdef SCAN_I2C_CLOCK_SPEED - uint32_t currentClock; - currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); - LOG_INFO("Clock speed: %uHz on WIRE", currentClock); - LOG_DEBUG("Setting Wire with defined clock speed, %uHz...", SCAN_I2C_CLOCK_SPEED); - - if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, SCAN_I2C_CLOCK_SPEED)) { - LOG_ERROR("Unable to set clock speed on WIRE"); - } else { - currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); - LOG_INFO("Set clock speed: %uHz on WIRE", currentClock); - } - - // TODO Check if necessary - // LOG_DEBUG("Starting Wire with defined clock speed, %d...", SCAN_I2C_CLOCK_SPEED); - // if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE1, SCAN_I2C_CLOCK_SPEED)) { - // LOG_ERROR("Unable to set clock speed on WIRE1"); - // } else { - // LOG_INFO("Set clock speed: %d on WIRE1", SCAN_I2C_CLOCK_SPEED); - // } - - // Restore clock speed - if (currentClock != SCAN_I2C_CLOCK_SPEED) { - if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, currentClock)) { - LOG_ERROR("Unable to restore clock speed on WIRE"); - } else { - currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); - LOG_INFO("Set clock speed restored to: %uHz on WIRE", currentClock); - } - } -#endif - #if defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2)) i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1); #endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 39f8269a2c7..c83c3e01aca 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -56,9 +56,11 @@ int32_t PMSA003ISensor::runOnce() return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } +#ifdef PMSA003I_I2C_CLOCK_SPEED restoreClock(currentClock); +#endif - status = 1; + status = 1; LOG_INFO("PMSA003I Enabled"); return initI2CSensor(); @@ -86,7 +88,9 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) return false; } +#ifdef PMSA003I_I2C_CLOCK_SPEED restoreClock(currentClock); +#endif for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { buffer[i] = bus->read(); @@ -136,19 +140,19 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) measurement->variant.air_quality_metrics.has_particles_05um = true; measurement->variant.air_quality_metrics.particles_05um = read16(buffer, 18); - + measurement->variant.air_quality_metrics.has_particles_10um = true; measurement->variant.air_quality_metrics.particles_10um = read16(buffer, 20); - + measurement->variant.air_quality_metrics.has_particles_25um = true; measurement->variant.air_quality_metrics.particles_25um = read16(buffer, 22); - + measurement->variant.air_quality_metrics.has_particles_50um = true; measurement->variant.air_quality_metrics.particles_50um = read16(buffer, 24); - + measurement->variant.air_quality_metrics.has_particles_100um = true; measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26); - + return true; } diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index ee3258ab1ce..35a4df735e1 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -6,7 +6,6 @@ #define PMSA003I_FRAME_LENGTH 32 #define PMSA003I_WARMUP_MS 30000 - class PMSA003ISensor : public TelemetrySensor { public: From 24b3a2225ca79eec91a39ac5bc37a5d605c6d4d8 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 10 Sep 2025 15:29:50 -0500 Subject: [PATCH 14/52] Unify build epoch to add flag in platformio-custom.py (#7917) * Unify build_epoch replacement logic in platformio-custom * Missed one --- .github/actions/setup-base/action.yml | 5 ----- bin/build-firmware.sh | 2 -- bin/platformio-custom.py | 7 +++++++ platformio.ini | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/actions/setup-base/action.yml b/.github/actions/setup-base/action.yml index 350ca290c81..f6c1fd80c8a 100644 --- a/.github/actions/setup-base/action.yml +++ b/.github/actions/setup-base/action.yml @@ -11,11 +11,6 @@ runs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - name: Uncomment build epoch - shell: bash - run: | - sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini - - name: Install dependencies shell: bash run: | diff --git a/bin/build-firmware.sh b/bin/build-firmware.sh index fdd7caa11c4..7bd19aaa90f 100644 --- a/bin/build-firmware.sh +++ b/bin/build-firmware.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini - export PIP_BREAK_SYSTEM_PACKAGES=1 if (echo $2 | grep -q "esp32"); then diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py index fc1b4bc2e54..e54d1586f28 100644 --- a/bin/platformio-custom.py +++ b/bin/platformio-custom.py @@ -6,6 +6,8 @@ import subprocess import json import re +import time +from datetime import datetime from readprops import readProps @@ -125,11 +127,16 @@ def esp32_create_combined_bin(source, target, env): pref_flags.append("-D" + pref + "=" + env.StringifyMacro(userPrefs[pref]) + "") # General options that are passed to the C and C++ compilers +# Calculate unix epoch for current day (midnight) +current_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) +build_epoch = int(current_date.timestamp()) + flags = [ "-DAPP_VERSION=" + verObj["long"], "-DAPP_VERSION_SHORT=" + verObj["short"], "-DAPP_ENV=" + env.get("PIOENV"), "-DAPP_REPO=" + repo_owner, + "-DBUILD_EPOCH=" + str(build_epoch), ] + pref_flags print ("Using flags:") diff --git a/platformio.ini b/platformio.ini index d1089a4f3be..81bda239c80 100644 --- a/platformio.ini +++ b/platformio.ini @@ -53,7 +53,7 @@ build_flags = -Wno-missing-field-initializers -DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware -DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1 -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage - #-DBUILD_EPOCH=$UNIX_TIME + #-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now #-D OLED_PL=1 monitor_speed = 115200 From 0827bd02c0ce9dca5f3a7fb662d7c657b78a3e3e Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Mon, 8 Sep 2025 20:54:08 +1000 Subject: [PATCH 15/52] Fix build error in rak_wismesh_tap_v2 (#7905) In the logs was: "No screen resolution defined in build_flags. Please define DISPLAY_SIZE." set according to similar devices. --- variants/esp32s3/rak_wismesh_tap_v2/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini index 8b86e021766..de4714efaa1 100644 --- a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini +++ b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini @@ -70,6 +70,7 @@ build_flags = ${ft5x06.build_flags} -D LGFX_SCREEN_WIDTH=240 -D LGFX_SCREEN_HEIGHT=320 + -D DISPLAY_SIZE=320x240 ; landscape mode -D LGFX_PANEL=ST7789 -D LGFX_ROTATION=1 -D LGFX_TOUCH_X_MIN=0 From 0b900dd42e50f1c7de15d7529a776ecd6dc32c4a Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 11 Sep 2025 07:57:42 -0500 Subject: [PATCH 16/52] Put guards in place around debug heap operations (#7955) * Put guards in place around debug heap operations * Add macros to clean up code * Add pointer as well --- src/DebugConfiguration.h | 19 +++++++++++++++++++ src/memGet.cpp | 12 ++++++++++++ src/mesh/MeshService.cpp | 11 ++++------- src/mesh/ReliableRouter.cpp | 5 ++--- src/mesh/Router.cpp | 12 +++++------- src/modules/NodeInfoModule.cpp | 5 ++--- src/modules/Telemetry/DeviceTelemetry.cpp | 6 ++---- 7 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/DebugConfiguration.h b/src/DebugConfiguration.h index 26f2db1f426..98bbe0f729a 100644 --- a/src/DebugConfiguration.h +++ b/src/DebugConfiguration.h @@ -2,6 +2,12 @@ #include "configuration.h" +// Forward declarations +#if defined(DEBUG_HEAP) +class MemGet; +extern MemGet memGet; +#endif + // DEBUG LED #ifndef LED_STATE_ON #define LED_STATE_ON 1 @@ -65,8 +71,21 @@ #if defined(DEBUG_HEAP) #define LOG_HEAP(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_HEAP, __VA_ARGS__) + +// Macro-based heap debugging +#define DEBUG_HEAP_BEFORE auto heapBefore = memGet.getFreeHeap(); +#define DEBUG_HEAP_AFTER(context, ptr) \ + do { \ + auto heapAfter = memGet.getFreeHeap(); \ + if (heapBefore != heapAfter) { \ + LOG_HEAP("Alloc in %s pointer 0x%x, size: %u, free: %u", context, ptr, heapBefore - heapAfter, heapAfter); \ + } \ + } while (0) + #else #define LOG_HEAP(...) +#define DEBUG_HEAP_BEFORE +#define DEBUG_HEAP_AFTER(context, ptr) #endif /// A C wrapper for LOG_DEBUG that can be used from arduino C libs that don't know about C++ or meshtastic diff --git a/src/memGet.cpp b/src/memGet.cpp index e8cd177dd93..14e61401495 100644 --- a/src/memGet.cpp +++ b/src/memGet.cpp @@ -88,4 +88,16 @@ uint32_t MemGet::getPsramSize() #else return 0; #endif +} + +void displayPercentHeapFree() +{ + uint32_t freeHeap = memGet.getFreeHeap(); + uint32_t totalHeap = memGet.getHeapSize(); + if (totalHeap == 0 || totalHeap == UINT32_MAX) { + LOG_INFO("Heap size unavailable"); + return; + } + int percent = (int)((freeHeap * 100) / totalHeap); + LOG_INFO("Heap free: %d%% (%u/%u bytes)", percent, freeHeap, totalHeap); } \ No newline at end of file diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 157a2eda3cd..607766ab6aa 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -193,11 +193,9 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p) // (so we update our nodedb for the local node) // Send the packet into the mesh - auto heapBefore = memGet.getFreeHeap(); + DEBUG_HEAP_BEFORE; auto a = packetPool.allocCopy(p); - auto heapAfter = memGet.getFreeHeap(); - LOG_HEAP("Alloc in MeshService::handleToRadio() pointer 0x%x, size: %u, free: %u", a, heapBefore - heapAfter, heapAfter); - + DEBUG_HEAP_AFTER("MeshService::handleToRadio", a); sendToMesh(a, RX_SRC_USER); bool loopback = false; // if true send any packet the phone sends back itself (for testing) @@ -254,10 +252,9 @@ void MeshService::sendToMesh(meshtastic_MeshPacket *p, RxSource src, bool ccToPh } if ((res == ERRNO_OK || res == ERRNO_SHOULD_RELEASE) && ccToPhone) { // Check if p is not released in case it couldn't be sent - auto heapBefore = memGet.getFreeHeap(); + DEBUG_HEAP_BEFORE; auto a = packetPool.allocCopy(*p); - auto heapAfter = memGet.getFreeHeap(); - LOG_HEAP("Alloc in MeshService::sendToMesh() pointer 0x%x, size: %u, free: %u", a, heapBefore - heapAfter, heapAfter); + DEBUG_HEAP_AFTER("MeshService::sendToMesh", a); sendToPhone(a); } diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 890d42b00a3..6d098b6696a 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -22,10 +22,9 @@ ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p) if (p->hop_limit == 0) { p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); } - auto heapBefore = memGet.getFreeHeap(); + DEBUG_HEAP_BEFORE; auto copy = packetPool.allocCopy(*p); - auto heapAfter = memGet.getFreeHeap(); - LOG_HEAP("Alloc in ReliableRouter::send() pointer 0x%x, size: %u, free: %u", copy, heapBefore - heapAfter, heapAfter); + DEBUG_HEAP_AFTER("ReliableRouter::send", copy); startRetransmission(copy, NUM_RELIABLE_RETX); } diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 603dfda4a86..4442b5d506a 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -276,11 +276,9 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it - auto heapBefore = memGet.getFreeHeap(); + DEBUG_HEAP_BEFORE; meshtastic_MeshPacket *p_decoded = packetPool.allocCopy(*p); - auto heapAfter = memGet.getFreeHeap(); - - LOG_HEAP("Alloc in Router::send pointer 0x%x, size: %u, free: %u", p_decoded, heapBefore - heapAfter, heapAfter); + DEBUG_HEAP_AFTER("Router::send", p_decoded); auto encodeResult = perhapsEncode(p); if (encodeResult != meshtastic_Routing_Error_NONE) { @@ -612,11 +610,11 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) bool skipHandle = false; // Also, we should set the time from the ISR and it should have msec level resolution p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone + // Store a copy of encrypted packet for MQTT - auto heapBefore = memGet.getFreeHeap(); + DEBUG_HEAP_BEFORE; meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p); - auto heapAfter = memGet.getFreeHeap(); - LOG_HEAP("Alloc in Router::handleReceived pointer 0x%x, size: %u, free: %u", p_encrypted, heapBefore - heapAfter, heapAfter); + DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted); // Take those raw bytes and convert them back into a well structured protobuf we can understand auto decodedState = perhapsDecode(p); diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index 82632f66714..86ceaae24d1 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -44,11 +44,10 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal) service->cancelSending(prevPacketId); shorterTimeout = _shorterTimeout; - auto heapBefore = memGet.getFreeHeap(); + DEBUG_HEAP_BEFORE; meshtastic_MeshPacket *p = allocReply(); - auto heapAfter = memGet.getFreeHeap(); + DEBUG_HEAP_AFTER("NodeInfoModule::sendOurNodeInfo", p); - LOG_HEAP("Alloc in NodeInfoModule::sendOurNodeInfo pointer 0x%x, size: %u, free: %u", p, heapBefore - heapAfter, heapAfter); if (p) { // Check whether we didn't ignore it p->to = dest; p->decoded.want_response = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && diff --git a/src/modules/Telemetry/DeviceTelemetry.cpp b/src/modules/Telemetry/DeviceTelemetry.cpp index 8694de99387..98d5b19d0a2 100644 --- a/src/modules/Telemetry/DeviceTelemetry.cpp +++ b/src/modules/Telemetry/DeviceTelemetry.cpp @@ -172,11 +172,9 @@ bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) telemetry.variant.device_metrics.battery_level, telemetry.variant.device_metrics.voltage, telemetry.variant.device_metrics.uptime_seconds); - auto heapBefore = memGet.getFreeHeap(); + DEBUG_HEAP_BEFORE; meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); - auto heapAfter = memGet.getFreeHeap(); - LOG_HEAP("Alloc in DeviceTelemetryModule::sendTelemetry() pointer 0x%x, size: %u, free: %u", p, heapBefore - heapAfter, - heapAfter); + DEBUG_HEAP_AFTER("DeviceTelemetryModule::sendTelemetry", p); p->to = dest; p->decoded.want_response = false; From 7b75468d64d68dc12c73a21239b2c7042467fa40 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 11 Sep 2025 18:57:30 -0500 Subject: [PATCH 17/52] Cleanup --- src/mesh/MeshModule.cpp | 1 - src/mesh/Router.cpp | 4 +--- src/modules/NodeInfoModule.cpp | 4 ++-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index 22fcec663f1..c5748a56066 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -100,7 +100,6 @@ void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src) // Was this message directed to us specifically? Will be false if we are sniffing someone elses packets auto ourNodeNum = nodeDB->getNodeNum(); bool toUs = isBroadcast(mp.to) || isToUs(&mp); - bool fromUs = mp.from == ourNodeNum; for (auto i = modules->begin(); i != modules->end(); ++i) { auto &pi = **i; diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 4442b5d506a..44d09637f26 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -662,7 +662,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) // call modules here // If this could be a spoofed packet, don't let the modules see it. - if (!skipHandle && p->from != nodeDB->getNodeNum()) { + if (!skipHandle) { MeshModule::callModules(*p, src); #if !MESHTASTIC_EXCLUDE_MQTT @@ -676,8 +676,6 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) !isFromUs(p) && mqtt) mqtt->onSend(*p_encrypted, *p, p->channel); #endif - } else if (p->from == nodeDB->getNodeNum() && !skipHandle) { - MeshModule::callModules(*p, src); } packetPool.release(p_encrypted); // Release the encrypted packet diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index 86ceaae24d1..276a11b3a40 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -12,12 +12,12 @@ NodeInfoModule *nodeInfoModule; bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr) { - auto p = *pptr; - if (mp.from == nodeDB->getNodeNum()) { LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from); return false; } + + auto p = *pptr; if (p.is_licensed != owner.is_licensed) { LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!"); return true; From d7ef19b19c2a4711a7b3e35903575473ccf82b9e Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Fri, 12 Sep 2025 10:40:13 -0700 Subject: [PATCH 18/52] Fix memory leak in NextHopRouter: always free packet copy when removing from pending --- src/mesh/NextHopRouter.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 7ceca219578..db3d62038b8 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -175,12 +175,18 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key) config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { // remove the 'original' (identified by originator and packet->id) from the txqueue and free it cancelSending(getFrom(p), p->id); - // now free the pooled copy for retransmission too - packetPool.release(p); } } + + // Regardless of whether or not we canceled this packet from the txQueue, remove it from our pending list so it doesn't + // get scheduled again. (This is the core of stopRetransmission.) auto numErased = pending.erase(key); assert(numErased == 1); + + // When we remove an entry from pending, always be sure to release the copy of the packet that was allocated in the call + // to startRetransmission. + packetPool.release(p); + return true; } else return false; From ab09d9bc37036609549c8975fa1aaec1164e7d4c Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 12 Sep 2025 15:49:56 -0500 Subject: [PATCH 19/52] Formatting --- src/mesh/MemoryPool.h | 76 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h index ea7c8f5832a..0c5ba6c71ce 100644 --- a/src/mesh/MemoryPool.h +++ b/src/mesh/MemoryPool.h @@ -6,6 +6,7 @@ #include #include "PointerQueue.h" +#include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP template class Allocator { @@ -14,13 +15,14 @@ template class Allocator Allocator() : deleter([this](T *p) { this->release(p); }) {} virtual ~Allocator() {} - /// Return a queable object which has been prefilled with zeros. Panic if no buffer is available + /// Return a queable object which has been prefilled with zeros. Return nullptr if no buffer is available /// Note: this method is safe to call from regular OR ISR code T *allocZeroed() { T *p = allocZeroed(0); - - assert(p); // FIXME panic instead + if (!p) { + LOG_WARN("Failed to allocate zeroed memory"); + } return p; } @@ -39,10 +41,12 @@ template class Allocator T *allocCopy(const T &src, TickType_t maxWait = portMAX_DELAY) { T *p = alloc(maxWait); - assert(p); + if (!p) { + LOG_WARN("Failed to allocate memory for copy"); + return nullptr; + } - if (p) - *p = src; + *p = src; return p; } @@ -83,7 +87,9 @@ template class MemoryDynamic : public Allocator /// Return a buffer for use by others virtual void release(T *p) override { - assert(p); + if (p == nullptr) + return; + LOG_HEAP("Freeing 0x%x", p); free(p); @@ -98,3 +104,59 @@ template class MemoryDynamic : public Allocator return p; } }; + +/** + * A static memory pool that uses a fixed buffer instead of heap allocation + */ +template class MemoryPool : public Allocator +{ + private: + T pool[MaxSize]; + bool used[MaxSize]; + + public: + MemoryPool() + { + // Initialize the used array to false (all slots free) + for (int i = 0; i < MaxSize; i++) { + used[i] = false; + } + } + + /// Return a buffer for use by others + virtual void release(T *p) override + { + if (!p) { + LOG_DEBUG("Failed to release memory, pointer is null"); + return; + } + + // Find the index of this pointer in our pool + int index = p - pool; + if (index >= 0 && index < MaxSize) { + assert(used[index]); // Should be marked as used + used[index] = false; + LOG_HEAP("Released static pool item %d at 0x%x", index, p); + } else { + LOG_WARN("Pointer 0x%x not from our pool!", p); + } + } + + protected: + // Alloc some storage from our static pool + virtual T *alloc(TickType_t maxWait) override + { + // Find first free slot + for (int i = 0; i < MaxSize; i++) { + if (!used[i]) { + used[i] = true; + LOG_HEAP("Allocated static pool item %d at 0x%x", i, &pool[i]); + return &pool[i]; + } + } + + // No free slots available - return nullptr instead of asserting + LOG_WARN("No free slots available in static memory pool!"); + return nullptr; + } +}; From 4aa5e91c3117469ca8ab70088a75e897c3376487 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 12 Sep 2025 16:07:27 -0500 Subject: [PATCH 20/52] Only queue 2 client notification --- src/mesh/mesh-pb-constants.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index 224f45de251..12aec98cd5d 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -30,7 +30,7 @@ /// max number of ClientNotification packets which can be waiting for delivery to phone #ifndef MAX_RX_NOTIFICATION_TOPHONE -#define MAX_RX_NOTIFICATION_TOPHONE 4 +#define MAX_RX_NOTIFICATION_TOPHONE 2 #endif /// Verify baseline assumption of node size. If it increases, we need to reevaluate From 8468bf9a13e17e3fe78dfa4190199d01b8517f69 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 12 Sep 2025 17:12:18 -0500 Subject: [PATCH 21/52] Merge pull request #7965 from compumike/compumike/fix-nrf52-bluetooth-memory-leak Fix memory leak in `NRF52Bluetooth`: allocate `BluetoothStatus` on stack, not heap --- src/platform/nrf52/NRF52Bluetooth.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 6f0e7250fdd..f8366ae32eb 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -59,7 +59,8 @@ void onConnect(uint16_t conn_handle) LOG_INFO("BLE Connected to %s", central_name); // Notify UI (or any other interested firmware components) - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED)); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newStatus); } /** * Callback invoked when a connection is dropped @@ -74,7 +75,8 @@ void onDisconnect(uint16_t conn_handle, uint8_t reason) } // Notify UI (or any other interested firmware components) - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newStatus); } void onCccd(uint16_t conn_hdl, BLECharacteristic *chr, uint16_t cccd_value) { @@ -326,7 +328,8 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke textkey += (char)passkey[i]; // Notify UI (or other components) of pairing event and passkey - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(textkey)); + meshtastic::BluetoothStatus newStatus(textkey); + bluetoothStatus->updateStatus(&newStatus); #if !defined(MESHTASTIC_EXCLUDE_SCREEN) // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (screen) { @@ -398,12 +401,13 @@ void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_statu { if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) { LOG_INFO("BLE pair success"); - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED)); + meshtastic::BluetoothStatus newConnectedStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newConnectedStatus); } else { LOG_INFO("BLE pair failed"); // Notify UI (or any other interested firmware components) - bluetoothStatus->updateStatus( - new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + meshtastic::BluetoothStatus newDisconnectedStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newDisconnectedStatus); } // Todo: migrate this display code back into Screen class, and observe bluetoothStatus From 8d1b20db04f928c7ed2deebd639861165b57834e Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 12 Sep 2025 17:12:27 -0500 Subject: [PATCH 22/52] Merge pull request #7964 from compumike/compumike/fix-nimble-bluetooth-memory-leak Fix memory leak in `NimbleBluetooth`: allocate `BluetoothStatus` on stack, not heap --- src/nimble/NimbleBluetooth.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 95e191c8e62..ee95168c3b6 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -133,7 +133,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks LOG_INFO("*** Enter passkey %d on the peer side ***", passkey); powerFSM.trigger(EVENT_BLUETOOTH_PAIR); - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(std::to_string(passkey))); + meshtastic::BluetoothStatus newStatus(std::to_string(passkey)); + bluetoothStatus->updateStatus(&newStatus); #if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (screen) { @@ -173,7 +174,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { LOG_INFO("BLE authentication complete"); - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED)); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newStatus); // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (passkeyShowing) { @@ -187,8 +189,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { LOG_INFO("BLE disconnect"); - bluetoothStatus->updateStatus( - new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newStatus); if (bluetoothPhoneAPI) { std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); From 9f25738ea07d16b6acc04778d37935e232210e01 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 13 Sep 2025 06:37:58 -0500 Subject: [PATCH 23/52] Update protobufs (#7973) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/device_ui.pb.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/protobufs b/protobufs index a84657c2204..8caf4239643 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit a84657c220421536f18d11fc5edf680efadbceeb +Subproject commit 8caf42396438f0d8a0305143485fd671c1fc7126 diff --git a/src/mesh/generated/meshtastic/device_ui.pb.h b/src/mesh/generated/meshtastic/device_ui.pb.h index 8313438f871..8f693e5703d 100644 --- a/src/mesh/generated/meshtastic/device_ui.pb.h +++ b/src/mesh/generated/meshtastic/device_ui.pb.h @@ -66,6 +66,8 @@ typedef enum _meshtastic_Language { meshtastic_Language_UKRAINIAN = 16, /* Bulgarian */ meshtastic_Language_BULGARIAN = 17, + /* Czech */ + meshtastic_Language_CZECH = 18, /* Simplified Chinese (experimental) */ meshtastic_Language_SIMPLIFIED_CHINESE = 30, /* Traditional Chinese (experimental) */ From a5876f86df6566a1a482a1acd6eb1f116391201e Mon Sep 17 00:00:00 2001 From: WillyJL Date: Sat, 13 Sep 2025 13:50:02 +0200 Subject: [PATCH 24/52] T-Lora Pager: Support LR1121 and SX1280 models (#7956) * T-Lora Pager: Support LR1121 and SX1280 models * Remove ifdefs --- variants/esp32s3/tlora-pager/rfswitch.h | 18 ++++++++++++++++++ variants/esp32s3/tlora-pager/variant.h | 19 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 variants/esp32s3/tlora-pager/rfswitch.h diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h new file mode 100644 index 00000000000..1e5eb7a9e20 --- /dev/null +++ b/variants/esp32s3/tlora-pager/rfswitch.h @@ -0,0 +1,18 @@ +#include "RadioLib.h" + +static const uint32_t rfswitch_dio_pins[] = { + RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, + RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC +}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, { LOW, LOW } }, + { LR11x0::MODE_RX, { LOW, HIGH } }, + { LR11x0::MODE_TX, { HIGH, LOW } }, + { LR11x0::MODE_TX_HP, { HIGH, LOW } }, + { LR11x0::MODE_TX_HF, { LOW, LOW } }, + { LR11x0::MODE_GNSS, { LOW, LOW } }, + { LR11x0::MODE_WIFI, { LOW, LOW } }, + END_OF_MODE_TABLE, +}; \ No newline at end of file diff --git a/variants/esp32s3/tlora-pager/variant.h b/variants/esp32s3/tlora-pager/variant.h index ee48088c885..2875f6804f0 100644 --- a/variants/esp32s3/tlora-pager/variant.h +++ b/variants/esp32s3/tlora-pager/variant.h @@ -105,14 +105,16 @@ // LoRa #define USE_SX1262 #define USE_SX1268 +#define USE_SX1280 +#define USE_LR1121 #define LORA_SCK 35 #define LORA_MISO 33 #define LORA_MOSI 34 #define LORA_CS 36 +#define LORA_RESET 47 #define LORA_DIO0 -1 // a No connect on the SX1262 module -#define LORA_RESET 47 #define LORA_DIO1 14 // SX1262 IRQ #define LORA_DIO2 48 // SX1262 BUSY #define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled @@ -123,3 +125,18 @@ #define SX126X_RESET LORA_RESET #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 3.0 + +#define SX128X_CS LORA_CS +#define SX128X_DIO1 LORA_DIO1 +#define SX128X_BUSY LORA_DIO2 +#define SX128X_RESET LORA_RESET + +#define LR1121_IRQ_PIN LORA_DIO1 +#define LR1121_NRESET_PIN LORA_RESET +#define LR1121_BUSY_PIN LORA_DIO2 +#define LR1121_SPI_NSS_PIN LORA_CS +#define LR1121_SPI_SCK_PIN LORA_SCK +#define LR1121_SPI_MOSI_PIN LORA_MOSI +#define LR1121_SPI_MISO_PIN LORA_MISO +#define LR11X0_DIO3_TCXO_VOLTAGE 3.0 +#define LR11X0_DIO_AS_RF_SWITCH From 1d757ba739660e23a123f9665b42010a7730c6ad Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 06:51:18 -0500 Subject: [PATCH 25/52] Trunk --- variants/esp32s3/tlora-pager/rfswitch.h | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h index 1e5eb7a9e20..0fba5a30579 100644 --- a/variants/esp32s3/tlora-pager/rfswitch.h +++ b/variants/esp32s3/tlora-pager/rfswitch.h @@ -1,18 +1,11 @@ #include "RadioLib.h" -static const uint32_t rfswitch_dio_pins[] = { - RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, - RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC -}; +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - { LR11x0::MODE_STBY, { LOW, LOW } }, - { LR11x0::MODE_RX, { LOW, HIGH } }, - { LR11x0::MODE_TX, { HIGH, LOW } }, - { LR11x0::MODE_TX_HP, { HIGH, LOW } }, - { LR11x0::MODE_TX_HF, { LOW, LOW } }, - { LR11x0::MODE_GNSS, { LOW, LOW } }, - { LR11x0::MODE_WIFI, { LOW, LOW } }, - END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH}}, + {LR11x0::MODE_TX, {HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; \ No newline at end of file From 375ab363b6821a6ebd4184e8c2b3d07690002f22 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 06:57:12 -0500 Subject: [PATCH 26/52] Trunk From 33ffa8df9ea066feb781b11c3ea3cdbd61ef8c19 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 07:01:07 -0500 Subject: [PATCH 27/52] Static memory pool allocation (#7966) * Static memory pool * Initializer * T-Lora Pager: Support LR1121 and SX1280 models (#7956) * T-Lora Pager: Support LR1121 and SX1280 models * Remove ifdefs --------- Co-authored-by: WillyJL --- src/mesh/MemoryPool.h | 9 ++++----- src/mesh/MeshService.cpp | 9 ++++++--- src/mesh/Router.cpp | 3 +-- variants/esp32s3/tlora-pager/rfswitch.h | 12 ++++++++---- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h index 0c5ba6c71ce..eb5ac5109de 100644 --- a/src/mesh/MemoryPool.h +++ b/src/mesh/MemoryPool.h @@ -115,12 +115,11 @@ template class MemoryPool : public Allocator bool used[MaxSize]; public: - MemoryPool() + MemoryPool() : pool{}, used{} { - // Initialize the used array to false (all slots free) - for (int i = 0; i < MaxSize; i++) { - used[i] = false; - } + // Arrays are now zero-initialized by member initializer list + // pool array: all elements are default-constructed (zero for POD types) + // used array: all elements are false (zero-initialized) } /// Return a buffer for use by others diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 607766ab6aa..96782cda550 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -46,11 +46,14 @@ the new node can build its node db) MeshService *service; -static MemoryDynamic staticMqttClientProxyMessagePool; +#define MAX_MQTT_PROXY_MESSAGES 16 +static MemoryPool staticMqttClientProxyMessagePool; -static MemoryDynamic staticQueueStatusPool; +#define MAX_QUEUE_STATUS 4 +static MemoryPool staticQueueStatusPool; -static MemoryDynamic staticClientNotificationPool; +#define MAX_CLIENT_NOTIFICATIONS 4 +static MemoryPool staticClientNotificationPool; Allocator &mqttClientProxyMessagePool = staticMqttClientProxyMessagePool; diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 44d09637f26..b5ae1ec0ccc 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -31,8 +31,7 @@ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ 2) // max number of packets which can be in flight (either queued from reception or queued for sending) -// static MemoryPool staticPool(MAX_PACKETS); -static MemoryDynamic staticPool; +static MemoryPool staticPool; Allocator &packetPool = staticPool; diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h index 0fba5a30579..337346ec597 100644 --- a/variants/esp32s3/tlora-pager/rfswitch.h +++ b/variants/esp32s3/tlora-pager/rfswitch.h @@ -4,8 +4,12 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH}}, - {LR11x0::MODE_TX, {HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, - {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, + {LR11x0::MODE_RX, {LOW, HIGH}}, + {LR11x0::MODE_TX, {HIGH, LOW}}, + {LR11x0::MODE_TX_HP, {HIGH, LOW}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, + {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, + END_OF_MODE_TABLE, }; \ No newline at end of file From 55b42b95e19330a101c4638fa80fdc8c461c8727 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 11:59:50 -0500 Subject: [PATCH 28/52] Portduino dynamic alloc --- src/mesh/Router.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index b5ae1ec0ccc..c5eed5180ea 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -5,6 +5,7 @@ #include "MeshService.h" #include "NodeDB.h" #include "RTC.h" + #include "configuration.h" #include "detect/LoRaRadioType.h" #include "main.h" @@ -27,13 +28,24 @@ // I think this is right, one packet for each of the three fifos + one packet being currently assembled for TX or RX // And every TX packet might have a retransmission packet or an ack alive at any moment + +#ifdef ARCH_PORTDUINO +// Portduino (native) targets can use dynamic memory pools with runtime-configurable sizes #define MAX_PACKETS \ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ 2) // max number of packets which can be in flight (either queued from reception or queued for sending) -static MemoryPool staticPool; +static MemoryDynamic dynamicPool(MAX_PACKETS); +Allocator &packetPool = dynamicPool; +#else +// Embedded targets use static memory pools with compile-time constants +#define MAX_PACKETS_STATIC \ + (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ + 2) // max number of packets which can be in flight (either queued from reception or queued for sending) +static MemoryPool staticPool; Allocator &packetPool = staticPool; +#endif static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__)); From 1da6d2832f5db50ec9ae04468a2148037abab4eb Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 11:59:58 -0500 Subject: [PATCH 29/52] Missed From 3092ec7a0f3df3af59feb7c4283d35619b42900e Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 12:07:14 -0500 Subject: [PATCH 30/52] Drop the limit --- src/mesh/Router.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index c5eed5180ea..6c5d08a93a4 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -35,7 +35,7 @@ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ 2) // max number of packets which can be in flight (either queued from reception or queued for sending) -static MemoryDynamic dynamicPool(MAX_PACKETS); +static MemoryDynamic dynamicPool; Allocator &packetPool = dynamicPool; #else // Embedded targets use static memory pools with compile-time constants From 569c0d22067beb7860c0e67b43b4e728fc10a8b2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 13 Sep 2025 16:31:56 -0500 Subject: [PATCH 31/52] Update meshtastic-esp8266-oled-ssd1306 digest to 0cbc26b (#7977) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 81bda239c80..64e827b3efb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -60,7 +60,7 @@ monitor_speed = 115200 monitor_filters = direct lib_deps = # renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master - https://github.com/meshtastic/esp8266-oled-ssd1306/archive/9573abb64dc9c94f3051348f2bf4fc5cedf03c22.zip + https://github.com/meshtastic/esp8266-oled-ssd1306/archive/0cbc26b1f8f61957af0475f486b362eafe7cc4e2.zip # renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip # renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master From 2fe065ddc9b17a7e11fbede0c1a2acee0113d744 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 18:52:46 -0500 Subject: [PATCH 32/52] Fix json report crashes on esp32 (#7978) --- src/mesh/http/ContentHandler.cpp | 39 ++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index 74953d8fcc8..fb66dae7c6e 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -292,11 +292,14 @@ JSONArray htmlListDir(const char *dirname, uint8_t levels) JSONObject thisFileMap; thisFileMap["size"] = new JSONValue((int)file.size()); #ifdef ARCH_ESP32 - thisFileMap["name"] = new JSONValue(String(file.path()).substring(1).c_str()); + String fileName = String(file.path()).substring(1); + thisFileMap["name"] = new JSONValue(fileName.c_str()); #else - thisFileMap["name"] = new JSONValue(String(file.name()).substring(1).c_str()); + String fileName = String(file.name()).substring(1); + thisFileMap["name"] = new JSONValue(fileName.c_str()); #endif - if (String(file.name()).substring(1).endsWith(".gz")) { + String tempName = String(file.name()).substring(1); + if (tempName.endsWith(".gz")) { #ifdef ARCH_ESP32 String modifiedFile = String(file.path()).substring(1); #else @@ -339,7 +342,8 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res) JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; @@ -367,7 +371,8 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) JSONObject jsonObjOuter; jsonObjOuter["status"] = new JSONValue("ok"); JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; return; } else { @@ -376,7 +381,8 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) JSONObject jsonObjOuter; jsonObjOuter["status"] = new JSONValue("Error"); JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; return; } @@ -622,10 +628,7 @@ void handleReport(HTTPRequest *req, HTTPResponse *res) tempArray.push_back(new JSONValue((int)logArray[i])); } JSONValue *result = new JSONValue(tempArray); - // Clean up original array to prevent memory leak - for (auto *val : tempArray) { - delete val; - } + // Note: Don't delete tempArray elements here - JSONValue now owns them return result; }; @@ -656,7 +659,9 @@ void handleReport(HTTPRequest *req, HTTPResponse *res) // data->wifi JSONObject jsonObjWifi; jsonObjWifi["rssi"] = new JSONValue(WiFi.RSSI()); - jsonObjWifi["ip"] = new JSONValue(WiFi.localIP().toString().c_str()); + String wifiIPString = WiFi.localIP().toString(); + std::string wifiIP = wifiIPString.c_str(); + jsonObjWifi["ip"] = new JSONValue(wifiIP.c_str()); // data->memory JSONObject jsonObjMemory; @@ -702,7 +707,8 @@ void handleReport(HTTPRequest *req, HTTPResponse *res) jsonObjOuter["status"] = new JSONValue("ok"); // serialize and write it to the stream JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; } @@ -773,7 +779,8 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res) jsonObjOuter["status"] = new JSONValue("ok"); // serialize and write it to the stream JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; // Clean up the nodesArray to prevent memory leak @@ -926,7 +933,8 @@ void handleBlinkLED(HTTPRequest *req, HTTPResponse *res) JSONObject jsonObjOuter; jsonObjOuter["status"] = new JSONValue("ok"); JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; } @@ -968,7 +976,8 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res) // serialize and write it to the stream JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; // Clean up the networkObjs to prevent memory leak From d9eb18fdff5eab0f78e065644eb8d05fa5ed0801 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 18:57:00 -0500 Subject: [PATCH 33/52] Tweak maximums --- src/mesh/mesh-pb-constants.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index 12aec98cd5d..868670f42b1 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -20,12 +20,12 @@ /// max number of QueueStatus packets which can be waiting for delivery to phone #ifndef MAX_RX_QUEUESTATUS_TOPHONE -#define MAX_RX_QUEUESTATUS_TOPHONE 4 +#define MAX_RX_QUEUESTATUS_TOPHONE 2 #endif /// max number of MqttClientProxyMessage packets which can be waiting for delivery to phone #ifndef MAX_RX_MQTTPROXY_TOPHONE -#define MAX_RX_MQTTPROXY_TOPHONE 32 +#define MAX_RX_MQTTPROXY_TOPHONE 16 #endif /// max number of ClientNotification packets which can be waiting for delivery to phone From 9fdd31a0484c0b594467a669cf837545c4d0f3a6 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 20:14:10 -0500 Subject: [PATCH 34/52] Fix DRAM overflow on old esp32 targets --- src/mesh/mesh-pb-constants.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index 868670f42b1..e4f65aa283e 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -15,8 +15,12 @@ // FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big array in // RAM #define MAX_RX_TOPHONE (member_size(DeviceState, receive_queue) / member_size(DeviceState, receive_queue[0])) #ifndef MAX_RX_TOPHONE +#if defined(ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)) +#define MAX_RX_TOPHONE 8 +#else #define MAX_RX_TOPHONE 32 #endif +#endif /// max number of QueueStatus packets which can be waiting for delivery to phone #ifndef MAX_RX_QUEUESTATUS_TOPHONE @@ -25,7 +29,7 @@ /// max number of MqttClientProxyMessage packets which can be waiting for delivery to phone #ifndef MAX_RX_MQTTPROXY_TOPHONE -#define MAX_RX_MQTTPROXY_TOPHONE 16 +#define MAX_RX_MQTTPROXY_TOPHONE 8 #endif /// max number of ClientNotification packets which can be waiting for delivery to phone From e9fb1b5ff6e0db9d07683700c01341a467dc2874 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Mon, 8 Sep 2025 10:29:26 +1000 Subject: [PATCH 35/52] Guard bad time warning logs using GPS_DEBUG (#7897) In 2.7.7 / 2.7.8 we introduced some new checks for time accuracy. In combination, these result in a spamming of the logs when a bad time is found When the GPS is active, we're calling the GPS thread every 0.2secs. So this log could be printed 4,500 times in a no-lock scenario :) Reserve this experience for developers using GPS_DEBUG. Fixes https://github.com/meshtastic/firmware/issues/7896 --- src/gps/RTC.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index e208e2df9cc..39b633e477e 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -130,11 +130,15 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv->tv_sec < BUILD_EPOCH) { +#ifdef GPS_DEBUG LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); +#endif return RTCSetResultInvalidTime; } else if (tv->tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { +#ifdef GPS_DEBUG LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, BUILD_EPOCH + FORTY_YEARS); +#endif return RTCSetResultInvalidTime; } #endif @@ -252,11 +256,15 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { +#ifdef GPS_DEBUG LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); +#endif return RTCSetResultInvalidTime; } else if (tv.tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { +#ifdef GPS_DEBUG LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, BUILD_EPOCH + FORTY_YEARS); +#endif return RTCSetResultInvalidTime; } #endif From 4b4609f2645baadcecad865f5d677d8125d220c3 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 14 Sep 2025 06:31:17 -0500 Subject: [PATCH 36/52] Scale probe buffer size based on current baud rate (#7975) * Scale probe buffer size based on current baud rate * Throttle bad time validation logging and fix time comparison logic * Remove comment * Missed the other instances * Copy pasta --- src/gps/GPS.cpp | 19 +++++++++++++---- src/gps/GPS.h | 2 +- src/gps/RTC.cpp | 54 ++++++++++++++++++++++++++++++++----------------- src/gps/RTC.h | 2 +- 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index d4e9076d9e0..a663f46c489 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1205,7 +1205,7 @@ static const char *DETECTED_MESSAGE = "%s detected"; LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME); \ clearBuffer(); \ _serial_gps->write(COMMAND "\r\n"); \ - GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP); \ + GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP, serialSpeed); \ if (detectedDriver != GNSS_MODEL_UNKNOWN) { \ return detectedDriver; \ } \ @@ -1367,9 +1367,18 @@ GnssModel_t GPS::probe(int serialSpeed) return GNSS_MODEL_UNKNOWN; } -GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap) +GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed) { - char response[256] = {0}; // Fixed buffer instead of String + // Calculate buffer size based on baud rate - 256 bytes for 9600 baud as baseline + // Higher baud rates get proportionally larger buffers to handle more data + int bufferSize = (serialSpeed * 256) / 9600; + // Clamp buffer size between reasonable limits + if (bufferSize < 128) + bufferSize = 128; + if (bufferSize > 2048) + bufferSize = 2048; + + char *response = new char[bufferSize](); // Dynamically allocate based on baud rate uint16_t responseLen = 0; unsigned long start = millis(); while (millis() - start < timeout) { @@ -1377,7 +1386,7 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vectorread(); // Add char to buffer if there's space - if (responseLen < sizeof(response) - 1) { + if (responseLen < bufferSize - 1) { response[responseLen++] = c; response[responseLen] = '\0'; } @@ -1390,6 +1399,7 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap); + GnssModel_t getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed); // Get GNSS model GnssModel_t probe(int serialSpeed); diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 39b633e477e..3e410d236b7 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -9,6 +9,9 @@ static RTCQuality currentQuality = RTCQualityNone; uint32_t lastSetFromPhoneNtpOrGps = 0; +static uint32_t lastTimeValidationWarning = 0; +static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds + RTCQuality getRTCQuality() { return currentQuality; @@ -48,7 +51,9 @@ RTCSetResult readFromRTC() #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + } return RTCSetResultInvalidTime; } #endif @@ -87,7 +92,10 @@ RTCSetResult readFromRTC() #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; } #endif @@ -130,15 +138,20 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv->tv_sec < BUILD_EPOCH) { -#ifdef GPS_DEBUG - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); -#endif + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; - } else if (tv->tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { -#ifdef GPS_DEBUG - LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, - BUILD_EPOCH + FORTY_YEARS); -#endif + } else if (tv->tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + // Calculate max allowed time safely to avoid overflow in logging + uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; + uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; + LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, + (uint32_t)BUILD_EPOCH, maxAllowedPrintable); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; } #endif @@ -256,15 +269,20 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { -#ifdef GPS_DEBUG - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); -#endif + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; - } else if (tv.tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { -#ifdef GPS_DEBUG - LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, - BUILD_EPOCH + FORTY_YEARS); -#endif + } else if (tv.tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + // Calculate max allowed time safely to avoid overflow in logging + uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; + uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; + LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, + (uint32_t)BUILD_EPOCH, maxAllowedPrintable); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; } #endif diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 03350823cb9..1ecde79aee8 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -56,5 +56,5 @@ time_t gm_mktime(struct tm *tm); #define SEC_PER_HOUR 3600 #define SEC_PER_MIN 60 #ifdef BUILD_EPOCH -#define FORTY_YEARS (40UL * 365 * SEC_PER_DAY) // probably time to update your firmware +#define FORTY_YEARS (40ULL * 365 * SEC_PER_DAY) // Use 64-bit arithmetic to prevent overflow #endif From aea2072128ea6c3be80effc2fba3e797a683a7cc Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Sun, 14 Sep 2025 03:05:06 -0700 Subject: [PATCH 37/52] Fix GPS gm_mktime memory leak (#7981) --- src/gps/RTC.cpp | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 3e410d236b7..4a629d755d1 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -342,14 +342,40 @@ uint32_t getValidTime(RTCQuality minQuality, bool local) time_t gm_mktime(struct tm *tm) { #if !MESHTASTIC_EXCLUDE_TZ - setenv("TZ", "GMT0", 1); - time_t res = mktime(tm); - if (*config.device.tzdef) { - setenv("TZ", config.device.tzdef, 1); - } else { - setenv("TZ", "UTC0", 1); + time_t result = 0; + + // First, get us to the start of tm->year, by calcuating the number of days since the Unix epoch. + int year = 1900 + tm->tm_year; // tm_year is years since 1900 + int year_minus_one = year - 1; + int days_before_this_year = 0; + days_before_this_year += year_minus_one * 365; + // leap days: every 4 years, except 100s, but including 400s. + days_before_this_year += year_minus_one / 4 - year_minus_one / 100 + year_minus_one / 400; + // subtract from 1970-01-01 to get days since epoch + days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400); + + // Now, within this tm->year, compute the days *before* this tm->month starts. + int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year + int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11 + + // If this is a leap year, and we're past February, add a day: + if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) { + days_this_year_before_this_month += 1; } - return res; + + // And within this month: + int days_this_month_before_today = tm->tm_mday - 1; // tm->tm_mday is 1..31 + + // Now combine them all together, and convert days to seconds: + result += (days_before_this_year + days_this_year_before_this_month + days_this_month_before_today); + result *= 86400L; + + // Finally, add in the hours, minutes, and seconds of today: + result += tm->tm_hour * 3600; + result += tm->tm_min * 60; + result += tm->tm_sec; + + return result; #else return mktime(tm); #endif From 520c47956522870e71ff101184cdbeffce6bf763 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 14 Sep 2025 08:12:38 -0500 Subject: [PATCH 38/52] Fix overflow of time value (#7984) * Fix overflow of time value * Revert "Fix overflow of time value" This reverts commit 084796920179e80a7500d36c25fd4d82b3ef4214. * That got boogered up --- src/gps/RTC.cpp | 4 ++-- src/gps/RTC.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 4a629d755d1..da20e28eb96 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -143,7 +143,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd lastTimeValidationWarning = millis(); } return RTCSetResultInvalidTime; - } else if (tv->tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) { + } else if ((uint64_t)tv->tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { // Calculate max allowed time safely to avoid overflow in logging uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; @@ -274,7 +274,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) lastTimeValidationWarning = millis(); } return RTCSetResultInvalidTime; - } else if (tv.tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) { + } else if ((uint64_t)tv.tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { // Calculate max allowed time safely to avoid overflow in logging uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 1ecde79aee8..eca17bf3527 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -56,5 +56,5 @@ time_t gm_mktime(struct tm *tm); #define SEC_PER_HOUR 3600 #define SEC_PER_MIN 60 #ifdef BUILD_EPOCH -#define FORTY_YEARS (40ULL * 365 * SEC_PER_DAY) // Use 64-bit arithmetic to prevent overflow +static constexpr uint64_t FORTY_YEARS = (40ULL * 365 * SEC_PER_DAY); // Use 64-bit arithmetic to prevent overflow #endif From 7c1fea039b5bbf9c854d8637115dcbe9cd74ecb9 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sun, 14 Sep 2025 19:38:18 +0200 Subject: [PATCH 39/52] Remove PMSA003 include from modules --- src/modules/Modules.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index f8ac9f2585b..15cd87d660b 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -242,11 +242,9 @@ void setupModules() (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { new EnvironmentTelemetryModule(); } -#if __has_include("Adafruit_PM25AQI.h") if (moduleConfig.has_telemetry && moduleConfig.telemetry.air_quality_enabled) { new AirQualityTelemetryModule(); } -#endif #if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 || nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) { From 44f5e060ff2598b1df837aab7f23d6fb7d2a4b4a Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 7 Jan 2026 12:38:09 +0100 Subject: [PATCH 40/52] Add flag to exclude air quality module --- src/configuration.h | 1 + src/modules/Modules.cpp | 8 ++++---- src/modules/Telemetry/AirQualityTelemetry.cpp | 9 ++++----- src/modules/Telemetry/AirQualityTelemetry.h | 2 +- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 2 +- variants/esp32/heltec_wireless_bridge/platformio.ini | 5 +++-- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/configuration.h b/src/configuration.h index b4ab570531e..73d9fb0fd70 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -468,6 +468,7 @@ along with this program. If not, see . #define MESHTASTIC_EXCLUDE_AUDIO 1 #define MESHTASTIC_EXCLUDE_DETECTIONSENSOR 1 #define MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR 1 +#define MESHTASTIC_EXCLUDE_AIR_QUALITY 1 #define MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY 1 #define MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION 1 #define MESHTASTIC_EXCLUDE_PAXCOUNTER 1 diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index d17730178ab..10a1a9a1eb2 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -252,12 +252,12 @@ void setupModules() (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { new EnvironmentTelemetryModule(); } - - if (moduleConfig.has_telemetry && moduleConfig.telemetry.air_quality_enabled && - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) { +#if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY + if (moduleConfig.has_telemetry && + (moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled)) { new AirQualityTelemetryModule(); } - +#endif #if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 || nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) { diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index df193c8a528..6e99205a69f 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "Default.h" @@ -42,11 +42,10 @@ int32_t AirQualityTelemetryModule::runOnce() */ // moduleConfig.telemetry.air_quality_enabled = 1; - // TODO there is no config in module_config.proto for air_quality_screen_enabled. Reusing environment one, although it should have its own - // moduleConfig.telemetry.environment_screen_enabled = 1; + // moduleConfig.telemetry.air_quality_screen_enabled = 1; // moduleConfig.telemetry.air_quality_interval = 15; - if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.environment_screen_enabled || + if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled || AIR_QUALITY_TELEMETRY_MODULE_ENABLE)) { // 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(); @@ -104,7 +103,7 @@ int32_t AirQualityTelemetryModule::runOnce() bool AirQualityTelemetryModule::wantUIFrame() { - return moduleConfig.telemetry.environment_screen_enabled; + return moduleConfig.telemetry.air_quality_screen_enabled; } void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index 8314c54bc28..b76ae589790 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY #pragma once diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index c83c3e01aca..8dc4c7dcee1 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "PMSA003ISensor.h" diff --git a/variants/esp32/heltec_wireless_bridge/platformio.ini b/variants/esp32/heltec_wireless_bridge/platformio.ini index 93c3e3394c2..62be4b45ed4 100644 --- a/variants/esp32/heltec_wireless_bridge/platformio.ini +++ b/variants/esp32/heltec_wireless_bridge/platformio.ini @@ -1,9 +1,9 @@ [env:heltec-wireless-bridge] -;build_type = debug ; to make it possible to step through our jtag debugger +;build_type = debug ; to make it possible to step through our jtag debugger extends = esp32_base board_level = extra board = heltec_wifi_lora_32 -build_flags = +build_flags = ${esp32_base.build_flags} -I variants/esp32/heltec_wireless_bridge -D HELTEC_WIRELESS_BRIDGE @@ -13,6 +13,7 @@ build_flags = -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D MESHTASTIC_EXCLUDE_DETECTIONSENSOR=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -D MESHTASTIC_EXCLUDE_AIR_QUALITY=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL=1 -D MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 -D MESHTASTIC_EXCLUDE_GPS=1 From d893954daa775b8ec413d1525ce3c37b1ad5b20d Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Thu, 8 Jan 2026 17:52:38 +0100 Subject: [PATCH 41/52] Rework PMSA003I to align with new I2C scanner * Reworks AQ telemetry to match new dynamic allocation method * Adds VBLE_I2C_CLOCK_SPEED build flag for sensors with different I2C speed requirements * Reworks PMSA003I --- src/configuration.h | 4 +- src/detect/ScanI2C.cpp | 2 +- src/detect/ScanI2C.h | 2 +- src/detect/ScanI2CTwoWire.cpp | 2 +- src/main.cpp | 1 - src/modules/Modules.cpp | 2 +- src/modules/Telemetry/AirQualityTelemetry.cpp | 87 +++++++++++++++---- src/modules/Telemetry/AirQualityTelemetry.h | 11 ++- .../Telemetry/Sensor/PMSA003ISensor.cpp | 64 +++++++------- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 14 +-- .../heltec_wireless_bridge/platformio.ini | 2 +- 11 files changed, 121 insertions(+), 70 deletions(-) diff --git a/src/configuration.h b/src/configuration.h index 73d9fb0fd70..e81caf89d5e 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -214,7 +214,7 @@ along with this program. If not, see . #define LPS22HB_ADDR_ALT 0x5D #define SHT31_4x_ADDR 0x44 #define SHT31_4x_ADDR_ALT 0x45 -#define PMSA0031_ADDR 0x12 +#define PMSA003I_ADDR 0x12 #define QMA6100P_ADDR 0x12 #define AHT10_ADDR 0x38 #define RCWL9620_ADDR 0x57 @@ -468,7 +468,7 @@ along with this program. If not, see . #define MESHTASTIC_EXCLUDE_AUDIO 1 #define MESHTASTIC_EXCLUDE_DETECTIONSENSOR 1 #define MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR 1 -#define MESHTASTIC_EXCLUDE_AIR_QUALITY 1 +#define MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR 1 #define MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY 1 #define MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION 1 #define MESHTASTIC_EXCLUDE_PAXCOUNTER 1 diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index 8ac503b83fe..165f7a99bca 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -43,7 +43,7 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const ScanI2C::FoundDevice ScanI2C::firstAQI() const { - ScanI2C::DeviceType types[] = {PMSA0031, SCD4X}; + ScanI2C::DeviceType types[] = {PMSA003I, SCD4X}; return firstOfOrNONE(2, types); } diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index cced980a66d..8c6880d7d32 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -38,7 +38,7 @@ class ScanI2C QMI8658, QMC5883L, HMC5883L, - PMSA0031, + PMSA003I, QMA6100P, MPU6050, LIS3DH, diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 45e7fda2d1b..78caa2e7623 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -438,7 +438,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) #ifdef HAS_QMA6100P SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address) #else - SCAN_SIMPLE_CASE(PMSA0031_ADDR, PMSA0031, "PMSA0031", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(PMSA003I_ADDR, PMSA003I, "PMSA003I", (uint8_t)addr.address) #endif case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2); diff --git a/src/main.cpp b/src/main.cpp index 4af275bf4c3..8986fb417dc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -753,7 +753,6 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PMSA0031, meshtastic_TelemetrySensorType_PMSA003I); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102); diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 10a1a9a1eb2..e17868bafd6 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -252,7 +252,7 @@ void setupModules() (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { new EnvironmentTelemetryModule(); } -#if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY +#if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR if (moduleConfig.has_telemetry && (moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled)) { new AirQualityTelemetryModule(); diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 6e99205a69f..5966d461392 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "Default.h" @@ -13,16 +13,61 @@ #include "UnitConversions.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" +#include "graphics/ScreenFonts.h" #include "main.h" #include "sleep.h" #include -// Sensor includes -#include "Sensor/PMSA003ISensor.h" // Sensors -PMSA003ISensor pmsa003iSensor; +#ifdef VBLE_I2C_CLOCK_SPEED +#include "Sensor/PMSA003ISensor.h" +#endif -#include "graphics/ScreenFonts.h" +#include + +static std::forward_list sensors; + +template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) +{ + ScanI2C::FoundDevice dev = i2cScanner->find(type); + if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { + TelemetrySensor *sensor = new T(); +#if WIRE_INTERFACES_COUNT > 1 + TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); + if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { + // This sensor only works on Wire (Wire1 is not supported) + delete sensor; + return; + } +#else + TwoWire *bus = &Wire; +#endif + if (sensor->initDevice(bus, &dev)) { + sensors.push_front(sensor); + return; + } + // destroy sensor + delete sensor; + } +} + +void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) +{ + if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) { + return; + } + LOG_INFO("Air Quality Telemetry adding I2C devices..."); + + // order by priority of metrics/values (low top, high bottom) + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR +// Sensors that require variable I2C clock speed +#ifdef VBLE_I2C_CLOCK_SPEED + addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); +#endif +#endif + +} int32_t AirQualityTelemetryModule::runOnce() { @@ -58,8 +103,11 @@ int32_t AirQualityTelemetryModule::runOnce() if (moduleConfig.telemetry.air_quality_enabled) { LOG_INFO("Air quality Telemetry: init"); - if (pmsa003iSensor.hasSensor()) - result = pmsa003iSensor.runOnce(); + // check if we have at least one sensor + if (!sensors.empty()) { + result = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + } // it's possible to have this module enabled, only for displaying values on the screen. @@ -72,10 +120,12 @@ int32_t AirQualityTelemetryModule::runOnce() } // Wake up the sensors that need it +// #ifdef VBLE_I2C_CLOCK_SPEED #ifdef PMSA003I_ENABLE_PIN if (pmsa003iSensor.hasSensor() && !pmsa003iSensor.isActive()) return pmsa003iSensor.wakeUp(); #endif /* PMSA003I_ENABLE_PIN */ +// #endif if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( @@ -93,9 +143,12 @@ int32_t AirQualityTelemetryModule::runOnce() lastSentToPhone = millis(); } +// Send to sleep sensors that consume power +// #ifdef VBLE_I2C_CLOCK_SPEED #ifdef PMSA003I_ENABLE_PIN pmsa003iSensor.sleep(); #endif /* PMSA003I_ENABLE_PIN */ +// #endif } return min(sendToPhoneIntervalMs, result); @@ -228,10 +281,11 @@ bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero; - if (pmsa003iSensor.hasSensor()) { - // TODO - Should we check for sensor state here? - // If a sensor is sleeping, we should know and check to wake it up - valid = valid && pmsa003iSensor.getMetrics(m); + // TODO - Should we check for sensor state here? + // If a sensor is sleeping, we should know and check to wake it up + for (TelemetrySensor *sensor : sensors) { + LOG_INFO("Reading AQ sensors"); + valid = valid && sensor->getMetrics(m); hasSensor = true; } @@ -322,18 +376,13 @@ AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule( meshtastic_AdminMessage *response) { AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL - if (pmsa003iSensor.hasSensor()) { - // TODO - Potentially implement an admin message to choose between pm_standard - // and pm_environmental. This could be configurable as it doesn't make sense so - // have both - result = pmsa003iSensor.handleAdminMessage(mp, request, response); + + for (TelemetrySensor *sensor : sensors) { + result = sensor->handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) return result; } - -#endif return result; } diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index b76ae589790..af9c4ebc000 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_AIR_QUALITY +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #pragma once @@ -11,10 +11,13 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "NodeDB.h" #include "ProtobufModule.h" +#include "detect/ScanI2CConsumer.h" #include #include -class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule +class AirQualityTelemetryModule : private concurrency::OSThread, + public ScanI2CConsumer, + public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, @@ -22,7 +25,7 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf public: AirQualityTelemetryModule() - : concurrency::OSThread("AirQualityTelemetry"), + : concurrency::OSThread("AirQualityTelemetry"), ScanI2CConsumer(), ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { lastMeasurementPacket = nullptr; @@ -55,6 +58,8 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; + void i2cScanFinished(ScanI2C *i2cScanner); + private: bool firstTime = true; meshtastic_MeshPacket *lastMeasurementPacket; diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 8dc4c7dcee1..25988380345 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_AIR_QUALITY +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && defined(VBLE_I2C_CLOCK_SPEED) #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "PMSA003ISensor.h" @@ -13,47 +13,29 @@ PMSA003ISensor::PMSA003ISensor() { } -void PMSA003ISensor::setup() +bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); #ifdef PMSA003I_ENABLE_PIN pinMode(PMSA003I_ENABLE_PIN, OUTPUT); #endif -} - -bool PMSA003ISensor::restoreClock(uint32_t currentClock){ -#ifdef PMSA003I_I2C_CLOCK_SPEED - if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ - // LOG_DEBUG("Restoring I2C clock to %uHz", currentClock); - return bus->setClock(currentClock); - } - return true; -#endif -} -int32_t PMSA003ISensor::runOnce() -{ - LOG_INFO("Init sensor: %s", sensorName); - - if (!hasSensor()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } - - bus = nodeTelemetrySensorsMap[sensorType].second; - address = (uint8_t)nodeTelemetrySensorsMap[sensorType].first; + _bus = bus; + _address = dev->address.address; #ifdef PMSA003I_I2C_CLOCK_SPEED uint32_t currentClock; - currentClock = bus->getClock(); + currentClock = _bus->getClock(); if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ // LOG_DEBUG("Changing I2C clock to %u", PMSA003I_I2C_CLOCK_SPEED); - bus->setClock(PMSA003I_I2C_CLOCK_SPEED); + _bus->setClock(PMSA003I_I2C_CLOCK_SPEED); } #endif - bus->beginTransmission(address); - if (bus->endTransmission() != 0) { + _bus->beginTransmission(_address); + if (_bus->endTransmission() != 0) { LOG_WARN("PMSA003I not found on I2C at 0x12"); - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + return false; } #ifdef PMSA003I_I2C_CLOCK_SPEED @@ -63,7 +45,18 @@ int32_t PMSA003ISensor::runOnce() status = 1; LOG_INFO("PMSA003I Enabled"); - return initI2CSensor(); + initI2CSensor(); + return true; +} + +bool PMSA003ISensor::restoreClock(uint32_t currentClock){ +#ifdef PMSA003I_I2C_CLOCK_SPEED + if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ + // LOG_DEBUG("Restoring I2C clock to %uHz", currentClock); + return _bus->setClock(currentClock); + } + return true; +#endif } bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) @@ -75,16 +68,16 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) #ifdef PMSA003I_I2C_CLOCK_SPEED uint32_t currentClock; - currentClock = bus->getClock(); + currentClock = _bus->getClock(); if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ // LOG_DEBUG("Changing I2C clock to %u", PMSA003I_I2C_CLOCK_SPEED); - bus->setClock(PMSA003I_I2C_CLOCK_SPEED); + _bus->setClock(PMSA003I_I2C_CLOCK_SPEED); } #endif - bus->requestFrom(address, PMSA003I_FRAME_LENGTH); - if (bus->available() < PMSA003I_FRAME_LENGTH) { - LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", bus->available()); + _bus->requestFrom(_address, PMSA003I_FRAME_LENGTH); + if (_bus->available() < PMSA003I_FRAME_LENGTH) { + LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", _bus->available()); return false; } @@ -93,7 +86,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) #endif for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { - buffer[i] = bus->read(); + buffer[i] = _bus->read(); } if (buffer[0] != 0x42 || buffer[1] != 0x4D) { @@ -170,6 +163,7 @@ void PMSA003ISensor::sleep() uint32_t PMSA003ISensor::wakeUp() { + LOG_INFO('Waking up PMSA003I') digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; return PMSA003I_WARMUP_MS; diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 35a4df735e1..aa926028e42 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -1,5 +1,8 @@ -#pragma once +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && defined(VBLE_I2C_CLOCK_SPEED) + +#include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #define PMSA003I_I2C_CLOCK_SPEED 100000 @@ -10,11 +13,10 @@ class PMSA003ISensor : public TelemetrySensor { public: PMSA003ISensor(); - virtual void setup() override; - virtual int32_t runOnce() override; virtual bool restoreClock(uint32_t currentClock); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool isActive(); + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; #ifdef PMSA003I_ENABLE_PIN void sleep(); @@ -24,11 +26,13 @@ class PMSA003ISensor : public TelemetrySensor private: enum class State { IDLE, ACTIVE }; State state = State::ACTIVE; - TwoWire * bus; - uint8_t address; + TwoWire * _bus; + uint8_t _address; uint16_t computedChecksum = 0; uint16_t receivedChecksum = 0; uint8_t buffer[PMSA003I_FRAME_LENGTH]; }; + +#endif \ No newline at end of file diff --git a/variants/esp32/heltec_wireless_bridge/platformio.ini b/variants/esp32/heltec_wireless_bridge/platformio.ini index 62be4b45ed4..6f9de7a8472 100644 --- a/variants/esp32/heltec_wireless_bridge/platformio.ini +++ b/variants/esp32/heltec_wireless_bridge/platformio.ini @@ -13,7 +13,7 @@ build_flags = -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D MESHTASTIC_EXCLUDE_DETECTIONSENSOR=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 - -D MESHTASTIC_EXCLUDE_AIR_QUALITY=1 + -D MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL=1 -D MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 -D MESHTASTIC_EXCLUDE_GPS=1 From 23f82c92a7df72ec60aba889d5f77a62a1a5953e Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sat, 10 Jan 2026 11:26:50 +0100 Subject: [PATCH 42/52] Move add sensor template to separate file --- src/modules/Telemetry/AirQualityTelemetry.cpp | 29 +------------------ .../Telemetry/EnvironmentTelemetry.cpp | 29 +------------------ .../Telemetry/Sensor/AddI2CSensorTemplate.h | 26 +++++++++++++++++ 3 files changed, 28 insertions(+), 56 deletions(-) create mode 100644 src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 5966d461392..2d2cfa0b67f 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -17,40 +17,13 @@ #include "main.h" #include "sleep.h" #include +#include "Sensor/AddI2CSensorTemplate.h" // Sensors #ifdef VBLE_I2C_CLOCK_SPEED #include "Sensor/PMSA003ISensor.h" #endif -#include - -static std::forward_list sensors; - -template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) -{ - ScanI2C::FoundDevice dev = i2cScanner->find(type); - if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { - TelemetrySensor *sensor = new T(); -#if WIRE_INTERFACES_COUNT > 1 - TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); - if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { - // This sensor only works on Wire (Wire1 is not supported) - delete sensor; - return; - } -#else - TwoWire *bus = &Wire; -#endif - if (sensor->initDevice(bus, &dev)) { - sensors.push_front(sensor); - return; - } - // destroy sensor - delete sensor; - } -} - void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) { diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 5ded83a29ee..c394c6bb118 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -143,34 +143,7 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #include "graphics/ScreenFonts.h" #include - -#include - -static std::forward_list sensors; - -template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) -{ - ScanI2C::FoundDevice dev = i2cScanner->find(type); - if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { - TelemetrySensor *sensor = new T(); -#if WIRE_INTERFACES_COUNT > 1 - TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); - if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { - // This sensor only works on Wire (Wire1 is not supported) - delete sensor; - return; - } -#else - TwoWire *bus = &Wire; -#endif - if (sensor->initDevice(bus, &dev)) { - sensors.push_front(sensor); - return; - } - // destroy sensor - delete sensor; - } -} +#include "Sensor/AddI2CSensorTemplate.h" void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { diff --git a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h new file mode 100644 index 00000000000..3e213954d1c --- /dev/null +++ b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h @@ -0,0 +1,26 @@ +#include +static std::forward_list sensors; + +template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) +{ + ScanI2C::FoundDevice dev = i2cScanner->find(type); + if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { + TelemetrySensor *sensor = new T(); +#if WIRE_INTERFACES_COUNT > 1 + TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); + if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { + // This sensor only works on Wire (Wire1 is not supported) + delete sensor; + return; + } +#else + TwoWire *bus = &Wire; +#endif + if (sensor->initDevice(bus, &dev)) { + sensors.push_front(sensor); + return; + } + // destroy sensor + delete sensor; + } +} \ No newline at end of file From 80f63b649823f6dfe430c8c60a4d3ccfed6c215a Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sat, 10 Jan 2026 11:28:22 +0100 Subject: [PATCH 43/52] Split telemetry on screen options --- src/graphics/draw/MenuHandler.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 586bdd4a66c..ef49a896e10 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -1541,7 +1541,8 @@ void menuHandler::FrameToggles_menu() lora, clock, show_favorites, - show_telemetry, + show_env_telemetry, + show_aq_telemetry, show_power, enumEnd }; @@ -1581,8 +1582,11 @@ void menuHandler::FrameToggles_menu() optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites"; optionsEnumArray[options++] = show_favorites; - optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Telemetry" : "Show Telemetry"; - optionsEnumArray[options++] = show_telemetry; + optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Env. Telemetry" : "Show Env. Telemetry"; + optionsEnumArray[options++] = show_env_telemetry; + + optionsArray[options] = moduleConfig.telemetry.air_quality_screen_enabled ? "Hide AQ Telemetry" : "Show AQ Telemetry"; + optionsEnumArray[options++] = show_aq_telemetry; optionsArray[options] = moduleConfig.telemetry.power_screen_enabled ? "Hide Power" : "Show Power"; optionsEnumArray[options++] = show_power; @@ -1641,10 +1645,14 @@ void menuHandler::FrameToggles_menu() screen->toggleFrameVisibility("show_favorites"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); - } else if (selected == show_telemetry) { + } else if (selected == show_env_telemetry) { moduleConfig.telemetry.environment_screen_enabled = !moduleConfig.telemetry.environment_screen_enabled; menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); + } else if (selected == show_aq_telemetry) { + moduleConfig.telemetry.air_quality_screen_enabled = !moduleConfig.telemetry.air_quality_screen_enabled; + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); } else if (selected == show_power) { moduleConfig.telemetry.power_screen_enabled = !moduleConfig.telemetry.power_screen_enabled; menuHandler::menuQueue = menuHandler::FrameToggles; From 998726e54e499e91b87a3a763f14e52d1e3fc792 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Mon, 12 Jan 2026 11:06:52 +0100 Subject: [PATCH 44/52] Add variable I2C clock compile flag * Added to Seeed Xiao S3 as demo --- variants/esp32s3/seeed_xiao_s3/variant.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/esp32s3/seeed_xiao_s3/variant.h b/variants/esp32s3/seeed_xiao_s3/variant.h index d8dcbc8d4f9..40060052614 100644 --- a/variants/esp32s3/seeed_xiao_s3/variant.h +++ b/variants/esp32s3/seeed_xiao_s3/variant.h @@ -88,3 +88,5 @@ L76K GPS Module Information : https://www.seeedstudio.com/L76K-GNSS-Module-for-S #define SX126X_TXEN RADIOLIB_NC #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #endif + +#define VBLE_I2C_CLOCK_SPEED From aa943a9e029f16f2801323881684ce3cd56b7d40 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Mon, 12 Jan 2026 12:07:33 +0100 Subject: [PATCH 45/52] Fix drawFrame in AQ module --- src/modules/Telemetry/AirQualityTelemetry.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 2d2cfa0b67f..4e648192099 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -132,6 +132,7 @@ bool AirQualityTelemetryModule::wantUIFrame() return moduleConfig.telemetry.air_quality_screen_enabled; } +#if HAS_SCREEN void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // === Setup display === @@ -141,7 +142,7 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta int line = 1; // === Set Title - const char *titleStr = (graphics::isHighResolution) ? "Environment" : "Env."; + const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Air Quality" : "AQ."; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); @@ -190,11 +191,11 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta std::vector entries; if (m.has_pm10_standard) - entries.push_back("PM1.0: " + String(m.pm10_standard) + "ug/m3"); + entries.push_back("PM1: " + String(m.pm10_standard) + "ug/m3"); if (m.has_pm25_standard) entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3"); if (m.has_pm100_standard) - entries.push_back("PM10.0: " + String(m.pm100_standard) + "ug/m3"); + entries.push_back("PM10: " + String(m.pm100_standard) + "ug/m3"); // === Show first available metric on top-right of first line === if (!entries.empty()) { @@ -220,7 +221,9 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta currentY += rowHeight; } + graphics::drawCommonFooter(display, x, y); } +#endif bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { From 7f35c38d848af1c5ff948b9e5481a0673c1a2f02 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Mon, 12 Jan 2026 12:08:39 +0100 Subject: [PATCH 46/52] Module settings override to i2cScan module function --- src/modules/Telemetry/AirQualityTelemetry.cpp | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 4e648192099..bfd90b69662 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -31,15 +31,24 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) } LOG_INFO("Air Quality Telemetry adding I2C devices..."); - // order by priority of metrics/values (low top, high bottom) + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + Note: this was previously on runOnce, which didnt take effect + as other modules already had already been initialized (screen) + */ + // moduleConfig.telemetry.air_quality_enabled = 1; + // moduleConfig.telemetry.air_quality_screen_enabled = 1; + // moduleConfig.telemetry.air_quality_interval = 15; + + // order by priority of metrics/values (low top, high bottom) #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR // Sensors that require variable I2C clock speed #ifdef VBLE_I2C_CLOCK_SPEED addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); #endif #endif - } int32_t AirQualityTelemetryModule::runOnce() @@ -54,15 +63,6 @@ int32_t AirQualityTelemetryModule::runOnce() 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.air_quality_enabled = 1; - // moduleConfig.telemetry.air_quality_screen_enabled = 1; - // moduleConfig.telemetry.air_quality_interval = 15; - if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled || AIR_QUALITY_TELEMETRY_MODULE_ENABLE)) { // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it From 25e431969e3fd34a6f6a7082a518a58cfd4e8d2e Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Mon, 12 Jan 2026 15:46:49 +0100 Subject: [PATCH 47/52] Move to CAN_RECLOCK_I2C per architecture * Add reclock function in TelemetrySensor.cpp * Add flag in ESP32 common --- src/modules/Telemetry/AirQualityTelemetry.cpp | 9 +--- .../Telemetry/Sensor/PMSA003ISensor.cpp | 43 ++++++------------- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 5 +-- .../Telemetry/Sensor/TelemetrySensor.cpp | 38 +++++++++++++++- .../Telemetry/Sensor/TelemetrySensor.h | 5 +++ variants/esp32/esp32-common.ini | 1 + variants/esp32s3/seeed_xiao_s3/variant.h | 2 - 7 files changed, 59 insertions(+), 44 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index bfd90b69662..58b66e1c347 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -20,9 +20,8 @@ #include "Sensor/AddI2CSensorTemplate.h" // Sensors -#ifdef VBLE_I2C_CLOCK_SPEED #include "Sensor/PMSA003ISensor.h" -#endif + void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { @@ -45,10 +44,8 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) // order by priority of metrics/values (low top, high bottom) #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR // Sensors that require variable I2C clock speed -#ifdef VBLE_I2C_CLOCK_SPEED addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); #endif -#endif } int32_t AirQualityTelemetryModule::runOnce() @@ -93,12 +90,10 @@ int32_t AirQualityTelemetryModule::runOnce() } // Wake up the sensors that need it -// #ifdef VBLE_I2C_CLOCK_SPEED #ifdef PMSA003I_ENABLE_PIN if (pmsa003iSensor.hasSensor() && !pmsa003iSensor.isActive()) return pmsa003iSensor.wakeUp(); #endif /* PMSA003I_ENABLE_PIN */ -// #endif if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( @@ -117,11 +112,9 @@ int32_t AirQualityTelemetryModule::runOnce() } // Send to sleep sensors that consume power -// #ifdef VBLE_I2C_CLOCK_SPEED #ifdef PMSA003I_ENABLE_PIN pmsa003iSensor.sleep(); #endif /* PMSA003I_ENABLE_PIN */ -// #endif } return min(sendToPhoneIntervalMs, result); diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 25988380345..37b88090ca3 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && defined(VBLE_I2C_CLOCK_SPEED) +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "PMSA003ISensor.h" @@ -23,12 +23,11 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) _bus = bus; _address = dev->address.address; -#ifdef PMSA003I_I2C_CLOCK_SPEED - uint32_t currentClock; - currentClock = _bus->getClock(); - if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ - // LOG_DEBUG("Changing I2C clock to %u", PMSA003I_I2C_CLOCK_SPEED); - _bus->setClock(PMSA003I_I2C_CLOCK_SPEED); +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + uint32_t currentClock = setClock(PMSA003I_I2C_CLOCK_SPEED); + if (!currentClock){ + LOG_WARN("PMSA003I can't be used at this clock speed"); + return false; } #endif @@ -38,8 +37,8 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) return false; } -#ifdef PMSA003I_I2C_CLOCK_SPEED - restoreClock(currentClock); +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + setClock(currentClock); #endif status = 1; @@ -49,16 +48,6 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) return true; } -bool PMSA003ISensor::restoreClock(uint32_t currentClock){ -#ifdef PMSA003I_I2C_CLOCK_SPEED - if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ - // LOG_DEBUG("Restoring I2C clock to %uHz", currentClock); - return _bus->setClock(currentClock); - } - return true; -#endif -} - bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) { if(!isActive()){ @@ -66,13 +55,8 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) return false; } -#ifdef PMSA003I_I2C_CLOCK_SPEED - uint32_t currentClock; - currentClock = _bus->getClock(); - if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ - // LOG_DEBUG("Changing I2C clock to %u", PMSA003I_I2C_CLOCK_SPEED); - _bus->setClock(PMSA003I_I2C_CLOCK_SPEED); - } +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + uint32_t currentClock = setClock(PMSA003I_I2C_CLOCK_SPEED); #endif _bus->requestFrom(_address, PMSA003I_FRAME_LENGTH); @@ -81,8 +65,8 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) return false; } -#ifdef PMSA003I_I2C_CLOCK_SPEED - restoreClock(currentClock); +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + setClock(currentClock); #endif for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { @@ -119,6 +103,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) measurement->variant.air_quality_metrics.has_pm100_standard = true; measurement->variant.air_quality_metrics.pm100_standard = read16(buffer, 8); + // TODO - Add admin command to remove environmental metrics to save protobuf space measurement->variant.air_quality_metrics.has_pm10_environmental = true; measurement->variant.air_quality_metrics.pm10_environmental = read16(buffer, 10); @@ -128,6 +113,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) measurement->variant.air_quality_metrics.has_pm100_environmental = true; measurement->variant.air_quality_metrics.pm100_environmental = read16(buffer, 14); + // TODO - Add admin command to remove PN to save protobuf space measurement->variant.air_quality_metrics.has_particles_03um = true; measurement->variant.air_quality_metrics.particles_03um = read16(buffer, 16); @@ -169,5 +155,4 @@ uint32_t PMSA003ISensor::wakeUp() return PMSA003I_WARMUP_MS; } #endif - #endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index aa926028e42..64a47ccaf3c 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && defined(VBLE_I2C_CLOCK_SPEED) +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" @@ -13,7 +13,6 @@ class PMSA003ISensor : public TelemetrySensor { public: PMSA003ISensor(); - virtual bool restoreClock(uint32_t currentClock); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool isActive(); virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; @@ -26,8 +25,6 @@ class PMSA003ISensor : public TelemetrySensor private: enum class State { IDLE, ACTIVE }; State state = State::ACTIVE; - TwoWire * _bus; - uint8_t _address; uint16_t computedChecksum = 0; uint16_t receivedChecksum = 0; diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.cpp b/src/modules/Telemetry/Sensor/TelemetrySensor.cpp index d6e7d1fac9d..c91f63a4c4c 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.cpp +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.cpp @@ -1,10 +1,46 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "NodeDB.h" #include "TelemetrySensor.h" #include "main.h" +#ifdef CAN_RECLOCK_I2C + +uint32_t TelemetrySensor::setClock(uint32_t desiredClock) { + + uint32_t currentClock; + + // See https://github.com/arduino/Arduino/issues/11457 + // Currently, only ESP32 can getClock + // While all cores can setClock() + // https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50 + // https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60 + // https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103 + +#ifdef ARCH_ESP32 + currentClock = _bus->getClock(); +#elif defined(ARCH_NRF52) + // TODO add getClock function or return a predefined clock speed per variant + return 0; +#elif defined(ARCH_RP2040) + // TODO add getClock function or return a predefined clock speed per variant + return 0; +#elif defined(ARCH_STM32WL) + // TODO add getClock function or return a predefined clock speed per variant + return 0; +#else + return 0; +#endif + + if (currentClock != desiredClock){ + LOG_DEBUG("Changing I2C clock to %u", desiredClock); + _bus->setClock(desiredClock); + } + return currentClock; +} +#endif + #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index 3c3e6180890..268a0c8e1ff 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -30,6 +30,8 @@ class TelemetrySensor meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET; unsigned status; bool initialized = false; + TwoWire * _bus; + uint8_t _address; int32_t initI2CSensor() { @@ -69,6 +71,9 @@ class TelemetrySensor virtual bool getMetrics(meshtastic_Telemetry *measurement) = 0; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { return false; }; +#ifdef CAN_RECLOCK_I2C + virtual uint32_t setClock(uint32_t desiredClock); +#endif }; #endif \ No newline at end of file diff --git a/variants/esp32/esp32-common.ini b/variants/esp32/esp32-common.ini index e582b68803c..1710f9b0175 100644 --- a/variants/esp32/esp32-common.ini +++ b/variants/esp32/esp32-common.ini @@ -49,6 +49,7 @@ build_flags = -DLIBPAX_BLE -DHAS_UDP_MULTICAST=1 ;-DDEBUG_HEAP + -DCAN_RECLOCK_I2C lib_deps = ${arduino_base.lib_deps} diff --git a/variants/esp32s3/seeed_xiao_s3/variant.h b/variants/esp32s3/seeed_xiao_s3/variant.h index 40060052614..d8dcbc8d4f9 100644 --- a/variants/esp32s3/seeed_xiao_s3/variant.h +++ b/variants/esp32s3/seeed_xiao_s3/variant.h @@ -88,5 +88,3 @@ L76K GPS Module Information : https://www.seeedstudio.com/L76K-GNSS-Module-for-S #define SX126X_TXEN RADIOLIB_NC #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #endif - -#define VBLE_I2C_CLOCK_SPEED From 0d420ebc26f526b2e06e8f77abd8b8e42e4c429f Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Mon, 12 Jan 2026 17:42:12 +0100 Subject: [PATCH 48/52] Minor fix --- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 37b88090ca3..5dee5ddab4c 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -149,7 +149,7 @@ void PMSA003ISensor::sleep() uint32_t PMSA003ISensor::wakeUp() { - LOG_INFO('Waking up PMSA003I') + LOG_INFO('Waking up PMSA003I'); digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; return PMSA003I_WARMUP_MS; From a134f413e50934e69ae3bacd0d3533ef80c9e5eb Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 14 Jan 2026 09:14:41 +0100 Subject: [PATCH 49/52] Move I2C reclock function to src/detect --- src/detect/reClockI2C.h | 40 +++++++++++++++++++ .../Telemetry/Sensor/PMSA003ISensor.cpp | 9 +++-- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 2 + .../Telemetry/Sensor/TelemetrySensor.cpp | 36 ----------------- .../Telemetry/Sensor/TelemetrySensor.h | 5 --- 5 files changed, 47 insertions(+), 45 deletions(-) create mode 100644 src/detect/reClockI2C.h diff --git a/src/detect/reClockI2C.h b/src/detect/reClockI2C.h new file mode 100644 index 00000000000..edcd0afb601 --- /dev/null +++ b/src/detect/reClockI2C.h @@ -0,0 +1,40 @@ +#ifdef CAN_RECLOCK_I2C +#include "ScanI2CTwoWire.h" + +uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus) { + + uint32_t currentClock; + + /* See https://github.com/arduino/Arduino/issues/11457 + Currently, only ESP32 can getClock() + While all cores can setClock() + https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50 + https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60 + https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103 + For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes) + we need to reclock I2C and set it back to the previous desired speed. + Only for cases where we can know OR predefine the speed, we can do this. + */ + +#ifdef ARCH_ESP32 + currentClock = i2cBus->getClock(); +#elif defined(ARCH_NRF52) + // TODO add getClock function or return a predefined clock speed per variant? + return 0; +#elif defined(ARCH_RP2040) + // TODO add getClock function or return a predefined clock speed per variant + return 0; +#elif defined(ARCH_STM32WL) + // TODO add getClock function or return a predefined clock speed per variant + return 0; +#else + return 0; +#endif + + if (currentClock != desiredClock){ + LOG_DEBUG("Changing I2C clock to %u", desiredClock); + i2cBus->setClock(desiredClock); + } + return currentClock; +} +#endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 5dee5ddab4c..231ba4eb76f 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -5,6 +5,7 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "PMSA003ISensor.h" #include "TelemetrySensor.h" +#include "../detect/reClockI2C.h" #include @@ -24,7 +25,7 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) _address = dev->address.address; #if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - uint32_t currentClock = setClock(PMSA003I_I2C_CLOCK_SPEED); + uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus); if (!currentClock){ LOG_WARN("PMSA003I can't be used at this clock speed"); return false; @@ -38,7 +39,7 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) } #if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - setClock(currentClock); + reClockI2C(currentClock, _bus); #endif status = 1; @@ -56,7 +57,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) } #if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - uint32_t currentClock = setClock(PMSA003I_I2C_CLOCK_SPEED); + uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus); #endif _bus->requestFrom(_address, PMSA003I_FRAME_LENGTH); @@ -66,7 +67,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) } #if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - setClock(currentClock); + reClockI2C(currentClock, _bus); #endif for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 64a47ccaf3c..fa1518a51ac 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -30,6 +30,8 @@ class PMSA003ISensor : public TelemetrySensor uint16_t receivedChecksum = 0; uint8_t buffer[PMSA003I_FRAME_LENGTH]; + TwoWire * _bus; + uint8_t _address; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.cpp b/src/modules/Telemetry/Sensor/TelemetrySensor.cpp index c91f63a4c4c..f854cb5feec 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.cpp +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.cpp @@ -7,40 +7,4 @@ #include "TelemetrySensor.h" #include "main.h" -#ifdef CAN_RECLOCK_I2C - -uint32_t TelemetrySensor::setClock(uint32_t desiredClock) { - - uint32_t currentClock; - - // See https://github.com/arduino/Arduino/issues/11457 - // Currently, only ESP32 can getClock - // While all cores can setClock() - // https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50 - // https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60 - // https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103 - -#ifdef ARCH_ESP32 - currentClock = _bus->getClock(); -#elif defined(ARCH_NRF52) - // TODO add getClock function or return a predefined clock speed per variant - return 0; -#elif defined(ARCH_RP2040) - // TODO add getClock function or return a predefined clock speed per variant - return 0; -#elif defined(ARCH_STM32WL) - // TODO add getClock function or return a predefined clock speed per variant - return 0; -#else - return 0; -#endif - - if (currentClock != desiredClock){ - LOG_DEBUG("Changing I2C clock to %u", desiredClock); - _bus->setClock(desiredClock); - } - return currentClock; -} -#endif - #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index 268a0c8e1ff..3c3e6180890 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -30,8 +30,6 @@ class TelemetrySensor meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET; unsigned status; bool initialized = false; - TwoWire * _bus; - uint8_t _address; int32_t initI2CSensor() { @@ -71,9 +69,6 @@ class TelemetrySensor virtual bool getMetrics(meshtastic_Telemetry *measurement) = 0; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { return false; }; -#ifdef CAN_RECLOCK_I2C - virtual uint32_t setClock(uint32_t desiredClock); -#endif }; #endif \ No newline at end of file From 886d5428e33775acff19ae33f0854d27c3874d88 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 14 Jan 2026 09:34:04 +0100 Subject: [PATCH 50/52] Fix uninitMemberVar errors and compile issue --- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 2 +- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 231ba4eb76f..1ad9e87244f 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -150,7 +150,7 @@ void PMSA003ISensor::sleep() uint32_t PMSA003ISensor::wakeUp() { - LOG_INFO('Waking up PMSA003I'); + LOG_INFO("Waking up PMSA003I"); digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; return PMSA003I_WARMUP_MS; diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index fa1518a51ac..05431b9d3d6 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -29,9 +29,9 @@ class PMSA003ISensor : public TelemetrySensor uint16_t computedChecksum = 0; uint16_t receivedChecksum = 0; - uint8_t buffer[PMSA003I_FRAME_LENGTH]; - TwoWire * _bus; - uint8_t _address; + uint8_t buffer[PMSA003I_FRAME_LENGTH]{}; + TwoWire * _bus{}; + uint8_t _address{}; }; #endif \ No newline at end of file From 068009ece6336b1956e5177398902d7fa5361eda Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 14 Jan 2026 14:10:13 +0100 Subject: [PATCH 51/52] Make sleep, wakeUp functions generic --- src/modules/Telemetry/AirQualityTelemetry.cpp | 22 +++++++++---------- .../Telemetry/Sensor/PMSA003ISensor.cpp | 9 ++++++-- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 8 +++---- .../Telemetry/Sensor/TelemetrySensor.h | 6 +++++ 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 58b66e1c347..dff23abf1b3 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -42,10 +42,7 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) // moduleConfig.telemetry.air_quality_interval = 15; // order by priority of metrics/values (low top, high bottom) -#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR -// Sensors that require variable I2C clock speed addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); -#endif } int32_t AirQualityTelemetryModule::runOnce() @@ -90,10 +87,12 @@ int32_t AirQualityTelemetryModule::runOnce() } // Wake up the sensors that need it -#ifdef PMSA003I_ENABLE_PIN - if (pmsa003iSensor.hasSensor() && !pmsa003iSensor.isActive()) - return pmsa003iSensor.wakeUp(); -#endif /* PMSA003I_ENABLE_PIN */ + LOG_INFO("Waking up sensors"); + for (TelemetrySensor *sensor : sensors) { + if (!sensor->isActive()) { + return sensor->wakeUp(); + } + } if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( @@ -111,10 +110,11 @@ int32_t AirQualityTelemetryModule::runOnce() lastSentToPhone = millis(); } -// Send to sleep sensors that consume power -#ifdef PMSA003I_ENABLE_PIN - pmsa003iSensor.sleep(); -#endif /* PMSA003I_ENABLE_PIN */ + // Send to sleep sensors that consume power + LOG_INFO("Sending sensors to sleep"); + for (TelemetrySensor *sensor : sensors) { + sensor->sleep(); + } } return min(sendToPhoneIntervalMs, result); diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 1ad9e87244f..467659efe6c 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -141,19 +141,24 @@ bool PMSA003ISensor::isActive() return state == State::ACTIVE; } -#ifdef PMSA003I_ENABLE_PIN + void PMSA003ISensor::sleep() { +#ifdef PMSA003I_ENABLE_PIN digitalWrite(PMSA003I_ENABLE_PIN, LOW); state = State::IDLE; +#endif } uint32_t PMSA003ISensor::wakeUp() { +#ifdef PMSA003I_ENABLE_PIN LOG_INFO("Waking up PMSA003I"); digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; return PMSA003I_WARMUP_MS; -} #endif + // No need to wait for warmup if already active + return 0; +} #endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 05431b9d3d6..47c8a05cc5d 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -14,13 +14,11 @@ class PMSA003ISensor : public TelemetrySensor public: PMSA003ISensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool isActive(); virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; -#ifdef PMSA003I_ENABLE_PIN - void sleep(); - uint32_t wakeUp(); -#endif + virtual bool isActive() override; + virtual void sleep() override; + virtual uint32_t wakeUp() override; private: enum class State { IDLE, ACTIVE }; diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index 3c3e6180890..4a325aeed79 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -58,6 +58,11 @@ class TelemetrySensor // TODO: delete after migration bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } + // Functions to sleep / wakeup sensors that support it + virtual void sleep() {}; + virtual uint32_t wakeUp() { return 0; } + // Return active by default, override per sensor + virtual bool isActive() { return true; } #if WIRE_INTERFACES_COUNT > 1 // Set to true if Implementation only works first I2C port (Wire) @@ -65,6 +70,7 @@ class TelemetrySensor #endif virtual int32_t runOnce() { return INT32_MAX; } virtual bool isInitialized() { return initialized; } + // TODO: is this used? virtual bool isRunning() { return status > 0; } virtual bool getMetrics(meshtastic_Telemetry *measurement) = 0; From d641206164359b7fd9bb690d12a58718d0c7d81b Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 14 Jan 2026 18:39:05 +0100 Subject: [PATCH 52/52] Fix STM32 builds * Add exclude AQ sensor to builds that have environmental sensor excludes * Add includes to AddI2CSensorTemplate.h --- src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h | 10 +++++++++- variants/stm32/CDEBYTE_E77-MBL/platformio.ini | 1 + variants/stm32/rak3172/platformio.ini | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h index 3e213954d1c..01aacc6741b 100644 --- a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h +++ b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h @@ -1,4 +1,11 @@ +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR + #include +#include "TelemetrySensor.h" +#include "detect/ScanI2C.h" +#include "detect/ScanI2CTwoWire.h" +#include + static std::forward_list sensors; template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) @@ -23,4 +30,5 @@ template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType ty // destroy sensor delete sensor; } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/variants/stm32/CDEBYTE_E77-MBL/platformio.ini b/variants/stm32/CDEBYTE_E77-MBL/platformio.ini index c5af9a4a4de..b4c0c958ff4 100644 --- a/variants/stm32/CDEBYTE_E77-MBL/platformio.ini +++ b/variants/stm32/CDEBYTE_E77-MBL/platformio.ini @@ -13,6 +13,7 @@ build_flags = -DPIN_SERIAL1_RX=PB7 -DPIN_SERIAL1_TX=PB6 -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 -DMESHTASTIC_EXCLUDE_I2C=1 -DMESHTASTIC_EXCLUDE_GPS=1 diff --git a/variants/stm32/rak3172/platformio.ini b/variants/stm32/rak3172/platformio.ini index b9a4b8a0440..4d96e98f93f 100644 --- a/variants/stm32/rak3172/platformio.ini +++ b/variants/stm32/rak3172/platformio.ini @@ -12,6 +12,7 @@ build_flags = -DPIN_WIRE_SDA=PA11 -DPIN_WIRE_SCL=PA12 -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 -DMESHTASTIC_EXCLUDE_I2C=1 -DMESHTASTIC_EXCLUDE_GPS=1