#include "MQTTMessageBuilder.h" #include #include #include #include "MeshCore.h" int MQTTMessageBuilder::buildStatusMessage( const char* origin, const char* origin_id, const char* model, const char* firmware_version, const char* radio, const char* client_version, const char* status, const char* timestamp, char* buffer, size_t buffer_size, int battery_mv, int uptime_secs, int errors, int queue_len, int noise_floor, int tx_air_secs, int rx_air_secs, int recv_errors ) { // Use StaticJsonDocument to avoid heap fragmentation (fixed-size stack allocation) StaticJsonDocument<768> doc; // Increased size to accommodate stats JsonObject root = doc.to(); root["status"] = status; root["timestamp"] = timestamp; root["origin"] = origin; root["origin_id"] = origin_id; root["model"] = model; root["firmware_version"] = firmware_version; root["radio"] = radio; root["client_version"] = client_version; // Add stats object if any stats are provided if (battery_mv >= 0 || uptime_secs >= 0 || errors >= 0 || queue_len >= 0 || noise_floor > -999 || tx_air_secs >= 0 || rx_air_secs >= 0 || recv_errors >= 0) { JsonObject stats = root.createNestedObject("stats"); if (battery_mv >= 0) { stats["battery_mv"] = battery_mv; } if (uptime_secs >= 0) { stats["uptime_secs"] = uptime_secs; } if (errors >= 0) { stats["errors"] = errors; } if (queue_len >= 0) { stats["queue_len"] = queue_len; } if (noise_floor > -999) { stats["noise_floor"] = noise_floor; } if (tx_air_secs >= 0) { stats["tx_air_secs"] = tx_air_secs; } if (rx_air_secs >= 0) { stats["rx_air_secs"] = rx_air_secs; } if (recv_errors >= 0) { stats["recv_errors"] = recv_errors; } } size_t len = serializeJson(root, buffer, buffer_size); return (len > 0 && len < buffer_size) ? len : 0; } int MQTTMessageBuilder::buildPacketMessage( const char* origin, const char* origin_id, const char* timestamp, const char* direction, const char* time, const char* date, int len, int packet_type, const char* route, int payload_len, const char* raw, float snr, int rssi, const char* hash, const char* path, char* buffer, size_t buffer_size ) { // Use StaticJsonDocument with fixed maximum size to avoid heap fragmentation // Base JSON overhead ~200 bytes, raw hex can be up to 510 chars (255 bytes packet) // Use maximum size (2048) to handle all packet sizes without heap allocation StaticJsonDocument<2048> doc; JsonObject root = doc.to(); // Format numeric values as strings to avoid String object allocations char len_str[16]; char packet_type_str[16]; char payload_len_str[16]; char snr_str[16]; char rssi_str[16]; snprintf(len_str, sizeof(len_str), "%d", len); snprintf(packet_type_str, sizeof(packet_type_str), "%d", packet_type); snprintf(payload_len_str, sizeof(payload_len_str), "%d", payload_len); snprintf(snr_str, sizeof(snr_str), "%.1f", snr); snprintf(rssi_str, sizeof(rssi_str), "%d", rssi); root["origin"] = origin; root["origin_id"] = origin_id; root["timestamp"] = timestamp; root["type"] = "PACKET"; root["direction"] = direction; root["time"] = time; root["date"] = date; root["len"] = len_str; root["packet_type"] = packet_type_str; root["route"] = route; root["payload_len"] = payload_len_str; root["raw"] = raw; root["SNR"] = snr_str; root["RSSI"] = rssi_str; root["hash"] = hash; if (path && strlen(path) > 0) { root["path"] = path; } size_t json_len = serializeJson(root, buffer, buffer_size); return (json_len > 0 && json_len < buffer_size) ? json_len : 0; } int MQTTMessageBuilder::buildRawMessage( const char* origin, const char* origin_id, const char* timestamp, const char* raw, char* buffer, size_t buffer_size ) { // Use StaticJsonDocument to avoid heap fragmentation (fixed-size stack allocation) StaticJsonDocument<512> doc; JsonObject root = doc.to(); root["origin"] = origin; root["origin_id"] = origin_id; root["timestamp"] = timestamp; root["type"] = "RAW"; root["data"] = raw; size_t len = serializeJson(root, buffer, buffer_size); return (len > 0 && len < buffer_size) ? len : 0; } int MQTTMessageBuilder::buildPacketJSON( mesh::Packet* packet, 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 (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) char timestamp[32]; 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"); } // 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 (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"); } // Convert packet to hex // MAX_TRANS_UNIT is 255 bytes, hex = 510 chars, but allow for larger with headers char raw_hex[1024]; packetToHex(packet, raw_hex, sizeof(raw_hex)); // Get packet characteristics int packet_type = packet->getPayloadType(); const char* route_str = getRouteTypeString(packet->isRouteDirect() ? 1 : 0); // Create proper packet hash using MeshCore's calculatePacketHash method char hash_str[17]; uint8_t packet_hash[MAX_HASH_SIZE]; packet->calculatePacketHash(packet_hash); bytesToHex(packet_hash, MAX_HASH_SIZE, hash_str, sizeof(hash_str)); // Build path string for direct packets (multibyte-path: show hash count, hash size, byte length) char path_str[128] = ""; if (packet->isRouteDirect() && packet->path_len > 0) { snprintf(path_str, sizeof(path_str), "path_%dx%d_%db", (int)packet->getPathHashCount(), (int)packet->getPathHashSize(), (int)packet->getPathByteLen()); } return buildPacketMessage( origin, origin_id, timestamp, is_tx ? "tx" : "rx", time_str, date_str, packet->getRawLength(), packet_type, route_str, packet->payload_len, raw_hex, 12.5f, // SNR - using reasonable default -65, // RSSI - using reasonable default hash_str, packet->isRouteDirect() ? path_str : nullptr, buffer, buffer_size ); } int MQTTMessageBuilder::buildPacketJSONFromRaw( const uint8_t* raw_data, int raw_len, mesh::Packet* packet, bool is_tx, const char* origin, const char* origin_id, float snr, float rssi, Timezone* timezone, char* buffer, size_t buffer_size ) { 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) char timestamp[32]; 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"); } // 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 (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"); } // Convert raw radio data to hex (this includes radio headers) // MAX_TRANS_UNIT is 255 bytes, hex = 510 chars, but allow for larger with headers char raw_hex[1024]; bytesToHex(raw_data, raw_len, raw_hex, sizeof(raw_hex)); // Get packet characteristics from the parsed packet int packet_type = packet->getPayloadType(); const char* route_str = getRouteTypeString(packet->isRouteDirect() ? 1 : 0); // Create proper packet hash using MeshCore's calculatePacketHash method char hash_str[17]; uint8_t packet_hash[MAX_HASH_SIZE]; packet->calculatePacketHash(packet_hash); bytesToHex(packet_hash, MAX_HASH_SIZE, hash_str, sizeof(hash_str)); // Build path string for direct packets (multibyte-path: show hash count, hash size, byte length) char path_str[128] = ""; if (packet->isRouteDirect() && packet->path_len > 0) { snprintf(path_str, sizeof(path_str), "path_%dx%d_%db", (int)packet->getPathHashCount(), (int)packet->getPathHashSize(), (int)packet->getPathByteLen()); } return buildPacketMessage( origin, origin_id, timestamp, is_tx ? "tx" : "rx", time_str, date_str, raw_len, // Use actual raw radio data length packet_type, route_str, packet->payload_len, raw_hex, snr, // Use actual SNR from radio rssi, // Use actual RSSI from radio hash_str, packet->isRouteDirect() ? path_str : nullptr, buffer, buffer_size ); } int MQTTMessageBuilder::buildRawJSON( mesh::Packet* packet, const char* origin, const char* origin_id, Timezone* timezone, char* buffer, size_t buffer_size ) { if (!packet) return 0; // Get current device time time_t now = time(nullptr); // 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]; if (timeinfo) { strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S.000000", timeinfo); } else { strcpy(timestamp, "2024-01-01T12:00:00.000000"); } // Convert packet to hex // MAX_TRANS_UNIT is 255, so max hex size is 510 chars + null = 511 bytes char raw_hex[1024]; packetToHex(packet, raw_hex, sizeof(raw_hex)); return buildRawMessage(origin, origin_id, timestamp, raw_hex, buffer, buffer_size); } const char* MQTTMessageBuilder::getPacketTypeString(int packet_type) { switch (packet_type) { case 0: return "0"; // REQ case 1: return "1"; // RESPONSE case 2: return "2"; // TXT_MSG case 3: return "3"; // ACK case 4: return "4"; // ADVERT case 5: return "5"; // GRP_TXT case 6: return "6"; // GRP_DATA case 7: return "7"; // ANON_REQ case 8: return "8"; // PATH case 9: return "9"; // TRACE case 10: return "10"; // MULTIPART case 11: return "11"; // Type11 case 12: return "12"; // Type12 case 13: return "13"; // Type13 case 14: return "14"; // Type14 case 15: return "15"; // RAW_CUSTOM default: return "0"; } } const char* MQTTMessageBuilder::getRouteTypeString(int route_type) { switch (route_type) { case 0: return "F"; // FLOOD case 1: return "D"; // DIRECT case 2: return "T"; // TRANSPORT_DIRECT default: return "U"; // UNKNOWN } } void MQTTMessageBuilder::formatTimestamp(unsigned long timestamp, char* buffer, size_t buffer_size) { // Simplified timestamp formatting - in real implementation would use proper time snprintf(buffer, buffer_size, "2024-01-01T12:00:00.000000"); } void MQTTMessageBuilder::formatTime(unsigned long timestamp, char* buffer, size_t buffer_size) { // Simplified time formatting snprintf(buffer, buffer_size, "12:00:00"); } void MQTTMessageBuilder::formatDate(unsigned long timestamp, char* buffer, size_t buffer_size) { // Simplified date formatting snprintf(buffer, buffer_size, "01/01/2024"); } void MQTTMessageBuilder::bytesToHex(const uint8_t* data, size_t len, char* hex, size_t hex_size) { if (hex_size < len * 2 + 1) return; for (size_t i = 0; i < len; i++) { snprintf(hex + i * 2, 3, "%02X", data[i]); } hex[len * 2] = '\0'; } void MQTTMessageBuilder::packetToHex(mesh::Packet* packet, char* hex, size_t hex_size) { // Serialize full on-air/wire format using Packet::writeTo() // This includes header, transport codes (if present), path_len, path, and payload uint8_t raw_buf[512]; uint8_t raw_len = packet->writeTo(raw_buf); if (raw_len == 0 || raw_len > sizeof(raw_buf)) return; // Check if hex buffer is large enough (2 hex chars per byte + null terminator) if (hex_size < (size_t)raw_len * 2 + 1) return; // Convert serialized packet to hex bytesToHex(raw_buf, raw_len, hex, hex_size); }