#include "configuration.h" #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "Default.h" #include "EnvironmentTelemetry.h" #include "MeshService.h" #include "NodeDB.h" #include "Power.h" #include "PowerFSM.h" #include "Router.h" #include "TransmitHistory.h" #include "UnitConversions.h" #include "buzz.h" #include "gps/RTC.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" #include "main.h" #include "modules/ExternalNotificationModule.h" #include "sleep.h" #include "target_specific.h" #include #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL // Sensors #include "Sensor/CGRadSensSensor.h" #include "Sensor/RCWL9620Sensor.h" #include "Sensor/nullSensor.h" namespace graphics { extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date); } #if __has_include() #include "Sensor/AHT10.h" #endif #if __has_include() #include "Sensor/BME280Sensor.h" #endif #if __has_include() #include "Sensor/BMP085Sensor.h" #endif #if __has_include() #include "Sensor/BMP280Sensor.h" #endif #if __has_include() #include "Sensor/LTR390UVSensor.h" #endif #if __has_include() || __has_include() #include "Sensor/BME680Sensor.h" #endif #if __has_include() #include "Sensor/DPS310Sensor.h" #endif #if __has_include() #include "Sensor/MCP9808Sensor.h" #endif #if __has_include() #include "Sensor/LPS22HBSensor.h" #endif #if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 #include "Sensor/RAK12035Sensor.h" #endif #if __has_include() #include "Sensor/VEML7700Sensor.h" #endif #if __has_include() #include "Sensor/TSL2591Sensor.h" #endif #if __has_include() #include "Sensor/OPT3001Sensor.h" #endif #if __has_include() #include "Sensor/SHTXXSensor.h" #endif #if __has_include() #include "Sensor/MLX90632Sensor.h" #endif #if __has_include() #include "Sensor/DFRobotLarkSensor.h" #endif #if __has_include() #include "Sensor/DFRobotGravitySensor.h" #endif #if __has_include() #include "Sensor/NAU7802Sensor.h" #endif #if __has_include() #include "Sensor/BMP3XXSensor.h" #endif #if __has_include() #include "Sensor/PCT2075Sensor.h" #endif #endif #ifdef T1000X_SENSOR_EN #include "Sensor/T1000xSensor.h" #endif #if __has_include() #include "Sensor/SPA06Sensor.h" #endif #if __has_include() #include "Sensor/TSL2561Sensor.h" #endif #if __has_include() #include "Sensor/BH1750Sensor.h" #endif #if __has_include() #include "Sensor/ADS1X15Sensor.h" #endif #if __has_include() #include "Sensor/DS248XSensor.h" #endif #define FAILED_STATE_SENSOR_READ_MULTIPLIER 10 #define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true #include "Sensor/AddI2CSensorTemplate.h" #include "graphics/ScreenFonts.h" #include EnvironmentTelemetryModule *environmentTelemetryModule = nullptr; namespace { EnvironmentTelemetryModule::DisplaySource gDisplaySource = EnvironmentTelemetryModule::DisplaySource::Mesh; } // namespace static constexpr uint16_t TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY = 0x8002; static constexpr uint32_t LOCAL_DISPLAY_REFRESH_INTERVAL_MS = 1000; EnvironmentTelemetryModule::DisplaySource EnvironmentTelemetryModule::getDisplaySource() { return gDisplaySource; } void EnvironmentTelemetryModule::setDisplaySource(DisplaySource source) { gDisplaySource = source; if (environmentTelemetryModule != nullptr) { environmentTelemetryModule->lastLocalDisplayRefreshMs = 0; environmentTelemetryModule->refreshDisplayedMeasurement(); } } void EnvironmentTelemetryModule::clearMeasurementPacket() { if (lastMeasurementPacket != nullptr) { packetPool.release(lastMeasurementPacket); lastMeasurementPacket = nullptr; } } bool EnvironmentTelemetryModule::shouldDisplayRemoteNode(NodeNum nodeNum) const { if (nodeNum == 0 || nodeNum == nodeDB->getNodeNum()) { return false; } switch (getDisplaySource()) { case DisplaySource::LocalSensor: return false; case DisplaySource::Mesh: return true; case DisplaySource::FavoriteNodesOnly: return nodeDB->isFavorite(nodeNum); } return false; } bool EnvironmentTelemetryModule::shouldKeepCurrentRemoteDisplay() const { if (lastMeasurementPacket == nullptr) { return false; } return shouldDisplayRemoteNode(getFrom(lastMeasurementPacket)); } bool EnvironmentTelemetryModule::shouldDisplayLocalMeasurement() const { switch (getDisplaySource()) { case DisplaySource::LocalSensor: return true; case DisplaySource::Mesh: return !shouldKeepCurrentRemoteDisplay(); case DisplaySource::FavoriteNodesOnly: return false; } return false; } bool EnvironmentTelemetryModule::refreshLocalMeasurementPacket() { meshtastic_Telemetry local = meshtastic_Telemetry_init_zero; if (!getEnvironmentTelemetry(&local)) { return false; } meshtastic_MeshPacket *localPacket = allocDataProtobuf(local); if (localPacket == nullptr) { return false; } clearMeasurementPacket(); lastMeasurementPacket = packetPool.allocCopy(*localPacket); packetPool.release(localPacket); return lastMeasurementPacket != nullptr; } void EnvironmentTelemetryModule::refreshDisplayedMeasurement() { if (shouldDisplayLocalMeasurement()) { if (lastMeasurementPacket != nullptr && Throttle::isWithinTimespanMs(lastLocalDisplayRefreshMs, LOCAL_DISPLAY_REFRESH_INTERVAL_MS)) { return; } lastLocalDisplayRefreshMs = millis(); if (!refreshLocalMeasurementPacket()) { clearMeasurementPacket(); } return; } if (!shouldKeepCurrentRemoteDisplay()) { clearMeasurementPacket(); } } void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { return; } LOG_INFO("Environment Telemetry adding I2C devices"); /* Uncomment the preferences below if you want to use the module without having to configure it from the PythonAPI or WebUI. */ // moduleConfig.telemetry.environment_measurement_enabled = 1; // moduleConfig.telemetry.environment_screen_enabled = 1; // moduleConfig.telemetry.environment_update_interval = 15; // order by priority of metrics/values (low top, high bottom) #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #ifdef T1000X_SENSOR_EN // Not a real I2C device addSensor(i2cScanner, ScanI2C::DeviceType::NONE); #else #if HAS_SPA06 && __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::SPA06); #endif #endif #endif #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL addSensor(i2cScanner, ScanI2C::DeviceType::RCWL9620); addSensor(i2cScanner, ScanI2C::DeviceType::CGRADSENS); #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::DFROBOT_LARK); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::DFROBOT_RAIN); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::AHT10); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BMP_085); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BME_280); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::LTR390UV); #endif #if __has_include() || __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BME_680); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BMP_280); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::DPS310); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::MCP9808); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::LPS22HB); #endif #if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 addSensor(i2cScanner, ScanI2C::DeviceType::RAK12035); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::VEML7700); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::TSL2591); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::OPT3001); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::MLX90632); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BMP_3XX); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::PCT2075); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::TSL2561); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::NAU7802); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BH1750); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::ADS1X15); addSensor(i2cScanner, ScanI2C::DeviceType::ADS1X15_ALT); #endif #if __has_include() // TODO Can we scan for multiple sensors connected on the same bus? addSensor(i2cScanner, ScanI2C::DeviceType::SHTXX); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::DS248X); #endif #endif } int32_t EnvironmentTelemetryModule::runOnce() { if (sleepOnNextExecution == true) { if (shouldDeferDeepSleep()) return PREFLIGHT_SLEEP_RETRY_MS; sleepOnNextExecution = false; uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, default_telemetry_broadcast_interval_secs); LOG_DEBUG("Sleep %ims until next send", nightyNightMs); doDeepSleep(nightyNightMs, true, false); } uint32_t result = UINT32_MAX; if (!(moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled || ENVIRONMENTAL_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(); } if (firstTime) { // This is the first time the OSThread library has called this function, so do some setup firstTime = 0; if (moduleConfig.telemetry.environment_measurement_enabled || ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { LOG_INFO("Environment Telemetry: init"); // check if we have at least one sensor if (!sensors.empty()) { result = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } #ifdef T1000X_SENSOR_EN #elif !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL if (ina219Sensor.hasSensor()) result = ina219Sensor.runOnce(); if (ina260Sensor.hasSensor()) result = ina260Sensor.runOnce(); if (ina3221Sensor.hasSensor()) result = ina3221Sensor.runOnce(); if (max17048Sensor.hasSensor()) result = max17048Sensor.runOnce(); // this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the // sensormap here. #ifdef HAS_RAKPROT if (rak9154Sensor.hasSensor()) result = rak9154Sensor.runOnce(); #endif #endif refreshDisplayedMeasurement(); } // 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.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { return disable(); } for (TelemetrySensor *sensor : sensors) { uint32_t delay = sensor->runOnce(); if (delay < result) { result = delay; } } refreshDisplayedMeasurement(); uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY) : 0; if (((lastTelemetry == 0) || !Throttle::isWithinTimespanMs( lastTelemetry, Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.environment_update_interval, default_telemetry_broadcast_interval_secs, numOnlineNodes, TrafficType::TELEMETRY))) && airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && airTime->isTxAllowedAirUtil()) { sendTelemetry(); if (transmitHistory) transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY); } 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(); } } if (sleepOnNextExecution) { // Honor the pre-sleep grace period armed in sendTelemetry(): OSThread reschedules with // this return value, which would otherwise override setIntervalFromNow() with the sensor // polling interval (35 ms for BSEC2) and trigger deep sleep while the TX is still on air return FIVE_SECONDS_MS; } return min(sendToPhoneIntervalMs, result); } bool EnvironmentTelemetryModule::wantUIFrame() { return moduleConfig.telemetry.environment_screen_enabled; } #if HAS_SCREEN void EnvironmentTelemetryModule::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::currentResolution == graphics::ScreenResolution::High) ? "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; } // Bound the float metrics before String(float) renders them (see UnitConversions::displaySafeFloat); // the stored packet is always the environment variant. { auto &e = telemetry.variant.environment_metrics; e.temperature = UnitConversions::displaySafeFloat(e.temperature); e.relative_humidity = UnitConversions::displaySafeFloat(e.relative_humidity); e.barometric_pressure = UnitConversions::displaySafeFloat(e.barometric_pressure); e.voltage = UnitConversions::displaySafeFloat(e.voltage); e.current = UnitConversions::displaySafeFloat(e.current); e.lux = UnitConversions::displaySafeFloat(e.lux); e.white_lux = UnitConversions::displaySafeFloat(e.white_lux); e.weight = UnitConversions::displaySafeFloat(e.weight); e.distance = UnitConversions::displaySafeFloat(e.distance); e.radiation = UnitConversions::displaySafeFloat(e.radiation); } const auto &m = telemetry.variant.environment_metrics; // Check if any telemetry field has valid data bool hasAny = m.has_temperature || m.has_relative_humidity || m.barometric_pressure != 0 || m.iaq != 0 || m.voltage != 0 || m.current != 0 || m.lux != 0 || m.white_lux != 0 || m.weight != 0 || m.distance != 0 || m.radiation != 0; if (!hasAny) { display->drawString(x, currentY, "No Telemetry"); return; } // === First line: Show sender name + time since received (left), and first metric (right) === const bool isLocalTelemetry = (getFrom(lastMeasurementPacket) == nodeDB->getNodeNum()); const char *sender = isLocalTelemetry ? "Local Sensor" : getSenderShortName(*lastMeasurementPacket); String leftStr = String(sender); if (!isLocalTelemetry) { 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"; leftStr += " (" + 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_temperature) { String tempStr = moduleConfig.telemetry.environment_display_fahrenheit ? "Tmp: " + String(UnitConversions::CelsiusToFahrenheit(m.temperature), 1) + "°F" : "Tmp: " + String(m.temperature, 1) + "°C"; entries.push_back(tempStr); } if (m.has_relative_humidity) entries.push_back("Hum: " + String(m.relative_humidity, 0) + "%"); if (m.barometric_pressure != 0) entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa"); if (m.iaq != 0) { String aqi = "IAQ: " + String(m.iaq); const char *bannerMsg = nullptr; // Default: no banner if (m.iaq <= 25) aqi += " (Excellent)"; else if (m.iaq <= 50) aqi += " (Good)"; else if (m.iaq <= 100) aqi += " (Moderate)"; else if (m.iaq <= 150) aqi += " (Poor)"; else if (m.iaq <= 200) { aqi += " (Unhealthy)"; bannerMsg = "Unhealthy IAQ"; } else if (m.iaq <= 300) { aqi += " (Very Unhealthy)"; bannerMsg = "Very Unhealthy IAQ"; } else { aqi += " (Hazardous)"; bannerMsg = "Hazardous IAQ"; } entries.push_back(aqi); // === IAQ alert logic === static uint32_t lastAlertTime = 0; uint32_t now = millis(); bool isOwnTelemetry = isLocalTelemetry; bool isCooldownOver = (now - lastAlertTime > 60000); if (isOwnTelemetry && bannerMsg && isCooldownOver) { LOG_INFO("drawFrame: IAQ %d (own) - showing banner: %s", m.iaq, bannerMsg); screen->showSimpleBanner(bannerMsg, 3000); // Only buzz if IAQ is over 200 if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) { playLongBeep(); } lastAlertTime = now; } } if (m.voltage != 0 || m.current != 0) entries.push_back(String(m.voltage, 1) + "V / " + String(m.current, 0) + "mA"); if (m.lux != 0) entries.push_back("Light: " + String(m.lux, 0) + "lx"); if (m.white_lux != 0) entries.push_back("White: " + String(m.white_lux, 0) + "lx"); if (m.weight != 0) entries.push_back("Weight: " + String(m.weight, 0) + "kg"); if (m.distance != 0) entries.push_back("Level: " + String(m.distance, 0) + "mm"); if (m.radiation != 0) entries.push_back("Rad: " + String(m.radiation, 2) + " µR/h"); // === 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; } graphics::drawCommonFooter(display, x, y); } #endif bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { if (t->which_variant == meshtastic_Telemetry_environment_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) const char *sender = getSenderShortName(mp); LOG_INFO("(Received from %s): barometric_pressure=%f, current=%f, gas_resistance=%f, relative_humidity=%f, " "temperature=%f", sender, t->variant.environment_metrics.barometric_pressure, t->variant.environment_metrics.current, t->variant.environment_metrics.gas_resistance, t->variant.environment_metrics.relative_humidity, t->variant.environment_metrics.temperature); LOG_INFO("(Received from %s): voltage=%f, IAQ=%d, distance=%f, lux=%f, white_lux=%f", sender, t->variant.environment_metrics.voltage, t->variant.environment_metrics.iaq, t->variant.environment_metrics.distance, t->variant.environment_metrics.lux, t->variant.environment_metrics.white_lux); LOG_INFO("(Received from %s): wind speed=%fm/s, direction=%d degrees, weight=%fkg", sender, t->variant.environment_metrics.wind_speed, t->variant.environment_metrics.wind_direction, t->variant.environment_metrics.weight); LOG_INFO("(Received from %s): radiation=%fµR/h", sender, t->variant.environment_metrics.radiation); #endif // Cache the latest env metrics per node on NodeDB so the phone can // pull last-known values across reboots and replays. nodeDB->updateTelemetry(getFrom(&mp), *t, RX_SRC_RADIO); const NodeNum senderNode = getFrom(&mp); if (shouldDisplayRemoteNode(senderNode)) { clearMeasurementPacket(); lastMeasurementPacket = packetPool.allocCopy(mp); } else if (!shouldKeepCurrentRemoteDisplay()) { refreshDisplayedMeasurement(); } } return false; // Let others look at this message also if they want } bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m) { bool valid = false; bool hasSensor = false; // getMetrics() doesn't always get evaluated because of // short-circuit evaluation rules in c++ bool get_metrics; m->time = getTime(); m->which_variant = meshtastic_Telemetry_environment_metrics_tag; m->variant.environment_metrics = meshtastic_EnvironmentMetrics_init_zero; for (TelemetrySensor *sensor : sensors) { get_metrics = sensor->getMetrics(m); // avoid short-circuit evaluation rules valid = valid || get_metrics; hasSensor = true; } #ifndef T1000X_SENSOR_EN if (ina219Sensor.hasSensor()) { get_metrics = ina219Sensor.getMetrics(m); valid = valid || get_metrics; hasSensor = true; } if (ina260Sensor.hasSensor()) { get_metrics = ina260Sensor.getMetrics(m); valid = valid || get_metrics; hasSensor = true; } if (ina3221Sensor.hasSensor()) { get_metrics = ina3221Sensor.getMetrics(m); valid = valid || get_metrics; hasSensor = true; } if (max17048Sensor.hasSensor()) { get_metrics = max17048Sensor.getMetrics(m); valid = valid || get_metrics; hasSensor = true; } #endif #ifdef HAS_RAKPROT if (rak9154Sensor.hasSensor()) { get_metrics = rak9154Sensor.getMetrics(m); valid = valid || get_metrics; hasSensor = true; } #endif return valid && hasSensor; } meshtastic_MeshPacket *EnvironmentTelemetryModule::allocReply() { if (currentRequest) { if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) { ignoreRequest = true; return NULL; } auto req = *currentRequest; const auto &p = req.decoded; meshtastic_Telemetry scratch; meshtastic_Telemetry *decoded = NULL; memset(&scratch, 0, sizeof(scratch)); if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { decoded = &scratch; } else { LOG_ERROR("Error decoding EnvironmentTelemetry module"); return NULL; } // Check for a request for environment metrics if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; if (getEnvironmentTelemetry(&m)) { LOG_INFO("Environment telemetry reply to request"); return allocDataProtobuf(m); } else { return NULL; } } } return NULL; } bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; m.which_variant = meshtastic_Telemetry_environment_metrics_tag; m.time = getTime(); bool validTelemetry = getEnvironmentTelemetry(&m); if (validTelemetry) { if (m.variant.environment_metrics.has_temperature || m.variant.environment_metrics.has_relative_humidity || m.variant.environment_metrics.has_barometric_pressure) LOG_INFO("Send: barometric_pressure=%fkPa, relative_humidity=%f%RH, temperature=%fdegC", m.variant.environment_metrics.barometric_pressure, m.variant.environment_metrics.relative_humidity, m.variant.environment_metrics.temperature); if (m.variant.environment_metrics.has_voltage || m.variant.environment_metrics.has_current || m.variant.environment_metrics.has_iaq || m.variant.environment_metrics.has_gas_resistance) LOG_INFO("Send: voltage=%f, current=%f, IAQ=%d, gas_resistance=%f", m.variant.environment_metrics.voltage, m.variant.environment_metrics.current, m.variant.environment_metrics.iaq, m.variant.environment_metrics.gas_resistance); if (m.variant.environment_metrics.has_distance || m.variant.environment_metrics.has_lux) LOG_INFO("Send: distance=%f, lux=%f", m.variant.environment_metrics.distance, m.variant.environment_metrics.lux); if (m.variant.environment_metrics.has_wind_speed || m.variant.environment_metrics.has_wind_direction) LOG_INFO("Send: wind speed=%fm/s, direction=%d degrees", m.variant.environment_metrics.wind_speed, m.variant.environment_metrics.wind_direction); if (m.variant.environment_metrics.has_weight) LOG_INFO("Send: weight=%fkg", m.variant.environment_metrics.weight); if (m.variant.environment_metrics.has_radiation) LOG_INFO("Send: radiation=%fµR/h", m.variant.environment_metrics.radiation); if (m.variant.environment_metrics.has_soil_temperature || m.variant.environment_metrics.has_soil_moisture) LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature, m.variant.environment_metrics.soil_moisture); if (m.variant.environment_metrics.has_adc_voltage_ch0 || m.variant.environment_metrics.has_adc_voltage_ch1 || m.variant.environment_metrics.has_adc_voltage_ch2 || m.variant.environment_metrics.has_adc_voltage_ch3) LOG_INFO("Send: adc_ch0=%f, adc_ch1=%f, adc_ch2=%f, adc_ch3=%f", m.variant.environment_metrics.adc_voltage_ch0, m.variant.environment_metrics.adc_voltage_ch1, m.variant.environment_metrics.adc_voltage_ch2, m.variant.environment_metrics.adc_voltage_ch3); if (m.variant.environment_metrics.has_adc_voltage_ch4 || m.variant.environment_metrics.has_adc_voltage_ch5 || m.variant.environment_metrics.has_adc_voltage_ch6 || m.variant.environment_metrics.has_adc_voltage_ch7) LOG_INFO("Send: adc_ch4=%f, adc_ch5=%f, adc_ch6=%f, adc_ch7=%f", m.variant.environment_metrics.adc_voltage_ch4, m.variant.environment_metrics.adc_voltage_ch5, m.variant.environment_metrics.adc_voltage_ch6, m.variant.environment_metrics.adc_voltage_ch7); meshtastic_MeshPacket *p = allocDataProtobuf(m); if (!p) { validTelemetry = false; } else { p->to = dest; p->decoded.want_response = false; if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) p->priority = meshtastic_MeshPacket_Priority_RELIABLE; else p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; if (phoneOnly) { LOG_INFO("Send packet to phone"); service->sendToPhone(p); } else { LOG_INFO("Send packet to mesh"); service->sendToMesh(p, RX_SRC_LOCAL, true); if (isPowerSavingSensor()) { meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); if (notification) { 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.environment_update_interval, default_telemetry_broadcast_interval_secs) / 1000U); service->sendClientNotification(notification); } } } } } // Arm the pre-sleep sequence even when no valid reading was available this cycle (e.g. a // BSEC2 call timing violation): a power-saving SENSOR node must still return to deep sleep, // otherwise it stays awake until the next telemetry interval and drains its battery if (!phoneOnly && isPowerSavingSensor()) { if (!validTelemetry) LOG_WARN("Env telemetry unavailable, sleep without send"); sleepOnNextExecution = true; preflightSleepDeferrals = 0; LOG_DEBUG("Start next execution in 5s, then sleep"); setIntervalFromNow(FIVE_SECONDS_MS); } return validTelemetry; } AdminMessageHandleResult EnvironmentTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) { AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL for (TelemetrySensor *sensor : sensors) { result = sensor->handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) return result; } if (ina219Sensor.hasSensor()) { result = ina219Sensor.handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) return result; } if (ina260Sensor.hasSensor()) { result = ina260Sensor.handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) return result; } if (ina3221Sensor.hasSensor()) { result = ina3221Sensor.handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) return result; } if (max17048Sensor.hasSensor()) { result = max17048Sensor.handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) return result; } #endif return result; } #endif