Add timezone support to MQTT Bridge and CLI configuration

- Introduce timezone settings in NodePrefs for persistent storage
- Update CLI commands to get/set timezone and offset
- Modify MQTT message builder to utilize timezone for accurate timestamps
- Implement timezone handling in MQTTBridge for local time conversion
- Include timezone library dependency in platformio.ini
This commit is contained in:
agessaman committed 2025-12-21 11:26:11 -08:00
1 parent e5bb21931d
commit b47c565b0d
8 files changed
+222 -22

No files matched your search

+4
View File
@@ -733,6 +733,10 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
// WiFi defaults
StrHelper::strncpy(_prefs.wifi_ssid, "ssid_here", sizeof(_prefs.wifi_ssid));
StrHelper::strncpy(_prefs.wifi_password, "password_here", sizeof(_prefs.wifi_password));
// Timezone defaults (Pacific Time with DST support)
StrHelper::strncpy(_prefs.timezone_string, "America/Los_Angeles", sizeof(_prefs.timezone_string));
_prefs.timezone_offset = -8; // fallback
}
void MyMesh::begin(FILESYSTEM *fs) {
+19
View File
@@ -69,6 +69,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156
file.read((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157
file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy)); // 161
<<<<<<< HEAD
file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 (from upstream)
// MQTT settings - skip reading from main prefs file (now stored separately)
// For backward compatibility, we'll skip these bytes if they exist in old files
@@ -173,6 +174,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156
file.write((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157
file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161
<<<<<<< HEAD
file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 (from upstream)
// MQTT settings - no longer saved here (stored in separate /mqtt_prefs file)
// Write zeros/padding to maintain file format compatibility
@@ -403,6 +405,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
sprintf(reply, "> %s", _prefs->wifi_ssid);
} else if (memcmp(config, "wifi.pwd", 8) == 0) {
sprintf(reply, "> %s", _prefs->wifi_password);
} else if (memcmp(config, "timezone", 8) == 0) {
sprintf(reply, "> %s", _prefs->timezone_string);
} else if (memcmp(config, "timezone.offset", 15) == 0) {
sprintf(reply, "> %d", _prefs->timezone_offset);
#endif
} else {
sprintf(reply, "??: %s", config);
@@ -634,6 +640,19 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
StrHelper::strncpy(_prefs->wifi_password, &config[9], sizeof(_prefs->wifi_password));
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "timezone ", 10) == 0) {
StrHelper::strncpy(_prefs->timezone_string, &config[10], sizeof(_prefs->timezone_string));
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "timezone.offset ", 17) == 0) {
int8_t offset = _atoi(&config[17]);
if (offset >= -12 && offset <= 14) {
_prefs->timezone_offset = offset;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: timezone offset must be between -12 and +14");
}
#endif
} else {
sprintf(reply, "unknown config: %s", config);
+3
View File
@@ -96,6 +96,7 @@ struct MQTTPrefs {
// Timezone settings
char timezone_string[32]; // Timezone string (e.g., "America/Los_Angeles")
int8_t timezone_offset; // Timezone offset in hours (-12 to +14) - fallback
<<<<<<< HEAD
// MQTT server settings
char mqtt_server[64]; // MQTT server hostname
@@ -108,6 +109,8 @@ struct MQTTPrefs {
uint8_t mqtt_analyzer_eu_enabled; // Enable EU analyzer server
char mqtt_owner_public_key[65]; // Owner public key (hex string, same length as repeater public key)
char mqtt_email[64]; // Owner email address for matching nodes with owners
=======
>>>>>>> 0b761e8 (Add timezone support to MQTT Bridge and CLI configuration)
};
#endif
+22 -10
View File
@@ -1,6 +1,7 @@
#include "MQTTMessageBuilder.h"
#include <ArduinoJson.h>
#include <time.h>
#include <Timezone.h>
int MQTTMessageBuilder::buildStatusMessage(
const char* origin,
@@ -102,29 +103,36 @@ int MQTTMessageBuilder::buildPacketJSON(
bool is_tx,
const char* origin,
const char* origin_id,
Timezone* timezone,
char* buffer,
size_t buffer_size
) {
if (!packet) return 0;
// Get current device time
// Get current device time (should be UTC since system timezone is set to UTC)
time_t now = time(nullptr);
struct tm* timeinfo = localtime(&now);
// Format timestamp in ISO 8601 format
// 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)
char timestamp[32];
if (timeinfo) {
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S.000000", timeinfo);
if (local_timeinfo) {
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S.000000", local_timeinfo);
} else {
strcpy(timestamp, "2024-01-01T12:00:00.000000");
}
// Format time and date
// Get UTC time (since system timezone is UTC, time() returns UTC)
struct tm* utc_timeinfo = gmtime(&now);
// Format time and date (ALWAYS UTC)
char time_str[16];
char date_str[16];
if (timeinfo) {
strftime(time_str, sizeof(time_str), "%H:%M:%S", timeinfo);
strftime(date_str, sizeof(date_str), "%d/%m/%Y", timeinfo);
if (utc_timeinfo) {
strftime(time_str, sizeof(time_str), "%H:%M:%S", utc_timeinfo);
strftime(date_str, sizeof(date_str), "%d/%m/%Y", utc_timeinfo);
} else {
strcpy(time_str, "12:00:00");
strcpy(date_str, "01/01/2024");
@@ -169,6 +177,7 @@ int MQTTMessageBuilder::buildRawJSON(
mesh::Packet* packet,
const char* origin,
const char* origin_id,
Timezone* timezone,
char* buffer,
size_t buffer_size
) {
@@ -176,7 +185,10 @@ int MQTTMessageBuilder::buildRawJSON(
// Get current device time
time_t now = time(nullptr);
struct tm* timeinfo = localtime(&now);
// Convert to local time using timezone library
time_t local_time = timezone ? timezone->toLocal(now) : now;
struct tm* timeinfo = localtime(&local_time);
// Format timestamp in ISO 8601 format
char timestamp[32];
+3
View File
@@ -3,6 +3,7 @@
#include "MeshCore.h"
#include <ArduinoJson.h>
#include <Mesh.h>
#include <Timezone.h>
/**
* @brief Utility class for building MQTT JSON messages
@@ -122,6 +123,7 @@ public:
bool is_tx,
const char* origin,
const char* origin_id,
Timezone* timezone,
char* buffer,
size_t buffer_size
);
@@ -140,6 +142,7 @@ public:
mesh::Packet* packet,
const char* origin,
const char* origin_id,
Timezone* timezone,
char* buffer,
size_t buffer_size
);
+165 -12
View File
@@ -2,6 +2,7 @@
#include "../MQTTMessageBuilder.h"
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <Timezone.h>
#ifdef WITH_MQTT_BRIDGE
@@ -9,13 +10,14 @@ MQTTBridge::MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCCloc
: BridgeBase(prefs, mgr, rtc), _mqtt_client(nullptr), _wifi_client(nullptr),
_active_brokers(0), _queue_head(0), _queue_tail(0), _queue_count(0),
_last_status_publish(0), _status_interval(300000), // 5 minutes default
_ntp_client(_ntp_udp, "pool.ntp.org", 0, 60000), _last_ntp_sync(0), _ntp_synced(false) {
_ntp_client(_ntp_udp, "pool.ntp.org", 0, 60000), _last_ntp_sync(0), _ntp_synced(false),
_timezone(nullptr) {
// Initialize default values
strncpy(_origin, "MeshCore-Repeater", sizeof(_origin) - 1);
strncpy(_iata, "SEA", sizeof(_iata) - 1);
strncpy(_iata, "XXX", sizeof(_iata) - 1);
strncpy(_device_id, "DEVICE_ID_PLACEHOLDER", sizeof(_device_id) - 1);
strncpy(_firmware_version, "v1.9.1", sizeof(_firmware_version) - 1);
strncpy(_firmware_version, "unknown", sizeof(_firmware_version) - 1);
strncpy(_board_model, "unknown", sizeof(_board_model) - 1);
_status_enabled = true;
_packets_enabled = true;
@@ -319,7 +321,7 @@ void MQTTBridge::publishPacket(mesh::Packet* packet, bool is_tx) {
// Build packet message
int len = MQTTMessageBuilder::buildPacketJSON(
packet, is_tx, _origin, origin_id, json_buffer, sizeof(json_buffer)
packet, is_tx, _origin, origin_id, _timezone, json_buffer, sizeof(json_buffer)
);
if (len > 0) {
@@ -349,7 +351,7 @@ void MQTTBridge::publishRaw(mesh::Packet* packet) {
// Build raw message
int len = MQTTMessageBuilder::buildRawJSON(
packet, _origin, origin_id, json_buffer, sizeof(json_buffer)
packet, _origin, origin_id, _timezone, json_buffer, sizeof(json_buffer)
);
if (len > 0) {
@@ -483,7 +485,11 @@ void MQTTBridge::syncTimeWithNTP() {
if (_ntp_client.forceUpdate()) {
unsigned long epochTime = _ntp_client.getEpochTime();
// Update the device's RTC clock
// Set system timezone to UTC first
// This ensures time() returns UTC time
configTime(0, 0, "pool.ntp.org");
// Update the device's RTC clock with UTC time
if (_rtc) {
_rtc->setCurrentTime(epochTime);
_ntp_synced = true;
@@ -491,12 +497,36 @@ void MQTTBridge::syncTimeWithNTP() {
MQTT_DEBUG_PRINTLN("Time synced: %lu", epochTime);
// Show current time
struct tm* timeinfo = localtime((time_t*)&epochTime);
if (timeinfo) {
MQTT_DEBUG_PRINTLN("Current time: %04d-%02d-%02d %02d:%02d:%02d UTC",
timeinfo->tm_year + 1900, timeinfo->tm_mon + 1, timeinfo->tm_mday,
timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
// Set timezone from string (with DST support)
MQTT_DEBUG_PRINTLN("Setting timezone: %s", _prefs->timezone_string);
// Create timezone object based on timezone string
Timezone* tz = createTimezoneFromString(_prefs->timezone_string);
if (tz) {
MQTT_DEBUG_PRINTLN("Timezone created successfully");
// Store timezone for later use in message building
_timezone = tz;
} else {
MQTT_DEBUG_PRINTLN("Failed to create timezone, using UTC");
// Create UTC timezone as fallback
TimeChangeRule utc = {"UTC", Last, Sun, Mar, 0, 0};
_timezone = new Timezone(utc, utc);
}
// Show current time in both UTC and local
struct tm* utc_timeinfo = gmtime((time_t*)&epochTime);
struct tm* local_timeinfo = localtime((time_t*)&epochTime);
if (utc_timeinfo) {
MQTT_DEBUG_PRINTLN("UTC time: %04d-%02d-%02d %02d:%02d:%02d",
utc_timeinfo->tm_year + 1900, utc_timeinfo->tm_mon + 1, utc_timeinfo->tm_mday,
utc_timeinfo->tm_hour, utc_timeinfo->tm_min, utc_timeinfo->tm_sec);
}
if (local_timeinfo) {
MQTT_DEBUG_PRINTLN("Local time: %04d-%02d-%02d %02d:%02d:%02d",
local_timeinfo->tm_year + 1900, local_timeinfo->tm_mon + 1, local_timeinfo->tm_mday,
local_timeinfo->tm_hour, local_timeinfo->tm_min, local_timeinfo->tm_sec);
}
} else {
MQTT_DEBUG_PRINTLN("No RTC clock available for time sync");
@@ -508,4 +538,127 @@ void MQTTBridge::syncTimeWithNTP() {
_ntp_client.end();
}
Timezone* MQTTBridge::createTimezoneFromString(const char* tz_string) {
// Create Timezone objects for common IANA timezone strings
// Using TimeChangeRule definitions for proper DST handling
// North America
if (strcmp(tz_string, "America/Los_Angeles") == 0 || strcmp(tz_string, "America/Vancouver") == 0) {
TimeChangeRule pst = {"PST", First, Sun, Nov, 2, -480}; // UTC-8
TimeChangeRule pdt = {"PDT", Second, Sun, Mar, 2, -420}; // UTC-7
return new Timezone(pdt, pst);
} else if (strcmp(tz_string, "America/Denver") == 0) {
TimeChangeRule mst = {"MST", First, Sun, Nov, 2, -420}; // UTC-7
TimeChangeRule mdt = {"MDT", Second, Sun, Mar, 2, -360}; // UTC-6
return new Timezone(mdt, mst);
} else if (strcmp(tz_string, "America/Chicago") == 0) {
TimeChangeRule cst = {"CST", First, Sun, Nov, 2, -360}; // UTC-6
TimeChangeRule cdt = {"CDT", Second, Sun, Mar, 2, -300}; // UTC-5
return new Timezone(cdt, cst);
} else if (strcmp(tz_string, "America/New_York") == 0 || strcmp(tz_string, "America/Toronto") == 0) {
TimeChangeRule est = {"EST", First, Sun, Nov, 2, -300}; // UTC-5
TimeChangeRule edt = {"EDT", Second, Sun, Mar, 2, -240}; // UTC-4
return new Timezone(edt, est);
} else if (strcmp(tz_string, "America/Anchorage") == 0) {
TimeChangeRule akst = {"AKST", First, Sun, Nov, 2, -540}; // UTC-9
TimeChangeRule akdt = {"AKDT", Second, Sun, Mar, 2, -480}; // UTC-8
return new Timezone(akdt, akst);
} else if (strcmp(tz_string, "Pacific/Honolulu") == 0) {
TimeChangeRule hst = {"HST", Last, Sun, Oct, 2, -600}; // UTC-10 (no DST)
return new Timezone(hst, hst);
// Europe
} else if (strcmp(tz_string, "Europe/London") == 0) {
TimeChangeRule gmt = {"GMT", Last, Sun, Oct, 2, 0}; // UTC+0
TimeChangeRule bst = {"BST", Last, Sun, Mar, 1, 60}; // UTC+1
return new Timezone(bst, gmt);
} else if (strcmp(tz_string, "Europe/Paris") == 0 || strcmp(tz_string, "Europe/Berlin") == 0) {
TimeChangeRule cet = {"CET", Last, Sun, Oct, 3, 60}; // UTC+1
TimeChangeRule cest = {"CEST", Last, Sun, Mar, 2, 120}; // UTC+2
return new Timezone(cest, cet);
} else if (strcmp(tz_string, "Europe/Moscow") == 0) {
TimeChangeRule msk = {"MSK", Last, Sun, Oct, 3, 180}; // UTC+3 (no DST since 2014)
return new Timezone(msk, msk);
// Asia
} else if (strcmp(tz_string, "Asia/Tokyo") == 0) {
TimeChangeRule jst = {"JST", Last, Sun, Oct, 2, 540}; // UTC+9 (no DST)
return new Timezone(jst, jst);
} else if (strcmp(tz_string, "Asia/Shanghai") == 0 || strcmp(tz_string, "Asia/Hong_Kong") == 0) {
TimeChangeRule cst = {"CST", Last, Sun, Oct, 2, 480}; // UTC+8 (no DST)
return new Timezone(cst, cst);
} else if (strcmp(tz_string, "Asia/Kolkata") == 0) {
TimeChangeRule ist = {"IST", Last, Sun, Oct, 2, 330}; // UTC+5:30 (no DST)
return new Timezone(ist, ist);
} else if (strcmp(tz_string, "Asia/Dubai") == 0) {
TimeChangeRule gst = {"GST", Last, Sun, Oct, 2, 240}; // UTC+4 (no DST)
return new Timezone(gst, gst);
// Australia
} else if (strcmp(tz_string, "Australia/Sydney") == 0 || strcmp(tz_string, "Australia/Melbourne") == 0) {
TimeChangeRule aest = {"AEST", First, Sun, Apr, 3, 600}; // UTC+10
TimeChangeRule aedt = {"AEDT", First, Sun, Oct, 2, 660}; // UTC+11
return new Timezone(aedt, aest);
} else if (strcmp(tz_string, "Australia/Perth") == 0) {
TimeChangeRule awst = {"AWST", Last, Sun, Oct, 2, 480}; // UTC+8 (no DST)
return new Timezone(awst, awst);
// Timezone abbreviations (with DST handling)
} else if (strcmp(tz_string, "PDT") == 0 || strcmp(tz_string, "PST") == 0) {
// Pacific Time (PST/PDT)
TimeChangeRule pst = {"PST", First, Sun, Nov, 2, -480}; // UTC-8
TimeChangeRule pdt = {"PDT", Second, Sun, Mar, 2, -420}; // UTC-7
return new Timezone(pdt, pst);
} else if (strcmp(tz_string, "MDT") == 0 || strcmp(tz_string, "MST") == 0) {
// Mountain Time (MST/MDT)
TimeChangeRule mst = {"MST", First, Sun, Nov, 2, -420}; // UTC-7
TimeChangeRule mdt = {"MDT", Second, Sun, Mar, 2, -360}; // UTC-6
return new Timezone(mdt, mst);
} else if (strcmp(tz_string, "CDT") == 0 || strcmp(tz_string, "CST") == 0) {
// Central Time (CST/CDT)
TimeChangeRule cst = {"CST", First, Sun, Nov, 2, -360}; // UTC-6
TimeChangeRule cdt = {"CDT", Second, Sun, Mar, 2, -300}; // UTC-5
return new Timezone(cdt, cst);
} else if (strcmp(tz_string, "EDT") == 0 || strcmp(tz_string, "EST") == 0) {
// Eastern Time (EST/EDT)
TimeChangeRule est = {"EST", First, Sun, Nov, 2, -300}; // UTC-5
TimeChangeRule edt = {"EDT", Second, Sun, Mar, 2, -240}; // UTC-4
return new Timezone(edt, est);
} else if (strcmp(tz_string, "BST") == 0 || strcmp(tz_string, "GMT") == 0) {
// British Time (GMT/BST)
TimeChangeRule gmt = {"GMT", Last, Sun, Oct, 2, 0}; // UTC+0
TimeChangeRule bst = {"BST", Last, Sun, Mar, 1, 60}; // UTC+1
return new Timezone(bst, gmt);
} else if (strcmp(tz_string, "CEST") == 0 || strcmp(tz_string, "CET") == 0) {
// Central European Time (CET/CEST)
TimeChangeRule cet = {"CET", Last, Sun, Oct, 3, 60}; // UTC+1
TimeChangeRule cest = {"CEST", Last, Sun, Mar, 2, 120}; // UTC+2
return new Timezone(cest, cet);
// UTC and simple offsets
} else if (strcmp(tz_string, "UTC") == 0) {
TimeChangeRule utc = {"UTC", Last, Sun, Mar, 0, 0};
return new Timezone(utc, utc);
} else if (strncmp(tz_string, "UTC", 3) == 0) {
// Handle UTC+/-X format (UTC-8, UTC+5, etc.)
int offset = atoi(tz_string + 3);
TimeChangeRule utc_offset = {"UTC", Last, Sun, Mar, 0, offset * 60};
return new Timezone(utc_offset, utc_offset);
} else if (strncmp(tz_string, "GMT", 3) == 0) {
// Handle GMT+/-X format (GMT-8, GMT+5, etc.)
int offset = atoi(tz_string + 3);
TimeChangeRule gmt_offset = {"GMT", Last, Sun, Mar, 0, offset * 60};
return new Timezone(gmt_offset, gmt_offset);
} else if (strncmp(tz_string, "+", 1) == 0 || strncmp(tz_string, "-", 1) == 0) {
// Handle simple +/-X format (+5, -8, etc.)
int offset = atoi(tz_string);
TimeChangeRule offset_tz = {"TZ", Last, Sun, Mar, 0, offset * 60};
return new Timezone(offset_tz, offset_tz);
} else {
// Unknown timezone, return null
MQTT_DEBUG_PRINTLN("Unknown timezone: %s", tz_string);
return nullptr;
}
}
#endif
+5
View File
@@ -6,6 +6,7 @@
#include <WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <Timezone.h>
#if defined(MQTT_DEBUG) && defined(ARDUINO)
#include <Arduino.h>
@@ -95,6 +96,9 @@ private:
unsigned long _last_ntp_sync;
bool _ntp_synced;
// Timezone handling
Timezone* _timezone;
// Internal methods
void connectToBrokers();
void processPacketQueue();
@@ -106,6 +110,7 @@ private:
bool isAnyBrokerConnected();
void setBrokerDefaults();
void syncTimeWithNTP();
Timezone* createTimezoneFromString(const char* tz_string);
public:
/**
+1
View File
@@ -132,6 +132,7 @@ lib_deps =
knolleary/PubSubClient @ ^2.8
bblanchon/ArduinoJson @ 6.17.3
arduino-libraries/NTPClient
JChristensen/Timezone
[env:Heltec_v3_room_server]
extends = Heltec_lora32_v3