Files
firmware/src/modules/Telemetry/DeviceTelemetry.cpp
T
Andrew YongandBen Meadors b71c1adb26 stm32wl: add hardware RTC support (rak3172) (#10961)
* stm32wl: add hardware RTC support infrastructure

Wires the STM32WL chip's internal RTC (running off the LSE 32.768kHz
crystal) into meshtastic's existing time-of-day framework
(perhapsSetRTC()/readFromRTC()), following the same pattern already
used for I2C RTC chips (RV3028, PCF8563/85063, RX8130CE).

LSE is started and polled manually before ever calling into the
STM32RTC library, with our own bounded timeout - the library's own
internal LSE startup path has no bounded fallback and hangs forever
via Error_Handler() if the crystal never locks, so this is required
for a board with a missing/faulty crystal to boot normally rather
than hang.

Gated behind a new HAS_LSE variant flag (currently unset everywhere,
so this is inert until a variant opts in - see follow-up commit).

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

* gps: qualify RTC.h includes to avoid case-insensitive filesystem collision with STM32RTC

The stm32duino STM32RTC library (added to lib_deps in a follow-up
commit) ships its own src/rtc.h. On case-insensitive filesystems
(the macOS default), an unqualified #include "RTC.h"/<RTC.h> from
any file outside src/gps/ resolves to the library's rtc.h instead of
src/gps/RTC.h, since PlatformIO's LDF puts lib_deps include paths
ahead of the project's own -Isrc/gps.

Qualify every include as gps/RTC.h so it can't collide with any
same-named header a future dependency might ship, regardless of
filesystem case sensitivity. Purely mechanical, no behavior change.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

* stm32wl(rak3172): enable hardware RTC support

Opts rak3172 into the HAS_LSE infrastructure added previously: sets
STM32WL_LSE_DRIVE to a conservative default and pulls in the
STM32RTC library. rak3172 has ~63KB flash headroom going in;
build-verified at 76.7% flash usage after this change (up from a
73.8% baseline), well within budget.

wio-e5 is not opted in here despite sharing the same STM32WLE5 chip
- it's already at 96.8% flash usage today (GPS + I2C sensor support
compiled in, unlike rak3172), leaving too little headroom to safely
add STM32RTC without first trimming something else.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

* stm32wl: add docstrings for LSE/RTC setup functions

Addresses CodeRabbit's docstring coverage check on PR #10961.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

* stm32wl: address CodeRabbit nitpicks on PR #10961

- Brace the single-statement HAS_LSE branch in perhapsSetRTC() to
  match the sibling readFromRTC() branch's style.
- Quote the RTC.h include in PhoneAPI.cpp for consistency with every
  other qualified include site.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

---------

Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-07-16 19:00:45 -05:00

208 lines
9.8 KiB
C++

