diff --git a/.gitignore b/.gitignore index dc59682..d900137 100644 --- a/.gitignore +++ b/.gitignore @@ -29,9 +29,5 @@ # Visual Studio .vscode -#files -configure.h -configure.mk - # Executables tcd diff --git a/Configure.cpp b/Configure.cpp new file mode 100644 index 0000000..800f775 --- /dev/null +++ b/Configure.cpp @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2023 by Thomas A. Early N7TAE + * + * 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 "Configure.h" + +// ini file keywords +#define DMRGAININ "DmrYsfGainIn" +#define DMRGAINOUT "DmrYsfGainOut" +#define DSTARGAININ "DStarGainIn" +#define DSTARGAINOUT "DStarGainOut" +#define TRANSCODED "Transcoded" + +static inline void split(const std::string &s, char delim, std::vector &v) +{ + std::istringstream iss(s); + std::string item; + while (std::getline(iss, item, delim)) + v.push_back(item); +} + +// trim from start (in place) +static inline void ltrim(std::string &s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { + return !std::isspace(ch); + })); +} + +// trim from end (in place) +static inline void rtrim(std::string &s) { + s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { + return !std::isspace(ch); + }).base(), s.end()); +} + +// trim from both ends (in place) +static inline void trim(std::string &s) { + ltrim(s); + rtrim(s); +} + +bool CConfigure::ReadData(const std::string &path) +// returns true on failure +{ + std::string modstmp; + + std::ifstream cfgfile(path.c_str(), std::ifstream::in); + if (! cfgfile.is_open()) { + std::cerr << "ERROR: '" << path << "' was not found!" << std::endl; + return true; + } + + std::string line; + while (std::getline(cfgfile, line)) + { + trim(line); + if (3 > line.size()) + continue; // can't be anything + if ('#' == line.at(0)) + continue; // skip comments + + std::vector tokens; + split(line, '=', tokens); + // check value for end-of-line comment + if (2 > tokens.size()) + { + std::cout << "WARNING: '" << line << "' does not contain an equal sign, skipping" << std::endl; + continue; + } + auto pos = tokens[1].find('#'); + if (std::string::npos != pos) + { + tokens[1].assign(tokens[1].substr(0, pos)); + rtrim(tokens[1]); // whitespace between the value and the end-of-line comment + } + // trim whitespace from around the '=' + rtrim(tokens[0]); + ltrim(tokens[1]); + const std::string key(tokens[0]); + const std::string value(tokens[1]); + if (key.empty() || value.empty()) + { + std::cout << "WARNING: missing key or value: '" << line << "'" << std::endl; + continue; + } + if (0 == key.compare(TRANSCODED)) + modstmp.assign(value); + else if (0 == key.compare(DSTARGAININ)) + dstar_in = getSigned(key, value); + else if (0 == key.compare(DSTARGAINOUT)) + dstar_out = getSigned(key, value); + else if (0 == key.compare(DMRGAININ)) + dmr_in = getSigned(key, value); + else if (0 == key.compare(DMRGAINOUT)) + dmr_out = getSigned(key, value); + else + badParam(key); + break; + } + cfgfile.close(); + + for (auto c : modstmp) + { + if (isalpha(c)) + { + if (islower(c)) + c = toupper(c); + if (std::string::npos == tcmods.find(c)) + tcmods.append(1, c); + } + } + if (tcmods.empty()) + { + std::cerr << "ERROR: no identifable module letters in '" << modstmp << "'. Halt." << std::endl; + return true; + } + + std::cout << TRANSCODED << " = " << tcmods << std::endl; + std::cout << DSTARGAININ << " = " << int(dstar_in) << std::endl; + std::cout << DSTARGAINOUT << " = " << int(dstar_out) << std::endl; + std::cout << DMRGAININ << " = " << int(dmr_in) << std::endl; + std::cout << DMRGAINOUT << " = " << int(dmr_out) << std::endl; + + return false; +} + +int CConfigure::getSigned(const std::string &key, const std::string &value) const +{ + auto i = std::stoi(value.c_str()); + if ( i < -90 || i > 90 ) + { + std::cout << "WARNING: " << key << " = " << value << " is out of range. Reset to zero!" << std::endl; + i = 0; + } + return i; +} + +void CConfigure::badParam(const std::string &key) const +{ + std::cout << "WARNING: Unexpected parameter: '" << key << "'" << std::endl; +} + +int8_t CConfigure::GetGain(EGainType gt) const +{ + switch (gt) + { + case EGainType::dmrin: return int8_t(dmr_in); + case EGainType::dmrout: return int8_t(dmr_out); + case EGainType::dstarin: return int8_t(dstar_in); + case EGainType::dstarout: return int8_t(dstar_out); + default: return 0; + } +} diff --git a/Configure.h b/Configure.h new file mode 100644 index 0000000..99f573e --- /dev/null +++ b/Configure.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 by Thomas A. Early N7TAE + * + * 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 + +enum class EGainType { dmrin, dmrout, dstarin, dstarout }; + +#define IS_TRUE(a) ((a)=='t' || (a)=='T' || (a)=='1') + +class CConfigure +{ +public: + bool ReadData(const std::string &path); + int8_t GetGain(EGainType gt) const; + std::string GetTCMods(void) const { return tcmods; } + +private: + // CFGDATA data; + std::string tcmods; + int dstar_in, dstar_out, dmr_in, dmr_out; + + int getSigned(const std::string &key, const std::string &value) const; + void badParam(const std::string &key) const; +}; diff --git a/Controller.cpp b/Controller.cpp index 10f8bb3..109a83d 100644 --- a/Controller.cpp +++ b/Controller.cpp @@ -22,13 +22,16 @@ #include #include #include + #ifdef USE_SW_AMBE2 #include #endif +#include "Configure.h" #include "TranscoderPacket.h" #include "Controller.h" +extern CConfigure g_Conf; //#define AMBE_GAIN 16 //Encoder gain in dB (I use 16 here) //#define AMBE2_GAIN -24 //Encoder gain in dB (I use -24 here) @@ -38,11 +41,11 @@ int16_t calcGainVal(float db) { float ratio = powf(10.0, (db/20.0)); - + if(db < 0){ ratio = (1/ratio) * (-1); } - + return (int16_t)roundf(ratio); } @@ -52,7 +55,7 @@ bool CController::Start() { usrp_rxgain = calcGainVal(USRP_RXGAIN); usrp_txgain = calcGainVal(USRP_TXGAIN); - + if (InitVocoders() || reader.Open(REF2TC)) { keep_running = false; @@ -130,7 +133,7 @@ bool CController::DiscoverFtdiDevices(std::list(new CCodec2(false)); @@ -210,10 +213,10 @@ bool CController::InitVocoders() dmrsf_device = std::unique_ptr(new CDV3003(Encoding::dmrsf)); #endif } - + if (dstar_device) { - if (dstar_device->OpenDevice(deviceset.front().first, deviceset.front().second, dvtype, DSTAR_IN_GAIN, DSTAR_OUT_GAIN)) + if (dstar_device->OpenDevice(deviceset.front().first, deviceset.front().second, dvtype, g_Conf.GetGain(EGainType::dstarin), g_Conf.GetGain(EGainType::dstarout))) return true; deviceset.pop_front(); } @@ -225,7 +228,7 @@ bool CController::InitVocoders() #ifndef USE_SW_AMBE2 if (dmrsf_device) { - if (dmrsf_device->OpenDevice(deviceset.front().first, deviceset.front().second, dvtype, DMR_IN_GAIN, DMR_OUT_GAIN)) + if (dmrsf_device->OpenDevice(deviceset.front().first, deviceset.front().second, dvtype, g_Conf.GetGain(EGainType::dmrin), g_Conf.GetGain(EGainType::dmrout))) return true; deviceset.pop_front(); } @@ -239,7 +242,7 @@ bool CController::InitVocoders() // and start them (or it) up! dstar_device->Start(); - + #ifndef USE_SW_AMBE2 dmrsf_device->Start(); #endif @@ -331,7 +334,7 @@ void CController::Codec2toAudio(std::shared_ptr packet) { uint8_t ambe2[9]; uint8_t imbe[11]; - + if (packet->IsSecond()) { if (packet->GetCodecIn() == ECodecType::c2_1600) @@ -380,7 +383,7 @@ void CController::Codec2toAudio(std::shared_ptr packet) #else dmrsf_device->AddPacket(packet); #endif - + packet->SetDMRData(ambe2); p25vocoder.encode_4400((int16_t*)packet->GetAudioSamples(), imbe); packet->SetP25Data(imbe); @@ -421,7 +424,7 @@ void CController::AudiotoSWAMBE2(std::shared_ptr packet) int16_t tmp[160]; const int16_t *p = packet->GetAudioSamples(); const uint32_t g = abs(ambe_gain); - + for(int i = 0; i < 160; ++i){ if(ambe_gain < 0){ tmp[i] = p[i] / g; @@ -430,10 +433,10 @@ void CController::AudiotoSWAMBE2(std::shared_ptr packet) tmp[i] = p[i] * g; } } - + md380_encode_fec(ambe2, tmp); packet->SetDMRData(ambe2); - + // we might be all done... send_mux.lock(); if (packet->AllCodecsAreSet() && packet->HasNotBeenSent()) SendToReflector(packet); @@ -494,13 +497,13 @@ void CController::IMBEtoAudio(std::shared_ptr packet) packet->SetAudioSamples(tmp, false); dstar_device->AddPacket(packet); codec2_queue.push(packet); - + #ifdef USE_SW_AMBE2 swambe2_queue.push(packet); #else dmrsf_device->AddPacket(packet); #endif - + usrp_queue.push(packet); } @@ -532,7 +535,7 @@ void CController::AudiotoUSRP(std::shared_ptr packet) int16_t tmp[160]; const int16_t *p = packet->GetAudioSamples(); const uint32_t g = abs(usrp_txgain); - + for(int i = 0; i < 160; ++i){ if(usrp_txgain < 0){ tmp[i] = p[i] / g; @@ -541,9 +544,9 @@ void CController::AudiotoUSRP(std::shared_ptr packet) tmp[i] = p[i] * g; } } - + packet->SetUSRPData(tmp); - + // we might be all done... send_mux.lock(); if (packet->AllCodecsAreSet() && packet->HasNotBeenSent()) SendToReflector(packet); @@ -555,7 +558,7 @@ void CController::USRPtoAudio(std::shared_ptr packet) int16_t tmp[160]; const int16_t *p = packet->GetUSRPData(); const uint32_t g = abs(usrp_rxgain); - + for(int i = 0; i < 160; ++i){ if(usrp_rxgain < 0){ tmp[i] = p[i] / g; @@ -564,18 +567,18 @@ void CController::USRPtoAudio(std::shared_ptr packet) tmp[i] = p[i] * g; } } - + //packet->SetAudioSamples(packet->GetUSRPData(), false); packet->SetAudioSamples(tmp, false); dstar_device->AddPacket(packet); codec2_queue.push(packet); - + #ifdef USE_SW_AMBE2 swambe2_queue.push(packet); #else dmrsf_device->AddPacket(packet); #endif - + imbe_queue.push(packet); } diff --git a/Controller.h b/Controller.h index 5b2ae04..1566027 100644 --- a/Controller.h +++ b/Controller.h @@ -30,7 +30,6 @@ #include "DV3000.h" #include "DV3003.h" #include "UnixDgramSocket.h" -#include "configure.h" class CController { @@ -55,7 +54,7 @@ protected: std::unique_ptr dstar_device, dmrsf_device; CPacketQueue codec2_queue; - + CPacketQueue imbe_queue; CPacketQueue usrp_queue; std::mutex send_mux; @@ -69,7 +68,7 @@ protected: // processing threads void ReadReflectorThread(); void ProcessC2Thread(); - + void ProcessIMBEThread(); void ProcessUSRPThread(); void Codec2toAudio(std::shared_ptr packet); diff --git a/Main.cpp b/Main.cpp index d27f7f7..7a4910d 100644 --- a/Main.cpp +++ b/Main.cpp @@ -18,20 +18,31 @@ #include #include "Controller.h" +#include "Configure.h" // the global controller object -CController Controller; +CConfigure g_Conf; +CController g_Cont; -int main() +int main(int argc, char *argv[]) { - if (Controller.Start()) + if (2 != argc) + { + std::cerr << "ERROR: Usage: " << argv[0] << " PATHTOINIFILE" << std::endl; return EXIT_FAILURE; + } - std::cout << "Hybrid Transcoder version 0.0.5 successfully started" << std::endl; + if (g_Conf.ReadData(argv[1])) + return EXIT_FAILURE; + + if (g_Cont.Start()) + return EXIT_FAILURE; + + std::cout << "Hybrid Transcoder version 0.1.0 successfully started" << std::endl; pause(); - Controller.Stop(); + g_Cont.Stop(); return EXIT_SUCCESS; } diff --git a/Makefile b/Makefile index a616efb..b67209f 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,6 @@ #Copyright (C) 2021 by Thomas A. Early, N7TAE -include configure.mk - -# If you are going to change this path, you will -# need to update the systemd service script -BINDIR = /usr/local/bin - -swambe2 = false +include tcd.mk GCC = g++ @@ -49,7 +43,7 @@ clean : # The install and uninstall targets need to be run by root install : $(EXE) cp $(EXE) $(BINDIR) - cp systemd/$(EXE).service /etc/systemd/system/ + cp $(EXE).service /etc/systemd/system/ systemctl enable $(EXE) systemctl daemon-reload systemctl start $(EXE) diff --git a/README.md b/README.md index 61c23e3..5146c57 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,22 @@ In the parent directory of you urfd repository: ```bash git clone https://github.com/n7tae/tcd.git +cd tcd ``` To be perfectly clear, the urfd reflector repository clone and this clone **must be in the same directory**. -## Configuring, compiling, installing and other activities +## Configuring and compiling -All other activities will be performed by the ./rconfig and ./radmin scripts in your urfd repo. +Copy the three configuration files: `cp config/* .` +Use your favorite text editor: +- *tcd.mk* defines some compile time options. Once you've set these options, do `make` +- *tcd.ini* defines run-time options. It is especially imporant that the `Transcoded` line for the tcd.ini file is exactly the same as the same line in the urfd.ini file! +- *tcd.service* is the systemd service file. be sure the `ExecStart` line will successfully start tcd! + +## Installing and other activities + +If the transcoder is local, all other activities will be performed by the ./radmin scripts in your urfd repo. ## 73 diff --git a/config/tcd.ini b/config/tcd.ini new file mode 100644 index 0000000..05410a7 --- /dev/null +++ b/config/tcd.ini @@ -0,0 +1,15 @@ +# Transcoder Ini file +# +# this is a comment + +# VERY IMPORTANT: This need to be idential to the same line in the urfd ini file! +# this will either be a single letter (for DVSI-3000), or +# three letters (for DVSI-3003) +Transcoded = A + +# Gain (or attenuation) in dB should be integers in the range of +/-90 +# Something is probably terribly wrong if you need more than +/-20 +DStarGainIn = 0 +DStarGainOut = 0 +DmrYsfGainIn = 0 +DmrYsfGainOut = 0 diff --git a/config/tcd.mk b/config/tcd.mk new file mode 100644 index 0000000..8e5cfe8 --- /dev/null +++ b/config/tcd.mk @@ -0,0 +1,9 @@ +# This is where the executable will be installed `make install` +BINDIR = /usr/local/bin + +# set to true to build a transcoder that supports gdb +debug = false + +# set to true to use the md-380 software vocoder +# this will only work on an arm-based system, like a raspberry pi +swambe2 = false diff --git a/systemd/tcd.service b/config/tcd.service similarity index 83% rename from systemd/tcd.service rename to config/tcd.service index a029840..2481c4d 100644 --- a/systemd/tcd.service +++ b/config/tcd.service @@ -7,7 +7,7 @@ After=systemd-user-session.service network.target Type=simple ExecStartPre=-/sbin/rmmod ftdi_sio ExecStartPre=-/sbin/rmmod usbserial -ExecStart=/usr/local/bin/tcd +ExecStart=/usr/local/bin/tcd /PATH_TO_INI_FILE Restart=always [Install]