add code dump of private sysview FNE monitoring utility to public GPL; correct issue with large REST responses getting truncated;

pull/72/head
Bryan Biedenkapp 1 year ago
parent 555ec97c90
commit fe6f7508c0

@ -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

@ -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"

@ -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
#

@ -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<char, 8192> m_buffer;
std::size_t m_sizeToTransfer;
std::size_t m_bytesTransferred;
std::array<char, 65535> m_fullBuffer;
std::array<char, 1024> m_buffer;
HTTPPayload m_request;
HTTPLexer m_lexer;

@ -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<char, 8192> m_buffer;
std::size_t m_sizeToTransfer;
std::size_t m_bytesTransferred;
std::array<char, 65535> m_fullBuffer;
std::array<char, 1024> m_buffer;
HTTPPayload m_request;
HTTPLexer m_lexer;

@ -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...
}
}

@ -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"

@ -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 <final/final.h>
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<std::string>("127.0.0.1");
uint16_t fneRESTPort = (uint16_t)fne["restPort"].as<uint32_t>(9990U);
std::string fnePassword = fne["restPassword"].as<std::string>("PASSWORD");
bool fneSSL = fne["restSsl"].as<bool>(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<json::array>();
for (auto entry : fneAffils) {
json::object peerAffils = entry.get<json::object>();
uint32_t peerId = peerAffils["peerId"].get<uint32_t>();
json::array affils = peerAffils["affiliations"].get<json::array>();
for (auto pentry : affils) {
json::object peerEntry = pentry.get<json::object>();
uint32_t dstId = peerEntry["dstId"].get<uint32_t>();
uint32_t srcId = peerEntry["srcId"].get<uint32_t>();
// 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<std::string, 3U> 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__

@ -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"
)

@ -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__

@ -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 <final/final.h>
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__

@ -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 <final/final.h>
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<std::string>("127.0.0.1");
uint16_t fneRESTPort = (uint16_t)fne["restPort"].as<uint32_t>(9990U);
std::string fnePassword = fne["restPassword"].as<std::string>("PASSWORD");
bool fneSSL = fne["restSsl"].as<bool>(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<json::array>();
for (auto entry : fnePeers) {
json::object peerObj = entry.get<json::object>();
uint32_t peerId = peerObj["peerId"].get<uint32_t>();
std::string peerAddress = peerObj["address"].get<std::string>();
uint16_t port = (uint16_t)peerObj["port"].get<uint32_t>();
bool connected = peerObj["connected"].get<bool>();
uint32_t connectionState = (uint32_t)peerObj["connectionState"].get<uint32_t>();
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<uint32_t>();
uint64_t lastPing = (uint64_t)peerObj["lastPing"].get<uint32_t>();
uint32_t ccPeerId = (uint32_t)peerObj["controlChannel"].get<uint32_t>();
json::array voiceChannels = peerObj["voiceChannels"].get<json::array>();
std::vector<uint32_t> voiceChannelPeers;
for (auto vcEntry : voiceChannels) {
uint32_t vcPeerId = vcEntry.get<uint32_t>();
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<std::string, 8U> 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__

@ -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 <final/final.h>
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>();
std::string address = fne["masterAddress"].as<std::string>();
uint16_t port = (uint16_t)fne["masterPort"].as<uint32_t>();
uint32_t id = fne["peerId"].as<uint32_t>();
bool encrypted = fne["encrypted"].as<bool>(false);
std::string key = fne["presharedKey"].as<std::string>();
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<std::string>();
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<lc::CSBK> 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<lc::csbk::CSBK_BROADCAST*>(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<uint8_t[]>(new uint8_t[length]);
::memset(data.get(), 0x00U, length);
::memcpy(data.get(), p25Buffer.get(), length);
}
else {
if (frameLength <= 24) {
data = std::unique_ptr<uint8_t[]>(new uint8_t[frameLength]);
::memset(data.get(), 0x00U, frameLength);
}
else {
data = std::unique_ptr<uint8_t[]>(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<lc::TDULC> 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<lc::TSBK> 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<lc::tsbk::IOSP_UU_ANS*>(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<lc::tsbk::IOSP_STS_UPDT*>(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<lc::tsbk::IOSP_MSG_UPDT*>(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<lc::tsbk::IOSP_RAD_MON*>(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<lc::tsbk::IOSP_ACK_RSP*>(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<lc::tsbk::IOSP_EXT_FNCT*>(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<lc::tsbk::OSP_DENY_RSP*>(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<lc::tsbk::IOSP_GRP_AFF*>(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<lc::tsbk::OSP_U_DEREG_ACK*>(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<lc::tsbk::OSP_LOC_REG_RSP*>(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<lc::tsbk::OSP_ADJ_STS_BCAST*>(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<const uint32_t, RxStatus> StatusMapPair;
std::unordered_map<uint32_t, RxStatus> m_dmrStatus;
std::unordered_map<uint32_t, RxStatus> m_p25Status;
std::unordered_map<uint32_t, RxStatus> m_nxdnStatus;
StopWatch m_stopWatch;
};
#endif // __SYS_VIEW_APPLICATION_H__

@ -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 <cstdio>
#include <cstdarg>
#include <cstring>
#include <vector>
// ---------------------------------------------------------------------------
// 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 <configuration file>]"
"\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 <file> 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<std::string>();
uint32_t idenReloadTime = g_conf["iden_table"]["time"].as<uint32_t>(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<std::string>();
uint32_t ridReloadTime = g_conf["radio_id"]["time"].as<uint32_t>(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<std::string>();
uint32_t tidReloadTime = g_conf["talkgroup_id"]["time"].as<uint32_t>(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<dvmColorTheme>();
app.resetColors();
app.redraw();
int _errno = app.exec();
::LogFinalise();
return _errno;
}

@ -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 <string>
// ---------------------------------------------------------------------------
// 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__

@ -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 <final/final.h>
using namespace finalcut;
#undef null
#include "SysViewMain.h"
#include "AffListWnd.h"
#include "PeerListWnd.h"
#include "LogDisplayWnd.h"
#include <vector>
// ---------------------------------------------------------------------------
// 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__

@ -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 <cassert>
// ---------------------------------------------------------------------------
// 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<std::string>(m_identity); // Identity
config["rxFrequency"].set<uint32_t>(m_rxFrequency); // Rx Frequency
config["txFrequency"].set<uint32_t>(m_txFrequency); // Tx Frequency
// system info
json::object sysInfo = json::object();
sysInfo["latitude"].set<float>(m_latitude); // Latitude
sysInfo["longitude"].set<float>(m_longitude); // Longitude
sysInfo["height"].set<int>(m_height); // Height
sysInfo["location"].set<std::string>(m_location); // Location
config["info"].set<json::object>(sysInfo);
// channel data
json::object channel = json::object();
channel["txPower"].set<uint32_t>(m_power); // Tx Power
channel["txOffsetMhz"].set<float>(m_txOffsetMhz); // Tx Offset (Mhz)
channel["chBandwidthKhz"].set<float>(m_chBandwidthKhz); // Ch. Bandwidth (khz)
channel["channelId"].set<uint8_t>(m_channelId); // Channel ID
channel["channelNo"].set<uint32_t>(m_channelNo); // Channel No
config["channel"].set<json::object>(channel);
// RCON
json::object rcon = json::object();
rcon["password"].set<std::string>(m_restApiPassword); // REST API Password
rcon["port"].set<uint16_t>(m_restApiPort); // REST API Port
config["rcon"].set<json::object>(rcon);
bool convPeer = true;
config["conventionalPeer"].set<bool>(convPeer); // Conventional Peer Marker
config["software"].set<std::string>(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);
}

@ -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 <string>
#include <cstdint>
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__
Loading…
Cancel
Save

Powered by TurnKey Linux.