From fe6f7508c03e2eaded82302840ca775a19fb3d53 Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Fri, 4 Oct 2024 23:52:29 -0400 Subject: [PATCH] add code dump of private sysview FNE monitoring utility to public GPL; correct issue with large REST responses getting truncated; --- CMakeLists.txt | 11 +- configs/fne-sysview.example.yml | 61 ++ src/CMakeLists.txt | 10 + .../network/rest/http/ClientConnection.h | 74 +- .../rest/http/SecureClientConnection.h | 70 +- src/remote/RESTClient.cpp | 5 +- src/remote/RESTClientMain.cpp | 4 +- src/sysview/AffListWnd.h | 243 ++++++ src/sysview/CMakeLists.txt | 22 + src/sysview/Defines.h | 33 + src/sysview/LogDisplayWnd.h | 144 ++++ src/sysview/PeerListWnd.h | 277 +++++++ src/sysview/SysViewApplication.h | 739 ++++++++++++++++++ src/sysview/SysViewMain.cpp | 287 +++++++ src/sysview/SysViewMain.h | 80 ++ src/sysview/SysViewMainWnd.h | 175 +++++ src/sysview/network/PeerNetwork.cpp | 99 +++ src/sysview/network/PeerNetwork.h | 68 ++ 18 files changed, 2367 insertions(+), 35 deletions(-) create mode 100644 configs/fne-sysview.example.yml create mode 100644 src/sysview/AffListWnd.h create mode 100644 src/sysview/CMakeLists.txt create mode 100644 src/sysview/Defines.h create mode 100644 src/sysview/LogDisplayWnd.h create mode 100644 src/sysview/PeerListWnd.h create mode 100644 src/sysview/SysViewApplication.h create mode 100644 src/sysview/SysViewMain.cpp create mode 100644 src/sysview/SysViewMain.h create mode 100644 src/sysview/SysViewMainWnd.h create mode 100644 src/sysview/network/PeerNetwork.cpp create mode 100644 src/sysview/network/PeerNetwork.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2702609d..44109b02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -252,9 +252,11 @@ install(TARGETS dvmcmd DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) install(TARGETS dvmfne DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) install(TARGETS dvmmon DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) +install(TARGETS sysview DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) +install(TARGETS tged DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) install(TARGETS dvmbridge DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) -install(FILES configs/config.example.yml configs/fne-config.example.yml configs/monitor-config.example.yml configs/iden_table.dat configs/RSSI.dat configs/rid_acl.example.dat configs/talkgroup_rules.example.yml configs/bridge-config.example.yml DESTINATION ${CMAKE_INSTALL_PREFIX}/etc) +install(FILES configs/config.example.yml configs/fne-config.example.yml configs/fne-sysview.example.yml configs/monitor-config.example.yml configs/iden_table.dat configs/RSSI.dat configs/rid_acl.example.dat configs/talkgroup_rules.example.yml configs/bridge-config.example.yml DESTINATION ${CMAKE_INSTALL_PREFIX}/etc) install(PROGRAMS tools/start-dvm.sh tools/stop-dvm.sh tools/dvm-watchdog.sh tools/stop-watchdog.sh tools/fne-watchdog.sh tools/start-dvm-fne.sh DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) install(CODE "execute_process(COMMAND bash \"-c\" \"sed -i 's/filePath: ./filePath: \\\\/var\\\\/log\\\\//' /usr/local/etc/config.example.yml\")") install(CODE "execute_process(COMMAND bash \"-c\" \"sed -i 's/activityFilePath: ./activityFilePath: \\\\/var\\\\/log\\\\//' /usr/local/etc/config.example.yml\")") @@ -273,6 +275,7 @@ if (NOT TARGET strip) COMMAND arm-linux-gnueabihf-strip -s dvmfne COMMAND arm-linux-gnueabihf-strip -s dvmcmd COMMAND arm-linux-gnueabihf-strip -s dvmmon + COMMAND arm-linux-gnueabihf-strip -s sysview COMMAND arm-linux-gnueabihf-strip -s tged COMMAND arm-linux-gnueabihf-strip -s dvmbridge) else() @@ -289,6 +292,7 @@ if (NOT TARGET strip) COMMAND aarch64-linux-gnu-strip -s dvmfne COMMAND aarch64-linux-gnu-strip -s dvmcmd COMMAND aarch64-linux-gnu-strip -s dvmmon + COMMAND aarch64-linux-gnu-strip -s sysview COMMAND aarch64-linux-gnu-strip -s tged COMMAND aarch64-linux-gnu-strip -s dvmbridge) else() @@ -319,6 +323,7 @@ if (NOT TARGET strip) COMMAND strip -s dvmfne COMMAND strip -s dvmcmd COMMAND strip -s dvmmon + COMMAND strip -s sysview COMMAND strip -s tged COMMAND strip -s dvmbridge) else() @@ -351,6 +356,7 @@ if (NOT TARGET tarball) COMMAND cp -v dvmhost ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp -v dvmcmd ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp -v dvmmon ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin + COMMAND cp -v sysview ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp -v tged ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp -v dvmfne ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp -v dvmbridge ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin @@ -429,11 +435,13 @@ add_custom_target(old_install COMMAND install -m 755 dvmhost ${CMAKE_LEGACY_INSTALL_PREFIX}/bin COMMAND install -m 755 dvmcmd ${CMAKE_LEGACY_INSTALL_PREFIX}/bin COMMAND install -m 755 dvmmon ${CMAKE_LEGACY_INSTALL_PREFIX}/bin + COMMAND install -m 755 sysview ${CMAKE_LEGACY_INSTALL_PREFIX}/bin COMMAND install -m 755 tged ${CMAKE_LEGACY_INSTALL_PREFIX}/bin COMMAND install -m 755 dvmfne ${CMAKE_LEGACY_INSTALL_PREFIX}/bin COMMAND install -m 755 dvmbridge ${CMAKE_LEGACY_INSTALL_PREFIX}/bin COMMAND install -m 644 ../configs/config.example.yml ${CMAKE_LEGACY_INSTALL_PREFIX}/config.example.yml COMMAND install -m 644 ../configs/fne-config.example.yml ${CMAKE_LEGACY_INSTALL_PREFIX}/fne-config.example.yml + COMMAND install -m 644 ../configs/fne-sysview.example.yml ${CMAKE_LEGACY_INSTALL_PREFIX}/fne-sysview.example.yml COMMAND install -m 644 ../configs/monitor-config.example.yml ${CMAKE_LEGACY_INSTALL_PREFIX}/monitor-config.example.yml COMMAND install -m 644 ../configs/iden_table.dat ${CMAKE_LEGACY_INSTALL_PREFIX}/iden_table.dat COMMAND install -m 644 ../configs/RSSI.dat ${CMAKE_LEGACY_INSTALL_PREFIX}/RSSI.dat @@ -475,6 +483,7 @@ add_custom_target(old_install-service COMMAND usermod --groups dialout --append dvmhost || true COMMAND chown dvmhost:dvmhost ${CMAKE_LEGACY_INSTALL_PREFIX}/config.example.yml COMMAND chown dvmhost:dvmhost ${CMAKE_LEGACY_INSTALL_PREFIX}/fne-config.example.yml + COMMAND chown dvmhost:dvmhost ${CMAKE_LEGACY_INSTALL_PREFIX}/fne-sysview.example.yml COMMAND chown dvmhost:dvmhost ${CMAKE_LEGACY_INSTALL_PREFIX}/monitor-config.example.yml COMMAND chown dvmhost:dvmhost ${CMAKE_LEGACY_INSTALL_PREFIX}/iden_table.dat COMMAND chown dvmhost:dvmhost ${CMAKE_LEGACY_INSTALL_PREFIX}/RSSI.dat diff --git a/configs/fne-sysview.example.yml b/configs/fne-sysview.example.yml new file mode 100644 index 00000000..b5981129 --- /dev/null +++ b/configs/fne-sysview.example.yml @@ -0,0 +1,61 @@ +# +# Digital Voice Modem - FNE System View +# + +# +# Radio ID ACL Configuration +# +radio_id: + # Full path to the Radio ID ACL file. + file: rid_acl.dat + # Amount of time between updates of Radio ID ACL file. (minutes) + time: 2 + +# +# Talkgroup Rules Configuration +# +talkgroup_rules: + # Full path to the talkgroup rules file. + file: talkgroup_rules.yml + # Amount of time between updates of talkgroup rules file. (minutes) + time: 30 + +# +# Channel Identity Table Configuration +# +iden_table: + # Full path to the identity table file. + file: iden_table.dat + # Amount of time between updates of identity table file. (minutes) + time: 30 + +# +# FNE Configuration +# +fne: + # Hostname/IP address of the FNE master to connect to. + masterAddress: 127.0.0.1 + # Port number of the FNE master to connect to. + masterPort: 32090 + # FNE access password. + password: RPT1234 + # Textual identity of this peer. + identity: EXPEER + # Network Peer ID + peerId: 9000990 + + # Flag indicating whether or not peer endpoint networking is encrypted. + encrypted: false + # AES-256 32-byte Preshared Key + # (This field *must* be 32 hex bytes in length or 64 characters + # 0 - 9, A - F.) + presharedKey: "000102030405060708090A0B0C0D0E0F000102030405060708090A0B0C0D0E0F" + + # IP address of the FNE REST API. + restAddress: 127.0.0.1 + # Port number for the FNE REST API. + restPort: 9990 + # Flag indicating whether or not REST API is operating in SSL mode. + restSsl: false + # REST API authentication password. + restPassword: "PASSWORD" \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d75e0ba4..05a05b26 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -89,6 +89,16 @@ if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) target_include_directories(dvmmon PRIVATE ${OPENSSL_INCLUDE_DIR} src src/host src/monitor) endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) +# +## sysview +# +if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) + include(src/sysview/CMakeLists.txt) + add_executable(sysview ${common_INCLUDE} ${sysView_SRC}) + target_link_libraries(sysview PRIVATE common ${OPENSSL_LIBRARIES} asio::asio finalcut Threads::Threads) + target_include_directories(sysview PRIVATE ${OPENSSL_INCLUDE_DIR} src src/host src/sysview) +endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) + # ## tged # diff --git a/src/common/network/rest/http/ClientConnection.h b/src/common/network/rest/http/ClientConnection.h index 918c7d32..7c4f4ff3 100644 --- a/src/common/network/rest/http/ClientConnection.h +++ b/src/common/network/rest/http/ClientConnection.h @@ -106,6 +106,7 @@ namespace network */ void send(HTTPPayload request) { + m_sizeToTransfer = m_bytesTransferred = 0U; request.attachHostHeader(m_socket.remote_endpoint()); write(request); } @@ -122,24 +123,61 @@ namespace network try { - std::tie(result, content) = m_lexer.parse(m_request, m_buffer.data(), m_buffer.data() + bytes_transferred); + if (m_sizeToTransfer > 0U && (m_bytesTransferred + bytes_transferred) < m_sizeToTransfer) { + ::memcpy(m_fullBuffer.data() + m_bytesTransferred, m_buffer.data(), bytes_transferred); + m_bytesTransferred += bytes_transferred; - std::string contentLength = m_request.headers.find("Content-Length"); - if (contentLength != "") { - size_t length = (size_t)::strtoul(contentLength.c_str(), NULL, 10); - m_request.content = std::string(content, length); - } - - m_request.headers.add("RemoteHost", m_socket.remote_endpoint().address().to_string()); - - if (result == HTTPLexer::GOOD) { - m_requestHandler.handleRequest(m_request, m_reply); - } - else if (result == HTTPLexer::BAD) { - return; + read(); } else { - read(); + if (m_sizeToTransfer > 0U) { + // final copy + ::memcpy(m_fullBuffer.data() + m_bytesTransferred, m_buffer.data(), bytes_transferred); + m_bytesTransferred += bytes_transferred; + + m_sizeToTransfer = 0U; + bytes_transferred = m_bytesTransferred; + + // reset lexer and re-parse the full content + m_lexer.reset(); + std::tie(result, content) = m_lexer.parse(m_request, m_fullBuffer.data(), m_fullBuffer.data() + bytes_transferred); + } else { + ::memcpy(m_fullBuffer.data() + m_bytesTransferred, m_buffer.data(), bytes_transferred); + m_bytesTransferred += bytes_transferred; + + std::tie(result, content) = m_lexer.parse(m_request, m_buffer.data(), m_buffer.data() + bytes_transferred); + } + + // determine content length + std::string contentLength = m_request.headers.find("Content-Length"); + if (contentLength != "") { + size_t length = (size_t)::strtoul(contentLength.c_str(), NULL, 10); + + // setup a full read if necessary + if (length > bytes_transferred && m_sizeToTransfer == 0U) { + m_sizeToTransfer = length; + } + + if (m_sizeToTransfer > 0U) { + result = HTTPLexer::CONTINUE; + } else { + m_request.content = std::string(content, length); + } + } + + m_request.headers.add("RemoteHost", m_socket.remote_endpoint().address().to_string()); + + if (result == HTTPLexer::GOOD) { + m_sizeToTransfer = m_bytesTransferred = 0U; + m_requestHandler.handleRequest(m_request, m_reply); + } + else if (result == HTTPLexer::BAD) { + m_sizeToTransfer = m_bytesTransferred = 0U; + return; + } + else { + read(); + } } } catch(const std::exception& e) { ::LogError(LOG_REST, "ClientConnection::read(), %s", ec.message().c_str()); } @@ -187,7 +225,11 @@ namespace network RequestHandlerType& m_requestHandler; - std::array m_buffer; + std::size_t m_sizeToTransfer; + std::size_t m_bytesTransferred; + std::array m_fullBuffer; + + std::array m_buffer; HTTPPayload m_request; HTTPLexer m_lexer; diff --git a/src/common/network/rest/http/SecureClientConnection.h b/src/common/network/rest/http/SecureClientConnection.h index d798b52c..7083254c 100644 --- a/src/common/network/rest/http/SecureClientConnection.h +++ b/src/common/network/rest/http/SecureClientConnection.h @@ -145,24 +145,60 @@ namespace network try { - std::tie(result, content) = m_lexer.parse(m_request, m_buffer.data(), m_buffer.data() + bytes_transferred); + if (m_sizeToTransfer > 0U && (m_bytesTransferred + bytes_transferred) < m_sizeToTransfer) { + ::memcpy(m_fullBuffer.data() + m_bytesTransferred, m_buffer.data(), bytes_transferred); + m_bytesTransferred += bytes_transferred; - std::string contentLength = m_request.headers.find("Content-Length"); - if (contentLength != "") { - size_t length = (size_t)::strtoul(contentLength.c_str(), NULL, 10); - m_request.content = std::string(content, length); + read(); } + else { + if (m_sizeToTransfer > 0U) { + // final copy + ::memcpy(m_fullBuffer.data() + m_bytesTransferred, m_buffer.data(), bytes_transferred); + m_bytesTransferred += bytes_transferred; - m_request.headers.add("RemoteHost", m_socket.lowest_layer().remote_endpoint().address().to_string()); + m_sizeToTransfer = 0U; + bytes_transferred = m_bytesTransferred; - if (result == HTTPLexer::GOOD) { - m_requestHandler.handleRequest(m_request, m_reply); - } - else if (result == HTTPLexer::BAD) { - return; - } - else { - read(); + // reset lexer and re-parse the full content + m_lexer.reset(); + std::tie(result, content) = m_lexer.parse(m_request, m_fullBuffer.data(), m_fullBuffer.data() + bytes_transferred); + } else { + ::memcpy(m_fullBuffer.data() + m_bytesTransferred, m_buffer.data(), bytes_transferred); + m_bytesTransferred += bytes_transferred; + + std::tie(result, content) = m_lexer.parse(m_request, m_buffer.data(), m_buffer.data() + bytes_transferred); + } + + // determine content length + std::string contentLength = m_request.headers.find("Content-Length"); + if (contentLength != "") { + size_t length = (size_t)::strtoul(contentLength.c_str(), NULL, 10); + + // setup a full read if necessary + if (length > bytes_transferred && m_sizeToTransfer == 0U) { + m_sizeToTransfer = length; + } + + if (m_sizeToTransfer > 0U) { + result = HTTPLexer::CONTINUE; + } else { + m_request.content = std::string(content, length); + } + } + + m_request.headers.add("RemoteHost", m_socket.lowest_layer().remote_endpoint().address().to_string()); + if (result == HTTPLexer::GOOD) { + m_sizeToTransfer = m_bytesTransferred = 0U; + m_requestHandler.handleRequest(m_request, m_reply); + } + else if (result == HTTPLexer::BAD) { + m_sizeToTransfer = m_bytesTransferred = 0U; + return; + } + else { + read(); + } } } catch(const std::exception& e) { ::LogError(LOG_REST, "SecureClientConnection::read(), %s", ec.message().c_str()); } @@ -212,7 +248,11 @@ namespace network RequestHandlerType& m_requestHandler; - std::array m_buffer; + std::size_t m_sizeToTransfer; + std::size_t m_bytesTransferred; + std::array m_fullBuffer; + + std::array m_buffer; HTTPPayload m_request; HTTPLexer m_lexer; diff --git a/src/remote/RESTClient.cpp b/src/remote/RESTClient.cpp index 0b187149..de5cf11a 100644 --- a/src/remote/RESTClient.cpp +++ b/src/remote/RESTClient.cpp @@ -332,7 +332,10 @@ int RESTClient::send(const std::string& address, uint32_t port, const std::strin } else { if (m_debug) { - ::LogDebug(LOG_REST, "REST Response: %s", m_response.content.c_str()); + if (m_response.content.size() < 4095) { + ::LogDebug(LOG_REST, "REST Response: %s", m_response.content.c_str()); + } + // bryanb: this will cause REST responses >4095 characters to simply not print... } } diff --git a/src/remote/RESTClientMain.cpp b/src/remote/RESTClientMain.cpp index 42361f93..b7b1a8bb 100644 --- a/src/remote/RESTClientMain.cpp +++ b/src/remote/RESTClientMain.cpp @@ -186,8 +186,8 @@ void usage(const char* message, const char* arg) " -v show version information\n" " -h show this screen\n" "\n" - " -a remote modem command address\n" - " -p remote modem command port\n" + " -a remote DVM REST address\n" + " -p remote DVM REST port\n" " -P remote modem authentication password\n" "\n" " -s use HTTPS/SSL\n" diff --git a/src/sysview/AffListWnd.h b/src/sysview/AffListWnd.h new file mode 100644 index 00000000..b83e13ba --- /dev/null +++ b/src/sysview/AffListWnd.h @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - FNE System View + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file AffListWnd.h + * @ingroup fneSysView + */ +#if !defined(__AFF_LIST_WND_H__) +#define __AFF_LIST_WND_H__ + +#include "common/lookups/AffiliationLookup.h" +#include "common/Log.h" +#include "fne/network/RESTDefines.h" +#include "remote/RESTClient.h" + +#include "SysViewMainWnd.h" + +#include +using namespace finalcut; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#define AFF_LIST_WIDTH 74 +#define AFF_LIST_HEIGHT 15 + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the affiliations list window. + * @ingroup fneSysView + */ +class HOST_SW_API AffListWnd final : public finalcut::FDialog { +public: + /** + * @brief Initializes a new instance of the AffListWnd class. + * @param widget + */ + explicit AffListWnd(FWidget* widget = nullptr) : FDialog{widget} + { + m_timerId = addTimer(10000); // starts the timer every 10 seconds + } + /** + * @brief Copy constructor. + */ + AffListWnd(const AffListWnd&) = delete; + /** + * @brief Move constructor. + */ + AffListWnd(AffListWnd&&) noexcept = delete; + /** + * @brief Finalizes an instance of the AffListWnd class. + */ + ~AffListWnd() noexcept override = default; + + /** + * @brief Disable copy assignment operator (=). + */ + auto operator= (const AffListWnd&) -> AffListWnd& = delete; + /** + * @brief Disable move assignment operator (=). + */ + auto operator= (AffListWnd&&) noexcept -> AffListWnd& = delete; + + /** + * @brief Disable set X coordinate. + */ + void setX(int, bool = true) override { } + /** + * @brief Disable set Y coordinate. + */ + void setY(int, bool = true) override { } + /** + * @brief Disable set position. + */ + void setPos(const FPoint&, bool = true) override { } + + /** + * @brief Populates the talkgroup listview. + */ + void loadListView() + { + yaml::Node fne = g_conf["fne"]; + std::string fneRESTAddress = fne["restAddress"].as("127.0.0.1"); + uint16_t fneRESTPort = (uint16_t)fne["restPort"].as(9990U); + std::string fnePassword = fne["restPassword"].as("PASSWORD"); + bool fneSSL = fne["restSsl"].as(false); + + // 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(fneRESTAddress, fneRESTPort, fnePassword, + HTTP_GET, FNE_GET_AFF_LIST, req, rsp, fneSSL, g_debug); + if (ret != network::rest::http::HTTPPayload::StatusType::OK) { + ::LogError(LOG_HOST, "[AFFVIEW] failed to get affiliations for %s:%u", fneRESTAddress.c_str(), fneRESTPort); + } + else { + try { + m_listView.clear(); + + json::array fneAffils = rsp["affiliations"].get(); + for (auto entry : fneAffils) { + json::object peerAffils = entry.get(); + uint32_t peerId = peerAffils["peerId"].get(); + json::array affils = peerAffils["affiliations"].get(); + for (auto pentry : affils) { + json::object peerEntry = pentry.get(); + uint32_t dstId = peerEntry["dstId"].get(); + uint32_t srcId = peerEntry["srcId"].get(); + + // pad peer IDs properly + std::ostringstream peerOss; + peerOss << std::setw(9) << std::setfill('0') << peerId; + + // pad TGs properly + std::ostringstream tgidOss; + tgidOss << std::setw(5) << std::setfill('0') << dstId; + + // build list view entry + const std::array columns = { + peerOss.str(), + std::to_string(srcId), + tgidOss.str() + }; + + const finalcut::FStringList line(columns.cbegin(), columns.cend()); + m_listView.insert(line); + } + } + } + catch (std::exception& e) { + ::LogWarning(LOG_HOST, "[AFFVIEW] %s:%u, failed to properly handle affiliation request, %s", fneRESTAddress.c_str(), fneRESTPort, e.what()); + } + } + + setFocusWidget(&m_listView); + redraw(); + } + +private: + int m_timerId; + + FListView m_listView{this}; + + FButton m_refresh{"&Refresh", this}; + + /** + * @brief Initializes the window layout. + */ + void initLayout() override + { + FDialog::setMinimumSize(FSize{AFF_LIST_WIDTH, AFF_LIST_HEIGHT}); + + FDialog::setResizeable(false); + FDialog::setMinimizable(false); + FDialog::setTitlebarButtonVisibility(false); + FDialog::setModal(false); + + FDialog::setText("Affiliations View (10s)"); + + initControls(); + loadListView(); + + FDialog::initLayout(); + } + + /** + * @brief Initializes window controls. + */ + void initControls() + { + m_refresh.setGeometry(FPoint(int(getWidth()) - 12, 1), FSize(9, 1)); + m_refresh.addCallback("clicked", [&]() { loadListView(); }); + + m_listView.setGeometry(FPoint{1, 3}, FSize{getWidth() - 1, getHeight() - 5}); + + // configure list view columns + m_listView.addColumn("Peer ID", 10); + m_listView.addColumn("RID", 10); + m_listView.addColumn("TGID", 9); + + // set right alignment for TGID + m_listView.setColumnAlignment(1, finalcut::Align::Right); + m_listView.setColumnAlignment(2, finalcut::Align::Right); + m_listView.setColumnAlignment(3, finalcut::Align::Right); + + // set type of sorting + m_listView.setColumnSortType(1, finalcut::SortType::Name); + m_listView.setColumnSortType(2, finalcut::SortType::Name); + m_listView.setColumnSortType(3, finalcut::SortType::Name); + + // sort by TGID + m_listView.setColumnSort(1, finalcut::SortOrder::Ascending); + m_listView.setColumnSort(3, finalcut::SortOrder::Ascending); + + setFocusWidget(&m_listView); + redraw(); + } + + /* + ** Event Handlers + */ + + /** + * @brief Event that occurs on keyboard key press. + * @param e Keyboard Event. + */ + void onKeyPress(finalcut::FKeyEvent* e) override + { + const auto key = e->key(); + if (key == FKey::F5) { + loadListView(); + } + } + + + /** + * @brief Event that occurs on interval by timer. + * @param timer Timer Event + */ + void onTimer(FTimerEvent* timer) override + { + if (timer != nullptr) { + // update timer + if (timer->getTimerId() == m_timerId) { + loadListView(); + redraw(); + } + } + } +}; + +#endif // __AFF_LIST_WND_H__ \ No newline at end of file diff --git a/src/sysview/CMakeLists.txt b/src/sysview/CMakeLists.txt new file mode 100644 index 00000000..c6b4e961 --- /dev/null +++ b/src/sysview/CMakeLists.txt @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: GPL-2.0-only +#/* +# * Digital Voice Modem - FNE System View +# * GPLv2 Open Source. Use is subject to license terms. +# * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# * +# * Copyright (C) 2024 Bryan Biedenkapp, N2PLL +# * +# */ +file(GLOB sysView_SRC + "src/host/modem/Modem.h" + "src/host/network/Network.h" + "src/host/network/Network.cpp" + + "src/remote/RESTClient.cpp" + "src/remote/RESTClient.h" + + "src/sysview/network/*.h" + "src/sysview/network/*.cpp" + "src/sysview/*.h" + "src/sysview/*.cpp" +) diff --git a/src/sysview/Defines.h b/src/sysview/Defines.h new file mode 100644 index 00000000..cb32edcd --- /dev/null +++ b/src/sysview/Defines.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - FNE System View + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2023 Bryan Biedenkapp, N2PLL + * + */ +/** + * @defgroup fneSysView FNE System View (dvmmon) + * @brief Digital Voice Modem - FNE System View + * @details Monitor software that connects to the DVM FNE and is a quick TUI for monitoring affiliations on them. + * @ingroup fneSysView + * + * @file Defines.h + * @ingroup fneSysView + */ +#if !defined(__DEFINES_H__) +#define __DEFINES_H__ + +#include "common/Defines.h" + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#undef __PROG_NAME__ +#define __PROG_NAME__ "Digital Voice Modem (DVM) FNE System View" +#undef __EXE_NAME__ +#define __EXE_NAME__ "sysview" + +#endif // __DEFINES_H__ diff --git a/src/sysview/LogDisplayWnd.h b/src/sysview/LogDisplayWnd.h new file mode 100644 index 00000000..3ff039b3 --- /dev/null +++ b/src/sysview/LogDisplayWnd.h @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - FNE System View + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2023 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file LogDisplayWnd.h + * @ingroup fneSysView + */ +#if !defined(__LOG_DISPLAY_WND_H__) +#define __LOG_DISPLAY_WND_H__ + +#include +using namespace finalcut; + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the log display window. + * @ingroup fneSysView + */ +class HOST_SW_API LogDisplayWnd final : public finalcut::FDialog, public std::ostringstream { +public: + /** + * @brief Initializes a new instance of the LogDisplayWnd class. + * @param widget + */ + explicit LogDisplayWnd(FWidget* widget = nullptr) : FDialog{widget} + { + m_scrollText.ignorePadding(); + + m_timerId = addTimer(250); // starts the timer every 250 milliseconds + } + /** + * @brief Copy constructor. + */ + LogDisplayWnd(const LogDisplayWnd&) = delete; + /** + * @brief Move constructor. + */ + LogDisplayWnd(LogDisplayWnd&&) noexcept = delete; + /** + * @brief Finalizes an instance of the LogDisplayWnd class. + */ + ~LogDisplayWnd() noexcept override = default; + + /** + * @brief Disable copy assignment operator (=). + */ + auto operator= (const LogDisplayWnd&) -> LogDisplayWnd& = delete; + /** + * @brief Disable move assignment operator (=). + */ + auto operator= (LogDisplayWnd&&) noexcept -> LogDisplayWnd& = delete; + +private: + FTextView m_scrollText{this}; + int m_timerId; + + /** + * @brief Initializes the window layout. + */ + void initLayout() override + { + using namespace std::string_literals; + auto lightning = "\u26a1"; + FDialog::setText("System Log"s + lightning); + + std::size_t maxWidth; + const auto& rootWidget = getRootWidget(); + + if (rootWidget) { + maxWidth = rootWidget->getClientWidth() - 3; + } + else { + // fallback to xterm default size + maxWidth = 77; + } + + FDialog::setGeometry(FPoint{2, (int)(rootWidget->getClientHeight() - 40)}, FSize{maxWidth, 40}); + 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(); + } + + /** + * @brief Adjusts window size. + */ + void adjustSize() override + { + FDialog::adjustSize(); + + m_scrollText.setGeometry(FPoint{1, 2}, FSize{getWidth(), getHeight() - 1}); + } + + /* + ** Event Handlers + */ + + /** + * @brief Event that occurs when the window is closed. + * @param e Close Event + */ + void onClose(FCloseEvent* e) override + { + minimizeWindow(); + } + + /** + * @brief Event that occurs on interval by timer. + * @param timer Timer Event + */ + 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/sysview/PeerListWnd.h b/src/sysview/PeerListWnd.h new file mode 100644 index 00000000..87bb6ae8 --- /dev/null +++ b/src/sysview/PeerListWnd.h @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - FNE System View + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file PeerListWnd.h + * @ingroup fneSysView + */ +#if !defined(__PEER_LIST_WND_H__) +#define __PEER_LIST_WND_H__ + +#include "common/lookups/AffiliationLookup.h" +#include "common/Log.h" +#include "fne/network/RESTDefines.h" +#include "remote/RESTClient.h" + +#include "SysViewMainWnd.h" + +#include +using namespace finalcut; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#define PEER_LIST_WIDTH 56 +#define PEER_LIST_HEIGHT 15 + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the peer list window. + * @ingroup fneSysView + */ +class HOST_SW_API PeerListWnd final : public finalcut::FDialog { +public: + /** + * @brief Initializes a new instance of the PeerListWnd class. + * @param widget + */ + explicit PeerListWnd(FWidget* widget = nullptr) : FDialog{widget} + { + m_timerId = addTimer(25000); // starts the timer every 25 seconds + } + /** + * @brief Copy constructor. + */ + PeerListWnd(const PeerListWnd&) = delete; + /** + * @brief Move constructor. + */ + PeerListWnd(PeerListWnd&&) noexcept = delete; + /** + * @brief Finalizes an instance of the PeerListWnd class. + */ + ~PeerListWnd() noexcept override = default; + + /** + * @brief Disable copy assignment operator (=). + */ + auto operator= (const PeerListWnd&) -> PeerListWnd& = delete; + /** + * @brief Disable move assignment operator (=). + */ + auto operator= (PeerListWnd&&) noexcept -> PeerListWnd& = delete; + + /** + * @brief Disable set X coordinate. + */ + void setX(int, bool = true) override { } + /** + * @brief Disable set Y coordinate. + */ + void setY(int, bool = true) override { } + /** + * @brief Disable set position. + */ + void setPos(const FPoint&, bool = true) override { } + + /** + * @brief Populates the talkgroup listview. + */ + void loadListView() + { + yaml::Node fne = g_conf["fne"]; + std::string fneRESTAddress = fne["restAddress"].as("127.0.0.1"); + uint16_t fneRESTPort = (uint16_t)fne["restPort"].as(9990U); + std::string fnePassword = fne["restPassword"].as("PASSWORD"); + bool fneSSL = fne["restSsl"].as(false); + + // 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(fneRESTAddress, fneRESTPort, fnePassword, + HTTP_GET, FNE_GET_PEER_QUERY, req, rsp, fneSSL, g_debug); + if (ret != network::rest::http::HTTPPayload::StatusType::OK) { + ::LogError(LOG_HOST, "[AFFVIEW] failed to query peers for %s:%u", fneRESTAddress.c_str(), fneRESTPort); + } + else { + try { + m_listView.clear(); + + json::array fnePeers = rsp["peers"].get(); + for (auto entry : fnePeers) { + json::object peerObj = entry.get(); + uint32_t peerId = peerObj["peerId"].get(); + std::string peerAddress = peerObj["address"].get(); + uint16_t port = (uint16_t)peerObj["port"].get(); + bool connected = peerObj["connected"].get(); + uint32_t connectionState = (uint32_t)peerObj["connectionState"].get(); + std::string strConnState = ""; + switch (connectionState) { + case network::NET_CONN_STATUS::NET_STAT_RUNNING: + strConnState = "Connected"; + break; + case network::NET_CONN_STATUS::NET_STAT_WAITING_LOGIN: + strConnState = "Waiting for Login"; + break; + case network::NET_CONN_STATUS::NET_STAT_WAITING_AUTHORISATION: + strConnState = "Waiting for Auth"; + break; + case network::NET_CONN_STATUS::NET_STAT_WAITING_CONFIG: + strConnState = "Waiting for Config"; + break; + default: + strConnState = " ?? "; + break; + } + + uint32_t pingsReceived = (uint32_t)peerObj["pingsReceived"].get(); + uint64_t lastPing = (uint64_t)peerObj["lastPing"].get(); + + uint32_t ccPeerId = (uint32_t)peerObj["controlChannel"].get(); + + json::array voiceChannels = peerObj["voiceChannels"].get(); + std::vector voiceChannelPeers; + for (auto vcEntry : voiceChannels) { + uint32_t vcPeerId = vcEntry.get(); + voiceChannelPeers.push_back(vcPeerId); + } + + // pad peer IDs properly + std::ostringstream peerOss; + peerOss << std::setw(9) << std::setfill('0') << peerId; + + // pad peer IDs properly + std::ostringstream ccPeerOss; + ccPeerOss << std::setw(9) << std::setfill('0') << ccPeerId; + + // build list view entry + const std::array columns = { + peerOss.str(), + peerAddress, std::to_string(port), + ccPeerOss.str(), + (voiceChannelPeers.size() > 0U) ? "X" : "", + (connected) ? "X" : "", + strConnState, + std::to_string(pingsReceived) + }; + + const finalcut::FStringList line(columns.cbegin(), columns.cend()); + m_listView.insert(line); + } + } + catch (std::exception& e) { + ::LogWarning(LOG_HOST, "[AFFVIEW] %s:%u, failed to properly handle peer query request, %s", fneRESTAddress.c_str(), fneRESTPort, e.what()); + } + } + + setFocusWidget(&m_listView); + redraw(); + } + +private: + int m_timerId; + + FListView m_listView{this}; + + FButton m_refresh{"&Refresh", this}; + + /** + * @brief Initializes the window layout. + */ + void initLayout() override + { + FDialog::setMinimumSize(FSize{PEER_LIST_WIDTH, PEER_LIST_HEIGHT}); + + FDialog::setResizeable(false); + FDialog::setMinimizable(false); + FDialog::setTitlebarButtonVisibility(false); + FDialog::setModal(false); + + FDialog::setText("Peers View (25s)"); + + initControls(); + loadListView(); + + FDialog::initLayout(); + } + + /** + * @brief Initializes window controls. + */ + void initControls() + { + m_refresh.setGeometry(FPoint(int(getWidth()) - 12, 1), FSize(9, 1)); + m_refresh.addCallback("clicked", [&]() { loadListView(); }); + + m_listView.setGeometry(FPoint{1, 3}, FSize{getWidth() - 1, getHeight() - 5}); + + // configure list view columns + m_listView.addColumn("Peer ID", 10); + m_listView.addColumn("IP Address", 15); + m_listView.addColumn("Port", 8); + m_listView.addColumn("CC Peer ID", 10); + m_listView.addColumn("VC Count", 8); + m_listView.addColumn("Connected", 5); + m_listView.addColumn("State", 15); + m_listView.addColumn("Pings Received", 8); + + // set right alignment for TGID + m_listView.setColumnAlignment(1, finalcut::Align::Right); + m_listView.setColumnAlignment(4, finalcut::Align::Right); + m_listView.setColumnAlignment(6, finalcut::Align::Center); + + // set type of sorting + m_listView.setColumnSortType(1, finalcut::SortType::Name); + + // sort by TGID + m_listView.setColumnSort(1, finalcut::SortOrder::Ascending); + + setFocusWidget(&m_listView); + redraw(); + } + + /* + ** Event Handlers + */ + + /** + * @brief Event that occurs on keyboard key press. + * @param e Keyboard Event. + */ + void onKeyPress(finalcut::FKeyEvent* e) override + { + const auto key = e->key(); + if (key == FKey::F5) { + loadListView(); + } + } + + + /** + * @brief Event that occurs on interval by timer. + * @param timer Timer Event + */ + void onTimer(FTimerEvent* timer) override + { + if (timer != nullptr) { + // update timer + if (timer->getTimerId() == m_timerId) { + loadListView(); + redraw(); + } + } + } +}; + +#endif // __PEER_LIST_WND_H__ \ No newline at end of file diff --git a/src/sysview/SysViewApplication.h b/src/sysview/SysViewApplication.h new file mode 100644 index 00000000..83dc20fe --- /dev/null +++ b/src/sysview/SysViewApplication.h @@ -0,0 +1,739 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - FNE System View + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file SysViewApplication.h + * @ingroup fneSysView + */ +#if !defined(__SYS_VIEW_APPLICATION_H__) +#define __SYS_VIEW_APPLICATION_H__ + +#include "common/Clock.h" +#include "common/dmr/DMRDefines.h" +#include "common/dmr/lc/csbk/CSBKFactory.h" +#include "common/dmr/lc/LC.h" +#include "common/dmr/lc/FullLC.h" +#include "common/dmr/SlotType.h" +#include "common/dmr/Sync.h" +#include "common/p25/P25Defines.h" +#include "common/p25/lc/tdulc/TDULCFactory.h" +#include "common/p25/lc/tsbk/TSBKFactory.h" +#include "common/nxdn/NXDNDefines.h" +#include "common/nxdn/lc/RTCH.h" +#include "common/Log.h" +#include "common/StopWatch.h" +#include "network/PeerNetwork.h" +#include "SysViewMain.h" +#include "SysViewMainWnd.h" + +using namespace system_clock; +using namespace network; + +#include +using namespace finalcut; + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements a color theme for a finalcut application. + * @ingroup fneSysView + */ +class HOST_SW_API dvmColorTheme final : public FWidgetColors +{ +public: + /** + * @brief Initializes a new instance of the dvmColorTheme class. + */ + dvmColorTheme() + { + dvmColorTheme::setColorTheme(); + } + + /** + * @brief Finalizes a instance of the dvmColorTheme class. + */ + ~dvmColorTheme() noexcept override = default; + + /** + * @brief Get the Class Name object + * @return FString + */ + auto getClassName() const -> FString override { return "dvmColorTheme"; } + /** + * @brief Set the Color Theme object + */ + void setColorTheme() override + { + term_fg = FColor::Cyan; + term_bg = FColor::Blue; + + list_fg = FColor::Black; + list_bg = FColor::LightGray; + selected_list_fg = FColor::Red; + selected_list_bg = FColor::LightGray; + + dialog_fg = FColor::Black; + dialog_resize_fg = FColor::LightBlue; + dialog_emphasis_fg = FColor::Blue; + dialog_bg = FColor::LightGray; + + error_box_fg = FColor::LightRed; + error_box_emphasis_fg = FColor::Yellow; + error_box_bg = FColor::Black; + + tooltip_fg = FColor::White; + tooltip_bg = FColor::Black; + + shadow_fg = FColor::Black; + shadow_bg = FColor::LightGray; // only for transparent shadow + + current_element_focus_fg = FColor::White; + current_element_focus_bg = FColor::Cyan; + current_element_fg = FColor::LightBlue; + current_element_bg = FColor::Cyan; + current_inc_search_element_fg = FColor::LightRed; + selected_current_element_focus_fg = FColor::LightRed; + selected_current_element_focus_bg = FColor::Cyan; + selected_current_element_fg = FColor::Red; + selected_current_element_bg = FColor::Cyan; + + label_fg = FColor::Black; + label_bg = FColor::LightGray; + label_inactive_fg = FColor::DarkGray; + label_inactive_bg = FColor::LightGray; + label_hotkey_fg = FColor::Red; + label_hotkey_bg = FColor::LightGray; + label_emphasis_fg = FColor::Blue; + label_ellipsis_fg = FColor::DarkGray; + + inputfield_active_focus_fg = FColor::Yellow; + inputfield_active_focus_bg = FColor::Blue; + inputfield_active_fg = FColor::LightGray; + inputfield_active_bg = FColor::Blue; + inputfield_inactive_fg = FColor::Black; + inputfield_inactive_bg = FColor::DarkGray; + + toggle_button_active_focus_fg = FColor::Yellow; + toggle_button_active_focus_bg = FColor::Blue; + toggle_button_active_fg = FColor::LightGray; + toggle_button_active_bg = FColor::Blue; + toggle_button_inactive_fg = FColor::Black; + toggle_button_inactive_bg = FColor::DarkGray; + + button_active_focus_fg = FColor::Yellow; + button_active_focus_bg = FColor::Blue; + button_active_fg = FColor::White; + button_active_bg = FColor::Blue; + button_inactive_fg = FColor::Black; + button_inactive_bg = FColor::DarkGray; + button_hotkey_fg = FColor::Yellow; + + titlebar_active_fg = FColor::Blue; + titlebar_active_bg = FColor::White; + titlebar_inactive_fg = FColor::Blue; + titlebar_inactive_bg = FColor::LightGray; + titlebar_button_fg = FColor::Yellow; + titlebar_button_bg = FColor::LightBlue; + titlebar_button_focus_fg = FColor::LightGray; + titlebar_button_focus_bg = FColor::Black; + + menu_active_focus_fg = FColor::Black; + menu_active_focus_bg = FColor::White; + menu_active_fg = FColor::Black; + menu_active_bg = FColor::LightGray; + menu_inactive_fg = FColor::DarkGray; + menu_inactive_bg = FColor::LightGray; + menu_hotkey_fg = FColor::Blue; + menu_hotkey_bg = FColor::LightGray; + + statusbar_fg = FColor::Black; + statusbar_bg = FColor::LightGray; + statusbar_hotkey_fg = FColor::Blue; + statusbar_hotkey_bg = FColor::LightGray; + statusbar_separator_fg = FColor::Black; + statusbar_active_fg = FColor::Black; + statusbar_active_bg = FColor::White; + statusbar_active_hotkey_fg = FColor::Blue; + statusbar_active_hotkey_bg = FColor::White; + + scrollbar_fg = FColor::Cyan; + scrollbar_bg = FColor::DarkGray; + scrollbar_button_fg = FColor::Yellow; + scrollbar_button_bg = FColor::DarkGray; + scrollbar_button_inactive_fg = FColor::LightGray; + scrollbar_button_inactive_bg = FColor::Black; + + progressbar_fg = FColor::Yellow; + progressbar_bg = FColor::Blue; + } +}; + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the finalcut application. + * @ingroup fneSysView + */ +class HOST_SW_API SysViewApplication final : public finalcut::FApplication { +public: + /** + * @brief Initializes a new instance of the SysViewApplication class. + * @param argc Passed argc. + * @param argv Passed argv. + */ + explicit SysViewApplication(const int& argc, char** argv) : FApplication{argc, argv} + { + m_stopWatch.start(); + } + /** + * @brief Finalizes an instance of the SysViewApplication class. + */ + ~SysViewApplication() noexcept override + { + closePeerNetwork(); + } + + /** + * @brief Initializes peer network connectivity. + * @returns bool + */ + bool createPeerNetwork() + { + yaml::Node fne = g_conf["fne"]; + + std::string password = fne["password"].as(); + + std::string address = fne["masterAddress"].as(); + uint16_t port = (uint16_t)fne["masterPort"].as(); + uint32_t id = fne["peerId"].as(); + + bool encrypted = fne["encrypted"].as(false); + std::string key = fne["presharedKey"].as(); + uint8_t presharedKey[AES_WRAPPED_PCKT_KEY_LEN]; + if (!key.empty()) { + if (key.size() == 32) { + // bryanb: shhhhhhh....dirty nasty hacks + key = key.append(key); // since the key is 32 characters (16 hex pairs), double it on itself for 64 characters (32 hex pairs) + LogWarning(LOG_HOST, "Half-length network preshared encryption key detected, doubling key on itself."); + } + + if (key.size() == 64) { + if ((key.find_first_not_of("0123456789abcdefABCDEF", 2) == std::string::npos)) { + const char* keyPtr = key.c_str(); + ::memset(presharedKey, 0x00U, AES_WRAPPED_PCKT_KEY_LEN); + + for (uint8_t i = 0; i < AES_WRAPPED_PCKT_KEY_LEN; i++) { + char t[4] = {keyPtr[0], keyPtr[1], 0}; + presharedKey[i] = (uint8_t)::strtoul(t, NULL, 16); + keyPtr += 2 * sizeof(char); + } + } + else { + LogWarning(LOG_HOST, "Invalid characters in the network preshared encryption key. Encryption disabled."); + encrypted = false; + } + } + else { + LogWarning(LOG_HOST, "Invalid network preshared encryption key length, key should be 32 hex pairs, or 64 characters. Encryption disabled."); + encrypted = false; + } + } + + std::string identity = fne["identity"].as(); + + LogInfo("Network Parameters"); + LogInfo(" Peer ID: %u", id); + LogInfo(" Address: %s", address.c_str()); + LogInfo(" Port: %u", port); + + LogInfo(" Encrypted: %s", encrypted ? "yes" : "no"); + + if (id > 999999999U) { + ::LogError(LOG_HOST, "Network Peer ID cannot be greater then 999999999."); + return false; + } + + // initialize networking + network = new PeerNetwork(address, port, 0U, id, password, true, g_debug, true, true, true, true, true, true, true, true, false); + network->setMetadata(identity, 0U, 0U, 0.0F, 0.0F, 0, 0, 0, 0.0F, 0.0F, 0, ""); + network->setLookups(g_ridLookup, g_tidLookup); + + ::LogSetNetwork(network); + + if (encrypted) { + network->setPresharedKey(presharedKey); + } + + network->enable(true); + bool ret = network->open(); + if (!ret) { + delete network; + network = nullptr; + LogError(LOG_HOST, "failed to initialize traffic networking for PEER %u", id); + return false; + } + + ::LogSetNetwork(network); + + return true; + } + + /** + * @brief Shuts down peer networking. + */ + void closePeerNetwork() + { + if (network != nullptr) { + network->close(); + delete network; + } + } + + /** + * @brief Instance of the peer network. + */ + network::PeerNetwork* network; + +protected: + /** + * @brief Process external user events. + */ + void processExternalUserEvent() override + { + uint32_t ms = m_stopWatch.elapsed(); + + ms = m_stopWatch.elapsed(); + m_stopWatch.start(); + + // ------------------------------------------------------ + // -- Network Clocking -- + // ------------------------------------------------------ + + if (network != nullptr) { + network->clock(ms); + + hrc::hrc_t pktTime = hrc::now(); + + uint32_t length = 0U; + bool netReadRet = false; + UInt8Array dmrBuffer = network->readDMR(netReadRet, length); + if (netReadRet) { + using namespace dmr; + + uint8_t seqNo = dmrBuffer[4U]; + + uint32_t srcId = __GET_UINT16(dmrBuffer, 5U); + uint32_t dstId = __GET_UINT16(dmrBuffer, 8U); + + DMRDEF::FLCO::E flco = (dmrBuffer[15U] & 0x40U) == 0x40U ? DMRDEF::FLCO::PRIVATE : DMRDEF::FLCO::GROUP; + + uint32_t slotNo = (dmrBuffer[15U] & 0x80U) == 0x80U ? 2U : 1U; + + DMRDEF::DataType::E dataType = (DMRDEF::DataType::E)(dmrBuffer[15U] & 0x0FU); + + data::NetData dmrData; + dmrData.setSeqNo(seqNo); + dmrData.setSlotNo(slotNo); + dmrData.setSrcId(srcId); + dmrData.setDstId(dstId); + dmrData.setFLCO(flco); + + bool dataSync = (dmrBuffer[15U] & 0x20U) == 0x20U; + bool voiceSync = (dmrBuffer[15U] & 0x10U) == 0x10U; + + if (dataSync) { + dmrData.setData(dmrBuffer.get() + 20U); + dmrData.setDataType(dataType); + dmrData.setN(0U); + } + else if (voiceSync) { + dmrData.setData(dmrBuffer.get() + 20U); + dmrData.setDataType(DMRDEF::DataType::VOICE_SYNC); + dmrData.setN(0U); + } + else { + uint8_t n = dmrBuffer[15U] & 0x0FU; + dmrData.setData(dmrBuffer.get() + 20U); + dmrData.setDataType(DMRDEF::DataType::VOICE); + dmrData.setN(n); + } + + // is this the end of the call stream? + if (dataSync && (dataType == DMRDEF::DataType::TERMINATOR_WITH_LC)) { + if (srcId == 0U && dstId == 0U) { + LogWarning(LOG_NET, "DMR, invalid TERMINATOR, srcId = %u, dstId = %u", srcId, dstId); + } + + RxStatus status; + auto it = std::find_if(m_dmrStatus.begin(), m_dmrStatus.end(), [&](StatusMapPair x) { return (x.second.dstId == dstId && x.second.slotNo == slotNo); }); + if (it == m_dmrStatus.end()) { + LogError(LOG_NET, "DMR, tried to end call for non-existent call in progress?, srcId = %u, dstId = %u", + srcId, dstId); + } + else { + status = it->second; + } + + uint64_t duration = hrc::diff(pktTime, status.callStartTime); + + if (std::find_if(m_dmrStatus.begin(), m_dmrStatus.end(), [&](StatusMapPair x) { return (x.second.dstId == dstId && x.second.slotNo == slotNo); }) != m_dmrStatus.end()) { + m_dmrStatus.erase(dstId); + + LogMessage(LOG_NET, "DMR, Call End, srcId = %u, dstId = %u, duration = %u", + srcId, dstId, duration / 1000); + } + } + + // is this a new call stream? + if (dataSync && (dataType == DMRDEF::DataType::VOICE_LC_HEADER)) { + if (srcId == 0U && dstId == 0U) { + LogWarning(LOG_NET, "DMR, invalid call, srcId = %u, dstId = %u", srcId, dstId); + } + + auto it = std::find_if(m_dmrStatus.begin(), m_dmrStatus.end(), [&](StatusMapPair x) { return (x.second.dstId == dstId && x.second.slotNo == slotNo); }); + if (it == m_dmrStatus.end()) { + // this is a new call stream + RxStatus status = RxStatus(); + status.callStartTime = pktTime; + status.srcId = srcId; + status.dstId = dstId; + status.slotNo = slotNo; + m_dmrStatus[dstId] = status; // this *could* be an issue if a dstId appears on both slots somehow... + + LogMessage(LOG_NET, "DMR, Call Start, srcId = %u, dstId = %u", srcId, dstId); + } + } + + // are we receiving a CSBK? + if (dmrData.getDataType() == DMRDEF::DataType::CSBK) { + uint8_t data[DMRDEF::DMR_FRAME_LENGTH_BYTES + 2U]; + dmrData.getData(data + 2U); + + std::unique_ptr csbk = lc::csbk::CSBKFactory::createCSBK(data + 2U, DMRDEF::DataType::CSBK); + if (csbk != nullptr) { + switch (csbk->getCSBKO()) { + case DMRDEF::CSBKO::BROADCAST: + { + lc::csbk::CSBK_BROADCAST* osp = static_cast(csbk.get()); + if (osp->getAnncType() == DMRDEF::BroadcastAnncType::ANN_WD_TSCC) { + LogMessage(LOG_NET, "DMR Slot %u, DT_CSBK, %s, sysId = $%03X, chNo = %u", dmrData.getSlotNo(), csbk->toString().c_str(), + osp->getSystemId(), osp->getLogicalCh1()); + } + } + break; + default: + LogMessage(LOG_NET, "DMR Slot %u, DT_CSBK, %s, srcId = %u, dstId = %u", dmrData.getSlotNo(), csbk->toString().c_str(), srcId, dstId); + break; + } + } + } + + if (g_debug) + LogMessage(LOG_NET, "DMR, slotNo = %u, seqNo = %u, flco = $%02X, srcId = %u, dstId = %u, len = %u", slotNo, seqNo, flco, srcId, dstId, length); + } + + UInt8Array p25Buffer = network->readP25(netReadRet, length); + if (netReadRet) { + using namespace p25; + + uint8_t duid = p25Buffer[22U]; + uint8_t MFId = p25Buffer[15U]; + + // process raw P25 data bytes + UInt8Array data; + uint8_t frameLength = p25Buffer[23U]; + if (duid == P25DEF::DUID::PDU) { + frameLength = length; + data = std::unique_ptr(new uint8_t[length]); + ::memset(data.get(), 0x00U, length); + ::memcpy(data.get(), p25Buffer.get(), length); + } + else { + if (frameLength <= 24) { + data = std::unique_ptr(new uint8_t[frameLength]); + ::memset(data.get(), 0x00U, frameLength); + } + else { + data = std::unique_ptr(new uint8_t[frameLength]); + ::memset(data.get(), 0x00U, frameLength); + ::memcpy(data.get(), p25Buffer.get() + 24U, frameLength); + } + } + + uint8_t lco = p25Buffer[4U]; + + uint32_t srcId = __GET_UINT16(p25Buffer, 5U); + uint32_t dstId = __GET_UINT16(p25Buffer, 8U); + + uint32_t sysId = (p25Buffer[11U] << 8) | (p25Buffer[12U] << 0); + uint32_t netId = __GET_UINT16(p25Buffer, 16U); + + // log call status + if (duid != P25DEF::DUID::TSDU && duid != P25DEF::DUID::PDU) { + // is this the end of the call stream? + if ((duid == P25DEF::DUID::TDU) || (duid == P25DEF::DUID::TDULC)) { + if (srcId == 0U && dstId == 0U) { + LogWarning(LOG_NET, "P25, invalid TDU, srcId = %u, dstId = %u", srcId, dstId); + } + + RxStatus status = m_p25Status[dstId]; + uint64_t duration = hrc::diff(pktTime, status.callStartTime); + + if (std::find_if(m_p25Status.begin(), m_p25Status.end(), [&](StatusMapPair x) { return x.second.dstId == dstId; }) != m_p25Status.end()) { + m_p25Status.erase(dstId); + + LogMessage(LOG_NET, "P25, Call End, srcId = %u, dstId = %u, duration = %u", + srcId, dstId, duration / 1000); + } + } + + // is this a new call stream? + if ((duid != P25DEF::DUID::TDU) && (duid != P25DEF::DUID::TDULC)) { + if (srcId == 0U && dstId == 0U) { + LogWarning(LOG_NET, "P25, invalid call, srcId = %u, dstId = %u", srcId, dstId); + } + + auto it = std::find_if(m_p25Status.begin(), m_p25Status.end(), [&](StatusMapPair x) { return x.second.dstId == dstId; }); + if (it == m_p25Status.end()) { + // this is a new call stream + RxStatus status = RxStatus(); + status.callStartTime = pktTime; + status.srcId = srcId; + status.dstId = dstId; + m_p25Status[dstId] = status; + + LogMessage(LOG_NET, "P25, Call Start, srcId = %u, dstId = %u", srcId, dstId); + } + } + } + + switch (duid) { + case P25DEF::DUID::TDU: + case P25DEF::DUID::TDULC: + if (duid == P25DEF::DUID::TDU) { + LogMessage(LOG_NET, P25_TDU_STR ", srcId = %u, dstId = %u", srcId, dstId); + } + else { + std::unique_ptr tdulc = lc::tdulc::TDULCFactory::createTDULC(data.get()); + if (tdulc == nullptr) { + LogWarning(LOG_NET, P25_TDULC_STR ", undecodable TDULC"); + } + else { + LogMessage(LOG_NET, P25_TDULC_STR ", srcId = %u, dstId = %u", srcId, dstId); + } + } + break; + + case P25DEF::DUID::TSDU: + std::unique_ptr tsbk = lc::tsbk::TSBKFactory::createTSBK(data.get()); + if (tsbk == nullptr) { + LogWarning(LOG_NET, P25_TSDU_STR ", undecodable TSBK"); + } + else { + switch (tsbk->getLCO()) { + case P25DEF::TSBKO::IOSP_GRP_VCH: + case P25DEF::TSBKO::IOSP_UU_VCH: + { + LogMessage(LOG_NET, P25_TSDU_STR ", %s, emerg = %u, encrypt = %u, prio = %u, chNo = %u, srcId = %u, dstId = %u", + tsbk->toString(true).c_str(), tsbk->getEmergency(), tsbk->getEncrypted(), tsbk->getPriority(), tsbk->getGrpVchNo(), srcId, dstId); + } + break; + case P25DEF::TSBKO::IOSP_UU_ANS: + { + lc::tsbk::IOSP_UU_ANS* iosp = static_cast(tsbk.get()); + if (iosp->getResponse() > 0U) { + LogMessage(LOG_NET, P25_TSDU_STR ", %s, response = $%02X, srcId = %u, dstId = %u", + tsbk->toString(true).c_str(), iosp->getResponse(), srcId, dstId); + } + } + break; + case P25DEF::TSBKO::IOSP_STS_UPDT: + { + lc::tsbk::IOSP_STS_UPDT* iosp = static_cast(tsbk.get()); + LogMessage(LOG_NET, P25_TSDU_STR ", %s, status = $%02X, srcId = %u", + tsbk->toString(true).c_str(), iosp->getStatus(), srcId); + } + break; + case P25DEF::TSBKO::IOSP_MSG_UPDT: + { + lc::tsbk::IOSP_MSG_UPDT* iosp = static_cast(tsbk.get()); + LogMessage(LOG_NET, P25_TSDU_STR ", %s, message = $%02X, srcId = %u, dstId = %u", + tsbk->toString(true).c_str(), iosp->getMessage(), srcId, dstId); + } + break; + case P25DEF::TSBKO::IOSP_RAD_MON: + { + lc::tsbk::IOSP_RAD_MON* iosp = static_cast(tsbk.get()); + LogMessage(LOG_NET, P25_TSDU_STR ", %s, srcId = %u, dstId = %u", tsbk->toString(true).c_str(), srcId, dstId); + } + break; + case P25DEF::TSBKO::IOSP_CALL_ALRT: + { + LogMessage(LOG_NET, P25_TSDU_STR ", %s, srcId = %u, dstId = %u", tsbk->toString(true).c_str(), srcId, dstId); + } + break; + case P25DEF::TSBKO::IOSP_ACK_RSP: + { + lc::tsbk::IOSP_ACK_RSP* iosp = static_cast(tsbk.get()); + LogMessage(LOG_NET, P25_TSDU_STR ", %s, AIV = %u, serviceType = $%02X, srcId = %u, dstId = %u", + tsbk->toString(true).c_str(), iosp->getAIV(), iosp->getService(), dstId, srcId); + } + break; + case P25DEF::TSBKO::IOSP_EXT_FNCT: + { + lc::tsbk::IOSP_EXT_FNCT* iosp = static_cast(tsbk.get()); + LogMessage(LOG_NET, P25_TSDU_STR ", %s, serviceType = $%02X, arg = %u, tgt = %u", + tsbk->toString(true).c_str(), iosp->getService(), srcId, dstId); + } + break; + case P25DEF::TSBKO::ISP_EMERG_ALRM_REQ: + { + // non-emergency mode is a TSBKO::OSP_DENY_RSP + if (!tsbk->getEmergency()) { + lc::tsbk::OSP_DENY_RSP* osp = static_cast(tsbk.get()); + LogMessage(LOG_NET, P25_TSDU_STR ", %s, AIV = %u, reason = $%02X, srcId = %u, dstId = %u", + osp->toString().c_str(), osp->getAIV(), osp->getResponse(), osp->getSrcId(), osp->getDstId()); + } else { + LogMessage(LOG_NET, P25_TSDU_STR ", %s, srcId = %u, dstId = %u", tsbk->toString().c_str(), srcId, dstId); + } + } + break; + case P25DEF::TSBKO::IOSP_GRP_AFF: + { + lc::tsbk::IOSP_GRP_AFF* iosp = static_cast(tsbk.get()); + LogMessage(LOG_NET, P25_TSDU_STR ", %s, sysId = $%03X, srcId = %u, dstId = %u", tsbk->toString().c_str(), + iosp->getSysId(), srcId, dstId); + } + break; + case P25DEF::TSBKO::OSP_U_DEREG_ACK: + { + lc::tsbk::OSP_U_DEREG_ACK* iosp = static_cast(tsbk.get()); + LogMessage(LOG_NET, P25_TSDU_STR ", %s, srcId = %u", + tsbk->toString(true).c_str(), srcId); + } + break; + case P25DEF::TSBKO::OSP_LOC_REG_RSP: + { + lc::tsbk::OSP_LOC_REG_RSP* osp = static_cast(tsbk.get()); + LogMessage(LOG_NET, P25_TSDU_STR ", %s, srcId = %u, dstId = %u", osp->toString().c_str(), srcId, dstId); + } + break; + case P25DEF::TSBKO::OSP_ADJ_STS_BCAST: + { + lc::tsbk::OSP_ADJ_STS_BCAST* osp = static_cast(tsbk.get()); + LogMessage(LOG_NET, P25_TSDU_STR ", %s, sysId = $%03X, rfss = $%02X, site = $%02X, chId = %u, chNo = %u, svcClass = $%02X", tsbk->toString().c_str(), + osp->getAdjSiteSysId(), osp->getAdjSiteRFSSId(), osp->getAdjSiteId(), osp->getAdjSiteChnId(), osp->getAdjSiteChnNo(), osp->getAdjSiteSvcClass()); + } + break; + default: + LogMessage(LOG_NET, P25_TSDU_STR ", %s, srcId = %u, dstId = %u", tsbk->toString().c_str(), srcId, dstId); + break; + } + } + break; + } + + if (g_debug) + LogMessage(LOG_NET, "P25, duid = $%02X, lco = $%02X, MFId = $%02X, srcId = %u, dstId = %u, len = %u", duid, lco, MFId, srcId, dstId, length); + } + + UInt8Array nxdnBuffer = network->readNXDN(netReadRet, length); + if (netReadRet) { + using namespace nxdn; + + uint8_t messageType = nxdnBuffer[4U]; + + uint32_t srcId = __GET_UINT16(nxdnBuffer, 5U); + uint32_t dstId = __GET_UINT16(nxdnBuffer, 8U); + + lc::RTCH lc; + + lc.setMessageType(messageType); + lc.setSrcId((uint16_t)srcId & 0xFFFFU); + lc.setDstId((uint16_t)dstId & 0xFFFFU); + + bool group = (nxdnBuffer[15U] & 0x40U) == 0x40U ? false : true; + lc.setGroup(group); + + // specifically only check the following logic for end of call, voice or data frames + if ((messageType == NXDDEF::MessageType::RTCH_TX_REL || messageType == NXDDEF::MessageType::RTCH_TX_REL_EX) || + (messageType == NXDDEF::MessageType::RTCH_VCALL || messageType == NXDDEF::MessageType::RTCH_DCALL_HDR || + messageType == NXDDEF::MessageType::RTCH_DCALL_DATA)) { + // is this the end of the call stream? + if (messageType == NXDDEF::MessageType::RTCH_TX_REL || messageType == NXDDEF::MessageType::RTCH_TX_REL_EX) { + if (srcId == 0U && dstId == 0U) { + LogWarning(LOG_NET, "NXDN, invalid TX_REL, srcId = %u, dstId = %u", srcId, dstId); + } + + RxStatus status = m_nxdnStatus[dstId]; + uint64_t duration = hrc::diff(pktTime, status.callStartTime); + + if (std::find_if(m_nxdnStatus.begin(), m_nxdnStatus.end(), [&](StatusMapPair x) { return x.second.dstId == dstId; }) != m_nxdnStatus.end()) { + m_nxdnStatus.erase(dstId); + + LogMessage(LOG_NET, "NXDN, Call End, srcId = %u, dstId = %u, duration = %u", + srcId, dstId, duration / 1000); + } + } + + // is this a new call stream? + if ((messageType != NXDDEF::MessageType::RTCH_TX_REL && messageType != NXDDEF::MessageType::RTCH_TX_REL_EX)) { + if (srcId == 0U && dstId == 0U) { + LogWarning(LOG_NET, "NXDN, invalid call, srcId = %u, dstId = %u", srcId, dstId); + } + + auto it = std::find_if(m_nxdnStatus.begin(), m_nxdnStatus.end(), [&](StatusMapPair x) { return x.second.dstId == dstId; }); + if (it == m_nxdnStatus.end()) { + // this is a new call stream + RxStatus status = RxStatus(); + status.callStartTime = pktTime; + status.srcId = srcId; + status.dstId = dstId; + m_nxdnStatus[dstId] = status; + + LogMessage(LOG_NET, "NXDN, Call Start, srcId = %u, dstId = %u", srcId, dstId); + } + } + } + + if (g_debug) + LogMessage(LOG_NET, "NXDN, messageType = $%02X, srcId = %u, dstId = %u, len = %u", messageType, srcId, dstId, length); + } + } + + if (ms < 2U) + Thread::sleep(1U); + } + +private: + /** + * @brief Represents the receive status of a call. + */ + class RxStatus { + public: + system_clock::hrc::hrc_t callStartTime; + system_clock::hrc::hrc_t lastPacket; + uint32_t srcId; + uint32_t dstId; + uint8_t slotNo; + uint32_t streamId; + }; + typedef std::pair StatusMapPair; + std::unordered_map m_dmrStatus; + std::unordered_map m_p25Status; + std::unordered_map m_nxdnStatus; + + StopWatch m_stopWatch; +}; + +#endif // __SYS_VIEW_APPLICATION_H__ \ No newline at end of file diff --git a/src/sysview/SysViewMain.cpp b/src/sysview/SysViewMain.cpp new file mode 100644 index 00000000..c6f4c533 --- /dev/null +++ b/src/sysview/SysViewMain.cpp @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - FNE System View + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * + */ +#include "Defines.h" +#include "common/yaml/Yaml.h" +#include "common/Log.h" +#include "SysViewMain.h" +#include "SysViewApplication.h" +#include "SysViewMainWnd.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; + +bool g_hideLoggingWnd = false; + +lookups::RadioIdLookup* g_ridLookup = nullptr; +lookups::TalkgroupRulesLookup* g_tidLookup = nullptr; +lookups::IdenTableLookup* g_idenTable = nullptr; + +SysViewApplication* g_app = nullptr; + +// --------------------------------------------------------------------------- +// Global Functions +// --------------------------------------------------------------------------- + +/* Helper to print a fatal error message and exit. */ + +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: FATAL PANIC; %s\n", g_progExe.c_str(), buffer); + exit(EXIT_FAILURE); +} + +/** + * @brief Initializes peer network connectivity. + * @returns bool + */ +bool createPeerNetwork() +{ + if (g_app != nullptr) + return g_app->createPeerNetwork(); + return false; +} + +/** + * @brief Shuts down peer networking. + */ +void closePeerNetwork() +{ + if (g_app != nullptr) + g_app->closePeerNetwork(); +} + +/* Helper to pring usage the command line arguments. (And optionally an error.) */ + +void usage(const char* message, const char* arg) +{ + ::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__); + ::fprintf(stdout, "Copyright (c) 2017-2024 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]" + "[--hide-log]" + "[-c ]" + "\n\n" + " -d enable debug\n" + " -v show version information\n" + " -h show this screen\n" + "\n" + " --hide-log hide interactive logging window on startup\n" + "\n" + " -c specifies the system view configuration file to use\n" + "\n" + " -- stop handling options\n", + g_progExe.c_str()); + + exit(EXIT_FAILURE); +} + +/* Helper to validate the command line 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 monitor configuration file to use"); + g_iniFile = std::string(argv[++i]); + + if (g_iniFile.empty()) + usage("error: %s", "monitor configuration file cannot be blank!"); + + p += 2; + } + else if (IS("--hide-log")) { + ++p; + g_hideLoggingWnd = true; + } + 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-2024 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 (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; + } + + ::LogInfo(__PROG_NAME__ " " __VER__ " (built " __BUILD__ ")\r\n" \ + "Copyright (c) 2017-2024 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\r\n" \ + "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\r\n" \ + ">> FNE System View\r\n"); + + 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 + SysViewApplication app{argc, argv}; + g_app = &app; + + SysViewMainWnd wnd{&app}; + finalcut::FWidget::setMainWidget(&wnd); + + // try to load bandplan identity table + std::string idenLookupFile = g_conf["iden_table"]["file"].as(); + uint32_t idenReloadTime = g_conf["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; + + // try to load radio IDs table + std::string ridLookupFile = g_conf["radio_id"]["file"].as(); + uint32_t ridReloadTime = g_conf["radio_id"]["time"].as(0U); + + LogInfo("Radio Id Lookups"); + LogInfo(" File: %s", ridLookupFile.length() > 0U ? ridLookupFile.c_str() : "None"); + if (ridReloadTime > 0U) + LogInfo(" Reload: %u mins", ridReloadTime); + + g_ridLookup = new RadioIdLookup(ridLookupFile, ridReloadTime, false); + g_ridLookup->read(); + + // try to load talkgroup IDs table + std::string tidLookupFile = g_conf["talkgroup_id"]["file"].as(); + uint32_t tidReloadTime = g_conf["talkgroup_id"]["time"].as(0U); + + LogInfo("Talkgroup Rule Lookups"); + LogInfo(" File: %s", tidLookupFile.length() > 0U ? tidLookupFile.c_str() : "None"); + if (tidReloadTime > 0U) + LogInfo(" Reload: %u mins", tidReloadTime); + + g_tidLookup = new TalkgroupRulesLookup(tidLookupFile, tidReloadTime, false); + g_tidLookup->read(); + + 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(); + + // show and start the application + wnd.show(); + + finalcut::FApplication::setColorTheme(); + app.resetColors(); + app.redraw(); + + int _errno = app.exec(); + ::LogFinalise(); + return _errno; +} diff --git a/src/sysview/SysViewMain.h b/src/sysview/SysViewMain.h new file mode 100644 index 00000000..888f4add --- /dev/null +++ b/src/sysview/SysViewMain.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - FNE System View + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file SysViewMain.h + * @ingroup fneSysView + * @file SysViewMain.cpp + * @ingroup fneSysView + */ +#if !defined(__SYS_VIEW_MAIN_H__) +#define __SYS_VIEW_MAIN_H__ + +#include "Defines.h" +#include "common/lookups/RadioIdLookup.h" +#include "common/lookups/TalkgroupRulesLookup.h" +#include "common/lookups/IdenTableLookup.h" +#include "common/yaml/Yaml.h" + +#include + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#undef __PROG_NAME__ +#define __PROG_NAME__ "Digital Voice Modem (DVM) FNE System View" +#undef __EXE_NAME__ +#define __EXE_NAME__ "sysview" + +// --------------------------------------------------------------------------- +// Externs +// --------------------------------------------------------------------------- + +/** @brief */ +extern std::string g_progExe; +/** @brief */ +extern std::string g_iniFile; +/** @brief */ +extern yaml::Node g_conf; +/** @brief */ +extern bool g_debug; + +/** @brief */ +extern bool g_hideLoggingWnd; + +/** @brief */ +extern lookups::RadioIdLookup* g_ridLookup; +/** @brief */ +extern lookups::TalkgroupRulesLookup* g_tidLookup; +/** @brief */ +extern lookups::IdenTableLookup* g_idenTable; + +/** + * @brief Helper to trigger a fatal error message. This will cause the program to terminate + * immediately with an error message. + * + * @param msg String format. + * + * This is a variable argument function. + */ +extern HOST_SW_API void fatal(const char* msg, ...); + +/** + * @brief Initializes peer network connectivity. + * @returns bool + */ +extern HOST_SW_API bool createPeerNetwork(); + +/** + * @brief Shuts down peer networking. + */ +extern HOST_SW_API void closePeerNetwork(); + +#endif // __SYS_VIEW_MAIN_H__ diff --git a/src/sysview/SysViewMainWnd.h b/src/sysview/SysViewMainWnd.h new file mode 100644 index 00000000..529fccbb --- /dev/null +++ b/src/sysview/SysViewMainWnd.h @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - FNE System View + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file SysViewMainWnd.h + * @ingroup fneSysView + */ +#if !defined(__AFF_VIEW_WND_H__) +#define __AFF_VIEW_WND_H__ + +#include "common/Log.h" +#include "common/Thread.h" + +using namespace lookups; + +#include +using namespace finalcut; +#undef null + +#include "SysViewMain.h" +#include "AffListWnd.h" +#include "PeerListWnd.h" + +#include "LogDisplayWnd.h" + +#include + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#define MINIMUM_SUPPORTED_SIZE_WIDTH 83 +#define MINIMUM_SUPPORTED_SIZE_HEIGHT 30 + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the root window control. + * @ingroup fneSysView + */ +class HOST_SW_API SysViewMainWnd final : public finalcut::FWidget { +public: + /** + * @brief Initializes a new instance of the SysViewMainWnd class. + * @param widget + */ + explicit SysViewMainWnd(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); + + // help menu + m_aboutItem.addCallback("clicked", this, [&]() { + const FString line(2, UniChar::BoxDrawingsHorizontal); + FMessageBox info("About", line + __PROG_NAME__ + line + L"\n\n" + L"" + __BANNER__ + L"\n" + L"Version " + __VER__ + L"\n\n" + L"Copyright (c) 2017-2024 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 AffViewApplication; + + LogDisplayWnd m_logWnd{this}; + AffListWnd* m_wnd; + PeerListWnd *m_peerWnd; + + FString m_line{13, UniChar::BoxDrawingsHorizontal}; + + FMenuBar m_menuBar{this}; + + FMenu m_fileMenu{"&File", &m_menuBar}; + FMenuItem m_quitItem{"&Quit", &m_fileMenu}; + + FMenu m_helpMenu{"&Help", &m_menuBar}; + FMenuItem m_aboutItem{"&About", &m_helpMenu}; + + FStatusBar m_statusBar{this}; + FStatusKey m_keyF3{FKey::F3, "Quit", &m_statusBar}; + + /* + ** Event Handlers + */ + + /** + * @brief Event that occurs when the window is shown. + * @param e Show Event + */ + void onShow(FShowEvent* e) override + { + const auto& rootWidget = getRootWidget(); + + createPeerNetwork(); + + int fullWidth = rootWidget->getWidth(); + if (fullWidth < MINIMUM_SUPPORTED_SIZE_WIDTH) { + clearArea(); + ::fatal("screen resolution too small must be wider then %u characters, console width = %u", MINIMUM_SUPPORTED_SIZE_WIDTH, fullWidth); + } + + int fullHeight = rootWidget->getHeight(); + if (fullHeight < MINIMUM_SUPPORTED_SIZE_HEIGHT) { + clearArea(); + ::fatal("screen resolution too small must be taller then %u characters, console height = %u", MINIMUM_SUPPORTED_SIZE_HEIGHT, fullHeight); + } + + int maxWidth = 77; + if (rootWidget) { + maxWidth = rootWidget->getClientWidth() - 3; + } + + int maxHeight = AFF_LIST_HEIGHT; + if (rootWidget) { + maxHeight = rootWidget->getClientHeight() - 3; + } + + m_wnd = new AffListWnd(this); + if (maxHeight - 41 < AFF_LIST_HEIGHT) + maxHeight = AFF_LIST_HEIGHT + 41; + + m_wnd->setGeometry(FPoint{2, 2}, FSize{AFF_LIST_WIDTH, (size_t)(maxHeight - 41)}); + + m_wnd->setModal(false); + m_wnd->show(); + + m_peerWnd = new PeerListWnd(this); + if (maxHeight - 41 < PEER_LIST_HEIGHT) + maxHeight = PEER_LIST_HEIGHT + 41; + + m_peerWnd->setGeometry(FPoint{AFF_LIST_WIDTH + 6, 2}, FSize{(size_t)(maxWidth - AFF_LIST_WIDTH - 6), (size_t)(maxHeight - 41)}); + + m_peerWnd->setModal(false); + m_peerWnd->show(); + + m_wnd->raiseWindow(); + m_wnd->activateWindow(); + + redraw(); + + if (g_hideLoggingWnd) { + const auto& rootWidget = getRootWidget(); + m_logWnd.setGeometry(FPoint{(int)(rootWidget->getClientWidth() - 81), (int)(rootWidget->getClientHeight() - 1)}, FSize{80, 20}); + + m_logWnd.minimizeWindow(); + } + } + + /** + * @brief Event that occurs when the window is closed. + * @param e Close Event + */ + void onClose(FCloseEvent* e) override + { + FApplication::closeConfirmationDialog(this, e); + } +}; + +#endif // __AFF_VIEW_WND_H__ \ No newline at end of file diff --git a/src/sysview/network/PeerNetwork.cpp b/src/sysview/network/PeerNetwork.cpp new file mode 100644 index 00000000..609da34f --- /dev/null +++ b/src/sysview/network/PeerNetwork.cpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: PROPRIETARY +/* + * Digital Voice Modem - FNE Affiliations View + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * + */ +#include "Defines.h" +#include "common/network/json/json.h" +#include "common/p25/dfsi/DFSIDefines.h" +#include "common/p25/dfsi/LC.h" +#include "common/Utils.h" +#include "network/PeerNetwork.h" + +using namespace network; + +#include + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a new instance of the PeerNetwork class. */ + +PeerNetwork::PeerNetwork(const std::string& address, uint16_t port, uint16_t localPort, uint32_t peerId, const std::string& password, + bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup) : + Network(address, port, localPort, peerId, password, duplex, debug, dmr, p25, nxdn, slot1, slot2, allowActivityTransfer, allowDiagnosticTransfer, updateLookup, saveLookup) +{ + assert(!address.empty()); + assert(port > 0U); + assert(!password.empty()); +} + +// --------------------------------------------------------------------------- +// Protected Class Members +// --------------------------------------------------------------------------- + +/* Writes configuration to the network. */ + +bool PeerNetwork::writeConfig() +{ + if (m_loginStreamId == 0U) { + LogWarning(LOG_NET, "BUGBUG: tried to write network authorisation with no stream ID?"); + return false; + } + + const char* software = __NETVER__; + + json::object config = json::object(); + + // identity and frequency + config["identity"].set(m_identity); // Identity + config["rxFrequency"].set(m_rxFrequency); // Rx Frequency + config["txFrequency"].set(m_txFrequency); // Tx Frequency + + // system info + json::object sysInfo = json::object(); + sysInfo["latitude"].set(m_latitude); // Latitude + sysInfo["longitude"].set(m_longitude); // Longitude + + sysInfo["height"].set(m_height); // Height + sysInfo["location"].set(m_location); // Location + config["info"].set(sysInfo); + + // channel data + json::object channel = json::object(); + channel["txPower"].set(m_power); // Tx Power + channel["txOffsetMhz"].set(m_txOffsetMhz); // Tx Offset (Mhz) + channel["chBandwidthKhz"].set(m_chBandwidthKhz); // Ch. Bandwidth (khz) + channel["channelId"].set(m_channelId); // Channel ID + channel["channelNo"].set(m_channelNo); // Channel No + config["channel"].set(channel); + + // RCON + json::object rcon = json::object(); + rcon["password"].set(m_restApiPassword); // REST API Password + rcon["port"].set(m_restApiPort); // REST API Port + config["rcon"].set(rcon); + + bool convPeer = true; + config["conventionalPeer"].set(convPeer); // Conventional Peer Marker + config["software"].set(std::string(software)); // Software ID + + json::value v = json::value(config); + std::string json = v.serialize(); + + char buffer[json.length() + 8U]; + + ::memcpy(buffer + 0U, TAG_REPEATER_CONFIG, 4U); + ::sprintf(buffer + 8U, "%s", json.c_str()); + + if (m_debug) { + Utils::dump(1U, "Network Message, Configuration", (uint8_t*)buffer, json.length() + 8U); + } + + return writeMaster({ NET_FUNC::RPTC, NET_SUBFUNC::NOP }, (uint8_t*)buffer, json.length() + 8U, pktSeq(), m_loginStreamId); +} diff --git a/src/sysview/network/PeerNetwork.h b/src/sysview/network/PeerNetwork.h new file mode 100644 index 00000000..68490da0 --- /dev/null +++ b/src/sysview/network/PeerNetwork.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: PROPRIETARY +/* + * Digital Voice Modem - FNE Affiliations View + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * + */ +/** + * @defgroup fneAffView_network Networking + * @brief Implementation for the FNE networking. + * @ingroup fneAffView + * + * @file PeerNetwork.h + * @ingroup fneAffView_network + * @file PeerNetwork.cpp + * @ingroup fneAffView_network + */ +#if !defined(__PEER_NETWORK_H__) +#define __PEER_NETWORK_H__ + +#include "Defines.h" +#include "host/network/Network.h" + +#include +#include + +namespace network +{ + // --------------------------------------------------------------------------- + // Class Declaration + // Implements the core peer networking logic. + // --------------------------------------------------------------------------- + + class HOST_SW_API PeerNetwork : public Network { + public: + /** + * @brief Initializes a new instance of the PeerNetwork class. + * @param address Network Hostname/IP address to connect to. + * @param port Network port number. + * @param local + * @param peerId Unique ID on the network. + * @param password Network authentication password. + * @param duplex Flag indicating full-duplex operation. + * @param debug Flag indicating whether network debug is enabled. + * @param dmr Flag indicating whether DMR is enabled. + * @param p25 Flag indicating whether P25 is enabled. + * @param nxdn Flag indicating whether NXDN is enabled. + * @param slot1 Flag indicating whether DMR slot 1 is enabled for network traffic. + * @param slot2 Flag indicating whether DMR slot 2 is enabled for network traffic. + * @param allowActivityTransfer Flag indicating that the system activity logs will be sent to the network. + * @param allowDiagnosticTransfer Flag indicating that the system diagnostic logs will be sent to the network. + * @param updateLookup Flag indicating that the system will accept radio ID and talkgroup ID lookups from the network. + */ + PeerNetwork(const std::string& address, uint16_t port, uint16_t localPort, uint32_t peerId, const std::string& password, + bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup); + + protected: + /** + * @brief Writes configuration to the network. + * @returns bool True, if configuration was sent, otherwise false. + */ + bool writeConfig() override; + }; +} // namespace network + +#endif // __PEER_NETWORK_H__