From 1ef4caea05939c23cf3dfdedbf250bc0eff8b04e Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Thu, 15 May 2025 09:23:37 -0500 Subject: [PATCH] Host metrics (#6817) * Add std::string exec() function to PortduinoGlue for future work * MVP for HostMetrics module. * Fix compilation for other targets * Remove extra debug calls * Big numbers don't do well as INTs. * Add HostMetrics to config.yaml --- bin/config-dist.yaml | 6 ++ src/modules/Modules.cpp | 4 + src/modules/Telemetry/HostMetrics.cpp | 131 +++++++++++++++++++++++ src/modules/Telemetry/HostMetrics.h | 40 +++++++ src/platform/portduino/PortduinoGlue.cpp | 19 ++++ src/platform/portduino/PortduinoGlue.h | 7 +- 6 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 src/modules/Telemetry/HostMetrics.cpp create mode 100644 src/modules/Telemetry/HostMetrics.h diff --git a/bin/config-dist.yaml b/bin/config-dist.yaml index 98c7696f0..2907e4aab 100644 --- a/bin/config-dist.yaml +++ b/bin/config-dist.yaml @@ -193,6 +193,12 @@ Webserver: # SSLKey: /etc/meshtasticd/ssl/private_key.pem # Path to SSL Key, generated if not present # SSLCert: /etc/meshtasticd/ssl/certificate.pem # Path to SSL Certificate, generated if not present + +HostMetrics: +# ReportInterval: 30 # Interval in minutes between HostMetrics report packets, or 0 for disabled +# Channel: 0 # channel to send Host Metrics over. Defaults to the primary channel. + + General: MaxNodes: 200 MaxMessageQueue: 100 diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 1f2b50057..fac2ca976 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -49,6 +49,7 @@ #endif #if ARCH_PORTDUINO #include "input/LinuxInputImpl.h" +#include "modules/Telemetry/HostMetrics.h" #if !MESHTASTIC_EXCLUDE_STOREFORWARD #include "modules/StoreForwardModule.h" #endif @@ -196,6 +197,9 @@ void setupModules() #if HAS_SCREEN && !MESHTASTIC_EXCLUDE_CANNEDMESSAGES cannedMessageModule = new CannedMessageModule(); #endif +#if ARCH_PORTDUINO + new HostMetricsModule(); +#endif #if HAS_TELEMETRY new DeviceTelemetryModule(); #endif diff --git a/src/modules/Telemetry/HostMetrics.cpp b/src/modules/Telemetry/HostMetrics.cpp new file mode 100644 index 000000000..655be4b25 --- /dev/null +++ b/src/modules/Telemetry/HostMetrics.cpp @@ -0,0 +1,131 @@ +#include "HostMetrics.h" +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "MeshService.h" +#if ARCH_PORTDUINO +#include "PortduinoGlue.h" +#include +#endif + +int32_t HostMetricsModule::runOnce() +{ +#if ARCH_PORTDUINO + if (settingsMap[hostMetrics_interval] == 0) { + return disable(); + } else { + sendMetrics(); + return 60 * 1000 * settingsMap[hostMetrics_interval]; + } +#else + return disable(); +#endif +} + +bool HostMetricsModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) +{ + // Don't worry about storing telemetry in NodeDB if we're a repeater + if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) + return false; + + if (t->which_variant == meshtastic_Telemetry_host_metrics_tag) { +#ifdef DEBUG_PORT + const char *sender = getSenderShortName(mp); + + LOG_INFO("(Received Host Metrics from %s): uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", sender, + t->variant.host_metrics.uptime_seconds, t->variant.host_metrics.diskfree1_bytes, + t->variant.host_metrics.freemem_bytes, static_cast(t->variant.host_metrics.load1) / 100, + static_cast(t->variant.host_metrics.load5) / 100, + static_cast(t->variant.host_metrics.load15) / 100); +#endif + } + return false; // Let others look at this message also if they want +} + +/* +meshtastic_MeshPacket *HostMetricsModule::allocReply() +{ + if (currentRequest) { + 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_HostMetrics_msg, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding HostMetrics module!"); + return NULL; + } + // Check for a request for device metrics + if (decoded->which_variant == meshtastic_Telemetry_host_metrics_tag) { + LOG_INFO("Device telemetry reply to request"); + return allocDataProtobuf(getHostMetrics()); + } + } + return NULL; +} + */ + +#if ARCH_PORTDUINO +meshtastic_Telemetry HostMetricsModule::getHostMetrics() +{ + std::string file_line; + meshtastic_Telemetry t = meshtastic_HostMetrics_init_zero; + t.which_variant = meshtastic_Telemetry_host_metrics_tag; + + if (access("/proc/uptime", R_OK) == 0) { + std::ifstream proc_uptime("/proc/uptime"); + if (proc_uptime.is_open()) { + std::getline(proc_uptime, file_line, ' '); + proc_uptime.close(); + t.variant.host_metrics.uptime_seconds = stoul(file_line); + } + } + + std::filesystem::space_info root = std::filesystem::space("/"); + t.variant.host_metrics.diskfree1_bytes = root.available; + + if (access("/proc/meminfo", R_OK) == 0) { + std::ifstream proc_meminfo("/proc/meminfo"); + if (proc_meminfo.is_open()) { + do { + std::getline(proc_meminfo, file_line); + } while (file_line.find("MemAvailable") == std::string::npos); + proc_meminfo.close(); + t.variant.host_metrics.freemem_bytes = stoull(file_line.substr(file_line.find_first_of("0123456789"))) * 1024; + } + } + if (access("/proc/loadavg", R_OK) == 0) { + std::ifstream proc_loadavg("/proc/loadavg"); + if (proc_loadavg.is_open()) { + std::getline(proc_loadavg, file_line, ' '); + t.variant.host_metrics.load1 = stof(file_line) * 100; + std::getline(proc_loadavg, file_line, ' '); + t.variant.host_metrics.load5 = stof(file_line) * 100; + std::getline(proc_loadavg, file_line, ' '); + t.variant.host_metrics.load15 = stof(file_line) * 100; + proc_loadavg.close(); + } + } + + return t; +} + +bool HostMetricsModule::sendMetrics() +{ + meshtastic_Telemetry telemetry = getHostMetrics(); + LOG_INFO("Send: uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", + telemetry.variant.host_metrics.uptime_seconds, telemetry.variant.host_metrics.diskfree1_bytes, + telemetry.variant.host_metrics.freemem_bytes, static_cast(telemetry.variant.host_metrics.load1) / 100, + static_cast(telemetry.variant.host_metrics.load5) / 100, + static_cast(telemetry.variant.host_metrics.load15) / 100); + + meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); + p->to = NODENUM_BROADCAST; + p->decoded.want_response = false; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + p->channel = settingsMap[hostMetrics_channel]; + LOG_INFO("Send packet to mesh"); + service->sendToMesh(p, RX_SRC_LOCAL, true); + return true; +} +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/HostMetrics.h b/src/modules/Telemetry/HostMetrics.h new file mode 100644 index 000000000..99ee631c1 --- /dev/null +++ b/src/modules/Telemetry/HostMetrics.h @@ -0,0 +1,40 @@ +#pragma once +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "ProtobufModule.h" + +class HostMetricsModule : private concurrency::OSThread, public ProtobufModule +{ + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &HostMetricsModule::handleStatusUpdate); + + public: + HostMetricsModule() + : concurrency::OSThread("HostMetrics"), + ProtobufModule("HostMetrics", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) + { + uptimeWrapCount = 0; + uptimeLastMs = millis(); + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent + } + virtual bool wantUIFrame() { return false; } + + protected: + /** Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; + // virtual meshtastic_MeshPacket *allocReply() override; + virtual int32_t runOnce() override; + /** + * Send our Telemetry into the mesh + */ + bool sendMetrics(); + + private: + meshtastic_Telemetry getHostMetrics(); + + uint32_t lastSentToMesh = 0; + uint32_t uptimeWrapCount; + uint32_t uptimeLastMs; +}; \ No newline at end of file diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index 4b96e662a..c6aa30629 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -600,6 +600,11 @@ bool loadConfig(const char *configPath) (yamlConfig["Webserver"]["SSLCert"]).as("/etc/meshtasticd/ssl/certificate.pem"); } + if (yamlConfig["HostMetrics"]) { + settingsMap[hostMetrics_channel] = (yamlConfig["HostMetrics"]["Channel"]).as(0); + settingsMap[hostMetrics_interval] = (yamlConfig["HostMetrics"]["ReportInterval"]).as(0); + } + if (yamlConfig["General"]) { settingsMap[maxnodes] = (yamlConfig["General"]["MaxNodes"]).as(200); settingsMap[maxtophone] = (yamlConfig["General"]["MaxMessageQueue"]).as(100); @@ -650,4 +655,18 @@ bool MAC_from_string(std::string mac_str, uint8_t *dmac) } else { return false; } +} + +std::string exec(const char *cmd) +{ // https://stackoverflow.com/a/478960 + std::array buffer; + std::string result; + std::unique_ptr pipe(popen(cmd, "r"), pclose); + if (!pipe) { + throw std::runtime_error("popen() failed!"); + } + while (fgets(buffer.data(), static_cast(buffer.size()), pipe.get()) != nullptr) { + result += buffer.data(); + } + return result; } \ No newline at end of file diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index 6393d7294..0cf0201aa 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -100,7 +100,9 @@ enum configNames { ascii_logs, config_directory, available_directory, - mac_address + mac_address, + hostMetrics_interval, + hostMetrics_channel }; enum { no_screen, x11, fb, st7789, st7735, st7735s, st7796, ili9341, ili9342, ili9486, ili9488, hx8357d }; enum { no_touchscreen, xpt2046, stmpe610, gt911, ft5x06 }; @@ -114,4 +116,5 @@ int initGPIOPin(int pinNum, std::string gpioChipname, int line); bool loadConfig(const char *configPath); static bool ends_with(std::string_view str, std::string_view suffix); void getMacAddr(uint8_t *dmac); -bool MAC_from_string(std::string mac_str, uint8_t *dmac); \ No newline at end of file +bool MAC_from_string(std::string mac_str, uint8_t *dmac); +std::string exec(const char *cmd); \ No newline at end of file