From 5c12bbb7b068d1e18373f6ee9947668289730c9e Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Thu, 29 Dec 2022 15:44:00 +0000 Subject: [PATCH] Add JSON to the M17 protocol. --- Log.cpp | 7 ++ Log.h | 2 + M17Control.cpp | 202 +++++++++++++++++++++++++++++++++++++++++++++++-- M17Control.h | 8 +- Utils.cpp | 31 +++++++- Utils.h | 4 +- schema.json | 47 ++++++------ 7 files changed, 269 insertions(+), 32 deletions(-) diff --git a/Log.cpp b/Log.cpp index fee5304..6a3655c 100644 --- a/Log.cpp +++ b/Log.cpp @@ -199,3 +199,10 @@ void Log(unsigned int level, const char* fmt, ...) exit(1); } } + +void WriteJSON(const std::string& json) +{ + if (m_mqtt != NULL) + m_mqtt->publish("json", json.c_str()); +} + diff --git a/Log.h b/Log.h index 96c3f13..71a51a2 100644 --- a/Log.h +++ b/Log.h @@ -33,4 +33,6 @@ extern void Log(unsigned int level, const char* fmt, ...); extern bool LogInitialise(bool daemon, const std::string& filePath, const std::string& fileRoot, unsigned int fileLevel, unsigned int displayLevel, unsigned int mqttLevel, bool rotate); extern void LogFinalise(); +extern void WriteJSON(const std::string& json); + #endif diff --git a/M17Control.cpp b/M17Control.cpp index 79def48..5498504 100644 --- a/M17Control.cpp +++ b/M17Control.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020,2021 Jonathan Naylor, G4KLX + * Copyright (C) 2020,2021,2022 Jonathan Naylor, G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,6 +25,8 @@ #include #include +#include + const unsigned int INTERLEAVER[] = { 0U, 137U, 90U, 227U, 180U, 317U, 270U, 39U, 360U, 129U, 82U, 219U, 172U, 309U, 262U, 31U, 352U, 121U, 74U, 211U, 164U, 301U, 254U, 23U, 344U, 113U, 66U, 203U, 156U, 293U, 246U, 15U, 336U, 105U, 58U, 195U, 148U, 285U, 238U, 7U, 328U, 97U, @@ -113,10 +115,13 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) unsigned char type = data[0U]; if (type == TAG_LOST && (m_rfState == RS_RF_AUDIO || m_rfState == RS_RF_DATA_AUDIO)) { - if (m_rssi != 0U) + if (m_rssi != 0U) { LogMessage("M17, transmission lost from %s to %s, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", m_source.c_str(), m_dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); - else + writeJSON("lost", m_rfState, m_source, m_dest, float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); + } else { LogMessage("M17, transmission lost from %s to %s, %.1f seconds, BER: %.1f%%", m_source.c_str(), m_dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits)); + writeJSON("lost", m_rfState, m_source, m_dest, float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits)); + } writeEndRF(); return false; } @@ -419,10 +424,13 @@ bool CM17Control::writeModem(unsigned char* data, unsigned int len) m_network->write(netData); } - if (m_rssi != 0U) + if (m_rssi != 0U) { LogMessage("M17, received RF end of transmission from %s to %s, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", m_source.c_str(), m_dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); - else + writeJSON("end", m_rfState, m_source, m_dest, float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits)); + } else { LogMessage("M17, received RF end of transmission from %s to %s, %.1f seconds, BER: %.1f%%", m_source.c_str(), m_dest.c_str(), float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits)); + writeJSON("end", m_rfState, m_source, m_dest, float(m_rfFrames) / 25.0F, float(m_rfErrs * 100U) / float(m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); + } writeEndRF(); return true; @@ -529,14 +537,17 @@ void CM17Control::writeNetwork() case M17_DATA_TYPE_DATA: LogMessage("M17, received network data transmission from %s to %s", m_source.c_str(), m_dest.c_str()); m_netState = RS_NET_DATA; + writeJSON("header", m_netState, m_source, m_dest); break; case M17_DATA_TYPE_VOICE: LogMessage("M17, received network voice transmission from %s to %s", m_source.c_str(), m_dest.c_str()); m_netState = RS_NET_AUDIO; + writeJSON("header", m_netState, m_source, m_dest); break; case M17_DATA_TYPE_VOICE_DATA: LogMessage("M17, received network voice + data transmission from %s to %s", m_source.c_str(), m_dest.c_str()); m_netState = RS_NET_DATA_AUDIO; + writeJSON("header", m_netState, m_source, m_dest); break; default: LogMessage("M17, received network unknown transmission from %s to %s", m_source.c_str(), m_dest.c_str()); @@ -638,6 +649,7 @@ void CM17Control::writeNetwork() uint16_t fn = (netData[28U] << 8) + (netData[29U] << 0); if ((fn & 0x8000U) == 0x8000U) { LogMessage("M17, received network end of transmission from %s to %s, %.1f seconds", m_source.c_str(), m_dest.c_str(), float(m_netFrames) / 25.0F); + writeJSON("end", m_netState, m_source, m_dest, float(m_netFrames) / 25.0F); unsigned char data[M17_FRAME_LENGTH_BYTES + 2U]; @@ -691,14 +703,17 @@ bool CM17Control::processRFHeader(bool lateEntry) case M17_DATA_TYPE_DATA: LogMessage("M17, received RF%sdata transmission from %s to %s", lateEntry ? " late entry " : " ", m_source.c_str(), m_dest.c_str()); m_rfState = RS_RF_DATA; + writeJSON(lateEntry ? "late_entry" : "header", m_rfState, m_source, m_dest); break; case M17_DATA_TYPE_VOICE: LogMessage("M17, received RF%svoice transmission from %s to %s", lateEntry ? " late entry " : " ", m_source.c_str(), m_dest.c_str()); m_rfState = RS_RF_AUDIO; + writeJSON(lateEntry ? "late_entry" : "header", m_rfState, m_source, m_dest); break; case M17_DATA_TYPE_VOICE_DATA: LogMessage("M17, received RF%svoice + data transmission from %s to %s", lateEntry ? " late entry " : " ", m_source.c_str(), m_dest.c_str()); m_rfState = RS_RF_DATA_AUDIO; + writeJSON(lateEntry ? "late_entry" : "header", m_rfState, m_source, m_dest); break; default: return false; @@ -767,6 +782,7 @@ void CM17Control::clock(unsigned int ms) if (m_networkWatchdog.hasExpired()) { LogMessage("M17, network watchdog has expired, %.1f seconds", float(m_netFrames) / 25.0F); + writeJSON("lost", m_netState, m_source, m_dest, float(m_netFrames) / 25.0F); writeEndNet(); } } @@ -909,3 +925,179 @@ void CM17Control::enable(bool enabled) m_enabled = enabled; } + +void CM17Control::writeJSON(const char* action, RPT_RF_STATE state, const std::string& source, const std::string& dest) +{ + assert(action != NULL); + + nlohmann::json json; + + json["timestamp"] = CUtils::createTimestamp(); + + json["source_callsign"] = source; + json["destination_callsign"] = dest; + + json["source"] = "rf"; + json["action"] = action; + + switch (state) + { + case RS_RF_AUDIO: + json["traffic_type"] = "audio"; + break; + case RS_RF_DATA_AUDIO: + json["traffic_type"] = "audio_data"; + break; + case RS_RF_DATA: + json["traffic_type"] = "data"; + break; + default: + break; + } + + WriteJSON(json.dump()); +} + +void CM17Control::writeJSON(const char* action, RPT_RF_STATE state, const std::string& source, const std::string& dest, float duration, float ber) +{ + assert(action != NULL); + + nlohmann::json json; + + json["timestamp"] = CUtils::createTimestamp(); + + json["source_callsign"] = source; + json["destination_callsign"] = dest; + + json["source"] = "rf"; + json["action"] = action; + + switch (state) + { + case RS_RF_AUDIO: + json["traffic_type"] = "audio"; + break; + case RS_RF_DATA_AUDIO: + json["traffic_type"] = "audio_data"; + break; + case RS_RF_DATA: + json["traffic_type"] = "data"; + break; + default: + break; + } + + json["duration"] = duration; + json["ber"] = ber; + + WriteJSON(json.dump()); +} + +void CM17Control::writeJSON(const char* action, RPT_RF_STATE state, const std::string& source, const std::string& dest, float duration, float ber, float minRSSI, float maxRSSI, float aveRSSI) +{ + assert(action != NULL); + + nlohmann::json json; + + json["timestamp"] = CUtils::createTimestamp(); + + json["source_callsign"] = source; + json["destination_callsign"] = dest; + + json["source"] = "rf"; + json["action"] = action; + + switch (state) + { + case RS_RF_AUDIO: + json["traffic_type"] = "audio"; + break; + case RS_RF_DATA_AUDIO: + json["traffic_type"] = "audio_data"; + break; + case RS_RF_DATA: + json["traffic_type"] = "data"; + break; + default: + break; + } + + json["duration"] = duration; + json["ber"] = ber; + + nlohmann::json rssi; + rssi["minimumm"] = minRSSI; + rssi["maximumm"] = maxRSSI; + rssi["average"] = aveRSSI; + + json["rssi"] = rssi; + + WriteJSON(json.dump()); +} + +void CM17Control::writeJSON(const char* action, RPT_NET_STATE state, const std::string& source, const std::string& dest) +{ + assert(action != NULL); + + nlohmann::json json; + + json["timestamp"] = CUtils::createTimestamp(); + + json["source_callsign"] = source; + json["destination_callsign"] = dest; + + json["source"] = "network"; + json["action"] = action; + + switch (state) + { + case RS_NET_AUDIO: + json["traffic_type"] = "audio"; + break; + case RS_NET_DATA_AUDIO: + json["traffic_type"] = "audio_data"; + break; + case RS_NET_DATA: + json["traffic_type"] = "data"; + break; + default: + break; + } + + WriteJSON(json.dump()); +} + +void CM17Control::writeJSON(const char* action, RPT_NET_STATE state, const std::string& source, const std::string& dest, float duration) +{ + assert(action != NULL); + + nlohmann::json json; + + json["timestamp"] = CUtils::createTimestamp(); + + json["source_callsign"] = source; + json["destination_callsign"] = dest; + + json["source"] = "network"; + json["action"] = action; + + switch (state) + { + case RS_NET_AUDIO: + json["traffic_type"] = "audio"; + break; + case RS_NET_DATA_AUDIO: + json["traffic_type"] = "audio_data"; + break; + case RS_NET_DATA: + json["traffic_type"] = "data"; + break; + default: + break; + } + + json["duration"] = duration; + + WriteJSON(json.dump()); +} + diff --git a/M17Control.h b/M17Control.h index 1e06a28..36c4e0f 100644 --- a/M17Control.h +++ b/M17Control.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020,2021 by Jonathan Naylor G4KLX + * Copyright (C) 2020,2021,2022 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -103,6 +103,12 @@ private: void writeEndRF(); void writeEndNet(); + void writeJSON(const char* action, RPT_RF_STATE state, const std::string& source, const std::string& dest); + void writeJSON(const char* action, RPT_RF_STATE state, const std::string& source, const std::string& dest, float duration, float ber); + void writeJSON(const char* action, RPT_RF_STATE state, const std::string& source, const std::string& dest, float duration, float ber, float minRSSI, float maxRSSI, float aveRSSI); + void writeJSON(const char* action, RPT_NET_STATE state, const std::string& source, const std::string& dest); + void writeJSON(const char* action, RPT_NET_STATE state, const std::string& source, const std::string& dest, float duration); + bool openFile(); bool writeFile(const unsigned char* data); void closeFile(); diff --git a/Utils.cpp b/Utils.cpp index 091ebf8..4fe0e28 100644 --- a/Utils.cpp +++ b/Utils.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009,2014,2015,2016,2021 Jonathan Naylor, G4KLX + * Copyright (C) 2009,2014,2015,2016,2021,2022 Jonathan Naylor, G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,6 +17,13 @@ #include #include +#if defined(_WIN32) || defined(_WIN64) +#include +#else +#include +#include +#endif + void CUtils::dump(const std::string& title, const unsigned char* data, unsigned int length) { assert(data != NULL); @@ -170,3 +177,25 @@ void CUtils::removeChar(unsigned char * haystack, char needdle) haystack[j] = '\0'; } + +std::string CUtils::createTimestamp() +{ + char buffer[100U]; + +#if defined(_WIN32) || defined(_WIN64) + SYSTEMTIME st; + ::GetSystemTime(&st); + + ::sprintf(buffer, "%04u-%02u-%02u %02u:%02u:%02u.%03u", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); +#else + struct timeval now; + ::gettimeofday(&now, NULL); + + struct tm* tm = ::gmtime(&now.tv_sec); + + ::sprintf(buffer, "%04d-%02d-%02d %02d:%02d:%02d.%03lld", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, now.tv_usec / 1000LL); +#endif + + return buffer; +} + diff --git a/Utils.h b/Utils.h index 1e2ee9e..bcd55da 100644 --- a/Utils.h +++ b/Utils.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009,2014,2015,2021 by Jonathan Naylor, G4KLX + * Copyright (C) 2009,2014,2015,2021,2022 by Jonathan Naylor, G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -34,6 +34,8 @@ public: static void removeChar(unsigned char * haystack, char needdle); + static std::string createTimestamp(); + private: }; diff --git a/schema.json b/schema.json index 819c1fa..09fc5a0 100644 --- a/schema.json +++ b/schema.json @@ -1,6 +1,6 @@ { "$defs": { - "mmdvm_state": {"enum": ["lockout", "idle", "d-star", "dmr", "ysf", "nxdn", "pocsag", "fm", "ax,25", "m17"]), + "mmdvm_state": {"enum": ["lockout", "idle", "d-star", "dmr", "ysf", "nxdn", "pocsag", "fm", "ax,25", "m17"]}, "dstar_callsign": {"type": "string", "minLength": 8, "maxLength": 8}, "dstar_extension": {"type": "string", "minLength": 4, "maxLength": 4}, "ysf_callsign": {"type": "string", "minLength": 10, "maxLength": 10}, @@ -11,7 +11,7 @@ "nxdn_id": {"type": "integer", "minimum": 1, "maximum": 65535}, "pocsag_ric": {"type": "integer"}, "id_type": {"enum": ["group", "individual"]}, - "ysf_mode": {"enum": ["v/d_1", "v/d_2", "voice_fr", "data_fr"]}. + "ysf_mode": {"enum": ["v/d_1", "v/d_2", "voice_fr", "data_fr"]}, "ax25_type": {"enum": ["sabm", "disc", "ui", "ua", "rr", "rnr", "rej", "frmr", "i"]}, "dmr_slot": {"enum": [1, 2]}, "source": {"enum": ["rf", "network"]}, @@ -20,7 +20,7 @@ "ax25_pid": {"type": "string"}, "pocsag_source": {"enum": ["local", "network"]}, "pocsag_function": {"enum": ["numeric", "alphanumeric", "alert_1", "alert_2"]}, - "action": {"enum": ["invalid", "header", "late_entry", "end", "lost"]}, + "action": {"enum": ["invalid", "rejected", "header", "late_entry", "end", "lost"]}, "duration": {"type": "number", "minimum": 0.0}, "loss": {"type": "number", "minimum": 0.0}, "ber": {"type": "number", "minimum": 0.0}, @@ -47,9 +47,9 @@ "loss": {"$ref": "#/$defs/loss"}, "ber": {"$ref": "#/$defs/ber"}, "rssi": { - "min_rssi": {"$ref": "#/$defs/rssi"}, - "max_rssi": {"$ref": "#/$defs/rssi"}, - "ave_rssi": {"$ref": "#/$defs/rssi"} + "minimum": {"$ref": "#/$defs/rssi"}, + "maximum": {"$ref": "#/$defs/rssi"}, + "average": {"$ref": "#/$defs/rssi"} }, "required": ["timestamp", "source_callsign", "source_ext", "destination_callsign", "source", "action"] }, @@ -67,9 +67,9 @@ "loss": {"$ref": "#/$defs/loss"}, "ber": {"$ref": "#/$defs/ber"}, "rssi": { - "min_rssi": {"$ref": "#/$defs/rssi"}, - "max_rssi": {"$ref": "#/$defs/rssi"}, - "ave_rssi": {"$ref": "#/$defs/rssi"} + "minimum": {"$ref": "#/$defs/rssi"}, + "maximum": {"$ref": "#/$defs/rssi"}, + "average": {"$ref": "#/$defs/rssi"} }, "required": ["timestamp", "source_id", "destination_id", "destination_type", "slot", "source", "action"] }, @@ -86,9 +86,9 @@ "loss": {"$ref": "#/$defs/loss"}, "ber": {"$ref": "#/$defs/ber"}, "rssi": { - "min_rssi": {"$ref": "#/$defs/rssi"}, - "max_rssi": {"$ref": "#/$defs/rssi"}, - "ave_rssi": {"$ref": "#/$defs/rssi"} + "minimum": {"$ref": "#/$defs/rssi"}, + "maximum": {"$ref": "#/$defs/rssi"}, + "average": {"$ref": "#/$defs/rssi"} }, "required": ["timestamp", "source_callsign", "destination_callsign", "source", "action", "mode"] }, @@ -105,9 +105,9 @@ "loss": {"$ref": "#/$defs/loss"}, "ber": {"$ref": "#/$defs/ber"}, "rssi": { - "min_rssi": {"$ref": "#/$defs/rssi"}, - "max_rssi": {"$ref": "#/$defs/rssi"}, - "ave_rssi": {"$ref": "#/$defs/rssi"} + "minimum": {"$ref": "#/$defs/rssi"}, + "maximum": {"$ref": "#/$defs/rssi"}, + "average": {"$ref": "#/$defs/rssi"} }, "required": ["timestamp", "source_id", "destination_id", "destination_type", "source", "action"] }, @@ -124,9 +124,9 @@ "loss": {"$ref": "#/$defs/loss"}, "ber": {"$ref": "#/$defs/ber"}, "rssi": { - "min_rssi": {"$ref": "#/$defs/rssi"}, - "max_rssi": {"$ref": "#/$defs/rssi"}, - "ave_rssi": {"$ref": "#/$defs/rssi"} + "minimum": {"$ref": "#/$defs/rssi"}, + "maximum": {"$ref": "#/$defs/rssi"}, + "average": {"$ref": "#/$defs/rssi"} }, "required": ["timestamp", "source_id", "destination_id", "destination_type", "source", "action"] }, @@ -139,7 +139,7 @@ "source": {"$ref": "#/$defs/pocsag_source"}, "data": {"type": "string"}, "required": ["timestamp", "ric", "function", "source", "data"] - }. + }, "FM": { "type": "object", @@ -162,7 +162,7 @@ "pid": {"$ref": "#/$defs/ax25_pid"}, "data": {"type": "string"}, "required": ["timestamp", "source", "destination", "source", "type"] - }. + }, "M17": { "type": "object", @@ -173,12 +173,11 @@ "action": {"$ref": "#/$defs/action"}, "traffic_type": {"$ref": "#/$defs/m17_traffic_type"}, "duration": {"$ref": "#/$defs/duration"}, - "loss": {"$ref": "#/$defs/loss"}, "ber": {"$ref": "#/$defs/ber"}, "rssi": { - "min_rssi": {"$ref": "#/$defs/rssi"}, - "max_rssi": {"$ref": "#/$defs/rssi"}, - "ave_rssi": {"$ref": "#/$defs/rssi"} + "minimum": {"$ref": "#/$defs/rssi"}, + "maximum": {"$ref": "#/$defs/rssi"}, + "average": {"$ref": "#/$defs/rssi"} }, "required": ["timestamp", "source_callsign", "destination_callsign", "source", "action", "traffic_type"] }