#include "DeviceTelemetry.h"
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "Default.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "PowerFSM.h"
#include "RadioLibInterface.h"
#include "Router.h"
#include "TransmitHistory.h"
#include "configuration.h"
#include "gps/RTC.h"
#include "main.h"
#include "memGet.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>
#include <meshUtils.h>
#define MAGIC_USB_BATTERY_LEVEL 101
static constexpr uint16_t TX_HISTORY_KEY_DEVICE_TELEMETRY = 0x8001;
int32_t DeviceTelemetryModule::runOnce()
{
refreshUptime();
uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_DEVICE_TELEMETRY) : 0;
bool isImpoliteRole = isSensorOrRouterRole();
if (((lastTelemetry == 0) ||
((uptimeLastMs - lastTelemetry) >= Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval,
default_telemetry_broadcast_interval_secs,
numOnlineNodes, TrafficType::TELEMETRY))) &&
airTime->isTxAllowedChannelUtil(!isImpoliteRole) && airTime->isTxAllowedAirUtil() &&
config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN &&
moduleConfig.telemetry.device_telemetry_enabled) {
sendTelemetry();
if (transmitHistory)
transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_DEVICE_TELEMETRY);
} 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 (lastSentStatsToPhone == 0 || (uptimeLastMs - lastSentStatsToPhone) >= sendStatsToPhoneIntervalMs) {
sendLocalStatsToPhone();
lastSentStatsToPhone = uptimeLastMs;
}
}
return sendToPhoneIntervalMs;
}
bool DeviceTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
{
if (t->which_variant == meshtastic_Telemetry_device_metrics_tag) {
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
const char *sender = getSenderShortName(mp);
LOG_INFO("(Received from %s): air_util_tx=%f, channel_utilization=%f, battery_level=%i, voltage=%f", sender,
t->variant.device_metrics.air_util_tx, t->variant.device_metrics.channel_utilization,
t->variant.device_metrics.battery_level, t->variant.device_metrics.voltage);
#endif
nodeDB->updateTelemetry(getFrom(&mp), *t, RX_SRC_RADIO);
}
return false; // Let others look at this message also if they want
}
meshtastic_MeshPacket *DeviceTelemetryModule::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 DeviceTelemetry module!");
return NULL;
}
// Check for a request for device metrics
if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) {
LOG_INFO("Device telemetry reply to request");
return allocDataProtobuf(getDeviceTelemetry());
} else if (decoded->which_variant == meshtastic_Telemetry_local_stats_tag) {
LOG_INFO("Device telemetry reply w/ LocalStats to request");
return allocDataProtobuf(getLocalStatsTelemetry());
}
}
return NULL;
}
meshtastic_Telemetry DeviceTelemetryModule::getDeviceTelemetry()
{
meshtastic_Telemetry t = meshtastic_Telemetry_init_zero;
t.which_variant = meshtastic_Telemetry_device_metrics_tag;
t.time = getTime();
t.variant.device_metrics = meshtastic_DeviceMetrics_init_zero;
t.variant.device_metrics.has_air_util_tx = true;
t.variant.device_metrics.has_battery_level = true;
t.variant.device_metrics.has_channel_utilization = true;
t.variant.device_metrics.has_uptime_seconds = true;
t.variant.device_metrics.air_util_tx = airTime->utilizationTXPercent();
t.variant.device_metrics.battery_level = (!powerStatus->getHasBattery() || powerStatus->getIsCharging())
? MAGIC_USB_BATTERY_LEVEL
: powerStatus->getBatteryChargePercent();
t.variant.device_metrics.channel_utilization = airTime->channelUtilizationPercent();
// Only populate voltage when we actually have a battery reading. Previously this assigned
// -0.001 (from -1 mV / 1000) whenever the ADC returned -1, leaking a sentinel onto the wire
// that clients then displayed as a real negative voltage. See GH #7958.
int32_t batteryMv = powerStatus->getBatteryVoltageMv();
if (powerStatus->getHasBattery() && batteryMv > 0) {
t.variant.device_metrics.has_voltage = true;
t.variant.device_metrics.voltage = batteryMv / 1000.0f;
}
t.variant.device_metrics.uptime_seconds = getUptimeSeconds();
return t;
}
meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry()
{
meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero;
telemetry.which_variant = meshtastic_Telemetry_local_stats_tag;
telemetry.variant.local_stats = meshtastic_LocalStats_init_zero;
telemetry.time = getTime();
telemetry.variant.local_stats.uptime_seconds = getUptimeSeconds();
telemetry.variant.local_stats.channel_utilization = airTime->channelUtilizationPercent();
telemetry.variant.local_stats.air_util_tx = airTime->utilizationTXPercent();
telemetry.variant.local_stats.num_online_nodes = numOnlineNodes;
telemetry.variant.local_stats.num_total_nodes = nodeDB->getNumMeshNodes();
if (RadioLibInterface::instance) {
RadioLibInterface::instance->updateNoiseFloor();
telemetry.variant.local_stats.noise_floor = RadioLibInterface::instance->getAverageNoiseFloor();
telemetry.variant.local_stats.num_packets_tx = RadioLibInterface::instance->txGood;
telemetry.variant.local_stats.num_packets_rx = RadioLibInterface::instance->rxGood + RadioLibInterface::instance->rxBad;
telemetry.variant.local_stats.num_packets_rx_bad = RadioLibInterface::instance->rxBad;
telemetry.variant.local_stats.num_tx_relay = RadioLibInterface::instance->txRelay;
telemetry.variant.local_stats.num_tx_dropped = RadioLibInterface::instance->txDrop;
}
#ifdef ARCH_PORTDUINO
if (SimRadio::instance) {
if (!RadioLibInterface::instance)
telemetry.variant.local_stats.noise_floor = SimRadio::instance->getCurrentRSSI();
telemetry.variant.local_stats.num_packets_tx = SimRadio::instance->txGood;
telemetry.variant.local_stats.num_packets_rx = SimRadio::instance->rxGood + SimRadio::instance->rxBad;
telemetry.variant.local_stats.num_packets_rx_bad = SimRadio::instance->rxBad;
telemetry.variant.local_stats.num_tx_relay = SimRadio::instance->txRelay;
telemetry.variant.local_stats.num_tx_dropped = SimRadio::instance->txDrop;
}
#else
telemetry.variant.local_stats.heap_total_bytes = memGet.getHeapSize();
telemetry.variant.local_stats.heap_free_bytes = memGet.getFreeHeap();
#endif
if (router) {
telemetry.variant.local_stats.num_rx_dupe = router->rxDupe;
telemetry.variant.local_stats.num_tx_relay_canceled = router->txRelayCanceled;
}
LOG_INFO("Sending local stats: uptime=%i, channel_utilization=%f, air_util_tx=%f, num_online_nodes=%i, num_total_nodes=%i, "
"noise_floor=%d",
telemetry.variant.local_stats.uptime_seconds, telemetry.variant.local_stats.channel_utilization,
telemetry.variant.local_stats.air_util_tx, telemetry.variant.local_stats.num_online_nodes,
telemetry.variant.local_stats.num_total_nodes, telemetry.variant.local_stats.noise_floor);
LOG_INFO("num_packets_tx=%i, num_packets_rx=%i, num_packets_rx_bad=%i", telemetry.variant.local_stats.num_packets_tx,
telemetry.variant.local_stats.num_packets_rx, telemetry.variant.local_stats.num_packets_rx_bad);
return telemetry;
}
void DeviceTelemetryModule::sendLocalStatsToPhone()
{
meshtastic_MeshPacket *p = allocDataProtobuf(getLocalStatsTelemetry());
p->to = NODENUM_BROADCAST;
p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
service->sendToPhone(p);
}
bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
{
meshtastic_Telemetry telemetry = getDeviceTelemetry();
LOG_INFO("Send: air_util_tx=%f, channel_utilization=%f, battery_level=%i, voltage=%f, uptime=%i",
telemetry.variant.device_metrics.air_util_tx, telemetry.variant.device_metrics.channel_utilization,
telemetry.variant.device_metrics.battery_level, telemetry.variant.device_metrics.voltage,
telemetry.variant.device_metrics.uptime_seconds);
DEBUG_HEAP_BEFORE;
meshtastic_MeshPacket *p = allocDataProtobuf(telemetry);
DEBUG_HEAP_AFTER("DeviceTelemetryModule::sendTelemetry", p);
p->to = dest;
p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
nodeDB->updateTelemetry(nodeDB->getNodeNum(), telemetry, RX_SRC_LOCAL);
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);
}
return true;
}