diff --git a/.gitignore b/.gitignore index 8bf1885..b2f773d 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ Tests/dstargateway_tests DGWRemoteControl/dgwremotecontrol DGWVoiceTransmit/dgwvoicetransmit DGWTextTransmit/dgwtexttransmit +DGWTimeServer/dgwtimeserver diff --git a/.vscode/launch.json b/.vscode/launch.json index 5dced1b..55284ba 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -80,6 +80,30 @@ } ] }, + { + "name": "(gdb) dgwtimeserver", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/DGWTimeServer/dgwtimeserver", + "args": ["${workspaceFolder}/Sandbox/__timeserver_test.cfg"], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Activer l'impression en mode Pretty pour gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "Définir la version désassemblage sur Intel", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } + ] + }, { "name": "(gdb) dgwvoicetransmit", "type": "cppdbg", @@ -111,7 +135,7 @@ "program": "${workspaceFolder}/Tests/dstargateway_tests", "args": [ ], "stopAtEntry": false, - "cwd": "${workspaceFolder}", + "cwd": "${workspaceFolder}/Tests/", "environment": [], "externalConsole": false, "MIMode": "gdb", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6066b4b..995a9f1 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -13,10 +13,7 @@ "USE_GPSD=1", "all" ], - "group": { - "kind": "build", - "isDefault": true - }, + "group": "build", "problemMatcher": [] }, { @@ -58,6 +55,19 @@ "group": "build", "problemMatcher": [] }, + { + "label": "Build DGWTimeServer", + "type": "shell", + "command": "make", + "args": [ + "-j3", + "ENABLE_DEBUG=1", + "USE_GPSD=1", + "DGWTimeServer/dgwtimeserver" + ], + "group": "build", + "problemMatcher": [] + }, { "label": "Build DGWVoiceTransmit", "type": "shell", @@ -81,7 +91,10 @@ "ENABLE_DEBUG=1", "USE_GPSD=1" ], - "group": "build", + "group": { + "kind": "build", + "isDefault": true + }, "problemMatcher": [] } ] diff --git a/BaseCommon/LogFileTarget.cpp b/BaseCommon/LogFileTarget.cpp index d99d09e..081222d 100644 --- a/BaseCommon/LogFileTarget.cpp +++ b/BaseCommon/LogFileTarget.cpp @@ -21,19 +21,19 @@ #include #include #include +#include #include "LogFileTarget.h" -#define LOG_FILE_ROOT "dstargateway" - -CLogFileTarget::CLogFileTarget(LOG_SEVERITY logLevel, const std::string & dir, bool rotate) : +CLogFileTarget::CLogFileTarget(LOG_SEVERITY logLevel, const std::string & dir, const std::string& fileRoot, bool rotate) : CLogTarget(logLevel), m_dir(dir), +m_fileRoot(fileRoot), m_rotate(rotate), m_file(), m_day(0) { - + assert(!fileRoot.empty()); } CLogFileTarget::~CLogFileTarget() @@ -53,7 +53,7 @@ void CLogFileTarget::printLogIntFixed(const std::string& msg) std::string filename(m_dir); if(filename[filename.length() - 1U] != '/') filename.push_back('/'); - filename.append(LOG_FILE_ROOT).append(".log"); + filename.append(m_fileRoot).append(".log"); m_file.open(filename, std::ios::app); if(m_file.is_open()) { @@ -80,7 +80,7 @@ void CLogFileTarget::printLogIntRotate(const std::string& msg) if(filename[filename.length() - 1U] != '/') filename.push_back('/'); char buf[64]; std::strftime(buf, 42, "-%Y-%m-%d", now_tm); - filename.append(LOG_FILE_ROOT).append(buf).append(".log"); + filename.append(m_fileRoot).append(buf).append(".log"); m_file.open(filename, std::ios::app); if(!m_file.is_open()) { std::cerr << "FAILED TO OPEN LOG FILE :" << filename; diff --git a/BaseCommon/LogFileTarget.h b/BaseCommon/LogFileTarget.h index 8df8c8b..10dc77d 100644 --- a/BaseCommon/LogFileTarget.h +++ b/BaseCommon/LogFileTarget.h @@ -26,7 +26,7 @@ class CLogFileTarget : public CLogTarget { public: - CLogFileTarget(LOG_SEVERITY logLevel, const std::string& directory, bool rotate); + CLogFileTarget(LOG_SEVERITY logLevel, const std::string& directory, const std::string& fileRoot, bool rotate); ~CLogFileTarget(); protected: @@ -37,6 +37,7 @@ private: void printLogIntFixed(const std::string& msg); std::string buildFileName(); std::string m_dir; + std::string m_fileRoot; bool m_rotate; std::fstream m_file; int m_day; diff --git a/Common/AudioUnit.cpp b/Common/AudioUnit.cpp index 66684a0..0adeac4 100644 --- a/Common/AudioUnit.cpp +++ b/Common/AudioUnit.cpp @@ -30,9 +30,8 @@ #include "Utils.h" #include "Log.h" -unsigned char* CAudioUnit::m_ambe = NULL; -unsigned int CAudioUnit::m_ambeLength = 0U; -std::map CAudioUnit::m_index; + +CAMBEFileReader * CAudioUnit::m_ambeFilereader = nullptr; TEXT_LANG CAudioUnit::m_language = TL_ENGLISH_UK; @@ -46,6 +45,11 @@ void CAudioUnit::initialise() void CAudioUnit::setLanguage(const std::string & dir, TEXT_LANG language) { + if(m_ambeFilereader != nullptr) { + delete m_ambeFilereader; + m_ambeFilereader = nullptr; + } + m_language = language; std::string ambeFileName; @@ -94,32 +98,22 @@ void CAudioUnit::setLanguage(const std::string & dir, TEXT_LANG language) break; } - bool ret = readAMBE(dir, ambeFileName); - if (!ret) { - delete[] m_ambe; - m_ambe = NULL; - return; - } - - ret = readIndex(dir, indxFileName); - if (!ret) { - delete[] m_ambe; - m_ambe = NULL; + m_ambeFilereader = new CAMBEFileReader(dir + "/" + indxFileName, dir + "/" + ambeFileName); + bool ret = m_ambeFilereader->read(); + if(!ret) { + delete m_ambeFilereader; + m_ambeFilereader = nullptr; } } void CAudioUnit::finalise() { - for (std::map::iterator it = m_index.begin(); it != m_index.end(); ++it) - delete it->second; - - delete[] m_ambe; + delete m_ambeFilereader; } CAudioUnit::CAudioUnit(IRepeaterCallback* handler, const std::string& callsign) : m_handler(handler), m_callsign(callsign), -m_encoder(), m_status(AS_IDLE), m_linkStatus(LS_NONE), m_tempLinkStatus(LS_NONE), @@ -129,28 +123,24 @@ m_reflector(), m_tempReflector(), m_hasTemporary(false), m_timer(1000U, REPLY_TIME), -m_data(NULL), -m_in(0U), -m_out(0U), -m_seqNo(0U) //, +m_data(), +m_out(0U) //m_time() { assert(handler != NULL); - - m_data = new CAMBEData*[MAX_FRAMES]; - - for (unsigned int i = 0U; i < MAX_FRAMES; i++) - m_data[i] = NULL; } CAudioUnit::~CAudioUnit() { - delete[] m_data; + for (auto item : m_data) { + delete item; + } + m_data.clear(); } void CAudioUnit::sendStatus() { - if (m_ambe == NULL) + if (m_ambeFilereader == nullptr) return; if (m_status != AS_IDLE) @@ -190,7 +180,6 @@ void CAudioUnit::clock(unsigned int ms) m_timer.stop(); m_out = 0U; - m_seqNo = 0U; m_status = AS_TRANSMIT; m_time = std::chrono::high_resolution_clock::now(); @@ -199,29 +188,20 @@ void CAudioUnit::clock(unsigned int ms) } if (m_status == AS_TRANSMIT) { - std::chrono::high_resolution_clock::time_point hrctp = std::chrono::high_resolution_clock::now(); - auto elapse = std::chrono::duration_cast(hrctp - m_time); - unsigned int needed = elapse.count() / DSTAR_FRAME_TIME_MS; + unsigned int needed = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - m_time).count(); + needed /= DSTAR_FRAME_TIME_MS; - while (m_out < needed) { + while (m_out < needed && m_out < m_data.size()) { CAMBEData* data = m_data[m_out]; - m_data[m_out] = NULL; m_out++; - - if (m_in == m_out) - data->setEnd(true); - + // CLog::logTrace("m_out %u, needed %u, m_data %u", m_out, needed, m_data.size()); m_handler->process(*data, DIR_INCOMING, AS_INFO); + } - delete data; - - if (m_in == m_out) { - m_in = 0U; - m_out = 0U; - m_status = AS_IDLE; - m_timer.stop(); - return; - } + if (m_out >= m_data.size()) { + m_out = 0U; + m_status = AS_IDLE; + m_timer.stop(); } return; @@ -230,63 +210,14 @@ void CAudioUnit::clock(unsigned int ms) void CAudioUnit::cancel() { - for (unsigned int i = 0U; i < MAX_FRAMES; i++) { - if (m_data[i] != NULL) { - delete m_data[i]; - m_data[i] = NULL; - } - } - + CLog::logTrace("Audio Unit Cancel"); m_status = AS_IDLE; m_out = 0U; - m_in = 0U; m_timer.stop(); } -bool CAudioUnit::lookup(unsigned int id, const std::string &name) -{ - CIndexRecord* info = m_index[name]; - if (info == NULL) { - // CLog::logError("Cannot find the AMBE index for *%s*", name.c_str()); - return false; - } - - unsigned int start = info->getStart(); - unsigned int length = info->getLength(); - - for (unsigned int i = 0U; i < length; i++) { - unsigned char* dataIn = m_ambe + (start + i) * VOICE_FRAME_LENGTH_BYTES; - - CAMBEData* dataOut = new CAMBEData; - dataOut->setSeq(m_seqNo); - dataOut->setId(id); - - unsigned char buffer[DV_FRAME_LENGTH_BYTES]; - memcpy(buffer + 0U, dataIn, VOICE_FRAME_LENGTH_BYTES); - - // Insert sync bytes when the sequence number is zero, slow data otherwise - if (m_seqNo == 0U) { - memcpy(buffer + VOICE_FRAME_LENGTH_BYTES, DATA_SYNC_BYTES, DATA_FRAME_LENGTH_BYTES); - m_encoder.sync(); - } else { - m_encoder.getTextData(buffer + VOICE_FRAME_LENGTH_BYTES); - } - - dataOut->setData(buffer, DV_FRAME_LENGTH_BYTES); - - m_seqNo++; - if (m_seqNo == 21) - m_seqNo = 0; - - m_data[m_in] = dataOut; - m_in++; - } - - return true; -} - -void CAudioUnit::spellReflector(unsigned int id, const std::string &reflector) +void CAudioUnit::spellReflector(const std::string &reflector) { unsigned int length = reflector.size(); @@ -294,11 +225,10 @@ void CAudioUnit::spellReflector(unsigned int id, const std::string &reflector) std::string c = reflector.substr(i, 1); if (c.compare(" ")) - lookup(id, c); + m_ambeFilereader->lookup(c, m_data); } char c = reflector.at(length - 1); - if (c == ' ') return; @@ -306,197 +236,113 @@ void CAudioUnit::spellReflector(unsigned int id, const std::string &reflector) cstr.push_back(c); if (m_linkStatus == LS_LINKING_DCS || m_linkStatus == LS_LINKED_DCS || m_linkStatus == LS_LINKING_CCS || m_linkStatus == LS_LINKED_CCS) { - lookup(id, cstr); + m_ambeFilereader->lookup(cstr, m_data); return; } switch (c) { case 'A': - lookup(id, "alpha"); + m_ambeFilereader->lookup("alpha", m_data); break; case 'B': - lookup(id, "bravo"); + m_ambeFilereader->lookup("bravo", m_data); break; case 'C': - lookup(id, "charlie"); + m_ambeFilereader->lookup("charlie", m_data); break; case 'D': - lookup(id, "delta"); + m_ambeFilereader->lookup("delta", m_data); break; default: - lookup(id, cstr); + m_ambeFilereader->lookup(cstr, m_data); break; } } -bool CAudioUnit::readAMBE(const std::string& dir, const std::string& name) +void CAudioUnit::sendStatus(LINK_STATUS status, const std::string& reflector, const std::string &text) { - std::string fileName = dir + "/" + name; - struct stat sbuf; - - if (stat(fileName.c_str(), &sbuf)) { - CLog::logInfo("File %s not readable\n", fileName.c_str()); - fileName.append("/data/"); - fileName += name; - if (stat(fileName.c_str(), &sbuf)) { - CLog::logInfo("File %s not readable\n", fileName.c_str()); - return false; - } - } - unsigned int fsize = sbuf.st_size; - - FILE *file = fopen(fileName.c_str(), "rb"); - if (NULL == file) { - CLog::logInfo("Cannot open %s for reading\n", fileName.c_str()); - return false; - } - - CLog::logInfo("Reading %s\n", fileName.c_str()); - - unsigned char buffer[VOICE_FRAME_LENGTH_BYTES]; - - size_t n = fread(buffer, sizeof(unsigned char), 4, file); - if (n != 4) { - CLog::logError("Unable to read the header from %s\n", fileName.c_str()); - fclose(file); - return false; - } + CLog::logTrace("Audio Unit sendStatus"); - if (memcmp(buffer, "AMBE", 4)) { - CLog::logError("Invalid header from %s\n", fileName.c_str()); - fclose(file); - return false; + // do some clean up, delete old message + for (auto item : m_data) { + delete item; } + m_data.clear(); - // Length of the file minus the header - unsigned int length = fsize - 4U; - - // Hold the file data plus silence at the end - m_ambe = new unsigned char[length + SILENCE_LENGTH * VOICE_FRAME_LENGTH_BYTES]; - m_ambeLength = length / VOICE_FRAME_LENGTH_BYTES; - - // Add silence to the beginning of the buffer - unsigned char* p = m_ambe; - for (unsigned int i = 0U; i < SILENCE_LENGTH; i++, p += VOICE_FRAME_LENGTH_BYTES) - memcpy(p, NULL_AMBE_DATA_BYTES, VOICE_FRAME_LENGTH_BYTES); - - n = fread(p, 1, length, file); - if (n != length) { - CLog::logError("Unable to read the AMBE data from %s\n", fileName.c_str()); - fclose(file); - delete[] m_ambe; - m_ambe = NULL; - return false; - } + // Create the message + m_ambeFilereader->lookup(" ", m_data); + m_ambeFilereader->lookup(" ", m_data); + m_ambeFilereader->lookup(" ", m_data); + m_ambeFilereader->lookup(" ", m_data); - fclose(file); + bool found; - return true; -} - -bool CAudioUnit::readIndex(const std::string& dir, const std::string& name) -{ - std::string fileName = dir + "/" + name; - struct stat sbuf; - - if (stat(fileName.c_str(), &sbuf)) { - CLog::logInfo("File %s not readable\n", fileName.c_str()); - fileName.append("/data/"); - fileName += name; - if (stat(fileName.c_str(), &sbuf)) { - CLog::logInfo("File %s not readable\n", fileName.c_str()); - return false; - } - } - - FILE *file = fopen(fileName.c_str(), "r"); - if (NULL == file) { - CLog::logInfo("Cannot open %s for reading\n", fileName.c_str()); - return false; + switch (status) { + case LS_NONE: + m_ambeFilereader->lookup("notlinked", m_data); + break; + case LS_LINKED_CCS: + case LS_LINKED_DCS: + case LS_LINKED_DPLUS: + case LS_LINKED_DEXTRA: + case LS_LINKED_LOOPBACK: + found = m_ambeFilereader->lookup("linkedto", m_data); + if (!found) { + m_ambeFilereader->lookup("linked", m_data); + m_ambeFilereader->lookup("2", m_data); + } + spellReflector(reflector); + break; + default: + found = m_ambeFilereader->lookup("linkingto", m_data); + if (!found) { + m_ambeFilereader->lookup("linking", m_data); + m_ambeFilereader->lookup("2", m_data); + } + spellReflector(reflector); + break; } - // Add a silence entry at the beginning - m_index[" "] = new CIndexRecord(" ", 0, SILENCE_LENGTH); + m_ambeFilereader->lookup(" ", m_data); + m_ambeFilereader->lookup(" ", m_data); + m_ambeFilereader->lookup(" ", m_data); + m_ambeFilereader->lookup(" ", m_data); - CLog::logInfo("Reading %s\n", fileName.c_str()); + unsigned int id = CHeaderData::createId(); + // RPT1 and RPT2 will be filled in later + CHeaderData header; + header.setMyCall1(m_callsign); + header.setMyCall2("INFO"); + header.setYourCall("CQCQCQ "); + header.setId(id); - char line[128]; - while (fgets(line, 128, file)) { + CSlowDataEncoder slowDataEncoder; + slowDataEncoder.setTextData(text); + unsigned int seqNo = 0U; - if (strlen(line) && '#'!=line[0]) { - const std::string space(" \t\r\n"); - std::string name(strtok(line, space.c_str())); - std::string strt(strtok(NULL, space.c_str())); - std::string leng(strtok(NULL, space.c_str())); + // add the slow data, id, seq num etc ... + for(unsigned int i = 0U; i < m_data.size(); i++) { + m_data[i]->setId(id); + m_data[i]->setSeq(seqNo); - if (name.size() && strt.size() && leng.size()) { - unsigned long start = std::stoul(strt); - unsigned long length = std::stoul(leng); + unsigned char buffer[DV_FRAME_LENGTH_BYTES]; + m_data[i]->getData(buffer, DV_FRAME_LENGTH_BYTES); - if (start >= m_ambeLength || (start + length) >= m_ambeLength) - CLog::logInfo("The start or end for *%s* is out of range, start: %lu, end: %lu\n", name.c_str(), start, start + length); - else - m_index[name] = new CIndexRecord(name, start + SILENCE_LENGTH, length); - } + // Insert sync bytes when the sequence number is zero, slow data otherwise + if (seqNo == 0U) { + ::memcpy(buffer + VOICE_FRAME_LENGTH_BYTES, DATA_SYNC_BYTES, DATA_FRAME_LENGTH_BYTES); + slowDataEncoder.sync(); + } else { + slowDataEncoder.getInterleavedData(buffer + VOICE_FRAME_LENGTH_BYTES); } - } - - fclose(file); - - return true; -} -void CAudioUnit::sendStatus(LINK_STATUS status, const std::string& reflector, const std::string &text) -{ - m_encoder.setTextData(text); - - // Create the message - unsigned int id = CHeaderData::createId(); - - lookup(id, " "); - lookup(id, " "); - lookup(id, " "); - lookup(id, " "); - - bool found; - - switch (status) { - case LS_NONE: - lookup(id, "notlinked"); - break; - case LS_LINKED_CCS: - case LS_LINKED_DCS: - case LS_LINKED_DPLUS: - case LS_LINKED_DEXTRA: - case LS_LINKED_LOOPBACK: - found = lookup(id, "linkedto"); - if (!found) { - lookup(id, "linked"); - lookup(id, "2"); - } - spellReflector(id, reflector); - break; - default: - found = lookup(id, "linkingto"); - if (!found) { - lookup(id, "linking"); - lookup(id, "2"); - } - spellReflector(id, reflector); - break; - } + m_data[i]->setData(buffer, DV_FRAME_LENGTH_BYTES); - lookup(id, " "); - lookup(id, " "); - lookup(id, " "); - lookup(id, " "); + seqNo++; + if(seqNo >= 21U) seqNo = 0U; + } - // RPT1 and RPT2 will be filled in later - CHeaderData header; - header.setMyCall1(m_callsign); - header.setMyCall2("INFO"); - header.setYourCall("CQCQCQ "); - header.setId(id); + m_data[m_data.size() - 1]->setEnd(true); - m_handler->process(header, DIR_INCOMING, AS_INFO); + m_handler->process(header, DIR_INCOMING, AS_INFO); } diff --git a/Common/AudioUnit.h b/Common/AudioUnit.h index 6cdd862..7b27ba8 100644 --- a/Common/AudioUnit.h +++ b/Common/AudioUnit.h @@ -23,42 +23,14 @@ #include #include #include +#include #include "RepeaterCallback.h" #include "SlowDataEncoder.h" #include "AMBEData.h" #include "Timer.h" #include "Defs.h" - -class CIndexRecord { -public: - CIndexRecord(const std::string& name, unsigned int start, unsigned int length) : - m_name(name), - m_start(start), - m_length(length) - { - } - - std::string getName() const - { - return m_name; - } - - unsigned int getStart() const - { - return m_start; - } - - unsigned int getLength() const - { - return m_length; - } - -private: - std::string m_name; - unsigned int m_start; - unsigned int m_length; -}; +#include "AMBEFileReader.h" enum AUDIO_STATUS { AS_IDLE, @@ -87,14 +59,10 @@ public: static void finalise(); private: - static std::map m_index; - static unsigned char* m_ambe; - static unsigned int m_ambeLength; static TEXT_LANG m_language; IRepeaterCallback* m_handler; std::string m_callsign; - CSlowDataEncoder m_encoder; AUDIO_STATUS m_status; LINK_STATUS m_linkStatus; LINK_STATUS m_tempLinkStatus; @@ -104,17 +72,12 @@ private: std::string m_tempReflector; bool m_hasTemporary; CTimer m_timer; - CAMBEData** m_data; - unsigned int m_in; + std::vector m_data; + static CAMBEFileReader* m_ambeFilereader; unsigned int m_out; - unsigned int m_seqNo; std::chrono::high_resolution_clock::time_point m_time; - bool lookup(unsigned int id, const std::string& name); - void spellReflector(unsigned int id, const std::string& reflector); + void spellReflector(const std::string& reflector); void sendStatus(LINK_STATUS status, const std::string& reflector, const std::string& text); - - static bool readAMBE(const std::string& dir, const std::string& name); - static bool readIndex(const std::string& dir, const std::string& name); }; diff --git a/DGWTimeServer/DGWTimeServerApp.cpp b/DGWTimeServer/DGWTimeServerApp.cpp new file mode 100644 index 0000000..847b327 --- /dev/null +++ b/DGWTimeServer/DGWTimeServerApp.cpp @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2014 by Jonathan Naylor G4KLX + * Copyright (C) 2022 by Geoffrey Merck F4FXL / KC3FRA + * + * 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 +#include +#include +#include +#include +#ifdef DEBUG_DSTARGW +#include +#endif + +#include "DGWTimeServerApp.h" +#include "Version.h" +#include "Log.h" +#include "Daemon.h" +#include "LogConsoleTarget.h" +#include "LogFileTarget.h" + +CDGWTimeServerApp * CDGWTimeServerApp::g_app = nullptr; +const std::string BANNER_1 = CStringUtils::string_format("%s v%s Copyright (C) %s\n", APPLICATION_NAME.c_str(), LONG_VERSION.c_str(), VENDOR_NAME.c_str()); +const std::string BANNER_2 = "DGWTimeServer comes with ABSOLUTELY NO WARRANTY; see the LICENSE for details.\n"; +const std::string BANNER_3 = "This is free software, and you are welcome to distribute it under certain conditions that are discussed in the LICENSE file.\n\n"; + +int main(int argc, char * argv[]) +{ + std::set_terminate(CDGWTimeServerApp::terminateHandler); + + signal(SIGSEGV, CDGWTimeServerApp::sigHandlerFatal); + signal(SIGILL, CDGWTimeServerApp::sigHandlerFatal); + signal(SIGFPE, CDGWTimeServerApp::sigHandlerFatal); + signal(SIGABRT, CDGWTimeServerApp::sigHandlerFatal); + signal(SIGTERM, CDGWTimeServerApp::sigHandler); + signal(SIGINT, CDGWTimeServerApp::sigHandler); + + if (2 != argc) { + printf("usage: %s path_to_config_file\n", argv[0]); + printf(" %s --version\n", argv[0]); + return 1; + } + + std::cout << std::endl << BANNER_1 << BANNER_2 << BANNER_3; + if(argv[1][0] == '-') { + return 0; + } + + // Load config + std::string configfile(argv[1]); + CTimeServerConfig config(configfile); + if(!config.load()) + return 1; + + // Do daemon stuff + TDaemon daemon; + config.getDameon(daemon); + if (daemon.daemon) { + CLog::logInfo("Configured as a daemon, detaching ..."); + auto res = CDaemon::daemonise(daemon.pidFile, daemon.user); + + switch (res) + { + case DR_PARENT: + return 0; + case DR_CHILD: + break; + case DR_PIDFILE_FAILED: + case DR_FAILURE: + default: + CLog::logFatal("Failed to run as daemon"); + CLog::finalise(); + return 1; + } + } + + // Setup Log + TLog logConf; + config.getLog(logConf); + CLog::finalise(); + if(logConf.displayLevel != LOG_NONE && !daemon.daemon) CLog::addTarget(new CLogConsoleTarget(logConf.displayLevel)); + if(logConf.fileLevel != LOG_NONE) CLog::addTarget(new CLogFileTarget(logConf.fileLevel, logConf.logDir, logConf.fileRoot, logConf.fileRotate)); + + // Start the app + CDGWTimeServerApp app(&config); + if(app.init()) { + app.run(); + return 0; + } + + return 1; +} + +CDGWTimeServerApp::CDGWTimeServerApp(const CTimeServerConfig * config) : +m_config(config) +{ + g_app = this; + assert(config != nullptr); +} + +CDGWTimeServerApp::~CDGWTimeServerApp() +{ + delete m_thread; +} + +bool CDGWTimeServerApp::init() +{ + return createThread(); +} + +void CDGWTimeServerApp::run() +{ + m_thread->Run(); + m_thread->Wait(); +} + +bool CDGWTimeServerApp::createThread() +{ + m_thread = new CTimeServerThread(); + + TTimeServer timeserver; + m_config->getTimeServer(timeserver); + + std::vector rptrs = { "", "", "", "" }; + TRepeater repeater; + for(unsigned int i = 0u; i < m_config->getRepeaterCount(); i++) { + m_config->getRepeater(i, repeater); + rptrs[i].assign(repeater.band); + } + + TPaths paths; + m_config->getPaths(paths); + + m_thread = new CTimeServerThread(); + bool ret = m_thread->setGateway(timeserver.callsign, rptrs[0], rptrs[1], rptrs[2], rptrs[3], timeserver.address, paths.data); + if(ret) { + m_thread->setAnnouncements(timeserver.language, timeserver.format, timeserver.interval); + } + + return ret; +} + +void CDGWTimeServerApp::sigHandler(int sig) +{ + CLog::logInfo("Caught signal : %s, shutting down time server", strsignal(sig)); + + if(g_app != nullptr && g_app->m_thread != nullptr) { + g_app->m_thread->kill(); + } +} + +void CDGWTimeServerApp::sigHandlerFatal(int sig) +{ + CLog::logFatal("Caught signal : %s", strsignal(sig)); + fprintf(stderr, "Caught signal : %s\n", strsignal(sig)); +#ifdef DEBUG_DSTARGW + std::stringstream stackTrace; + stackTrace << boost::stacktrace::stacktrace(); + CLog::logFatal("Stack Trace : \n%s", stackTrace.str().c_str()); + fprintf(stderr, "Stack Trace : \n%s\n", stackTrace.str().c_str()); +#endif + exit(3); +} + +void CDGWTimeServerApp::terminateHandler() +{ +#ifdef DEBUG_DSTARGW + std::stringstream stackTrace; + stackTrace << boost::stacktrace::stacktrace(); +#endif + + std::exception_ptr eptr; + eptr = std::current_exception(); + + try { + if (eptr != nullptr) { + std::rethrow_exception(eptr); + } + else { + CLog::logFatal("Unhandled unknown exception occured"); + fprintf(stderr, "Unknown ex\n"); + } + } catch(const std::exception& e) { + CLog::logFatal("Unhandled exception occured %s", e.what()); + fprintf(stderr, "Unhandled ex %s\n", e.what()); + } + +#ifdef DEBUG_DSTARGW + CLog::logFatal("Stack Trace : \n%s", stackTrace.str().c_str()); +#endif + exit(2); +} \ No newline at end of file diff --git a/DGWTimeServer/DGWTimeServerApp.h b/DGWTimeServer/DGWTimeServerApp.h new file mode 100644 index 0000000..a25cdaf --- /dev/null +++ b/DGWTimeServer/DGWTimeServerApp.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2014 by Jonathan Naylor G4KLX + * Copyright (C) 2022 by Geoffrey Merck F4FXL / KC3FRA + * + * 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. + */ + +#pragma once + +#include "TimeServerDefs.h" +#include "TimeServerConfig.h" +#include "TimeServerThread.h" + +class CDGWTimeServerApp +{ +public: + CDGWTimeServerApp(const CTimeServerConfig * config); + ~CDGWTimeServerApp(); + + bool init(); + void run(); + + static void sigHandler(int sig); + static void sigHandlerFatal(int sig); + static void terminateHandler(); + +private: + bool createThread(); + + static CDGWTimeServerApp * g_app; + + const CTimeServerConfig * m_config; + CTimeServerThread * m_thread; +}; diff --git a/DGWTimeServer/Makefile b/DGWTimeServer/Makefile new file mode 100644 index 0000000..87b80ab --- /dev/null +++ b/DGWTimeServer/Makefile @@ -0,0 +1,36 @@ +SRCS = $(wildcard *.cpp) +OBJS = $(SRCS:.cpp=.o) +DEPS = $(SRCS:.cpp=.d) + +dgwtimeserver: ../VersionInfo/GitVersion.h $(OBJS) ../DStarBase/DStarBase.a ../BaseCommon/BaseCommon.a + $(CC) $(CPPFLAGS) -o dgwtimeserver $(OBJS) ../DStarBase/DStarBase.a ../BaseCommon/BaseCommon.a $(LDFLAGS) + +%.o : %.cpp + $(CC) -I../BaseCommon -I../DStarBase -I../VersionInfo $(CPPFLAGS) -MMD -MD -c $< -o $@ + +.PHONY clean: +clean: + $(RM) *.o *.d dgwtimeserver + +.PHONY install: +install: dgwtimeserver +# copy executable + @cp -f dgwtimeserver $(BIN_DIR) + +# copy and adjust config + @cp -fn example.cfg $(CFG_DIR)/dgwtimeserver.cfg + @sed -i "s|path=/var/log/dstargateway/|path=$(LOG_DIR)|g" $(CFG_DIR)/dgwtimeserver.cfg + @sed -i "s|data=/usr/local/share/dstargateway.d/|data=$(DATA_DIR)|g" $(CFG_DIR)/dgwtimeserver.cfg + +# SystemD service install + @cp -f ../debian/dgwtimeserver.service /lib/systemd/system/ + @sed -i "s|%CFG_DIR%|$(CFG_DIR)|g" /lib/systemd/system/dgwtimeserver.service + systemctl enable dgwtimeserver.service + @systemctl daemon-reload + @echo "\n" + @echo "DGWTimeserver Install complete, edit $(CFG_DIR)dgwtimeserver.cfg and start the daemon with 'systemctl start dgwtimeserver.service'" + @echo "\n" + +../BaseCommon/BaseCommon.a: +../DStarBase/DStarBase.a: +../VersionInfo/GitVersion.h: diff --git a/DGWTimeServer/README.md b/DGWTimeServer/README.md new file mode 100644 index 0000000..1a48592 --- /dev/null +++ b/DGWTimeServer/README.md @@ -0,0 +1,11 @@ +Time Server for DStarGateway. Installs as a systemd service. + +If you did not change build settings, the configuration can be found in +``` +/usr/local/etc/dgwtimeserver.cfg +``` +After configuring enable and start the dgwtimeserver systemd service using : +``` +sudo systemctl enable dgwtimeserver +sudo systemctl start dgwtimeserver +``` \ No newline at end of file diff --git a/DGWTimeServer/TimeServerConfig.cpp b/DGWTimeServer/TimeServerConfig.cpp new file mode 100644 index 0000000..0a2f170 --- /dev/null +++ b/DGWTimeServer/TimeServerConfig.cpp @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2022 by Geoffrey Merck F4FXL / KC3FRA + * + * 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 +#include + +#include "TimeServerConfig.h" +#include "StringUtils.h" + +CTimeServerConfig::CTimeServerConfig(const std::string &pathname) : +m_fileName(pathname), +m_repeaters() +{ + +} + +CTimeServerConfig::~CTimeServerConfig() +{ + for(auto repeater : m_repeaters) { + delete repeater; + } + m_repeaters.clear(); +} + +bool CTimeServerConfig::load() +{ + bool ret = false; + CLog::logInfo("Loading Configuration from %s", m_fileName.c_str()); + CConfig cfg(m_fileName); + + ret = open(cfg); + if(ret) { + ret = loadTimeServer(cfg) && ret; + ret = loadRepeaters(cfg) && ret; + ret = loadDaemon(cfg) && ret; + ret = loadPaths(cfg) && ret; + ret = loadLog(cfg) && ret; + } + + return ret; +} + +bool CTimeServerConfig::open(CConfig & cfg) +{ + try { + return cfg.load(); + } + catch(...) { + CLog::logError("Can't read %s\n", m_fileName.c_str()); + return false; + } + return true; +} + +bool CTimeServerConfig::loadRepeaters(const CConfig & cfg) +{ + m_repeaters.clear(); + + for(unsigned int i = 0; i < 4; i++) { + std::string section = CStringUtils::string_format("repeater_%d", i+ 1); + bool repeaterEnabled; + + bool ret = cfg.getValue(section, "enabled", repeaterEnabled, false); + if(!ret || !repeaterEnabled) + continue; + + TRepeater * repeater = new TRepeater; + ret = cfg.getValue(section, "band", repeater->band, 1, 2, "") && ret; + if(!ret) { + delete repeater; + continue; + } + boost::to_upper(repeater->band); + + bool alreadyConfigured = std::any_of(m_repeaters.begin(), m_repeaters.end(), [repeater](TRepeater * r) { return r->band == repeater->band;}); + if(alreadyConfigured) { + CLog::logWarning("%s-%s repeater already configured, ignoring", m_timeServer.callsign.c_str(), repeater->band.c_str()); + delete repeater; + continue; + } + + m_repeaters.push_back(repeater); + } + + return m_repeaters.size() > 0U; +} + +bool CTimeServerConfig::loadTimeServer(const CConfig & cfg) +{ + bool ret = cfg.getValue("timeserver", "callsign", m_timeServer.callsign, 3, 8, ""); + boost::to_upper(m_timeServer.callsign); + ret = cfg.getValue("timeserver", "address", m_timeServer.address, 0, 1024, "127.0.0.1") && ret; + + std::string format; + ret = cfg.getValue("timeserver", "format", format, "voice", {"voice", "text" }) && ret; + if(format == "voice") m_timeServer.format = FORMAT_VOICE_TIME; + else if(format == "text") m_timeServer.format = FORMAT_TEXT_TIME; + + std::string lang; + ret = cfg.getValue("timeserver", "language", lang, "english_uk_1", {"english_uk_1", "english_uk_2", "english_us_1", "english_us_2", "deutsch_1", "deutsch_2", "francais", "nederlands", "svenska", "espanol", "norsk", "portugues"}) && ret;; + if (lang == "english_uk_1") m_timeServer.language = LANG_ENGLISH_UK_1; + else if(lang == "english_uk_2") m_timeServer.language = LANG_ENGLISH_UK_2; + else if(lang == "english_us_1") m_timeServer.language = LANG_ENGLISH_US_1; + else if(lang == "english_us_2") m_timeServer.language = LANG_ENGLISH_US_2; + else if(lang == "deutsch_1" ) m_timeServer.language = LANG_DEUTSCH_1; + else if(lang == "detusch_2" ) m_timeServer.language = LANG_DEUTSCH_2; + else if(lang == "francais" ) m_timeServer.language = LANG_FRANCAIS; + else if(lang == "nederlands" ) m_timeServer.language = LANG_NEDERLANDS; + else if(lang == "svenska" ) m_timeServer.language = LANG_SVENSKA; + else if(lang == "espanol" ) m_timeServer.language = LANG_ESPANOL; + else if(lang == "norsk" ) m_timeServer.language = LANG_NORSK; + else if(lang == "portugues" ) m_timeServer.language = LANG_PORTUGUES; + + std::string interval; + ret = cfg.getValue("timeserver", "interval", interval, "30", {"15", "30", "60"}) && ret; + if(interval == "15") m_timeServer.interval = INTERVAL_15MINS; + else if(interval == "30") m_timeServer.interval = INTERVAL_30MINS; + else if(interval == "60") m_timeServer.interval = INTERVAL_60MINS; + + return ret; +} + +bool CTimeServerConfig::loadDaemon(const CConfig & cfg) +{ + bool ret = cfg.getValue("daemon", "daemon", m_daemon.daemon, false); + ret = cfg.getValue("daemon", "pidfile", m_daemon.pidFile, 0, 1024, "") && ret; + ret = cfg.getValue("daemon", "user", m_daemon.user, 0, 1024, "") && ret; + return ret; +} + +bool CTimeServerConfig::loadPaths(const CConfig & cfg) +{ + bool ret = cfg.getValue("paths", "data", m_paths.data, 1, 1024, ""); + return ret; +} + +bool CTimeServerConfig::loadLog(const CConfig & cfg) +{ + bool ret = cfg.getValue("log", "path", m_log.logDir, 0, 2048, "/var/log/dstargateway/"); + if(ret && m_log.logDir[m_log.logDir.length() - 1] != '/') { + m_log.logDir.push_back('/'); + } + + ret = cfg.getValue("log", "fileRoot", m_log.fileRoot, 0, 64, "dgwtimeserver") && ret; + ret = cfg.getValue("log", "fileRotate", m_log.fileRotate, true) && ret; + + std::string levelStr; + ret = cfg.getValue("log", "fileLevel", levelStr, "info", {"trace", "debug", "info", "warning", "error", "fatal", "none"}) && ret; + if(ret) { + if(levelStr == "trace") m_log.fileLevel = LOG_TRACE; + else if(levelStr == "debug") m_log.fileLevel = LOG_DEBUG; + else if(levelStr == "info") m_log.fileLevel = LOG_INFO; + else if(levelStr == "warning") m_log.fileLevel = LOG_WARNING; + else if(levelStr == "error") m_log.fileLevel = LOG_ERROR; + else if(levelStr == "fatal") m_log.fileLevel = LOG_FATAL; + else if(levelStr == "none") m_log.fileLevel = LOG_NONE; + } + + ret = cfg.getValue("log", "displayLevel", levelStr, "info", {"trace", "debug", "info", "warning", "error", "fatal", "none"}) && ret; + if(ret) { + if(levelStr == "trace") m_log.displayLevel = LOG_TRACE; + else if(levelStr == "debug") m_log.displayLevel = LOG_DEBUG; + else if(levelStr == "info") m_log.displayLevel = LOG_INFO; + else if(levelStr == "warning") m_log.displayLevel = LOG_WARNING; + else if(levelStr == "error") m_log.displayLevel = LOG_ERROR; + else if(levelStr == "fatal") m_log.displayLevel = LOG_FATAL; + else if(levelStr == "none") m_log.displayLevel = LOG_NONE; + } + + return ret; +} + +void CTimeServerConfig::getTimeServer(TTimeServer& timeserver) const +{ + timeserver = m_timeServer; +} + +void CTimeServerConfig::getDameon(TDaemon& daemon) const +{ + daemon = m_daemon; +} + +unsigned int CTimeServerConfig::getRepeaterCount() const +{ + return m_repeaters.size(); +} + +void CTimeServerConfig::getRepeater(unsigned int idx, TRepeater& repeater) const +{ + repeater = *(m_repeaters[idx]); +} + +void CTimeServerConfig::getPaths(TPaths& paths) const +{ + paths = m_paths; +} + +void CTimeServerConfig::getLog(TLog& log) const +{ + log = m_log; +} diff --git a/DGWTimeServer/TimeServerConfig.h b/DGWTimeServer/TimeServerConfig.h new file mode 100644 index 0000000..7eeaacf --- /dev/null +++ b/DGWTimeServer/TimeServerConfig.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2022 by Geoffrey Merck F4FXL / KC3FRA + * + * 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. + */ + +#pragma once + +#include +#include + +#include "Config.h" +#include "TimeServerDefs.h" +#include "Log.h" + +typedef struct { + std::string callsign; + std::string address; + FORMAT format; + LANGUAGE language; + INTERVAL interval; +} TTimeServer; + +typedef struct { + std::string band; +} TRepeater; + +typedef struct { + bool daemon; + std::string pidFile; + std::string user; +} TDaemon; + +typedef struct { + std::string data; +} TPaths; + +typedef struct { + std::string logDir; + LOG_SEVERITY displayLevel; + LOG_SEVERITY fileLevel; + std::string fileRoot; + bool fileRotate; +} TLog; + +class CTimeServerConfig +{ +public: + CTimeServerConfig(const std::string &pathname); + ~CTimeServerConfig(); + + bool load(); + void getTimeServer(TTimeServer& timeserver) const; + void getDameon(TDaemon& daemon) const; + unsigned int getRepeaterCount() const; + void getRepeater(unsigned int idx, TRepeater& repeater) const; + void getPaths(TPaths& paths) const; + void getLog(TLog& log) const; + +private: + bool open(CConfig & cfg); + bool loadRepeaters(const CConfig & cfg); + bool loadTimeServer(const CConfig & cfg); + bool loadDaemon(const CConfig & cfg); + bool loadPaths(const CConfig & cfg); + bool loadLog(const CConfig & cfg); + + std::string m_fileName; + std::vector m_repeaters; + TTimeServer m_timeServer; + TDaemon m_daemon; + TPaths m_paths; + TLog m_log; +}; diff --git a/DGWTimeServer/TimeServerDefs.h b/DGWTimeServer/TimeServerDefs.h new file mode 100644 index 0000000..b6910c2 --- /dev/null +++ b/DGWTimeServer/TimeServerDefs.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2012,2013,2014 by Jonathan Naylor G4KLX + * Copyright (C) 2022 by Geoffrey Merck F4FXL / KC3FRA + * + * 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. + */ + +#pragma once + +const std::string APPLICATION_NAME("DStarGateway Time Server"); + +enum LANGUAGE { + LANG_ENGLISH_UK_1, + LANG_ENGLISH_UK_2, + LANG_ENGLISH_US_1, + LANG_ENGLISH_US_2, + LANG_DEUTSCH_1, + LANG_DEUTSCH_2, + LANG_FRANCAIS, + LANG_NEDERLANDS, + LANG_SVENSKA, + LANG_ESPANOL, + LANG_NORSK, + LANG_PORTUGUES +}; + +enum INTERVAL { + INTERVAL_15MINS, + INTERVAL_30MINS, + INTERVAL_60MINS +}; + +enum FORMAT { + FORMAT_VOICE_TIME, + FORMAT_TEXT_TIME +}; diff --git a/DGWTimeServer/TimeServerThread.cpp b/DGWTimeServer/TimeServerThread.cpp new file mode 100644 index 0000000..e3f90ff --- /dev/null +++ b/DGWTimeServer/TimeServerThread.cpp @@ -0,0 +1,1253 @@ +/* + * Copyright (C) 2012,2013 by Jonathan Naylor G4KLX + * Copyright (C) 2022 by Geoffrey Merck F4FXL / KC3FRA + * + * 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 +#include +#include +#include +#include +#include +#include + +#include "TimeServerThread.h" +#include "DStarDefines.h" +#include "Utils.h" +#include "NetUtils.h" +#include "StringUtils.h" +#include "Log.h" + +const unsigned int MAX_FRAMES = 60U * DSTAR_FRAMES_PER_SEC; + +const unsigned int SILENCE_LENGTH = 10U; + +CTimeServerThread::CTimeServerThread() : +CThread("Time Server"), +m_callsign(), +m_repeaters(), +m_callsignG(), +m_address(), +m_addressStr(), +m_language(LANG_ENGLISH_UK_1), +m_format(FORMAT_VOICE_TIME), +m_interval(INTERVAL_15MINS), +m_data(), +m_killed(false), +m_dataPath(""), +m_ambeFileReader(nullptr) +{ + CHeaderData::initialise(); + m_address.s_addr = INADDR_NONE; +} + +CTimeServerThread::~CTimeServerThread() +{ + for(auto d : m_data) + delete d; + + m_data.clear(); + + delete[] m_ambeFileReader; +} + +void * CTimeServerThread::Entry() +{ + // Wait here until we have the essentials to run + while (!m_killed && m_address.s_addr == INADDR_NONE && m_repeaters.size() == 0U) + Sleep(500UL); // 1/2 sec + + if (m_killed) + return nullptr; + + if (m_format != FORMAT_TEXT_TIME) { + bool ret = loadAMBE(); + if (!ret) { + CLog::logWarning(("Cannot load the AMBE data, using text only time")); + m_format = FORMAT_TEXT_TIME; + } + } + + CLog::logInfo(("Starting the Time Server thread")); + + unsigned int lastMin = 0U; + + while (!m_killed) { + time_t now; + ::time(&now); + + struct tm* tm = ::localtime(&now); + + unsigned int hour = tm->tm_hour; + unsigned int min = tm->tm_min; + + // if (min != lastMin) + // sendTime(15, 45); + + if (min != lastMin) { + if (m_interval == INTERVAL_15MINS && (min == 0U || min == 15U || min == 30U || min == 45U)) + sendTime(hour, min); + else if (m_interval == INTERVAL_30MINS && (min == 0U || min == 30U)) + sendTime(hour, min); + else if (m_interval == INTERVAL_60MINS && min == 0U) + sendTime(hour, min); + } + + lastMin = min; + + Sleep(450UL); + } + + CLog::logInfo(("Stopping the Time Server thread")); + + return nullptr; +} + +void CTimeServerThread::kill() +{ + m_killed = true; +} + +bool CTimeServerThread::setGateway(const std::string& callsign, const std::string& rpt1, const std::string& rpt2, const std::string& rpt3, const std::string& rpt4, const std::string& address, const std::string& dataPath) +{ + m_callsign = callsign; + m_callsign.resize(LONG_CALLSIGN_LENGTH - 1U, (' ')); + + m_callsignG = m_callsign; + m_callsignG.push_back('G'); + + if (!rpt1.empty()) { + m_repeaters.push_back(m_callsign + rpt1); + } + + if (!rpt2.empty()) { + m_repeaters.push_back(m_callsign + rpt2); + } + + if (!rpt3.empty()) { + m_repeaters.push_back(m_callsign + rpt3); + } + + if (!rpt4.empty()) { + m_repeaters.push_back(m_callsign + rpt4); + } + + m_callsign.push_back(' '); + + m_addressStr.assign(address); + m_dataPath.assign(dataPath); + m_address = CUDPReaderWriter::lookup(address); + + return true; +} + +void CTimeServerThread::setAnnouncements(LANGUAGE language, FORMAT format, INTERVAL interval) +{ + m_language = language; + m_format = format; + m_interval = interval; +} + +void CTimeServerThread::sendTime(unsigned int hour, unsigned int min) +{ + std::vector words; + + switch (m_language) { + case LANG_ENGLISH_UK_1: + words = sendTimeEnGB1(hour, min); + break; + case LANG_ENGLISH_UK_2: + words = sendTimeEnGB2(hour, min); + break; + case LANG_ENGLISH_US_1: + words = sendTimeEnUS1(hour, min); + break; + case LANG_ENGLISH_US_2: + words = sendTimeEnUS2(hour, min); + break; + case LANG_DEUTSCH_1: + words = sendTimeDeDE1(hour, min); + break; + case LANG_DEUTSCH_2: + words = sendTimeDeDE2(hour, min); + break; + case LANG_FRANCAIS: + words = sendTimeFrFR(hour, min); + break; + case LANG_NEDERLANDS: + words = sendTimeNlNL(hour, min); + break; + case LANG_SVENSKA: + words = sendTimeSeSE(hour, min); + break; + case LANG_ESPANOL: + words = sendTimeEsES(hour, min); + break; + case LANG_NORSK: + words = sendTimeNoNO(hour, min); + break; + case LANG_PORTUGUES: + words = sendTimePtPT(hour, min); + break; + default: + break; + } + + send(words, hour, min); +} + +std::vector CTimeServerThread::sendTimeEnGB1(unsigned int hour, unsigned int min) +{ + std::vector words; + + words.push_back(("It_is")); + + switch (hour) { + case 0U: + case 12U: + words.push_back(("twelve")); + break; + case 1U: + case 13U: + words.push_back(("one")); + break; + case 2U: + case 14U: + words.push_back(("two")); + break; + case 3U: + case 15U: + words.push_back(("three")); + break; + case 4U: + case 16U: + words.push_back(("four")); + break; + case 5U: + case 17U: + words.push_back(("five")); + break; + case 6U: + case 18U: + words.push_back(("six")); + break; + case 7U: + case 19U: + words.push_back(("seven")); + break; + case 8U: + case 20U: + words.push_back(("eight")); + break; + case 9U: + case 21U: + words.push_back(("nine")); + break; + case 10U: + case 22U: + words.push_back(("ten")); + break; + case 11U: + case 23U: + words.push_back(("eleven")); + break; + default: + break; + } + + switch (min) { + case 15U: + words.push_back(("fifteen")); + break; + case 30U: + words.push_back(("thirty")); + break; + case 45U: + words.push_back(("forty-five")); + break; + default: + break; + } + + if (hour >= 12U) + words.push_back(("PM")); + else + words.push_back(("AM")); + + return words; +} + +std::vector CTimeServerThread::sendTimeEnGB2(unsigned int hour, unsigned int min) +{ + std::vector words; + + words.push_back("It_is"); + + if (min == 15U) { + words.push_back(("a_quarter_past")); + } else if (min == 30U) { + words.push_back(("half_past")); + } else if (min == 45U) { + words.push_back(("a_quarter_to")); + if (hour == 23U) + hour = 0U; + else + hour++; + } + + if (hour == 0U && min == 0U) { + words.push_back(("midnight")); + } else if (hour == 12U && min == 0U) { + words.push_back(("twelve")); + words.push_back(("noon")); + } else if (hour == 0U || hour == 12U) { + words.push_back(("twelve")); + } else if (hour == 1U || hour == 13U) { + words.push_back(("one")); + } else if (hour == 2U || hour == 14U) { + words.push_back(("two")); + } else if (hour == 3U || hour == 15U) { + words.push_back(("three")); + } else if (hour == 4U || hour == 16U) { + words.push_back(("four")); + } else if (hour == 5U || hour == 17U) { + words.push_back(("five")); + } else if (hour == 6U || hour == 18U) { + words.push_back(("six")); + } else if (hour == 7U || hour == 19U) { + words.push_back(("seven")); + } else if (hour == 8U || hour == 20U) { + words.push_back(("eight")); + } else if (hour == 9U || hour == 21U) { + words.push_back(("nine")); + } else if (hour == 10U || hour == 22U) { + words.push_back(("ten")); + } else if (hour == 11U || hour == 23U) { + words.push_back(("eleven")); + } + + if (hour != 0U && hour != 12U && min == 0U) + words.push_back(("O_Clock")); + + return words; +} + +std::vector CTimeServerThread::sendTimeEnUS1(unsigned int hour, unsigned int min) +{ + std::vector words; + + words.push_back(("It_is")); + + switch (hour) { + case 0U: + case 12U: + words.push_back(("twelve")); + break; + case 1U: + case 13U: + words.push_back(("one")); + break; + case 2U: + case 14U: + words.push_back(("two")); + break; + case 3U: + case 15U: + words.push_back(("three")); + break; + case 4U: + case 16U: + words.push_back(("four")); + break; + case 5U: + case 17U: + words.push_back(("five")); + break; + case 6U: + case 18U: + words.push_back(("six")); + break; + case 7U: + case 19U: + words.push_back(("seven")); + break; + case 8U: + case 20U: + words.push_back(("eight")); + break; + case 9U: + case 21U: + words.push_back(("nine")); + break; + case 10U: + case 22U: + words.push_back(("ten")); + break; + case 11U: + case 23U: + words.push_back(("eleven")); + break; + default: + break; + } + + switch (min) { + case 15U: + words.push_back(("fifteen")); + break; + case 30U: + words.push_back(("thirty")); + break; + case 45U: + words.push_back(("forty-five")); + break; + default: + break; + } + + if (hour >= 12U) + words.push_back(("PM")); + else + words.push_back(("AM")); + + return words; +} + +std::vector CTimeServerThread::sendTimeEnUS2(unsigned int hour, unsigned int min) +{ + std::vector words; + + words.push_back(("It_is")); + + if (min == 15U) { + words.push_back(("a_quarter_past")); + } else if (min == 30U) { + words.push_back(("half_past")); + } else if (min == 45U) { + words.push_back(("a_quarter_to")); + if (hour == 23U) + hour = 0U; + else + hour++; + } + + if (hour == 0U && min == 0U) { + words.push_back(("midnight")); + } else if (hour == 12U && min == 0U) { + words.push_back(("twelve")); + words.push_back(("noon")); + } else if (hour == 0U || hour == 12U) { + words.push_back(("twelve")); + } else if (hour == 1U || hour == 13U) { + words.push_back(("one")); + } else if (hour == 2U || hour == 14U) { + words.push_back(("two")); + } else if (hour == 3U || hour == 15U) { + words.push_back(("three")); + } else if (hour == 4U || hour == 16U) { + words.push_back(("four")); + } else if (hour == 5U || hour == 17U) { + words.push_back(("five")); + } else if (hour == 6U || hour == 18U) { + words.push_back(("six")); + } else if (hour == 7U || hour == 19U) { + words.push_back(("seven")); + } else if (hour == 8U || hour == 20U) { + words.push_back(("eight")); + } else if (hour == 9U || hour == 21U) { + words.push_back(("nine")); + } else if (hour == 10U || hour == 22U) { + words.push_back(("ten")); + } else if (hour == 11U || hour == 23U) { + words.push_back(("eleven")); + } + + if (hour != 0U && hour != 12U && min == 0U) + words.push_back(("O_Clock")); + + return words; +} + +std::vector CTimeServerThread::sendTimeDeDE1(unsigned int hour, unsigned int min) +{ + std::vector words; + + words.push_back(("Es_ist")); + + switch (hour) { + case 0U: words.push_back(("null")); break; + case 1U: words.push_back(("ein")); break; + case 2U: words.push_back(("zwei")); break; + case 3U: words.push_back(("drei")); break; + case 4U: words.push_back(("vier")); break; + case 5U: words.push_back(("fuenf")); break; + case 6U: words.push_back(("sechs")); break; + case 7U: words.push_back(("sieben")); break; + case 8U: words.push_back(("acht")); break; + case 9U: words.push_back(("neun")); break; + case 10U: words.push_back(("zehn")); break; + case 11U: words.push_back(("elf")); break; + case 12U: words.push_back(("zwoelf")); break; + case 13U: words.push_back(("dreizehn")); break; + case 14U: words.push_back(("vierzehn")); break; + case 15U: words.push_back(("fuenfzehn")); break; + case 16U: words.push_back(("sechzehn")); break; + case 17U: words.push_back(("siebzehn")); break; + case 18U: words.push_back(("achtzehn")); break; + case 19U: words.push_back(("neunzehn")); break; + case 20U: words.push_back(("zwanzig")); break; + case 21U: words.push_back(("einundzwanzig")); break; + case 22U: words.push_back(("zweiundzwanzig")); break; + case 23U: words.push_back(("dreiundzwanzig")); break; + default: break; + } + + words.push_back(("Uhr")); + + switch (min) { + case 15U: + words.push_back(("fuenfzehn")); + break; + case 30U: + words.push_back(("dreissig")); + break; + case 45U: + words.push_back(("fuenfundvierzig")); + break; + default: + break; + } + + return words; +} + +std::vector CTimeServerThread::sendTimeDeDE2(unsigned int hour, unsigned int min) +{ + std::vector words; + + words.push_back(("Es_ist")); + + if (min == 15U) { + words.push_back(("viertel_nach")); + } else if (min == 30U) { + words.push_back(("halb")); + if (hour == 23U) + hour = 0U; + else + hour++; + } else if (min == 45U) { + words.push_back(("viertel_vor")); + if (hour == 23U) + hour = 0U; + else + hour++; + } + + if (hour == 0U) { + words.push_back(("null")); + } else if (hour == 1U || hour == 13U) { + words.push_back(("ein")); + } else if (hour == 2U || hour == 14U) { + words.push_back(("zwei")); + } else if (hour == 3U || hour == 15U) { + words.push_back(("drei")); + } else if (hour == 4U || hour == 16U) { + words.push_back(("vier")); + } else if (hour == 5U || hour == 17U) { + words.push_back(("fuenf")); + } else if (hour == 6U || hour == 18U) { + words.push_back(("sechs")); + } else if (hour == 7U || hour == 19U) { + words.push_back(("sieben")); + } else if (hour == 8U || hour == 20U) { + words.push_back(("acht")); + } else if (hour == 9U || hour == 21U) { + words.push_back(("neun")); + } else if (hour == 10U || hour == 22U) { + words.push_back(("zehn")); + } else if (hour == 11U || hour == 23U) { + words.push_back(("elf")); + } else if (hour == 12U) { + words.push_back(("zwoelf")); + } + + if (min == 0U) + words.push_back(("Uhr")); + + return words; +} + +std::vector CTimeServerThread::sendTimeFrFR(unsigned int hour, unsigned int min) +{ + std::vector words; + + // if (hour > 17U) + // words.push_back(("bonsoir")); + // else + // words.push_back(("bonjour")); + + words.push_back(("il_est")); + + if (min == 45U) + hour++; + + if (hour == 0U) { + words.push_back(("minuit")); + } else if (hour == 1U || hour == 13U) { + words.push_back(("une")); + } else if (hour == 2U || hour == 14U) { + words.push_back(("deux")); + } else if (hour == 3U || hour == 15U) { + words.push_back(("trois")); + } else if (hour == 4U || hour == 16U) { + words.push_back(("quatre")); + } else if (hour == 5U || hour == 17U) { + words.push_back(("cinq")); + } else if (hour == 6U || hour == 18U) { + words.push_back(("six")); + } else if (hour == 7U || hour == 19U) { + words.push_back(("sept")); + } else if (hour == 8U || hour == 20U) { + words.push_back(("huit")); + } else if (hour == 9U || hour == 21U) { + words.push_back(("neuf")); + } else if (hour == 10U || hour == 22U) { + words.push_back(("dix")); + } else if (hour == 11U || hour == 23U) { + words.push_back(("onze")); + } else if (hour == 12U) { + words.push_back(("midi")); + } + + if (hour == 1U || hour == 13U) + words.push_back(("heure")); + else if (hour != 12U && hour != 0U) + words.push_back(("heures")); + + if (min == 15U) { + words.push_back(("et_quart")); + } else if (min == 30U) { + words.push_back(("et_demie")); + } else if (min == 45U) { + words.push_back(("moins_le_quart")); + if (hour == 23U) + hour = 0U; + else + hour++; + } + + return words; +} + +std::vector CTimeServerThread::sendTimeNlNL(unsigned int hour, unsigned int min) +{ + std::vector words; + + words.push_back(("Het_is")); + + if (min == 15U) { + words.push_back(("kwart_over")); + } else if (min == 30U) { + words.push_back(("half")); + if (hour == 23U) + hour = 0U; + else + hour++; + } else if (min == 45U) { + words.push_back(("kwart_voor")); + if (hour == 23U) + hour = 0U; + else + hour++; + } + + if (hour == 0U || hour == 12U) { + words.push_back(("twaalf")); + } else if (hour == 1U || hour == 13U) { + words.push_back(("een")); + } else if (hour == 2U || hour == 14U) { + words.push_back(("twee")); + } else if (hour == 3U || hour == 15U) { + words.push_back(("drie")); + } else if (hour == 4U || hour == 16U) { + words.push_back(("vier")); + } else if (hour == 5U || hour == 17U) { + words.push_back(("vijf")); + } else if (hour == 6U || hour == 18U) { + words.push_back(("zes")); + } else if (hour == 7U || hour == 19U) { + words.push_back(("zeven")); + } else if (hour == 8U || hour == 20U) { + words.push_back(("acht")); + } else if (hour == 9U || hour == 21U) { + words.push_back(("negen")); + } else if (hour == 10U || hour == 22U) { + words.push_back(("tien")); + } else if (hour == 11U || hour == 23U) { + words.push_back(("elf")); + } + + if (min == 0U) + words.push_back(("uur")); + + return words; +} + +std::vector CTimeServerThread::sendTimeSeSE(unsigned int hour, unsigned int min) +{ + std::vector words; + + words.push_back(("Klockan_ar")); + + if (min == 15U) { + words.push_back(("kvart_over")); + } else if (min == 30U) { + words.push_back(("halv")); + if (hour == 23U) + hour = 0U; + else + hour++; + } else if (min == 45U) { + words.push_back(("kvart_i")); + if (hour == 23U) + hour = 0U; + else + hour++; + } + + if (hour == 0U || hour == 12U) { + words.push_back(("tolv")); + } else if (hour == 1U || hour == 13U) { + words.push_back(("ett")); + } else if (hour == 2U || hour == 14U) { + words.push_back(("tva")); + } else if (hour == 3U || hour == 15U) { + words.push_back(("tre")); + } else if (hour == 4U || hour == 16U) { + words.push_back(("fyra")); + } else if (hour == 5U || hour == 17U) { + words.push_back(("fem")); + } else if (hour == 6U || hour == 18U) { + words.push_back(("sex")); + } else if (hour == 7U || hour == 19U) { + words.push_back(("sju")); + } else if (hour == 8U || hour == 20U) { + words.push_back(("atta")); + } else if (hour == 9U || hour == 21U) { + words.push_back(("nio")); + } else if (hour == 10U || hour == 22U) { + words.push_back(("tio")); + } else if (hour == 11U || hour == 23U) { + words.push_back(("elva")); + } + + return words; +} + +std::vector CTimeServerThread::sendTimeEsES(unsigned int hour, unsigned int min) +{ + std::vector words; + + if (min == 45U) { + hour++; + if (hour == 24U) + hour = 0U; + } + + if (hour == 1U) + words.push_back(("Es_la")); + else if (hour == 0U || hour == 12U) + words.push_back(("Es")); + else + words.push_back(("Son_las")); + + if (hour == 0U) { + words.push_back(("medianoche")); + } else if (hour == 1U || hour == 13U) { + words.push_back(("una")); + } else if (hour == 2U || hour == 14U) { + words.push_back(("dos")); + } else if (hour == 3U || hour == 15U) { + words.push_back(("tres")); + } else if (hour == 4U || hour == 16U) { + words.push_back(("cuarto")); + } else if (hour == 5U || hour == 17U) { + words.push_back(("cinco")); + } else if (hour == 6U || hour == 18U) { + words.push_back(("seis")); + } else if (hour == 7U || hour == 19U) { + words.push_back(("siete")); + } else if (hour == 8U || hour == 20U) { + words.push_back(("ocho")); + } else if (hour == 9U || hour == 21U) { + words.push_back(("nueve")); + } else if (hour == 10U || hour == 22U) { + words.push_back(("diez")); + } else if (hour == 11U || hour == 23U) { + words.push_back(("once")); + } else { + words.push_back(("mediodia")); + } + + if (min == 15U) + words.push_back(("y_cuarto")); + else if (min == 30U) + words.push_back(("y_media")); + else if (min == 45U) + words.push_back(("menos_cuarto")); + + if (hour > 0U && hour < 12U) + words.push_back(("de_la_manana")); + else if (hour > 12U && hour < 19U) + words.push_back(("de_la_tarde")); + else if (hour >= 19U && hour <= 23U) + words.push_back(("de_la_noche")); + + return words; +} + +std::vector CTimeServerThread::sendTimeNoNO(unsigned int hour, unsigned int min) +{ + std::vector words; + + words.push_back(("Klokken_er")); + + if (min == 15U) { + words.push_back(("kvart_over")); + } else if (min == 30U) { + words.push_back(("halv")); + if (hour == 23U) + hour = 0U; + else + hour++; + } else if (min == 45U) { + words.push_back(("kvart_pa")); + if (hour == 23U) + hour = 0U; + else + hour++; + } + + if (hour == 0U || hour == 12U) { + words.push_back(("tolv")); + } else if (hour == 1U || hour == 13U) { + words.push_back(("ett")); + } else if (hour == 2U || hour == 14U) { + words.push_back(("to")); + } else if (hour == 3U || hour == 15U) { + words.push_back(("tre")); + } else if (hour == 4U || hour == 16U) { + words.push_back(("fire")); + } else if (hour == 5U || hour == 17U) { + words.push_back(("fem")); + } else if (hour == 6U || hour == 18U) { + words.push_back(("seks")); + } else if (hour == 7U || hour == 19U) { + words.push_back(("sju")); + } else if (hour == 8U || hour == 20U) { + words.push_back(("atte")); + } else if (hour == 9U || hour == 21U) { + words.push_back(("ni")); + } else if (hour == 10U || hour == 22U) { + words.push_back(("ti")); + } else if (hour == 11U || hour == 23U) { + words.push_back(("elleve")); + } + + return words; +} + +std::vector CTimeServerThread::sendTimePtPT(unsigned int hour, unsigned int min) +{ + std::vector words; + + if (min == 45U) { + hour++; + if (hour == 24U) + hour = 0U; + } + + if (hour == 1U || hour == 13U) + words.push_back(("E")); + else if (hour == 0U || hour == 12U) + words.push_back(("Es")); + else + words.push_back(("Sao")); + + if (min == 45U) { + if (hour == 0U || hour == 12U || hour == 1U || hour == 13U) + words.push_back(("quinze_para")); + else + words.push_back(("quinze_para_as")); + } + + if (hour == 0U) { + words.push_back(("meia-noite")); + } else if (hour == 1U || hour == 13U) { + words.push_back(("uma")); + } else if (hour == 2U || hour == 14U) { + words.push_back(("duas")); + } else if (hour == 3U || hour == 15U) { + words.push_back(("tres")); + } else if (hour == 4U || hour == 16U) { + words.push_back(("quatro")); + } else if (hour == 5U || hour == 17U) { + words.push_back(("cinco")); + } else if (hour == 6U || hour == 18U) { + words.push_back(("seis")); + } else if (hour == 7U || hour == 19U) { + words.push_back(("sete")); + } else if (hour == 8U || hour == 20U) { + words.push_back(("oito")); + } else if (hour == 9U || hour == 21U) { + words.push_back(("nove")); + } else if (hour == 10U || hour == 22U) { + words.push_back(("dez")); + } else if (hour == 11U || hour == 23U) { + words.push_back(("onze")); + } else { + words.push_back(("meio-dia")); + } + + if (min == 0U) + words.push_back(("hora")); + else if (min == 15U) + words.push_back(("e_quinze")); + else if (min == 30U) + words.push_back(("e_meia")); + + return words; +} + +bool CTimeServerThread::loadAMBE() +{ + std::string ambeFileName; + std::string indxFileName; + + switch (m_language) { + case LANG_ENGLISH_US_1: + case LANG_ENGLISH_US_2: + ambeFileName = "TIME_en_US.ambe"; + indxFileName = "TIME_en_US.indx"; + break; + case LANG_DEUTSCH_1: + case LANG_DEUTSCH_2: + ambeFileName = "TIME_de_DE.ambe"; + indxFileName = "TIME_de_DE.indx"; + break; + case LANG_FRANCAIS: + ambeFileName = "TIME_fr_FR.ambe"; + indxFileName = "TIME_fr_FR.indx"; + break; + case LANG_NEDERLANDS: + ambeFileName = "TIME_nl_NL.ambe"; + indxFileName = "TIME_nl_NL.indx"; + break; + case LANG_SVENSKA: + ambeFileName = "TIME_se_SE.ambe"; + indxFileName = "TIME_se_SE.indx"; + break; + case LANG_ESPANOL: + ambeFileName = "TIME_es_ES.ambe"; + indxFileName = "TIME_es_ES.indx"; + break; + case LANG_NORSK: + ambeFileName = "TIME_no_NO.ambe"; + indxFileName = "TIME_no_NO.indx"; + break; + case LANG_PORTUGUES: + ambeFileName = "TIME_pt_PT.ambe"; + indxFileName = "TIME_pt_PT.indx"; + break; + default: + ambeFileName = "TIME_en_GB.ambe"; + indxFileName = "TIME_en_GB.indx"; + break; + } + + m_ambeFileReader = new CAMBEFileReader(m_dataPath + "/" + indxFileName, m_dataPath + "/" + ambeFileName); + bool ret = m_ambeFileReader->read(); + + if (!ret) { + delete[] m_ambeFileReader; + m_ambeFileReader = nullptr; + return false; + } + + return true; +} + +void CTimeServerThread::buildAudio(const std::vector& words, CSlowDataEncoder& slowDataEncoder) +{ + unsigned int seqNo = 0U; + + m_data.clear(); + + if(words.size() == 0U || m_ambeFileReader == nullptr) + CLog::logWarning("No words, falling back to text only"); + + if(m_format == FORMAT_VOICE_TIME && words.size() != 0U) { + // Build the audio + m_ambeFileReader->lookup(" ", m_data); + m_ambeFileReader->lookup(" ", m_data); + m_ambeFileReader->lookup(" ", m_data); + m_ambeFileReader->lookup(" ", m_data); + + for (unsigned int i = 0U; i < words.size(); i++) + m_ambeFileReader->lookup(words.at(i), m_data); + + m_ambeFileReader->lookup(" ", m_data); + m_ambeFileReader->lookup(" ", m_data); + m_ambeFileReader->lookup(" ", m_data); + m_ambeFileReader->lookup(" ", m_data); + + + // add the slow data + for(unsigned int i = 0U; i < m_data.size(); i++) { + m_data[i]->setDestination(m_address, G2_DV_PORT); + m_data[i]->setSeq(seqNo); + + unsigned char buffer[DV_FRAME_LENGTH_BYTES]; + m_data[i]->getData(buffer, DV_FRAME_LENGTH_BYTES); + + // Insert sync bytes when the sequence number is zero, slow data otherwise + if (seqNo == 0U) { + ::memcpy(buffer + VOICE_FRAME_LENGTH_BYTES, DATA_SYNC_BYTES, DATA_FRAME_LENGTH_BYTES); + slowDataEncoder.sync(); + } else { + slowDataEncoder.getInterleavedData(buffer + VOICE_FRAME_LENGTH_BYTES); + } + + m_data[i]->setData(buffer, DV_FRAME_LENGTH_BYTES); + + seqNo++; + if(seqNo >= 21U) seqNo = 0U; + } + } + else { + for (unsigned int i = 0U; i < 21U; i++, seqNo++) { + CAMBEData* dataOut = new CAMBEData; + unsigned char buffer[DV_FRAME_LENGTH_BYTES]; + dataOut->setDestination(m_address, G2_DV_PORT); + dataOut->setSeq(i); + + ::memcpy(buffer + 0U, NULL_AMBE_DATA_BYTES, VOICE_FRAME_LENGTH_BYTES); + + // Insert sync bytes when the sequence number is zero, slow data otherwise + if (i == 0U) { + ::memcpy(buffer + VOICE_FRAME_LENGTH_BYTES, DATA_SYNC_BYTES, DATA_FRAME_LENGTH_BYTES); + slowDataEncoder.sync(); + } else { + slowDataEncoder.getTextData(buffer + VOICE_FRAME_LENGTH_BYTES); + } + + dataOut->setData(buffer, DV_FRAME_LENGTH_BYTES); + + m_data.push_back(dataOut); + } + } + + if(seqNo >= 21U) { + seqNo = 0U; + } + + CAMBEData* dataOut = new CAMBEData; + dataOut->setData(END_PATTERN_BYTES, DV_FRAME_LENGTH_BYTES); + dataOut->setDestination(m_address, G2_DV_PORT); + dataOut->setSeq(seqNo); + dataOut->setEnd(true); + + m_data.push_back(dataOut); +} + +bool CTimeServerThread::send(const std::vector &words, unsigned int hour, unsigned int min) +{ + CSlowDataEncoder encoder; + + CHeaderData header; + header.setMyCall1(m_callsign); + header.setRptCall1(m_callsignG); + header.setRptCall2(m_callsign); // Just for the slow data header + header.setYourCall("CQCQCQ "); + header.setDestination(m_address, G2_DV_PORT); + + std::string slowData; + switch (m_language) { + case LANG_DEUTSCH_1: + case LANG_DEUTSCH_2: + header.setMyCall2(("ZEIT")); + slowData = CStringUtils::string_format(("Es ist %02u:%02u Uhr"), hour, min); + break; + case LANG_FRANCAIS: + header.setMyCall2(("TIME")); + slowData = CStringUtils::string_format(("Il est %02u:%02u"), hour, min); + break; + case LANG_NEDERLANDS: + header.setMyCall2(("TIJD")); + slowData = CStringUtils::string_format(("Het is %02u:%02u"), hour, min); + break; + case LANG_SVENSKA: + header.setMyCall2(("TID ")); + slowData = CStringUtils::string_format(("Klockan ar %02u:%02u"), hour, min); + break; + case LANG_ENGLISH_US_1: + case LANG_ENGLISH_UK_1: + header.setMyCall2(("TIME")); + if (hour == 0U) + slowData = CStringUtils::string_format(("It is 12:%02u AM"), min); + else if (hour == 12U) + slowData = CStringUtils::string_format(("It is 12:%02u PM"), min); + else if (hour > 12U) + slowData = CStringUtils::string_format(("It is %02u:%02u PM"), hour - 12U, min); + else + slowData = CStringUtils::string_format(("It is %02u:%02u AM"), hour, min); + break; + case LANG_ESPANOL: + header.setMyCall2(("HORA")); + if (hour == 1U) + slowData = CStringUtils::string_format(("Es la %02u:%02u"), hour, min); + else + slowData = CStringUtils::string_format(("Son las %02u:%02u"), hour, min); + break; + case LANG_NORSK: + header.setMyCall2(("TID ")); + slowData = CStringUtils::string_format(("Klokken er %02u:%02u"), hour, min); + break; + case LANG_PORTUGUES: + header.setMyCall2(("HORA")); + if (hour == 1U) + slowData = CStringUtils::string_format(("E %02u:%02u"), hour, min); + else + slowData = CStringUtils::string_format(("Sao %02u:%02u"), hour, min); + break; + default: + header.setMyCall2(("TIME")); + slowData = CStringUtils::string_format(("It is %02u:%02u"), hour, min); + break; + } + + encoder.setHeaderData(header); + encoder.setTextData(slowData); + + buildAudio(words, encoder); + + if (m_data.size() == 0U) { + CLog::logWarning(("Not sending, no audio files loaded")); + return false; + } + + if(m_format == FORMAT_VOICE_TIME) { + std::string text = boost::algorithm::join(words, " "); + boost::replace_all(text, "_", " "); + CLog::logInfo("Sending voice \"%s\", sending text \"%s\"", text.c_str(), slowData.c_str()); + } + else { + CLog::logInfo("Sending text \"%s\"", slowData.c_str()); + } + + // Build id and socket lists + std::vector ids; + std::vector sockets; + for(auto rpt : m_repeaters) { + auto socket = new CUDPReaderWriter("", 0U); + sockets.push_back(socket); + ids.push_back(CHeaderData::createId()); + } + + // open them all + bool allOpen = std::all_of(sockets.begin(), sockets.end(), [](CUDPReaderWriter* s) { return s->open(); }); + if(allOpen) { + //send headers + for(unsigned int i = 0; i < m_repeaters.size(); i++) { + CHeaderData headerCopy(header); + headerCopy.setId(ids[i]); + headerCopy.setRptCall2(m_repeaters[i]); + sendHeader(*(sockets[i]), headerCopy); + Sleep(5); + } + + // send audio + bool loop = true; + unsigned int out = 0U; + auto start = std::chrono::high_resolution_clock::now(); + + while(loop) { + unsigned int needed = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count(); + needed /= DSTAR_FRAME_TIME_MS; + + while (out < needed) { + for(unsigned int i = 0; i < m_repeaters.size(); i++) { + CAMBEData data(*(m_data[out])); + data.setId(ids[i]); + sendData(*(sockets[i]), data); + Sleep(5); + } + + delete m_data[out]; + m_data[out] = nullptr; + out++; + + if (out >= m_data.size()) { + loop = false; + break; + } + } + } + } + + m_data.clear(); + + for(auto socket : sockets) { + socket->close(); + delete socket; + } + + return true; +} + + +bool CTimeServerThread::sendHeader(CUDPReaderWriter& socket, const CHeaderData &header) +{ + unsigned char buffer[60U]; + unsigned int length = header.getG2Data(buffer, 60U, true); + +#if defined(DUMP_TX) + CUtils::dump(("Sending Header"), buffer, length); + return true; +#else + for (unsigned int i = 0U; i < 5U; i++) { + bool res = socket.write(buffer, length, header.getYourAddress(), header.getYourPort()); + if (!res) + return false; + } + + return true; +#endif +} + +bool CTimeServerThread::sendData(CUDPReaderWriter& socket, const CAMBEData& data) +{ + unsigned char buffer[40U]; + unsigned int length = data.getG2Data(buffer, 40U); + +#if defined(DUMP_TX) + CUtils::dump(("Sending Data"), buffer, length); + return true; +#else + return socket.write(buffer, length, data.getYourAddress(), data.getYourPort()); +#endif +} diff --git a/DGWTimeServer/TimeServerThread.h b/DGWTimeServer/TimeServerThread.h new file mode 100644 index 0000000..36bb9c7 --- /dev/null +++ b/DGWTimeServer/TimeServerThread.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2012,2013 by Jonathan Naylor G4KLX + * Copyright (C) 2022 by Geoffrey Merck F4FXL / KC3FRA + * + * 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. + */ + +#pragma once + +#include + +#include "SlowDataEncoder.h" +#include "UDPReaderWriter.h" +#include "TimeServerDefs.h" +#include "HeaderData.h" +#include "AMBEData.h" +#include "Thread.h" +#include "AMBEFileReader.h" + +class CTimeServerThread : public CThread +{ +public: + CTimeServerThread(); + ~CTimeServerThread(); + + bool setGateway(const std::string& callsign, const std::string& rpt1, const std::string& rpt2, const std::string& rpt3, const std::string& rpt4, const std::string& address, const std::string& dataPath); + void setAnnouncements(LANGUAGE language, FORMAT format, INTERVAL interval); + + void * Entry(); + void kill(); + +private: + std::string m_callsign; + std::vector m_repeaters; + std::string m_callsignG; + in_addr m_address; + std::string m_addressStr; + LANGUAGE m_language; + FORMAT m_format; + INTERVAL m_interval; + CSlowDataEncoder m_encoder; + std::vector m_data; + bool m_killed; + std::string m_dataPath; + CAMBEFileReader * m_ambeFileReader; + + void sendTime(unsigned int hour, unsigned int min); + + std::vector sendTimeEnGB1(unsigned int hour, unsigned int min); + std::vector sendTimeEnGB2(unsigned int hour, unsigned int min); + std::vector sendTimeEnUS1(unsigned int hour, unsigned int min); + std::vector sendTimeEnUS2(unsigned int hour, unsigned int min); + std::vector sendTimeDeDE1(unsigned int hour, unsigned int min); + std::vector sendTimeDeDE2(unsigned int hour, unsigned int min); + std::vector sendTimeFrFR(unsigned int hour, unsigned int min); + std::vector sendTimeNlNL(unsigned int hour, unsigned int min); + std::vector sendTimeSeSE(unsigned int hour, unsigned int min); + std::vector sendTimeEsES(unsigned int hour, unsigned int min); + std::vector sendTimeNoNO(unsigned int hour, unsigned int min); + std::vector sendTimePtPT(unsigned int hour, unsigned int min); + + bool send(const std::vector& words, unsigned int hour, unsigned int min); + bool sendHeader(CUDPReaderWriter& socket, const CHeaderData& header); + bool sendData(CUDPReaderWriter& socket, const CAMBEData& data); + + bool loadAMBE(); + + bool lookup(const std::string& id); + + void buildAudio(const std::vector& words, CSlowDataEncoder& slowDataEncoder); +}; diff --git a/DGWTimeServer/example.cfg b/DGWTimeServer/example.cfg new file mode 100644 index 0000000..94a823a --- /dev/null +++ b/DGWTimeServer/example.cfg @@ -0,0 +1,41 @@ +[TimeServer] +callsign= # call of the gateway to send time beacons without G letter +address= # address of the gateway, defaults to 127.0.0.1 +format= # possible values are voice, text, voiceandtext, defaults to voice and text +language= # valid values: english_uk_1, english_uk_2, english_us_1, english_us_2, deutsch_1, deutsch_2, francais, nederlands, svenska, espanol, norsk, portugues. Defaults to english_uk_1 +interval= # valid values are 15, 30 and 60, defaults to 30 + +[Paths] +data=/usr/local/share/dstargateway.d/ #Path where the data (hostfiles, audio files etc) can be found + +# Up to 4 repeaters can be enabled to transmit time beacons +[Repeater_1] +enabled=true # enable time beacons on this repeater +band=B # Module letter of the repeater + +[Repeater_2] +enabled=false # enable time beacons on this repeater +band= # Module letter of the repeater + +[Repeater_3] +enabled=false # enable time beacons on this repeater +band= # Module letter of the repeater + +[Repeater_4] +enabled=false # enable time beacons on this repeater +band= # Module letter of the repeater + +[Log] +path=/var/log/dstargateway/ +fileRoot= # defaults to dgwtimeserver +fileRotate= # rotate log files daily, defaults to true +fileLevel= # defaults to info, valid values are trace, debug, info, warning, error, fatal, none +displayLevel= # defaults to info, valid values are trace, debug, info, warning, error, fatal, none + +# Provided install routines install the program as a systemd unit. SystemD does not recommand "old-school" forking daemons nor does systemd +# require a pid file. Moreover systemd handles the user under which the program is started. This is provided as convenience for people who might +# run the program using sysv or any other old school init system. +[Daemon] +daemon=false +pidfile=/var/run/dstargateway/dstargateway.pid # pid file is in our case useless when running as a daemon using systemd as systemd takes care of the service not being started twice +user=dstar # user account the daemon will run under, ideally a user with low privileges. Switching to this user will only happen when the program is started as root diff --git a/DStarBase/AMBEFileReader.cpp b/DStarBase/AMBEFileReader.cpp new file mode 100644 index 0000000..119ea4a --- /dev/null +++ b/DStarBase/AMBEFileReader.cpp @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2021 by Geoffrey Merck F4FXL / KC3FRA + * + * 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 +#include +#include + +#include "AMBEFileReader.h" +#include "DStarDefines.h" +#include "Log.h" + +const unsigned int SILENCE_LENGTH = 10U; + +CAMBEFileReader::CAMBEFileReader(const std::string& indexFile, const std::string& ambeFile) : +m_indexFile(indexFile), +m_ambeFile(ambeFile), +m_ambe(nullptr), +m_ambeLength(0U), +m_index() +{ + +} + +CAMBEFileReader::~CAMBEFileReader() +{ + if(m_ambe != nullptr) { + delete[] m_ambe; + } +} + +bool CAMBEFileReader::read() +{ + bool ret = readAmbe() && readIndex(); + return ret; +} + +bool CAMBEFileReader::readAmbe() +{ + struct stat sbuf; + if (stat(m_ambeFile.c_str(), &sbuf)) { + CLog::logWarning("File %s not readable\n", m_ambeFile.c_str()); + return false; + } + + unsigned int fsize = sbuf.st_size; + + FILE *file = fopen(m_ambeFile.c_str(), "rb"); + if (NULL == file) { + CLog::logError("Cannot open %s for reading\n", m_ambeFile.c_str()); + return false; + } + + CLog::logInfo("Reading %s\n", m_ambeFile.c_str()); + + unsigned char buffer[VOICE_FRAME_LENGTH_BYTES]; + + size_t n = fread(buffer, sizeof(unsigned char), 4, file); + if (n != 4) { + CLog::logError("Unable to read the header from %s\n", m_ambeFile.c_str()); + fclose(file); + return false; + } + + if (memcmp(buffer, "AMBE", 4)) { + CLog::logError("Invalid header from %s\n", m_ambeFile.c_str()); + fclose(file); + return false; + } + + // Length of the file minus the header + unsigned int length = fsize - 4U; + + // Hold the file data plus silence at the end + m_ambe = new unsigned char[length + SILENCE_LENGTH * VOICE_FRAME_LENGTH_BYTES]; + m_ambeLength = length / VOICE_FRAME_LENGTH_BYTES; + + // Add silence to the beginning of the buffer + unsigned char* p = m_ambe; + for (unsigned int i = 0U; i < SILENCE_LENGTH; i++, p += VOICE_FRAME_LENGTH_BYTES) + memcpy(p, NULL_AMBE_DATA_BYTES, VOICE_FRAME_LENGTH_BYTES); + + n = fread(p, 1, length, file); + if (n != length) { + CLog::logError("Unable to read the AMBE data from %s\n", m_ambeFile.c_str()); + fclose(file); + delete[] m_ambe; + m_ambeLength = 0U; + m_ambe = nullptr; + return false; + } + + fclose(file); + + return true; +} + +bool CAMBEFileReader::readIndex() +{ + struct stat sbuf; + + if (stat(m_indexFile.c_str(), &sbuf)) { + CLog::logError("File %s not readable\n", m_indexFile.c_str()); + return false; + } + + FILE *file = fopen(m_indexFile.c_str(), "r"); + if (file == nullptr) { + CLog::logError("Cannot open %s for reading\n", m_indexFile.c_str()); + return false; + } + + // Add a silence entry at the beginning + m_index[" "] = new CIndexRecord(" ", 0, SILENCE_LENGTH); + + CLog::logInfo("Reading %s\n", m_indexFile.c_str()); + + char line[128]; + while (fgets(line, 128, file)) { + if (strlen(line) && '#'!=line[0]) { + const std::string space(" \t\r\n"); + std::string name(strtok(line, space.c_str())); + std::string strt(strtok(NULL, space.c_str())); + std::string leng(strtok(NULL, space.c_str())); + + if (name.size() && strt.size() && leng.size()) { + unsigned long start = std::stoul(strt); + unsigned long length = std::stoul(leng); + + if (start >= m_ambeLength || (start + length) >= m_ambeLength) + CLog::logInfo("The start or end for *%s* is out of range, start: %lu, end: %lu\n", name.c_str(), start, start + length); + else + m_index[name] = new CIndexRecord(name, start + SILENCE_LENGTH, length); + } + } + } + + fclose(file); + + return true; +} + +bool CAMBEFileReader::lookup(const std::string &id, std::vector& data) +{ + if(m_index.count(id) == 0U) { + CLog::logError("Cannot find the AMBE index for *%s*", id.c_str()); + return false; + } + + CIndexRecord* info = m_index[id]; + unsigned int start = info->getStart(); + unsigned int length = info->getLength(); + + for (unsigned int i = 0U; i < length; i++) { + unsigned char* dataIn = m_ambe + (start + i) * VOICE_FRAME_LENGTH_BYTES; + unsigned char buffer[DV_FRAME_LENGTH_BYTES]; + ::memcpy(buffer + 0U, dataIn, VOICE_FRAME_LENGTH_BYTES); + + CAMBEData* dataOut = new CAMBEData; + dataOut->setData(buffer, DV_FRAME_LENGTH_BYTES); + data.push_back(dataOut); + } + + return true; +} \ No newline at end of file diff --git a/DStarBase/AMBEFileReader.h b/DStarBase/AMBEFileReader.h new file mode 100644 index 0000000..cd66e6c --- /dev/null +++ b/DStarBase/AMBEFileReader.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2021 by Geoffrey Merck F4FXL / KC3FRA + * + * 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. + */ + +#pragma once + +#include +#include +#include + +#include "AMBEData.h" + +class CIndexRecord { +public: + CIndexRecord(const std::string& name, unsigned int start, unsigned int length) : + m_name(name), + m_start(start), + m_length(length) + { + } + + std::string getName() const + { + return m_name; + } + + unsigned int getStart() const + { + return m_start; + } + + unsigned int getLength() const + { + return m_length; + } + +private: + std::string m_name; + unsigned int m_start; + unsigned int m_length; +}; + +class CAMBEFileReader +{ +public: + CAMBEFileReader(const std::string& indexFile, const std::string& ambeFile); + ~CAMBEFileReader(); + bool read(); + bool lookup(const std::string &id, std::vector& data); + +private: + bool readAmbe(); + bool readIndex(); + + std::string m_indexFile; + std::string m_ambeFile; + unsigned char* m_ambe; + unsigned int m_ambeLength; + std::unordered_map m_index; +}; \ No newline at end of file diff --git a/DStarGateway/DStarGatewayApp.cpp b/DStarGateway/DStarGatewayApp.cpp index d0d5c52..634dd41 100644 --- a/DStarGateway/DStarGatewayApp.cpp +++ b/DStarGateway/DStarGatewayApp.cpp @@ -115,8 +115,8 @@ int main(int argc, char *argv[]) TLog logConf; config->getLog(logConf); CLog::finalise(); - if(logConf.m_displayLevel != LOG_NONE && !daemon.daemon) CLog::addTarget(new CLogConsoleTarget(logConf.m_displayLevel)); - if(logConf.m_fileLevel != LOG_NONE) CLog::addTarget(new CLogFileTarget(logConf.m_fileLevel, logConf.logDir, logConf.m_fileRotate)); + if(logConf.displayLevel != LOG_NONE && !daemon.daemon) CLog::addTarget(new CLogConsoleTarget(logConf.displayLevel)); + if(logConf.fileLevel != LOG_NONE) CLog::addTarget(new CLogFileTarget(logConf.fileLevel, logConf.logDir, logConf.fileRoot, logConf.fileRotate)); //write banner in log file if we are dameon if(daemon.daemon) { diff --git a/DStarGateway/DStarGatewayConfig.cpp b/DStarGateway/DStarGatewayConfig.cpp index 5d236ca..77f8086 100644 --- a/DStarGateway/DStarGatewayConfig.cpp +++ b/DStarGateway/DStarGatewayConfig.cpp @@ -153,34 +153,32 @@ bool CDStarGatewayConfig::loadLog(const CConfig & cfg) m_log.logDir.push_back('/'); } - ret = cfg.getValue("log", "fileRoot", m_log.m_fileRoot, 0, 64, "dstargateway") && ret; - ret = cfg.getValue("log", "fileRotate", m_log.m_fileRotate, true) && ret; + ret = cfg.getValue("log", "fileRoot", m_log.fileRoot, 0, 64, "dstargateway") && ret; + ret = cfg.getValue("log", "fileRotate", m_log.fileRotate, true) && ret; std::string levelStr; ret = cfg.getValue("log", "fileLevel", levelStr, "info", {"trace", "debug", "info", "warning", "error", "fatal", "none"}) && ret; if(ret) { - if(levelStr == "trace") m_log.m_fileLevel = LOG_TRACE; - else if(levelStr == "debug") m_log.m_fileLevel = LOG_DEBUG; - else if(levelStr == "info") m_log.m_fileLevel = LOG_INFO; - else if(levelStr == "warning") m_log.m_fileLevel = LOG_WARNING; - else if(levelStr == "error") m_log.m_fileLevel = LOG_ERROR; - else if(levelStr == "fatal") m_log.m_fileLevel = LOG_FATAL; - else if(levelStr == "none") m_log.m_fileLevel = LOG_NONE; + if(levelStr == "trace") m_log.fileLevel = LOG_TRACE; + else if(levelStr == "debug") m_log.fileLevel = LOG_DEBUG; + else if(levelStr == "info") m_log.fileLevel = LOG_INFO; + else if(levelStr == "warning") m_log.fileLevel = LOG_WARNING; + else if(levelStr == "error") m_log.fileLevel = LOG_ERROR; + else if(levelStr == "fatal") m_log.fileLevel = LOG_FATAL; + else if(levelStr == "none") m_log.fileLevel = LOG_NONE; } ret = cfg.getValue("log", "displayLevel", levelStr, "info", {"trace", "debug", "info", "warning", "error", "fatal", "none"}) && ret; if(ret) { - if(levelStr == "trace") m_log.m_displayLevel = LOG_TRACE; - else if(levelStr == "debug") m_log.m_displayLevel = LOG_DEBUG; - else if(levelStr == "info") m_log.m_displayLevel = LOG_INFO; - else if(levelStr == "warning") m_log.m_displayLevel = LOG_WARNING; - else if(levelStr == "error") m_log.m_displayLevel = LOG_ERROR; - else if(levelStr == "fatal") m_log.m_displayLevel = LOG_FATAL; - else if(levelStr == "none") m_log.m_displayLevel = LOG_NONE; + if(levelStr == "trace") m_log.displayLevel = LOG_TRACE; + else if(levelStr == "debug") m_log.displayLevel = LOG_DEBUG; + else if(levelStr == "info") m_log.displayLevel = LOG_INFO; + else if(levelStr == "warning") m_log.displayLevel = LOG_WARNING; + else if(levelStr == "error") m_log.displayLevel = LOG_ERROR; + else if(levelStr == "fatal") m_log.displayLevel = LOG_FATAL; + else if(levelStr == "none") m_log.displayLevel = LOG_NONE; } - //TODO 20211226 check if directories are accessible - return ret; } @@ -199,6 +197,7 @@ bool CDStarGatewayConfig::loadPaths(const CConfig & cfg) bool CDStarGatewayConfig::loadRepeaters(const CConfig & cfg) { + m_repeaters.clear(); for(unsigned int i = 0; i < 4; i++) { std::string section = CStringUtils::string_format("repeater_%d", i+ 1); bool repeaterEnabled; diff --git a/DStarGateway/DStarGatewayConfig.h b/DStarGateway/DStarGatewayConfig.h index dec8ca5..1bfce6c 100644 --- a/DStarGateway/DStarGatewayConfig.h +++ b/DStarGateway/DStarGatewayConfig.h @@ -86,10 +86,10 @@ typedef struct { typedef struct { std::string logDir; - LOG_SEVERITY m_displayLevel; - LOG_SEVERITY m_fileLevel; - std::string m_fileRoot; - bool m_fileRotate; + LOG_SEVERITY displayLevel; + LOG_SEVERITY fileLevel; + std::string fileRoot; + bool fileRotate; } TLog; typedef struct { diff --git a/DStarGateway/Makefile b/DStarGateway/Makefile index 73318d1..03b2ea3 100644 --- a/DStarGateway/Makefile +++ b/DStarGateway/Makefile @@ -22,6 +22,15 @@ install: dstargateway @sed -i "s|path=/var/log/dstargateway/|path=$(LOG_DIR)|g" $(CFG_DIR)/dstargateway.cfg @sed -i "s|data=/usr/local/share/dstargateway.d/|data=$(DATA_DIR)|g" $(CFG_DIR)/dstargateway.cfg +# SystemD service install + @cp -f ../debian/dstargateway.service /lib/systemd/system/ + @sed -i "s|%CFG_DIR%|$(CFG_DIR)|g" /lib/systemd/system/dstargateway.service + systemctl enable dstargateway.service + @systemctl daemon-reload + @echo "\n" + @echo "DStarGateway Install complete, edit $(CFG_DIR)dstargateway.cfg and start the daemon with 'systemctl start dstargateway.service'" + @echo "\n" + ../APRS/APRS.a: ../Common/Common.a: ../DStarBase/DStarBase.a: diff --git a/DStarGateway/example.cfg b/DStarGateway/example.cfg index 36e38de..f79e4df 100644 --- a/DStarGateway/example.cfg +++ b/DStarGateway/example.cfg @@ -178,9 +178,9 @@ port=4242 password=CHANGE_ME # If password is left blank, remote will be disabled regardless of the enabled field # Provided install routines install the program as a systemd unit. SystemD does not recommand "old-school" forking daemons nor does systemd -# require a pid file. Moreover system handles the user under which the program is started. This is provided as convenience for people who might +# require a pid file. Moreover systemd handles the user under which the program is started. This is provided as convenience for people who might # run the program using sysv or any other old school init system. [Daemon] daemon=false pidfile=/var/run/dstargateway/dstargateway.pid # pid file is in our case useless when running as a daemon using systemd as systemd takes care of the service not being started twice -user=dstar # user account the daemon will run under, ideally a user with low privileges. Switching to this user will only happen when the program is started as root \ No newline at end of file +user=dstar # user account the daemon will run under, ideally a user with low privileges. Switching to this user will only happen when the program is started as root diff --git a/Makefile b/Makefile index 7a131cb..1ed1c97 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ endif .PHONY: all -all: DStarGateway/dstargateway DGWRemoteControl/dgwremotecontrol DGWTextTransmit/dgwtexttransmit DGWVoiceTransmit/dgwvoicetransmit #tests +all: DStarGateway/dstargateway DGWRemoteControl/dgwremotecontrol DGWTextTransmit/dgwtexttransmit DGWTimeServer/dgwtimeserver DGWVoiceTransmit/dgwvoicetransmit #tests APRS/APRS.a: BaseCommon/BaseCommon.a FORCE $(MAKE) -C APRS @@ -63,6 +63,9 @@ DGWRemoteControl/dgwremotecontrol: VersionInfo/GitVersion.h $(OBJS) DStarBase/DS DGWTextTransmit/dgwtexttransmit: VersionInfo/GitVersion.h $(OBJS) DStarBase/DStarBase.a BaseCommon/BaseCommon.a FORCE $(MAKE) -C DGWTextTransmit +DGWTimeServer/dgwtimeserver: VersionInfo/GitVersion.h $(OBJS) DStarBase/DStarBase.a BaseCommon/BaseCommon.a FORCE + $(MAKE) -C DGWTimeServer + DGWVoiceTransmit/dgwvoicetransmit: VersionInfo/GitVersion.h $(OBJS) DStarBase/DStarBase.a BaseCommon/BaseCommon.a FORCE $(MAKE) -C DGWVoiceTransmit @@ -99,6 +102,7 @@ install : DStarGateway/dstargateway DGWRemoteControl/dgwremotecontrol # install accessories $(MAKE) -C DGWRemoteControl install $(MAKE) -C DGWTextTransmit install + $(MAKE) -C DGWTimeServer install $(MAKE) -C DGWVoiceTransmit install # create user for daemon @@ -115,18 +119,9 @@ install : DStarGateway/dstargateway DGWRemoteControl/dgwremotecontrol $(MAKE) -C Data install @chown -R dstar:dstar $(DATA_DIR) -#install executables +# install services executables $(MAKE) -C DStarGateway install -# SystemD service install - @cp -f debian/dstargateway.service /lib/systemd/system/ - @sed -i "s|%CFG_DIR%|$(CFG_DIR)|g" /lib/systemd/system/dstargateway.service - systemctl enable dstargateway.service - @systemctl daemon-reload - @echo "\n\n" - @echo "Install complete, edit $(CFG_DIR)dstargateway.cfg and start the daemon with 'systemctl start dstargateway.service'" - @echo "\n\n" - .PHONY: uninstall uninstall : systemctl stop dstargateway.service || true diff --git a/README.md b/README.md index 091d893..03649c1 100644 --- a/README.md +++ b/README.md @@ -135,26 +135,27 @@ the testing framwework used is Google Test. # 5. Version History ## 5.1. Version 0.6 -- [Improvement] Gracefully exit on SIGINT and SIGTERM ([#21](https://github.com/F4FXL/DStarGateway/issues/21)) -- [Improvement] Add text transmit utility dgwtexttransmit ([#18](https://github.com/F4FXL/DStarGateway/issues/18)) -- [Improvement] Add voice transmit utility dgwvoicetransmit ([#18](https://github.com/F4FXL/DStarGateway/issues/18)) +- [**Improvement**] Add time server +- [**Improvement**] Gracefully exit on SIGINT and SIGTERM ([#21](https://github.com/F4FXL/DStarGateway/issues/21)). DStarGateway can also be run as a "forking" daemon. This might be required for distros still using sysv. Systemd can live without it. +- [**Improvement**] Add text transmit utility dgwtexttransmit ([#18](https://github.com/F4FXL/DStarGateway/issues/18)) +- [**Improvement**] Add voice transmit utility dgwvoicetransmit ([#18](https://github.com/F4FXL/DStarGateway/issues/18)) ## 5.2. Version 0.5 -- [Improvement] Add remote control utility dgwremotecontrol ([#17](https://github.com/F4FXL/DStarGateway/issues/17)) -- [Bugfix] Two simultaneous incoming G2 streams would fail to be transmitted on dual band repeaters ([#16](https://github.com/F4FXL/DStarGateway/issues/16)) -- [Improvement] Add NAT Traversal for G2 and DExtra, using IRCDDB as a Rendez Vous server ([#5](https://github.com/F4FXL/DStarGateway/issues/5)) -- [Improvement] Add forwarding of RS-MS1A messages to APRS-IS ([#9](https://github.com/F4FXL/DStarGateway/issues/9)) -- [Bugfix] Failed to download XLX Hosts when URL contains a = sign ([#14](https://github.com/F4FXL/DStarGateway/issues/14)) -- [Bugfix] Remote control connection failed ([#13](https://github.com/F4FXL/DStarGateway/issues/13)) -- [Bugfix] Trying to connect to ghost ircDDB when no ircDDB is configured +- [**Improvement**] Add remote control utility dgwremotecontrol ([#17](https://github.com/F4FXL/DStarGateway/issues/17)) +- [**Bugfix**] Two simultaneous incoming G2 streams would fail to be transmitted on dual band repeaters ([#16](https://github.com/F4FXL/DStarGateway/issues/16)) +- [**Improvement**] Add NAT Traversal for G2 and DExtra, using IRCDDB as a Rendez Vous server ([#5](https://github.com/F4FXL/DStarGateway/issues/5)) +- [**Improvement**] Add forwarding of RS-MS1A messages to APRS-IS ([#9](https://github.com/F4FXL/DStarGateway/issues/9)) +- [**Bugfix**] Failed to download XLX Hosts when URL contains a = sign ([#14](https://github.com/F4FXL/DStarGateway/issues/14)) +- [**Bugfix**] Remote control connection failed ([#13](https://github.com/F4FXL/DStarGateway/issues/13)) +- [**Bugfix**] Trying to connect to ghost ircDDB when no ircDDB is configured ## 5.3. Version 0.4 -- [Improvement] Add APRS status link feature ([#8](https://github.com/F4FXL/DStarGateway/issues/8)) -- [Bugfix] Posotions received over radio were not sent to APRS-IS when GPDS connection failed. ([#7](https://github.com/F4FXL/DStarGateway/issues/7)) -- [Improvement] Bring back GPSD support ([#6](https://github.com/F4FXL/DStarGateway/issues/6)) -- [Improvement] Log enhancements ([#4](https://github.com/F4FXL/DStarGateway/issues/4)) +- [**Improvement**] Add APRS status link feature ([#8](https://github.com/F4FXL/DStarGateway/issues/8)) +- [**Bugfix**] Posotions received over radio were not sent to APRS-IS when GPDS connection failed. ([#7](https://github.com/F4FXL/DStarGateway/issues/7)) +- [**Improvement**] Bring back GPSD support ([#6](https://github.com/F4FXL/DStarGateway/issues/6)) +- [**Improvement**] Log enhancements ([#4](https://github.com/F4FXL/DStarGateway/issues/4)) ## 5.4. Version 0.3 -- [Improvement] Get ride of libcongif++ dependency. When upgrading from earlier version you need to manualy delete the config file before reinstalling. +- [**Improvement**] Get ride of libcongig++ dependency. When upgrading from earlier version you need to manualy delete the config file before reinstalling. ## 5.5. Version 0.2 -- [Bugfix] ircDDBFreeze when repeater not found ([#1](https://github.com/F4FXL/DStarGateway/issues/1)) +- [**Bugfix**] ircDDBFreeze when repeater not found ([#1](https://github.com/F4FXL/DStarGateway/issues/1)) - Code sanitization ## 5.6. Version 0.1 First working version @@ -163,10 +164,9 @@ I started this during my 2021 seasons holiday. It took me almost 8 days to get t - ☑ Better NatTraversal - No banging on every gateway: use ircDDB (or something else) as mitigation server to notify peer - Support for all protocols (G2, DExtra, DPlus) DCS does nto make sense as it was historically never used as protocol for linking repeaters - - A [branch](https://github.com/F4FXL/DStarGateway/tree/feature/NatTraversal) already exists for this - ☑ Send the connection status to APRS-IS as a status frame - ☒ Reinstantiate DRATS -- ☒ Migrate all the "accessories" (VoiceTransmit, RemoteControl ...) +- ☑ Migrate all the "accessories" (VoiceTransmit, RemoteControl ...) - ☒ Automatic refresh of host files - ☒ Reduce ircDDB dependency, build something more P2P, maybe based on [Distributed Hashtable](https://github.com/DavidKeller/kademlia) ? - ☒ Forward messages from RS-MS1A to APRS and vice versa diff --git a/Tests/AMBEFileReader/fr_FR.ambe b/Tests/AMBEFileReader/fr_FR.ambe new file mode 100644 index 0000000..c81d063 Binary files /dev/null and b/Tests/AMBEFileReader/fr_FR.ambe differ diff --git a/Tests/AMBEFileReader/fr_FR.indx b/Tests/AMBEFileReader/fr_FR.indx new file mode 100644 index 0000000..f67b54a --- /dev/null +++ b/Tests/AMBEFileReader/fr_FR.indx @@ -0,0 +1,44 @@ +0 94 29 +1 140 16 +2 173 20 +3 215 19 +4 255 24 +5 298 29 +6 344 34 +7 398 25 +8 440 24 +9 481 23 +alpha 521 29 +bravo 568 27 +charlie 615 30 +delta 666 30 +A 714 21 +B 753 27 +C 796 34 +D 848 30 +E 895 23 +F 936 27 +G 982 27 +H 1026 30 +I 1075 20 +J 1113 29 +K 1162 22 +L 1201 28 +M 1248 28 +N 1295 28 +O 1341 23 +P 1386 21 +Q 1426 23 +R 1466 28 +S 1511 31 +T 1567 21 +U 1606 22 +V 1646 30 +W 1693 46 +X 1756 31 +Y 1806 21 +Z 1844 14 +linkedto 1858 35 +notlinked 1941 46 +linkingto 2008 38 +isbusy 2091 47 diff --git a/Tests/AMBEFileReader/lookup.cpp b/Tests/AMBEFileReader/lookup.cpp new file mode 100644 index 0000000..9f75475 --- /dev/null +++ b/Tests/AMBEFileReader/lookup.cpp @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2021 by Geoffrey Merck F4FXL / KC3FRA + * + * 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 +#include +#include +#include + +#include "AMBEFileReader.h" +#include "AMBEData.h" + +namespace AMBEFileReaderTests +{ + class AMBEFileReader_lookup : public ::testing::Test { + + }; + + TEST_F(AMBEFileReader_lookup, nonExistentFiles) + { + CAMBEFileReader reader("/this/file/does/not/exist", "/neither/does/this/file"); + bool res = reader.read(); + EXPECT_FALSE(res) << "read shall return false on non existent files"; + + std::vector data; + res = reader.lookup("0", data); + EXPECT_FALSE(res) << "read shall return false on non existent files"; + + for(auto d : data) { + delete d; + } + } + + TEST_F(AMBEFileReader_lookup, onlyIndexFileExists) + { + std::string indexFile = std::string(std::filesystem::current_path()) + "/AMBEFileReader/fr_FR.indx"; + CAMBEFileReader reader(indexFile, "/this/file/does/not/exist"); + bool res = reader.read(); + EXPECT_FALSE(res) << "read shall return false on non existent file"; + + std::vector data; + res = reader.lookup("0", data); + EXPECT_FALSE(res) << "read shall return false on non existent file"; + + for(auto d : data) { + delete d; + } + } + + TEST_F(AMBEFileReader_lookup, onlyAmbeFileExists) + { + std::string ambeFile = std::string(std::filesystem::current_path()) + "/AMBEFileReader/fr_FR.ambe"; + CAMBEFileReader reader("/this/file/does/not/exist", ambeFile); + bool res = reader.read(); + EXPECT_FALSE(res) << "read shall return false on non existent file"; + + std::vector data; + res = reader.lookup("0", data); + EXPECT_FALSE(res) << "read shall return false on non existent file"; + + for(auto d : data) { + delete d; + } + } + + TEST_F(AMBEFileReader_lookup, validId) + { + std::string indexFile = std::string(std::filesystem::current_path()) + "/AMBEFileReader/fr_FR.indx"; + std::string ambeFile = std::string(std::filesystem::current_path()) + "/AMBEFileReader/fr_FR.ambe"; + CAMBEFileReader reader(indexFile, ambeFile); + bool res = reader.read(); + EXPECT_TRUE(res) << "read shall return true on existent files"; + + std::vector data; + res = reader.lookup("0", data); + EXPECT_TRUE(res) << "read shall return true on existent files and valid Id"; + EXPECT_NE(data.size(), 0U) << "Vector shall contain data"; + + for(auto d : data) { + delete d; + } + } + + TEST_F(AMBEFileReader_lookup, invalidId) + { + std::string indexFile = std::string(std::filesystem::current_path()) + "/AMBEFileReader/fr_FR.indx"; + std::string ambeFile = std::string(std::filesystem::current_path()) + "/AMBEFileReader/fr_FR.ambe"; + CAMBEFileReader reader(indexFile, ambeFile); + bool res = reader.read(); + EXPECT_TRUE(res) << "read shall return true on existent files"; + + std::vector data; + res = reader.lookup("This Id does not exist", data); + EXPECT_FALSE(res) << "read shall return false on existent files and invalid Id"; + EXPECT_EQ(data.size(), 0U) << "Vector shall not contain data"; + + for(auto d : data) { + delete d; + } + } +} \ No newline at end of file diff --git a/Tests/AMBEFileReader/read.cpp b/Tests/AMBEFileReader/read.cpp new file mode 100644 index 0000000..f7b1753 --- /dev/null +++ b/Tests/AMBEFileReader/read.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2021 by Geoffrey Merck F4FXL / KC3FRA + * + * 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 +#include +#include + +#include "AMBEFileReader.h" +#include "Log.h" + +namespace AMBEFileReaderTests +{ + class AMBEFileReader_read : public ::testing::Test { + + }; + + TEST_F(AMBEFileReader_read, nonExistentFiles) + { + CAMBEFileReader reader("/this/file/does/not/exist", "/neither/does/this/file"); + bool res = reader.read(); + EXPECT_FALSE(res) << "read shall return false on non existent files"; + } + + TEST_F(AMBEFileReader_read, onlyIndexFileExists) + { + std::string indexFile = std::string(std::filesystem::current_path()) + "/AMBEFileReader/fr_FR.indx"; + CAMBEFileReader reader(indexFile, "/this/file/does/not/exist"); + bool res = reader.read(); + EXPECT_FALSE(res) << "read shall return false on non existent file"; + } + + TEST_F(AMBEFileReader_read, onlyAmbeFileExists) + { + std::string ambeFile = std::string(std::filesystem::current_path()) + "/AMBEFileReader/fr_FR.ambe"; + CAMBEFileReader reader("/this/file/does/not/exist", ambeFile); + bool res = reader.read(); + EXPECT_FALSE(res) << "read shall return false on non existent file"; + } + + TEST_F(AMBEFileReader_read, bothFileExist) + { + std::string indexFile = std::string(std::filesystem::current_path()) + "/AMBEFileReader/fr_FR.indx"; + std::string ambeFile = std::string(std::filesystem::current_path()) + "/AMBEFileReader/fr_FR.ambe"; + + CAMBEFileReader reader(indexFile, ambeFile); + bool res = reader.read(); + EXPECT_TRUE(res) << "read shall return true on existent files"; + } +} \ No newline at end of file diff --git a/Tests/Makefile b/Tests/Makefile index 2e29d24..d3b30bb 100644 --- a/Tests/Makefile +++ b/Tests/Makefile @@ -10,12 +10,14 @@ dstargateway_tests: ../VersionInfo/GitVersion.h $(OBJS) ../APRS/APRS.a ../IRCDDB -include $(DEPS) -.PHONY run-tests: dstargateway_tests +.PHONY run-tests: dstargateway_tests ./dstargateway_tests .PHONY clean : clean : - @$(RM) *.o *.d dstargateway_tests + find . -name "*.o" -type f -delete + find . -name "*.d" -type f -delete + $(RM) *.o *.d dstargateway_tests ../APRS/APRS.a: ../Common/Common.a: diff --git a/Tests/ProgramArgs.cpp b/Tests/ProgramArgs/ProgramArgs.cpp similarity index 100% rename from Tests/ProgramArgs.cpp rename to Tests/ProgramArgs/ProgramArgs.cpp diff --git a/debian/dgwtimeserver.service b/debian/dgwtimeserver.service new file mode 100644 index 0000000..6767c9c --- /dev/null +++ b/debian/dgwtimeserver.service @@ -0,0 +1,13 @@ +[Unit] +Description=D-STAR Time Server Daemon +After=network.target,network-online.target +Wants=network-online.target + +[Service] +User=dstar +Type=simple +ExecStart=/usr/local/bin/dgwtimeserver %CFG_DIR%/dgwtimeserver.cfg +Restart=on-failure + +[Install] +WantedBy=multi-user.target