Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 44 additions & 27 deletions src/helpers/MQTTMessageBuilder.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "MQTTMessageBuilder.h"
#include <ArduinoJson.h>
#include <time.h>
#include <sys/time.h>
#include <Timezone.h>
#include "MeshCore.h"

Expand Down Expand Up @@ -168,18 +169,25 @@ int MQTTMessageBuilder::buildPacketJSON(
if (!packet) return 0;

// Get current device time (should be UTC since system timezone is set to UTC)
time_t now = time(nullptr);

// Convert to local time using timezone library (for timestamp field only)
time_t local_time = timezone ? timezone->toLocal(now) : now;
struct tm* local_timeinfo = localtime(&local_time);

// Format timestamp in ISO 8601 format (LOCAL TIME)
struct timeval now_tv;
gettimeofday(&now_tv, nullptr);
time_t now = now_tv.tv_sec;

// Packet timestamp is emitted as zone-aware UTC (RFC3339 "Zulu") via gmtime — not naive
// local time. (timezone is intentionally not applied here; see the timestamp comment below.)
struct tm* utc_ts_info = gmtime(&now);

// Zone-aware UTC (RFC3339 Zulu) with a REAL sub-second from gettimeofday(). The prior ".000000"
// was a hardcoded literal carrying no information; gettimeofday() yields genuine microseconds
// (SNTP-maintained on ESP32), useful for self-hosted logging/correlation. Aggregators that only
// need second resolution (e.g. CoreScope) simply truncate the fraction. Emitting UTC with an
// explicit Z (not naive local time) also stops such aggregators from clamping the value.
char timestamp[32];
if (local_timeinfo) {
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S.000000", local_timeinfo);
if (utc_ts_info) {
size_t ts_len = strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S", utc_ts_info);
snprintf(timestamp + ts_len, sizeof(timestamp) - ts_len, ".%06ldZ", (long)now_tv.tv_usec);
} else {
strcpy(timestamp, "2024-01-01T12:00:00.000000");
strcpy(timestamp, "2024-01-01T12:00:00.000000Z");
}

// Get UTC time (since system timezone is UTC, time() returns UTC)
Expand Down Expand Up @@ -250,18 +258,25 @@ int MQTTMessageBuilder::buildPacketJSONFromRaw(
if (!packet || !raw_data || raw_len <= 0) return 0;

// Get current device time (should be UTC since system timezone is set to UTC)
time_t now = time(nullptr);

// Convert to local time using timezone library (for timestamp field only)
time_t local_time = timezone ? timezone->toLocal(now) : now;
struct tm* local_timeinfo = localtime(&local_time);

// Format timestamp in ISO 8601 format (LOCAL TIME)
struct timeval now_tv;
gettimeofday(&now_tv, nullptr);
time_t now = now_tv.tv_sec;

// Packet timestamp is emitted as zone-aware UTC (RFC3339 "Zulu") via gmtime — not naive
// local time. (timezone is intentionally not applied here; see the timestamp comment below.)
struct tm* utc_ts_info = gmtime(&now);

// Zone-aware UTC (RFC3339 Zulu) with a REAL sub-second from gettimeofday(). The prior ".000000"
// was a hardcoded literal carrying no information; gettimeofday() yields genuine microseconds
// (SNTP-maintained on ESP32), useful for self-hosted logging/correlation. Aggregators that only
// need second resolution (e.g. CoreScope) simply truncate the fraction. Emitting UTC with an
// explicit Z (not naive local time) also stops such aggregators from clamping the value.
char timestamp[32];
if (local_timeinfo) {
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S.000000", local_timeinfo);
if (utc_ts_info) {
size_t ts_len = strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S", utc_ts_info);
snprintf(timestamp + ts_len, sizeof(timestamp) - ts_len, ".%06ldZ", (long)now_tv.tv_usec);
} else {
strcpy(timestamp, "2024-01-01T12:00:00.000000");
strcpy(timestamp, "2024-01-01T12:00:00.000000Z");
}

// Get UTC time (since system timezone is UTC, time() returns UTC)
Expand Down Expand Up @@ -327,18 +342,20 @@ int MQTTMessageBuilder::buildRawJSON(
if (!packet) return 0;

// Get current device time
time_t now = time(nullptr);
struct timeval now_tv;
gettimeofday(&now_tv, nullptr);
time_t now = now_tv.tv_sec;

// Convert to local time using timezone library
time_t local_time = timezone ? timezone->toLocal(now) : now;
struct tm* timeinfo = localtime(&local_time);
// Emit zone-aware UTC (RFC3339 Zulu) — consistent with the packet builders above.
struct tm* timeinfo = gmtime(&now);

// Format timestamp in ISO 8601 format
// Format timestamp as zone-aware UTC (RFC3339 Zulu) with real sub-second from gettimeofday()
char timestamp[32];
if (timeinfo) {
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S.000000", timeinfo);
size_t ts_len = strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S", timeinfo);
snprintf(timestamp + ts_len, sizeof(timestamp) - ts_len, ".%06ldZ", (long)now_tv.tv_usec);
} else {
strcpy(timestamp, "2024-01-01T12:00:00.000000");
strcpy(timestamp, "2024-01-01T12:00:00.000000Z");
}

// Convert packet to hex
Expand Down
32 changes: 22 additions & 10 deletions src/helpers/bridges/MQTTBridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1480,12 +1480,18 @@ bool MQTTBridge::publishStatus() {
char timestamp[32];
char radio_info[64];

// Get current timestamp in ISO 8601 format
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S.000000", &timeinfo);
// Zone-aware UTC timestamp (RFC3339 "Zulu") with a real sub-second from gettimeofday(), matching
// the packet builder (MQTTMessageBuilder). The prior ".000000" literal carried no sub-second and
// (with a local TZ set) could read as naive local; gmtime() + an explicit Z keeps it unambiguous UTC.
struct timeval now_tv;
gettimeofday(&now_tv, nullptr);
time_t now_sec = now_tv.tv_sec;
struct tm* utc_info = gmtime(&now_sec);
if (utc_info) {
size_t ts_len = strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S", utc_info);
snprintf(timestamp + ts_len, sizeof(timestamp) - ts_len, ".%06ldZ", (long)now_tv.tv_usec);
} else {
strcpy(timestamp, "2024-01-01T12:00:00.000000");
strcpy(timestamp, "2024-01-01T12:00:00.000000Z");
}

// Build radio info string (freq,bw,sf,cr)
Expand Down Expand Up @@ -2497,12 +2503,18 @@ void MQTTBridge::publishStatusToAnalyzerClient(PsychicMqttClient* client, const
char timestamp[32];
char radio_info[64];

// Get current timestamp in ISO 8601 format
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S.000000", &timeinfo);
// Zone-aware UTC timestamp (RFC3339 "Zulu") with a real sub-second from gettimeofday(), matching
// the packet builder (MQTTMessageBuilder). The prior ".000000" literal carried no sub-second and
// (with a local TZ set) could read as naive local; gmtime() + an explicit Z keeps it unambiguous UTC.
struct timeval now_tv;
gettimeofday(&now_tv, nullptr);
time_t now_sec = now_tv.tv_sec;
struct tm* utc_info = gmtime(&now_sec);
if (utc_info) {
size_t ts_len = strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S", utc_info);
snprintf(timestamp + ts_len, sizeof(timestamp) - ts_len, ".%06ldZ", (long)now_tv.tv_usec);
} else {
strcpy(timestamp, "2024-01-01T12:00:00.000000");
strcpy(timestamp, "2024-01-01T12:00:00.000000Z");
}

// Build radio info string (freq,bw,sf,cr)
Expand Down