add peer ID editor; implement support in the peer lookup and FNE to validate whether or not a peer can perform a encryption key request;

pull/86/head
Bryan Biedenkapp 11 months ago
parent 6927ea284c
commit 5a96f10906

@ -298,6 +298,7 @@ if (NOT TARGET strip)
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 peered
COMMAND arm-linux-gnueabihf-strip -s dvmbridge)
else()
add_custom_target(strip
@ -315,6 +316,7 @@ if (NOT TARGET strip)
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 peered
COMMAND aarch64-linux-gnu-strip -s dvmbridge)
else()
add_custom_target(strip
@ -346,6 +348,7 @@ if (NOT TARGET strip)
COMMAND strip -s dvmmon
COMMAND strip -s sysview
COMMAND strip -s tged
COMMAND strip -s peered
COMMAND strip -s dvmbridge)
else()
add_custom_target(strip
@ -379,6 +382,7 @@ if (NOT TARGET tarball)
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 peered ${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
COMMAND cp ${CMAKE_SOURCE_DIR}/tools/*.sh ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm
@ -457,6 +461,7 @@ if (NOT TARGET tarball_notools)
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 peered ${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
COMMAND cp -v ${CMAKE_SOURCE_DIR}/configs/*.yml ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm
@ -532,6 +537,7 @@ add_custom_target(old_install
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 peered ${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 ${CMAKE_SOURCE_DIR}/configs/config.example.yml ${CMAKE_LEGACY_INSTALL_PREFIX}/config.example.yml

@ -1,9 +1,9 @@
#
# This file sets the valid peer IDs allowed on a FNE.
#
# Entry Format: "Peer ID,Peer Password,Peer Link (1 = Enabled / 0 = Disabled),Peer Alias (optional),<newline>"
#1234,,0,
#5678,MYSECUREPASSWORD,0,
#9876,MYSECUREPASSWORD,1,
#5432,MYSECUREPASSWORD,,Peer Alias 1,
#1012,MYSECUREPASSWORD,1,Peer Alias 2,
# Entry Format: "Peer ID,Peer Password,Peer Link (1 = Enabled / 0 = Disabled),Peer Alias (optional),Can Request Keys (1 = Enabled / 0 = Disabled),<newline>"
#1234,,0,,1,
#5678,MYSECUREPASSWORD,0,,0,
#9876,MYSECUREPASSWORD,1,,0,
#5432,MYSECUREPASSWORD,,Peer Alias 1,0,
#1012,MYSECUREPASSWORD,1,Peer Alias 2,1,

@ -113,6 +113,16 @@ if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS))
target_include_directories(tged PRIVATE ${OPENSSL_INCLUDE_DIR} websocketpp src src/host src/tged)
endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS))
#
## peered
#
if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS))
include(src/peered/CMakeLists.txt)
add_executable(peered ${common_INCLUDE} ${peered_SRC})
target_link_libraries(peered PRIVATE common ${OPENSSL_LIBRARIES} asio::asio finalcut Threads::Threads)
target_include_directories(peered PRIVATE ${OPENSSL_INCLUDE_DIR} websocketpp src src/host src/peered)
endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS))
#
## dvmcmd
#

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2016 Jonathan Naylor, G4KLX
* Copyright (C) 2017-2022,2024 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017-2022,2024,2025 Bryan Biedenkapp, N2PLL
* Copyright (c) 2024 Patrick McDonnell, W3AXL
* Copyright (c) 2024 Caleb, KO4UYJ
*
@ -46,16 +46,16 @@ void PeerListLookup::clear()
/* Adds a new entry to the list. */
void PeerListLookup::addEntry(uint32_t id, const std::string& alias, const std::string& password, bool peerLink)
void PeerListLookup::addEntry(uint32_t id, const std::string& alias, const std::string& password, bool peerLink, bool canRequestKeys)
{
PeerId entry = PeerId(id, alias, password, peerLink, false);
PeerId entry = PeerId(id, alias, password, peerLink, canRequestKeys, false);
std::lock_guard<std::mutex> lock(m_mutex);
try {
PeerId _entry = m_table.at(id);
// if either the alias or the enabled flag doesn't match, update the entry
if (_entry.peerId() == id) {
_entry = PeerId(id, alias, password, peerLink, false);
_entry = PeerId(id, alias, password, peerLink, canRequestKeys, false);
m_table[id] = _entry;
}
} catch (...) {
@ -87,7 +87,7 @@ PeerId PeerListLookup::find(uint32_t id)
try {
entry = m_table.at(id);
} catch (...) {
entry = PeerId(0U, "", "", false, true);
entry = PeerId(0U, "", "", false, false, true);
}
return entry;
@ -226,19 +226,25 @@ bool PeerListLookup::load()
if (parsed.size() >= 3)
peerLink = ::atoi(parsed[2].c_str()) == 1;
// Parse can request keys flag
bool canRequestKeys = false;
if (parsed.size() >= 5)
canRequestKeys = ::atoi(parsed[4].c_str()) == 1;
// Parse optional password
std::string password = "";
if (parsed.size() >= 2)
password = parsed[1].c_str();
// Load into table
m_table[id] = PeerId(id, alias, password, peerLink, false);
m_table[id] = PeerId(id, alias, password, peerLink, canRequestKeys, false);
// Log depending on what was loaded
LogMessage(LOG_HOST, "Loaded peer ID %u%s into peer ID lookup table, %s%s", id,
(!alias.empty() ? (" (" + alias + ")").c_str() : ""),
(!password.empty() ? "using unique peer password" : "using master password"),
(peerLink) ? ", Peer-Link Enabled" : "");
(peerLink) ? ", Peer-Link Enabled" : "",
(canRequestKeys) ? ", Can Request Keys" : "");
}
}
@ -299,6 +305,15 @@ bool PeerListLookup::save()
if (alias.length() > 0) {
line += alias;
line += ",";
} else {
line += ",";
}
// Add canRequestKeys flag
bool canRequestKeys = entry.second.canRequestKeys();
if (canRequestKeys) {
line += "1,";
} else {
line += "0,";
}
// Add the newline
line += "\n";

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2016 Jonathan Naylor, G4KLX
* Copyright (C) 2017-2022,2024 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017-2022,2024,2025 Bryan Biedenkapp, N2PLL
* Copyright (c) 2024 Patrick McDonnell, W3AXL
* Copyright (c) 2024 Caleb, KO4UYJ
*
@ -50,6 +50,7 @@ namespace lookups
m_peerAlias(),
m_peerPassword(),
m_peerLink(false),
m_canRequestKeys(false),
m_peerDefault(false)
{
/* stub */
@ -60,13 +61,16 @@ namespace lookups
* @param peerAlias Peer alias
* @param peerPassword Per Peer Password.
* @param sendConfiguration Flag indicating this peer participates in peer link and should be sent configuration.
* @param peerLink lag indicating if the peer participates in peer link and should be sent configuration.
* @param canRequestKeys Flag indicating if the peer can request encryption keys.
* @param peerDefault Flag indicating this is a "default" (i.e. undefined) peer.
*/
PeerId(uint32_t peerId, const std::string& peerAlias, const std::string& peerPassword, bool peerLink, bool peerDefault) :
PeerId(uint32_t peerId, const std::string& peerAlias, const std::string& peerPassword, bool peerLink, bool canRequestKeys, bool peerDefault) :
m_peerId(peerId),
m_peerAlias(peerAlias),
m_peerPassword(peerPassword),
m_peerLink(peerLink),
m_canRequestKeys(canRequestKeys),
m_peerDefault(peerDefault)
{
/* stub */
@ -83,6 +87,7 @@ namespace lookups
m_peerAlias = data.m_peerAlias;
m_peerPassword = data.m_peerPassword;
m_peerLink = data.m_peerLink;
m_canRequestKeys = data.m_canRequestKeys;
m_peerDefault = data.m_peerDefault;
}
@ -95,14 +100,17 @@ namespace lookups
* @param peerAlias Peer Alias
* @param peerPassword Per Peer Password.
* @param sendConfiguration Flag indicating this peer participates in peer link and should be sent configuration.
* @param peerLink lag indicating if the peer participates in peer link and should be sent configuration.
* @param canRequestKeys Flag indicating if the peer can request encryption keys.
* @param peerDefault Flag indicating this is a "default" (i.e. undefined) peer.
*/
void set(uint32_t peerId, const std::string& peerAlias, const std::string& peerPassword, bool peerLink, bool peerDefault)
void set(uint32_t peerId, const std::string& peerAlias, const std::string& peerPassword, bool peerLink, bool canRequestKeys, bool peerDefault)
{
m_peerId = peerId;
m_peerAlias = peerAlias;
m_peerPassword = peerPassword;
m_peerLink = peerLink;
m_canRequestKeys = canRequestKeys;
m_peerDefault = peerDefault;
}
@ -123,6 +131,10 @@ namespace lookups
* @brief Flag indicating if the peer participates in peer link and should be sent configuration.
*/
__PROPERTY_PLAIN(bool, peerLink);
/**
* @brief Flag indicating if the peer can request encryption keys.
*/
__PROPERTY_PLAIN(bool, canRequestKeys);
/**
* @brief Flag indicating if the peer is default.
*/
@ -166,8 +178,9 @@ namespace lookups
* @param peerId Unique peer ID to add.
* @param password Per Peer Password.
* @param peerLink Flag indicating this peer will participate in peer link and should be sent configuration.
* @param canRequestKeys Flag indicating if the peer can request encryption keys.
*/
void addEntry(uint32_t id, const std::string& alias = "", const std::string& password = "", bool peerLink = false);
void addEntry(uint32_t id, const std::string& alias = "", const std::string& password = "", bool peerLink = false, bool canRequestKeys = false);
/**
* @brief Removes an existing entry from the list.
* @param peerId Unique peer ID to remove.

@ -1102,6 +1102,19 @@ void* FNENetwork::threadedNetworkRx(void* arg)
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
// is this peer allowed to request keys?
if (network->m_peerListLookup->getACL()) {
if (network->m_peerListLookup->getMode() == lookups::PeerListLookup::WHITELIST) {
lookups::PeerId peerEntry = network->m_peerListLookup->find(peerId);
if (peerEntry.peerDefault()) {
break;
} else {
if (!peerEntry.canRequestKeys())
break;
}
}
}
std::unique_ptr<KMMFrame> frame = KMMFactory::create(req->buffer + 11U);
if (frame == nullptr) {
LogWarning(LOG_NET, "PEER %u (%s), undecodable KMM frame from peer", peerId, connection->identity().c_str());

@ -0,0 +1,13 @@
# SPDX-License-Identifier: GPL-2.0-only
#/*
# * Digital Voice Modem - Peer ID Editor
# * 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 peered_SRC
"src/peered/*.h"
"src/peered/*.cpp"
)

@ -0,0 +1,130 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Peer ID Editor
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023,2024,2025 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file CloseWndBase.h
* @ingroup peered
*/
#if !defined(__CLOSE_WND_BASE_H__)
#define __CLOSE_WND_BASE_H__
#include "common/Thread.h"
#include "FDblDialog.h"
#include <final/final.h>
using namespace finalcut;
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief This class implements the base class for windows with close buttons.
* @ingroup tged
*/
class HOST_SW_API CloseWndBase : public FDblDialog {
public:
/**
* @brief Initializes a new instance of the CloseWndBase class.
* @param widget
*/
explicit CloseWndBase(FWidget* widget = nullptr) : FDblDialog{widget}
{
/* stub */
}
protected:
bool m_enableSetButton = false;
FButton m_setButton{"Set", this};
bool m_enableCloseButton = true;
FButton m_closeButton{"&Close", this};
/**
* @brief Initializes the window layout.
*/
void initLayout() override
{
FDialog::setMinimizable(true);
FDialog::setShadow();
std::size_t maxWidth, maxHeight;
const auto& rootWidget = getRootWidget();
if (rootWidget) {
maxWidth = rootWidget->getClientWidth();
maxHeight = rootWidget->getClientHeight();
}
else {
// fallback to xterm default size
maxWidth = 80;
maxHeight = 24;
}
const int x = 1 + int((maxWidth - getWidth()) / 2);
const int y = 1 + int((maxHeight - getHeight()) / 3);
FWindow::setPos(FPoint{x, y}, false);
FDialog::adjustSize();
FDialog::setModal();
initControls();
FDialog::initLayout();
rootWidget->redraw(); // bryanb: wtf?
redraw();
}
/**
* @brief Initializes window controls.
*/
virtual void initControls()
{
m_closeButton.setGeometry(FPoint(int(getWidth()) - 12, int(getHeight()) - 6), FSize(9, 3));
m_closeButton.addCallback("clicked", [&]() { close(); });
if (!m_enableCloseButton) {
m_closeButton.setDisable();
m_closeButton.setVisible(false);
}
m_setButton.setDisable();
m_setButton.setVisible(false);
if (m_enableSetButton) {
m_setButton.setEnable();
m_setButton.setVisible(true);
m_setButton.setGeometry(FPoint(int(getWidth()) - 24, int(getHeight()) - 6), FSize(9, 3));
}
focusFirstChild();
}
/**
* @brief Adjusts window size.
*/
void adjustSize() override
{
FDialog::adjustSize();
}
/*
** Event Handlers
*/
/**
* @brief Event that occurs when the window is closed.
* @param e Close event.
*/
void onClose(FCloseEvent* e) override
{
hide();
}
};
#endif // __CLOSE_WND_BASE_H__

@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Peer ID Editor
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023,2024 Bryan Biedenkapp, N2PLL
*
*/
/**
* @defgroup peered Peer ID Editor (peered)
* @brief Digital Voice Modem - Peer ID Editor
* @details Helper software that edits peer ID files with a graphical TUI.
* @ingroup peered
*
* @file Defines.h
* @ingroup peered
*/
#if !defined(__DEFINES_H__)
#define __DEFINES_H__
#include "common/Defines.h"
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
#undef __PROG_NAME__
#define __PROG_NAME__ "Digital Voice Modem (DVM) Peer ID Editor"
#undef __EXE_NAME__
#define __EXE_NAME__ "peered"
#endif // __DEFINES_H__

@ -0,0 +1,87 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Peer ID Editor
* 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 FDblDialog.h
* @ingroup peered
*/
#if !defined(__F_DBL_DIALOG_H__)
#define __F_DBL_DIALOG_H__
#include "common/Defines.h"
#include <final/final.h>
using namespace finalcut;
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief This class implements the double-border dialog.
* @ingroup peered
*/
class HOST_SW_API FDblDialog : public finalcut::FDialog {
public:
/**
* @brief Initializes a new instance of the FDblDialog class.
* @param widget
*/
explicit FDblDialog(FWidget* widget = nullptr) : finalcut::FDialog{widget}
{
/* stub */
}
protected:
/**
* @brief
*/
void drawBorder() override
{
if (!hasBorder())
return;
setColor();
FRect box{{1, 2}, getSize()};
box.scaleBy(0, -1);
FRect rect = box;
if (rect.x1_ref() > rect.x2_ref())
std::swap(rect.x1_ref(), rect.x2_ref());
if (rect.y1_ref() > rect.y2_ref())
std::swap(rect.y1_ref(), rect.y2_ref());
rect.x1_ref() = std::max(rect.x1_ref(), 1);
rect.y1_ref() = std::max(rect.y1_ref(), 1);
rect.x2_ref() = std::min(rect.x2_ref(), rect.x1_ref() + int(getWidth()) - 1);
rect.y2_ref() = std::min(rect.y2_ref(), rect.y1_ref() + int(getHeight()) - 1);
if (box.getWidth() < 3)
return;
// Use box-drawing characters to draw a border
constexpr std::array<wchar_t, 8> box_char
{{
static_cast<wchar_t>(0x2554), // ╔
static_cast<wchar_t>(0x2550), // ═
static_cast<wchar_t>(0x2557), // ╗
static_cast<wchar_t>(0x2551), // ║
static_cast<wchar_t>(0x2551), // ║
static_cast<wchar_t>(0x255A), // ╚
static_cast<wchar_t>(0x2550), // ═
static_cast<wchar_t>(0x255D) // ╝
}};
drawGenericBox(this, box, box_char);
}
};
#endif // __F_DBL_DIALOG_H__

@ -0,0 +1,144 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Peer ID Editor
* 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 peered
*/
#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 peered
*/
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() - 20)}, FSize{maxWidth, 20});
FDialog::setMinimumSize(FSize{80, 20});
FDialog::setResizeable(true);
FDialog::setMinimizable(true);
FDialog::setTitlebarButtonVisibility(true);
FDialog::setShadow();
minimizeWindow();
m_scrollText.setGeometry(FPoint{1, 2}, FSize{getWidth(), getHeight() - 1});
FDialog::initLayout();
}
/**
* @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,214 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Peer ID Editor
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023,2024 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file PeerEdApplication.h
* @ingroup peered
*/
#if !defined(__PEERED_APPLICATION_H__)
#define __PEERED_APPLICATION_H__
#include "common/Log.h"
#include "PeerEdMain.h"
#include "PeerEdMainWnd.h"
#include <final/final.h>
using namespace finalcut;
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief This class implements a color theme for a finalcut application.
* @ingroup setup
*/
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::Blue;
current_element_fg = FColor::LightGray;
current_element_bg = FColor::DarkGray;
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 tged
*/
class HOST_SW_API PeerEdApplication final : public finalcut::FApplication {
public:
/**
* @brief Initializes a new instance of the PeerEdApplication class.
* @param argc Passed argc.
* @param argv Passed argv.
*/
explicit PeerEdApplication(const int& argc, char** argv) : FApplication{argc, argv}
{
m_statusRefreshTimer = addTimer(1000);
}
protected:
/**
* @brief Process external user events.
*/
void processExternalUserEvent() override
{
/* stub */
}
/*
** Event Handlers
*/
/**
* @brief Event that occurs on interval by timer.
* @param timer Timer Event
*/
void onTimer(FTimerEvent* timer) override
{
if (timer != nullptr) {
if (timer->getTimerId() == m_statusRefreshTimer) {
/* stub */
}
}
}
private:
int m_statusRefreshTimer;
};
#endif // __PEERED_APPLICATION_H__

