diff --git a/DVMHost.vcxproj b/DVMHost.vcxproj
index b0b5d572..5b709d4b 100644
--- a/DVMHost.vcxproj
+++ b/DVMHost.vcxproj
@@ -190,6 +190,7 @@
+
@@ -267,6 +268,7 @@
+
diff --git a/DVMHost.vcxproj.filters b/DVMHost.vcxproj.filters
index 25cdafd9..e413b15e 100644
--- a/DVMHost.vcxproj.filters
+++ b/DVMHost.vcxproj.filters
@@ -117,6 +117,12 @@
{bd26735c-6610-4bfe-b1c4-27da1cb18e29}
+
+ {fdb4c489-755e-4655-a0e3-b1174c8e7acc}
+
+
+ {145b73dd-7759-4018-9df2-af162f652699}
+
@@ -353,6 +359,9 @@
Header Files\dmr\lc
+
+ Header Files\host\setup
+
@@ -568,6 +577,9 @@
Source Files\dmr\lc
+
+ Source Files\host\setup
+
diff --git a/HostMain.cpp b/HostMain.cpp
index b015578a..b194520c 100644
--- a/HostMain.cpp
+++ b/HostMain.cpp
@@ -12,7 +12,7 @@
//
/*
* Copyright (C) 2015,2016,2017 by Jonathan Naylor G4KLX
-* Copyright (C) 2020 by Bryan Biedenkapp N2PLL
+* Copyright (C) 2020,2021 by Bryan Biedenkapp N2PLL
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -32,6 +32,7 @@
#include "HostMain.h"
#include "host/Host.h"
#include "host/calibrate/HostCal.h"
+#include "host/setup/HostSetup.h"
#include "Log.h"
using namespace network;
@@ -61,6 +62,7 @@ using namespace lookups;
int g_signal = 0;
bool g_calibrate = false;
+bool g_setup = false;
std::string g_progExe = std::string(__EXE_NAME__);
std::string g_iniFile = std::string(DEFAULT_CONF_FILE);
std::string g_lockFile = std::string(DEFAULT_LOCK_FILE);
@@ -109,9 +111,10 @@ void usage(const char* message, const char* arg)
::fprintf(stderr, "\n\n");
}
- ::fprintf(stdout, "usage: %s [-v] [-f] [--cal] [-c ]\n\n"
+ ::fprintf(stdout, "usage: %s [-v] [-f] [--cal] [--setup] [-c ]\n\n"
" -f foreground mode\n"
" --cal calibration mode\n"
+ " --setup setup mode\n"
"\n"
" -v show version information\n"
" -h show this screen\n"
@@ -144,6 +147,9 @@ int checkArgs(int argc, char* argv[])
else if (IS("--cal")) {
g_calibrate = true;
}
+ else if (IS("--setup")) {
+ g_setup = true;
+ }
else if (IS("-c")) {
if (argc-- <= 0)
usage("error: %s", "must specify the configuration file to use");
@@ -209,10 +215,17 @@ int main(int argc, char** argv)
do {
g_signal = 0;
- if (g_calibrate) {
- HostCal* cal = new HostCal(g_iniFile);
- ret = cal->run();
- delete cal;
+ if (g_calibrate || g_setup) {
+ if (g_setup) {
+ HostSetup* setup = new HostSetup(g_iniFile);
+ ret = setup->run();
+ delete setup;
+ }
+ else {
+ HostCal* cal = new HostCal(g_iniFile);
+ ret = cal->run();
+ delete cal;
+ }
}
else {
Host* host = new Host(g_iniFile);
diff --git a/Log.h b/Log.h
index aece653e..b820ce11 100644
--- a/Log.h
+++ b/Log.h
@@ -46,6 +46,7 @@
#define LOG_P25 "P25"
#define LOG_DMR "DMR"
#define LOG_CAL "CAL"
+#define LOG_SETUP "SETUP"
// ---------------------------------------------------------------------------
// Macros
diff --git a/Makefile b/Makefile
index a46e2bea..887aeefc 100644
--- a/Makefile
+++ b/Makefile
@@ -68,6 +68,7 @@ OBJECTS = \
yaml/Yaml.o \
host/calibrate/Console.o \
host/calibrate/HostCal.o \
+ host/setup/HostSetup.o \
host/Host.o \
Log.o \
Mutex.o \
@@ -86,4 +87,4 @@ dvmhost: $(OBJECTS)
$(CXX) $(CFLAGS) -c -o $@ $<
clean:
- $(RM) dvmhost *.o *.d *.bak *~ edac/*.o dmr/*.o dmr/acl/*.o dmr/data/*.o dmr/edac/*.o dmr/lc/*.o p25/*.o p25/acl/*.o p25/data/*.o p25/edac/*.o p25/lc/*.o lookups/*.o modem/*.o modem/port/*.o network/*.o yaml/*.o host/*.o host/calibrate/*.o
+ $(RM) dvmhost *.o *.d *.bak *~ edac/*.o dmr/*.o dmr/acl/*.o dmr/data/*.o dmr/edac/*.o dmr/lc/*.o p25/*.o p25/acl/*.o p25/data/*.o p25/edac/*.o p25/lc/*.o lookups/*.o modem/*.o modem/port/*.o network/*.o yaml/*.o host/*.o host/calibrate/*.o host/setup/*.o
diff --git a/Makefile.arm b/Makefile.arm
index 2cee2e35..effff533 100644
--- a/Makefile.arm
+++ b/Makefile.arm
@@ -68,6 +68,7 @@ OBJECTS = \
yaml/Yaml.o \
host/calibrate/Console.o \
host/calibrate/HostCal.o \
+ host/setup/HostSetup.o \
host/Host.o \
Log.o \
Mutex.o \
@@ -86,4 +87,4 @@ dvmhost: $(OBJECTS)
$(CXX) $(CFLAGS) -c -o $@ $<
clean:
- $(RM) dvmhost *.o *.d *.bak *~ edac/*.o dmr/*.o dmr/acl/*.o dmr/data/*.o dmr/edac/*.o dmr/lc/*.o p25/*.o p25/acl/*.o p25/data/*.o p25/edac/*.o p25/lc/*.o lookups/*.o modem/*.o modem/port/*.o network/*.o yaml/*.o host/*.o host/calibrate/*.o
+ $(RM) dvmhost *.o *.d *.bak *~ edac/*.o dmr/*.o dmr/acl/*.o dmr/data/*.o dmr/edac/*.o dmr/lc/*.o p25/*.o p25/acl/*.o p25/data/*.o p25/edac/*.o p25/lc/*.o lookups/*.o modem/*.o modem/port/*.o network/*.o yaml/*.o host/*.o host/calibrate/*.o host/setup/*.o
diff --git a/Makefile.rpi-arm b/Makefile.rpi-arm
index 84f9fbdd..09dd4dde 100644
--- a/Makefile.rpi-arm
+++ b/Makefile.rpi-arm
@@ -68,6 +68,7 @@ OBJECTS = \
yaml/Yaml.o \
host/calibrate/Console.o \
host/calibrate/HostCal.o \
+ host/setup/HostSetup.o \
host/Host.o \
Log.o \
Mutex.o \
@@ -86,4 +87,4 @@ dvmhost: $(OBJECTS)
$(CXX) $(CFLAGS) -c -o $@ $<
clean:
- $(RM) dvmhost *.o *.d *.bak *~ edac/*.o dmr/*.o dmr/acl/*.o dmr/data/*.o dmr/edac/*.o dmr/lc/*.o p25/*.o p25/acl/*.o p25/data/*.o p25/edac/*.o p25/lc/*.o lookups/*.o modem/*.o modem/port/*.o network/*.o yaml/*.o host/*.o host/calibrate/*.o
+ $(RM) dvmhost *.o *.d *.bak *~ edac/*.o dmr/*.o dmr/acl/*.o dmr/data/*.o dmr/edac/*.o dmr/lc/*.o p25/*.o p25/acl/*.o p25/data/*.o p25/edac/*.o p25/lc/*.o lookups/*.o modem/*.o modem/port/*.o network/*.o yaml/*.o host/*.o host/calibrate/*.o host/setup/*.o
diff --git a/host/calibrate/Console.cpp b/host/calibrate/Console.cpp
index 2959f60d..b674e05a 100644
--- a/host/calibrate/Console.cpp
+++ b/host/calibrate/Console.cpp
@@ -43,7 +43,6 @@
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
-
#if defined(_WIN32) || defined(_WIN64)
///
/// Initializes a new instance of the Console class.
@@ -174,3 +173,72 @@ void Console::close()
::fprintf(stderr, "tcsetattr: returned %d\r\n", n);
}
#endif
+
+///
+/// Retrieves an array of characters input on the keyboard.
+///
+///
+///
+///
+///
+int Console::getLine(char line[], int max, char mask)
+{
+ int nch = 0;
+ int c;
+ bool skipNext = false;
+ max = max - 1; /* leave room for '\0' */
+
+ while ((c = getChar()) != '\n') {
+ if (c != -1) {
+ if (c == 10 || c == 13)
+ break;
+
+ // skip "double-byte" control characters
+ if (c == 224) {
+ skipNext = true;
+ continue;
+ }
+
+ if (skipNext) {
+ skipNext = false;
+ continue;
+ }
+
+ // has characters and backspace character?
+ if (nch > 0 && (c == 127 || c == 8)) {
+ // handle backspace
+ ::fputc(0x8, stdout);
+ ::fputc(' ', stdout);
+ ::fputc(0x8, stdout);
+ ::fflush(stdout);
+
+ line[--nch] = 0;
+ }
+ else {
+ // skip control characters
+ if (iscntrl(c))
+ continue;
+
+ if (nch < max) {
+ // valid mask character?
+ if (' ' - 1 < mask && mask < 127)
+ ::fputc(mask, stdout);
+ else
+ ::fputc(c, stdout);
+ ::fflush(stdout);
+
+ line[nch++] = c;
+ }
+ }
+ }
+ }
+
+ if (c == EOF && nch == 0)
+ return EOF;
+
+ ::fputc('\n', stdout);
+ ::fflush(stdout);
+
+ line[nch] = '\0';
+ return nch;
+}
diff --git a/host/calibrate/Console.h b/host/calibrate/Console.h
index 563566ae..16101d3c 100644
--- a/host/calibrate/Console.h
+++ b/host/calibrate/Console.h
@@ -55,6 +55,9 @@ public:
/// Retrieves a character input on the keyboard.
int getChar();
+ /// Retrieves an array of characters input on the keyboard.
+ int getLine(char line[], int max, char mask);
+
/// Closes the terminal console.
void close();
diff --git a/host/calibrate/HostCal.cpp b/host/calibrate/HostCal.cpp
index 0c169b4e..d14d82d3 100644
--- a/host/calibrate/HostCal.cpp
+++ b/host/calibrate/HostCal.cpp
@@ -198,6 +198,7 @@ int HostCal::run()
::LogInfo(">> Modem Calibration");
yaml::Node systemConf = m_conf["system"];
+ m_duplex = systemConf["duplex"].as(true);
// try to load bandplan identity table
std::string idenLookupFile = systemConf["iden_table"]["file"].as();
@@ -442,6 +443,7 @@ int HostCal::run()
setTXDCOffset(1);
break;
+ /** Engineering Commands */
case '-':
setDMRSymLevel3Adj(-1);
break;
diff --git a/host/setup/HostSetup.cpp b/host/setup/HostSetup.cpp
new file mode 100644
index 00000000..9d3df641
--- /dev/null
+++ b/host/setup/HostSetup.cpp
@@ -0,0 +1,424 @@
+/**
+* Digital Voice Modem - Host Software
+* GPLv2 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / Host Software
+*
+*/
+//
+// Based on code from the MMDVMCal project. (https://github.com/g4klx/MMDVMCal)
+// Licensed under the GPLv2 License (https://opensource.org/licenses/GPL-2.0)
+//
+/*
+* Copyright (C) 2021 by Bryan Biedenkapp N2PLL
+*
+* This program is free software; you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation; either version 2 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+*/
+#include "host/setup/HostSetup.h"
+#include "HostMain.h"
+#include "Log.h"
+#include "Utils.h"
+
+using namespace lookups;
+
+#include
+#include
+
+#if !defined(_WIN32) && !defined(_WIN64)
+#include
+#endif
+
+// ---------------------------------------------------------------------------
+// Public Class Members
+// ---------------------------------------------------------------------------
+///
+/// Initializes a new instance of the HostSetup class.
+///
+/// Full-path to the configuration file.
+HostSetup::HostSetup(const std::string& confFile) :
+ m_confFile(confFile),
+ m_conf(),
+ m_console(),
+ m_duplex(true),
+ m_identity("ABCD123"),
+ m_callsign("ABCD123"),
+ m_rxFrequency(0U),
+ m_txFrequency(0U),
+ m_channelId(0U),
+ m_channelNo(0U),
+ m_idenTable(NULL)
+{
+ /* stub */
+}
+
+///
+/// Finalizes a instance of the HostSetup class.
+///
+HostSetup::~HostSetup()
+{
+ /* stub */
+}
+
+///
+/// Executes the processing loop.
+///
+/// Zero if successful, otherwise error occurred.
+int HostSetup::run()
+{
+ bool ret = yaml::Parse(m_conf, m_confFile.c_str());
+ if (!ret) {
+ ::fatal("cannot read the configuration file, %s\n", m_confFile.c_str());
+ }
+
+ // initialize system logging
+ ret = ::LogInitialise("", "", 0U, 1U);
+ if (!ret) {
+ ::fprintf(stderr, "unable to open the log file\n");
+ return 1;
+ }
+
+ getHostVersion();
+ ::LogInfo(">> Modem Setup");
+
+ yaml::Node systemConf = m_conf["system"];
+ m_duplex = systemConf["duplex"].as(true);
+
+ // try to load bandplan identity table
+ std::string idenLookupFile = systemConf["iden_table"]["file"].as();
+ uint32_t idenReloadTime = systemConf["iden_table"]["time"].as(0U);
+
+ if (idenLookupFile.length() <= 0U) {
+ ::LogError(LOG_HOST, "No bandplan identity table? This must be defined!");
+ return 1;
+ }
+
+ LogInfo("Iden Table Lookups");
+ LogInfo(" File: %s", idenLookupFile.length() > 0U ? idenLookupFile.c_str() : "None");
+ if (idenReloadTime > 0U)
+ LogInfo(" Reload: %u mins", idenReloadTime);
+
+ m_idenTable = new IdenTableLookup(idenLookupFile, idenReloadTime);
+ m_idenTable->read();
+
+ LogInfo("General Parameters");
+
+ m_identity = systemConf["identity"].as();
+ ::LogInfo(" Identity: %s", m_identity.c_str());
+
+ yaml::Node cwId = systemConf["cwId"];
+ uint32_t time = systemConf["cwId"].as(10U);
+ m_callsign = systemConf["cwId"].as();
+
+ LogInfo("CW Id Parameters");
+ LogInfo(" Time: %u mins", time);
+ LogInfo(" Callsign: %s", m_callsign.c_str());
+
+ m_callsign = cwId["callsign"].as();
+
+ yaml::Node rfssConfig = systemConf["config"];
+ m_channelId = (uint8_t)rfssConfig["channelId"].as(0U);
+ if (m_channelId > 15U) { // clamp to 15
+ m_channelId = 15U;
+ }
+
+ m_channelNo = (uint32_t)::strtoul(rfssConfig["channelNo"].as("1").c_str(), NULL, 16);
+ if (m_channelNo == 0U) { // clamp to 1
+ m_channelNo = 1U;
+ }
+ if (m_channelNo > 4095U) { // clamp to 4095
+ m_channelNo = 4095U;
+ }
+
+ if (!calculateRxTxFreq()) {
+ return false;
+ }
+
+ IdenTable entry = m_idenTable->find(m_channelId);
+ if (entry.baseFrequency() == 0U) {
+ ::LogError(LOG_HOST, "Channel Id %u has an invalid base frequency.", m_channelId);
+ return false;
+ }
+
+ LogInfo("System Config Parameters");
+ LogInfo(" RX Frequency: %uHz", m_rxFrequency);
+ LogInfo(" TX Frequency: %uHz", m_txFrequency);
+ LogInfo(" Base Frequency: %uHz", entry.baseFrequency());
+ LogInfo(" TX Offset: %fMHz", entry.txOffsetMhz());
+
+ // open terminal console
+ ret = m_console.open();
+ if (!ret) {
+ return 1;
+ }
+
+ displayHelp();
+
+ printStatus();
+
+ bool end = false;
+ while (!end) {
+ int c = m_console.getChar();
+ switch (c) {
+
+ /** Setup Commands */
+ case 'I':
+ {
+ char value[9] = { '\0' };
+ ::fprintf(stdout, "> Identity ? ");
+ ::fflush(stdout);
+
+ m_console.getLine(value, 9, 0);
+ m_identity = std::string(value);
+ writeConfig();
+ }
+ break;
+
+ case 'i':
+ {
+ char value[3] = { '\0' };
+ ::fprintf(stdout, "> Channel ID ? ");
+ ::fflush(stdout);
+
+ m_console.getLine(value, 3, 0);
+
+ uint8_t prevChannelId = m_channelId;
+
+ // bryanb: appease the compiler...
+ uint32_t channelId = m_channelId;
+ sscanf(value, "%u", &channelId);
+
+ m_channelId = (uint8_t)channelId;
+
+ IdenTable entry = m_idenTable->find(m_channelId);
+ if (entry.baseFrequency() == 0U) {
+ ::LogError(LOG_SETUP, "Channel Id %u has an invalid base frequency.", m_channelId);
+ m_channelId = prevChannelId;
+ }
+
+ writeConfig();
+ }
+ break;
+
+ case 'c':
+ {
+ char value[5] = { '\0' };
+ ::fprintf(stdout, "> Channel No ? ");
+ ::fflush(stdout);
+
+ m_console.getLine(value, 5, 0);
+
+ uint8_t prevChannelNo = m_channelNo;
+ sscanf(value, "%u", &m_channelNo);
+
+ if (m_channelNo < 0 || m_channelNo > 4096) {
+ ::LogError(LOG_SETUP, "Channel No %u is invalid.", m_channelNo);
+ m_channelNo = prevChannelNo;
+ }
+
+ writeConfig();
+ }
+ break;
+
+ case 'f':
+ {
+ char value[10] = { '\0' };
+ ::fprintf(stdout, "> Tx Frequency (Hz) ? ");
+ ::fflush(stdout);
+
+ m_console.getLine(value, 10, 0);
+
+ uint32_t txFrequency = m_txFrequency;
+ sscanf(value, "%u", &txFrequency);
+
+ IdenTable entry = m_idenTable->find(m_channelId);
+ if (txFrequency < entry.baseFrequency()) {
+ ::LogError(LOG_SETUP, "Tx Frequency %uHz is out of band range for base frequency %uHz. Tx Frequency must be greater then base frequency!", txFrequency, entry.baseFrequency());
+ break;
+ }
+
+ if (txFrequency > entry.baseFrequency() + 25500000) {
+ ::LogError(LOG_SETUP, "Tx Frequency %uHz is out of band range for base frequency %uHz. Tx Frequency must be no more then 25.5 Mhz higher then base frequency!", txFrequency, entry.baseFrequency());
+ break;
+ }
+
+ uint32_t prevTxFrequency = m_txFrequency;
+ m_txFrequency = txFrequency;
+ uint32_t prevRxFrequency = m_rxFrequency;
+ m_rxFrequency = m_txFrequency + (entry.txOffsetMhz() * 1000000);
+
+ float spaceHz = entry.chSpaceKhz() * 1000;
+
+ uint32_t rootFreq = m_txFrequency - entry.baseFrequency();
+ uint8_t prevChannelNo = m_channelNo;
+ m_channelNo = rootFreq / spaceHz;
+
+ if (m_channelNo < 0 || m_channelNo > 4096) {
+ ::LogError(LOG_SETUP, "Channel No %u is invalid.", m_channelNo);
+ m_channelNo = prevChannelNo;
+ m_txFrequency = prevTxFrequency;
+ m_rxFrequency = prevRxFrequency;
+ break;
+ }
+
+ writeConfig();
+ }
+ break;
+
+ /** General Commands */
+ case '`':
+ printStatus();
+ break;
+ case 'V':
+ getHostVersion();
+ break;
+ case 'H':
+ case 'h':
+ displayHelp();
+ break;
+ case 'S':
+ case 's':
+ {
+ yaml::Serialize(m_conf, m_confFile.c_str(), yaml::SerializeConfig(4, 64, false, false));
+ LogMessage(LOG_SETUP, " - Saved configuration to %s", m_confFile.c_str());
+ }
+ break;
+ case 'Q':
+ case 'q':
+ end = true;
+ break;
+
+ case 13:
+ case 10:
+ case -1:
+ break;
+ default:
+ LogError(LOG_SETUP, "Unknown command - %c (H/h for help)", c);
+ break;
+ }
+
+ sleep(5U);
+ }
+
+ m_console.close();
+ return 0;
+}
+
+// ---------------------------------------------------------------------------
+// Private Class Members
+// ---------------------------------------------------------------------------
+
+///
+/// Helper to print the help to the console.
+///
+void HostSetup::displayHelp()
+{
+ LogMessage(LOG_SETUP, "General Commands:");
+ LogMessage(LOG_SETUP, " ` Display current settings");
+ LogMessage(LOG_SETUP, " V Display version of host");
+ LogMessage(LOG_SETUP, " H/h Display help");
+ LogMessage(LOG_SETUP, " S/s Save settings to configuration file");
+ LogMessage(LOG_SETUP, " Q/q Quit");
+ LogMessage(LOG_SETUP, "Setup Commands:");
+ LogMessage(LOG_SETUP, " I Set identity (logical name)");
+ LogMessage(LOG_SETUP, " i Set logical channel ID");
+ LogMessage(LOG_SETUP, " c Set logical channel number (by channel number)");
+ LogMessage(LOG_SETUP, " f Set logical channel number (by Tx frequency)");
+}
+
+///
+/// Helper to calculate the Rx/Tx frequencies.
+///
+bool HostSetup::calculateRxTxFreq()
+{
+ IdenTable entry = m_idenTable->find(m_channelId);
+ if (entry.baseFrequency() == 0U) {
+ ::LogError(LOG_HOST, "Channel Id %u has an invalid base frequency.", m_channelId);
+ return false;
+ }
+
+ if (m_channelNo == 0U) { // clamp to 1
+ m_channelNo = 1U;
+ }
+ if (m_channelNo > 4095U) { // clamp to 4095
+ m_channelNo = 4095U;
+ }
+
+ if (m_duplex) {
+ if (entry.txOffsetMhz() == 0U) {
+ ::LogError(LOG_HOST, "Channel Id %u has an invalid Tx offset.", m_channelId);
+ return false;
+ }
+
+ uint32_t calcSpace = (uint32_t)(entry.chSpaceKhz() / 0.125);
+ float calcTxOffset = entry.txOffsetMhz() * 1000000;
+
+ m_rxFrequency = (uint32_t)((entry.baseFrequency() + ((calcSpace * 125) * m_channelNo)) + calcTxOffset);
+ m_txFrequency = (uint32_t)((entry.baseFrequency() + ((calcSpace * 125) * m_channelNo)));
+ }
+ else {
+ uint32_t calcSpace = (uint32_t)(entry.chSpaceKhz() / 0.125);
+
+ m_rxFrequency = (uint32_t)((entry.baseFrequency() + ((calcSpace * 125) * m_channelNo)));
+ m_txFrequency = m_rxFrequency;
+ }
+
+ return true;
+}
+
+///
+/// Write configuration file.
+///
+/// True, if configuration is written, otherwise false.
+bool HostSetup::writeConfig()
+{
+ m_conf["system"]["identity"] = m_identity;
+
+ m_conf["system"]["config"]["channelId"] = __INT_STR(m_channelId);
+ m_conf["system"]["config"]["channelNo"] = __INT_STR(m_channelNo);
+
+ printStatus();
+ return true;
+}
+
+///
+/// Helper to sleep the thread.
+///
+/// Milliseconds to sleep.
+void HostSetup::sleep(uint32_t ms)
+{
+#if defined(_WIN32) || defined(_WIN64)
+ ::Sleep(ms);
+#else
+ ::usleep(ms * 1000);
+#endif
+}
+
+///
+/// Prints the current status of the calibration.
+///
+void HostSetup::printStatus()
+{
+ IdenTable entry = m_idenTable->find(m_channelId);
+ if (entry.baseFrequency() == 0U) {
+ ::LogError(LOG_HOST, "Channel Id %u has an invalid base frequency.", m_channelId);
+ }
+
+ calculateRxTxFreq();
+
+ LogMessage(LOG_SETUP, " - Channel ID: %u, Channel No: %u", m_channelId, m_channelNo);
+ LogMessage(LOG_SETUP, " - Base Freq: %uHz, TX Offset: %fMHz, Bandwidth: %fKHz, Channel Spacing: %fKHz", entry.baseFrequency(), entry.txOffsetMhz(), entry.chBandwidthKhz(), entry.chSpaceKhz());
+ LogMessage(LOG_SETUP, " - Rx Freq: %uHz, Tx Freq: %uHz, Identity: %s, Callsign: %s", m_rxFrequency, m_txFrequency, m_identity.c_str(), m_callsign.c_str());
+}
diff --git a/host/setup/HostSetup.h b/host/setup/HostSetup.h
new file mode 100644
index 00000000..a9501fb2
--- /dev/null
+++ b/host/setup/HostSetup.h
@@ -0,0 +1,89 @@
+/**
+* Digital Voice Modem - Host Software
+* GPLv2 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / Host Software
+*
+*/
+//
+// Based on code from the MMDVMCal project. (https://github.com/g4klx/MMDVMCal)
+// Licensed under the GPLv2 License (https://opensource.org/licenses/GPL-2.0)
+//
+/*
+* Copyright (C) 2021 by Bryan Biedenkapp N2PLL
+*
+* This program is free software; you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation; either version 2 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+*/
+#if !defined(__HOST_SETUP_H__)
+#define __HOST_SETUP_H__
+
+#include "Defines.h"
+#include "host/calibrate/Console.h"
+#include "host/Host.h"
+#include "lookups/IdenTableLookup.h"
+#include "yaml/Yaml.h"
+
+#include
+
+// ---------------------------------------------------------------------------
+// Class Declaration
+// This class implements an interactive session to setup the DVM.
+// ---------------------------------------------------------------------------
+
+class HOST_SW_API HostSetup {
+public:
+ /// Initializes a new instance of the HostSetup class.
+ HostSetup(const std::string& confFile);
+ /// Finalizes a instance of the HostSetup class.
+ ~HostSetup();
+
+ /// Executes the processing loop.
+ int run();
+
+private:
+ const std::string& m_confFile;
+ yaml::Node m_conf;
+
+ Console m_console;
+
+ bool m_duplex;
+
+ std::string m_identity;
+ std::string m_callsign;
+
+ uint32_t m_rxFrequency;
+ uint32_t m_txFrequency;
+ uint8_t m_channelId;
+ uint32_t m_channelNo;
+
+ lookups::IdenTableLookup* m_idenTable;
+
+ /// Helper to print the help to the console.
+ void displayHelp();
+
+ /// Helper to calculate the Rx/Tx frequencies.
+ bool calculateRxTxFreq();
+
+ /// Write configuration file.
+ bool writeConfig();
+ /// Helper to sleep the thread.
+ void sleep(uint32_t ms);
+
+ /// Prints the current status.
+ void printStatus();
+};
+
+#endif // __HOST_SETUP_H__