Files
MeshCore/src/helpers/AutoDiscoverRTCClock.cpp
T
mikecarper ddf625e9d0 Fix packet, delivery, clock and radio stability regressions
Reject truncated anonymous reply paths and advert metadata before copying,
while preserving timestamp-only empty-password logins. Limit room posting to
read-write/admin ACL roles, retain inbox frames until the requesting transport
accepts them, and lock raw LittleFS traversal against concurrent writes.

Separate ISR event generations from main-loop radio state. Confirm RX/TX IRQs,
detach concrete wrapper callbacks safely, and discard inconsistent LR2021 FIFO
snapshots before rearming. Preserve bridge RX frames through packet-pool
exhaustion and bound ESP-NOW retries and missing-callback recovery. Screen RTC
register layouts without losing DS1307 startup, and ignore poisoned future
contact timestamps during clock bootstrap.

Keep the nRF52 loop stack at 8 KiB while reducing the OTA listing call chain
from 7,832 to 6,056 static bytes. Retain all target names through lossless Full
Companion compression and the canonical size optimizer. Preserve constrained
STM32 filesystems and bind corrected ESP32 UART/OLED budgets to source size
assertions. Document the separately measured preexisting W12 package limits.

Adapted for this branch from meshcore-dev/MeshCore PRs #3521, #3340, #3362,
#2062, #3503, #3038, #3534, #3544 and #3512.

Validation: 1,829 native tests; sanitized parser, inbox, LittleFS, ACL, clock,
RTC, IRQ/FIFO and bridge fault tests; 332 additional Companion cases; real
RAK4631 Full and both repeater profiles, G2 Full/standard, V3 Full and both
constrained STM32 release recipes. Radio-family compile coverage continues
separately; W12 full/portable package guards remain enforced.
2026-10-09 19:56:08 -07:00

195 lines
7.5 KiB
C++

