* 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>
315 lines
9.5 KiB
C++
315 lines
9.5 KiB
C++
#include "TransmitHistory.h"
|
|
#include "FSCommon.h"
|
|
#include "SPILock.h"
|
|
#include "gps/RTC.h"
|
|
#include <Throttle.h>
|
|
|
|
#ifdef FSCom
|
|
|
|
TransmitHistory *transmitHistory = nullptr;
|
|
|
|
TransmitHistory *TransmitHistory::getInstance()
|
|
{
|
|
if (!transmitHistory) {
|
|
transmitHistory = new TransmitHistory();
|
|
}
|
|
return transmitHistory;
|
|
}
|
|
|
|
TransmitHistory::StoredTimestamp TransmitHistory::makeStoredTimestamp(uint32_t seconds, uint8_t flags)
|
|
{
|
|
StoredTimestamp stored;
|
|
stored.seconds = seconds;
|
|
stored.flags = flags;
|
|
return stored;
|
|
}
|
|
|
|
TransmitHistory::StoredTimestamp TransmitHistory::decodeLegacyTimestamp(uint32_t seconds)
|
|
{
|
|
const bool isProbablyBootRelative = seconds > 0 && seconds <= LEGACY_BOOT_RELATIVE_MAX_SEC;
|
|
return makeStoredTimestamp(seconds, isProbablyBootRelative ? ENTRY_FLAG_BOOT_RELATIVE : ENTRY_FLAG_NONE);
|
|
}
|
|
|
|
void TransmitHistory::loadFromDisk()
|
|
{
|
|
spiLock->lock();
|
|
auto file = FSCom.open(FILENAME, FILE_O_READ);
|
|
if (file) {
|
|
FileHeader header{};
|
|
if (file.read((uint8_t *)&header, sizeof(header)) == sizeof(header) && header.magic == MAGIC &&
|
|
(header.version == 1 || header.version == VERSION) && header.count <= MAX_ENTRIES) {
|
|
for (uint8_t i = 0; i < header.count; i++) {
|
|
if (header.version == 1) {
|
|
LegacyEntry entry{};
|
|
if (file.read((uint8_t *)&entry, sizeof(entry)) == sizeof(entry) && entry.epochSeconds > 0) {
|
|
history[entry.key] = decodeLegacyTimestamp(entry.epochSeconds);
|
|
}
|
|
} else {
|
|
Entry entry{};
|
|
if (file.read((uint8_t *)&entry, sizeof(entry)) == sizeof(entry) && entry.epochSeconds > 0) {
|
|
history[entry.key] = makeStoredTimestamp(entry.epochSeconds, entry.flags);
|
|
// Do NOT seed lastMillis here.
|
|
//
|
|
// getLastSentToMeshMillis() reconstructs a millis()-relative value
|
|
// from the stored epoch, and Throttle::isWithinTimespanMs() uses
|
|
// the same unsigned subtraction pattern. Once getTime() has a valid
|
|
// wall-clock epoch comparable to stored values, recent reboots still
|
|
// throttle correctly while long power-off periods no longer look like
|
|
// "just sent" and incorrectly suppress the first send.
|
|
//
|
|
// Before RTC/NTP/GPS time is valid, persisted absolute epochs do not
|
|
// contribute, but boot-relative entries still suppress near-term reboot
|
|
// chatter via a narrow recovery window.
|
|
//
|
|
// If we seeded lastMillis to millis() here, every loaded entry would
|
|
// appear to have been sent at boot time, regardless of the true age
|
|
// of the last transmission. That was the regression behind #9901.
|
|
}
|
|
}
|
|
}
|
|
LOG_INFO("TransmitHistory: loaded %u entries from disk", header.count);
|
|
} else {
|
|
LOG_WARN("TransmitHistory: invalid file header, starting fresh");
|
|
}
|
|
file.close();
|
|
} else {
|
|
LOG_INFO("TransmitHistory: no history file found, starting fresh");
|
|
}
|
|
spiLock->unlock();
|
|
dirty = false;
|
|
}
|
|
|
|
void TransmitHistory::setLastSentToMesh(uint16_t key)
|
|
{
|
|
lastMillis[key] = millis();
|
|
uint32_t now = getTime();
|
|
if (now >= 2) {
|
|
const uint8_t flags = (getRTCQuality() == RTCQualityNone) ? ENTRY_FLAG_BOOT_RELATIVE : ENTRY_FLAG_NONE;
|
|
history[key] = makeStoredTimestamp(now, flags);
|
|
dirty = true;
|
|
// Don't flush to disk on every transmit - flash has limited write endurance.
|
|
// The in-memory lastMillis map handles throttle during normal operation.
|
|
// Disk is flushed: before deep sleep (sleep.cpp) and periodically here,
|
|
// throttled to at most once per 5 minutes. Always save the first time
|
|
// after boot so a crash-reboot loop can't avoid persisting.
|
|
if (lastDiskSave == 0 || !Throttle::isWithinTimespanMs(lastDiskSave, SAVE_INTERVAL_MS)) {
|
|
if (saveToDisk()) {
|
|
lastDiskSave = millis();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef PIO_UNIT_TESTING
|
|
void TransmitHistory::setLastSentAtEpoch(uint16_t key, uint32_t epochSeconds)
|
|
{
|
|
if (epochSeconds > 0) {
|
|
history[key] = makeStoredTimestamp(epochSeconds, ENTRY_FLAG_NONE);
|
|
dirty = true;
|
|
} else {
|
|
history.erase(key);
|
|
lastMillis.erase(key);
|
|
}
|
|
}
|
|
|
|
void TransmitHistory::setLastSentAtBootRelative(uint16_t key, uint32_t secondsSinceBoot)
|
|
{
|
|
if (secondsSinceBoot > 0) {
|
|
history[key] = makeStoredTimestamp(secondsSinceBoot, ENTRY_FLAG_BOOT_RELATIVE);
|
|
dirty = true;
|
|
} else {
|
|
history.erase(key);
|
|
lastMillis.erase(key);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
uint32_t TransmitHistory::getLastSentToMeshEpoch(uint16_t key) const
|
|
{
|
|
auto it = history.find(key);
|
|
if (it != history.end()) {
|
|
return it->second.seconds;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
uint32_t TransmitHistory::getLastSentAbsoluteMillis(uint32_t storedEpoch) const
|
|
{
|
|
uint32_t now = getTime();
|
|
if (now < 2) {
|
|
return 0;
|
|
}
|
|
|
|
if (storedEpoch > now) {
|
|
return 0;
|
|
}
|
|
|
|
uint32_t secondsAgo = now - storedEpoch;
|
|
uint32_t msAgo = secondsAgo * 1000;
|
|
|
|
if (secondsAgo > 86400 || msAgo / 1000 != secondsAgo) {
|
|
return 0;
|
|
}
|
|
|
|
return millis() - msAgo;
|
|
}
|
|
|
|
uint32_t TransmitHistory::getLastSentBootRelativeMillis(uint32_t storedSeconds) const
|
|
{
|
|
if (getRTCQuality() != RTCQualityNone) {
|
|
return 0;
|
|
}
|
|
|
|
uint32_t now = getTime();
|
|
|
|
if (storedSeconds <= now) {
|
|
uint32_t secondsAgo = now - storedSeconds;
|
|
if (secondsAgo > BOOT_RELATIVE_RECOVERY_WINDOW_SEC) {
|
|
return 0;
|
|
}
|
|
return millis() - (secondsAgo * 1000);
|
|
}
|
|
|
|
uint32_t secondsAhead = storedSeconds - now;
|
|
if (secondsAhead > BOOT_RELATIVE_RECOVERY_WINDOW_SEC) {
|
|
return 0;
|
|
}
|
|
|
|
return millis();
|
|
}
|
|
|
|
uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const
|
|
{
|
|
// Prefer runtime millis value (accurate within this boot)
|
|
auto mit = lastMillis.find(key);
|
|
if (mit != lastMillis.end()) {
|
|
return mit->second;
|
|
}
|
|
|
|
// Fall back to epoch conversion (loaded from disk after reboot)
|
|
auto it = history.find(key);
|
|
if (it == history.end() || it->second.seconds == 0) {
|
|
return 0; // No stored time - module has never sent
|
|
}
|
|
|
|
// Convert to a millis()-relative timestamp: millis() - msAgo.
|
|
//
|
|
// The result may wrap if msAgo is larger than the current uptime, and that is
|
|
// intentional. Throttle::isWithinTimespanMs() also uses unsigned subtraction,
|
|
// so the reconstructed age is preserved across wraparound:
|
|
// - recent reboot, 5 min ago -> (millis() - lastMs) == 300000, still throttled
|
|
// - long reboot, 30 min ago -> (millis() - lastMs) == 1800000, allowed
|
|
if ((it->second.flags & ENTRY_FLAG_BOOT_RELATIVE) != 0) {
|
|
return getLastSentBootRelativeMillis(it->second.seconds);
|
|
}
|
|
|
|
return getLastSentAbsoluteMillis(it->second.seconds);
|
|
}
|
|
|
|
bool TransmitHistory::saveToDisk()
|
|
{
|
|
if (!dirty) {
|
|
return true;
|
|
}
|
|
|
|
spiLock->lock();
|
|
|
|
FSCom.mkdir("/prefs");
|
|
|
|
// Remove old file first
|
|
if (FSCom.exists(FILENAME)) {
|
|
FSCom.remove(FILENAME);
|
|
}
|
|
|
|
auto file = FSCom.open(FILENAME, FILE_O_WRITE);
|
|
if (file) {
|
|
FileHeader header{};
|
|
header.magic = MAGIC;
|
|
header.version = VERSION;
|
|
header.count = (uint8_t)min((size_t)MAX_ENTRIES, history.size());
|
|
|
|
file.write((uint8_t *)&header, sizeof(header));
|
|
|
|
uint8_t written = 0;
|
|
for (const auto &[key, stored] : history) {
|
|
if (written >= MAX_ENTRIES)
|
|
break;
|
|
Entry entry{};
|
|
entry.key = key;
|
|
entry.epochSeconds = stored.seconds;
|
|
entry.flags = stored.flags;
|
|
file.write((uint8_t *)&entry, sizeof(entry));
|
|
written++;
|
|
}
|
|
file.flush();
|
|
file.close();
|
|
LOG_DEBUG("TransmitHistory: saved %u entries to disk", written);
|
|
dirty = false;
|
|
spiLock->unlock();
|
|
return true;
|
|
} else {
|
|
LOG_WARN("TransmitHistory: failed to open file for writing");
|
|
}
|
|
|
|
spiLock->unlock();
|
|
return false;
|
|
}
|
|
|
|
void TransmitHistory::clear()
|
|
{
|
|
history.clear();
|
|
lastMillis.clear();
|
|
dirty = false;
|
|
lastDiskSave = 0; // so the next legit broadcast persists immediately
|
|
|
|
spiLock->lock();
|
|
if (FSCom.exists(FILENAME)) {
|
|
FSCom.remove(FILENAME);
|
|
}
|
|
spiLock->unlock();
|
|
LOG_INFO("TransmitHistory: cleared in-memory state + on-disk file");
|
|
}
|
|
|
|
#else
|
|
// No filesystem available - provide stub with in-memory tracking
|
|
TransmitHistory *transmitHistory = nullptr;
|
|
|
|
TransmitHistory *TransmitHistory::getInstance()
|
|
{
|
|
if (!transmitHistory) {
|
|
transmitHistory = new TransmitHistory();
|
|
}
|
|
return transmitHistory;
|
|
}
|
|
|
|
void TransmitHistory::loadFromDisk() {}
|
|
|
|
void TransmitHistory::setLastSentToMesh(uint16_t key)
|
|
{
|
|
lastMillis[key] = millis();
|
|
}
|
|
|
|
uint32_t TransmitHistory::getLastSentToMeshEpoch(uint16_t key) const
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const
|
|
{
|
|
auto mit = lastMillis.find(key);
|
|
return (mit != lastMillis.end()) ? mit->second : 0;
|
|
}
|
|
|
|
bool TransmitHistory::saveToDisk()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
void TransmitHistory::clear()
|
|
{
|
|
history.clear();
|
|
lastMillis.clear();
|
|
}
|
|
|
|
#endif
|