diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 558c8feb..0694507e 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -144,6 +144,42 @@ file(GLOB dvmcmd_SRC
"src/Utils.cpp"
)
+#
+## dvmmon source/header files
+#
+file(GLOB dvmmon_SRC
+ "src/lookups/AffiliationLookup.h"
+ "src/lookups/LookupTable.h"
+ "src/lookups/IdenTableLookup.h"
+ "src/lookups/IdenTableLookup.cpp"
+ "src/modem/Modem.h"
+ "src/monitor/*.h"
+ "src/monitor/*.cpp"
+ "src/network/UDPSocket.h"
+ "src/network/UDPSocket.cpp"
+ "src/network/RESTDefines.h"
+ "src/network/json/*.h"
+ "src/network/rest/*.h"
+ "src/network/rest/*.cpp"
+ "src/network/rest/http/*.h"
+ "src/network/rest/http/*.cpp"
+ "src/remote/RESTClient.h"
+ "src/remote/RESTClient.cpp"
+ "src/edac/SHA256.h"
+ "src/edac/SHA256.cpp"
+ "src/yaml/*.h"
+ "src/yaml/*.cpp"
+ "src/Defines.h"
+ "src/Thread.h"
+ "src/Thread.cpp"
+ "src/Timer.h"
+ "src/Timer.cpp"
+ "src/Log.h"
+ "src/Log.cpp"
+ "src/Utils.h"
+ "src/Utils.cpp"
+)
+
# Digital mode options and other compilation features
option(ENABLE_DMR "Enable DMR Digtial Mode" on)
if (ENABLE_DMR)
@@ -350,3 +386,24 @@ target_link_libraries(asio::asio INTERFACE Threads::Threads)
add_executable(dvmcmd ${dvmcmd_SRC})
target_link_libraries(dvmcmd PRIVATE asio::asio Threads::Threads)
target_include_directories(dvmcmd PRIVATE src)
+
+#
+## dvmmon project
+#
+if (ENABLE_TUI_SUPPORT)
+project(dvmmon)
+set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
+find_package(Threads REQUIRED)
+
+# add ASIO
+target_include_directories(asio::asio INTERFACE ${ASIO_INCLUDE_DIR})
+target_compile_definitions(asio::asio INTERFACE "ASIO_STANDALONE")
+target_link_libraries(asio::asio INTERFACE Threads::Threads)
+
+# add finalcut
+target_include_directories(finalcut INTERFACE ${FINALCUT_INCLUDE_DIR})
+
+add_executable(dvmmon ${dvmmon_SRC})
+target_link_libraries(dvmmon PRIVATE asio::asio finalcut Threads::Threads)
+target_include_directories(dvmmon PRIVATE src)
+endif (ENABLE_TUI_SUPPORT)
diff --git a/src/dmr/Control.cpp b/src/dmr/Control.cpp
index 9c19a9f2..8cafdba8 100644
--- a/src/dmr/Control.cpp
+++ b/src/dmr/Control.cpp
@@ -630,6 +630,26 @@ void Control::setCSBKVerbose(bool verbose)
lc::CSBK::setVerbose(verbose);
}
+///
+/// Helper to get the last transmitted destination ID.
+///
+/// DMR slot number.
+///
+uint32_t Control::getLastDstId(uint32_t slotNo) const
+{
+ switch (slotNo) {
+ case 1U:
+ return m_slot1->getLastDstId();
+ case 2U:
+ return m_slot2->getLastDstId();
+ default:
+ LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo);
+ break;
+ }
+
+ return 0U;
+}
+
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------
diff --git a/src/dmr/Control.h b/src/dmr/Control.h
index 499060e8..7dca0794 100644
--- a/src/dmr/Control.h
+++ b/src/dmr/Control.h
@@ -130,6 +130,9 @@ namespace dmr
/// Helper to change the CSBK verbose state.
void setCSBKVerbose(bool verbose);
+ /// Helper to get the last transmitted destination ID.
+ uint32_t getLastDstId(uint32_t slotNo) const;
+
private:
friend class Slot;
diff --git a/src/dmr/Slot.cpp b/src/dmr/Slot.cpp
index 65405dbc..1ec500dc 100644
--- a/src/dmr/Slot.cpp
+++ b/src/dmr/Slot.cpp
@@ -676,6 +676,23 @@ void Slot::setSilenceThreshold(uint32_t threshold)
m_silenceThreshold = threshold;
}
+///
+/// Helper to get the last transmitted destination ID.
+///
+///
+uint32_t Slot::getLastDstId() const
+{
+ if (m_rfLastDstId != 0U) {
+ return m_rfLastDstId;
+ }
+
+ if (m_netLastDstId != 0U) {
+ return m_netLastDstId;
+ }
+
+ return 0U;
+}
+
///
/// Helper to initialize the DMR slot processor.
///
diff --git a/src/dmr/Slot.h b/src/dmr/Slot.h
index 8a715e9d..b1fff954 100644
--- a/src/dmr/Slot.h
+++ b/src/dmr/Slot.h
@@ -121,6 +121,9 @@ namespace dmr
/// Helper to set the voice error silence threshold.
void setSilenceThreshold(uint32_t threshold);
+ /// Helper to get the last transmitted destination ID.
+ uint32_t getLastDstId() const;
+
/// Helper to initialize the slot processor.
static void init(Control* dmr, bool authoritative, uint32_t colorCode, SiteData siteData, bool embeddedLCOnly, bool dumpTAData, uint32_t callHang, modem::Modem* modem,
network::Network* network, bool duplex, ::lookups::RadioIdLookup* ridLookup, ::lookups::TalkgroupRulesLookup* tidLookup,
diff --git a/src/host/Host.cpp b/src/host/Host.cpp
index c7d76c17..559c2340 100644
--- a/src/host/Host.cpp
+++ b/src/host/Host.cpp
@@ -99,6 +99,7 @@ Host::Host(const std::string& confFile) :
m_rfModeHang(10U),
m_rfTalkgroupHang(10U),
m_netModeHang(3U),
+ m_lastDstId(0U),
m_identity(),
m_cwCallsign(),
m_cwIdTime(0U),
@@ -889,6 +890,8 @@ int Host::run()
m_modeTimer.start();
}
+
+ m_lastDstId = dmr->getLastDstId(1U);
}
}
@@ -934,6 +937,8 @@ int Host::run()
m_modeTimer.start();
}
+
+ m_lastDstId = dmr->getLastDstId(2U);
}
}
}
@@ -972,6 +977,8 @@ int Host::run()
m_modeTimer.start();
}
+
+ m_lastDstId = p25->getLastDstId();
}
else {
nextLen = 0U;
@@ -1050,6 +1057,8 @@ int Host::run()
m_modeTimer.start();
}
+
+ m_lastDstId = nxdn->getLastDstId();
}
}
}
diff --git a/src/host/Host.h b/src/host/Host.h
index 3a05780f..813f9925 100644
--- a/src/host/Host.h
+++ b/src/host/Host.h
@@ -99,6 +99,8 @@ private:
uint32_t m_rfTalkgroupHang;
uint32_t m_netModeHang;
+ uint32_t m_lastDstId;
+
std::string m_identity;
std::string m_cwCallsign;
uint32_t m_cwIdTime;
diff --git a/src/host/setup/ChannelConfigSetWnd.h b/src/host/setup/ChannelConfigSetWnd.h
index 7db4bed4..b3820901 100644
--- a/src/host/setup/ChannelConfigSetWnd.h
+++ b/src/host/setup/ChannelConfigSetWnd.h
@@ -88,7 +88,7 @@ private:
///
///
///
- void initControls()
+ void initControls() override
{
yaml::Node rfssConfig = m_setup->m_conf["system"]["config"];
m_setup->m_channelId = (uint8_t)rfssConfig["channelId"].as(0U);
diff --git a/src/host/setup/FIFOBufferAdjustWnd.h b/src/host/setup/FIFOBufferAdjustWnd.h
index f92a0c6f..c6365f2d 100644
--- a/src/host/setup/FIFOBufferAdjustWnd.h
+++ b/src/host/setup/FIFOBufferAdjustWnd.h
@@ -77,7 +77,7 @@ private:
///
///
///
- void initControls()
+ void initControls() override
{
// symbol levels
{
diff --git a/src/host/setup/HSBandwidthAdjustWnd.h b/src/host/setup/HSBandwidthAdjustWnd.h
index 432fef1b..da75bb51 100644
--- a/src/host/setup/HSBandwidthAdjustWnd.h
+++ b/src/host/setup/HSBandwidthAdjustWnd.h
@@ -82,7 +82,7 @@ private:
///
///
///
- void initControls()
+ void initControls() override
{
// symbol levels
{
diff --git a/src/host/setup/HSGainAdjustWnd.h b/src/host/setup/HSGainAdjustWnd.h
index 6a7c5de5..309b8cb9 100644
--- a/src/host/setup/HSGainAdjustWnd.h
+++ b/src/host/setup/HSGainAdjustWnd.h
@@ -85,7 +85,7 @@ private:
///
///
///
- void initControls()
+ void initControls() override
{
// gain
{
diff --git a/src/host/setup/LoggingAndDataSetWnd.h b/src/host/setup/LoggingAndDataSetWnd.h
index c27387f6..596bbbcd 100644
--- a/src/host/setup/LoggingAndDataSetWnd.h
+++ b/src/host/setup/LoggingAndDataSetWnd.h
@@ -85,7 +85,7 @@ private:
///
///
///
- void initControls()
+ void initControls() override
{
yaml::Node logConf = m_setup->m_conf["log"];
uint32_t logLevel = logConf["fileLevel"].as(1U);
diff --git a/src/host/setup/SiteParamSetWnd.h b/src/host/setup/SiteParamSetWnd.h
index c945248f..57c13cef 100644
--- a/src/host/setup/SiteParamSetWnd.h
+++ b/src/host/setup/SiteParamSetWnd.h
@@ -105,7 +105,7 @@ private:
///
///
///
- void initControls()
+ void initControls() override
{
yaml::Node cwId = m_setup->m_conf["system"]["cwId"];
bool enabled = cwId["enable"].as(false);
diff --git a/src/host/setup/SymbLevelAdjustWnd.h b/src/host/setup/SymbLevelAdjustWnd.h
index 0887f335..b7be84f9 100644
--- a/src/host/setup/SymbLevelAdjustWnd.h
+++ b/src/host/setup/SymbLevelAdjustWnd.h
@@ -82,7 +82,7 @@ private:
///
///
///
- void initControls()
+ void initControls() override
{
// symbol levels
{
diff --git a/src/host/setup/SystemConfigSetWnd.h b/src/host/setup/SystemConfigSetWnd.h
index dc20d2b7..3b5af33c 100644
--- a/src/host/setup/SystemConfigSetWnd.h
+++ b/src/host/setup/SystemConfigSetWnd.h
@@ -99,7 +99,7 @@ private:
///
///
///
- void initControls()
+ void initControls() override
{
yaml::Node modemConfig = m_setup->m_conf["system"]["modem"];
m_setup->m_conf["system"]["modem"]["protocol"]["type"] = std::string("uart"); // configuring modem, always sets type to UART
diff --git a/src/monitor/LogDisplayWnd.h b/src/monitor/LogDisplayWnd.h
new file mode 100644
index 00000000..1b77cfd0
--- /dev/null
+++ b/src/monitor/LogDisplayWnd.h
@@ -0,0 +1,137 @@
+/**
+* Digital Voice Modem - Monitor
+* GPLv2 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / Monitor
+*
+*/
+/*
+* Copyright (C) 2023 by Bryan Biedenkapp N2PLL
+*
+* 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
+* the Free Software Foundation; either version 2 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+*/
+#if !defined(__LOG_DISPLAY_WND_H__)
+#define __LOG_DISPLAY_WND_H__
+
+#include
+using namespace finalcut;
+
+// ---------------------------------------------------------------------------
+// Class Declaration
+// This class implements the log display window.
+// ---------------------------------------------------------------------------
+
+class HOST_SW_API LogDisplayWnd final : public finalcut::FDialog, public std::ostringstream
+{
+public:
+ ///
+ /// Initializes a new instance of the LogDisplayWnd class.
+ ///
+ ///
+ explicit LogDisplayWnd(FWidget* widget = nullptr) : FDialog{widget}
+ {
+ m_scrollText.ignorePadding();
+
+ m_timerId = addTimer(250); // starts the timer every 250 milliseconds
+ }
+ /// Copy constructor.
+ LogDisplayWnd(const LogDisplayWnd&) = delete;
+ /// Move constructor.
+ LogDisplayWnd(LogDisplayWnd&&) noexcept = delete;
+ /// Finalizes an instance of the LogDisplayWnd class.
+ ~LogDisplayWnd() noexcept override = default;
+
+ /// Disable copy assignment operator (=).
+ auto operator= (const LogDisplayWnd&) -> LogDisplayWnd& = delete;
+ /// Disable move assignment operator (=).
+ auto operator= (LogDisplayWnd&&) noexcept -> LogDisplayWnd& = delete;
+
+private:
+ FTextView m_scrollText{this};
+ int m_timerId;
+
+ ///
+ ///
+ ///
+ void initLayout() override
+ {
+ using namespace std::string_literals;
+ auto lightning = "\u26a1";
+ FDialog::setText("System Log"s + lightning);
+
+ const auto& rootWidget = getRootWidget();
+
+ FDialog::setGeometry(FPoint{(int)(rootWidget->getClientWidth() - 81), (int)(rootWidget->getClientHeight() - 20)}, FSize{80, 20});
+ FDialog::setMinimumSize(FSize{80, 20});
+ FDialog::setResizeable(true);
+ FDialog::setMinimizable(true);
+ FDialog::setTitlebarButtonVisibility(true);
+ FDialog::setShadow();
+
+ minimizeWindow();
+
+ m_scrollText.setGeometry(FPoint{1, 2}, FSize{getWidth(), getHeight() - 1});
+
+ FDialog::initLayout();
+ }
+
+ ///
+ ///
+ ///
+ void adjustSize() override
+ {
+ FDialog::adjustSize();
+
+ m_scrollText.setGeometry(FPoint{1, 2}, FSize{getWidth(), getHeight() - 1});
+
+ redraw();
+ }
+
+ /*
+ ** Event Handlers
+ */
+
+ ///
+ ///
+ ///
+ ///
+ void onClose(FCloseEvent* e) override
+ {
+ minimizeWindow();
+ }
+
+ ///
+ ///
+ ///
+ ///
+ void onTimer(FTimerEvent* timer) override
+ {
+ if (timer != nullptr) {
+ if (timer->getTimerId() == m_timerId) {
+ if (str().empty()) {
+ return;
+ }
+
+ m_scrollText.append(str());
+ str("");
+ m_scrollText.scrollToEnd();
+ redraw();
+ }
+ }
+ }
+};
+
+#endif // __LOG_DISPLAY_WND_H__
\ No newline at end of file
diff --git a/src/monitor/MonitorApplication.h b/src/monitor/MonitorApplication.h
new file mode 100644
index 00000000..12cb513f
--- /dev/null
+++ b/src/monitor/MonitorApplication.h
@@ -0,0 +1,83 @@
+/**
+* Digital Voice Modem - Monitor
+* GPLv2 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / Monitor
+*
+*/
+/*
+* Copyright (C) 2023 by Bryan Biedenkapp N2PLL
+*
+* 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
+* the Free Software Foundation; either version 2 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+*/
+#if !defined(__MONITOR_APPLICATION_H__)
+#define __MONITOR_APPLICATION_H__
+
+#include "monitor/MonitorMain.h"
+#include "monitor/MonitorMainWnd.h"
+#include "Log.h"
+
+#include
+using namespace finalcut;
+
+// ---------------------------------------------------------------------------
+// Class Declaration
+// This class implements the finalcut application.
+// ---------------------------------------------------------------------------
+
+class HOST_SW_API MonitorApplication final : public finalcut::FApplication {
+public:
+ ///
+ /// Initializes a new instance of the MonitorApplication class.
+ ///
+ ///
+ ///
+ explicit MonitorApplication(const int& argc, char** argv) : FApplication{argc, argv}
+ {
+ m_statusRefreshTimer = addTimer(1000);
+ }
+
+protected:
+ ///
+ ///
+ ///
+ virtual void processExternalUserEvent()
+ {
+ /* stub */
+ }
+
+ /*
+ ** Event Handlers
+ */
+
+ ///
+ ///
+ ///
+ ///
+ void onTimer(FTimerEvent* timer) override
+ {
+ if (timer != nullptr) {
+ if (timer->getTimerId() == m_statusRefreshTimer) {
+ /* stub */
+ }
+ }
+ }
+
+private:
+ int m_statusRefreshTimer;
+};
+
+#endif // __MONITOR_APPLICATION_H__
\ No newline at end of file
diff --git a/src/monitor/MonitorMain.cpp b/src/monitor/MonitorMain.cpp
new file mode 100644
index 00000000..831d4495
--- /dev/null
+++ b/src/monitor/MonitorMain.cpp
@@ -0,0 +1,258 @@
+/**
+* Digital Voice Modem - Monitor
+* GPLv2 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / Monitor
+*
+*/
+/*
+* Copyright (C) 2023 by Bryan Biedenkapp
+*
+* 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
+* the Free Software Foundation; either version 2 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+*/
+#include "Defines.h"
+#include "Log.h"
+#include "monitor/MonitorMain.h"
+#include "monitor/MonitorApplication.h"
+#include "monitor/MonitorMainWnd.h"
+#include "yaml/Yaml.h"
+#include "Utils.h"
+
+using namespace lookups;
+
+#include
+#include
+#include
+#include
+
+// ---------------------------------------------------------------------------
+// Macros
+// ---------------------------------------------------------------------------
+
+#define IS(s) (::strcmp(argv[i], s) == 0)
+
+// ---------------------------------------------------------------------------
+// Global Variables
+// ---------------------------------------------------------------------------
+
+std::string g_progExe = std::string(__EXE_NAME__);
+std::string g_iniFile = std::string(DEFAULT_CONF_FILE);
+yaml::Node g_conf;
+bool g_debug = false;
+
+lookups::IdenTableLookup* g_idenTable = nullptr;
+uint32_t g_channelId = 0U;
+
+// ---------------------------------------------------------------------------
+// Global Functions
+// ---------------------------------------------------------------------------
+
+///
+/// Helper to print a fatal error message and exit.
+///
+/// This is a variable argument function.
+/// Message.
+void fatal(const char* msg, ...)
+{
+ char buffer[400U];
+ ::memset(buffer, 0x20U, 400U);
+
+ va_list vl;
+ va_start(vl, msg);
+
+ ::vsprintf(buffer, msg, vl);
+
+ va_end(vl);
+
+ ::fprintf(stderr, "%s: %s\n", g_progExe.c_str(), buffer);
+ exit(EXIT_FAILURE);
+}
+
+///
+/// Helper to pring usage the command line arguments. (And optionally an error.)
+///
+/// Error message.
+/// Error message arguments.
+void usage(const char* message, const char* arg)
+{
+ ::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__);
+ ::fprintf(stdout, "Copyright (c) 2023 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\n");
+ ::fprintf(stdout, "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\n\n");
+ if (message != nullptr) {
+ ::fprintf(stderr, "%s: ", g_progExe.c_str());
+ ::fprintf(stderr, message, arg);
+ ::fprintf(stderr, "\n\n");
+ }
+
+ ::fprintf(stdout, "usage: %s [-dvh] [-c ]\n\n"
+ " -c specifies the configuration file to use\n"
+ "\n"
+ " -d enable debug\n"
+ " -v show version information\n"
+ " -h show this screen\n"
+ " -- stop handling options\n",
+ g_progExe.c_str());
+
+ exit(EXIT_FAILURE);
+}
+
+///
+/// Helper to validate the command line arguments.
+///
+/// Argument count.
+/// Array of argument strings.
+/// Count of remaining unprocessed arguments.
+int checkArgs(int argc, char* argv[])
+{
+ int i, p = 0;
+
+ // iterate through arguments
+ for (i = 1; i <= argc; i++)
+ {
+ if (argv[i] == nullptr) {
+ break;
+ }
+
+ if (*argv[i] != '-') {
+ continue;
+ }
+ else if (IS("--")) {
+ ++p;
+ break;
+ }
+ else if (IS("-c")) {
+ if (argc-- <= 0)
+ usage("error: %s", "must specify the configuration file to use");
+ g_iniFile = std::string(argv[++i]);
+
+ if (g_iniFile == "")
+ usage("error: %s", "configuration file cannot be blank!");
+
+ p += 2;
+ }
+ else if (IS("-d")) {
+ ++p;
+ g_debug = true;
+ }
+ else if (IS("-v")) {
+ ::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__);
+ ::fprintf(stdout, "Copyright (c) 2017-2023 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\r\n");
+ if (argc == 2)
+ exit(EXIT_SUCCESS);
+ }
+ else if (IS("-h")) {
+ usage(nullptr, nullptr);
+ if (argc == 2)
+ exit(EXIT_SUCCESS);
+ }
+ else {
+ usage("unrecognized option `%s'", argv[i]);
+ }
+ }
+
+ if (p < 0 || p > argc) {
+ p = 0;
+ }
+
+ return ++p;
+}
+
+// ---------------------------------------------------------------------------
+// Program Entry Point
+// ---------------------------------------------------------------------------
+
+int main(int argc, char** argv)
+{
+ if (argv[0] != nullptr && *argv[0] != 0)
+ g_progExe = std::string(argv[0]);
+
+ if (argc > 1) {
+ // check arguments
+ int i = checkArgs(argc, argv);
+ if (i < argc) {
+ argc -= i;
+ argv += i;
+ }
+ else {
+ argc--;
+ argv++;
+ }
+ }
+
+ // initialize system logging
+ bool ret = ::LogInitialise("", "", 0U, 1U);
+ if (!ret) {
+ ::fprintf(stderr, "unable to open the log file\n");
+ return 1;
+ }
+
+ getHostVersion();
+ ::LogInfo(">> Monitor");
+
+ try {
+ ret = yaml::Parse(g_conf, g_iniFile.c_str());
+ if (!ret) {
+ ::fatal("cannot read the configuration file, %s\n", g_iniFile.c_str());
+ }
+ }
+ catch (yaml::OperationException const& e) {
+ ::fatal("cannot read the configuration file - %s (%s)", g_iniFile.c_str(), e.message());
+ }
+
+ // setup the finalcut tui
+ MonitorApplication app{argc, argv};
+
+ MonitorMainWnd wnd{&app};
+ finalcut::FWidget::setMainWidget(&wnd);
+
+ yaml::Node systemConf = g_conf["system"];
+
+ // try to load bandplan identity table
+ std::string idenLookupFile = systemConf["iden_table"]["file"].as();
+ uint32_t idenReloadTime = systemConf["iden_table"]["time"].as(0U);
+
+ if (idenLookupFile.length() <= 0U) {
+ ::LogError(LOG_HOST, "No bandplan identity table? This must be defined!");
+ return 1;
+ }
+
+ g_logDisplayLevel = 0U;
+
+ LogInfo("Iden Table Lookups");
+ LogInfo(" File: %s", idenLookupFile.length() > 0U ? idenLookupFile.c_str() : "None");
+ if (idenReloadTime > 0U)
+ LogInfo(" Reload: %u mins", idenReloadTime);
+
+ g_idenTable = new IdenTableLookup(idenLookupFile, idenReloadTime);
+ g_idenTable->read();
+
+ yaml::Node rfssConfig = systemConf["config"];
+ g_channelId = (uint8_t)rfssConfig["channelId"].as(0U);
+ if (g_channelId > 15U) { // clamp to 15
+ g_channelId = 15U;
+ }
+
+ // show and start the application
+ wnd.show();
+
+ finalcut::FApplication::setDarkTheme();
+ app.resetColors();
+ app.redraw();
+
+ int _errno = app.exec();
+ ::LogFinalise();
+ return _errno;
+}
diff --git a/src/monitor/MonitorMain.h b/src/monitor/MonitorMain.h
new file mode 100644
index 00000000..711c10c1
--- /dev/null
+++ b/src/monitor/MonitorMain.h
@@ -0,0 +1,56 @@
+/**
+* Digital Voice Modem - Monitor
+* GPLv2 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / Monitor
+*
+*/
+/*
+* Copyright (C) 2023 by Bryan Biedenkapp N2PLL
+*
+* 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
+* the Free Software Foundation; either version 2 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+*/
+#if !defined(__MONITOR_MAIN_H__)
+#define __MONITOR_MAIN_H__
+
+#include "Defines.h"
+#include "lookups/IdenTableLookup.h"
+#include "yaml/Yaml.h"
+
+#include
+
+// ---------------------------------------------------------------------------
+// Constants
+// ---------------------------------------------------------------------------
+
+#undef __PROG_NAME__
+#define __PROG_NAME__ "Digital Voice Modem (DVM) Monitor Tool"
+#undef __EXE_NAME__
+#define __EXE_NAME__ "dvmmon"
+
+// ---------------------------------------------------------------------------
+// Externs
+// ---------------------------------------------------------------------------
+
+extern std::string g_progExe;
+extern std::string g_iniFile;
+extern yaml::Node g_conf;
+extern bool g_debug;
+
+extern lookups::IdenTableLookup* g_idenTable;
+extern uint32_t g_channelId;
+
+#endif // __MONITOR_MAIN_H__
diff --git a/src/monitor/MonitorMainWnd.h b/src/monitor/MonitorMainWnd.h
new file mode 100644
index 00000000..60fb9ffb
--- /dev/null
+++ b/src/monitor/MonitorMainWnd.h
@@ -0,0 +1,266 @@
+/**
+* Digital Voice Modem - Monitor
+* GPLv2 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / Monitor
+*
+*/
+/*
+* Copyright (C) 2023 by Bryan Biedenkapp N2PLL
+*
+* 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
+* the Free Software Foundation; either version 2 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+*/
+#if !defined(__MONITOR_WND_H__)
+#define __MONITOR_WND_H__
+
+#include "lookups/AffiliationLookup.h"
+#include "Log.h"
+#include "Thread.h"
+
+using namespace lookups;
+
+#include
+using namespace finalcut;
+#undef null
+
+#include "monitor/MonitorMain.h"
+
+#include "monitor/LogDisplayWnd.h"
+#include "monitor/NodeStatusWnd.h"
+
+#include
+
+// ---------------------------------------------------------------------------
+// Class Prototypes
+// ---------------------------------------------------------------------------
+
+class HOST_SW_API MonitorApplication;
+
+// ---------------------------------------------------------------------------
+// Class Declaration
+// This class implements the root window control.
+// ---------------------------------------------------------------------------
+
+class HOST_SW_API MonitorMainWnd final : public finalcut::FWidget {
+public:
+ ///
+ /// Initializes a new instance of the MonitorMainWnd class.
+ ///
+ ///
+ explicit MonitorMainWnd(FWidget* widget = nullptr) : FWidget{widget}
+ {
+ __InternalOutputStream(m_logWnd);
+
+ // file menu
+ m_quitItem.addAccelerator(FKey::Meta_x); // Meta/Alt + X
+ m_quitItem.addCallback("clicked", getFApplication(), &FApplication::cb_exitApp, this);
+ m_keyF3.addCallback("activate", getFApplication(), &FApplication::cb_exitApp, this);
+
+ // command menu
+ m_cmdMenuSeparator1.setSeparator();
+ m_cmdMenuSeparator2.setSeparator();
+
+ // engineering menu
+ m_engineeringMenuSeparator1.setSeparator();
+ m_engineeringMenuSeparator2.setSeparator();
+
+ // help menu
+ m_aboutItem.addCallback("clicked", this, [&]() {
+ const FString line(2, UniChar::BoxDrawingsHorizontal);
+ FMessageBox info("About", line + __PROG_NAME__ + line + L"\n\n"
+ L"Version " + __VER__ + L"\n\n"
+ L"Copyright (c) 2017-2023 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors." + L"\n"
+ L"Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others",
+ FMessageBox::ButtonType::Ok, FMessageBox::ButtonType::Reject, FMessageBox::ButtonType::Reject, this);
+ info.setCenterText();
+ info.show();
+ });
+ }
+
+private:
+ friend class MonitorApplication;
+
+ LogDisplayWnd m_logWnd{this};
+ std::vector m_nodes;
+
+ std::vector m_voiceChNo;
+ std::unordered_map m_voiceChData;
+
+ FString m_line{13, UniChar::BoxDrawingsHorizontal};
+
+ FMenuBar m_menuBar{this};
+
+ FMenu m_fileMenu{"&File", &m_menuBar};
+ FMenuItem m_quitItem{"&Quit", &m_fileMenu};
+
+ FMenu m_cmdMenu{"&Commands", &m_menuBar};
+ FMenuItem m_pageSU{"&Page Subscriber", &m_cmdMenu};
+ FMenuItem m_radioCheckSU{"Radio &Check Subscriber", &m_cmdMenu};
+ FMenuItem m_cmdMenuSeparator1{&m_cmdMenu};
+ FMenuItem m_inhibitSU{"&Inhibit Subscriber", &m_cmdMenu};
+ FMenuItem m_uninhibitSU{"&Uninhibit Subscriber", &m_cmdMenu};
+ FMenuItem m_cmdMenuSeparator2{&m_cmdMenu};
+ FMenuItem m_gaqSU{"&Group Affiliation Query", &m_cmdMenu};
+ FMenuItem m_uregSU{"&Force Unit Registration", &m_cmdMenu};
+
+ FMenu m_engineeringMenu{"&Engineering", &m_menuBar};
+ FMenuItem m_toggleDMRDebug{"Toggle DMR Debug", &m_engineeringMenu};
+ FMenuItem m_toggleDMRCSBKDump{"Toggle DMR CSBK Dump", &m_engineeringMenu};
+ FMenuItem m_engineeringMenuSeparator1{&m_engineeringMenu};
+ FMenuItem m_toggleP25Debug{"Toggle P25 Debug", &m_engineeringMenu};
+ FMenuItem m_toggleP25TSBKDump{"Toggle P25 TSBK Dump", &m_engineeringMenu};
+ FMenuItem m_engineeringMenuSeparator2{&m_engineeringMenu};
+ FMenuItem m_toggleNXDNDebug{"Toggle NXDN Debug", &m_engineeringMenu};
+ FMenuItem m_toggleNXDNRCCHDump{"Toggle NXDN RCCH Dump", &m_engineeringMenu};
+
+ FMenu m_helpMenu{"&Help", &m_menuBar};
+ FMenuItem m_aboutItem{"&About", &m_helpMenu};
+
+ FStatusBar m_statusBar{this};
+ FStatusKey m_keyF3{FKey::F3, "Quit", &m_statusBar};
+
+ ///
+ ///
+ ///
+ void intializeNodeDisplay()
+ {
+ const auto& rootWidget = getRootWidget();
+ const int defaultOffsX = 2;
+ int offsX = defaultOffsX, offsY = 2;
+
+ int maxWidth = 77;
+ if (rootWidget) {
+ maxWidth = rootWidget->getClientWidth() - 3;
+ }
+
+ yaml::Node systemConf = g_conf["system"];
+ yaml::Node rfssConfig = systemConf["config"];
+ uint8_t channelId = (uint8_t)rfssConfig["channelId"].as(0U);
+ if (channelId > 15U) { // clamp to 15
+ channelId = 15U;
+ }
+
+ // main/control channel
+ uint32_t mainChNo = 0U;
+ {
+ yaml::Node networkConf = g_conf["network"];
+ uint32_t chNo = (uint32_t)::strtoul(rfssConfig["channelNo"].as("1").c_str(), NULL, 16);
+ if (chNo == 0U) { // clamp to 1
+ chNo = 1U;
+ }
+ if (chNo > 4095U) { // clamp to 4095
+ chNo = 4095U;
+ }
+
+ mainChNo = chNo;
+
+ //std::string restApiAddress = networkConf["restAddress"].as("127.0.0.1");
+ std::string restApiAddress = std::string("127.0.0.1");
+ uint16_t restApiPort = (uint16_t)networkConf["restPort"].as(REST_API_DEFAULT_PORT);
+ std::string restApiPassword = networkConf["restPassword"].as();
+
+ VoiceChData data = VoiceChData(chNo, restApiAddress, restApiPort, restApiPassword);
+
+ // create configuration file node
+ NodeStatusWnd* wnd = new NodeStatusWnd(this);
+ wnd->setChData(data);
+ wnd->setChannelId(channelId);
+ wnd->setChannelNo(chNo);
+
+ wnd->setGeometry(FPoint{offsX, offsY}, FSize{NODE_STATUS_WIDTH, NODE_STATUS_HEIGHT});
+ offsX += NODE_STATUS_WIDTH + 2;
+ m_nodes.push_back(wnd);
+ }
+
+ /*
+ ** Voice Channels
+ */
+ yaml::Node& voiceChList = rfssConfig["voiceChNo"];
+
+ if (voiceChList.size() != 0U) {
+ for (size_t i = 0; i < voiceChList.size(); i++) {
+ yaml::Node& channel = voiceChList[i];
+
+ uint32_t chNo = (uint32_t)::strtoul(channel["channelNo"].as("1").c_str(), NULL, 16);
+ if (chNo == 0U) { // clamp to 1
+ chNo = 1U;
+ }
+ if (chNo > 4095U) { // clamp to 4095
+ chNo = 4095U;
+ }
+
+ if (chNo == mainChNo) {
+ continue;
+ }
+
+ std::string restApiAddress = channel["restAddress"].as("127.0.0.1");
+ uint16_t restApiPort = (uint16_t)channel["restPort"].as(REST_API_DEFAULT_PORT);
+ std::string restApiPassword = channel["restPassword"].as();
+
+ ::LogInfoEx(LOG_HOST, "Voice Channel Id %u Channel No $%04X REST API Adddress %s:%u", g_channelId, chNo, restApiAddress.c_str(), restApiPort);
+
+ VoiceChData data = VoiceChData(chNo, restApiAddress, restApiPort, restApiPassword);
+
+ NodeStatusWnd* wnd = new NodeStatusWnd(this);
+ wnd->setChData(data);
+ wnd->setChannelId(channelId);
+ wnd->setChannelNo(chNo);
+
+ // set control position
+ if (offsX + NODE_STATUS_WIDTH > maxWidth) {
+ offsY += NODE_STATUS_HEIGHT + 2;
+ offsX = defaultOffsX;
+ }
+
+ wnd->setGeometry(FPoint{offsX, offsY}, FSize{NODE_STATUS_WIDTH, NODE_STATUS_HEIGHT});
+ offsX += NODE_STATUS_WIDTH + 2;
+ m_nodes.push_back(wnd);
+ }
+ }
+
+ // display all the node windows
+ for (auto* wnd : m_nodes) {
+ wnd->setModal(false);
+ wnd->show();
+ }
+
+ redraw();
+ }
+
+ /*
+ ** Event Handlers
+ */
+
+ ///
+ ///
+ ///
+ ///
+ void onShow(FShowEvent* e) override
+ {
+ intializeNodeDisplay();
+ }
+
+ ///
+ ///
+ ///
+ ///
+ void onClose(FCloseEvent* e) override
+ {
+ FApplication::closeConfirmationDialog(this, e);
+ }
+};
+
+#endif // __MONITOR_WND_H__
\ No newline at end of file
diff --git a/src/monitor/NodeStatusWnd.h b/src/monitor/NodeStatusWnd.h
new file mode 100644
index 00000000..24ead7b3
--- /dev/null
+++ b/src/monitor/NodeStatusWnd.h
@@ -0,0 +1,342 @@
+/**
+* Digital Voice Modem - Monitor
+* GPLv2 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / Monitor
+*
+*/
+/*
+* Copyright (C) 2023 by Bryan Biedenkapp N2PLL
+*
+* 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
+* the Free Software Foundation; either version 2 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+*/
+#if !defined(__NODE_STATUS_WND_H__)
+#define __NODE_STATUS_WND_H__
+
+#include "lookups/AffiliationLookup.h"
+#include "modem/Modem.h"
+#include "network/RESTDefines.h"
+#include "remote/RESTClient.h"
+
+#include "monitor/MonitorMainWnd.h"
+
+#include
+using namespace finalcut;
+
+// ---------------------------------------------------------------------------
+// Constants
+// ---------------------------------------------------------------------------
+
+#define NODE_STATUS_WIDTH 28
+#define NODE_STATUS_HEIGHT 7
+
+// ---------------------------------------------------------------------------
+// Class Declaration
+// This class implements the node status display window.
+// ---------------------------------------------------------------------------
+
+class HOST_SW_API NodeStatusWnd final : public finalcut::FDialog
+{
+public:
+ ///
+ /// Initializes a new instance of the NodeStatusWnd class.
+ ///
+ ///
+ explicit NodeStatusWnd(FWidget* widget = nullptr) : FDialog{widget}
+ {
+ m_timerId = addTimer(250); // starts the timer every 250 milliseconds
+ m_reconnectTimerId = addTimer(15000); // starts the timer every 10 seconds
+ }
+ /// Copy constructor.
+ NodeStatusWnd(const NodeStatusWnd&) = delete;
+ /// Move constructor.
+ NodeStatusWnd(NodeStatusWnd&&) noexcept = delete;
+ /// Finalizes an instance of the NodeStatusWnd class.
+ ~NodeStatusWnd() noexcept override = default;
+
+ /// Disable copy assignment operator (=).
+ auto operator= (const NodeStatusWnd&) -> NodeStatusWnd& = delete;
+ /// Disable move assignment operator (=).
+ auto operator= (NodeStatusWnd&&) noexcept -> NodeStatusWnd& = delete;
+
+ /// Disable set X coordinate.
+ void setX(int, bool = true) override { }
+ /// Disable set Y coordinate.
+ void setY(int, bool = true) override { }
+ /// Disable set position.
+ void setPos(const FPoint&, bool = true) override { }
+
+ /// Sets the channel data.
+ void setChData(lookups::VoiceChData chData) { m_chData = chData; }
+ /// Sets the channel number.
+ void setChannelId(uint8_t channelId) { m_channelId = channelId; }
+ /// Sets the channel number.
+ void setChannelNo(uint32_t channelNo) { m_channelNo = channelNo; }
+
+private:
+ int m_timerId;
+ int m_reconnectTimerId;
+
+ bool m_failed;
+ bool m_control;
+ bool m_tx;
+
+ lookups::VoiceChData m_chData;
+ uint8_t m_channelId;
+ uint32_t m_channelNo;
+
+ FLabel m_channelNoLabel{"Ch. No.: ", this};
+ FLabel m_chanNo{this};
+
+ FLabel m_txFreqLabel{"Tx: ", this};
+ FLabel m_txFreq{this};
+ FLabel m_rxFreqLabel{"Rx: ", this};
+ FLabel m_rxFreq{this};
+
+ FLabel m_lastTGLabel{"Last TG: ", this};
+ FLabel m_lastTG{this};
+
+ ///
+ ///
+ ///
+ void initLayout() override
+ {
+ FDialog::setMinimumSize(FSize{NODE_STATUS_WIDTH, NODE_STATUS_HEIGHT});
+
+ FDialog::setResizeable(false);
+ FDialog::setMinimizable(false);
+ FDialog::setTitlebarButtonVisibility(false);
+ FDialog::setShadow(false);
+ FDialog::setModal(false);
+
+ FDialog::setText("UNKNOWN");
+
+ initControls();
+
+ FDialog::initLayout();
+ }
+
+ ///
+ ///
+ ///
+ void draw() override
+ {
+ FDialog::draw();
+
+ if (m_failed) {
+ setColor(FColor::Yellow1, FColor::Red3);
+ }
+ else if (m_control) {
+ setColor(FColor::LightGray, FColor::Purple3);
+ }
+ else if (m_tx) {
+ setColor(FColor::LightGray, FColor::Green3);
+ }
+ else {
+ setColor(FColor::LightGray, FColor::Black);
+ }
+
+ finalcut::drawBorder(this, FRect(FPoint{1, 2}, FPoint{NODE_STATUS_WIDTH, NODE_STATUS_HEIGHT}));
+ }
+
+ ///
+ ///
+ ///
+ void initControls()
+ {
+ // channel number
+ {
+ m_channelNoLabel.setGeometry(FPoint(2, 1), FSize(10, 1));
+
+ m_chanNo.setGeometry(FPoint(11, 1), FSize(8, 1));
+ m_chanNo.setText(__INT_STR(m_channelNo));
+ }
+
+ // channel frequency
+ {
+ IdenTable entry = g_idenTable->find(m_channelId);
+ if (entry.baseFrequency() == 0U) {
+ ::LogError(LOG_HOST, "Channel Id %u has an invalid base frequency.", m_channelId);
+ }
+
+ if (entry.txOffsetMhz() == 0U) {
+ ::LogError(LOG_HOST, "Channel Id %u has an invalid Tx offset.", m_channelId);
+ }
+
+ uint32_t calcSpace = (uint32_t)(entry.chSpaceKhz() / 0.125);
+ float calcTxOffset = entry.txOffsetMhz() * 1000000;
+
+ uint32_t rxFrequency = (uint32_t)((entry.baseFrequency() + ((calcSpace * 125) * m_channelNo)) + calcTxOffset);
+ uint32_t txFrequency = (uint32_t)((entry.baseFrequency() + ((calcSpace * 125) * m_channelNo)));
+
+ m_txFreqLabel.setGeometry(FPoint(2, 2), FSize(4, 1));
+ m_txFreq.setGeometry(FPoint(6, 2), FSize(8, 1));
+
+ std::stringstream ss;
+ ss << std::fixed << std::setprecision(4) << (float)(txFrequency / 1000000.0f);
+
+ m_txFreq.setText(ss.str());
+
+ m_rxFreqLabel.setGeometry(FPoint(2, 3), FSize(4, 1));
+ m_rxFreq.setGeometry(FPoint(6, 3), FSize(8, 1));
+
+ ss.str(std::string());
+ ss << std::fixed << std::setprecision(4) << (float)(rxFrequency / 1000000.0f);
+
+ m_rxFreq.setText(ss.str());
+ }
+
+ // last TG
+ {
+ m_lastTGLabel.setGeometry(FPoint(2, 4), FSize(10, 1));
+
+ m_lastTG.setGeometry(FPoint(11, 4), FSize(8, 1));
+ m_lastTG.setText("None");
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ void onTimer(FTimerEvent* timer) override
+ {
+ if (timer != nullptr) {
+ // update timer
+ if (timer->getTimerId() == m_timerId) {
+ if (!m_failed) {
+ // callback REST API to get status of the channel we represent
+ json::object req = json::object();
+ json::object rsp = json::object();
+
+ int ret = RESTClient::send(m_chData.address(), m_chData.port(), m_chData.password(),
+ HTTP_GET, GET_STATUS, req, rsp, g_debug);
+ if (ret != network::rest::http::HTTPPayload::StatusType::OK) {
+ ::LogError(LOG_HOST, "failed to get status for %s:%u, chNo = %u", m_chData.address().c_str(), m_chData.port(), m_channelNo);
+ m_failed = true;
+ setText("FAILED");
+ }
+ else {
+ // get remote node state
+ if (rsp["dmrTSCCEnable"].is() && rsp["p25CtrlEnable"].is() &&
+ rsp["nxdnCtrlEnable"].is()) {
+ bool dmrTSCCEnable = rsp["dmrTSCCEnable"].get();
+ bool dmrCC = rsp["dmrCC"].get();
+ bool p25CtrlEnable = rsp["p25CtrlEnable"].get();
+ bool p25CC = rsp["p25CC"].get();
+ bool nxdnCtrlEnable = rsp["nxdnCtrlEnable"].get();
+ bool nxdnCC = rsp["nxdnCC"].get();
+
+ // are we a dedicated control channel?
+ if (dmrCC || p25CC || nxdnCC) {
+ m_control = true;
+ setText("CONTROL");
+ }
+
+ // if we aren't a dedicated control channel; set our
+ // title bar appropriately and set Tx state
+ if (!m_control) {
+ if (dmrTSCCEnable || p25CtrlEnable || nxdnCtrlEnable) {
+ setText("ENH. VOICE/CONV");
+ }
+ else {
+ setText("VOICE/CONV");
+ }
+
+ // are we transmitting?
+ if (rsp["tx"].is()) {
+ m_tx = rsp["tx"].get();
+ }
+ else {
+ ::LogWarning(LOG_HOST, "%s:%u, does not report Tx status");
+ m_tx = false;
+ }
+ }
+ }
+
+ // get the remote node channel information
+ if (rsp["channelId"].is() && rsp["channelNo"].is()) {
+ uint8_t channelId = rsp["channelId"].get();
+ uint32_t channelNo = rsp["channelNo"].get();
+ bool resetControls = false;
+
+ // verify channel ID matches our configuration
+ if (channelId != m_channelId) {
+ ::LogWarning(LOG_HOST, "%s:%u, reports chId = %u, disagrees with chId = %u", m_chData.address().c_str(), m_chData.port(), channelId, m_channelId);
+ m_channelId = channelId;
+ resetControls = true;
+ }
+
+ // verify channel number matches our configuration
+ if (channelNo != m_channelNo) {
+ ::LogWarning(LOG_HOST, "%s:%u, reports chNo = %u, disagrees with chNo = %u", m_chData.address().c_str(), m_chData.port(), channelNo, m_channelNo);
+ m_channelNo = channelNo;
+ resetControls = true;
+ }
+
+ // reset controls for our display if necessary
+ if (resetControls) {
+ initControls();
+ setText(getText() + "*");
+ }
+ }
+ else {
+ ::LogWarning(LOG_HOST, "%s:%u, does not report channel information");
+ }
+
+ // report last known transmitted destination ID
+ if (rsp["lastDstId"].is()) {
+ uint32_t lastDstId = rsp["lastDstId"].get();
+ if (lastDstId == 0) {
+ m_lastTG.setText("None");
+ }
+ else {
+ m_lastTG.setText(__INT_STR(lastDstId));
+ }
+ }
+ else {
+ ::LogWarning(LOG_HOST, "%s:%u, does not report last TG information");
+ }
+ }
+ }
+
+ redraw();
+ }
+
+ // reconnect timer
+ if (timer->getTimerId() == m_reconnectTimerId) {
+ if (m_failed) {
+ ::LogInfoEx(LOG_HOST, "attempting to reconnect to %s:%u, chNo = %u", m_chData.address().c_str(), m_chData.port(), m_channelNo);
+ // callback REST API to get status of the channel we represent
+ json::object req = json::object();
+ int ret = RESTClient::send(m_chData.address(), m_chData.port(), m_chData.password(),
+ HTTP_GET, GET_STATUS, req, g_debug);
+ if (ret != network::rest::http::HTTPPayload::StatusType::OK) {
+ ::LogError(LOG_HOST, "failed to get status for %s:%u, chNo = %u", m_chData.address().c_str(), m_chData.port(), m_channelNo);
+ }
+ else {
+ m_failed = false;
+ setText("UNKNOWN");
+ }
+ }
+
+ redraw();
+ }
+ }
+ }
+};
+
+#endif // __NODE_STATUS_WND_H__
\ No newline at end of file
diff --git a/src/network/RESTAPI.cpp b/src/network/RESTAPI.cpp
index 76363366..d6b35855 100644
--- a/src/network/RESTAPI.cpp
+++ b/src/network/RESTAPI.cpp
@@ -508,8 +508,11 @@ void RESTAPI::restAPI_GetStatus(const HTTPPayload& request, HTTPPayload& reply,
uint8_t protoVer = m_host->m_modem->getVersion();
response["protoVer"].set(protoVer);
+ response["dmrTSCCEnable"].set(m_host->m_dmrTSCCData);
response["dmrCC"].set(m_host->m_dmrCtrlChannel);
+ response["p25CtrlEnable"].set(m_host->m_p25CCData);
response["p25CC"].set(m_host->m_p25CtrlChannel);
+ response["nxdnCtrlEnable"].set(m_host->m_nxdnCCData);
response["nxdnCC"].set(m_host->m_nxdnCtrlChannel);
yaml::Node p25Protocol = m_host->m_conf["protocols"]["p25"];
@@ -519,6 +522,13 @@ void RESTAPI::restAPI_GetStatus(const HTTPPayload& request, HTTPPayload& reply,
response["p25VOC"].set(p25VOC);
response["nxdnVOC"].set(nxdnVOC);
+
+ response["tx"].set(m_host->m_modem->m_tx);
+
+ response["channelId"].set(m_host->m_channelId);
+ response["channelNo"].set(m_host->m_channelNo);
+
+ response["lastDstId"].set(m_host->m_lastDstId);
}
{
diff --git a/src/nxdn/Control.cpp b/src/nxdn/Control.cpp
index cb0f3f3b..8129d55d 100644
--- a/src/nxdn/Control.cpp
+++ b/src/nxdn/Control.cpp
@@ -748,6 +748,23 @@ void Control::setRCCHVerbose(bool verbose)
lc::RTCH::setVerbose(verbose);
}
+///
+/// Helper to get the last transmitted destination ID.
+///
+///
+uint32_t Control::getLastDstId() const
+{
+ if (m_rfLastDstId != 0U) {
+ return m_rfLastDstId;
+ }
+
+ if (m_netLastDstId != 0U) {
+ return m_netLastDstId;
+ }
+
+ return 0U;
+}
+
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------
diff --git a/src/nxdn/Control.h b/src/nxdn/Control.h
index 59ef27c4..8a28be80 100644
--- a/src/nxdn/Control.h
+++ b/src/nxdn/Control.h
@@ -130,6 +130,9 @@ namespace nxdn
/// Helper to change the RCCH verbose state.
void setRCCHVerbose(bool verbose);
+ /// Helper to get the last transmitted destination ID.
+ uint32_t getLastDstId() const;
+
private:
friend class packet::Voice;
packet::Voice* m_voice;
diff --git a/src/p25/Control.cpp b/src/p25/Control.cpp
index 4d07ed6e..9e1d3bff 100644
--- a/src/p25/Control.cpp
+++ b/src/p25/Control.cpp
@@ -861,6 +861,23 @@ void Control::setDebugVerbose(bool debug, bool verbose)
m_verbose = m_voice->m_verbose = m_data->m_verbose = m_trunk->m_verbose = verbose;
}
+///
+/// Helper to get the last transmitted destination ID.
+///
+///
+uint32_t Control::getLastDstId() const
+{
+ if (m_rfLastDstId != 0U) {
+ return m_rfLastDstId;
+ }
+
+ if (m_netLastDstId != 0U) {
+ return m_netLastDstId;
+ }
+
+ return 0U;
+}
+
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------
diff --git a/src/p25/Control.h b/src/p25/Control.h
index 8dac6047..ce9fb32b 100644
--- a/src/p25/Control.h
+++ b/src/p25/Control.h
@@ -139,6 +139,9 @@ namespace p25
/// Helper to change the debug and verbose state.
void setDebugVerbose(bool debug, bool verbose);
+ /// Helper to get the last transmitted destination ID.
+ uint32_t getLastDstId() const;
+
private:
friend class packet::Voice;
packet::Voice* m_voice;