mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-22 14:41:18 +00:00
meshtimesync p1
This commit is contained in:
1 parent
fc940d27ba
commit
14cb8c61f9
30 files changed
+1231
-3
No files matched your search
@@ -342,6 +342,56 @@ void CompanionMesh::loop()
|
||||
}
|
||||
}
|
||||
|
||||
void CompanionMesh::onAdvertTimeSample(const mesh::Identity &id, uint32_t timestamp,
|
||||
uint8_t hops)
|
||||
{
|
||||
/* Signature already verified by mesh::Mesh before this hook fires. */
|
||||
_timesync.onAdvertHeard(id.pub_key, timestamp, hops,
|
||||
(uint32_t)(k_uptime_get() / 1000));
|
||||
}
|
||||
|
||||
void CompanionMesh::timeSyncTick()
|
||||
{
|
||||
if (!prefs.meshtimesync) return;
|
||||
|
||||
uint32_t up = (uint32_t)(k_uptime_get() / 1000);
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
MeshTimeSync::Verdict v = _timesync.tick(now, up);
|
||||
if (v.type != MeshTimeSync::VERDICT_STEP) return;
|
||||
|
||||
/* GPS gate: sense only while GPS owns the clock. */
|
||||
if (gps_is_available() && gps_is_enabled()) {
|
||||
LOG_INF("meshtimesync: step %+d s wanted, GPS gate active - not applied",
|
||||
(int)v.delta);
|
||||
return;
|
||||
}
|
||||
/* Forward-only role: our clock stamps outgoing DMs and peers hold
|
||||
* per-sender replay high-water marks — a backward step gets our
|
||||
* messages dropped as replays. Report, never apply. */
|
||||
if (v.delta < 0) {
|
||||
_timesync.noteBackwardSkipped();
|
||||
LOG_WRN("meshtimesync: backward step %+d s wanted - skipped (companion is forward-only)",
|
||||
(int)v.delta);
|
||||
return;
|
||||
}
|
||||
applyTimeSyncStep(v, now, up);
|
||||
}
|
||||
|
||||
void CompanionMesh::applyTimeSyncStep(const MeshTimeSync::Verdict &v, uint32_t now,
|
||||
uint32_t uptime_secs)
|
||||
{
|
||||
uint32_t new_time = (uint32_t)((int64_t)now + v.delta);
|
||||
getRTCClock()->setCurrentTime(new_time);
|
||||
zephcore_rtc_save(new_time);
|
||||
time_sync_report(TIME_SYNC_MESH);
|
||||
|
||||
_timesync.noteStepApplied(v.delta, new_time, uptime_secs, v.bootstrap);
|
||||
LOG_WRN("meshtimesync: stepped clock %+d s (%s, votes %u/%u) -> %u",
|
||||
(int)v.delta, v.bootstrap ? "bootstrap" : "consensus",
|
||||
(unsigned)v.consensus.votes_for, (unsigned)v.consensus.votes_against,
|
||||
(unsigned)new_time);
|
||||
}
|
||||
|
||||
void CompanionMesh::onLoginSent(const ContactInfo &contact)
|
||||
{
|
||||
memcpy(&_pending_login, contact.id.pub_key, 4);
|
||||
@@ -2237,6 +2287,7 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
getRTCClock()->setCurrentTime(secs);
|
||||
time_sync_report(TIME_SYNC_APP);
|
||||
zephcore_rtc_save(secs); /* persist to hardware RTC */
|
||||
_timesync.noteManualSync((uint32_t)(k_uptime_get() / 1000));
|
||||
sendPacketOk();
|
||||
} else {
|
||||
sendPacketError(ERR_ILLEGAL_ARG);
|
||||
@@ -2750,10 +2801,23 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
int n = snprintf(dp, remaining, "gps_interval:%u", (unsigned)gps_interval);
|
||||
if (n > 0 && (size_t)n < remaining) {
|
||||
dp += n;
|
||||
first = false;
|
||||
}
|
||||
// If snprintf would have truncated, dp stays put — writeFrame
|
||||
// sends only what we successfully wrote.
|
||||
}
|
||||
{
|
||||
size_t remaining = (size_t)(rsp_end - dp);
|
||||
if (!first && remaining > 0) {
|
||||
*dp++ = ',';
|
||||
remaining--;
|
||||
}
|
||||
int n = snprintf(dp, remaining, "meshtimesync:%d",
|
||||
prefs.meshtimesync ? 1 : 0);
|
||||
if (n > 0 && (size_t)n < remaining) {
|
||||
dp += n;
|
||||
}
|
||||
}
|
||||
// Note: Environment sensors are auto-detected, no settings needed
|
||||
|
||||
writeFrame(rsp, (size_t)(dp - (char *)rsp));
|
||||
@@ -2792,6 +2856,10 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
} else {
|
||||
sendPacketError(ERR_ILLEGAL_ARG);
|
||||
}
|
||||
} else if (strcmp(key, "meshtimesync") == 0) {
|
||||
prefs.meshtimesync = (val[0] == '1') ? 1 : 0;
|
||||
_store->savePrefs(prefs);
|
||||
sendPacketOk();
|
||||
} else {
|
||||
sendPacketError(ERR_ILLEGAL_ARG);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <helpers/BaseChatMesh.h>
|
||||
#include <helpers/MeshTimeSync.h>
|
||||
#include <helpers/TransportKeyStore.h>
|
||||
#include <ZephyrDataStore.h>
|
||||
#include <NodePrefs.h>
|
||||
#include <zephyr/kernel.h>
|
||||
|
||||
/* BLE push notification codes */
|
||||
#define PUSH_CODE_ADVERT 0x80
|
||||
@@ -231,6 +233,15 @@ public:
|
||||
bool onChannelLoaded(uint8_t idx, const ChannelDetails &ch) override;
|
||||
bool getChannelForSave(uint8_t idx, ChannelDetails &ch) override;
|
||||
|
||||
/* Mesh time sync (forward-only: our clock stamps outgoing DMs and peers
|
||||
* hold per-sender replay high-water marks, so a backward step gets our
|
||||
* messages dropped as replays until wall-clock catches up) */
|
||||
MeshTimeSync *getMeshTimeSync() { return &_timesync; }
|
||||
void noteGPSTimeSync() { _timesync.noteGPSSync((uint32_t)(k_uptime_get() / 1000)); }
|
||||
/* Paced evaluation — called from the housekeeping event (loop() only runs
|
||||
* on packet-driven events). */
|
||||
void timeSyncTick();
|
||||
|
||||
/* Prefs (includes node_lat/lon) */
|
||||
NodePrefs prefs;
|
||||
|
||||
@@ -369,6 +380,13 @@ private:
|
||||
void flushDirtyContacts();
|
||||
void flushDirtyChannels();
|
||||
|
||||
/* Mesh time sync */
|
||||
MeshTimeSync _timesync{FIRMWARE_BUILD_EPOCH};
|
||||
void onAdvertTimeSample(const mesh::Identity &id, uint32_t timestamp,
|
||||
uint8_t hops) override;
|
||||
void applyTimeSyncStep(const MeshTimeSync::Verdict &v, uint32_t now,
|
||||
uint32_t uptime_secs);
|
||||
|
||||
/* Protocol version negotiation */
|
||||
uint8_t _app_target_ver;
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include <mesh/Utils.h>
|
||||
#include <mesh/LoRaConfig.h>
|
||||
#include <adapters/radio/LoRaRadioBase.h>
|
||||
#include <adapters/clock/ZephyrRTCDiscover.h>
|
||||
#include <adapters/gps/ZephyrGPSManager.h>
|
||||
#include <helpers/MeshcoreJson.h>
|
||||
|
||||
#include <zephyr/logging/log.h>
|
||||
@@ -217,9 +219,80 @@ DispatcherAction ObserverMesh::onRecvPacket(Packet *pkt)
|
||||
* The same flood packet heard from different repeaters is published
|
||||
* separately, each with its own SNR/RSSI (propagation data). */
|
||||
enqueuePacket(pkt);
|
||||
harvestTimeSample(pkt);
|
||||
return ACTION_RELEASE; /* never retransmit */
|
||||
}
|
||||
|
||||
/* ========== Mesh time sync ========== */
|
||||
|
||||
void ObserverMesh::harvestTimeSample(Packet *pkt)
|
||||
{
|
||||
if (pkt->getPayloadType() != PAYLOAD_TYPE_ADVERT) return;
|
||||
if (pkt->getPathHashCount() > MeshTimeSync::HOP_CAP) return;
|
||||
if (pkt->payload_len < PUB_KEY_SIZE + 4 + SIGNATURE_SIZE) return;
|
||||
|
||||
int i = 0;
|
||||
Identity id;
|
||||
memcpy(id.pub_key, &pkt->payload[i], PUB_KEY_SIZE);
|
||||
i += PUB_KEY_SIZE;
|
||||
uint32_t timestamp;
|
||||
memcpy(×tamp, &pkt->payload[i], 4);
|
||||
i += 4;
|
||||
const uint8_t *signature = &pkt->payload[i];
|
||||
i += SIGNATURE_SIZE;
|
||||
|
||||
size_t app_data_len = pkt->payload_len - (size_t)i;
|
||||
if (app_data_len > MAX_ADVERT_DATA_SIZE) app_data_len = MAX_ADVERT_DATA_SIZE;
|
||||
|
||||
uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE];
|
||||
int msg_len = 0;
|
||||
memcpy(&message[msg_len], id.pub_key, PUB_KEY_SIZE); msg_len += PUB_KEY_SIZE;
|
||||
memcpy(&message[msg_len], ×tamp, 4); msg_len += 4;
|
||||
memcpy(&message[msg_len], &pkt->payload[i], app_data_len); msg_len += app_data_len;
|
||||
|
||||
if (!id.verify(signature, message, msg_len)) return;
|
||||
|
||||
_timesync.onAdvertHeard(id.pub_key, timestamp, pkt->getPathHashCount(),
|
||||
(uint32_t)(k_uptime_get() / 1000));
|
||||
}
|
||||
|
||||
void ObserverMesh::timeSyncTick()
|
||||
{
|
||||
if (!_prefs.meshtimesync || !_rtc) return;
|
||||
|
||||
uint32_t up = (uint32_t)(k_uptime_get() / 1000);
|
||||
uint32_t now = _rtc->getCurrentTime();
|
||||
MeshTimeSync::Verdict v = _timesync.tick(now, up);
|
||||
if (v.type != MeshTimeSync::VERDICT_STEP) return;
|
||||
|
||||
/* GPS gate: sense only while GPS owns the clock. */
|
||||
if (gps_is_available() && gps_is_enabled()) {
|
||||
LOG_INF("meshtimesync: step %+d s wanted, GPS gate active - not applied",
|
||||
(int)v.delta);
|
||||
return;
|
||||
}
|
||||
applyTimeSyncStep(v, now, up);
|
||||
}
|
||||
|
||||
void ObserverMesh::noteTrustedTimeSync()
|
||||
{
|
||||
_timesync.noteManualSync((uint32_t)(k_uptime_get() / 1000));
|
||||
}
|
||||
|
||||
void ObserverMesh::applyTimeSyncStep(const MeshTimeSync::Verdict &v, uint32_t now,
|
||||
uint32_t uptime_secs)
|
||||
{
|
||||
uint32_t new_time = (uint32_t)((int64_t)now + v.delta);
|
||||
_rtc->setCurrentTime(new_time);
|
||||
zephcore_rtc_save(new_time);
|
||||
|
||||
_timesync.noteStepApplied(v.delta, new_time, uptime_secs, v.bootstrap);
|
||||
LOG_WRN("meshtimesync: stepped clock %+d s (%s, votes %u/%u) -> %u",
|
||||
(int)v.delta, v.bootstrap ? "bootstrap" : "consensus",
|
||||
(unsigned)v.consensus.votes_for, (unsigned)v.consensus.votes_against,
|
||||
(unsigned)new_time);
|
||||
}
|
||||
|
||||
/* ========== Serial CLI ========== */
|
||||
|
||||
#define CLI_REPLY_SIZE 256
|
||||
@@ -289,6 +362,12 @@ bool ObserverMesh::handleCLI(const char *command, char *reply, int reply_size)
|
||||
snprintf(reply, reply_size, "%u",
|
||||
(_creds) ? (unsigned)_creds->mqtt_port : 8883u);
|
||||
|
||||
} else if (strcmp(key, "meshtimesync") == 0) {
|
||||
_timesync.formatStatus(reply, reply_size,
|
||||
_rtc ? _rtc->getCurrentTime() : 0,
|
||||
(uint32_t)(k_uptime_get() / 1000),
|
||||
_prefs.meshtimesync != 0);
|
||||
|
||||
} else if (strcmp(key, "mqtt.tls") == 0) {
|
||||
snprintf(reply, reply_size, "%u",
|
||||
(_creds) ? (unsigned)_creds->mqtt_tls : 1u);
|
||||
@@ -392,6 +471,19 @@ bool ObserverMesh::handleCLI(const char *command, char *reply, int reply_size)
|
||||
snprintf(reply, reply_size, "ERR cr must be 5-8");
|
||||
}
|
||||
|
||||
} else if ((val = find_val(rest, "meshtimesync")) != nullptr) {
|
||||
if (strcmp(val, "on") == 0) {
|
||||
_prefs.meshtimesync = 1;
|
||||
_store->savePrefs(_prefs);
|
||||
snprintf(reply, reply_size, "meshtimesync=on");
|
||||
} else if (strcmp(val, "off") == 0) {
|
||||
_prefs.meshtimesync = 0;
|
||||
_store->savePrefs(_prefs);
|
||||
snprintf(reply, reply_size, "meshtimesync=off");
|
||||
} else {
|
||||
snprintf(reply, reply_size, "ERR must be on or off");
|
||||
}
|
||||
|
||||
} else if (!_creds) {
|
||||
snprintf(reply, reply_size, "ERR creds not initialized");
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <mesh/Identity.h>
|
||||
#include <mesh/RNG.h>
|
||||
#include <mesh/RTC.h>
|
||||
#include <helpers/MeshTimeSync.h>
|
||||
#include <helpers/NodePrefs.h>
|
||||
#include "RepeaterDataStore.h"
|
||||
#include "observer_creds.h"
|
||||
@@ -58,6 +59,13 @@ class ObserverMesh : public Dispatcher {
|
||||
void buildStatusJson(const char *status, char *out, size_t out_size);
|
||||
uint32_t _start_uptime_secs;
|
||||
|
||||
/* Mesh time sync — the observer bypasses mesh::Mesh, so it verifies
|
||||
* advert signatures itself before harvesting. */
|
||||
MeshTimeSync _timesync{FIRMWARE_BUILD_EPOCH};
|
||||
void harvestTimeSample(Packet *pkt);
|
||||
void applyTimeSyncStep(const MeshTimeSync::Verdict &v, uint32_t now,
|
||||
uint32_t uptime_secs);
|
||||
|
||||
protected:
|
||||
/* Capture RSSI + raw bytes before packet is parsed */
|
||||
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
|
||||
@@ -84,6 +92,13 @@ public:
|
||||
void publishSelfAdvert();
|
||||
void publishStatus(const char *status);
|
||||
|
||||
/* Mesh time sync: paced evaluation (bidirectional stepping — the clock
|
||||
* is only load-bearing for observation timestamps, which is exactly
|
||||
* what this fixes). Driven from the 300 s status timer. */
|
||||
void timeSyncTick();
|
||||
/* SNTP just set the clock (trusted): arm suppression + drift envelope. */
|
||||
void noteTrustedTimeSync();
|
||||
|
||||
/* Accessors used by main_observer.cpp */
|
||||
NodePrefs *getNodePrefs() { return &_prefs; }
|
||||
const LocalIdentity &getSelfId() const { return _self_id; }
|
||||
|
||||
@@ -201,6 +201,8 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
|
||||
* read leaves the constructor defaults flood_max_unscoped=64, flood_max_advert=8). */
|
||||
fs_read(&file, &prefs.flood_max_unscoped, sizeof(prefs.flood_max_unscoped));
|
||||
fs_read(&file, &prefs.flood_max_advert, sizeof(prefs.flood_max_advert));
|
||||
/* Mesh time sync (absent in <297-byte files; no-op EOF read keeps default 0 = off) */
|
||||
fs_read(&file, &prefs.meshtimesync, sizeof(prefs.meshtimesync));
|
||||
|
||||
fs_close(&file);
|
||||
|
||||
@@ -231,6 +233,7 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
|
||||
if (prefs.rx_duty_cycle > 1) prefs.rx_duty_cycle = 0;
|
||||
if (prefs.apc_enabled > 1) prefs.apc_enabled = 0;
|
||||
if (prefs.apc_margin < 6 || prefs.apc_margin > 30) prefs.apc_margin = 16;
|
||||
if (prefs.meshtimesync > 1) prefs.meshtimesync = 0;
|
||||
|
||||
/* One-time format upgrade: old files (< 294 bytes) never saved the ZephCore
|
||||
* extension fields, and stored path_hash_mode/loop_detect as zero padding.
|
||||
@@ -328,6 +331,8 @@ bool RepeaterDataStore::savePrefs(const NodePrefs& prefs) {
|
||||
/* Flood hop-ceiling extensions (extend the format past 294 bytes) */
|
||||
fs_write(&file, &prefs.flood_max_unscoped, sizeof(prefs.flood_max_unscoped));
|
||||
fs_write(&file, &prefs.flood_max_advert, sizeof(prefs.flood_max_advert));
|
||||
/* Mesh time sync on/off (offset 296) */
|
||||
fs_write(&file, &prefs.meshtimesync, sizeof(prefs.meshtimesync));
|
||||
|
||||
ret = fs_sync(&file);
|
||||
fs_close(&file);
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <adapters/sensors/SimpleLPP.h>
|
||||
#include <adapters/sensors/ZephyrEnvSensors.h>
|
||||
#include <adapters/gps/ZephyrGPSManager.h>
|
||||
#include <adapters/clock/ZephyrRTCDiscover.h>
|
||||
#include <helpers/time_sync.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/devicetree.h>
|
||||
@@ -711,6 +713,10 @@ void RepeaterMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
|
||||
const uint8_t* app_data, size_t app_data_len) {
|
||||
mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len);
|
||||
|
||||
/* Signature already verified by mesh::Mesh before this hook fires. */
|
||||
_timesync.onAdvertHeard(id.pub_key, timestamp, packet->getPathHashCount(),
|
||||
(uint32_t)(k_uptime_get() / 1000));
|
||||
|
||||
if (packet->getPathHashCount() == 0 && !isShare(packet)) {
|
||||
AdvertDataParser parser(app_data, app_data_len);
|
||||
if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) {
|
||||
@@ -1374,11 +1380,71 @@ void RepeaterMesh::loop() {
|
||||
}
|
||||
#endif
|
||||
|
||||
timeSyncTick();
|
||||
|
||||
uint32_t now = k_uptime_get();
|
||||
uptime_millis += now - last_millis;
|
||||
last_millis = now;
|
||||
}
|
||||
|
||||
void RepeaterMesh::timeSyncTick() {
|
||||
if (!_prefs.meshtimesync) return;
|
||||
|
||||
uint32_t up = (uint32_t)(k_uptime_get() / 1000);
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
MeshTimeSync::Verdict v = _timesync.tick(now, up);
|
||||
if (v.type != MeshTimeSync::VERDICT_STEP) {
|
||||
if (v.type == MeshTimeSync::VERDICT_ABSTAIN) {
|
||||
LOG_DBG("meshtimesync: abstain (%s)", MeshTimeSync::reasonStr(v.reason));
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* GPS gate: GPS sets the clock unconditionally — a wrong mesh step
|
||||
* followed by a GPS step-back would poison our own advert high-water
|
||||
* marks at peers. Sense only. */
|
||||
if (gps_is_available() && gps_is_enabled()) {
|
||||
LOG_INF("meshtimesync: step %+d s wanted, GPS gate active - not applied",
|
||||
(int)v.delta);
|
||||
return;
|
||||
}
|
||||
applyTimeSyncStep(v, now, up);
|
||||
}
|
||||
|
||||
void RepeaterMesh::applyTimeSyncStep(const MeshTimeSync::Verdict& v, uint32_t now,
|
||||
uint32_t uptime_secs) {
|
||||
uint32_t new_time = (uint32_t)((int64_t)now + v.delta);
|
||||
getRTCClock()->setCurrentTime(new_time);
|
||||
zephcore_rtc_save(new_time);
|
||||
time_sync_report(TIME_SYNC_MESH);
|
||||
|
||||
/* Wall-clock-anchored bookkeeping must move with the step, or a backward
|
||||
* step underflows the unsigned "seconds ago" math. 0 = unset sentinel. */
|
||||
#if MAX_NEIGHBOURS > 0
|
||||
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
|
||||
if (neighbours[i].heard_timestamp == 0) continue;
|
||||
int64_t shifted = (int64_t)neighbours[i].heard_timestamp + v.delta;
|
||||
neighbours[i].heard_timestamp = (shifted > 0) ? (uint32_t)shifted : 1;
|
||||
}
|
||||
#endif
|
||||
for (int i = 0; i < acl.getNumClients(); i++) {
|
||||
ClientInfo* c = acl.getClientByIdx(i);
|
||||
if (c->last_activity == 0) continue;
|
||||
int64_t shifted = (int64_t)c->last_activity + v.delta;
|
||||
c->last_activity = (shifted > 0) ? (uint32_t)shifted : 1;
|
||||
}
|
||||
/* A backward step would otherwise wedge these shut until wall-clock
|
||||
* catch-up. */
|
||||
discover_limiter.reset();
|
||||
anon_limiter.reset();
|
||||
login_fail_limiter.reset();
|
||||
|
||||
_timesync.noteStepApplied(v.delta, new_time, uptime_secs, v.bootstrap);
|
||||
LOG_WRN("meshtimesync: stepped clock %+d s (%s, votes %u/%u) -> %u",
|
||||
(int)v.delta, v.bootstrap ? "bootstrap" : "consensus",
|
||||
(unsigned)v.consensus.votes_for, (unsigned)v.consensus.votes_against,
|
||||
(unsigned)new_time);
|
||||
}
|
||||
|
||||
bool RepeaterMesh::hasPendingWork() const {
|
||||
return _mgr->getOutboundTotal() > 0;
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <mesh/SimpleMeshTables.h>
|
||||
#include <helpers/ClientACL.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
#include <helpers/MeshTimeSync.h>
|
||||
#include <helpers/RegionMap.h>
|
||||
#include <helpers/TransportKeyStore.h>
|
||||
#include <helpers/RateLimiter.h>
|
||||
@@ -112,6 +113,7 @@ class RepeaterMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
uint8_t pending_sf;
|
||||
uint8_t pending_cr;
|
||||
int matching_peer_indexes[MAX_CLIENTS];
|
||||
MeshTimeSync _timesync{FIRMWARE_BUILD_EPOCH};
|
||||
#if IS_ENABLED(CONFIG_ZEPHCORE_REPEATER_UPLINK)
|
||||
ObserverCreds _uplink_creds;
|
||||
bool _uplink_reboot_required;
|
||||
@@ -131,6 +133,8 @@ class RepeaterMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
#endif
|
||||
|
||||
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
|
||||
void timeSyncTick();
|
||||
void applyTimeSyncStep(const MeshTimeSync::Verdict& v, uint32_t now, uint32_t uptime_secs);
|
||||
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
|
||||
uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data, size_t data_len);
|
||||
uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data, size_t data_len);
|
||||
@@ -244,6 +248,10 @@ public:
|
||||
void saveIdentity(const mesh::LocalIdentity& new_id) override;
|
||||
void clearStats() override;
|
||||
|
||||
/* Mesh time sync */
|
||||
MeshTimeSync* getMeshTimeSync() override { return &_timesync; }
|
||||
void noteGPSTimeSync() { _timesync.noteGPSSync((uint32_t)(k_uptime_get() / 1000)); }
|
||||
|
||||
/* Adaptive contention window callbacks */
|
||||
float getContentionEstimate() const override {
|
||||
return getContentionTracker().getContentionEstimate();
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
#include <adapters/sensors/SimpleLPP.h>
|
||||
#include <adapters/sensors/ZephyrEnvSensors.h>
|
||||
#include <adapters/gps/ZephyrGPSManager.h>
|
||||
#include <adapters/clock/ZephyrRTCDiscover.h>
|
||||
#include <helpers/time_sync.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/devicetree.h>
|
||||
@@ -1104,11 +1106,70 @@ void RoomServerMesh::loop() {
|
||||
dirty_contacts_expiry = 0;
|
||||
}
|
||||
|
||||
timeSyncTick();
|
||||
|
||||
uint32_t now = k_uptime_get();
|
||||
uptime_millis += now - last_millis;
|
||||
last_millis = now;
|
||||
}
|
||||
|
||||
void RoomServerMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
|
||||
uint32_t timestamp, const uint8_t* app_data,
|
||||
size_t app_data_len) {
|
||||
(void)app_data; (void)app_data_len;
|
||||
/* Signature already verified by mesh::Mesh before this hook fires. */
|
||||
_timesync.onAdvertHeard(id.pub_key, timestamp, packet->getPathHashCount(),
|
||||
(uint32_t)(k_uptime_get() / 1000));
|
||||
}
|
||||
|
||||
void RoomServerMesh::timeSyncTick() {
|
||||
if (!_prefs.meshtimesync) return;
|
||||
|
||||
uint32_t up = (uint32_t)(k_uptime_get() / 1000);
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
MeshTimeSync::Verdict v = _timesync.tick(now, up);
|
||||
if (v.type != MeshTimeSync::VERDICT_STEP) return;
|
||||
|
||||
/* GPS gate: sense only while GPS owns the clock. */
|
||||
if (gps_is_available() && gps_is_enabled()) {
|
||||
LOG_INF("meshtimesync: step %+d s wanted, GPS gate active - not applied",
|
||||
(int)v.delta);
|
||||
return;
|
||||
}
|
||||
/* Forward-only role: post timestamps feed client sync_since ordering,
|
||||
* so a backward step corrupts message sync. Report, never apply. */
|
||||
if (v.delta < 0) {
|
||||
_timesync.noteBackwardSkipped();
|
||||
LOG_WRN("meshtimesync: backward step %+d s wanted - skipped (room server is forward-only)",
|
||||
(int)v.delta);
|
||||
return;
|
||||
}
|
||||
applyTimeSyncStep(v, now, up);
|
||||
}
|
||||
|
||||
void RoomServerMesh::applyTimeSyncStep(const MeshTimeSync::Verdict& v, uint32_t now,
|
||||
uint32_t uptime_secs) {
|
||||
uint32_t new_time = (uint32_t)((int64_t)now + v.delta);
|
||||
getRTCClock()->setCurrentTime(new_time);
|
||||
zephcore_rtc_save(new_time);
|
||||
time_sync_report(TIME_SYNC_MESH);
|
||||
|
||||
/* Wall-clock-anchored bookkeeping moves with the step (see RepeaterMesh). */
|
||||
for (int i = 0; i < acl.getNumClients(); i++) {
|
||||
ClientInfo* c = acl.getClientByIdx(i);
|
||||
if (c->last_activity == 0) continue;
|
||||
int64_t shifted = (int64_t)c->last_activity + v.delta;
|
||||
c->last_activity = (shifted > 0) ? (uint32_t)shifted : 1;
|
||||
}
|
||||
login_fail_limiter.reset();
|
||||
|
||||
_timesync.noteStepApplied(v.delta, new_time, uptime_secs, v.bootstrap);
|
||||
LOG_WRN("meshtimesync: stepped clock %+d s (%s, votes %u/%u) -> %u",
|
||||
(int)v.delta, v.bootstrap ? "bootstrap" : "consensus",
|
||||
(unsigned)v.consensus.votes_for, (unsigned)v.consensus.votes_against,
|
||||
(unsigned)new_time);
|
||||
}
|
||||
|
||||
bool RoomServerMesh::hasPendingWork() const {
|
||||
return _mgr->getOutboundTotal() > 0;
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <mesh/SimpleMeshTables.h>
|
||||
#include <helpers/ClientACL.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
#include <helpers/MeshTimeSync.h>
|
||||
#include <helpers/RegionMap.h>
|
||||
#include <helpers/TransportKeyStore.h>
|
||||
#include <helpers/RateLimiter.h>
|
||||
@@ -94,9 +95,12 @@ class RoomServerMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
uint8_t pending_sf;
|
||||
uint8_t pending_cr;
|
||||
int matching_peer_indexes[MAX_CLIENTS];
|
||||
MeshTimeSync _timesync{FIRMWARE_BUILD_EPOCH};
|
||||
|
||||
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
|
||||
mesh::Packet* createSelfAdvert();
|
||||
void timeSyncTick();
|
||||
void applyTimeSyncStep(const MeshTimeSync::Verdict& v, uint32_t now, uint32_t uptime_secs);
|
||||
|
||||
/* Room server: shared-post buffer + push-to-client sync */
|
||||
void addPost(ClientInfo* client, const char* postData);
|
||||
@@ -145,6 +149,7 @@ protected:
|
||||
|
||||
bool filterRecvFloodPacket(mesh::Packet* pkt) override;
|
||||
|
||||
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override;
|
||||
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
|
||||
int searchPeersByHash(const uint8_t* hash) override;
|
||||
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
|
||||
@@ -192,6 +197,11 @@ public:
|
||||
void saveIdentity(const mesh::LocalIdentity& new_id) override;
|
||||
void clearStats() override;
|
||||
|
||||
/* Mesh time sync (forward-only: post timestamps feed client sync_since
|
||||
* ordering, so a backward step would corrupt message sync) */
|
||||
MeshTimeSync* getMeshTimeSync() override { return &_timesync; }
|
||||
void noteGPSTimeSync() { _timesync.noteGPSSync((uint32_t)(k_uptime_get() / 1000)); }
|
||||
|
||||
/* Adaptive contention window callbacks */
|
||||
float getContentionEstimate() const override {
|
||||
return getContentionTracker().getContentionEstimate();
|
||||
|
||||
@@ -173,11 +173,13 @@ static void print_banner(void)
|
||||
cli_println("set sf <7-12> Spreading factor");
|
||||
cli_println("set bw <idx> Bandwidth: 3=62.5 0=125 1=250 2=500 kHz");
|
||||
cli_println("set cr <5-8> Coding rate");
|
||||
cli_println("set meshtimesync <on|off> Mesh clock consensus correction");
|
||||
cli_println("");
|
||||
cli_println("--- Query ---");
|
||||
cli_println("get wifi.status WiFi connection state");
|
||||
cli_println("get mqtt.status MQTT connection state");
|
||||
cli_println("get radio LoRa radio parameters");
|
||||
cli_println("get meshtimesync Time-sync consensus state (dry-run)");
|
||||
cli_println("help Show this screen");
|
||||
cli_println("=========================");
|
||||
}
|
||||
@@ -228,9 +230,14 @@ static void process_cli_rx(void)
|
||||
|
||||
static mesh::ZephyrRTCClock s_rtc_clock;
|
||||
|
||||
/* SNTP callback runs on the WiFi thread; the mesh time-sync module is
|
||||
* main-thread-only, so just flag the sync and let the event loop arm it. */
|
||||
static atomic_t s_sntp_synced;
|
||||
|
||||
static void time_sync_cb(uint32_t unix_ts)
|
||||
{
|
||||
s_rtc_clock.setCurrentTime(unix_ts);
|
||||
atomic_set(&s_sntp_synced, 1);
|
||||
LOG_INF("RTC synced from SNTP: %u", unix_ts);
|
||||
}
|
||||
|
||||
@@ -366,8 +373,13 @@ int main(void)
|
||||
if (ev & MESH_EVENT_CLI_RX) {
|
||||
process_cli_rx();
|
||||
}
|
||||
if (atomic_cas(&s_sntp_synced, 1, 0)) {
|
||||
/* SNTP set the clock — arm suppression + drift envelope. */
|
||||
observer_mesh.noteTrustedTimeSync();
|
||||
}
|
||||
if (ev & MESH_EVENT_STATUS) {
|
||||
observer_mesh.publishStatus("online");
|
||||
observer_mesh.timeSyncTick();
|
||||
}
|
||||
if (ev & MESH_EVENT_MQTT_CONNECT) {
|
||||
observer_mesh.publishStatus("online");
|
||||
|
||||
Reference in new issue
Block a user