From 1120e64b684595ecfa7f79a8c54b637244d5cbfb Mon Sep 17 00:00:00 2001 From: Dave Behnke <916775+dbehnke@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:19:13 -0500 Subject: [PATCH 1/6] Implement non-blocking NNG Publisher for real-time dashboard events --- reflector/Clients.cpp | 29 ++++++++++++++----- reflector/Configure.cpp | 17 ++++++++++++ reflector/Configure.h | 2 +- reflector/Global.h | 2 ++ reflector/JsonKeys.h | 3 ++ reflector/Main.cpp | 12 ++++++-- reflector/Makefile | 2 +- reflector/NNGPublisher.cpp | 57 ++++++++++++++++++++++++++++++++++++++ reflector/NNGPublisher.h | 24 ++++++++++++++++ reflector/Users.cpp | 10 +++++++ 10 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 reflector/NNGPublisher.cpp create mode 100644 reflector/NNGPublisher.h diff --git a/reflector/Clients.cpp b/reflector/Clients.cpp index 74e7b66..e5e22c3 100644 --- a/reflector/Clients.cpp +++ b/reflector/Clients.cpp @@ -43,14 +43,11 @@ CClients::~CClients() void CClients::AddClient(std::shared_ptr client) { // first check if client already exists - for ( auto it=begin(); it!=end(); it++ ) + for ( auto it=m_Clients.begin(); it!=m_Clients.end(); it++ ) { if (*client == *(*it)) - // if found, just do nothing - // so *client keep pointing on a valid object - // on function return { - // delete new one + // if found, just do nothing return; } } @@ -63,13 +60,21 @@ void CClients::AddClient(std::shared_ptr client) std::cout << " on module " << client->GetReflectorModule(); } std::cout << std::endl; + + // dashboard event + nlohmann::json event; + event["type"] = "client_connect"; + event["callsign"] = client->GetCallsign().GetCS(); + event["ip"] = client->GetIp().GetAddress(); + event["protocol"] = client->GetProtocolName(); + event["module"] = std::string(1, client->GetReflectorModule()); + g_NNGPublisher.Publish(event); } void CClients::RemoveClient(std::shared_ptr client) { // look for the client - bool found = false; - for ( auto it=begin(); it!=end(); it++ ) + for ( auto it=m_Clients.begin(); it!=m_Clients.end(); it++ ) { // compare object pointers if ( *it == client ) @@ -84,6 +89,16 @@ void CClients::RemoveClient(std::shared_ptr client) std::cout << " on module " << (*it)->GetReflectorModule(); } std::cout << std::endl; + + // dashboard event + nlohmann::json event; + event["type"] = "client_disconnect"; + event["callsign"] = (*it)->GetCallsign().GetCS(); + event["ip"] = (*it)->GetIp().GetAddress(); + event["protocol"] = (*it)->GetProtocolName(); + event["module"] = std::string(1, (*it)->GetReflectorModule()); + g_NNGPublisher.Publish(event); + m_Clients.erase(it); break; } diff --git a/reflector/Configure.cpp b/reflector/Configure.cpp index 445e953..d391c88 100644 --- a/reflector/Configure.cpp +++ b/reflector/Configure.cpp @@ -38,6 +38,7 @@ #define JBRANDMEISTER "Brandmeister" #define JCALLSIGN "Callsign" #define JCOUNTRY "Country" +#define JDASHBOARD "Dashboard" #define JDASHBOARDURL "DashboardUrl" #define JDCS "DCS" #define JDEFAULTID "DefaultId" @@ -122,6 +123,9 @@ CConfigure::CConfigure() { IPv4RegEx = std::regex("^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3,3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9]){1,1}$", std::regex::extended); IPv6RegEx = std::regex("^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}(:[0-9a-fA-F]{1,4}){1,1}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|([0-9a-fA-F]{1,4}:){1,1}(:[0-9a-fA-F]{1,4}){1,6}|:((:[0-9a-fA-F]{1,4}){1,7}|:))$", std::regex::extended); + + data[g_Keys.dashboard.nngaddr] = "tcp://127.0.0.1:5555"; + data[g_Keys.dashboard.enable] = false; } bool CConfigure::ReadData(const std::string &path) @@ -183,6 +187,8 @@ bool CConfigure::ReadData(const std::string &path) section = ESection::ip; else if (0 == hname.compare(JTRANSCODER)) section = ESection::tc; + else if (0 == hname.compare(JDASHBOARD)) + section = ESection::dashboard; else if (0 == hname.compare(JMODULES)) section = ESection::modules; else if (0 == hname.compare(JDPLUS)) @@ -495,6 +501,14 @@ bool CConfigure::ReadData(const std::string &path) else badParam(key); break; + case ESection::dashboard: + if (0 == key.compare(JENABLE)) + data[g_Keys.dashboard.enable] = IS_TRUE(value[0]); + else if (0 == key.compare("NNGAddr")) + data[g_Keys.dashboard.nngaddr] = value; + else + badParam(key); + break; default: std::cout << "WARNING: parameter '" << line << "' defined before any [section]" << std::endl; } @@ -797,6 +811,9 @@ bool CConfigure::ReadData(const std::string &path) if (isDefined(ErrorLevel::fatal, JFILES, JG3TERMINALPATH, g_Keys.files.terminal, rval)) checkFile(JFILES, JG3TERMINALPATH, data[g_Keys.files.terminal]); } + // Dashboard section + isDefined(ErrorLevel::mild, JDASHBOARD, JENABLE, g_Keys.dashboard.enable, rval); + isDefined(ErrorLevel::mild, JDASHBOARD, "NNGAddr", g_Keys.dashboard.nngaddr, rval); return rval; } diff --git a/reflector/Configure.h b/reflector/Configure.h index b76fa3e..9396cf3 100644 --- a/reflector/Configure.h +++ b/reflector/Configure.h @@ -25,7 +25,7 @@ enum class ErrorLevel { fatal, mild }; enum class ERefreshType { file, http, both }; -enum class ESection { none, names, ip, modules, urf, dplus, dextra, dcs, g3, dmrplus, mmdvm, nxdn, bm, ysf, p25, m17, usrp, dmrid, nxdnid, ysffreq, files, tc }; +enum class ESection { none, names, ip, modules, urf, dplus, dextra, dcs, g3, dmrplus, mmdvm, nxdn, bm, ysf, p25, m17, usrp, dmrid, nxdnid, ysffreq, files, tc, dashboard }; #define IS_TRUE(a) ((a)=='t' || (a)=='T' || (a)=='1') diff --git a/reflector/Global.h b/reflector/Global.h index 0239507..e84af3c 100644 --- a/reflector/Global.h +++ b/reflector/Global.h @@ -23,6 +23,7 @@ #include "LookupYsf.h" #include "TCSocket.h" #include "JsonKeys.h" +#include "NNGPublisher.h" extern CReflector g_Reflector; extern CGateKeeper g_GateKeeper; @@ -33,3 +34,4 @@ extern CLookupNxdn g_LNid; extern CLookupYsf g_LYtr; extern SJsonKeys g_Keys; extern CTCServer g_TCServer; +extern CNNGPublisher g_NNGPublisher; diff --git a/reflector/JsonKeys.h b/reflector/JsonKeys.h index 72cfc0d..d04985c 100644 --- a/reflector/JsonKeys.h +++ b/reflector/JsonKeys.h @@ -71,4 +71,7 @@ struct SJsonKeys { struct FILES { const std::string pid, xml, json, white, black, interlink, terminal; } files { "pidFilePath", "xmlFilePath", "jsonFilePath", "whitelistFilePath", "blacklistFilePath", "interlinkFilePath", "g3TerminalFilePath" }; + + struct DASHBOARD { const std::string enable, nngaddr; } + dashboard { "DashboardEnable", "DashboardNNGAddr" }; }; diff --git a/reflector/Main.cpp b/reflector/Main.cpp index eb43f79..ac73544 100644 --- a/reflector/Main.cpp +++ b/reflector/Main.cpp @@ -33,6 +33,7 @@ CLookupDmr g_LDid; CLookupNxdn g_LNid; CLookupYsf g_LYtr; CTCServer g_TCServer; +CNNGPublisher g_NNGPublisher; //////////////////////////////////////////////////////////////////////////////////////// @@ -49,20 +50,26 @@ int main(int argc, char *argv[]) std::cout << "IPv4 binding address is '" << g_Configure.GetString(g_Keys.ip.ipv4bind) << "'" << std::endl; // remove pidfile - const std::string pidpath(g_Configure.GetString(g_Keys.files.pid)); + std::string pidpath = g_Configure.GetString(g_Keys.files.pid); const std::string callsign(g_Configure.GetString(g_Keys.names.callsign)); remove(pidpath.c_str()); // splash std::cout << "Starting " << callsign << " " << g_Version << std::endl; - // and let it run + // start everything if (g_Reflector.Start()) { std::cout << "Error starting reflector" << std::endl; return EXIT_FAILURE; } + // dashboard nng publisher + if (g_Configure.GetBoolean(g_Keys.dashboard.enable)) + { + g_NNGPublisher.Start(g_Configure.GetString(g_Keys.dashboard.nngaddr)); + } + std::cout << "Reflector " << callsign << " started and listening" << std::endl; // write new pid file @@ -72,6 +79,7 @@ int main(int argc, char *argv[]) pause(); // wait for any signal + g_NNGPublisher.Stop(); g_Reflector.Stop(); std::cout << "Reflector stopped" << std::endl; diff --git a/reflector/Makefile b/reflector/Makefile index 3116642..3dd9778 100644 --- a/reflector/Makefile +++ b/reflector/Makefile @@ -32,7 +32,7 @@ else CFLAGS = -W -Werror -std=c++17 -MMD -MD endif -LDFLAGS=-pthread -lcurl +LDFLAGS=-pthread -lcurl -lnng ifeq ($(DHT), true) LDFLAGS += -lopendht diff --git a/reflector/NNGPublisher.cpp b/reflector/NNGPublisher.cpp new file mode 100644 index 0000000..dba5e00 --- /dev/null +++ b/reflector/NNGPublisher.cpp @@ -0,0 +1,57 @@ +#include "NNGPublisher.h" +#include + +CNNGPublisher::CNNGPublisher() + : m_started(false) +{ + m_sock.id = 0; +} + +CNNGPublisher::~CNNGPublisher() +{ + Stop(); +} + +bool CNNGPublisher::Start(const std::string &addr) +{ + std::lock_guard lock(m_mutex); + if (m_started) return true; + + int rv; + if ((rv = nng_pub0_open(&m_sock)) != 0) { + std::cerr << "NNG: Failed to open pub socket: " << nng_strerror(rv) << std::endl; + return false; + } + + if ((rv = nng_listen(m_sock, addr.c_str(), nullptr, 0)) != 0) { + std::cerr << "NNG: Failed to listen on " << addr << ": " << nng_strerror(rv) << std::endl; + nng_close(m_sock); + return false; + } + + m_started = true; + std::cout << "NNG: Publisher started at " << addr << std::endl; + return true; +} + +void CNNGPublisher::Stop() +{ + std::lock_guard lock(m_mutex); + if (!m_started) return; + + nng_close(m_sock); + m_started = false; + std::cout << "NNG: Publisher stopped" << std::endl; +} + +void CNNGPublisher::Publish(const nlohmann::json &event) +{ + std::lock_guard lock(m_mutex); + if (!m_started) return; + + std::string msg = event.dump(); + int rv = nng_send(m_sock, (void *)msg.c_str(), msg.size(), NNG_FLAG_NONBLOCK); + if (rv != 0 && rv != NNG_EAGAIN) { + std::cerr << "NNG: Send error: " << nng_strerror(rv) << std::endl; + } +} diff --git a/reflector/NNGPublisher.h b/reflector/NNGPublisher.h new file mode 100644 index 0000000..478fb20 --- /dev/null +++ b/reflector/NNGPublisher.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include +#include +#include + +class CNNGPublisher +{ +public: + CNNGPublisher(); + ~CNNGPublisher(); + + bool Start(const std::string &addr); + void Stop(); + + void Publish(const nlohmann::json &event); + +private: + nng_socket m_sock; + std::mutex m_mutex; + bool m_started; +}; diff --git a/reflector/Users.cpp b/reflector/Users.cpp index 31f4591..2d655d6 100644 --- a/reflector/Users.cpp +++ b/reflector/Users.cpp @@ -64,4 +64,14 @@ void CUsers::Hearing(const CCallsign &my, const CCallsign &rpt1, const CCallsign } AddUser(heard); + + // dashboard event + nlohmann::json event; + event["type"] = "hearing"; + event["my"] = my.GetCS(); + event["ur"] = rpt1.GetCS(); + event["rpt1"] = rpt2.GetCS(); + event["rpt2"] = xlx.GetCS(); + event["module"] = std::string(1, xlx.GetCSModule()); + g_NNGPublisher.Publish(event); } From ba6a5dfcfa6a2415391f80533ffd73e6edab35a8 Mon Sep 17 00:00:00 2001 From: Dave Behnke <916775+dbehnke@users.noreply.github.com> Date: Thu, 25 Dec 2025 17:21:41 -0500 Subject: [PATCH 2/6] refactor: implement active talker state and always-on periodic NNG broadcast --- docs/nng.md | 129 +++++++++++++++++++++++++++++++++++++ docs/nng_diagram.png | Bin 0 -> 41321 bytes reflector/Configure.cpp | 5 ++ reflector/JsonKeys.h | 4 +- reflector/NNGPublisher.cpp | 9 ++- reflector/Reflector.cpp | 45 ++++++++++++- reflector/Reflector.h | 1 + 7 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 docs/nng.md create mode 100644 docs/nng_diagram.png diff --git a/docs/nng.md b/docs/nng.md new file mode 100644 index 0000000..b669a8e --- /dev/null +++ b/docs/nng.md @@ -0,0 +1,129 @@ +# NNG Event System Documentation + +This document describes the real-time event system in `urfd`, which uses NNG (nanomsg next gen) to broadcast system state and activity as JSON. + +## Architecture Overview + +The `urfd` reflector acts as an NNG **Publisher** (PUB). Any number of subscribers (e.g., a middle-tier service or dashboard) can connect as **Subscribers** (SUB) to receive the event stream. + +```mermaid +graph TD + subgraph "urfd Core" + CR["CReflector"] + CC["CClients"] + CU["CUsers"] + PS["CPacketStream"] + end + + subgraph "Publishing Layer" + NP["g_NNGPublisher"] + end + + subgraph "Network" + ADDR["tcp://0.0.0.0:5555"] + end + + subgraph "External" + MT["Middle Tier / Dashboard"] + end + + %% Internal Flows + CC -- "client_connect / client_disconnect" --> NP + CU -- "hearing (activity)" --> NP + CR -- "periodic state report" --> NP + PS -- "IsActive status" --> CR + + %% Network Flow + NP --> ADDR + ADDR -.-> MT +``` + +## Messaging Protocols + +Events are sent as serialized JSON strings. Each message contains a `type` field to identify the payload structure. + +### 1. State Broadcast (`state`) + +Sent periodically based on `DashboardInterval` (default 10s). It provides a full snapshot of the reflector's configuration and status. + +**Payload Structure:** + +```json +{ + "type": "state", + "Configure": { + "Key": "Value", + ... + }, + "Peers": [ + { + "Callsign": "XLX123", + "Modules": "ABC", + "Protocol": "D-Extra", + "ConnectTime": "2023-10-27T10:00:00Z" + } + ], + "Clients": [ + { + "Callsign": "N7TAE", + "OnModule": "A", + "Protocol": "DMR", + "ConnectTime": "2023-10-27T10:05:00Z" + } + ], + "Users": [ + { + "Callsign": "G4XYZ", + "Repeater": "GB3NB", + "OnModule": "B", + "ViaPeer": "XLX456", + "LastHeard": "2023-10-27T10:10:00Z" + } + ], + "ActiveTalkers": [ + { + "Module": "A", + "Callsign": "N7TAE" + } + ] +} +``` + +### 2. Client Connectivity (`client_connect` / `client_disconnect`) + +Triggered immediately when a client (Repeater, Hotspot, or Mobile App) links or unlinks from a module. + +**Payload Structure:** + +```json +{ + "type": "client_connect", + "callsign": "N7TAE", + "ip": "1.2.3.4", + "protocol": "DMR", + "module": "A" +} +``` + +### 3. Voice Activity (`hearing`) + +Triggered when the reflector "hears" an active transmission. This event is sent for every "tick" or heartbeat of voice activity processed by the reflector. + +**Payload Structure:** + +```json +{ + "type": "hearing", + "my": "G4XYZ", + "ur": "CQCQCQ", + "rpt1": "GB3NB", + "rpt2": "XLX123 A", + "module": "A" +} +``` + +## Middle Tier Design Considerations + +1. **Late Joining**: The `state` message is broadcast periodically to ensure a middle-tier connecting at any time (or reconnecting) can synchronize its internal state without waiting for new events. +2. **Active Talkers**: The `ActiveTalkers` array in the `state` message identifies who is currently keyed up. Real-time transitions (start/stop) are driven by the `hearing` events and the absence of such events over a timeout (typically 2-3 seconds). +3. **Deduplication**: The `state` report is a snapshot. If the middle-tier is already tracking events, it can use the `state` report to "re-base" its state and clear out stale data. diff --git a/docs/nng_diagram.png b/docs/nng_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..ec1347c7dc43a58d095680c06463fe6554c8ed61 GIT binary patch literal 41321 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 G5}E*2s9O~P literal 0 HcmV?d00001 diff --git a/reflector/Configure.cpp b/reflector/Configure.cpp index d391c88..0f2d144 100644 --- a/reflector/Configure.cpp +++ b/reflector/Configure.cpp @@ -125,7 +125,9 @@ CConfigure::CConfigure() IPv6RegEx = std::regex("^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}(:[0-9a-fA-F]{1,4}){1,1}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|([0-9a-fA-F]{1,4}:){1,1}(:[0-9a-fA-F]{1,4}){1,6}|:((:[0-9a-fA-F]{1,4}){1,7}|:))$", std::regex::extended); data[g_Keys.dashboard.nngaddr] = "tcp://127.0.0.1:5555"; + data[g_Keys.dashboard.interval] = 10U; data[g_Keys.dashboard.enable] = false; + data[g_Keys.ysf.ysfreflectordb.id] = 0U; } bool CConfigure::ReadData(const std::string &path) @@ -506,6 +508,8 @@ bool CConfigure::ReadData(const std::string &path) data[g_Keys.dashboard.enable] = IS_TRUE(value[0]); else if (0 == key.compare("NNGAddr")) data[g_Keys.dashboard.nngaddr] = value; + else if (0 == key.compare("Interval")) + data[g_Keys.dashboard.interval] = getUnsigned(value, "Dashboard Interval", 1, 3600, 10); else badParam(key); break; @@ -814,6 +818,7 @@ bool CConfigure::ReadData(const std::string &path) // Dashboard section isDefined(ErrorLevel::mild, JDASHBOARD, JENABLE, g_Keys.dashboard.enable, rval); isDefined(ErrorLevel::mild, JDASHBOARD, "NNGAddr", g_Keys.dashboard.nngaddr, rval); + isDefined(ErrorLevel::mild, JDASHBOARD, "Interval", g_Keys.dashboard.interval, rval); return rval; } diff --git a/reflector/JsonKeys.h b/reflector/JsonKeys.h index d04985c..09f2ac1 100644 --- a/reflector/JsonKeys.h +++ b/reflector/JsonKeys.h @@ -72,6 +72,6 @@ struct SJsonKeys { struct FILES { const std::string pid, xml, json, white, black, interlink, terminal; } files { "pidFilePath", "xmlFilePath", "jsonFilePath", "whitelistFilePath", "blacklistFilePath", "interlinkFilePath", "g3TerminalFilePath" }; - struct DASHBOARD { const std::string enable, nngaddr; } - dashboard { "DashboardEnable", "DashboardNNGAddr" }; + struct DASHBOARD { const std::string enable, nngaddr, interval; } + dashboard { "DashboardEnable", "DashboardNNGAddr", "DashboardInterval" }; }; diff --git a/reflector/NNGPublisher.cpp b/reflector/NNGPublisher.cpp index dba5e00..5c02ab2 100644 --- a/reflector/NNGPublisher.cpp +++ b/reflector/NNGPublisher.cpp @@ -49,9 +49,16 @@ void CNNGPublisher::Publish(const nlohmann::json &event) std::lock_guard lock(m_mutex); if (!m_started) return; + if (m_sock.id == 0) { + std::cerr << "NNG debug: Cannot publish, socket not initialized." << std::endl; + return; + } std::string msg = event.dump(); + std::cout << "NNG debug: Attempting to publish message of size " << msg.size() << ": " << msg << std::endl; int rv = nng_send(m_sock, (void *)msg.c_str(), msg.size(), NNG_FLAG_NONBLOCK); - if (rv != 0 && rv != NNG_EAGAIN) { + if (rv == 0) { + std::cout << "NNG: Published event: " << event["type"] << std::endl; + } else if (rv != NNG_EAGAIN) { std::cerr << "NNG: Send error: " << nng_strerror(rv) << std::endl; } } diff --git a/reflector/Reflector.cpp b/reflector/Reflector.cpp index 4c19087..5ffec10 100644 --- a/reflector/Reflector.cpp +++ b/reflector/Reflector.cpp @@ -340,9 +340,13 @@ void CReflector::MaintenanceThread() if (g_Configure.Contains(g_Keys.files.json)) jsonpath.assign(g_Configure.GetString(g_Keys.files.json)); auto tcport = g_Configure.GetUnsigned(g_Keys.tc.port); - - if (xmlpath.empty() && jsonpath.empty()) + if (xmlpath.empty() && jsonpath.empty() && !g_Configure.GetBoolean(g_Keys.dashboard.enable)) + { return; // nothing to do + } + + unsigned int nngInterval = g_Configure.GetUnsigned(g_Keys.dashboard.interval); + unsigned int nngCounter = 0; while (keep_running) { @@ -383,6 +387,20 @@ void CReflector::MaintenanceThread() // and wait a bit and do something useful at the same time for (int i=0; i< XML_UPDATE_PERIOD*10 && keep_running; i++) { + // NNG periodic state update + if (g_Configure.GetBoolean(g_Keys.dashboard.enable)) + { + if (++nngCounter >= (nngInterval * 10)) + { + nngCounter = 0; + std::cout << "NNG debug: Periodic state broadcast..." << std::endl; + nlohmann::json state; + state["type"] = "state"; + JsonReport(state); + g_NNGPublisher.Publish(state); + } + } + if (tcport && g_TCServer.AnyAreClosed()) { if (g_TCServer.Accept()) @@ -391,6 +409,7 @@ void CReflector::MaintenanceThread() abort(); } } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } @@ -408,6 +427,16 @@ std::shared_ptr CReflector::GetStream(char module) return nullptr; } +bool CReflector::IsAnyStreamOpen() +{ + for (auto it=m_Stream.begin(); it!=m_Stream.end(); it++) + { + if ( it->second->IsOpen() ) + return true; + } + return false; +} + bool CReflector::IsStreamOpen(const std::unique_ptr &DvHeader) { for (auto it=m_Stream.begin(); it!=m_Stream.end(); it++) @@ -456,6 +485,18 @@ void CReflector::JsonReport(nlohmann::json &report) for (auto uid=users->begin(); uid!=users->end(); uid++) (*uid).JsonReport(report); ReleaseUsers(); + + report["ActiveTalkers"] = nlohmann::json::array(); + for (auto const& [module, stream] : m_Stream) + { + if (stream->IsOpen()) + { + nlohmann::json jactive; + jactive["Module"] = std::string(1, module); + jactive["Callsign"] = stream->GetUserCallsign().GetCS(); + report["ActiveTalkers"].push_back(jactive); + } + } } void CReflector::WriteXmlFile(std::ofstream &xmlFile) diff --git a/reflector/Reflector.h b/reflector/Reflector.h index dd260f3..52969f5 100644 --- a/reflector/Reflector.h +++ b/reflector/Reflector.h @@ -92,6 +92,7 @@ protected: // streams std::shared_ptr GetStream(char); + bool IsAnyStreamOpen(void); bool IsStreamOpen(const std::unique_ptr &); char GetStreamModule(std::shared_ptr); From d02ebe31b0dd4bff7df138b709955df62dd20c67 Mon Sep 17 00:00:00 2001 From: Dave Behnke <916775+dbehnke@users.noreply.github.com> Date: Thu, 25 Dec 2025 19:54:59 -0500 Subject: [PATCH 3/6] feat(nng): add protocol field to hearing message --- .gitignore | 6 ++++++ docs/nng.md | 3 ++- reflector/BMProtocol.cpp | 2 +- reflector/DCSProtocol.cpp | 2 +- reflector/DExtraProtocol.cpp | 2 +- reflector/DMRMMDVMProtocol.cpp | 2 +- reflector/DMRPlusProtocol.cpp | 2 +- reflector/DPlusProtocol.cpp | 2 +- reflector/G3Protocol.cpp | 2 +- reflector/M17Protocol.cpp | 2 +- reflector/NXDNProtocol.cpp | 2 +- reflector/P25Protocol.cpp | 2 +- reflector/URFProtocol.cpp | 2 +- reflector/USRPProtocol.cpp | 2 +- reflector/Users.cpp | 7 ++++--- reflector/Users.h | 4 ++-- reflector/YSFProtocol.cpp | 2 +- 17 files changed, 27 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 949061f..b081193 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,9 @@ reflector/urfd.* urfd inicheck dbutil +.devcontainer/ +/test_urfd.ini +/staging_urfd.ini +/pr_comment_nng.md +/pr_body_fix.md +/staging/ diff --git a/docs/nng.md b/docs/nng.md index b669a8e..9c078ac 100644 --- a/docs/nng.md +++ b/docs/nng.md @@ -118,7 +118,8 @@ Triggered when the reflector "hears" an active transmission. This event is sent "ur": "CQCQCQ", "rpt1": "GB3NB", "rpt2": "XLX123 A", - "module": "A" + "module": "A", + "protocol": "M17" } ``` diff --git a/reflector/BMProtocol.cpp b/reflector/BMProtocol.cpp index 3f211b8..92942d1 100644 --- a/reflector/BMProtocol.cpp +++ b/reflector/BMProtocol.cpp @@ -368,7 +368,7 @@ void CBMProtocol::OnDvHeaderPacketIn(std::unique_ptr &Header, c // release g_Reflector.ReleaseClients(); // update last heard - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, peer); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, peer, EProtocol::bm); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/DCSProtocol.cpp b/reflector/DCSProtocol.cpp index 29dc576..0ea47f4 100644 --- a/reflector/DCSProtocol.cpp +++ b/reflector/DCSProtocol.cpp @@ -208,7 +208,7 @@ void CDcsProtocol::OnDvHeaderPacketIn(std::unique_ptr &Header, g_Reflector.ReleaseClients(); // update last heard - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::dcs); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/DExtraProtocol.cpp b/reflector/DExtraProtocol.cpp index 88ed7ef..c698b26 100644 --- a/reflector/DExtraProtocol.cpp +++ b/reflector/DExtraProtocol.cpp @@ -351,7 +351,7 @@ void CDextraProtocol::OnDvHeaderPacketIn(std::unique_ptr &Heade g_Reflector.ReleaseClients(); // update last heard - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::dextra); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/DMRMMDVMProtocol.cpp b/reflector/DMRMMDVMProtocol.cpp index 5f10ba7..f2b201e 100644 --- a/reflector/DMRMMDVMProtocol.cpp +++ b/reflector/DMRMMDVMProtocol.cpp @@ -335,7 +335,7 @@ void CDmrmmdvmProtocol::OnDvHeaderPacketIn(std::unique_ptr &Hea // update last heard if ( lastheard ) { - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::dmrmmdvm); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/DMRPlusProtocol.cpp b/reflector/DMRPlusProtocol.cpp index fefc957..d257eb9 100644 --- a/reflector/DMRPlusProtocol.cpp +++ b/reflector/DMRPlusProtocol.cpp @@ -208,7 +208,7 @@ void CDmrplusProtocol::OnDvHeaderPacketIn(std::unique_ptr &Head // release g_Reflector.ReleaseClients(); // update last heard - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::dmrplus); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/DPlusProtocol.cpp b/reflector/DPlusProtocol.cpp index 14682fe..24b819d 100644 --- a/reflector/DPlusProtocol.cpp +++ b/reflector/DPlusProtocol.cpp @@ -213,7 +213,7 @@ void CDplusProtocol::OnDvHeaderPacketIn(std::unique_ptr &Header g_Reflector.ReleaseClients(); // update last heard - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::dplus); g_Reflector.ReleaseUsers(); } else diff --git a/reflector/G3Protocol.cpp b/reflector/G3Protocol.cpp index 8d5e24b..97c5b47 100644 --- a/reflector/G3Protocol.cpp +++ b/reflector/G3Protocol.cpp @@ -570,7 +570,7 @@ void CG3Protocol::OnDvHeaderPacketIn(std::unique_ptr &Header, c } // update last heard - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::g3); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/M17Protocol.cpp b/reflector/M17Protocol.cpp index d51dfbd..be8c634 100644 --- a/reflector/M17Protocol.cpp +++ b/reflector/M17Protocol.cpp @@ -209,7 +209,7 @@ void CM17Protocol::OnDvHeaderPacketIn(std::unique_ptr &Header, g_Reflector.ReleaseClients(); // update last heard - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::m17); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/NXDNProtocol.cpp b/reflector/NXDNProtocol.cpp index e5d2b0e..7c7b832 100644 --- a/reflector/NXDNProtocol.cpp +++ b/reflector/NXDNProtocol.cpp @@ -235,7 +235,7 @@ void CNXDNProtocol::OnDvHeaderPacketIn(std::unique_ptr &Header, // update last heard if ( g_Reflector.IsValidModule(rpt2.GetCSModule()) ) { - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::nxdn); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/P25Protocol.cpp b/reflector/P25Protocol.cpp index 0c52204..919fc66 100644 --- a/reflector/P25Protocol.cpp +++ b/reflector/P25Protocol.cpp @@ -219,7 +219,7 @@ void CP25Protocol::OnDvHeaderPacketIn(std::unique_ptr &Header, g_Reflector.ReleaseClients(); // update last heard - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::p25); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/URFProtocol.cpp b/reflector/URFProtocol.cpp index 6a5bf3f..e9d7fcb 100644 --- a/reflector/URFProtocol.cpp +++ b/reflector/URFProtocol.cpp @@ -411,7 +411,7 @@ void CURFProtocol::OnDvHeaderPacketIn(std::unique_ptr &Header, // release g_Reflector.ReleaseClients(); // update last heard - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, peer); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, peer, EProtocol::urf); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/USRPProtocol.cpp b/reflector/USRPProtocol.cpp index fe794ae..4fa58c2 100644 --- a/reflector/USRPProtocol.cpp +++ b/reflector/USRPProtocol.cpp @@ -225,7 +225,7 @@ void CUSRPProtocol::OnDvHeaderPacketIn(std::unique_ptr &Header, g_Reflector.ReleaseClients(); // update last heard - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::usrp); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/Users.cpp b/reflector/Users.cpp index 2d655d6..1284d7a 100644 --- a/reflector/Users.cpp +++ b/reflector/Users.cpp @@ -44,12 +44,12 @@ void CUsers::AddUser(const CUser &user) //////////////////////////////////////////////////////////////////////////////////////// // operation -void CUsers::Hearing(const CCallsign &my, const CCallsign &rpt1, const CCallsign &rpt2) +void CUsers::Hearing(const CCallsign &my, const CCallsign &rpt1, const CCallsign &rpt2, EProtocol protocol) { - Hearing(my, rpt1, rpt2, g_Reflector.GetCallsign()); + Hearing(my, rpt1, rpt2, g_Reflector.GetCallsign(), protocol); } -void CUsers::Hearing(const CCallsign &my, const CCallsign &rpt1, const CCallsign &rpt2, const CCallsign &xlx) +void CUsers::Hearing(const CCallsign &my, const CCallsign &rpt1, const CCallsign &rpt2, const CCallsign &xlx, EProtocol protocol) { CUser heard(my, rpt1, rpt2, xlx); @@ -73,5 +73,6 @@ void CUsers::Hearing(const CCallsign &my, const CCallsign &rpt1, const CCallsign event["rpt1"] = rpt2.GetCS(); event["rpt2"] = xlx.GetCS(); event["module"] = std::string(1, xlx.GetCSModule()); + event["protocol"] = g_GateKeeper.ProtocolName(protocol); g_NNGPublisher.Publish(event); } diff --git a/reflector/Users.h b/reflector/Users.h index da8a680..0873317 100644 --- a/reflector/Users.h +++ b/reflector/Users.h @@ -47,8 +47,8 @@ public: std::list::const_iterator cend() { return m_Users.cend(); } // operation - void Hearing(const CCallsign &, const CCallsign &, const CCallsign &); - void Hearing(const CCallsign &, const CCallsign &, const CCallsign &, const CCallsign &); + void Hearing(const CCallsign &, const CCallsign &, const CCallsign &, EProtocol protocol = EProtocol::none); + void Hearing(const CCallsign &, const CCallsign &, const CCallsign &, const CCallsign &, EProtocol protocol); protected: // data diff --git a/reflector/YSFProtocol.cpp b/reflector/YSFProtocol.cpp index 0795db0..ea8e8c4 100644 --- a/reflector/YSFProtocol.cpp +++ b/reflector/YSFProtocol.cpp @@ -293,7 +293,7 @@ void CYsfProtocol::OnDvHeaderPacketIn(std::unique_ptr &Header, // update last heard if ( g_Reflector.IsValidModule(rpt2.GetCSModule()) ) { - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::ysf); g_Reflector.ReleaseUsers(); } } From 0f96f32848470b9208c858c0497ede9b5b185558 Mon Sep 17 00:00:00 2001 From: Dave Behnke <916775+dbehnke@users.noreply.github.com> Date: Thu, 25 Dec 2025 23:42:27 -0500 Subject: [PATCH 4/6] Implement closing event and fix hearing module in all protocols --- docs/nng.md | 17 ++++++++++++++++- mrefd-temp | 1 + reflector/DCSProtocol.cpp | 2 +- reflector/DExtraProtocol.cpp | 2 +- reflector/DMRMMDVMProtocol.cpp | 2 +- reflector/DPlusProtocol.cpp | 2 +- reflector/GateKeeper.h | 2 +- reflector/M17Protocol.cpp | 4 +++- reflector/NXDNProtocol.cpp | 2 +- reflector/P25Protocol.cpp | 2 +- reflector/Reflector.cpp | 3 +++ reflector/URFProtocol.cpp | 4 +++- reflector/Users.cpp | 11 +++++++++++ reflector/Users.h | 2 ++ reflector/YSFProtocol.cpp | 2 +- 15 files changed, 47 insertions(+), 11 deletions(-) create mode 160000 mrefd-temp diff --git a/docs/nng.md b/docs/nng.md index 9c078ac..5503406 100644 --- a/docs/nng.md +++ b/docs/nng.md @@ -29,7 +29,7 @@ graph TD %% Internal Flows CC -- "client_connect / client_disconnect" --> NP - CU -- "hearing (activity)" --> NP + CU -- "hearing / closing" --> NP CR -- "periodic state report" --> NP PS -- "IsActive status" --> CR @@ -123,6 +123,21 @@ Triggered when the reflector "hears" an active transmission. This event is sent } ``` +### 4. Transmission End (`closing`) + +Triggered when a transmission stream is closed (user stops talking). + +**Payload Structure:** + +```json +{ + "type": "closing", + "my": "G4XYZ", + "module": "A", + "protocol": "M17" +} +``` + ## Middle Tier Design Considerations 1. **Late Joining**: The `state` message is broadcast periodically to ensure a middle-tier connecting at any time (or reconnecting) can synchronize its internal state without waiting for new events. diff --git a/mrefd-temp b/mrefd-temp new file mode 160000 index 0000000..fbf88f1 --- /dev/null +++ b/mrefd-temp @@ -0,0 +1 @@ +Subproject commit fbf88f1e7f347f78a501b906fe814aa9c11bcd9f diff --git a/reflector/DCSProtocol.cpp b/reflector/DCSProtocol.cpp index 0ea47f4..3821dac 100644 --- a/reflector/DCSProtocol.cpp +++ b/reflector/DCSProtocol.cpp @@ -208,7 +208,7 @@ void CDcsProtocol::OnDvHeaderPacketIn(std::unique_ptr &Header, g_Reflector.ReleaseClients(); // update last heard - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::dcs); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, rpt2, EProtocol::dcs); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/DExtraProtocol.cpp b/reflector/DExtraProtocol.cpp index c698b26..32be8d2 100644 --- a/reflector/DExtraProtocol.cpp +++ b/reflector/DExtraProtocol.cpp @@ -351,7 +351,7 @@ void CDextraProtocol::OnDvHeaderPacketIn(std::unique_ptr &Heade g_Reflector.ReleaseClients(); // update last heard - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::dextra); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, rpt2, EProtocol::dextra); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/DMRMMDVMProtocol.cpp b/reflector/DMRMMDVMProtocol.cpp index f2b201e..c36c921 100644 --- a/reflector/DMRMMDVMProtocol.cpp +++ b/reflector/DMRMMDVMProtocol.cpp @@ -335,7 +335,7 @@ void CDmrmmdvmProtocol::OnDvHeaderPacketIn(std::unique_ptr &Hea // update last heard if ( lastheard ) { - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::dmrmmdvm); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, rpt2, EProtocol::dmrmmdvm); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/DPlusProtocol.cpp b/reflector/DPlusProtocol.cpp index 24b819d..ce908f4 100644 --- a/reflector/DPlusProtocol.cpp +++ b/reflector/DPlusProtocol.cpp @@ -213,7 +213,7 @@ void CDplusProtocol::OnDvHeaderPacketIn(std::unique_ptr &Header g_Reflector.ReleaseClients(); // update last heard - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::dplus); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, rpt2, EProtocol::dplus); g_Reflector.ReleaseUsers(); } else diff --git a/reflector/GateKeeper.h b/reflector/GateKeeper.h index 619dd86..d6baabd 100644 --- a/reflector/GateKeeper.h +++ b/reflector/GateKeeper.h @@ -47,6 +47,7 @@ public: // authorizations bool MayLink(const CCallsign &, const CIp &, const EProtocol, char * = nullptr) const; bool MayTransmit(const CCallsign &, const CIp &, EProtocol = EProtocol::any, char = ' ') const; + const std::string ProtocolName(EProtocol) const; protected: // thread @@ -56,7 +57,6 @@ protected: bool IsNodeListedOk(const std::string &) const; bool IsPeerListedOk(const std::string &, char) const; bool IsPeerListedOk(const std::string &, const CIp &, char *) const; - const std::string ProtocolName(EProtocol) const; protected: // data diff --git a/reflector/M17Protocol.cpp b/reflector/M17Protocol.cpp index be8c634..204909e 100644 --- a/reflector/M17Protocol.cpp +++ b/reflector/M17Protocol.cpp @@ -209,7 +209,9 @@ void CM17Protocol::OnDvHeaderPacketIn(std::unique_ptr &Header, g_Reflector.ReleaseClients(); // update last heard - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::m17); + CCallsign reflectorCall = rpt2; + reflectorCall.SetCSModule(Header->GetRpt2Module()); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, reflectorCall, EProtocol::m17); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/NXDNProtocol.cpp b/reflector/NXDNProtocol.cpp index 7c7b832..58638d2 100644 --- a/reflector/NXDNProtocol.cpp +++ b/reflector/NXDNProtocol.cpp @@ -235,7 +235,7 @@ void CNXDNProtocol::OnDvHeaderPacketIn(std::unique_ptr &Header, // update last heard if ( g_Reflector.IsValidModule(rpt2.GetCSModule()) ) { - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::nxdn); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, rpt2, EProtocol::nxdn); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/P25Protocol.cpp b/reflector/P25Protocol.cpp index 919fc66..011eec8 100644 --- a/reflector/P25Protocol.cpp +++ b/reflector/P25Protocol.cpp @@ -219,7 +219,7 @@ void CP25Protocol::OnDvHeaderPacketIn(std::unique_ptr &Header, g_Reflector.ReleaseClients(); // update last heard - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::p25); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, rpt2, EProtocol::p25); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/Reflector.cpp b/reflector/Reflector.cpp index 5ffec10..442fb8b 100644 --- a/reflector/Reflector.cpp +++ b/reflector/Reflector.cpp @@ -276,6 +276,9 @@ void CReflector::CloseStream(std::shared_ptr stream) // notify //OnStreamClose(stream->GetUserCallsign()); + // dashboard event + GetUsers()->Closing(stream->GetUserCallsign(), GetStreamModule(stream), stream->GetOwnerClient()->GetProtocol()); + std::cout << "Closing stream of module " << GetStreamModule(stream) << std::endl; } diff --git a/reflector/URFProtocol.cpp b/reflector/URFProtocol.cpp index e9d7fcb..cdac493 100644 --- a/reflector/URFProtocol.cpp +++ b/reflector/URFProtocol.cpp @@ -411,7 +411,9 @@ void CURFProtocol::OnDvHeaderPacketIn(std::unique_ptr &Header, // release g_Reflector.ReleaseClients(); // update last heard - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, peer, EProtocol::urf); + CCallsign xlx = rpt2; + xlx.SetCSModule(Header->GetRpt2Module()); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, xlx, EProtocol::urf); g_Reflector.ReleaseUsers(); } } diff --git a/reflector/Users.cpp b/reflector/Users.cpp index 1284d7a..80b2faf 100644 --- a/reflector/Users.cpp +++ b/reflector/Users.cpp @@ -76,3 +76,14 @@ void CUsers::Hearing(const CCallsign &my, const CCallsign &rpt1, const CCallsign event["protocol"] = g_GateKeeper.ProtocolName(protocol); g_NNGPublisher.Publish(event); } + +void CUsers::Closing(const CCallsign &my, char module, EProtocol protocol) +{ + // dashboard event + nlohmann::json event; + event["type"] = "closing"; + event["my"] = my.GetCS(); + event["module"] = std::string(1, module); + event["protocol"] = g_GateKeeper.ProtocolName(protocol); + g_NNGPublisher.Publish(event); +} diff --git a/reflector/Users.h b/reflector/Users.h index 0873317..9638ebd 100644 --- a/reflector/Users.h +++ b/reflector/Users.h @@ -22,6 +22,7 @@ #include #include "User.h" +#include "Defines.h" class CUsers { @@ -49,6 +50,7 @@ public: // operation void Hearing(const CCallsign &, const CCallsign &, const CCallsign &, EProtocol protocol = EProtocol::none); void Hearing(const CCallsign &, const CCallsign &, const CCallsign &, const CCallsign &, EProtocol protocol); + void Closing(const CCallsign &, char module, EProtocol protocol); protected: // data diff --git a/reflector/YSFProtocol.cpp b/reflector/YSFProtocol.cpp index ea8e8c4..ed6e09a 100644 --- a/reflector/YSFProtocol.cpp +++ b/reflector/YSFProtocol.cpp @@ -293,7 +293,7 @@ void CYsfProtocol::OnDvHeaderPacketIn(std::unique_ptr &Header, // update last heard if ( g_Reflector.IsValidModule(rpt2.GetCSModule()) ) { - g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, EProtocol::ysf); + g_Reflector.GetUsers()->Hearing(my, rpt1, rpt2, rpt2, EProtocol::ysf); g_Reflector.ReleaseUsers(); } } From 20e3c8a7a8ab9b8414c8b17b49d34f36723b1556 Mon Sep 17 00:00:00 2001 From: Dave Behnke <916775+dbehnke@users.noreply.github.com> Date: Thu, 25 Dec 2025 23:43:02 -0500 Subject: [PATCH 5/6] Remove accidentally committed mrefd-temp embedded repository --- mrefd-temp | 1 - 1 file changed, 1 deletion(-) delete mode 160000 mrefd-temp diff --git a/mrefd-temp b/mrefd-temp deleted file mode 160000 index fbf88f1..0000000 --- a/mrefd-temp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fbf88f1e7f347f78a501b906fe814aa9c11bcd9f From 9cc9d2dd810734d2b0e7efe10d10de9af9b430bf Mon Sep 17 00:00:00 2001 From: Dave Behnke <916775+dbehnke@users.noreply.github.com> Date: Fri, 26 Dec 2025 12:09:35 -0500 Subject: [PATCH 6/6] Implement NNG event system, fix deadlock, and add NNGDebug config --- reflector/Configure.cpp | 3 +++ reflector/JsonKeys.h | 4 ++-- reflector/M17Protocol.cpp | 18 ++++++++++++++++++ reflector/NNGPublisher.cpp | 4 +++- reflector/Reflector.cpp | 1 + reflector/YSFProtocol.cpp | 4 ++-- 6 files changed, 29 insertions(+), 5 deletions(-) diff --git a/reflector/Configure.cpp b/reflector/Configure.cpp index 0f2d144..f1a0d59 100644 --- a/reflector/Configure.cpp +++ b/reflector/Configure.cpp @@ -127,6 +127,7 @@ CConfigure::CConfigure() data[g_Keys.dashboard.nngaddr] = "tcp://127.0.0.1:5555"; data[g_Keys.dashboard.interval] = 10U; data[g_Keys.dashboard.enable] = false; + data[g_Keys.dashboard.debug] = false; data[g_Keys.ysf.ysfreflectordb.id] = 0U; } @@ -510,6 +511,8 @@ bool CConfigure::ReadData(const std::string &path) data[g_Keys.dashboard.nngaddr] = value; else if (0 == key.compare("Interval")) data[g_Keys.dashboard.interval] = getUnsigned(value, "Dashboard Interval", 1, 3600, 10); + else if (0 == key.compare("NNGDebug")) + data[g_Keys.dashboard.debug] = IS_TRUE(value[0]); else badParam(key); break; diff --git a/reflector/JsonKeys.h b/reflector/JsonKeys.h index 09f2ac1..1eac949 100644 --- a/reflector/JsonKeys.h +++ b/reflector/JsonKeys.h @@ -72,6 +72,6 @@ struct SJsonKeys { struct FILES { const std::string pid, xml, json, white, black, interlink, terminal; } files { "pidFilePath", "xmlFilePath", "jsonFilePath", "whitelistFilePath", "blacklistFilePath", "interlinkFilePath", "g3TerminalFilePath" }; - struct DASHBOARD { const std::string enable, nngaddr, interval; } - dashboard { "DashboardEnable", "DashboardNNGAddr", "DashboardInterval" }; + struct DASHBOARD { const std::string enable, nngaddr, interval, debug; } + dashboard { "DashboardEnable", "DashboardNNGAddr", "DashboardInterval", "NNGDebug" }; }; diff --git a/reflector/M17Protocol.cpp b/reflector/M17Protocol.cpp index 204909e..3ecce1f 100644 --- a/reflector/M17Protocol.cpp +++ b/reflector/M17Protocol.cpp @@ -23,6 +23,12 @@ #include "M17Packet.h" #include "Global.h" +//////////////////////////////////////////////////////////////////////////////////////// +// constructor +CM17Protocol::CM17Protocol() : CSEProtocol() +{ +} + //////////////////////////////////////////////////////////////////////////////////////// // operation @@ -413,3 +419,15 @@ void CM17Protocol::EncodeM17Packet(SM17Frame &frame, const CDvHeaderPacket &Head frame.streamid = Header.GetStreamId(); // no host<--->network byte swapping since we never do any math on this value // the CRC will be set in HandleQueue, after lich.dest is set } + +bool CM17Protocol::EncodeDvHeaderPacket(const CDvHeaderPacket &packet, CBuffer &buffer) const +{ + packet.EncodeInterlinkPacket(buffer); + return true; +} + +bool CM17Protocol::EncodeDvFramePacket(const CDvFramePacket &packet, CBuffer &buffer) const +{ + packet.EncodeInterlinkPacket(buffer); + return true; +} diff --git a/reflector/NNGPublisher.cpp b/reflector/NNGPublisher.cpp index 5c02ab2..9f87347 100644 --- a/reflector/NNGPublisher.cpp +++ b/reflector/NNGPublisher.cpp @@ -1,4 +1,5 @@ #include "NNGPublisher.h" +#include "Global.h" #include CNNGPublisher::CNNGPublisher() @@ -54,7 +55,8 @@ void CNNGPublisher::Publish(const nlohmann::json &event) return; } std::string msg = event.dump(); - std::cout << "NNG debug: Attempting to publish message of size " << msg.size() << ": " << msg << std::endl; + if (g_Configure.GetBoolean(g_Keys.dashboard.debug)) + std::cout << "NNG debug: Attempting to publish message of size " << msg.size() << ": " << msg << std::endl; int rv = nng_send(m_sock, (void *)msg.c_str(), msg.size(), NNG_FLAG_NONBLOCK); if (rv == 0) { std::cout << "NNG: Published event: " << event["type"] << std::endl; diff --git a/reflector/Reflector.cpp b/reflector/Reflector.cpp index 442fb8b..3ec3413 100644 --- a/reflector/Reflector.cpp +++ b/reflector/Reflector.cpp @@ -278,6 +278,7 @@ void CReflector::CloseStream(std::shared_ptr stream) // dashboard event GetUsers()->Closing(stream->GetUserCallsign(), GetStreamModule(stream), stream->GetOwnerClient()->GetProtocol()); + ReleaseUsers(); std::cout << "Closing stream of module " << GetStreamModule(stream) << std::endl; } diff --git a/reflector/YSFProtocol.cpp b/reflector/YSFProtocol.cpp index ed6e09a..b6dcceb 100644 --- a/reflector/YSFProtocol.cpp +++ b/reflector/YSFProtocol.cpp @@ -478,7 +478,7 @@ bool CYsfProtocol::IsValidDvHeaderPacket(const CIp &Ip, const CYSFFICH &Fich, co sz[YSF_CALLSIGN_LENGTH] = 0; CCallsign rpt1 = CCallsign((const char *)sz); rpt1.SetCSModule(YSF_MODULE_ID); - CCallsign rpt2 = m_ReflectorCallsign; + CCallsign rpt2 = g_Reflector.GetCallsign(); // as YSF protocol does not provide a module-tranlatable // destid, set module to none and rely on OnDvHeaderPacketIn() // to later fill it with proper value @@ -531,7 +531,7 @@ bool CYsfProtocol::IsValidDvFramePacket(const CIp &Ip, const CYSFFICH &Fich, con sz[YSF_CALLSIGN_LENGTH] = 0; CCallsign rpt1 = CCallsign((const char *)sz); rpt1.SetCSModule(YSF_MODULE_ID); - CCallsign rpt2 = m_ReflectorCallsign; + CCallsign rpt2 = g_Reflector.GetCallsign(); rpt2.SetCSModule(' '); header = std::unique_ptr(new CDvHeaderPacket(csMY, CCallsign("CQCQCQ"), rpt1, rpt2, uiStreamId, Fich.getFN()));