meshtimesync p1

This commit is contained in:
liquidraver committed 2026-07-03 09:11:43 +02:00
1 parent fc940d27ba
commit 14cb8c61f9
30 files changed
+1231 -3

No files matched your search

+68
View File
@@ -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);
}
+18
View File
@@ -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;
+92
View File
@@ -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(&timestamp, &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], &timestamp, 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");
+15
View File
@@ -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; }
+5
View File
@@ -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);
+66
View 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;
}
+8
View File
@@ -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();
+61
View File
@@ -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;
}
+10
View File
@@ -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();
+12
View File
@@ -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");