@ -0,0 +1,225 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Peer ID Editor
* 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
*
*/
#include "Defines.h"
#include "common/yaml/Yaml.h"
#include "common/Log.h"
#include "PeerEdMain.h"
#include "PeerEdApplication.h"
#include "PeerEdMainWnd.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::PeerListLookup* g_pidLookups = 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);
}
/* 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-2025 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 <peer ID 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 peer ID file to edit\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 peer ID file to edit");
g_iniFile = std::string(argv[++i]);
if (g_iniFile.empty())
usage("error: %s", "peer ID 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-2025 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-2025 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" \
">> Peer ID Editor\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
PeerEdApplication app{argc, argv};
PeerEdMainWnd wnd{&app};
finalcut::FWidget::setMainWidget(&wnd);
g_logDisplayLevel = 0U;
g_pidLookups = new PeerListLookup(g_iniFile, PeerListLookup::WHITELIST, 0U, false);
g_pidLookups->read();
LogMessage(LOG_HOST, "Loaded peer ID file: %s", g_iniFile.c_str());
// 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,101 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Peer ID Editor
* 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 PeerEdMain.h
* @ingroup peered
* @file PeerEdMain.cpp
* @ingroup peered
*/
#if !defined(__PEERED_MAIN_H__)
#define __PEERED_MAIN_H__
#include "Defines.h"
#include "common/lookups/PeerListLookup.h"
#include "common/yaml/Yaml.h"
#include <string>
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
#undef __PROG_NAME__
#define __PROG_NAME__ "Digital Voice Modem (DVM) Peer ID Editor"
#undef __EXE_NAME__
#define __EXE_NAME__ "peered"
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* bryanb: This is some low-down, dirty, C++ hack-o-ramma.
*/
/**
* @brief Implements RTTI type defining.
* @typedef Tag
*/
template<typename Tag>
struct RTTIResult {
typedef typename Tag::type type;
static type ptr;
};
template<typename Tag>
typename RTTIResult<Tag>::type RTTIResult<Tag>::ptr;
/**
* @brief Implements nasty hack to access private members of a class.
* @typedef Tag
* @typedef TypePtr
*/
template<typename Tag, typename Tag::type TypePtr>
struct HackTheGibson : RTTIResult<Tag> {
/* fill it ... */
struct filler {
filler() { RTTIResult<Tag>::ptr = TypePtr; }
};
static filler fillerObj;
};
template<typename Tag, typename Tag::type TypePtr>
typename HackTheGibson<Tag, TypePtr>::filler HackTheGibson<Tag, TypePtr>::fillerObj;
// ---------------------------------------------------------------------------
// 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::PeerListLookup* g_pidLookups;
/**
* @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, ...);
#endif // __PEERED_MAIN_H__

@ -0,0 +1,218 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Peer ID Editor
* 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 PeerEdMainWnd.h
* @ingroup peered
*/
#if !defined(__PEERED_MAIN_WND_H__)
#define __PEERED_MAIN_WND_H__
#include "common/Log.h"
#include "common/Thread.h"
using namespace lookups;
#include <final/final.h>
using namespace finalcut;
#undef null
#include "PeerEdMain.h"
#include "LogDisplayWnd.h"
#include "PeerListWnd.h"
#include "PeerEditWnd.h"
#include <vector>
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
#define MINIMUM_SUPPORTED_SIZE_WIDTH 83
#define MINIMUM_SUPPORTED_SIZE_HEIGHT 30
// ---------------------------------------------------------------------------
// Class Prototypes
// ---------------------------------------------------------------------------
class HOST_SW_API PeerEdApplication;
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief This class implements the root window control.
* @ingroup peered
*/
class HOST_SW_API PeerEdMainWnd final : public finalcut::FWidget {
public:
/**
* @brief Initializes a new instance of the PeerEdMainWnd class.
* @param widget
*/
explicit PeerEdMainWnd(FWidget* widget = nullptr) : FWidget{widget}
{
__InternalOutputStream(m_logWnd);
// file menu
m_fileMenuSeparator1.setSeparator();
m_fileMenuSeparator2.setSeparator();
m_saveSettingsItem.addAccelerator(FKey::Meta_s); // Meta/Alt + S
m_saveSettingsItem.addCallback("clicked", this, [&]() { save(); });
m_reloadSettingsItem.addAccelerator(FKey::Meta_r); // Meta/Alt + R
m_reloadSettingsItem.addCallback("clicked", this, [&]() { g_pidLookups->reload(); m_wnd->loadListView(); });
m_keyF2.addCallback("activate", this, [&]() { save(); });
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);
m_keyF5.addCallback("activate", this, [&]() { g_pidLookups->reload(); m_wnd->loadListView(); LogMessage(LOG_HOST, "Loaded peer ID file: %s", g_iniFile.c_str()); });
m_backupOnSave.setChecked();
// 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-2025 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 PeerEdApplication;
LogDisplayWnd m_logWnd{this};
PeerListWnd* m_wnd;
FMenuBar m_menuBar{this};
FMenu m_fileMenu{"&File", &m_menuBar};
FMenuItem m_reloadSettingsItem{"&Reload", &m_fileMenu};
FMenuItem m_saveSettingsItem{"&Save", &m_fileMenu};
FMenuItem m_fileMenuSeparator2{&m_fileMenu};
FCheckMenuItem m_saveOnCloseToggle{"Save on Close?", &m_fileMenu};
FCheckMenuItem m_backupOnSave{"Backup Rules File?", &m_fileMenu};
FMenuItem m_fileMenuSeparator1{&m_fileMenu};
FMenuItem m_quitItem{"&Quit", &m_fileMenu};
FMenu m_helpMenu{"&Help", &m_menuBar};
FMenuItem m_aboutItem{"&About", &m_helpMenu};
FStatusBar m_statusBar{this};
FStatusKey m_keyF2{FKey::F2, "Save", &m_statusBar};
FStatusKey m_keyF3{FKey::F3, "Quit", &m_statusBar};
FStatusKey m_keyF5{FKey::F5, "Reload", &m_statusBar};
/**
* @brief
*/
void save()
{
if (m_backupOnSave.isChecked()) {
std::string bakFile = g_iniFile + ".bak";
LogMessage(LOG_HOST, "Backing up existing file %s to %s", g_iniFile.c_str(), bakFile.c_str());
copyFile(g_iniFile.c_str(), bakFile.c_str());
}
g_pidLookups->commit();
}
/**
* @brief Helper to copy one file to another.
* @param src Source file.
* @param dest Destination File.
* @returns bool True, if file copied, otherwise false.
*/
bool copyFile(const char *srcFilePath, const char* destFilePath)
{
std::ifstream src(srcFilePath, std::ios::binary);
std::ofstream dest(destFilePath, std::ios::binary);
dest << src.rdbuf();
return src && dest;
}
/*
** Event Handlers
*/
/**
* @brief Event that occurs when the window is shown.
* @param e Show Event
*/
void onShow(FShowEvent* e) override
{
const auto& rootWidget = getRootWidget();
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 = PEER_LIST_HEIGHT;
if (rootWidget) {
maxHeight = rootWidget->getClientHeight() - 3;
}
m_wnd = new PeerListWnd(this);
if (maxHeight - 21 < PEER_LIST_HEIGHT)
maxHeight = PEER_LIST_HEIGHT + 21;
m_wnd->setGeometry(FPoint{2, 2}, FSize{(size_t)(maxWidth - 1), (size_t)(maxHeight - 21)});
m_wnd->setModal(false);
m_wnd->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
{
// if we are saving on close -- fire off the file save event
if (m_saveOnCloseToggle.isChecked()) {
g_pidLookups->commit();
}
FApplication::closeConfirmationDialog(this, e);
}
};
#endif // __PEERED_MAIN_WND_H__

@ -0,0 +1,367 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Peer ID Editor
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file PeerEditWnd.h
* @ingroup peered
*/
#if !defined(__PEER_EDIT_WND_H__)
#define __PEER_EDIT_WND_H__
#include "common/Log.h"
#include "peered/CloseWndBase.h"
#include "peered/PeerEdMain.h"
#include <final/final.h>
using namespace finalcut;
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief This class implements the line edit control for peer IDs.
* @ingroup peered
*/
class HOST_SW_API PeerIdLineEdit final : public FLineEdit {
public:
/**
* @brief Initializes a new instance of the PeerIdLineEdit class.
* @param widget
*/
explicit PeerIdLineEdit(FWidget* widget = nullptr) : FLineEdit{widget}
{
setInputFilter("[[:digit:]]");
}
/*
** 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::Up) {
emitCallback("up-pressed");
e->accept();
return;
} else if (key == FKey::Down) {
emitCallback("down-pressed");
e->accept();
return;
}
FLineEdit::onKeyPress(e);
}
};
/**
* @brief This class implements the peer ID editor window.
* @ingroup peered
*/
class HOST_SW_API PeerEditWnd final : public CloseWndBase {
public:
/**
* @brief Initializes a new instance of the PeerEditWnd class.
*
* @param rule
* @param widget
*/
explicit PeerEditWnd(lookups::PeerId rule, FWidget* widget = nullptr) : CloseWndBase{widget}
{
m_rule = rule;
if (m_rule.peerDefault()) {
m_new = true;
} else {
m_origPeerId = m_rule.peerId();
}
}
private:
bool m_new;
bool m_skipSaving;
lookups::PeerId m_rule;
uint32_t m_origPeerId;
FLabel m_peerAliasLabel{"Alias: ", this};
FLineEdit m_peerAlias{this};
FCheckBox m_saveCopy{"Save Copy", this};
FCheckBox m_incOnSave{"Increment On Save", this};
FButtonGroup m_sourceGroup{"Peer ID", this};
FLabel m_peerIdLabel{"Peer ID: ", &m_sourceGroup};
PeerIdLineEdit m_peerId{&m_sourceGroup};
FLabel m_peerPasswordLabel{"Password: ", &m_sourceGroup};
FLineEdit m_peerPassword{&m_sourceGroup};
FButtonGroup m_configGroup{"Configuration", this};
FCheckBox m_peerLinkEnabled{"Peer Link", &m_configGroup};
FCheckBox m_canReqKeysEnabled{"Request Keys", &m_configGroup};
/**
* @brief Initializes the window layout.
*/
void initLayout() override
{
FDialog::setText("Peer ID");
FDialog::setSize(FSize{60, 18});
m_enableSetButton = false;
CloseWndBase::initLayout();
}
/**
* @brief Initializes window controls.
*/
void initControls() override
{
m_closeButton.setText("&OK");
m_peerAliasLabel.setGeometry(FPoint(2, 2), FSize(8, 1));
m_peerAlias.setGeometry(FPoint(11, 2), FSize(24, 1));
if (!m_rule.peerDefault()) {
m_peerAlias.setText(m_rule.peerAlias());
}
m_peerAlias.setShadow(false);
m_peerAlias.addCallback("changed", [&]() { m_rule.peerAlias(m_peerAlias.getText().toString()); });
m_saveCopy.setGeometry(FPoint(36, 2), FSize(18, 1));
m_saveCopy.addCallback("toggled", [&]() {
if (m_saveCopy.isChecked()) {
m_incOnSave.setEnable();
} else {
m_incOnSave.setChecked(false);
m_incOnSave.setDisable();
}
redraw();
});
m_incOnSave.setGeometry(FPoint(36, 3), FSize(18, 1));
m_incOnSave.setDisable();
// talkgroup source
{
m_sourceGroup.setGeometry(FPoint(2, 5), FSize(30, 5));
m_peerIdLabel.setGeometry(FPoint(2, 1), FSize(10, 1));
m_peerId.setGeometry(FPoint(11, 1), FSize(17, 1));
m_peerId.setAlignment(finalcut::Align::Right);
if (!m_rule.peerDefault()) {
m_peerId.setText(std::to_string(m_rule.peerId()));
} else {
m_rule.peerId(1U);
m_peerId.setText("1");
}
m_peerId.setShadow(false);
m_peerId.addCallback("up-pressed", [&]() {
uint32_t peerId = ::atoi(m_peerId.getText().c_str());
peerId++;
if (peerId > 999999999U) {
peerId = 999999999U;
}
m_peerId.setText(std::to_string(peerId));
m_rule.peerId(peerId);
redraw();
});
m_peerId.addCallback("down-pressed", [&]() {
uint32_t peerId = ::atoi(m_peerId.getText().c_str());
peerId--;
if (peerId < 1U) {
peerId = 1U;
}
m_peerId.setText(std::to_string(peerId));
m_rule.peerId(peerId);
redraw();
});
m_peerId.addCallback("changed", [&]() {
if (m_peerId.getText().getLength() == 0) {
m_rule.peerId(1U);
return;
}
uint32_t peerId = ::atoi(m_peerId.getText().c_str());
if (peerId < 1U) {
peerId = 1U;
}
if (peerId > 999999999U) {
peerId = 999999999U;
}
m_peerId.setText(std::to_string(peerId));
m_rule.peerId(peerId);
});
m_peerPasswordLabel.setGeometry(FPoint(2, 2), FSize(10, 1));
m_peerPassword.setGeometry(FPoint(11, 2), FSize(17, 1));
if (!m_rule.peerDefault()) {
m_peerPassword.setText(m_rule.peerPassword());
}
m_peerPassword.setShadow(false);
m_peerPassword.addCallback("changed", [&]() { m_rule.peerPassword(m_peerPassword.getText().toString()); });
}
// configuration
{
m_configGroup.setGeometry(FPoint(34, 5), FSize(23, 5));
m_peerLinkEnabled.setGeometry(FPoint(2, 1), FSize(10, 1));
m_peerLinkEnabled.setChecked(m_rule.peerLink());
m_peerLinkEnabled.addCallback("toggled", [&]() {
m_rule.peerLink(m_peerLinkEnabled.isChecked());
});
m_canReqKeysEnabled.setGeometry(FPoint(2, 2), FSize(10, 1));
m_canReqKeysEnabled.setChecked(m_rule.peerLink());
m_canReqKeysEnabled.addCallback("toggled", [&]() {
m_rule.canRequestKeys(m_canReqKeysEnabled.isChecked());
});
}
CloseWndBase::initControls();
}
/**
* @brief
*/
void logRuleInfo()
{
std::string peerAlias = m_rule.peerAlias();
uint32_t peerId = m_rule.peerId();
bool peerLink = m_rule.peerLink();
bool canRequestKeys = m_rule.canRequestKeys();
::LogInfoEx(LOG_HOST, "Peer ALIAS: %s PEERID: %u PEER LINK: %u CAN REQUEST KEYS: %u", peerAlias.c_str(), peerId, peerLink, canRequestKeys);
}
/*
** 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::Enter) {
this->close();
} else if (key == FKey::Escape) {
m_skipSaving = true;
this->close();
}
}
/**
* @brief Event that occurs when the window is closed.
* @param e Close event.
*/
void onClose(FCloseEvent* e) override
{
if (m_skipSaving) {
m_skipSaving = false;
CloseWndBase::onClose(e);
return;
}
if (!m_rule.peerDefault()) {
if (m_incOnSave.isChecked()) {
uint32_t peerId = m_rule.peerId();
peerId++;
if (peerId > 999999999U) {
peerId = 999999999U;
}
m_rule.peerId(peerId);
m_peerId.setText(std::to_string(peerId));
redraw();
}
if (m_origPeerId != 0U && !m_saveCopy.isChecked()) {
if (m_rule.peerId() == 0U) {
LogError(LOG_HOST, "Not saving peer, peer %s (%u), peer ID must be greater then 0.", m_rule.peerAlias().c_str(), m_rule.peerId());
FMessageBox::error(this, "Peer ID must be valid.");
return;
}
// update peer
auto peers = g_pidLookups->tableAsList();
auto it = std::find_if(peers.begin(), peers.end(),
[&](lookups::PeerId x)
{
return x.peerId() == m_origPeerId;
});
if (it != peers.end()) {
LogMessage(LOG_HOST, "Updating peer %s (%u) to %s (%u)", it->peerAlias().c_str(), it->peerId(), m_rule.peerAlias().c_str(), m_rule.peerId());
g_pidLookups->eraseEntry(m_origPeerId);
g_pidLookups->addEntry(m_rule.peerId(), m_rule.peerAlias(), m_rule.peerPassword(), m_rule.peerLink());
logRuleInfo();
}
} else {
if (m_rule.peerId() == 0U) {
LogError(LOG_HOST, "Not saving peer, peer %s (%u), peer ID must be greater then 0.", m_rule.peerAlias().c_str(), m_rule.peerId());
FMessageBox::error(this, "Peer ID must be valid.");
return;
}
auto peers = g_pidLookups->tableAsList();
auto it = std::find_if(peers.begin(), peers.end(),
[&](lookups::PeerId x)
{
return x.peerId() == m_rule.peerId();
});
if (it != peers.end()) {
LogError(LOG_HOST, "Not saving duplicate peer, peer %s (%u), peers must be unique.", m_rule.peerAlias().c_str(), m_rule.peerId());
FMessageBox::error(this, "Duplicate peer, change peer ID. Peers must be unique.");
if (m_saveCopy.isChecked())
m_saveCopy.setChecked(false);
return;
}
// add new peer
if (m_saveCopy.isChecked()) {
LogMessage(LOG_HOST, "Copying Peer. Adding Peer %s (%u)", m_rule.peerAlias().c_str(), m_rule.peerId());
} else {
LogMessage(LOG_HOST, "Adding Peer %s (%u)", m_rule.peerAlias().c_str(), m_rule.peerId());
}
g_pidLookups->addEntry(m_rule.peerId(), m_rule.peerAlias(), m_rule.peerPassword(), m_rule.peerLink());
logRuleInfo();
// don't actually close the modal on a copy save
if (m_saveCopy.isChecked()) {
return;
}
}
} else {
LogError(LOG_HOST, "Not saving peer, peer %s (%u), have a peer ID greater than 0.", m_rule.peerAlias().c_str(), m_rule.peerId());
FMessageBox::error(this, "Talkgroup must have a peer ID greater than 0.");
return;
}
CloseWndBase::onClose(e);
}
};
#endif // __PEER_EDIT_WND_H__

@ -0,0 +1,391 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Peer ID Editor
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file PeerListWnd.h
* @ingroup peered
*/
#if !defined(__PEER_LIST_WND_H__)
#define __PEER_LIST_WND_H__
#include "common/Log.h"
#include "FDblDialog.h"
#include "PeerEdMainWnd.h"
#include "PeerEditWnd.h"
#include <final/final.h>
using namespace finalcut;
struct PrivateFListViewScrollToY { typedef void(FListView::*type)(int); };
template class HackTheGibson<PrivateFListViewScrollToY, &FListView::scrollToY>;
struct PrivateFListViewIteratorFirst { typedef FListViewIterator FListView::*type; };
template class HackTheGibson<PrivateFListViewIteratorFirst, &FListView::first_visible_line>;
struct PrivateFListViewVBarPtr { typedef FScrollbarPtr FListView::*type; };
template class HackTheGibson<PrivateFListViewVBarPtr, &FListView::vbar>;
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
#define PEER_LIST_WIDTH 74
#define PEER_LIST_HEIGHT 15
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief This class implements the peer list window.
* @ingroup peered
*/
class HOST_SW_API PeerListWnd final : public FDblDialog {
public:
/**
* @brief Initializes a new instance of the PeerListWnd class.
* @param widget
*/
explicit PeerListWnd(FWidget* widget = nullptr) : FDblDialog{widget}
{
/* stub */
}
/**
* @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()
{
m_selected = PeerId();
m_selectedPeerId = 0U;
auto entry = g_pidLookups->tableAsList()[0U];
m_selected = entry;
// bryanb: HACK -- use HackTheGibson to access the private current listview iterator to get the scroll position
/*
* This uses the RTTI hack to access private members on FListView; and this code *could* break as a consequence.
*/
int firstScrollLinePos = 0;
if (m_listView.getCount() > 0) {
firstScrollLinePos = (m_listView.*RTTIResult<PrivateFListViewIteratorFirst>::ptr).getPosition();
}
m_listView.clear();
for (auto entry : g_pidLookups->tableAsList()) {
// pad peer ID properly
std::ostringstream oss;
oss << std::setw(7) << std::setfill('0') << entry.peerId();
bool masterPassword = (entry.peerPassword().size() == 0U);
// build list view entry
const std::array<std::string, 5U> columns = {
oss.str(),
(masterPassword) ? "X" : "",
(entry.peerLink()) ? "X" : "",
(entry.canRequestKeys()) ? "X" : "",
entry.peerAlias()
};
const finalcut::FStringList line(columns.cbegin(), columns.cend());
m_listView.insert(line);
}
// bryanb: HACK -- use HackTheGibson to access the private set scroll Y to set the scroll position
/*
* This uses the RTTI hack to access private members on FListView; and this code *could* break as a consequence.
*/
if ((size_t)firstScrollLinePos > m_listView.getCount())
firstScrollLinePos = 0;
if (firstScrollLinePos > 0 && m_listView.getCount() > 0) {
(m_listView.*RTTIResult<PrivateFListViewScrollToY>::ptr)(firstScrollLinePos);
(m_listView.*RTTIResult<PrivateFListViewVBarPtr>::ptr)->setValue(firstScrollLinePos);
}
// generate dialog title
uint32_t len = g_pidLookups->tableAsList().size();
std::stringstream ss;
ss << "Peer ID List (" << len << " Peers)";
FDialog::setText(ss.str());
setFocusWidget(&m_listView);
redraw();
}
private:
lookups::PeerId m_selected;
uint32_t m_selectedPeerId;
FListView m_listView{this};
FButton m_addPeer{"&Add", this};
FButton m_editPeer{"&Edit", this};
FLabel m_fileName{"/path/to/peer.dat", this};
FButton m_deletePeer{"&Delete", 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("Peer ID List");
initControls();
loadListView();
FDialog::initLayout();
}
/**
* @brief Initializes window controls.
*/
void initControls()
{
m_addPeer.setGeometry(FPoint(2, int(getHeight() - 4)), FSize(9, 1));
m_addPeer.setBackgroundColor(FColor::DarkGreen);
m_addPeer.setFocusBackgroundColor(FColor::DarkGreen);
m_addPeer.addCallback("clicked", [&]() { addEntry(); });
m_editPeer.setGeometry(FPoint(13, int(getHeight() - 4)), FSize(10, 1));
m_editPeer.setDisable();
m_editPeer.addCallback("clicked", [&]() { editEntry(); });
m_fileName.setGeometry(FPoint(27, int(getHeight() - 4)), FSize(42, 1));
m_fileName.setText(g_iniFile);
m_deletePeer.setGeometry(FPoint(int(getWidth()) - 13, int(getHeight() - 4)), FSize(10, 1));
m_deletePeer.setDisable();
m_deletePeer.addCallback("clicked", [&]() { deleteEntry(); });
m_listView.setGeometry(FPoint{1, 1}, FSize{getWidth() - 1, getHeight() - 5});
// configure list view columns
m_listView.addColumn("Peer ID", 10);
m_listView.addColumn("Master Password", 16);
m_listView.addColumn("Peer Link", 12);
m_listView.addColumn("Can Request Keys", 12);
m_listView.addColumn("Alias", 40);
// set right alignment for peer ID
m_listView.setColumnAlignment(2, finalcut::Align::Center);
m_listView.setColumnAlignment(3, finalcut::Align::Center);
m_listView.setColumnAlignment(4, finalcut::Align::Center);
m_listView.setColumnAlignment(5, finalcut::Align::Left);
// set type of sorting
m_listView.setColumnSortType(1, finalcut::SortType::Name);
// sort by peer ID
m_listView.setColumnSort(1, finalcut::SortOrder::Ascending);
m_listView.addCallback("clicked", [&]() { editEntry(); });
m_listView.addCallback("row-changed", [&]() {
FListViewItem* curItem = m_listView.getCurrentItem();
if (curItem != nullptr) {
FString strPeerId = curItem->getText(1);
uint32_t peerId = ::atoi(strPeerId.c_str());
if (peerId != m_selectedPeerId) {
auto entry = g_pidLookups->find(peerId);
if (!entry.peerDefault()) {
m_selected = entry;
m_selectedPeerId = peerId;
m_editPeer.setEnable();
m_deletePeer.setEnable();
m_deletePeer.setBackgroundColor(FColor::DarkRed);
m_deletePeer.setFocusBackgroundColor(FColor::DarkRed);
} else {
m_editPeer.setDisable();
m_deletePeer.setDisable();
m_deletePeer.resetColors();
}
redraw();
}
}
});
setFocusWidget(&m_listView);
redraw();
}
/**
* @brief
*/
void addEntry()
{
this->lowerWindow();
this->deactivateWindow();
PeerEditWnd wnd{PeerId(), this};
wnd.show();
this->raiseWindow();
this->activateWindow();
loadListView();
}
/**
* @brief
*/
void editEntry()
{
if (m_selected.peerDefault())
return;
this->lowerWindow();
this->deactivateWindow();
PeerEditWnd wnd{m_selected, this};
wnd.show();
this->raiseWindow();
this->activateWindow();
loadListView();
}
/**
* @brief
*/
void deleteEntry()
{
if (m_selected.peerDefault())
return;
LogMessage(LOG_HOST, "Deleting peer ID %s (%u)", m_selected.peerAlias().c_str(), m_selected.peerId());
g_pidLookups->eraseEntry(m_selected.peerId());
// bryanb: HACK -- use HackTheGibson to access the private current listview iterator to get the scroll position
/*
* This uses the RTTI hack to access private members on FListView; and this code *could* break as a consequence.
*/
int firstScrollLinePos = 0;
if (m_listView.getCount() > 0) {
firstScrollLinePos = (m_listView.*RTTIResult<PrivateFListViewIteratorFirst>::ptr).getPosition();
}
if ((size_t)firstScrollLinePos > m_listView.getCount())
firstScrollLinePos = 0;
if (firstScrollLinePos > 0 && m_listView.getCount() > 0) {
--firstScrollLinePos;
(m_listView.*RTTIResult<PrivateFListViewScrollToY>::ptr)(firstScrollLinePos);
(m_listView.*RTTIResult<PrivateFListViewVBarPtr>::ptr)->setValue(firstScrollLinePos);
}
loadListView();
}
/**
* @brief
*/
void drawBorder() override
{
if (!hasBorder())
return;
setColor();
FRect box{{1, 2}, getSize()};
box.scaleBy(0, -1);
FRect rect = box;
if (rect.x1_ref() > rect.x2_ref())
std::swap(rect.x1_ref(), rect.x2_ref());
if (rect.y1_ref() > rect.y2_ref())
std::swap(rect.y1_ref(), rect.y2_ref());
rect.x1_ref() = std::max(rect.x1_ref(), 1);
rect.y1_ref() = std::max(rect.y1_ref(), 1);
rect.x2_ref() = std::min(rect.x2_ref(), rect.x1_ref() + int(getWidth()) - 1);
rect.y2_ref() = std::min(rect.y2_ref(), rect.y1_ref() + int(getHeight()) - 1);
if (box.getWidth() < 3)
return;
// Use box-drawing characters to draw a border
constexpr std::array<wchar_t, 8> box_char
{{
static_cast<wchar_t>(0x2554), // ╔
static_cast<wchar_t>(0x2550), // ═
static_cast<wchar_t>(0x2557), // ╗
static_cast<wchar_t>(0x2551), // ║
static_cast<wchar_t>(0x2551), // ║
static_cast<wchar_t>(0x255A), // ╚
static_cast<wchar_t>(0x2550), // ═
static_cast<wchar_t>(0x255D) // ╝
}};
drawGenericBox(this, box, box_char);
}
/*
** 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::Insert) {
addEntry();
} else if (key == FKey::Enter || key == FKey::Return) {
editEntry();
}
}
};
#endif // __PEER_LIST_WND_H__
Loading…
Cancel
Save

Powered by TurnKey Linux.