diff --git a/AX25Control.cpp b/AX25Control.cpp index 50aaf5f..cdd1b27 100644 --- a/AX25Control.cpp +++ b/AX25Control.cpp @@ -30,11 +30,13 @@ const unsigned char BIT_MASK_TABLE[] = { 0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04 #define WRITE_BIT1(p,i,b) p[(i)>>3] = (b) ? (p[(i)>>3] | BIT_MASK_TABLE[(i)&7]) : (p[(i)>>3] & ~BIT_MASK_TABLE[(i)&7]) #define READ_BIT1(p,i) (p[(i)>>3] & BIT_MASK_TABLE[(i)&7]) -CAX25Control::CAX25Control(CAX25Network* network, bool trace) : +CAX25Control::CAX25Control(CAX25Network* network, bool trace, CRSSIInterpolator* rssiMapper) : m_network(network), m_trace(trace), +m_rssiMapper(rssiMapper), m_enabled(true) { + assert(rssiMapper != NULL); } CAX25Control::~CAX25Control() @@ -48,17 +50,33 @@ bool CAX25Control::writeModem(unsigned char *data, unsigned int len) if (!m_enabled) return false; - if (m_trace) - decode(data, len); + bool hasRSSI = data[0U] == TAG_RSSI; - decodeJSON("rf", data, len); + unsigned int offset = hasRSSI ? 3U : 1U; + + int rssi = 0; + if (hasRSSI) { + uint16_t raw = 0U; + raw |= (data[1U] << 8) & 0xFF00U; + raw |= (data[2U] << 0) & 0x00FFU; + + // Convert the raw RSSI to dBm + rssi = m_rssiMapper->interpolate(raw); + if (rssi != 0) + LogDebug("AX.25, raw RSSI: %u, reported RSSI: %d dBm", raw, rssi); + } + + if (m_trace) + decode(data + offset, len - offset); + + decodeJSON("rf", data + offset, len - offset, rssi); CUtils::dump(1U, "AX.25 received packet", data, len); if (m_network == NULL) return true; - return m_network->write(data, len); + return m_network->write(data + offset, len - offset); } unsigned int CAX25Control::readModem(unsigned char* data) @@ -185,7 +203,7 @@ void CAX25Control::decode(const unsigned char* data, unsigned int length) LogMessage("AX.25, %s %.*s", text.c_str(), length - n, data + n); } -void CAX25Control::decodeJSON(const char* source, const unsigned char* data, unsigned int length) +void CAX25Control::decodeJSON(const char* source, const unsigned char* data, unsigned int length, int rssi) { assert(source != NULL); assert(data != NULL); @@ -205,6 +223,9 @@ void CAX25Control::decodeJSON(const char* source, const unsigned char* data, uns decodeAddressJSON(data + 0U, text, isDigi); json["destination_cs"] = text; + if (rssi != 0) + json["rssi"] = rssi; + unsigned int n = 14U; if (more) { diff --git a/AX25Control.h b/AX25Control.h index e0219e7..3f0bd6a 100644 --- a/AX25Control.h +++ b/AX25Control.h @@ -19,6 +19,7 @@ #if !defined(AX25Control_H) #define AX25Control_H +#include "RSSIInterpolator.h" #include "AX25Network.h" #include "Defines.h" @@ -28,7 +29,7 @@ class CAX25Control { public: - CAX25Control(CAX25Network* network, bool trace); + CAX25Control(CAX25Network* network, bool trace, CRSSIInterpolator* rssiMapper); ~CAX25Control(); bool writeModem(unsigned char* data, unsigned int len); @@ -38,12 +39,13 @@ public: void enable(bool enabled); private: - CAX25Network* m_network; - bool m_trace; - bool m_enabled; + CAX25Network* m_network; + bool m_trace; + CRSSIInterpolator* m_rssiMapper; + bool m_enabled; void decode(const unsigned char* data, unsigned int length); - void decodeJSON(const char* source, const unsigned char* data, unsigned int length); + void decodeJSON(const char* source, const unsigned char* data, unsigned int length, int rssi = 0); bool decodeAddress(const unsigned char* data, std::string& text, bool isDigi = false) const; bool decodeAddressJSON(const unsigned char* data, std::string& text, bool& isDigi) const; }; diff --git a/Defines.h b/Defines.h index 3b26fd6..c0776af 100644 --- a/Defines.h +++ b/Defines.h @@ -50,6 +50,7 @@ const unsigned char TAG_HEADER = 0x00U; const unsigned char TAG_DATA = 0x01U; const unsigned char TAG_LOST = 0x02U; const unsigned char TAG_EOT = 0x03U; +const unsigned char TAG_RSSI = 0x04U; const unsigned int DSTAR_MODEM_DATA_LEN = 220U; diff --git a/FMControl.cpp b/FMControl.cpp index 58e8602..2dccf7e 100644 --- a/FMControl.cpp +++ b/FMControl.cpp @@ -43,12 +43,13 @@ const unsigned char FS_TIMEOUT_EXT = 9U; const unsigned char FS_TIMEOUT_WAIT_EXT = 10U; const unsigned char FS_HANG = 11U; -CFMControl::CFMControl(CFMNetwork* network, float txAudioGain, float rxAudioGain, bool preEmphasisOn, bool deEmphasisOn) : +CFMControl::CFMControl(CFMNetwork* network, float txAudioGain, float rxAudioGain, bool preEmphasisOn, bool deEmphasisOn, CRSSIInterpolator* rssiMapper) : m_network(network), m_txAudioGain(txAudioGain), m_rxAudioGain(rxAudioGain), m_preEmphasisOn(preEmphasisOn), m_deEmphasisOn(deEmphasisOn), +m_rssiMapper(rssiMapper), m_enabled(false), m_incomingRFAudio(1600U, "Incoming RF FM Audio"), m_preEmphasis(NULL), @@ -57,160 +58,179 @@ m_filterStage1(NULL), m_filterStage2(NULL), m_filterStage3(NULL) { - assert(txAudioGain > 0.0F); - assert(rxAudioGain > 0.0F); + assert(txAudioGain > 0.0F); + assert(rxAudioGain > 0.0F); + assert(rssiMapper != NULL); - m_preEmphasis = new CIIRDirectForm1Filter(8.315375384336983F, -7.03334621603483F,0.0F,1.0F, 0.282029168302153F,0.0F, PREEMPHASIS_GAIN_DB); - m_deEmphasis = new CIIRDirectForm1Filter(0.07708787090460224F, 0.07708787090460224F,0.0F, 1.0F, -0.8458242581907955F,0.0F, DEEMPHASIS_GAIN_DB); + m_preEmphasis = new CIIRDirectForm1Filter(8.315375384336983F, -7.03334621603483F,0.0F,1.0F, 0.282029168302153F,0.0F, PREEMPHASIS_GAIN_DB); + m_deEmphasis = new CIIRDirectForm1Filter(0.07708787090460224F, 0.07708787090460224F,0.0F, 1.0F, -0.8458242581907955F,0.0F, DEEMPHASIS_GAIN_DB); - // Chebyshev type 1 0.2dB cheby type 1 3rd order 300-2700Hz fs=8000 - m_filterStage1 = new CIIRDirectForm1Filter(0.29495028f, 0.0f, -0.29495028f, 1.0f, -0.61384624f, -0.057158668f, FILTER_GAIN_DB); - m_filterStage2 = new CIIRDirectForm1Filter(1.0f, 2.0f, 1.0f, 1.0f, 0.9946123f, 0.6050482f, FILTER_GAIN_DB); - m_filterStage3 = new CIIRDirectForm1Filter(1.0f, -2.0f, 1.0f, 1.0f, -1.8414584f, 0.8804949f, FILTER_GAIN_DB); + // Chebyshev type 1 0.2dB cheby type 1 3rd order 300-2700Hz fs=8000 + m_filterStage1 = new CIIRDirectForm1Filter(0.29495028f, 0.0f, -0.29495028f, 1.0f, -0.61384624f, -0.057158668f, FILTER_GAIN_DB); + m_filterStage2 = new CIIRDirectForm1Filter(1.0f, 2.0f, 1.0f, 1.0f, 0.9946123f, 0.6050482f, FILTER_GAIN_DB); + m_filterStage3 = new CIIRDirectForm1Filter(1.0f, -2.0f, 1.0f, 1.0f, -1.8414584f, 0.8804949f, FILTER_GAIN_DB); } CFMControl::~CFMControl() { - delete m_preEmphasis; - delete m_deEmphasis; + delete m_preEmphasis; + delete m_deEmphasis; - delete m_filterStage1; - delete m_filterStage2; - delete m_filterStage3; + delete m_filterStage1; + delete m_filterStage2; + delete m_filterStage3; } bool CFMControl::writeModem(const unsigned char* data, unsigned int length) { - assert(data != NULL); - assert(length > 0U); + assert(data != NULL); + assert(length > 0U); - if (m_network == NULL) - return true; + if (data[0U] == TAG_HEADER) { + switch (data[1U]) { + case FS_LISTENING: writeJSON("listening"); break; + case FS_KERCHUNK_RF: writeJSON("kerchunk_rf"); break; + case FS_RELAYING_RF: writeJSON("relaying_rf"); break; + case FS_RELAYING_WAIT_RF: writeJSON("relaying_wait_rf"); break; + case FS_TIMEOUT_RF: writeJSON("timeout_rf"); break; + case FS_TIMEOUT_WAIT_RF: writeJSON("timeout_wait_rf"); break; + case FS_KERCHUNK_EXT: writeJSON("kerchunk_ext"); break; + case FS_RELAYING_EXT: writeJSON("relaying_ext"); break; + case FS_RELAYING_WAIT_EXT: writeJSON("relaying_wait_ext"); break; + case FS_TIMEOUT_EXT: writeJSON("timeout_ext"); break; + case FS_TIMEOUT_WAIT_EXT: writeJSON("timeout_wait_ext"); break; + case FS_HANG: writeJSON("hang"); break; + default: writeJSON("unknown"); break; + } - if (data[0U] == TAG_HEADER) { - switch (data[1U]) { - case FS_LISTENING: writeJSON("listening"); break; - case FS_KERCHUNK_RF: writeJSON("kerchunk_rf"); break; - case FS_RELAYING_RF: writeJSON("relaying_rf"); break; - case FS_RELAYING_WAIT_RF: writeJSON("relaying_wait_rf"); break; - case FS_TIMEOUT_RF: writeJSON("timeout_rf"); break; - case FS_TIMEOUT_WAIT_RF: writeJSON("timeout_wait_rf"); break; - case FS_KERCHUNK_EXT: writeJSON("kerchunk_ext"); break; - case FS_RELAYING_EXT: writeJSON("relaying_ext"); break; - case FS_RELAYING_WAIT_EXT: writeJSON("relaying_wait_ext"); break; - case FS_TIMEOUT_EXT: writeJSON("timeout_ext"); break; - case FS_TIMEOUT_WAIT_EXT: writeJSON("timeout_wait_ext"); break; - case FS_HANG: writeJSON("hang"); break; - default: writeJSON("unknown"); break; - } + return true; + } - return true; - } + if (data[0U] == TAG_RSSI) { + uint16_t raw = 0U; + raw |= (data[0U] << 8) & 0xFF00U; + raw |= (data[1U] << 0) & 0x00FFU; - if (data[0U] == TAG_EOT) - return m_network->writeEnd(); + // Convert the raw RSSI to dBm + int rssi = m_rssiMapper->interpolate(raw); + if (rssi != 0) { + LogDebug("FM, raw RSSI: %u, reported RSSI: %d dBm", raw, rssi); + writeJSONRSSI(rssi); + } - if (data[0U] != TAG_DATA) - return false; + return true; + } - m_incomingRFAudio.addData(data + 1U, length - 1U); - unsigned int bufferLength = m_incomingRFAudio.dataSize(); - if (bufferLength > 240U) // 160 samples 12-bit - bufferLength = 240U; // 160 samples 12-bit + if (m_network == NULL) + return true; - if (bufferLength >= 3U) { - bufferLength = bufferLength - bufferLength % 3U; // Round down to nearest multiple of 3 - unsigned char bufferData[240U]; // 160 samples 12-bit - m_incomingRFAudio.getData(bufferData, bufferLength); + if (data[0U] == TAG_EOT) + return m_network->writeEnd(); - unsigned int pack = 0U; - unsigned char* packPointer = (unsigned char*)&pack; - float out[160U]; // 160 samples 12-bit - unsigned int nOut = 0U; - short unpackedSamples[2U]; + if (data[0U] != TAG_DATA) + return false; - for (unsigned int i = 0U; i < bufferLength; i += 3U) { - // Extract unsigned 12 bit unsigned sample pairs packed into 3 bytes to 16 bit signed - packPointer[0U] = bufferData[i + 0U]; - packPointer[1U] = bufferData[i + 1U]; - packPointer[2U] = bufferData[i + 2U]; + m_incomingRFAudio.addData(data + 1U, length - 1U); + unsigned int bufferLength = m_incomingRFAudio.dataSize(); - unpackedSamples[1U] = short(int(pack & FM_MASK) - 2048); - unpackedSamples[0U] = short(int(pack >> 12 & FM_MASK) - 2048); // + if (bufferLength > 240U) // 160 samples 12-bit + bufferLength = 240U; // 160 samples 12-bit - // Process unpacked sample pair - for (unsigned char j = 0U; j < 2U; j++) { - // Convert to float (-1.0 to +1.0) - float sampleFloat = (float(unpackedSamples[j]) * m_rxAudioGain) / 2048.0F; + if (bufferLength >= 3U) { + bufferLength = bufferLength - bufferLength % 3U; // Round down to nearest multiple of 3 - // De-emphasise and remove CTCSS - if (m_deEmphasisOn) - sampleFloat = m_deEmphasis->filter(sampleFloat); + unsigned char bufferData[240U]; // 160 samples 12-bit + m_incomingRFAudio.getData(bufferData, bufferLength); - out[nOut++] = m_filterStage3->filter(m_filterStage2->filter(m_filterStage1->filter(sampleFloat))); - } - } + unsigned int pack = 0U; + unsigned char* packPointer = (unsigned char*)&pack; - return m_network->writeData(out, nOut); - } + float out[160U]; // 160 samples 12-bit + unsigned int nOut = 0U; - return true; + for (unsigned int i = 0U; i < bufferLength; i += 3U) { + // Extract unsigned 12 bit unsigned sample pairs packed into 3 bytes to 16 bit signed + packPointer[0U] = bufferData[i + 0U]; + packPointer[1U] = bufferData[i + 1U]; + packPointer[2U] = bufferData[i + 2U]; + + short unpackedSamples[2U]; + unpackedSamples[1U] = short(int((pack >> 0) & FM_MASK) - 2048); + unpackedSamples[0U] = short(int((pack >> 12) & FM_MASK) - 2048); // + + // Process unpacked sample pair + for (unsigned char j = 0U; j < 2U; j++) { + // Convert to float (-1.0 to +1.0) + float sampleFloat = (float(unpackedSamples[j]) * m_rxAudioGain) / 2048.0F; + + // De-emphasise and remove CTCSS + if (m_deEmphasisOn) + sampleFloat = m_deEmphasis->filter(sampleFloat); + + out[nOut++] = m_filterStage3->filter(m_filterStage2->filter(m_filterStage1->filter(sampleFloat))); + } + } + + return m_network->writeData(out, nOut); + } + + return true; } unsigned int CFMControl::readModem(unsigned char* data, unsigned int space) { - assert(data != NULL); - assert(space > 0U); + assert(data != NULL); + assert(space > 0U); - if (m_network == NULL) - return 0U; + if (m_network == NULL) + return 0U; - if (space > 240U) // 160 samples 12-bit - space = 240U; // 160 samples 12-bit + if (space > 240U) // 160 samples 12-bit + space = 240U; // 160 samples 12-bit - float netData[160U]; // Modem can handle up to 160 samples at a time - unsigned int length = m_network->read(netData, 160U); // 160 samples 12-bit - if (length == 0U) - return 0U; + float netData[160U]; // Modem can handle up to 160 samples at a time + unsigned int length = m_network->read(netData, 160U); // 160 samples 12-bit + if (length == 0U) + return 0U; - unsigned int pack = 0U; - unsigned char* packPointer = (unsigned char*)&pack; - unsigned int nData = 0U; + unsigned int pack = 0U; + unsigned char* packPointer = (unsigned char*)&pack; + unsigned int nData = 0U; - for (unsigned int i = 0; i < length; i++) { - float sampleFloat = netData[i] * m_txAudioGain; + for (unsigned int i = 0; i < length; i++) { + float sampleFloat = netData[i] * m_txAudioGain; - // Pre-emphasis - if (m_preEmphasisOn) - sampleFloat = m_preEmphasis->filter(sampleFloat); + // Pre-emphasis + if (m_preEmphasisOn) + sampleFloat = m_preEmphasis->filter(sampleFloat); - // Convert float to 12-bit samples (0 to 4095) - unsigned int sample12bit = (unsigned int)((sampleFloat + 1.0F) * 2048.0F + 0.5F); + // Convert float to 12-bit samples (0 to 4095) + unsigned int sample12bit = (unsigned int)((sampleFloat + 1.0F) * 2048.0F + 0.5F); - // Pack 2 samples into 3 bytes - if ((i & 1U) == 0) { - pack = 0U; - pack = sample12bit << 12; - } else { - pack |= sample12bit; + // Pack 2 samples into 3 bytes + if ((i & 1U) == 0U) { + pack = 0U; + pack = sample12bit << 12; + } else { + pack |= sample12bit; - data[nData++] = packPointer[0U]; - data[nData++] = packPointer[1U]; - data[nData++] = packPointer[2U]; - } - } + data[nData++] = packPointer[0U]; + data[nData++] = packPointer[1U]; + data[nData++] = packPointer[2U]; + } + } - return nData; + return nData; } void CFMControl::clock(unsigned int ms) { - // May not be needed + // May not be needed } void CFMControl::enable(bool enabled) { - // May not be needed + // May not be needed } void CFMControl::writeJSON(const char* state) @@ -225,5 +245,15 @@ void CFMControl::writeJSON(const char* state) WriteJSON("FM", json); } +void CFMControl::writeJSONRSSI(int rssi) +{ + nlohmann::json json; + + json["timestamp"] = CUtils::createTimestamp(); + json["mode"] = "FM"; + json["value"] = rssi; + + WriteJSON("RSSI", json); +} #endif diff --git a/FMControl.h b/FMControl.h index 5ee3248..f15902f 100644 --- a/FMControl.h +++ b/FMControl.h @@ -22,6 +22,7 @@ #include "FMNetwork.h" #include "Defines.h" #include "IIRDirectForm1Filter.h" +#include "RSSIInterpolator.h" #if defined(USE_FM) @@ -34,7 +35,7 @@ class CFMControl { public: - CFMControl(CFMNetwork* network, float txAudioGain, float rxAudioGain, bool preEmphasisOn, bool deEmphasisOn); + CFMControl(CFMNetwork* network, float txAudioGain, float rxAudioGain, bool preEmphasisOn, bool deEmphasisOn, CRSSIInterpolator* rssiMapper); ~CFMControl(); bool writeModem(const unsigned char* data, unsigned int length); @@ -46,12 +47,13 @@ public: void enable(bool enabled); private: - CFMNetwork* m_network; - float m_txAudioGain; - float m_rxAudioGain; - bool m_preEmphasisOn; - bool m_deEmphasisOn; - bool m_enabled; + CFMNetwork* m_network; + float m_txAudioGain; + float m_rxAudioGain; + bool m_preEmphasisOn; + bool m_deEmphasisOn; + CRSSIInterpolator* m_rssiMapper; + bool m_enabled; CRingBuffer m_incomingRFAudio; CIIRDirectForm1Filter* m_preEmphasis; CIIRDirectForm1Filter* m_deEmphasis; @@ -60,6 +62,7 @@ private: CIIRDirectForm1Filter* m_filterStage3; void writeJSON(const char* state); + void writeJSONRSSI(int rssi); }; #endif diff --git a/MMDVMHost.cpp b/MMDVMHost.cpp index 992e8c0..6903505 100644 --- a/MMDVMHost.cpp +++ b/MMDVMHost.cpp @@ -352,7 +352,6 @@ int CMMDVMHost::run() } #endif #endif - ::LogInitialise(m_conf.getLogDisplayLevel(), m_conf.getLogMQTTLevel()); std::vector> subscriptions; @@ -878,7 +877,7 @@ int CMMDVMHost::run() LogInfo(" P-Persist: %u", pPersist); LogInfo(" Trace: %s", trace ? "yes" : "no"); - m_ax25 = new CAX25Control(m_ax25Network, trace); + m_ax25 = new CAX25Control(m_ax25Network, trace, rssi); } #endif @@ -890,7 +889,7 @@ int CMMDVMHost::run() float rxAudioGain = m_conf.getFMRXAudioGain(); m_fmRFModeHang = m_conf.getFMModeHang(); - m_fm = new CFMControl(m_fmNetwork, txAudioGain, rxAudioGain, preEmphasis, deEmphasis); + m_fm = new CFMControl(m_fmNetwork, txAudioGain, rxAudioGain, preEmphasis, deEmphasis, rssi); } #endif @@ -1119,7 +1118,7 @@ int CMMDVMHost::run() if (m_mode == MODE_IDLE || m_mode == MODE_FM) { m_ax25->writeModem(data, len); } else if (m_mode != MODE_LOCKOUT) { - LogWarning("NXDN modem data received when in mode %u", m_mode); + LogWarning("AX.25 modem data received when in mode %u", m_mode); } } #endif diff --git a/Modem.cpp b/Modem.cpp index beac1a7..920a0dd 100644 --- a/Modem.cpp +++ b/Modem.cpp @@ -97,7 +97,8 @@ const unsigned char MMDVM_POCSAG_DATA = 0x50U; #endif #if defined(USE_AX25) -const unsigned char MMDVM_AX25_DATA = 0x55U; +const unsigned char MMDVM_AX25_DATA = 0x55U; +const unsigned char MMDVM_AX25_DATA_EX = 0x56U; #endif #if defined(USE_FM) @@ -108,6 +109,7 @@ const unsigned char MMDVM_FM_PARAMS4 = 0x63U; const unsigned char MMDVM_FM_DATA = 0x65U; const unsigned char MMDVM_FM_STATUS = 0x66U; const unsigned char MMDVM_FM_EOT = 0x67U; +const unsigned char MMDVM_FM_RSSI = 0x68U; #endif const unsigned char MMDVM_ACK = 0x70U; @@ -929,6 +931,20 @@ void CModem::clock(unsigned int ms) m_rxFMData.addData(m_buffer + m_offset, m_length - m_offset); } break; + + case MMDVM_FM_RSSI: { + if(m_trace) + CUtils::dump(1U, "RX FM RSSI", m_buffer, m_length); + + unsigned int data1 = m_length - m_offset + 1U; + m_rxFMData.addData((unsigned char*)&data1, sizeof(unsigned int)); + + unsigned char data2 = TAG_RSSI; + m_rxFMData.addData(&data2, 1U); + + m_rxFMData.addData(m_buffer + m_offset, m_length - m_offset); + } + break; #endif #if defined(USE_AX25) @@ -936,8 +952,25 @@ void CModem::clock(unsigned int ms) if (m_trace) CUtils::dump(1U, "RX AX.25 Data", m_buffer, m_length); - unsigned int data = m_length - m_offset; - m_rxAX25Data.addData((unsigned char*)&data, sizeof(unsigned int)); + unsigned int data1 = m_length - m_offset + 1U; + m_rxAX25Data.addData((unsigned char*)&data1, sizeof(unsigned int)); + + unsigned char data2 = TAG_DATA; + m_rxFMData.addData(&data2, 1U); + + m_rxAX25Data.addData(m_buffer + m_offset, m_length - m_offset); + } + break; + + case MMDVM_AX25_DATA_EX: { + if (m_trace) + CUtils::dump(1U, "RX AX.25 Data Extended", m_buffer, m_length); + + unsigned int data1 = m_length - m_offset + 1U; + m_rxAX25Data.addData((unsigned char*)&data1, sizeof(unsigned int)); + + unsigned char data2 = TAG_RSSI; + m_rxFMData.addData(&data2, 1U); m_rxAX25Data.addData(m_buffer + m_offset, m_length - m_offset); } diff --git a/Version.h b/Version.h index 8521601..4281350 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20230730"; +const char* VERSION = "20230804"; #endif diff --git a/schema.json b/schema.json index 132ca03..c9257f4 100644 --- a/schema.json +++ b/schema.json @@ -198,6 +198,8 @@ }, "type": {"$ref": "#/$defs/ax25_type"}, "pid": {"$ref": "#/$defs/ax25_pid"}, + "rssi": {"$ref": "#/$defs/rssi"}, + "ber": {"$ref": "#/$defs/ber"}, "data": {"type": "string"}, "required": ["timestamp", "source", "destination", "source", "type"] },