#include "AutoDiscoverRTCClock.h"
#include "RTClib.h"
#include <Melopero_RV3028.h>
#include "RTC_RX8130CE.h"
static RTC_DS3231 rtc_3231;
static bool ds3231_success = false;
static Melopero_RV3028 rtc_rv3028;
static bool rv3028_success = false;
static RTC_PCF8563 rtc_8563;
static bool rtc_8563_success = false;
static RTC_RX8130CE rtc_8130;
static bool rtc_8130_success = false;
#define DS3231_ADDRESS 0x68
#define RV3028_ADDRESS 0x52
#define PCF8563_ADDRESS 0x51
#define RX8130CE_ADDRESS 0x32
bool AutoDiscoverRTCClock::i2c_probe(TwoWire& wire, uint8_t addr) {
wire.beginTransmission(addr);
uint8_t error = wire.endTransmission();
return (error == 0);
}
// An ACK only says that something answers at an RTC's address: an IMU at 0x68 or an EEPROM in
// 0x50-0x57 answers too, and would then be read as the clock and written on every time sync.
// So an RTC is adopted only if its time registers read like that chip's, per its datasheet:
// - bits documented as always 0 must read 0 (the PCF8563 documents none), and the seven
// registers must not all read 0xFF, as an erased EEPROM does;
// - seconds, minutes, date and month must be valid BCD in range, unless the chip's power-loss
// flag is set, or a DS1307's clock-halt bit is set: startup time may be undefined
// (not checked for the RX8130CE, see its entry below).
// The year is not checked, as MeshCore can write an out-of-range one from a bad epoch. This
// cannot catch every device: one whose bytes happen to fit is still adopted, as before.
struct RtcId {
uint8_t time_reg; // seconds register; the seven time registers follow it
uint8_t zero[7]; // bits that always read 0
uint8_t date_idx; // index of the date register within the seven
uint8_t flag_reg; // power-loss flag register and bit; flag_bit 0 skips the field check
uint8_t flag_bit;
uint8_t halt_bit; // seconds clock-halt flag; 0 for every profile except DS1307 at 0x68
};
#if !defined(DISABLE_DS3231_PROBE)
// DS3231 (Maxim 19-5170 Rev 10): Figure 1, p. 11; OSF in Status (0Fh) bit 7, p. 14. 00h bit 7
// is left out: a DS1307 at 0x68, which this code also drives, powers up with its clock-halt bit
// there set (Maxim DS1307 Rev 3/15, Table 2 and text, p. 8). Its other bits match.
// A halted DS1307 may have unset calendar fields; 0Fh is RAM on that chip, not OSF.
static const RtcId DS3231_ID =
{ 0x00, { 0x00, 0x80, 0x80, 0xF8, 0xC0, 0x60, 0x00 }, 4, 0x0F, 0x80, 0x80 };
#endif
// RV3028 (RV-3028-C7 App Manual Rev 1.4): 3.2, p. 12; PORF in Status (0Eh) bit 0, 3.7, p. 22
static const RtcId RV3028_ID =
{ 0x00, { 0x80, 0x80, 0xC0, 0xF8, 0xC0, 0xE0, 0x00 }, 4, 0x0E, 0x01, 0 };
// PCF8563 (NXP Rev 11.1): unused bits are "not relevant", not 0, Table 4, p. 10; VL in
// VL_seconds (02h) bit 7, Table 8, p. 13, and set at power-up, Table 27, p. 24
static const RtcId PCF8563_ID = { 0x02, { 0 }, 3, 0x02, 0x80, 0 };
// RX8130CE (Epson ETM50E-10): read value always 0, 13.2.1, p. 22. No field check: its power-loss
// flag VLF (1Dh bit 1) cannot vouch for the fields, as RTC_RX8130CE::begin() clears it on every
// boot without setting the time.
static const RtcId RX8130CE_ID =
{ 0x10, { 0x80, 0x80, 0xC0, 0x80, 0xC0, 0xE0, 0x00 }, 4, 0x1D, 0x00, 0 };
// Reads n registers from reg, with a repeated start as every one of these datasheets documents
static bool rtcRead(TwoWire& wire, uint8_t addr, uint8_t reg, uint8_t* buf, uint8_t n) {
wire.beginTransmission(addr);
wire.write(reg);
if (wire.endTransmission(false) != 0) return false;
if (wire.requestFrom(addr, n) != n) return false;
for (uint8_t i = 0; i < n; i++) {
const int value = wire.read();
if (value < 0) return false;
buf[i] = uint8_t(value);
}
return true;
}
static bool bcdInRange(uint8_t v, uint8_t lo, uint8_t hi) {
if ((v & 0x0F) > 9 || (v >> 4) > 9) return false;
uint8_t d = (v >> 4) * 10 + (v & 0x0F);
return d >= lo && d <= hi;
}
// 1 if this read rules the device out, 0 if not, -1 if a read failed
static int rtcCheck(TwoWire& wire, uint8_t addr, const RtcId& id) {
uint8_t t[7];
if (!rtcRead(wire, addr, id.time_reg, t, 7)) return -1;
bool all_ff = true;
for (int i = 0; i < 7; i++) {
if (t[i] & id.zero[i]) return 1;
all_ff = all_ff && t[i] == 0xFF;
}
if (all_ff) return 1; // an erased EEPROM
if (t[0] & id.halt_bit) return 0; // DS1307 startup; never rely on its RAM at 0Fh
if (id.flag_bit == 0) return 0;
if (bcdInRange(t[0] & 0x7F, 0, 59) && bcdInRange(t[1] & 0x7F, 0, 59)
&& bcdInRange(t[id.date_idx] & 0x3F, 1, 31) && bcdInRange(t[5] & 0x1F, 1, 12)) {
return 0;
}
uint8_t flag;
if (!rtcRead(wire, addr, id.flag_reg, &flag, 1)) return -1;
return (flag & id.flag_bit) ? 0 : 1; // power was lost, so the time is undefined
}
// Retry one failed/corrupted read, but do not initialize or write an unknown
// device when neither read identifies a compatible register layout.
static bool rtcLooksCompatible(TwoWire& wire, uint8_t addr, const RtcId& id) {
for (int attempt = 0; attempt < 2; ++attempt) {
if (rtcCheck(wire, addr, id) == 0) return true;
}
MESH_DEBUG_PRINTLN("RTC: skipping unrecognized or unreadable device at 0x%02X", addr);
return false;
}
void AutoDiscoverRTCClock::begin(TwoWire& wire) {
ds3231_success = rv3028_success = rtc_8563_success = rtc_8130_success = false;
#if !defined(DISABLE_DS3231_PROBE)
if (i2c_probe(wire, DS3231_ADDRESS)
&& rtcLooksCompatible(wire, DS3231_ADDRESS, DS3231_ID)) {
ds3231_success = rtc_3231.begin(&wire);
}
#endif
if (i2c_probe(wire, RV3028_ADDRESS)
&& rtcLooksCompatible(wire, RV3028_ADDRESS, RV3028_ID)) {
rtc_rv3028.initI2C(wire);
rtc_rv3028.writeToRegister(0x35, 0x00);
rtc_rv3028.writeToRegister(0x37, 0xB4); // Direct Switching Mode (DSM): when VDD < VBACKUP, switchover occurs from VDD to VBACKUP
rtc_rv3028.set24HourMode(); // Set the device to use the 24hour format (default) instead of the 12 hour format
rv3028_success = true;
}
if (i2c_probe(wire, PCF8563_ADDRESS)
&& rtcLooksCompatible(wire, PCF8563_ADDRESS, PCF8563_ID)) {
MESH_DEBUG_PRINTLN("PCF8563: Found");
rtc_8563_success = rtc_8563.begin(&wire);
}
if (i2c_probe(wire, RX8130CE_ADDRESS)
&& rtcLooksCompatible(wire, RX8130CE_ADDRESS, RX8130CE_ID)) {
MESH_DEBUG_PRINTLN("RX8130CE: Found");
rtc_8130_success = rtc_8130.begin(&wire);
if (rtc_8130_success) MESH_DEBUG_PRINTLN("RX8130CE: Initialized");
}
}
uint32_t AutoDiscoverRTCClock::getCurrentTime() {
if (ds3231_success) {
return rtc_3231.now().unixtime();
}
if (rv3028_success) {
return DateTime(
rtc_rv3028.getYear(),
rtc_rv3028.getMonth(),
rtc_rv3028.getDate(),
rtc_rv3028.getHour(),
rtc_rv3028.getMinute(),
rtc_rv3028.getSecond()
).unixtime();
}
if (rtc_8563_success) {
return rtc_8563.now().unixtime();
}
if (rtc_8130_success) {
MESH_DEBUG_PRINTLN("RX8130CE: Reading time");
return rtc_8130.now().unixtime();
}
return _fallback->getCurrentTime();
}
void AutoDiscoverRTCClock::setCurrentTime(uint32_t time) {
if (ds3231_success) {
rtc_3231.adjust(DateTime(time));
} else if (rv3028_success) {
auto dt = DateTime(time);
rtc_rv3028.setTime(dt.year(), dt.month(), dt.dayOfTheWeek(), dt.day(),
dt.hour(), dt.minute(), dt.second());
} else if (rtc_8563_success) {
rtc_8563.adjust(DateTime(time));
} else if (rtc_8130_success) {
MESH_DEBUG_PRINTLN("RX8130CE: Setting time");
rtc_8130.adjust(DateTime(time));
} else {
_fallback->setCurrentTime(time);
}
}