mirror of
https://github.com/agessaman/MeshCore.git
synced 2026-07-22 06:41:00 +00:00
419 lines
13 KiB
C++
419 lines
13 KiB
C++
#include "MQTTMessageBuilder.h"
|
|
#include <ArduinoJson.h>
|
|
#include <time.h>
|
|
#include <Timezone.h>
|
|
#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<JsonObject>();
|
|
|
|
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<JsonObject>();
|
|
|
|
// 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<JsonObject>();
|
|
|
|
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);
|
|
} |