// SPDX-License-Identifier: GPL-2.0-only /** * Digital Voice Modem - 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 / Modem Host Software * @derivedfrom MMDVMHost (https://github.com/g4klx/MMDVMHost) * @license GPLv2 License (https://opensource.org/licenses/GPL-2.0) * * Copyright (C) 2015,2016,2017 Jonathan Naylor, G4KLX * Copyright (C) 2017-2023 Bryan Biedenkapp, N2PLL * Copyright (C) 2021 Nat Moore * */ #include "Defines.h" #include "common/lookups/RSSIInterpolator.h" #include "common/Log.h" #include "common/StopWatch.h" #include "common/Thread.h" #include "common/ThreadFunc.h" #include "common/Utils.h" #include "host/Host.h" #include "ActivityLog.h" #include "HostMain.h" using namespace modem; using namespace lookups; #include #include #include #include #include #include // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- #define CW_IDLE_SLEEP_MS 50U #define IDLE_WARMUP_MS 5U // --------------------------------------------------------------------------- // Public Class Members // --------------------------------------------------------------------------- /// /// Initializes a new instance of the Host class. /// /// Full-path to the configuration file. Host::Host(const std::string& confFile) : m_confFile(confFile), m_conf(), m_modem(nullptr), m_modemRemote(false), m_network(nullptr), m_modemRemotePort(nullptr), m_state(STATE_IDLE), m_isTxCW(false), m_modeTimer(1000U), m_dmrTXTimer(1000U), m_cwIdTimer(1000U), m_dmrEnabled(false), m_p25Enabled(false), m_nxdnEnabled(false), m_duplex(false), m_fixedMode(false), m_timeout(180U), m_rfModeHang(10U), m_rfTalkgroupHang(10U), m_netModeHang(3U), m_lastDstId(0U), m_lastSrcId(0U), m_identity(), m_cwCallsign(), m_cwIdTime(0U), m_latitude(0.0F), m_longitude(0.0F), m_height(0), m_power(0U), m_location(), m_rxFrequency(0U), m_txFrequency(0U), m_channelId(0U), m_channelNo(0U), m_voiceChNo(), m_voiceChData(), m_controlChData(), m_idenTable(nullptr), m_ridLookup(nullptr), m_tidLookup(nullptr), m_dmrBeacons(false), m_dmrTSCCData(false), m_dmrCtrlChannel(false), m_p25CCData(false), m_p25CtrlChannel(false), m_p25CtrlBroadcast(false), m_nxdnCCData(false), m_nxdnCtrlChannel(false), m_nxdnCtrlBroadcast(false), m_siteId(1U), m_sysId(1U), m_dmrNetId(1U), m_dmrColorCode(1U), m_p25NAC(0x293U), m_p25NetId(0xBB800U), m_p25RfssId(1U), m_nxdnRAN(1U), m_dmrQueueSizeBytes(3960U), // 24 frames m_p25QueueSizeBytes(2592U), // 12 frames m_nxdnQueueSizeBytes(1488U), // 31 frames m_authoritative(true), m_supervisor(false), m_dmrBeaconDurationTimer(1000U), m_p25BcastDurationTimer(1000U), m_nxdnBcastDurationTimer(1000U), m_activeTickDelay(5U), m_idleTickDelay(5U), m_RESTAPI(nullptr) { /* stub */ } /// /// Finalizes a instance of the Host class. /// Host::~Host() = default; /// /// Executes the main modem host processing loop. /// /// Zero if successful, otherwise error occurred. int Host::run() { bool ret = false; try { ret = yaml::Parse(m_conf, m_confFile.c_str()); if (!ret) { ::fatal("cannot read the configuration file, %s\n", m_confFile.c_str()); } } catch (yaml::OperationException const& e) { ::fatal("cannot read the configuration file - %s (%s)", m_confFile.c_str(), e.message()); } bool m_daemon = m_conf["daemon"].as(false); if (m_daemon && g_foreground) m_daemon = false; // initialize system logging yaml::Node logConf = m_conf["log"]; ret = ::LogInitialise(logConf["filePath"].as(), logConf["fileRoot"].as(), logConf["fileLevel"].as(0U), logConf["displayLevel"].as(0U)); if (!ret) { ::fatal("unable to open the log file\n"); } ret = ::ActivityLogInitialise(logConf["activityFilePath"].as(), logConf["fileRoot"].as()); if (!ret) { ::fatal("unable to open the activity log file\n"); } // handle POSIX process forking if (m_daemon) { // create new process pid_t pid = ::fork(); if (pid == -1) { ::fprintf(stderr, "%s: Couldn't fork() , exiting\n", g_progExe.c_str()); ::LogFinalise(); ::ActivityLogFinalise(); return EXIT_FAILURE; } else if (pid != 0) { ::LogFinalise(); ::ActivityLogFinalise(); exit(EXIT_SUCCESS); } // create new session and process group if (::setsid() == -1) { ::fprintf(stderr, "%s: Couldn't setsid(), exiting\n", g_progExe.c_str()); ::LogFinalise(); ::ActivityLogFinalise(); return EXIT_FAILURE; } // set the working directory to the root directory if (::chdir("/") == -1) { ::fprintf(stderr, "%s: Couldn't cd /, exiting\n", g_progExe.c_str()); ::LogFinalise(); ::ActivityLogFinalise(); return EXIT_FAILURE; } ::close(STDIN_FILENO); ::close(STDOUT_FILENO); ::close(STDERR_FILENO); } ::LogInfo(__BANNER__ "\r\n" __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" \ ">> Modem Controller\r\n"); // read base parameters from configuration ret = readParams(); if (!ret) return EXIT_FAILURE; // initialize modem ret = createModem(); if (!ret) return EXIT_FAILURE; // is the modem slaved to a remote DVM host? if (m_modemRemote) { ::LogInfoEx(LOG_HOST, "Host is up and running in remote modem mode"); StopWatch stopWatch; stopWatch.start(); bool killed = false; // main execution loop while (!killed) { if (m_modem->hasLockout() && m_state != HOST_STATE_LOCKOUT) setState(HOST_STATE_LOCKOUT); else if (!m_modem->hasLockout() && m_state == HOST_STATE_LOCKOUT) setState(STATE_IDLE); if (m_modem->hasError() && m_state != HOST_STATE_ERROR) setState(HOST_STATE_ERROR); else if (!m_modem->hasError() && m_state == HOST_STATE_ERROR) setState(STATE_IDLE); uint32_t ms = stopWatch.elapsed(); if (ms > 1U) m_modem->clock(ms); // ------------------------------------------------------ // -- Modem Clocking -- // ------------------------------------------------------ ms = stopWatch.elapsed(); stopWatch.start(); m_modem->clock(ms); if (g_killed) { if (!m_modem->hasTX()) { killed = true; } } m_modeTimer.clock(ms); if (ms < 2U) Thread::sleep(1U); } setState(HOST_STATE_QUIT); return EXIT_SUCCESS; } yaml::Node systemConf = m_conf["system"]; // try to load radio IDs table std::string ridLookupFile = systemConf["radio_id"]["file"].as(); uint32_t ridReloadTime = systemConf["radio_id"]["time"].as(0U); bool ridAcl = systemConf["radio_id"]["acl"].as(false); LogInfo("Radio Id Lookups"); LogInfo(" File: %s", ridLookupFile.length() > 0U ? ridLookupFile.c_str() : "None"); if (ridReloadTime > 0U) LogInfo(" Reload: %u mins", ridReloadTime); LogInfo(" ACL: %s", ridAcl ? "yes" : "no"); m_ridLookup = new RadioIdLookup(ridLookupFile, ridReloadTime, ridAcl); m_ridLookup->read(); // try to load talkgroup IDs table std::string tidLookupFile = systemConf["talkgroup_id"]["file"].as(); uint32_t tidReloadTime = systemConf["talkgroup_id"]["time"].as(0U); bool tidAcl = systemConf["talkgroup_id"]["acl"].as(false); LogInfo("Talkgroup Rule Lookups"); LogInfo(" File: %s", tidLookupFile.length() > 0U ? tidLookupFile.c_str() : "None"); if (tidReloadTime > 0U) LogInfo(" Reload: %u mins", tidReloadTime); LogInfo(" ACL: %s", tidAcl ? "yes" : "no"); m_tidLookup = new TalkgroupRulesLookup(tidLookupFile, tidReloadTime, tidAcl); m_tidLookup->read(); // initialize networking ret = createNetwork(); if (!ret) return EXIT_FAILURE; // set CW parameters if (systemConf["cwId"]["enable"].as(false)) { uint32_t time = systemConf["cwId"]["time"].as(10U); m_cwCallsign = systemConf["cwId"]["callsign"].as(); LogInfo("CW Id Parameters"); LogInfo(" Time: %u mins", time); LogInfo(" Callsign: %s", m_cwCallsign.c_str()); m_cwIdTime = time * 60U; m_cwIdTimer.setTimeout(m_cwIdTime / 2U); m_cwIdTimer.start(); } // for all modes we handle RSSI std::string rssiMappingFile = systemConf["modem"]["rssiMappingFile"].as(); RSSIInterpolator* rssi = new RSSIInterpolator; if (!rssiMappingFile.empty()) { LogInfo("RSSI"); LogInfo(" Mapping File: %s", rssiMappingFile.c_str()); rssi->load(rssiMappingFile); } yaml::Node protocolConf = m_conf["protocols"]; StopWatch stopWatch; stopWatch.start(); // initialize DMR Timer dmrBeaconIntervalTimer(1000U); std::unique_ptr dmr = nullptr; LogInfo("DMR Parameters"); LogInfo(" Enabled: %s", m_dmrEnabled ? "yes" : "no"); if (m_dmrEnabled) { yaml::Node dmrProtocol = protocolConf["dmr"]; m_dmrBeacons = dmrProtocol["beacons"]["enable"].as(false); m_dmrTSCCData = dmrProtocol["control"]["enable"].as(false); bool dmrCtrlChannel = dmrProtocol["control"]["dedicated"].as(false); bool embeddedLCOnly = dmrProtocol["embeddedLCOnly"].as(false); bool dmrDumpDataPacket = dmrProtocol["dumpDataPacket"].as(false); bool dmrRepeatDataPacket = dmrProtocol["repeatDataPacket"].as(true); bool dmrDumpCsbkData = dmrProtocol["dumpCsbkData"].as(false); bool dumpTAData = dmrProtocol["dumpTAData"].as(true); uint32_t callHang = dmrProtocol["callHang"].as(3U); uint32_t txHang = dmrProtocol["txHang"].as(4U); bool dmrVerbose = dmrProtocol["verbose"].as(true); bool dmrDebug = dmrProtocol["debug"].as(false); uint32_t jitter = m_conf["network"]["jitter"].as(360U); if (txHang > m_rfModeHang) txHang = m_rfModeHang; if (txHang > m_netModeHang) txHang = m_netModeHang; if (callHang > txHang) callHang = txHang; LogInfo(" Embedded LC Only: %s", embeddedLCOnly ? "yes" : "no"); LogInfo(" Dump Talker Alias Data: %s", dumpTAData ? "yes" : "no"); LogInfo(" Dump Packet Data: %s", dmrDumpDataPacket ? "yes" : "no"); LogInfo(" Repeat Packet Data: %s", dmrRepeatDataPacket ? "yes" : "no"); LogInfo(" Dump CSBK Data: %s", dmrDumpCsbkData ? "yes" : "no"); LogInfo(" Call Hang: %us", callHang); LogInfo(" TX Hang: %us", txHang); // forcibly enable beacons when TSCC is enabled but not in dedicated mode if (m_dmrTSCCData && !dmrCtrlChannel && !m_dmrBeacons) { m_dmrBeacons = true; } LogInfo(" Roaming Beacons: %s", m_dmrBeacons ? "yes" : "no"); if (m_dmrBeacons) { uint32_t dmrBeaconInterval = dmrProtocol["beacons"]["interval"].as(60U); uint32_t dmrBeaconDuration = dmrProtocol["beacons"]["duration"].as(3U); LogInfo(" Roaming Beacon Interval: %us", dmrBeaconInterval); LogInfo(" Roaming Beacon Duration: %us", dmrBeaconDuration); m_dmrBeaconDurationTimer.setTimeout(dmrBeaconDuration); dmrBeaconIntervalTimer.setTimeout(dmrBeaconInterval); dmrBeaconIntervalTimer.start(); g_fireDMRBeacon = true; } LogInfo(" TSCC Control: %s", m_dmrTSCCData ? "yes" : "no"); if (m_dmrTSCCData) { LogInfo(" TSCC Control Channel: %s", dmrCtrlChannel ? "yes" : "no"); if (dmrCtrlChannel) { m_dmrCtrlChannel = dmrCtrlChannel; } g_fireDMRBeacon = true; } dmr = std::make_unique(m_authoritative, m_dmrColorCode, callHang, m_dmrQueueSizeBytes, embeddedLCOnly, dumpTAData, m_timeout, m_rfTalkgroupHang, m_modem, m_network, m_duplex, m_ridLookup, m_tidLookup, m_idenTable, rssi, jitter, dmrDumpDataPacket, dmrRepeatDataPacket, dmrDumpCsbkData, dmrDebug, dmrVerbose); dmr->setOptions(m_conf, m_supervisor, m_voiceChNo, m_voiceChData, m_controlChData, m_dmrNetId, m_siteId, m_channelId, m_channelNo, true); if (dmrCtrlChannel) { dmr->setCCRunning(true); } m_dmrTXTimer.setTimeout(txHang); if (dmrVerbose) { LogInfo(" Verbose: yes"); } if (dmrDebug) { LogInfo(" Debug: yes"); } } // initialize P25 Timer p25BcastIntervalTimer(1000U); std::unique_ptr p25 = nullptr; LogInfo("P25 Parameters"); LogInfo(" Enabled: %s", m_p25Enabled ? "yes" : "no"); if (m_p25Enabled) { yaml::Node p25Protocol = protocolConf["p25"]; uint32_t tduPreambleCount = p25Protocol["tduPreambleCount"].as(8U); m_p25CCData = p25Protocol["control"]["enable"].as(false); bool p25CtrlChannel = p25Protocol["control"]["dedicated"].as(false); bool p25CtrlBroadcast = p25Protocol["control"]["broadcast"].as(true); bool p25DumpDataPacket = p25Protocol["dumpDataPacket"].as(false); bool p25RepeatDataPacket = p25Protocol["repeatDataPacket"].as(true); bool p25DumpTsbkData = p25Protocol["dumpTsbkData"].as(false); uint32_t callHang = p25Protocol["callHang"].as(3U); bool p25Verbose = p25Protocol["verbose"].as(true); bool p25Debug = p25Protocol["debug"].as(false); LogInfo(" TDU Preamble before Voice: %u", tduPreambleCount); LogInfo(" Dump Packet Data: %s", p25DumpDataPacket ? "yes" : "no"); LogInfo(" Repeat Packet Data: %s", p25RepeatDataPacket ? "yes" : "no"); LogInfo(" Dump TSBK Data: %s", p25DumpTsbkData ? "yes" : "no"); LogInfo(" Call Hang: %us", callHang); LogInfo(" Control: %s", m_p25CCData ? "yes" : "no"); uint32_t p25ControlBcstInterval = p25Protocol["control"]["interval"].as(300U); uint32_t p25ControlBcstDuration = p25Protocol["control"]["duration"].as(1U); if (m_p25CCData) { LogInfo(" Control Broadcast: %s", p25CtrlBroadcast ? "yes" : "no"); LogInfo(" Control Channel: %s", p25CtrlChannel ? "yes" : "no"); if (p25CtrlChannel) { m_p25CtrlChannel = p25CtrlChannel; } else { LogInfo(" Control Broadcast Interval: %us", p25ControlBcstInterval); LogInfo(" Control Broadcast Duration: %us", p25ControlBcstDuration); } m_p25BcastDurationTimer.setTimeout(p25ControlBcstDuration); p25BcastIntervalTimer.setTimeout(p25ControlBcstInterval); p25BcastIntervalTimer.start(); m_p25CtrlBroadcast = p25CtrlBroadcast; if (p25CtrlBroadcast) { g_fireP25Control = true; } } p25 = std::make_unique(m_authoritative, m_p25NAC, callHang, m_p25QueueSizeBytes, m_modem, m_network, m_timeout, m_rfTalkgroupHang, m_duplex, m_ridLookup, m_tidLookup, m_idenTable, rssi, p25DumpDataPacket, p25RepeatDataPacket, p25DumpTsbkData, p25Debug, p25Verbose); p25->setOptions(m_conf, m_supervisor, m_cwCallsign, m_voiceChNo, m_voiceChData, m_controlChData, m_p25NetId, m_sysId, m_p25RfssId, m_siteId, m_channelId, m_channelNo, true); if (p25CtrlChannel) { p25->setCCRunning(true); } if (p25Verbose) { LogInfo(" Verbose: yes"); } if (p25Debug) { LogInfo(" Debug: yes"); } } // initialize NXDN Timer nxdnBcastIntervalTimer(1000U); std::unique_ptr nxdn = nullptr; LogInfo("NXDN Parameters"); LogInfo(" Enabled: %s", m_nxdnEnabled ? "yes" : "no"); if (m_nxdnEnabled) { yaml::Node nxdnProtocol = protocolConf["nxdn"]; m_nxdnCCData = nxdnProtocol["control"]["enable"].as(false); bool nxdnCtrlChannel = nxdnProtocol["control"]["dedicated"].as(false); bool nxdnCtrlBroadcast = nxdnProtocol["control"]["broadcast"].as(true); bool nxdnDumpRcchData = nxdnProtocol["dumpRcchData"].as(false); uint32_t callHang = nxdnProtocol["callHang"].as(3U); bool nxdnVerbose = nxdnProtocol["verbose"].as(true); bool nxdnDebug = nxdnProtocol["debug"].as(false); LogInfo(" Call Hang: %us", callHang); LogInfo(" Control: %s", m_nxdnCCData ? "yes" : "no"); uint32_t nxdnControlBcstInterval = nxdnProtocol["control"]["interval"].as(300U); uint32_t nxdnControlBcstDuration = nxdnProtocol["control"]["duration"].as(1U); if (m_nxdnCCData) { LogInfo(" Control Broadcast: %s", nxdnCtrlBroadcast ? "yes" : "no"); LogInfo(" Control Channel: %s", nxdnCtrlChannel ? "yes" : "no"); if (nxdnCtrlChannel) { m_nxdnCtrlChannel = nxdnCtrlChannel; } else { LogInfo(" Control Broadcast Interval: %us", nxdnControlBcstInterval); LogInfo(" Control Broadcast Duration: %us", nxdnControlBcstDuration); } m_nxdnBcastDurationTimer.setTimeout(nxdnControlBcstDuration); nxdnBcastIntervalTimer.setTimeout(nxdnControlBcstInterval); nxdnBcastIntervalTimer.start(); m_nxdnCtrlBroadcast = nxdnCtrlBroadcast; if (nxdnCtrlBroadcast) { g_fireNXDNControl = true; } } nxdn = std::make_unique(m_authoritative, m_nxdnRAN, callHang, m_nxdnQueueSizeBytes, m_timeout, m_rfTalkgroupHang, m_modem, m_network, m_duplex, m_ridLookup, m_tidLookup, m_idenTable, rssi, nxdnDumpRcchData, nxdnDebug, nxdnVerbose); nxdn->setOptions(m_conf, m_supervisor, m_cwCallsign, m_voiceChNo, m_voiceChData, m_controlChData, m_siteId, m_sysId, m_channelId, m_channelNo, true); if (nxdnCtrlChannel) { nxdn->setCCRunning(true); } if (nxdnVerbose) { LogInfo(" Verbose: yes"); } if (nxdnDebug) { LogInfo(" Debug: yes"); } } if (!m_dmrEnabled && !m_p25Enabled && !m_nxdnEnabled) { ::LogError(LOG_HOST, "No protocols enabled? DMR, P25 and/or NXDN must be enabled!"); g_killed = true; } if (m_fixedMode && ((m_dmrEnabled && m_p25Enabled) || (m_dmrEnabled && m_nxdnEnabled) || (m_p25Enabled && m_nxdnEnabled))) { ::LogError(LOG_HOST, "Cannot have DMR, P25 and NXDN when using fixed state! Choose one protocol for fixed state operation."); g_killed = true; } // P25 CC checks if (m_dmrEnabled && m_p25CtrlChannel) { ::LogError(LOG_HOST, "Cannot have DMR enabled when using dedicated P25 control!"); g_killed = true; } if (m_nxdnEnabled && m_p25CtrlChannel) { ::LogError(LOG_HOST, "Cannot have NXDN enabled when using dedicated P25 control!"); g_killed = true; } if (!m_fixedMode && m_p25CtrlChannel) { ::LogWarning(LOG_HOST, "Fixed mode should be enabled when using dedicated P25 control!"); } if (!m_duplex && m_p25CCData) { ::LogError(LOG_HOST, "Cannot have P25 control and simplex mode at the same time."); g_killed = true; } // DMR TSCC checks if (m_p25Enabled && m_dmrCtrlChannel) { ::LogError(LOG_HOST, "Cannot have P25 enabled when using dedicated DMR TSCC control!"); g_killed = true; } if (m_nxdnEnabled && m_dmrCtrlChannel) { ::LogError(LOG_HOST, "Cannot have NXDN enabled when using dedicated DMR TSCC control!"); g_killed = true; } if (!m_fixedMode && m_dmrCtrlChannel) { ::LogWarning(LOG_HOST, "Fixed mode should be enabled when using dedicated DMR TSCC control!"); } if (!m_duplex && m_dmrTSCCData) { ::LogError(LOG_HOST, "Cannot have DMR TSCC control and simplex mode at the same time."); g_killed = true; } // NXDN CC checks if (m_dmrEnabled && m_nxdnCtrlChannel) { ::LogError(LOG_HOST, "Cannot have DMR enabled when using dedicated NXDN control!"); g_killed = true; } if (m_p25Enabled && m_nxdnCtrlChannel) { ::LogError(LOG_HOST, "Cannot have P25 enabled when using dedicated NXDN control!"); g_killed = true; } if (!m_fixedMode && m_nxdnCtrlChannel) { ::LogWarning(LOG_HOST, "Fixed mode should be enabled when using dedicated NXDN control!"); } if (!m_duplex && m_nxdnCCData) { ::LogError(LOG_HOST, "Cannot have NXDN control and simplex mode at the same time."); g_killed = true; } // DMR beacon checks if (m_dmrBeacons && m_p25CCData) { ::LogError(LOG_HOST, "Cannot have DMR roaming becaons and P25 control at the same time."); g_killed = true; } if (!m_duplex && m_dmrBeacons) { ::LogError(LOG_HOST, "Cannot have DMR roaming beacons and simplex mode at the same time."); g_killed = true; } bool killed = false; if (!g_killed) { // fixed mode will force a state change if (m_fixedMode) { if (dmr != nullptr) setState(STATE_DMR); if (p25 != nullptr) setState(STATE_P25); if (nxdn != nullptr) setState(STATE_NXDN); } else { if (m_dmrCtrlChannel) { m_fixedMode = true; setState(STATE_DMR); } if (m_p25CtrlChannel) { m_fixedMode = true; setState(STATE_P25); } if (m_nxdnCtrlChannel) { m_fixedMode = true; setState(STATE_NXDN); } setState(STATE_IDLE); } if (m_RESTAPI != nullptr) { m_RESTAPI->setProtocols(dmr.get(), p25.get(), nxdn.get()); } ::LogInfoEx(LOG_HOST, "Host is performing late initialization and warmup"); // perform early pumping of the modem clock (this is so the DSP has time to setup its buffers), // and clock the network (so it may perform early connect) uint32_t elapsedMs = 0U; while (!g_killed) { uint32_t ms = stopWatch.elapsed(); stopWatch.start(); elapsedMs += ms; m_modem->clock(ms); if (m_network != nullptr) m_network->clock(ms); Thread::sleep(IDLE_WARMUP_MS); if (elapsedMs > 15000U) break; } // check if the modem is a hotspot (this check must always be done after late init) if (m_modem->isHotspot()) { if ((m_dmrEnabled && m_p25Enabled) || (m_nxdnEnabled && m_dmrEnabled) || (m_nxdnEnabled && m_p25Enabled)) { ::LogError(LOG_HOST, "Multi-mode (DMR, P25 and NXDN) is not supported for hotspots!"); g_killed = true; killed = true; } else { if (!m_fixedMode) { ::LogInfoEx(LOG_HOST, "Host is running on a hotspot modem! Fixed mode is forced."); m_fixedMode = true; } } } ::LogInfoEx(LOG_HOST, "Host is up and running"); stopWatch.start(); } bool hasTxShutdown = false; std::mutex clockingMutex; // Macro to start DMR duplex idle transmission (or beacon) #define START_DMR_DUPLEX_IDLE(x) \ if (dmr != nullptr) { \ if (m_duplex) { \ m_modem->writeDMRStart(x); \ m_dmrTXTimer.start(); \ } \ } // setup protocol processor threads /** Digital Mobile Radio */ ThreadFunc dmrFrame1WriteThread([&, this]() { if (g_killed) return; if (dmr != nullptr) { LogDebug(LOG_HOST, "DMR, started slot 1 frame processor (modem write)"); while (!g_killed) { clockingMutex.lock(); { // ------------------------------------------------------ // -- Write to Modem Processing -- // ------------------------------------------------------ // write DMR slot 1 frames to modem writeFramesDMR1(dmr.get(), [&, this]() { // if there is a P25 CC running; halt the CC if (p25 != nullptr) { if (p25->getCCRunning() && !p25->getCCHalted()) { this->interruptP25Control(p25.get()); } } // if there is a NXDN CC running; halt the CC if (nxdn != nullptr) { if (nxdn->getCCRunning() && !nxdn->getCCHalted()) { this->interruptNXDNControl(nxdn.get()); } } }); } clockingMutex.unlock(); if (m_state != STATE_IDLE) Thread::sleep(m_activeTickDelay); if (m_state == STATE_IDLE) Thread::sleep(m_idleTickDelay); } } }); dmrFrame1WriteThread.run(); dmrFrame1WriteThread.setName("dmr:frame1-w"); ThreadFunc dmrFrame2WriteThread([&, this]() { if (g_killed) return; if (dmr != nullptr) { LogDebug(LOG_HOST, "DMR, started slot 2 frame processor (modem write)"); while (!g_killed) { clockingMutex.lock(); { // ------------------------------------------------------ // -- Write to Modem Processing -- // ------------------------------------------------------ // write DMR slot 2 frames to modem writeFramesDMR2(dmr.get(), [&, this]() { // if there is a P25 CC running; halt the CC if (p25 != nullptr) { if (p25->getCCRunning() && !p25->getCCHalted()) { this->interruptP25Control(p25.get()); } } // if there is a NXDN CC running; halt the CC if (nxdn != nullptr) { if (nxdn->getCCRunning() && !nxdn->getCCHalted()) { this->interruptNXDNControl(nxdn.get()); } } }); } clockingMutex.unlock(); if (m_state != STATE_IDLE) Thread::sleep(m_activeTickDelay); if (m_state == STATE_IDLE) Thread::sleep(m_idleTickDelay); } } }); dmrFrame2WriteThread.run(); dmrFrame2WriteThread.setName("dmr:frame2-w"); /** Project 25 */ ThreadFunc p25FrameWriteThread([&, this]() { if (g_killed) return; if (p25 != nullptr) { LogDebug(LOG_HOST, "P25, started frame processor (modem write)"); while (!g_killed) { clockingMutex.lock(); { // ------------------------------------------------------ // -- Write to Modem Processing -- // ------------------------------------------------------ // write P25 frames to modem writeFramesP25(p25.get(), [&, this]() { if (dmr != nullptr) { this->interruptDMRBeacon(dmr.get()); } // if there is a NXDN CC running; halt the CC if (nxdn != nullptr) { if (nxdn->getCCRunning() && !nxdn->getCCHalted()) { this->interruptNXDNControl(nxdn.get()); } } }); } clockingMutex.unlock(); if (m_state != STATE_IDLE) Thread::sleep(m_activeTickDelay); if (m_state == STATE_IDLE) Thread::sleep(m_idleTickDelay); } } }); p25FrameWriteThread.run(); p25FrameWriteThread.setName("p25:frame-w"); /** Next Generation Digital Narrowband */ ThreadFunc nxdnFrameWriteThread([&, this]() { if (g_killed) return; if (nxdn != nullptr) { LogDebug(LOG_HOST, "NXDN, started frame processor (modem write)"); while (!g_killed) { clockingMutex.lock(); { // ------------------------------------------------------ // -- Write to Modem Processing -- // ------------------------------------------------------ // write NXDN frames to modem writeFramesNXDN(nxdn.get(), [&, this]() { if (dmr != nullptr) { this->interruptDMRBeacon(dmr.get()); } // if there is a P25 CC running; halt the CC if (p25 != nullptr) { if (p25->getCCRunning() && !p25->getCCHalted()) { this->interruptP25Control(p25.get()); } } }); } clockingMutex.unlock(); if (m_state != STATE_IDLE) Thread::sleep(m_activeTickDelay); if (m_state == STATE_IDLE) Thread::sleep(m_idleTickDelay); } } }); nxdnFrameWriteThread.run(); nxdnFrameWriteThread.setName("nxdn:frame-w"); // main execution loop while (!killed) { if (m_modem->hasLockout() && m_state != HOST_STATE_LOCKOUT) setState(HOST_STATE_LOCKOUT); else if (!m_modem->hasLockout() && m_state == HOST_STATE_LOCKOUT) setState(STATE_IDLE); if (m_modem->hasError() && m_state != HOST_STATE_ERROR) setState(HOST_STATE_ERROR); else if (!m_modem->hasError() && m_state == HOST_STATE_ERROR) setState(STATE_IDLE); uint32_t ms = stopWatch.elapsed(); if (ms > 1U) m_modem->clock(ms); if (!m_fixedMode) { if (m_modeTimer.isRunning() && m_modeTimer.hasExpired()) { setState(STATE_IDLE); } } else { m_modeTimer.stop(); if (dmr != nullptr && m_state != STATE_DMR && !m_modem->hasTX()) { LogDebug(LOG_HOST, "fixed mode state abnormal, m_state = %u, state = %u", m_state, STATE_DMR); setState(STATE_DMR); } if (p25 != nullptr && m_state != STATE_P25 && !m_modem->hasTX()) { LogDebug(LOG_HOST, "fixed mode state abnormal, m_state = %u, state = %u", m_state, STATE_P25); setState(STATE_P25); } if (nxdn != nullptr && m_state != STATE_NXDN && !m_modem->hasTX()) { LogDebug(LOG_HOST, "fixed mode state abnormal, m_state = %u, state = %u", m_state, STATE_NXDN); setState(STATE_NXDN); } } clockingMutex.lock(); { // ------------------------------------------------------ // -- Modem Clocking -- // ------------------------------------------------------ ms = stopWatch.elapsed(); stopWatch.start(); m_modem->clock(ms); } clockingMutex.unlock(); // ------------------------------------------------------ // -- Read from Modem Processing -- // ------------------------------------------------------ /** Digital Mobile Radio */ if (dmr != nullptr) { // read DMR slot 1 frames from modem readFramesDMR1(dmr.get(), [&, this]() { if (dmr != nullptr) { this->interruptDMRBeacon(dmr.get()); } // if there is a P25 CC running; halt the CC if (p25 != nullptr) { if (p25->getCCRunning() && !p25->getCCHalted()) { this->interruptP25Control(p25.get()); } } // if there is a NXDN CC running; halt the CC if (nxdn != nullptr) { if (nxdn->getCCRunning() && !nxdn->getCCHalted()) { this->interruptNXDNControl(nxdn.get()); } } }); // read DMR slot 2 frames from modem readFramesDMR2(dmr.get(), [&, this]() { if (dmr != nullptr) { this->interruptDMRBeacon(dmr.get()); } // if there is a P25 CC running; halt the CC if (p25 != nullptr) { if (p25->getCCRunning() && !p25->getCCHalted()) { this->interruptP25Control(p25.get()); } } // if there is a NXDN CC running; halt the CC if (nxdn != nullptr) { if (nxdn->getCCRunning() && !nxdn->getCCHalted()) { this->interruptNXDNControl(nxdn.get()); } } }); } /** Project 25 */ if (p25 != nullptr) { // read P25 frames from modem readFramesP25(p25.get(), [&, this]() { if (dmr != nullptr) { this->interruptDMRBeacon(dmr.get()); } // if there is a NXDN CC running; halt the CC if (nxdn != nullptr) { if (nxdn->getCCRunning() && !nxdn->getCCHalted()) { this->interruptNXDNControl(nxdn.get()); } } }); } /** Next Generation Digital Narrowband */ if (nxdn != nullptr) { // read NXDN frames from modem readFramesNXDN(nxdn.get(), [&, this]() { if (dmr != nullptr) { this->interruptDMRBeacon(dmr.get()); } // if there is a P25 CC running; halt the CC if (p25 != nullptr) { if (p25->getCCRunning() && !p25->getCCHalted()) { this->interruptP25Control(p25.get()); } } }); } // ------------------------------------------------------ // -- Network, DMR, and P25 Clocking -- // ------------------------------------------------------ if (m_network != nullptr) m_network->clock(ms); if (dmr != nullptr) dmr->clock(ms); if (p25 != nullptr) p25->clock(ms); if (nxdn != nullptr) nxdn->clock(ms); // ------------------------------------------------------ // -- Timer Clocking -- // ------------------------------------------------------ // clock and check CW timer m_cwIdTimer.clock(ms); if (m_cwIdTimer.isRunning() && m_cwIdTimer.hasExpired()) { if (!m_modem->hasTX() && !m_p25CtrlChannel && !m_dmrCtrlChannel && !m_nxdnCtrlChannel) { if (m_dmrBeaconDurationTimer.isRunning() || m_p25BcastDurationTimer.isRunning() || m_nxdnBcastDurationTimer.isRunning()) { LogDebug(LOG_HOST, "CW, beacon or CC timer running, ceasing"); m_dmrBeaconDurationTimer.stop(); m_p25BcastDurationTimer.stop(); m_nxdnBcastDurationTimer.stop(); } LogDebug(LOG_HOST, "CW, start transmitting"); m_isTxCW = true; clockingMutex.lock(); setState(STATE_IDLE); m_modem->sendCWId(m_cwCallsign); Thread::sleep(CW_IDLE_SLEEP_MS); bool first = true; do { // ------------------------------------------------------ // -- Modem Clocking -- // ------------------------------------------------------ ms = stopWatch.elapsed(); stopWatch.start(); m_modem->clock(ms); if (!first && !m_modem->hasTX()) { LogDebug(LOG_HOST, "CW, finished transmitting"); break; } if (first) { first = false; Thread::sleep(200U + CW_IDLE_SLEEP_MS); // ~250ms; poll time of the modem } else Thread::sleep(CW_IDLE_SLEEP_MS); } while (true); clockingMutex.unlock(); m_isTxCW = false; m_cwIdTimer.setTimeout(m_cwIdTime); m_cwIdTimer.start(); } } /** Digial Mobile Radio */ if (dmr != nullptr) { if (m_dmrTSCCData && m_dmrCtrlChannel) { if (m_state != STATE_DMR) setState(STATE_DMR); if (!m_modem->hasTX()) { START_DMR_DUPLEX_IDLE(true); } } // clock and check DMR roaming beacon interval timer dmrBeaconIntervalTimer.clock(ms); if ((dmrBeaconIntervalTimer.isRunning() && dmrBeaconIntervalTimer.hasExpired()) || g_fireDMRBeacon) { if ((m_state == STATE_IDLE || m_state == STATE_DMR) && !m_modem->hasTX()) { if (m_modeTimer.isRunning()) { m_modeTimer.stop(); } if (m_state != STATE_DMR) setState(STATE_DMR); if (m_fixedMode) { START_DMR_DUPLEX_IDLE(true); } if (m_dmrTSCCData) { dmr->setCCRunning(true); } g_fireDMRBeacon = false; if (m_dmrTSCCData) { LogDebug(LOG_HOST, "DMR, start CC broadcast"); } else { LogDebug(LOG_HOST, "DMR, roaming beacon burst"); } dmrBeaconIntervalTimer.start(); m_dmrBeaconDurationTimer.start(); } } // clock and check DMR roaming beacon duration timer m_dmrBeaconDurationTimer.clock(ms); if (m_dmrBeaconDurationTimer.isRunning() && m_dmrBeaconDurationTimer.hasExpired()) { m_dmrBeaconDurationTimer.stop(); if (!m_fixedMode) { if (m_state == STATE_DMR && !m_modeTimer.isRunning()) { m_modeTimer.setTimeout(m_rfModeHang); m_modeTimer.start(); } } if (m_dmrTSCCData) { dmr->setCCRunning(false); } } // clock and check DMR Tx timer if (!m_dmrCtrlChannel) { m_dmrTXTimer.clock(ms); if (m_dmrTXTimer.isRunning() && m_dmrTXTimer.hasExpired()) { m_modem->writeDMRStart(false); m_dmrTXTimer.stop(); } } else { m_dmrTXTimer.stop(); } } /** Project 25 */ if (p25 != nullptr) { if (m_p25CCData) { p25BcastIntervalTimer.clock(ms); if (!m_p25CtrlChannel && m_p25CtrlBroadcast) { // clock and check P25 CC broadcast interval timer if ((p25BcastIntervalTimer.isRunning() && p25BcastIntervalTimer.hasExpired()) || g_fireP25Control) { if ((m_state == STATE_IDLE || m_state == STATE_P25) && !m_modem->hasTX()) { if (m_modeTimer.isRunning()) { m_modeTimer.stop(); } if (m_state != STATE_P25) setState(STATE_P25); p25->setCCRunning(true); // hide this message for continuous CC -- otherwise display every time we process if (!m_p25CtrlChannel) { LogMessage(LOG_HOST, "P25, start CC broadcast"); } g_fireP25Control = false; p25BcastIntervalTimer.start(); m_p25BcastDurationTimer.start(); // if the CC is continuous -- clock one cycle into the duration timer if (m_p25CtrlChannel) { m_p25BcastDurationTimer.clock(ms); } } } if (m_p25BcastDurationTimer.isPaused()) { m_p25BcastDurationTimer.resume(); } // clock and check P25 CC broadcast duration timer m_p25BcastDurationTimer.clock(ms); if (m_p25BcastDurationTimer.isRunning() && m_p25BcastDurationTimer.hasExpired()) { m_p25BcastDurationTimer.stop(); p25->setCCRunning(false); if (m_state == STATE_P25 && !m_modeTimer.isRunning()) { m_modeTimer.setTimeout(m_rfModeHang); m_modeTimer.start(); } } } } } /** Next Generation Digital Narrowband */ if (nxdn != nullptr) { if (m_nxdnCCData) { nxdnBcastIntervalTimer.clock(ms); if (!m_nxdnCtrlChannel && m_nxdnCtrlBroadcast) { // clock and check NXDN CC broadcast interval timer if ((nxdnBcastIntervalTimer.isRunning() && nxdnBcastIntervalTimer.hasExpired()) || g_fireNXDNControl) { if ((m_state == STATE_IDLE || m_state == STATE_NXDN) && !m_modem->hasTX()) { if (m_modeTimer.isRunning()) { m_modeTimer.stop(); } if (m_state != STATE_NXDN) setState(STATE_NXDN); //nxdn->writeAdjSSNetwork(); nxdn->setCCRunning(true); // hide this message for continuous CC -- otherwise display every time we process if (!m_nxdnCtrlChannel) { LogMessage(LOG_HOST, "NXDN, start CC broadcast"); } g_fireNXDNControl = false; nxdnBcastIntervalTimer.start(); m_nxdnBcastDurationTimer.start(); // if the CC is continuous -- clock one cycle into the duration timer if (m_nxdnCtrlChannel) { m_nxdnBcastDurationTimer.clock(ms); } } } if (m_nxdnBcastDurationTimer.isPaused()) { m_nxdnBcastDurationTimer.resume(); } // clock and check NXDN CC broadcast duration timer m_nxdnBcastDurationTimer.clock(ms); if (m_nxdnBcastDurationTimer.isRunning() && m_nxdnBcastDurationTimer.hasExpired()) { m_nxdnBcastDurationTimer.stop(); nxdn->setCCRunning(false); if (m_state == STATE_NXDN && !m_modeTimer.isRunning()) { m_modeTimer.setTimeout(m_rfModeHang); m_modeTimer.start(); } } } else { // simply use the NXDN CC interval timer in a non-broadcast state to transmit adjacent site data over // the network if (nxdnBcastIntervalTimer.isRunning() && nxdnBcastIntervalTimer.hasExpired()) { if ((m_state == STATE_IDLE || m_state == STATE_NXDN) && !m_modem->hasTX()) { //nxdn->writeAdjSSNetwork(); nxdnBcastIntervalTimer.start(); } } } } } if (g_killed) { // shutdown writer threads dmrFrame1WriteThread.wait(); dmrFrame2WriteThread.wait(); p25FrameWriteThread.wait(); nxdnFrameWriteThread.wait(); if (dmr != nullptr) { if (m_state == STATE_DMR && m_duplex && m_modem->hasTX()) { m_modem->writeDMRStart(false); m_dmrTXTimer.stop(); } if (m_dmrCtrlChannel) { if (!hasTxShutdown) { m_modem->clearDMRFrame1(); m_modem->clearDMRFrame2(); } dmr->setCCRunning(false); dmr->setCCHalted(true); m_dmrBeaconDurationTimer.stop(); dmrBeaconIntervalTimer.stop(); } } if (p25 != nullptr) { if (m_p25CtrlChannel) { if (!hasTxShutdown) { m_modem->clearP25Frame(); p25->reset(); } p25->setCCRunning(false); m_p25BcastDurationTimer.stop(); p25BcastIntervalTimer.stop(); } } if (nxdn != nullptr) { if (m_nxdnCtrlChannel) { if (!hasTxShutdown) { m_modem->clearNXDNFrame(); nxdn->reset(); } nxdn->setCCRunning(false); m_nxdnBcastDurationTimer.stop(); nxdnBcastIntervalTimer.stop(); } } hasTxShutdown = true; if (!m_modem->hasTX()) { killed = true; } } m_modeTimer.clock(ms); if ((m_state != STATE_IDLE) && ms <= m_activeTickDelay) Thread::sleep(m_activeTickDelay); if (m_state == STATE_IDLE) Thread::sleep(m_idleTickDelay); } if (rssi != nullptr) { delete rssi; } setState(HOST_STATE_QUIT); return EXIT_SUCCESS; } // --------------------------------------------------------------------------- // Private Class Members // --------------------------------------------------------------------------- /// /// /// /// bool Host::rmtPortModemOpen(Modem* modem) { assert(m_modemRemotePort != nullptr); bool ret = m_modemRemotePort->open(); if (!ret) return false; LogMessage(LOG_MODEM, "Modem Ready [Remote Mode]"); // handled modem open return true; } /// /// /// /// bool Host::rmtPortModemClose(Modem* modem) { assert(m_modemRemotePort != nullptr); m_modemRemotePort->close(); // handled modem close return true; } /// /// /// /// /// /// /// /// /// /// bool Host::rmtPortModemHandler(Modem* modem, uint32_t ms, modem::RESP_TYPE_DVM rspType, bool rspDblLen, const uint8_t* buffer, uint16_t len) { assert(m_modemRemotePort != nullptr); if (rspType == RTM_OK && len > 0U) { if (modem->getTrace()) Utils::dump(1U, "TX Remote Data", buffer, len); // send entire modem packet over the remote port m_modemRemotePort->write(buffer, len); } // read any data from the remote port for the air interface uint8_t data[BUFFER_LENGTH]; ::memset(data, 0x00U, BUFFER_LENGTH); uint32_t ret = m_modemRemotePort->read(data, BUFFER_LENGTH); if (ret > 0) { if (modem->getTrace()) Utils::dump(1U, "RX Remote Data", (uint8_t*)data, ret); if (ret < 3U) { LogError(LOG_MODEM, "Illegal length of remote data must be >3 bytes"); Utils::dump("Buffer dump", data, ret); // handled modem response return true; } uint8_t len = data[1U]; int ret = modem->write(data, len); if (ret != int(len)) LogError(LOG_MODEM, "Error writing remote data"); } // handled modem response return true; } /// /// Helper to set the host/modem running state. /// /// Mode enumeration to switch the host/modem state to. void Host::setState(uint8_t state) { assert(m_modem != nullptr); //if (m_state != state) { // LogDebug(LOG_HOST, "setState, m_state = %u, state = %u", m_state, state); //} switch (state) { case STATE_DMR: m_modem->setState(STATE_DMR); m_state = STATE_DMR; m_modeTimer.start(); //m_cwIdTimer.stop(); createLockFile("DMR"); break; case STATE_P25: m_modem->setState(STATE_P25); m_state = STATE_P25; m_modeTimer.start(); //m_cwIdTimer.stop(); createLockFile("P25"); break; case STATE_NXDN: m_modem->setState(STATE_NXDN); m_state = STATE_NXDN; m_modeTimer.start(); //m_cwIdTimer.stop(); createLockFile("NXDN"); break; case HOST_STATE_LOCKOUT: LogWarning(LOG_HOST, "Mode change, HOST_STATE_LOCKOUT"); if (m_network != nullptr) m_network->enable(false); if (m_state == STATE_DMR && m_duplex && m_modem->hasTX()) { m_modem->writeDMRStart(false); m_dmrTXTimer.stop(); } m_modem->setState(STATE_IDLE); m_state = HOST_STATE_LOCKOUT; m_modeTimer.stop(); //m_cwIdTimer.stop(); removeLockFile(); break; case HOST_STATE_ERROR: LogWarning(LOG_HOST, "Mode change, HOST_STATE_ERROR"); if (m_network != nullptr) m_network->enable(false); if (m_state == STATE_DMR && m_duplex && m_modem->hasTX()) { m_modem->writeDMRStart(false); m_dmrTXTimer.stop(); } m_state = HOST_STATE_ERROR; m_modeTimer.stop(); m_cwIdTimer.stop(); removeLockFile(); break; default: if (m_network != nullptr) m_network->enable(true); if (m_state == STATE_DMR && m_duplex && m_modem->hasTX()) { m_modem->writeDMRStart(false); m_dmrTXTimer.stop(); } m_modem->setState(STATE_IDLE); m_modem->clearDMRFrame1(); m_modem->clearDMRFrame2(); m_modem->clearP25Frame(); if (m_state == HOST_STATE_ERROR) { m_modem->sendCWId(m_cwCallsign); m_cwIdTimer.setTimeout(m_cwIdTime); m_cwIdTimer.start(); } removeLockFile(); m_modeTimer.stop(); if (m_state == HOST_STATE_QUIT) { ::LogInfoEx(LOG_HOST, "Host is shutting down"); if (m_modem != nullptr) { m_modem->close(); delete m_modem; } ::LogSetNetwork(nullptr); if (m_network != nullptr) { m_network->close(); delete m_network; } if (m_RESTAPI != nullptr) { m_RESTAPI->close(); delete m_RESTAPI; } if (m_tidLookup != nullptr) { m_tidLookup->stop(); delete m_tidLookup; } if (m_ridLookup != nullptr) { m_ridLookup->stop(); delete m_ridLookup; } } else { m_state = STATE_IDLE; } break; } } /// /// /// /// void Host::createLockFile(const char* mode) const { FILE* fp = ::fopen(g_lockFile.c_str(), "wt"); if (fp != nullptr) { ::fprintf(fp, "%s\n", mode); ::fclose(fp); } } /// /// /// void Host::removeLockFile() const { ::remove(g_lockFile.c_str()); }