aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias P. Braendli <matthias.braendli@mpb.li>2017-11-02 14:11:26 +0100
committerMatthias P. Braendli <matthias.braendli@mpb.li>2017-11-02 14:11:26 +0100
commit450c1e1d29a08326f4a370005bacafd528cd25e7 (patch)
treed9c21863bf6c1c0e2024203a5e1f1731f7e36f18
parent1c5372335338962ccbe9e876467b7e0ea46877ac (diff)
downloaddabmod-450c1e1d29a08326f4a370005bacafd528cd25e7.tar.gz
dabmod-450c1e1d29a08326f4a370005bacafd528cd25e7.tar.bz2
dabmod-450c1e1d29a08326f4a370005bacafd528cd25e7.zip
Create new SDR output abstraction and port Soapy
-rw-r--r--Makefile.am10
-rw-r--r--TODO4
-rw-r--r--src/ConfigParser.cpp3
-rw-r--r--src/ConfigParser.h5
-rw-r--r--src/DabMod.cpp18
-rw-r--r--src/OutputSoapy.cpp287
-rw-r--r--src/OutputSoapy.h138
-rw-r--r--src/TimestampDecoder.h8
-rw-r--r--src/output/SDR.cpp380
-rw-r--r--src/output/SDR.h169
-rw-r--r--src/output/Soapy.cpp223
-rw-r--r--src/output/Soapy.h89
-rw-r--r--src/output/UHD.cpp994
-rw-r--r--src/output/UHD.h244
14 files changed, 2131 insertions, 441 deletions
diff --git a/Makefile.am b/Makefile.am
index 2d84ab6..8ce5f89 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -81,12 +81,14 @@ odr_dabmod_SOURCES = src/DabMod.cpp \
src/OutputZeroMQ.h \
src/TimestampDecoder.h \
src/TimestampDecoder.cpp \
- src/OutputUHD.cpp \
- src/OutputUHD.h \
+ src/output/UHD.cpp \
+ src/output/UHD.h \
src/OutputUHDFeedback.cpp \
src/OutputUHDFeedback.h \
- src/OutputSoapy.cpp \
- src/OutputSoapy.h \
+ src/output/Soapy.cpp \
+ src/output/Soapy.h \
+ src/output/SDR.h \
+ src/output/SDR.cpp \
src/InputMemory.cpp \
src/InputMemory.h \
src/InputFileReader.cpp \
diff --git a/TODO b/TODO
index c5c58f1..3ee9d78 100644
--- a/TODO
+++ b/TODO
@@ -4,6 +4,10 @@ to some degree.
Unless written, no activity has been started on the topics.
+Move staticdelay into another process block
+-------------------------------------------
+It is currently in OutputUHD.
+
TII implementation incomplete
-----------------------------
diff --git a/src/ConfigParser.cpp b/src/ConfigParser.cpp
index 1cc94c0..c9ed212 100644
--- a/src/ConfigParser.cpp
+++ b/src/ConfigParser.cpp
@@ -274,11 +274,12 @@ static void parse_configfile(
#endif
#if defined(HAVE_SOAPYSDR)
else if (output_selected == "soapysdr") {
- auto& outputsoapy_conf = mod_settings.outputsoapy_conf;
+ auto& outputsoapy_conf = mod_settings.sdr_device_config;
outputsoapy_conf.device = pt.get("soapyoutput.device", "");
outputsoapy_conf.masterClockRate = pt.get<long>("soapyoutput.master_clock_rate", 0);
outputsoapy_conf.txgain = pt.get("soapyoutput.txgain", 0.0);
+ outputsoapy_conf.lo_offset = pt.get<double>("soapyoutput.lo_offset", 0.0);
outputsoapy_conf.frequency = pt.get<double>("soapyoutput.frequency", 0);
std::string chan = pt.get<std::string>("soapyoutput.channel", "");
outputsoapy_conf.dabMode = mod_settings.dabMode;
diff --git a/src/ConfigParser.h b/src/ConfigParser.h
index a8d7837..4d936b8 100644
--- a/src/ConfigParser.h
+++ b/src/ConfigParser.h
@@ -34,11 +34,12 @@
#include <string>
#include "GainControl.h"
#include "TII.h"
+#include "output/SDR.h"
#if defined(HAVE_OUTPUT_UHD)
# include "OutputUHD.h"
#endif
#if defined(HAVE_SOAPYSDR)
-# include "OutputSoapy.h"
+# include "output/Soapy.h"
#endif
#define ZMQ_INPUT_MAX_FRAME_QUEUE 500
@@ -88,7 +89,7 @@ struct mod_settings_t {
#endif
#if defined(HAVE_SOAPYSDR)
- OutputSoapyConfig outputsoapy_conf;
+ Output::SDRDeviceConfig sdr_device_config;
#endif
};
diff --git a/src/DabMod.cpp b/src/DabMod.cpp
index 2caba5d..b810f20 100644
--- a/src/DabMod.cpp
+++ b/src/DabMod.cpp
@@ -36,11 +36,12 @@
#include "InputMemory.h"
#include "OutputFile.h"
#include "FormatConverter.h"
+#include "output/SDR.h"
#if defined(HAVE_OUTPUT_UHD)
# include "OutputUHD.h"
#endif
#if defined(HAVE_SOAPYSDR)
-# include "OutputSoapy.h"
+# include "output/Soapy.h"
#endif
#include "OutputZeroMQ.h"
#include "InputReader.h"
@@ -155,8 +156,8 @@ static void printModSettings(const mod_settings_t& mod_settings)
fprintf(stderr, " SoapySDR\n"
" Device: %s\n"
" master_clock_rate: %ld\n",
- mod_settings.outputsoapy_conf.device.c_str(),
- mod_settings.outputsoapy_conf.masterClockRate);
+ mod_settings.sdr_device_config.device.c_str(),
+ mod_settings.sdr_device_config.masterClockRate);
}
#endif
else if (mod_settings.useZeroMQOutput) {
@@ -219,9 +220,10 @@ static shared_ptr<ModOutput> prepare_output(
else if (s.useSoapyOutput) {
/* We normalise the same way as for the UHD output */
s.normalise = 1.0f / normalise_factor;
- s.outputsoapy_conf.sampleRate = s.outputRate;
- output = make_shared<OutputSoapy>(s.outputsoapy_conf);
- rcs.enrol((OutputSoapy*)output.get());
+ s.sdr_device_config.sampleRate = s.outputRate;
+ auto soapydevice = make_shared<Output::Soapy>(s.sdr_device_config);
+ output = make_shared<Output::SDR>(s.sdr_device_config, soapydevice);
+ rcs.enrol((Output::SDR*)output.get());
}
#endif
#if defined(HAVE_ZEROMQ)
@@ -334,7 +336,7 @@ int launch_modulator(int argc, char* argv[])
#endif
#if defined(HAVE_SOAPYSDR)
if (mod_settings.useSoapyOutput) {
- ((OutputSoapy*)output.get())->setETISource(modulator->getEtiSource());
+ ((Output::SDR*)output.get())->setETISource(modulator->getEtiSource());
}
#endif
@@ -428,7 +430,7 @@ int launch_modulator(int argc, char* argv[])
#endif
#if defined(HAVE_SOAPYSDR)
if (mod_settings.useSoapyOutput) {
- ((OutputSoapy*)output.get())->setETISource(modulator->getEtiSource());
+ ((Output::SDR*)output.get())->setETISource(modulator->getEtiSource());
}
#endif
diff --git a/src/OutputSoapy.cpp b/src/OutputSoapy.cpp
deleted file mode 100644
index 3f8db88..0000000
--- a/src/OutputSoapy.cpp
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
- Queen in Right of Canada (Communications Research Center Canada)
-
- Copyright (C) 2017
- Matthias P. Braendli, matthias.braendli@mpb.li
-
- http://opendigitalradio.org
-
-DESCRIPTION:
- It is an output driver using the SoapySDR library that can output to
- many devices.
-*/
-
-/*
- This file is part of ODR-DabMod.
-
- ODR-DabMod 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 3 of the
- License, or (at your option) any later version.
-
- ODR-DabMod 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 ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include "OutputSoapy.h"
-#ifdef HAVE_SOAPYSDR
-
-#include <SoapySDR/Errors.hpp>
-#include <deque>
-#include <chrono>
-
-#include "Log.h"
-#include "Utils.h"
-
-#include <stdio.h>
-
-static const size_t FRAMES_MAX_SIZE = 2;
-
-
-using namespace std;
-
-
-
-OutputSoapy::OutputSoapy(OutputSoapyConfig& config) :
- ModOutput(),
- RemoteControllable("soapy"),
- m_conf(config),
- m_device(nullptr)
-{
- RC_ADD_PARAMETER(txgain, "SoapySDR analog daughterboard TX gain");
- RC_ADD_PARAMETER(freq, "SoapySDR transmission frequency");
- RC_ADD_PARAMETER(overflows, "SoapySDR overflow count [r/o]");
- RC_ADD_PARAMETER(underflows, "SoapySDR underflow count [r/o]");
-
- etiLog.level(info) <<
- "OutputSoapy:Creating the device with: " <<
- config.device;
- try
- {
- m_device = SoapySDR::Device::make(config.device);
- stringstream ss;
- ss << "SoapySDR driver=" << m_device->getDriverKey();
- ss << " hardware=" << m_device->getHardwareKey();
- for (const auto &it : m_device->getHardwareInfo())
- {
- ss << " " << it.first << "=" << it.second;
- }
- }
- catch (const std::exception &ex)
- {
- etiLog.level(error) << "Error making SoapySDR device: " <<
- ex.what();
- throw std::runtime_error("Cannot create SoapySDR output");
- }
-
- m_device->setMasterClockRate(config.masterClockRate);
- etiLog.level(info) << "SoapySDR master clock rate set to " <<
- m_device->getMasterClockRate()/1000.0 << " kHz";
-
- m_device->setSampleRate(SOAPY_SDR_TX, 0, m_conf.sampleRate);
- etiLog.level(info) << "OutputSoapySDR:Actual TX rate: " <<
- m_device->getSampleRate(SOAPY_SDR_TX, 0) / 1000.0 <<
- " ksps.";
-
- m_device->setFrequency(SOAPY_SDR_TX, 0, m_conf.frequency);
- m_conf.frequency = m_device->getFrequency(SOAPY_SDR_TX, 0);
- etiLog.level(info) << "OutputSoapySDR:Actual frequency: " <<
- m_conf.frequency / 1000.0 <<
- " kHz.";
-
- m_device->setGain(SOAPY_SDR_TX, 0, m_conf.txgain);
- etiLog.level(info) << "OutputSoapySDR:Actual tx gain: " <<
- m_device->getGain(SOAPY_SDR_TX, 0);
-
-}
-
-OutputSoapy::~OutputSoapy()
-{
- m_worker.stop();
- if (m_device != nullptr) {
- SoapySDR::Device::unmake(m_device);
- }
-}
-
-void SoapyWorker::stop()
-{
- running = false;
- queue.push({});
- if (m_thread.joinable()) {
- m_thread.join();
- }
-}
-
-void SoapyWorker::start(SoapySDR::Device *device)
-{
- m_device = device;
- underflows = 0;
- overflows = 0;
- running = true;
- m_thread = std::thread(&SoapyWorker::process_start, this);
-}
-
-void SoapyWorker::process_start()
-{
- // Set thread priority to realtime
- if (int ret = set_realtime_prio(1)) {
- etiLog.level(error) << "Could not set priority for SoapySDR worker:" << ret;
- }
-
- set_thread_name("soapyworker");
-
- std::vector<size_t> channels;
- channels.push_back(0);
- auto stream = m_device->setupStream(SOAPY_SDR_TX, "CF32", channels);
- m_device->activateStream(stream);
- process(stream);
- m_device->closeStream(stream);
- running = false;
- etiLog.level(warn) << "SoapySDR worker terminated";
-}
-
-void SoapyWorker::process(SoapySDR::Stream *stream)
-{
- while (running) {
- struct SoapyWorkerFrameData frame;
- queue.wait_and_pop(frame);
-
- // The frame buffer contains bytes representing FC32 samples
- const complexf *buf = reinterpret_cast<complexf*>(frame.buf.data());
- const size_t numSamples = frame.buf.size() / sizeof(complexf);
- if ((frame.buf.size() % sizeof(complexf)) != 0) {
- throw std::runtime_error("OutputSoapy: invalid buffer size");
- }
-
- // Stream MTU is in samples, not bytes.
- const size_t mtu = m_device->getStreamMTU(stream);
-
- size_t num_acc_samps = 0;
- while (running && (num_acc_samps < numSamples)) {
- const void *buffs[1];
- buffs[0] = buf + num_acc_samps;
-
- const size_t samps_to_send = std::min(numSamples - num_acc_samps, mtu);
-
- int flags = 0;
-
- auto ret = m_device->writeStream(stream, buffs, samps_to_send, flags);
-
- if (ret == SOAPY_SDR_TIMEOUT) {
- continue;
- }
- else if (ret == SOAPY_SDR_OVERFLOW) {
- overflows++;
- continue;
- }
- else if (ret == SOAPY_SDR_UNDERFLOW) {
- underflows++;
- continue;
- }
-
- if (ret < 0) {
- etiLog.level(error) << "Unexpected stream error " <<
- SoapySDR::errToStr(ret);
- running = false;
- }
-
- num_acc_samps += ret;
- }
- }
-}
-
-int OutputSoapy::process(Buffer* dataIn)
-{
- if (first_run) {
- m_worker.start(m_device);
- first_run = false;
- }
- else if (!m_worker.running) {
- etiLog.level(error) << "OutputSoapy: worker thread died";
- throw std::runtime_error("Fault in OutputSoapy");
- }
-
- SoapyWorkerFrameData frame;
- m_eti_source->calculateTimestamp(frame.ts);
-
-
- if (frame.ts.fct == -1) {
- etiLog.level(info) <<
- "OutputSoapy: dropping one frame with invalid FCT";
- }
- else {
- const uint8_t* pInData = reinterpret_cast<uint8_t*>(dataIn->getData());
- frame.buf.resize(dataIn->getLength());
- std::copy(pInData, pInData + dataIn->getLength(),
- frame.buf.begin());
- m_worker.queue.push_wait_if_full(frame, FRAMES_MAX_SIZE);
- }
-
- return dataIn->getLength();
-}
-
-
-void OutputSoapy::setETISource(EtiSource *etiSource)
-{
- m_eti_source = etiSource;
-}
-
-void OutputSoapy::set_parameter(const string& parameter, const string& value)
-{
- stringstream ss(value);
- ss.exceptions ( stringstream::failbit | stringstream::badbit );
-
- if (parameter == "txgain") {
- ss >> m_conf.txgain;
- m_device->setGain(SOAPY_SDR_TX, 0, m_conf.txgain);
- }
- else if (parameter == "freq") {
- ss >> m_conf.frequency;
- m_device->setFrequency(SOAPY_SDR_TX, 0, m_conf.frequency);
- m_conf.frequency = m_device->getFrequency(SOAPY_SDR_TX, 0);
- }
- else if (parameter == "underflows") {
- throw ParameterError("Parameter 'underflows' is read-only");
- }
- else if (parameter == "overflows") {
- throw ParameterError("Parameter 'overflows' is read-only");
- }
- else {
- stringstream ss;
- ss << "Parameter '" << parameter
- << "' is not exported by controllable " << get_rc_name();
- throw ParameterError(ss.str());
- }
-}
-
-const string OutputSoapy::get_parameter(const string& parameter) const
-{
- stringstream ss;
- if (parameter == "txgain") {
- ss << m_conf.txgain;
- }
- else if (parameter == "freq") {
- ss << m_conf.frequency;
- }
- else if (parameter == "underflows") {
- ss << m_worker.underflows;
- }
- else if (parameter == "overflows") {
- ss << m_worker.overflows;
- }
- else {
- ss << "Parameter '" << parameter <<
- "' is not exported by controllable " << get_rc_name();
- throw ParameterError(ss.str());
- }
- return ss.str();
-}
-
-#endif // HAVE_SOAPYSDR
-
diff --git a/src/OutputSoapy.h b/src/OutputSoapy.h
deleted file mode 100644
index 230f11b..0000000
--- a/src/OutputSoapy.h
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
- Queen in Right of Canada (Communications Research Center Canada)
-
- Copyright (C) 2017
- Matthias P. Braendli, matthias.braendli@mpb.li
-
- http://opendigitalradio.org
-
-DESCRIPTION:
- It is an output driver using the SoapySDR library that can output to
- many devices.
-*/
-
-/*
- This file is part of ODR-DabMod.
-
- ODR-DabMod 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 3 of the
- License, or (at your option) any later version.
-
- ODR-DabMod 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 ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#ifdef HAVE_CONFIG_H
-# include <config.h>
-#endif
-
-#ifdef HAVE_SOAPYSDR
-#include <SoapySDR/Version.hpp>
-#include <SoapySDR/Modules.hpp>
-#include <SoapySDR/Registry.hpp>
-#include <SoapySDR/Device.hpp>
-
-#include <string>
-#include <memory>
-
-#include "ModPlugin.h"
-#include "EtiReader.h"
-#include "RemoteControl.h"
-#include "ThreadsafeQueue.h"
-
-typedef std::complex<float> complexf;
-
-/* This structure is used as initial configuration for the Soapy output.
- * It must also contain all remote-controllable settings, otherwise
- * they will get lost on a modulator restart. */
-struct OutputSoapyConfig {
- std::string device;
-
- long masterClockRate = 32768000;
- unsigned sampleRate = 2048000;
- double frequency = 0.0;
- double txgain = 0.0;
- unsigned dabMode = 0;
-};
-
-// Each frame contains one OFDM frame, and its
-// associated timestamp
-struct SoapyWorkerFrameData {
- // Buffer holding frame data
- std::vector<uint8_t> buf;
-
- // A full timestamp contains a TIST according to standard
- // and time information within MNSC with tx_second.
- struct frame_timestamp ts;
-};
-
-class SoapyWorker
-{
- public:
- ThreadsafeQueue<SoapyWorkerFrameData> queue;
- SoapySDR::Device *m_device;
- std::atomic<bool> running;
- size_t underflows;
- size_t overflows;
-
- SoapyWorker() {}
- SoapyWorker(const SoapyWorker&) = delete;
- SoapyWorker operator=(const SoapyWorker&) = delete;
- ~SoapyWorker() { stop(); }
-
- void start(SoapySDR::Device *device);
- void stop(void);
-
- private:
- std::thread m_thread;
-
- void process_start(void);
- void process(SoapySDR::Stream *stream);
-};
-
-class OutputSoapy: public ModOutput, public RemoteControllable
-{
- public:
- OutputSoapy(OutputSoapyConfig& config);
- OutputSoapy(const OutputSoapy& other) = delete;
- OutputSoapy& operator=(const OutputSoapy& other) = delete;
- ~OutputSoapy();
-
- int process(Buffer* dataIn);
-
- const char* name() { return "OutputSoapy"; }
-
- void setETISource(EtiSource *etiSource);
-
- /*********** REMOTE CONTROL ***************/
-
- /* Base function to set parameters. */
- virtual void set_parameter(const std::string& parameter,
- const std::string& value);
-
- /* Getting a parameter always returns a string. */
- virtual const std::string get_parameter(
- const std::string& parameter) const;
-
-
- protected:
- SoapyWorker m_worker;
- EtiSource *m_eti_source;
- OutputSoapyConfig& m_conf;
-
- SoapySDR::Device *m_device;
-
- bool first_run = true;
-};
-
-
-#endif //HAVE_SOAPYSDR
diff --git a/src/TimestampDecoder.h b/src/TimestampDecoder.h
index e0dee2a..34d862f 100644
--- a/src/TimestampDecoder.h
+++ b/src/TimestampDecoder.h
@@ -43,7 +43,7 @@ struct frame_timestamp
uint32_t timestamp_sec;
uint32_t timestamp_pps; // In units of 1/16384000 s
- bool timestamp_valid;
+ bool timestamp_valid = false;
bool timestamp_refresh;
frame_timestamp() = default;
@@ -88,6 +88,12 @@ struct frame_timestamp
return timestamp_pps / 16384000.0;
}
+ double get_real_secs() const {
+ double t = timestamp_sec;
+ t += timestamp_pps;
+ return t;
+ }
+
void print(const char* t)
{
fprintf(stderr,
diff --git a/src/output/SDR.cpp b/src/output/SDR.cpp
new file mode 100644
index 0000000..c1183c4
--- /dev/null
+++ b/src/output/SDR.cpp
@@ -0,0 +1,380 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+ Queen in Right of Canada (Communications Research Center Canada)
+
+ Copyright (C) 2017
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://opendigitalradio.org
+ */
+/*
+ This file is part of ODR-DabMod.
+
+ ODR-DabMod 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 3 of the
+ License, or (at your option) any later version.
+
+ ODR-DabMod 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 ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "output/SDR.h"
+
+#include "PcDebug.h"
+#include "Log.h"
+#include "RemoteControl.h"
+#include "Utils.h"
+
+#include <cmath>
+#include <iostream>
+#include <assert.h>
+#include <stdexcept>
+#include <stdio.h>
+#include <time.h>
+#include <errno.h>
+#include <unistd.h>
+#include <pthread.h>
+
+using namespace std;
+
+namespace Output {
+
+// Maximum number of frames that can wait in frames
+static constexpr size_t FRAMES_MAX_SIZE = 8;
+
+// If the timestamp is further in the future than
+// 100 seconds, abort
+static constexpr double TIMESTAMP_ABORT_FUTURE = 100;
+
+// Add a delay to increase buffers when
+// frames are too far in the future
+static constexpr double TIMESTAMP_MARGIN_FUTURE = 0.5;
+
+SDR::SDR(SDRDeviceConfig& config, std::shared_ptr<SDRDevice> device) :
+ ModOutput(),
+ RemoteControllable("sdr"),
+ m_config(config),
+ m_running(false),
+ m_device(device)
+{
+ m_device_thread = std::thread(&SDR::process_thread_entry, this);
+}
+
+SDR::~SDR()
+{
+ stop();
+}
+
+void SDR::stop()
+{
+ m_running.store(false);
+
+ FrameData end_marker;
+ end_marker.buf.resize(0);
+ m_queue.push(end_marker);
+
+ if (m_device_thread.joinable()) {
+ m_device_thread.join();
+ }
+}
+
+int SDR::process(Buffer *dataIn)
+{
+ if (m_device) {
+ if (not m_device->is_clk_source_ok()) {
+ // Ignore frame
+ return dataIn->getLength();
+ }
+
+ FrameData frame;
+ frame.buf.resize(dataIn->getLength());
+
+ const uint8_t* pDataIn = (uint8_t*)dataIn->getData();
+ std::copy(pDataIn, pDataIn + dataIn->getLength(),
+ frame.buf.begin());
+
+ m_eti_source->calculateTimestamp(frame.ts);
+
+ // TODO check device running
+
+ if (frame.ts.fct == -1) {
+ etiLog.level(info) <<
+ "OutputUHD: dropping one frame with invalid FCT";
+ }
+ else {
+ // TODO setup Feedback and set tx_frame
+
+ size_t num_frames = m_queue.push_wait_if_full(frame,
+ FRAMES_MAX_SIZE);
+ etiLog.log(trace, "SDR,push %zu", num_frames);
+ }
+ }
+ else {
+ // Ignore frame
+ }
+
+ return dataIn->getLength();
+}
+
+void SDR::process_thread_entry()
+{
+ // Set thread priority to realtime
+ if (int ret = set_realtime_prio(1)) {
+ etiLog.level(error) << "Could not set priority for SDR device thread:" << ret;
+ }
+
+ set_thread_name("sdrdevice");
+
+ last_tx_time_initialised = false;
+
+ size_t last_num_underflows = 0;
+ size_t pop_prebuffering = FRAMES_MAX_SIZE;
+
+ while (m_running.load()) {
+ struct FrameData frame;
+ etiLog.log(trace, "SDR,wait");
+ m_queue.wait_and_pop(frame, pop_prebuffering);
+ etiLog.log(trace, "SDR,pop");
+
+ if (m_running.load() == false or frame.buf.empty()) {
+ break;
+ }
+
+ if (m_device) {
+ handle_frame(frame);
+
+ const auto rs = m_device->get_run_statistics();
+
+ /* Ensure we fill frames after every underrun and
+ * at startup to reduce underrun likelihood. */
+ if (last_num_underflows < rs.num_underruns) {
+ pop_prebuffering = FRAMES_MAX_SIZE;
+ }
+ else {
+ pop_prebuffering = 1;
+ }
+
+ last_num_underflows = rs.num_underruns;
+ }
+
+ }
+
+ m_running.store(false);
+ etiLog.level(warn) << "SDR Device thread terminated";
+}
+
+const char* SDR::name()
+{
+ if (m_device) {
+ m_name = "OutputSDR(";
+ m_name += m_device->device_name();
+ m_name += ")";
+ }
+ else {
+ m_name = "OutputSDR(<no device>)";
+ }
+ return m_name.c_str();
+}
+
+void SDR::setETISource(EtiSource *etiSource)
+{
+ m_eti_source = etiSource;
+}
+
+void SDR::handle_frame(struct FrameData& frame)
+{
+ // Assumes m_device is valid
+
+ constexpr double tx_timeout = 20.0;
+
+ if (not m_device->is_clk_source_ok()) {
+ return;
+ }
+
+ double device_time = m_device->get_real_secs();
+ bool timestamp_discontinuity = false;
+ const auto& time_spec = frame.ts;
+
+ if (sourceContainsTimestamp) {
+ // Tx time from MNSC and TIST
+ const uint32_t tx_second = frame.ts.timestamp_sec;
+ const uint32_t tx_pps = frame.ts.timestamp_pps;
+
+ if (not frame.ts.timestamp_valid) {
+ /* We have not received a full timestamp through
+ * MNSC. We sleep through the frame.
+ */
+ etiLog.level(info) <<
+ "OutputSDR: Throwing sample " << frame.ts.fct <<
+ " away: incomplete timestamp " << tx_second <<
+ " / " << tx_pps;
+ return;
+ }
+
+ if (last_tx_time_initialised) {
+ const size_t sizeIn = frame.buf.size() / sizeof(complexf);
+ uint64_t increment = (uint64_t)sizeIn * 16384000ul /
+ (uint64_t)m_config.sampleRate;
+ // samps * ticks/s / (samps/s)
+ // (samps * ticks * s) / (s * samps)
+ // ticks
+
+ uint32_t expected_sec = last_tx_second + increment / 16384000ul;
+ uint32_t expected_pps = last_tx_pps + increment % 16384000ul;
+
+ while (expected_pps >= 16384000) {
+ expected_sec++;
+ expected_pps -= 16384000;
+ }
+
+ if (expected_sec != tx_second or expected_pps != tx_pps) {
+ etiLog.level(warn) << "OutputSDR: timestamp irregularity!" <<
+ std::fixed <<
+ " Expected " <<
+ expected_sec << "+" << (double)expected_pps/16384000.0 <<
+ "(" << expected_pps << ")" <<
+ " Got " <<
+ tx_second << "+" << (double)tx_pps/16384000.0 <<
+ "(" << tx_pps << ")";
+
+ timestamp_discontinuity = true;
+ }
+ }
+
+ last_tx_second = tx_second;
+ last_tx_pps = tx_pps;
+ last_tx_time_initialised = true;
+
+ const double pps_offset = tx_pps / 16384000.0;
+
+ etiLog.log(trace, "SDR,tist %f", time_spec.get_real_secs());
+
+ if (time_spec.get_real_secs() + tx_timeout < device_time) {
+ etiLog.level(warn) <<
+ "OutputSDR: Timestamp in the past! offset: " <<
+ std::fixed <<
+ time_spec.get_real_secs() - device_time <<
+ " (" << device_time << ")"
+ " frame " << frame.ts.fct <<
+ ", tx_second " << tx_second <<
+ ", pps " << pps_offset;
+ return;
+ }
+
+ if (time_spec.get_real_secs() > device_time + TIMESTAMP_ABORT_FUTURE) {
+ etiLog.level(error) <<
+ "OutputSDR: Timestamp way too far in the future! offset: " <<
+ std::fixed <<
+ time_spec.get_real_secs() - device_time;
+ throw std::runtime_error("Timestamp error. Aborted.");
+ }
+ }
+ else { // !sourceContainsTimestamp
+ if (m_config.muting or m_config.muteNoTimestamps) {
+ /* There was some error decoding the timestamp */
+ if (m_config.muting) {
+ etiLog.log(info,
+ "OutputSDR: Muting sample %d requested\n",
+ frame.ts.fct);
+ }
+ else {
+ etiLog.log(info,
+ "OutputSDR: Muting sample %d : no timestamp\n",
+ frame.ts.fct);
+ }
+ return;
+ }
+ }
+
+ if (timestamp_discontinuity) {
+ frame.ts.timestamp_refresh = true;
+ }
+ m_device->transmit_frame(frame);
+}
+
+// =======================================
+// Remote Control
+// =======================================
+void SDR::set_parameter(const string& parameter, const string& value)
+{
+ stringstream ss(value);
+ ss.exceptions ( stringstream::failbit | stringstream::badbit );
+
+ if (parameter == "txgain") {
+ ss >> m_config.txgain;
+ m_device->set_txgain(m_config.txgain);
+ }
+ else if (parameter == "rxgain") {
+ ss >> m_config.rxgain;
+ // TODO myUsrp->set_rx_gain(m_config.rxgain);
+ throw ParameterError("Parameter " + parameter + " is TODO.");
+ }
+ else if (parameter == "freq") {
+ ss >> m_config.frequency;
+ m_device->tune(m_config.lo_offset, m_config.frequency);
+ m_config.frequency = m_device->get_tx_freq();
+ }
+ else if (parameter == "muting") {
+ ss >> m_config.muting;
+ }
+ else if (parameter == "underruns" or
+ parameter == "latepackets" or
+ parameter == "frames") {
+ throw ParameterError("Parameter " + parameter + " is read-only.");
+ }
+ else {
+ stringstream ss;
+ ss << "Parameter '" << parameter
+ << "' is not exported by controllable " << get_rc_name();
+ throw ParameterError(ss.str());
+ }
+}
+
+const string SDR::get_parameter(const string& parameter) const
+{
+ stringstream ss;
+ if (parameter == "txgain") {
+ ss << m_config.txgain;
+ }
+ else if (parameter == "rxgain") {
+ ss << m_config.rxgain;
+ }
+ else if (parameter == "freq") {
+ ss << m_config.frequency;
+ }
+ else if (parameter == "muting") {
+ ss << m_config.muting;
+ }
+ else if (parameter == "underruns" or
+ parameter == "latepackets" or
+ parameter == "frames" ) {
+ if (not m_device) {
+ throw ParameterError("OutputSDR has no device");
+ }
+ const auto stat = m_device->get_run_statistics();
+
+ if (parameter == "underruns") {
+ ss << stat.num_underruns;
+ }
+ else if (parameter == "latepackets") {
+ ss << stat.num_late_packets;
+ }
+ else if (parameter == "frames") {
+ ss << stat.num_frames_modulated;
+ }
+ }
+ else {
+ ss << "Parameter '" << parameter <<
+ "' is not exported by controllable " << get_rc_name();
+ throw ParameterError(ss.str());
+ }
+ return ss.str();
+}
+
+} // namespace Output
diff --git a/src/output/SDR.h b/src/output/SDR.h
new file mode 100644
index 0000000..ab23aa1
--- /dev/null
+++ b/src/output/SDR.h
@@ -0,0 +1,169 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+ Queen in Right of Canada (Communications Research Center Canada)
+
+ Copyright (C) 2017
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://opendigitalradio.org
+
+DESCRIPTION:
+ Common interface for all SDR outputs
+*/
+
+/*
+ This file is part of ODR-DabMod.
+
+ ODR-DabMod 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 3 of the
+ License, or (at your option) any later version.
+
+ ODR-DabMod 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 ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#include "ModPlugin.h"
+#include "EtiReader.h"
+
+namespace Output {
+
+using complexf = std::complex<float>;
+
+enum refclk_lock_loss_behaviour_t { CRASH, IGNORE };
+
+/* This structure is used as initial configuration for all SDR devices.
+ * It must also contain all remote-controllable settings, otherwise
+ * they will get lost on a modulator restart. */
+struct SDRDeviceConfig {
+ std::string device;
+
+ long masterClockRate = 32768000;
+ unsigned sampleRate = 2048000;
+ double frequency = 0.0;
+ double lo_offset = 0.0;
+ double txgain = 0.0;
+ double rxgain = 0.0;
+ bool enableSync = false;
+
+ // When working with timestamps, mute the frames that
+ // do not have a timestamp
+ bool muteNoTimestamps = false;
+ unsigned dabMode = 0;
+ unsigned maxGPSHoldoverTime = 0;
+
+ /* allowed values for UHD : auto, int, sma, mimo */
+ std::string refclk_src;
+
+ /* allowed values for UHD : int, sma, mimo */
+ std::string pps_src;
+
+ /* allowed values for UHD : pos, neg */
+ std::string pps_polarity;
+
+ /* What to do when the reference clock PLL loses lock */
+ refclk_lock_loss_behaviour_t refclk_lock_loss_behaviour;
+
+ // muting can only be changed using the remote control
+ bool muting = false;
+
+ // TCP port on which to serve TX and RX samples for the
+ // digital pre distortion learning tool
+ uint16_t dpdFeedbackServerPort = 0;
+};
+
+// Each frame contains one OFDM frame, and its
+// associated timestamp
+struct FrameData {
+ // Buffer holding frame data
+ std::vector<uint8_t> buf;
+
+ // A full timestamp contains a TIST according to standard
+ // and time information within MNSC with tx_second.
+ struct frame_timestamp ts;
+};
+
+
+// All SDR Devices must implement the SDRDevice interface
+class SDRDevice {
+ public:
+ struct RunStatistics {
+ size_t num_underruns;
+ size_t num_late_packets;
+ size_t num_overruns;
+ size_t num_frames_modulated; //TODO increment
+ };
+
+ // TODO make some functions const
+ virtual void tune(double lo_offset, double frequency) = 0;
+ virtual double get_tx_freq(void) = 0;
+ virtual void set_txgain(double txgain) = 0;
+ virtual double get_txgain(void) = 0;
+ virtual void transmit_frame(const struct FrameData& frame) = 0;
+ virtual RunStatistics get_run_statistics(void) = 0;
+ virtual double get_real_secs(void) = 0;
+
+
+ // Return true if GPS and reference clock inputs are ok
+ virtual bool is_clk_source_ok(void) = 0;
+
+ virtual const char* device_name(void) = 0;
+};
+
+class SDR : public ModOutput, public RemoteControllable {
+ public:
+ SDR(SDRDeviceConfig& config, std::shared_ptr<SDRDevice> device);
+ SDR(const SDR& other) = delete;
+ SDR operator=(const SDR& other) = delete;
+ ~SDR();
+
+ virtual int process(Buffer *dataIn) override;
+
+ virtual const char* name() override;
+
+ void setETISource(EtiSource *etiSource);
+
+ /*********** REMOTE CONTROL ***************/
+
+ /* Base function to set parameters. */
+ virtual void set_parameter(const std::string& parameter,
+ const std::string& value) override;
+
+ /* Getting a parameter always returns a string. */
+ virtual const std::string get_parameter(
+ const std::string& parameter) const override;
+
+ private:
+ void stop(void);
+ void process_thread_entry(void);
+ void handle_frame(struct FrameData &frame);
+
+ SDRDeviceConfig& m_config;
+
+ std::atomic<bool> m_running;
+ std::thread m_device_thread;
+ ThreadsafeQueue<FrameData> m_queue;
+
+ std::shared_ptr<SDRDevice> m_device;
+ std::string m_name;
+
+ EtiSource *m_eti_source = nullptr;
+ bool sourceContainsTimestamp = false;
+ bool last_tx_time_initialised = false;
+ uint32_t last_tx_second = 0;
+ uint32_t last_tx_pps = 0;
+};
+
+}
+
diff --git a/src/output/Soapy.cpp b/src/output/Soapy.cpp
new file mode 100644
index 0000000..d6b5953
--- /dev/null
+++ b/src/output/Soapy.cpp
@@ -0,0 +1,223 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+ Queen in Right of Canada (Communications Research Center Canada)
+
+ Copyright (C) 2017
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://opendigitalradio.org
+
+DESCRIPTION:
+ It is an output driver using the SoapySDR library that can output to
+ many devices.
+*/
+
+/*
+ This file is part of ODR-DabMod.
+
+ ODR-DabMod 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 3 of the
+ License, or (at your option) any later version.
+
+ ODR-DabMod 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 ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "output/Soapy.h"
+
+#ifdef HAVE_SOAPYSDR
+
+#include <SoapySDR/Errors.hpp>
+#include <chrono>
+#include <cstdio>
+
+#include "Log.h"
+#include "Utils.h"
+
+using namespace std;
+
+namespace Output {
+
+static constexpr size_t FRAMES_MAX_SIZE = 2;
+
+Soapy::Soapy(SDRDeviceConfig& config) :
+ SDRDevice(),
+ m_conf(config)
+{
+ etiLog.level(info) <<
+ "Soapy:Creating the device with: " <<
+ m_conf.device;
+
+ try {
+ m_device = SoapySDR::Device::make(m_conf.device);
+ stringstream ss;
+ ss << "SoapySDR driver=" << m_device->getDriverKey();
+ ss << " hardware=" << m_device->getHardwareKey();
+ for (const auto &it : m_device->getHardwareInfo()) {
+ ss << " " << it.first << "=" << it.second;
+ }
+ }
+ catch (const std::exception &ex) {
+ etiLog.level(error) << "Error making SoapySDR device: " <<
+ ex.what();
+ throw std::runtime_error("Cannot create SoapySDR output");
+ }
+
+ m_device->setMasterClockRate(m_conf.masterClockRate);
+ etiLog.level(info) << "SoapySDR master clock rate set to " <<
+ m_device->getMasterClockRate()/1000.0 << " kHz";
+
+ m_device->setSampleRate(SOAPY_SDR_TX, 0, m_conf.sampleRate);
+ etiLog.level(info) << "SoapySDR:Actual TX rate: " <<
+ m_device->getSampleRate(SOAPY_SDR_TX, 0) / 1000.0 <<
+ " ksps.";
+
+ tune(m_conf.lo_offset, m_conf.frequency);
+ m_conf.frequency = m_device->getFrequency(SOAPY_SDR_TX, 0);
+ etiLog.level(info) << "SoapySDR:Actual frequency: " <<
+ m_conf.frequency / 1000.0 <<
+ " kHz.";
+
+ m_device->setGain(SOAPY_SDR_TX, 0, m_conf.txgain);
+ etiLog.level(info) << "SoapySDR:Actual tx gain: " <<
+ m_device->getGain(SOAPY_SDR_TX, 0);
+
+ std::vector<size_t> channels;
+ channels.push_back(0);
+ m_stream = m_device->setupStream(SOAPY_SDR_TX, "CF32", channels);
+ m_device->activateStream(m_stream);
+}
+
+Soapy::~Soapy()
+{
+ if (m_device != nullptr) {
+ if (m_stream != nullptr) {
+ m_device->closeStream(m_stream);
+ }
+ SoapySDR::Device::unmake(m_device);
+ }
+}
+
+void Soapy::tune(double lo_offset, double frequency)
+{
+ if (not m_device) throw runtime_error("Soapy device not set up");
+
+ SoapySDR::Kwargs offset_arg;
+ offset_arg["OFFSET"] = to_string(lo_offset);
+ m_device->setFrequency(SOAPY_SDR_TX, 0, m_conf.frequency, offset_arg);
+}
+
+double Soapy::get_tx_freq(void)
+{
+ if (not m_device) throw runtime_error("Soapy device not set up");
+
+ // TODO lo offset
+ return m_device->getFrequency(SOAPY_SDR_TX, 0);
+}
+
+void Soapy::set_txgain(double txgain)
+{
+ m_conf.txgain = txgain;
+ if (not m_device) throw runtime_error("Soapy device not set up");
+ m_device->setGain(SOAPY_SDR_TX, 0, m_conf.txgain);
+}
+
+double Soapy::get_txgain(void)
+{
+ if (not m_device) throw runtime_error("Soapy device not set up");
+ return m_device->getGain(SOAPY_SDR_TX, 0);
+}
+
+SDRDevice::RunStatistics Soapy::get_run_statistics(void)
+{
+ RunStatistics rs;
+ rs.num_underruns = underflows;
+ rs.num_overruns = overflows;
+ rs.num_late_packets = late_packets;
+ rs.num_frames_modulated = num_frames_modulated;
+ return rs;
+}
+
+
+double Soapy::get_real_secs(void)
+{
+ if (m_device) {
+ long long time_ns = m_device->getHardwareTime();
+ return time_ns / 1e9;
+ }
+ else {
+ return 0.0;
+ }
+}
+
+bool Soapy::is_clk_source_ok()
+{
+ // TODO
+ return true;
+}
+
+const char* Soapy::device_name(void)
+{
+ return "Soapy";
+}
+
+void Soapy::transmit_frame(const struct FrameData& frame)
+{
+ if (not m_device) throw runtime_error("Soapy device not set up");
+
+ // TODO timestamps
+
+ // The frame buffer contains bytes representing FC32 samples
+ const complexf *buf = reinterpret_cast<const complexf*>(frame.buf.data());
+ const size_t numSamples = frame.buf.size() / sizeof(complexf);
+ if ((frame.buf.size() % sizeof(complexf)) != 0) {
+ throw std::runtime_error("Soapy: invalid buffer size");
+ }
+
+ // Stream MTU is in samples, not bytes.
+ const size_t mtu = m_device->getStreamMTU(m_stream);
+
+ size_t num_acc_samps = 0;
+ while (num_acc_samps < numSamples) {
+ const void *buffs[1];
+ buffs[0] = buf + num_acc_samps;
+
+ const size_t samps_to_send = std::min(numSamples - num_acc_samps, mtu);
+
+ int flags = 0;
+
+ auto ret = m_device->writeStream(m_stream, buffs, samps_to_send, flags);
+
+ if (ret == SOAPY_SDR_TIMEOUT) {
+ continue;
+ }
+ else if (ret == SOAPY_SDR_OVERFLOW) {
+ overflows++;
+ continue;
+ }
+ else if (ret == SOAPY_SDR_UNDERFLOW) {
+ underflows++;
+ continue;
+ }
+
+ if (ret < 0) {
+ etiLog.level(error) << "Unexpected stream error " <<
+ SoapySDR::errToStr(ret);
+ throw std::runtime_error("Fault in Soapy");
+ }
+
+ num_acc_samps += ret;
+ }
+}
+
+} // namespace Output
+
+#endif // HAVE_SOAPYSDR
+
+
diff --git a/src/output/Soapy.h b/src/output/Soapy.h
new file mode 100644
index 0000000..97076b5
--- /dev/null
+++ b/src/output/Soapy.h
@@ -0,0 +1,89 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+ Queen in Right of Canada (Communications Research Center Canada)
+
+ Copyright (C) 2017
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://opendigitalradio.org
+
+DESCRIPTION:
+ It is an output driver using the SoapySDR library that can output to
+ many devices.
+*/
+
+/*
+ This file is part of ODR-DabMod.
+
+ ODR-DabMod 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 3 of the
+ License, or (at your option) any later version.
+
+ ODR-DabMod 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 ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#ifdef HAVE_SOAPYSDR
+#include <SoapySDR/Version.hpp>
+#include <SoapySDR/Modules.hpp>
+#include <SoapySDR/Registry.hpp>
+#include <SoapySDR/Device.hpp>
+
+#include <string>
+#include <memory>
+
+#include "output/SDR.h"
+#include "ModPlugin.h"
+#include "EtiReader.h"
+#include "RemoteControl.h"
+#include "ThreadsafeQueue.h"
+
+namespace Output {
+
+class Soapy : public Output::SDRDevice
+{
+ public:
+ Soapy(SDRDeviceConfig& config);
+ Soapy(const Soapy& other) = delete;
+ Soapy& operator=(const Soapy& other) = delete;
+ ~Soapy();
+
+ virtual void tune(double lo_offset, double frequency) override;
+ virtual double get_tx_freq(void) override;
+ virtual void set_txgain(double txgain) override;
+ virtual double get_txgain(void) override;
+ virtual void transmit_frame(const struct FrameData& frame) override;
+ virtual RunStatistics get_run_statistics(void) override;
+ virtual double get_real_secs(void) override;
+
+ // Return true if GPS and reference clock inputs are ok
+ virtual bool is_clk_source_ok(void) override;
+ virtual const char* device_name(void) override;
+
+ private:
+ SDRDeviceConfig& m_conf;
+ SoapySDR::Device *m_device = nullptr;
+ SoapySDR::Stream *m_stream = nullptr;
+
+ size_t underflows = 0;
+ size_t overflows = 0;
+ size_t late_packets = 0;
+ size_t num_frames_modulated = 0; //TODO increment
+};
+
+} // namespace Output
+
+#endif //HAVE_SOAPYSDR
+
diff --git a/src/output/UHD.cpp b/src/output/UHD.cpp
new file mode 100644
index 0000000..4942f3f
--- /dev/null
+++ b/src/output/UHD.cpp
@@ -0,0 +1,994 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+ Queen in Right of Canada (Communications Research Center Canada)
+
+ Copyright (C) 2017
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://opendigitalradio.org
+ */
+/*
+ This file is part of ODR-DabMod.
+
+ ODR-DabMod 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 3 of the
+ License, or (at your option) any later version.
+
+ ODR-DabMod 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 ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "OutputUHD.h"
+
+#ifdef HAVE_OUTPUT_UHD
+
+#include "PcDebug.h"
+#include "Log.h"
+#include "RemoteControl.h"
+#include "Utils.h"
+
+#include <boost/thread/future.hpp>
+
+#include <uhd/utils/msg.hpp>
+
+#include <cmath>
+#include <iostream>
+#include <assert.h>
+#include <stdexcept>
+#include <stdio.h>
+#include <time.h>
+#include <errno.h>
+#include <unistd.h>
+#include <pthread.h>
+
+using namespace std;
+
+// Maximum number of frames that can wait in frames
+static const size_t FRAMES_MAX_SIZE = 8;
+
+typedef std::complex<float> complexf;
+
+std::string stringtrim(const std::string &s)
+{
+ auto wsfront = std::find_if_not(s.begin(), s.end(),
+ [](int c){ return std::isspace(c);} );
+ return std::string(wsfront,
+ std::find_if_not(s.rbegin(),
+ std::string::const_reverse_iterator(wsfront),
+ [](int c){ return std::isspace(c);} ).base());
+}
+
+void uhd_msg_handler(uhd::msg::type_t type, const std::string &msg)
+{
+ if (type == uhd::msg::warning) {
+ etiLog.level(warn) << "UHD Warning: " << msg;
+ }
+ else if (type == uhd::msg::error) {
+ etiLog.level(error) << "UHD Error: " << msg;
+ }
+ else {
+ // do not print very short U messages and such
+ if (stringtrim(msg).size() != 1) {
+ etiLog.level(debug) << "UHD Message: " << msg;
+ }
+ }
+}
+
+static void tune_usrp_to(
+ uhd::usrp::multi_usrp::sptr usrp,
+ double lo_offset,
+ double frequency)
+{
+ if (lo_offset != 0.0) {
+ etiLog.level(info) << std::fixed << std::setprecision(3) <<
+ "OutputUHD:Setting freq to " << frequency <<
+ " with LO offset " << lo_offset << "...";
+
+ const auto tr = uhd::tune_request_t(frequency, lo_offset);
+ uhd::tune_result_t result = usrp->set_tx_freq(tr);
+
+ etiLog.level(debug) << "OutputUHD:" <<
+ std::fixed << std::setprecision(0) <<
+ " Target RF: " << result.target_rf_freq <<
+ " Actual RF: " << result.actual_rf_freq <<
+ " Target DSP: " << result.target_dsp_freq <<
+ " Actual DSP: " << result.actual_dsp_freq;
+ }
+ else {
+ //set the centre frequency
+ etiLog.level(info) << std::fixed << std::setprecision(3) <<
+ "OutputUHD:Setting freq to " << frequency << "...";
+ usrp->set_tx_freq(frequency);
+ }
+
+ usrp->set_rx_freq(frequency);
+}
+
+// Check function for GPS TIMELOCK sensor from the ODR LEA-M8F board GPSDO
+bool check_gps_timelock(uhd::usrp::multi_usrp::sptr usrp)
+{
+ try {
+ std::string sensor_value(
+ usrp->get_mboard_sensor("gps_timelock", 0).to_pp_string());
+
+ if (sensor_value.find("TIME LOCKED") == std::string::npos) {
+ etiLog.level(warn) << "OutputUHD: gps_timelock " << sensor_value;
+ return false;
+ }
+
+ return true;
+ }
+ catch (uhd::lookup_error &e) {
+ etiLog.level(warn) << "OutputUHD: no gps_timelock sensor";
+ return false;
+ }
+}
+
+// Check function for GPS LOCKED sensor from the Ettus GPSDO
+bool check_gps_locked(uhd::usrp::multi_usrp::sptr usrp)
+{
+ try {
+ uhd::sensor_value_t sensor_value(
+ usrp->get_mboard_sensor("gps_locked", 0));
+ if (not sensor_value.to_bool()) {
+ etiLog.level(warn) << "OutputUHD: gps_locked " <<
+ sensor_value.to_pp_string();
+ return false;
+ }
+
+ return true;
+ }
+ catch (uhd::lookup_error &e) {
+ etiLog.level(warn) << "OutputUHD: no gps_locked sensor";
+ return false;
+ }
+}
+
+
+OutputUHD::OutputUHD(
+ OutputUHDConfig& config) :
+ ModOutput(),
+ RemoteControllable("uhd"),
+ myConf(config),
+ // Since we don't know the buffer size, we cannot initialise
+ // the buffers at object initialisation.
+ myDelayBuf(0),
+ running(false)
+{
+ myConf.muting = true; // is remote-controllable, and reset by the GPS fix check
+
+ // Variables needed for GPS fix check
+ first_gps_fix_check.tv_sec = 0;
+ last_gps_fix_check.tv_sec = 0;
+ time_last_frame.tv_sec = 0;
+
+
+ std::stringstream device;
+ device << myConf.device;
+
+ if (myConf.masterClockRate != 0) {
+ if (device.str() != "") {
+ device << ",";
+ }
+ device << "master_clock_rate=" << myConf.masterClockRate;
+ }
+
+ if (myConf.usrpType != "") {
+ if (device.str() != "") {
+ device << ",";
+ }
+ device << "type=" << myConf.usrpType;
+ }
+
+ MDEBUG("OutputUHD::OutputUHD(device: %s) @ %p\n",
+ device.str().c_str(), this);
+
+ /* register the parameters that can be remote controlled */
+ RC_ADD_PARAMETER(txgain, "UHD analog daughterboard TX gain");
+ RC_ADD_PARAMETER(rxgain, "UHD analog daughterboard RX gain for DPD feedback");
+ RC_ADD_PARAMETER(freq, "UHD transmission frequency");
+ RC_ADD_PARAMETER(muting, "Mute the output by stopping the transmitter");
+ RC_ADD_PARAMETER(underruns, "Read-only counter of number of underruns");
+ RC_ADD_PARAMETER(latepackets, "Read-only counter of number of late packets");
+ RC_ADD_PARAMETER(frames, "Read-only counter of number of frames modulated");
+
+ uhd::msg::register_handler(uhd_msg_handler);
+
+ uhd::set_thread_priority_safe();
+
+ etiLog.log(info, "OutputUHD:Creating the usrp device with: %s...",
+ device.str().c_str());
+
+ myUsrp = uhd::usrp::multi_usrp::make(device.str());
+
+ etiLog.log(info, "OutputUHD:Using device: %s...",
+ myUsrp->get_pp_string().c_str());
+
+ if (myConf.masterClockRate != 0.0) {
+ double master_clk_rate = myUsrp->get_master_clock_rate();
+ etiLog.log(debug, "OutputUHD:Checking master clock rate: %f...",
+ master_clk_rate);
+
+ if (fabs(master_clk_rate - myConf.masterClockRate) >
+ (myConf.masterClockRate * 1e-6)) {
+ throw std::runtime_error("Cannot set USRP master_clock_rate. Aborted.");
+ }
+ }
+
+ MDEBUG("OutputUHD:Setting REFCLK and PPS input...\n");
+
+ if (myConf.refclk_src == "gpsdo-ettus") {
+ myUsrp->set_clock_source("gpsdo");
+ }
+ else {
+ myUsrp->set_clock_source(myConf.refclk_src);
+ }
+ myUsrp->set_time_source(myConf.pps_src);
+
+ if (myConf.subDevice != "") {
+ myUsrp->set_tx_subdev_spec(uhd::usrp::subdev_spec_t(myConf.subDevice),
+ uhd::usrp::multi_usrp::ALL_MBOARDS);
+ }
+
+ etiLog.level(debug) << "UHD clock source is " << myUsrp->get_clock_source(0);
+
+ etiLog.level(debug) << "UHD time source is " << myUsrp->get_time_source(0);
+
+ myUsrp->set_tx_rate(myConf.sampleRate);
+ etiLog.log(debug, "OutputUHD:Set rate to %d. Actual TX Rate: %f sps...",
+ myConf.sampleRate, myUsrp->get_tx_rate());
+
+ if (fabs(myUsrp->get_tx_rate() / myConf.sampleRate) >
+ myConf.sampleRate * 1e-6) {
+ throw std::runtime_error("Cannot set USRP sample rate. Aborted.");
+ }
+
+ tune_usrp_to(myUsrp, myConf.lo_offset, myConf.frequency);
+
+ myConf.frequency = myUsrp->get_tx_freq();
+ etiLog.level(info) << std::fixed << std::setprecision(3) <<
+ "OutputUHD:Actual TX frequency: " << myConf.frequency;
+
+ etiLog.level(info) << std::fixed << std::setprecision(3) <<
+ "OutputUHD:Actual RX frequency: " << myUsrp->get_tx_freq();
+
+ myUsrp->set_tx_gain(myConf.txgain);
+ etiLog.log(debug, "OutputUHD:Actual TX Gain: %f", myUsrp->get_tx_gain());
+
+ etiLog.log(debug, "OutputUHD:Mute on missing timestamps: %s",
+ myConf.muteNoTimestamps ? "enabled" : "disabled");
+
+ // preparing output thread worker data
+ sourceContainsTimestamp = false;
+
+ SetDelayBuffer(myConf.dabMode);
+
+ myUsrp->set_rx_rate(myConf.sampleRate);
+ etiLog.log(debug, "OutputUHD:Actual RX Rate: %f sps.", myUsrp->get_rx_rate());
+
+ myUsrp->set_rx_antenna("RX2");
+ etiLog.log(debug, "OutputUHD:Set RX Antenna: %s",
+ myUsrp->get_rx_antenna().c_str());
+
+ myUsrp->set_rx_gain(myConf.rxgain);
+ etiLog.log(debug, "OutputUHD:Actual RX Gain: %f", myUsrp->get_rx_gain());
+
+ uhdFeedback = std::make_shared<OutputUHDFeedback>(
+ myUsrp, myConf.dpdFeedbackServerPort, myConf.sampleRate);
+
+ MDEBUG("OutputUHD:UHD ready.\n");
+}
+
+bool OutputUHD::refclk_loss_needs_check() const
+{
+ if (suppress_refclk_loss_check) {
+ return false;
+ }
+ return myConf.refclk_src != "internal";
+}
+
+bool OutputUHD::gpsfix_needs_check() const
+{
+ if (myConf.refclk_src == "internal") {
+ return false;
+ }
+ else if (myConf.refclk_src == "gpsdo") {
+ return (myConf.maxGPSHoldoverTime != 0);
+ }
+ else if (myConf.refclk_src == "gpsdo-ettus") {
+ return (myConf.maxGPSHoldoverTime != 0);
+ }
+ else {
+ return false;
+ }
+}
+
+bool OutputUHD::gpsdo_is_ettus() const
+{
+ return (myConf.refclk_src == "gpsdo-ettus");
+}
+
+OutputUHD::~OutputUHD()
+{
+ stop_threads();
+}
+
+void OutputUHD::stop_threads()
+{
+ running.store(false);
+ uhd_thread.interrupt();
+ uhd_thread.join();
+ async_rx_thread.join();
+}
+
+
+void OutputUHD::setETISource(EtiSource *etiSource)
+{
+ myEtiSource = etiSource;
+}
+
+int transmission_frame_duration_ms(unsigned int dabMode)
+{
+ switch (dabMode) {
+ // could happen when called from constructor and we take the mode from ETI
+ case 0: return 0;
+
+ case 1: return 96;
+ case 2: return 24;
+ case 3: return 24;
+ case 4: return 48;
+ default:
+ throw std::runtime_error("OutputUHD: invalid DAB mode");
+ }
+}
+
+void OutputUHD::SetDelayBuffer(unsigned int dabMode)
+{
+ // find out the duration of the transmission frame (Table 2 in ETSI 300 401)
+ myTFDurationMs = transmission_frame_duration_ms(dabMode);
+
+ // The buffer size equals the number of samples per transmission frame so
+ // we calculate it by multiplying the duration of the transmission frame
+ // with the samplerate.
+ myDelayBuf.resize(myTFDurationMs * myConf.sampleRate / 1000);
+}
+
+int OutputUHD::process(Buffer* dataIn)
+{
+ if (not gps_fix_verified) {
+ if (gpsfix_needs_check()) {
+ initial_gps_check();
+
+ if (num_checks_without_gps_fix == 0) {
+ set_usrp_time();
+ gps_fix_verified = true;
+ myConf.muting = false;
+ }
+ }
+ else {
+ set_usrp_time();
+ gps_fix_verified = true;
+ myConf.muting = false;
+ }
+ }
+ else {
+ if (first_run) {
+ etiLog.level(debug) << "OutputUHD: UHD initialising...";
+
+ // we only set the delay buffer from the dab mode signaled in ETI if the
+ // dab mode was not set in contructor
+ if (myTFDurationMs == 0) {
+ SetDelayBuffer(myEtiSource->getMode());
+ }
+
+ running.store(true);
+ uhd_thread = boost::thread(&OutputUHD::workerthread, this);
+ async_rx_thread = boost::thread(
+ &OutputUHD::print_async_thread, this);
+
+ lastLen = dataIn->getLength();
+ first_run = false;
+ etiLog.level(debug) << "OutputUHD: UHD initialising complete";
+ }
+
+ if (lastLen != dataIn->getLength()) {
+ // I expect that this never happens.
+ etiLog.level(emerg) <<
+ "OutputUHD: Fatal error, input length changed from " << lastLen <<
+ " to " << dataIn->getLength();
+ throw std::runtime_error("Non-constant input length!");
+ }
+
+ sourceContainsTimestamp = myConf.enableSync and
+ myEtiSource->sourceContainsTimestamp();
+
+ if (gpsfix_needs_check()) {
+ try {
+ check_gps();
+ }
+ catch (std::runtime_error& e) {
+ running.store(false);
+ etiLog.level(error) << e.what();
+ }
+ }
+
+ // Prepare the frame for the worker
+ UHDWorkerFrameData frame;
+ frame.buf.resize(dataIn->getLength());
+
+ std::copy(pInData, pInData + dataIn->getLength(),
+ frame.buf.begin());
+
+ myEtiSource->calculateTimestamp(frame.ts);
+
+ if (not running.load()) {
+ uhd_thread.interrupt();
+ uhd_thread.join();
+ async_rx_thread.join();
+ first_run = true;
+
+ etiLog.level(error) << "OutputUHD UHD worker failed";
+ throw std::runtime_error("UHD worker failed");
+ }
+
+ if (frame.ts.fct == -1) {
+ etiLog.level(info) <<
+ "OutputUHD: dropping one frame with invalid FCT";
+ }
+ else {
+ try {
+ uhdFeedback->set_tx_frame(frame.buf, frame.ts);
+ }
+ catch (const runtime_error& e) {
+ etiLog.level(warn) <<
+ "OutputUHD: Feedback server failed, restarting...";
+
+ uhdFeedback = std::make_shared<OutputUHDFeedback>(
+ myUsrp, myConf.dpdFeedbackServerPort, myConf.sampleRate);
+ }
+
+ size_t num_frames = frames.push_wait_if_full(frame,
+ FRAMES_MAX_SIZE);
+ etiLog.log(trace, "UHD,push %zu", num_frames);
+ }
+ }
+
+ return dataIn->getLength();
+}
+
+
+void OutputUHD::set_usrp_time()
+{
+ if (myConf.enableSync and (myConf.pps_src == "none")) {
+ etiLog.level(warn) <<
+ "OutputUHD: WARNING:"
+ " you are using synchronous transmission without PPS input!";
+
+ struct timespec now;
+ if (clock_gettime(CLOCK_REALTIME, &now)) {
+ perror("OutputUHD:Error: could not get time: ");
+ etiLog.level(error) << "OutputUHD: could not get time";
+ }
+ else {
+ myUsrp->set_time_now(uhd::time_spec_t(now.tv_sec));
+ etiLog.level(info) << "OutputUHD: Setting USRP time to " <<
+ std::fixed <<
+ uhd::time_spec_t(now.tv_sec).get_real_secs();
+ }
+ }
+
+ if (myConf.pps_src != "none") {
+ /* handling time for synchronisation: wait until the next full
+ * second, and set the USRP time at next PPS */
+ struct timespec now;
+ time_t seconds;
+ if (clock_gettime(CLOCK_REALTIME, &now)) {
+ etiLog.level(error) << "OutputUHD: could not get time :" <<
+ strerror(errno);
+ throw std::runtime_error("OutputUHD: could not get time.");
+ }
+ else {
+ seconds = now.tv_sec;
+
+ MDEBUG("OutputUHD:sec+1: %ld ; now: %ld ...\n", seconds+1, now.tv_sec);
+ while (seconds + 1 > now.tv_sec) {
+ usleep(1);
+ if (clock_gettime(CLOCK_REALTIME, &now)) {
+ etiLog.level(error) << "OutputUHD: could not get time :" <<
+ strerror(errno);
+ throw std::runtime_error("OutputUHD: could not get time.");
+ }
+ }
+ MDEBUG("OutputUHD:sec+1: %ld ; now: %ld ...\n", seconds+1, now.tv_sec);
+ /* We are now shortly after the second change. */
+
+ usleep(200000); // 200ms, we want the PPS to be later
+ myUsrp->set_time_unknown_pps(uhd::time_spec_t(seconds + 2));
+ etiLog.level(info) << "OutputUHD: Setting USRP time next pps to " <<
+ std::fixed <<
+ uhd::time_spec_t(seconds + 2).get_real_secs();
+ }
+
+ usleep(1e6);
+ etiLog.log(info, "OutputUHD: USRP time %f\n",
+ myUsrp->get_time_now().get_real_secs());
+ }
+}
+
+void OutputUHD::initial_gps_check()
+{
+ if (first_gps_fix_check.tv_sec == 0) {
+ etiLog.level(info) << "Waiting for GPS fix";
+
+ if (clock_gettime(CLOCK_MONOTONIC, &first_gps_fix_check) != 0) {
+ stringstream ss;
+ ss << "clock_gettime failure: " << strerror(errno);
+ throw std::runtime_error(ss.str());
+ }
+ }
+
+ check_gps();
+
+ if (last_gps_fix_check.tv_sec >
+ first_gps_fix_check.tv_sec + initial_gps_fix_wait) {
+ stringstream ss;
+ ss << "GPS did not show time lock in " << initial_gps_fix_wait << " seconds";
+ throw std::runtime_error(ss.str());
+ }
+
+ if (time_last_frame.tv_sec == 0) {
+ if (clock_gettime(CLOCK_MONOTONIC, &time_last_frame) != 0) {
+ stringstream ss;
+ ss << "clock_gettime failure: " << strerror(errno);
+ throw std::runtime_error(ss.str());
+ }
+ }
+
+ struct timespec now;
+ if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
+ stringstream ss;
+ ss << "clock_gettime failure: " << strerror(errno);
+ throw std::runtime_error(ss.str());
+ }
+
+ long delta_us = timespecdiff_us(time_last_frame, now);
+ long wait_time_us = transmission_frame_duration_ms(myConf.dabMode);
+
+ if (wait_time_us - delta_us > 0) {
+ usleep(wait_time_us - delta_us);
+ }
+
+ time_last_frame.tv_nsec += wait_time_us * 1000;
+ if (time_last_frame.tv_nsec >= 1000000000L) {
+ time_last_frame.tv_nsec -= 1000000000L;
+ time_last_frame.tv_sec++;
+ }
+}
+
+void OutputUHD::check_gps()
+{
+ struct timespec time_now;
+ if (clock_gettime(CLOCK_MONOTONIC, &time_now) != 0) {
+ stringstream ss;
+ ss << "clock_gettime failure: " << strerror(errno);
+ throw std::runtime_error(ss.str());
+ }
+
+ // Divide interval by two because we alternate between
+ // launch and check
+ if (gpsfix_needs_check() and
+ last_gps_fix_check.tv_sec + gps_fix_check_interval/2.0 <
+ time_now.tv_sec) {
+ last_gps_fix_check = time_now;
+
+ // Alternate between launching thread and checking the
+ // result.
+ if (gps_fix_task.joinable()) {
+ if (gps_fix_future.has_value()) {
+
+ gps_fix_future.wait();
+
+ gps_fix_task.join();
+
+ if (not gps_fix_future.get()) {
+ if (num_checks_without_gps_fix == 0) {
+ etiLog.level(alert) <<
+ "OutputUHD: GPS Time Lock lost";
+ }
+ num_checks_without_gps_fix++;
+ }
+ else {
+ if (num_checks_without_gps_fix) {
+ etiLog.level(info) <<
+ "OutputUHD: GPS Time Lock recovered";
+ }
+ num_checks_without_gps_fix = 0;
+ }
+
+ if (gps_fix_check_interval * num_checks_without_gps_fix >
+ myConf.maxGPSHoldoverTime) {
+ std::stringstream ss;
+ ss << "Lost GPS Time Lock for " << gps_fix_check_interval *
+ num_checks_without_gps_fix << " seconds";
+ throw std::runtime_error(ss.str());
+ }
+ }
+ }
+ else {
+ // Checking the sensor here takes too much
+ // time, it has to be done in a separate thread.
+ if (gpsdo_is_ettus()) {
+ gps_fix_pt = boost::packaged_task<bool>(
+ boost::bind(check_gps_locked, myUsrp) );
+ }
+ else {
+ gps_fix_pt = boost::packaged_task<bool>(
+ boost::bind(check_gps_timelock, myUsrp) );
+ }
+ gps_fix_future = gps_fix_pt.get_future();
+
+ gps_fix_task = boost::thread(boost::move(gps_fix_pt));
+ }
+ }
+}
+
+void OutputUHD::workerthread()
+{
+ // Set thread priority to realtime
+ if (int ret = set_realtime_prio(1)) {
+ etiLog.level(error) << "Could not set priority for UHD worker:" << ret;
+ }
+
+ set_thread_name("uhdworker");
+
+ last_tx_time_initialised = false;
+
+ uhd::stream_args_t stream_args("fc32"); //complex floats
+ myTxStream = myUsrp->get_tx_stream(stream_args);
+
+ md.start_of_burst = false;
+ md.end_of_burst = false;
+
+ num_underflows = 0;
+ num_late_packets = 0;
+
+ size_t last_num_underflows = 0;
+ size_t pop_prebuffering = FRAMES_MAX_SIZE;
+
+ while (running.load()) {
+ md.has_time_spec = false;
+ md.time_spec = uhd::time_spec_t(0.0);
+
+ struct UHDWorkerFrameData frame;
+ etiLog.log(trace, "UHD,wait");
+ frames.wait_and_pop(frame, pop_prebuffering);
+ etiLog.log(trace, "UHD,pop");
+
+ handle_frame(&frame);
+ num_frames_modulated++;
+
+ /* Ensure we fill frames after every underrun and
+ * at startup to reduce underrun likelihood. */
+ if (last_num_underflows < num_underflows) {
+ pop_prebuffering = FRAMES_MAX_SIZE;
+ }
+ else {
+ pop_prebuffering = 1;
+ }
+ last_num_underflows = num_underflows;
+ }
+ running.store(false);
+ etiLog.level(warn) << "UHD worker terminated";
+}
+
+void OutputUHD::handle_frame(const struct UHDWorkerFrameData *frame)
+{
+ // Transmit timeout
+ static const double tx_timeout = 20.0;
+
+ // Check for ref_lock
+ if (refclk_loss_needs_check()) {
+ try {
+ if (not myUsrp->get_mboard_sensor("ref_locked", 0).to_bool()) {
+ etiLog.log(alert,
+ "OutputUHD: External reference clock lock lost !");
+ if (myConf.refclk_lock_loss_behaviour == CRASH) {
+ throw std::runtime_error(
+ "OutputUHD: External reference clock lock lost.");
+ }
+ }
+ }
+ catch (uhd::lookup_error &e) {
+ suppress_refclk_loss_check = true;
+ etiLog.log(warn, "OutputUHD: This USRP does not have mboard "
+ "sensor for ext clock loss. Check disabled.");
+ }
+ }
+
+ double usrp_time = myUsrp->get_time_now().get_real_secs();
+ bool timestamp_discontinuity = false;
+
+ if (sourceContainsTimestamp) {
+ // Tx time from MNSC and TIST
+ uint32_t tx_second = frame->ts.timestamp_sec;
+ uint32_t tx_pps = frame->ts.timestamp_pps;
+
+ if (!frame->ts.timestamp_valid) {
+ /* We have not received a full timestamp through
+ * MNSC. We sleep through the frame.
+ */
+ etiLog.level(info) <<
+ "OutputUHD: Throwing sample " << frame->ts.fct <<
+ " away: incomplete timestamp " << tx_second <<
+ " / " << tx_pps;
+ usleep(20000); //TODO should this be TM-dependant ?
+ return;
+ }
+
+ if (last_tx_time_initialised) {
+ const size_t sizeIn = frame->buf.size() / sizeof(complexf);
+ uint64_t increment = (uint64_t)sizeIn * 16384000ul /
+ (uint64_t)myConf.sampleRate;
+ // samps * ticks/s / (samps/s)
+ // (samps * ticks * s) / (s * samps)
+ // ticks
+
+ uint32_t expected_sec = last_tx_second + increment / 16384000ul;
+ uint32_t expected_pps = last_tx_pps + increment % 16384000ul;
+
+ while (expected_pps >= 16384000) {
+ expected_sec++;
+ expected_pps -= 16384000;
+ }
+
+ if (expected_sec != tx_second or
+ expected_pps != tx_pps) {
+ etiLog.level(warn) << "OutputUHD: timestamp irregularity!" <<
+ std::fixed <<
+ " Expected " <<
+ expected_sec << "+" << (double)expected_pps/16384000.0 <<
+ "(" << expected_pps << ")" <<
+ " Got " <<
+ tx_second << "+" << (double)tx_pps/16384000.0 <<
+ "(" << tx_pps << ")";
+
+ timestamp_discontinuity = true;
+ }
+ }
+
+ last_tx_second = tx_second;
+ last_tx_pps = tx_pps;
+ last_tx_time_initialised = true;
+
+ double pps_offset = tx_pps / 16384000.0;
+
+ md.has_time_spec = true;
+ md.time_spec = uhd::time_spec_t(tx_second, pps_offset);
+ etiLog.log(trace, "UHD,tist %f", md.time_spec.get_real_secs());
+
+ // md is defined, let's do some checks
+ if (md.time_spec.get_real_secs() + tx_timeout < usrp_time) {
+ etiLog.level(warn) <<
+ "OutputUHD: Timestamp in the past! offset: " <<
+ std::fixed <<
+ md.time_spec.get_real_secs() - usrp_time <<
+ " (" << usrp_time << ")"
+ " frame " << frame->ts.fct <<
+ ", tx_second " << tx_second <<
+ ", pps " << pps_offset;
+ return;
+ }
+
+ if (md.time_spec.get_real_secs() > usrp_time + TIMESTAMP_ABORT_FUTURE) {
+ etiLog.level(error) <<
+ "OutputUHD: Timestamp way too far in the future! offset: " <<
+ std::fixed <<
+ md.time_spec.get_real_secs() - usrp_time;
+ throw std::runtime_error("Timestamp error. Aborted.");
+ }
+ }
+ else { // !sourceContainsTimestamp
+ if (myConf.muting or myConf.muteNoTimestamps) {
+ /* There was some error decoding the timestamp */
+ if (myConf.muting) {
+ etiLog.log(info,
+ "OutputUHD: Muting sample %d requested\n",
+ frame->ts.fct);
+ }
+ else {
+ etiLog.log(info,
+ "OutputUHD: Muting sample %d : no timestamp\n",
+ frame->ts.fct);
+ }
+ usleep(20000);
+ return;
+ }
+ }
+
+ tx_frame(frame, timestamp_discontinuity);
+}
+
+void OutputUHD::tx_frame(const struct UHDWorkerFrameData *frame, bool ts_update)
+{
+ const double tx_timeout = 20.0;
+ const size_t sizeIn = frame->buf.size() / sizeof(complexf);
+ const complexf* in_data = reinterpret_cast<const complexf*>(&frame->buf[0]);
+
+ size_t usrp_max_num_samps = myTxStream->get_max_num_samps();
+ size_t num_acc_samps = 0; //number of accumulated samples
+ while (running.load() and (not myConf.muting) and (num_acc_samps < sizeIn)) {
+ size_t samps_to_send = std::min(sizeIn - num_acc_samps, usrp_max_num_samps);
+
+ uhd::tx_metadata_t md_tx = md;
+
+ //ensure the the last packet has EOB set if the timestamps has been
+ //refreshed and need to be reconsidered.
+ md_tx.end_of_burst = (
+ sourceContainsTimestamp and
+ (frame->ts.timestamp_refresh or ts_update) and
+ samps_to_send <= usrp_max_num_samps );
+
+
+ //send a single packet
+ size_t num_tx_samps = myTxStream->send(
+ &in_data[num_acc_samps],
+ samps_to_send, md_tx, tx_timeout);
+ etiLog.log(trace, "UHD,sent %zu of %zu", num_tx_samps, samps_to_send);
+
+ num_acc_samps += num_tx_samps;
+
+ md_tx.time_spec = md.time_spec +
+ uhd::time_spec_t(0, num_tx_samps/myConf.sampleRate);
+
+ if (num_tx_samps == 0) {
+ etiLog.log(warn,
+ "OutputUHD::workerthread() unable to write to device, skipping frame!\n");
+ break;
+ }
+ }
+}
+
+void OutputUHD::print_async_thread()
+{
+ while (running.load()) {
+ uhd::async_metadata_t async_md;
+ if (myUsrp->get_device()->recv_async_msg(async_md, 1)) {
+ const char* uhd_async_message = "";
+ bool failure = false;
+ switch (async_md.event_code) {
+ case uhd::async_metadata_t::EVENT_CODE_BURST_ACK:
+ break;
+ case uhd::async_metadata_t::EVENT_CODE_UNDERFLOW:
+ uhd_async_message = "Underflow";
+ num_underflows++;
+ break;
+ case uhd::async_metadata_t::EVENT_CODE_SEQ_ERROR:
+ uhd_async_message = "Packet loss between host and device.";
+ failure = true;
+ break;
+ case uhd::async_metadata_t::EVENT_CODE_TIME_ERROR:
+ uhd_async_message = "Packet had time that was late.";
+ num_late_packets++;
+ break;
+ case uhd::async_metadata_t::EVENT_CODE_UNDERFLOW_IN_PACKET:
+ uhd_async_message = "Underflow occurred inside a packet.";
+ failure = true;
+ break;
+ case uhd::async_metadata_t::EVENT_CODE_SEQ_ERROR_IN_BURST:
+ uhd_async_message = "Packet loss within a burst.";
+ failure = true;
+ break;
+ default:
+ uhd_async_message = "unknown event code";
+ failure = true;
+ break;
+ }
+
+ if (failure) {
+ etiLog.level(alert) <<
+ "Received Async UHD Message '" <<
+ uhd_async_message << "' at time " <<
+ md.time_spec.get_real_secs();
+
+ }
+ }
+
+ auto time_now = std::chrono::steady_clock::now();
+ if (last_print_time + std::chrono::seconds(1) < time_now) {
+ const double usrp_time =
+ myUsrp->get_time_now().get_real_secs();
+
+ if ( (num_underflows > num_underflows_previous) or
+ (num_late_packets > num_late_packets_previous)) {
+ etiLog.log(info,
+ "OutputUHD status (usrp time: %f): "
+ "%d underruns and %d late packets since last status.\n",
+ usrp_time,
+ num_underflows, num_late_packets);
+ }
+
+ num_underflows_previous = num_underflows;
+ num_late_packets_previous = num_late_packets;
+
+ last_print_time = time_now;
+ }
+ }
+}
+
+// =======================================
+// Remote Control for UHD
+// =======================================
+void OutputUHD::set_parameter(const string& parameter, const string& value)
+{
+ stringstream ss(value);
+ ss.exceptions ( stringstream::failbit | stringstream::badbit );
+
+ if (parameter == "txgain") {
+ ss >> myConf.txgain;
+ myUsrp->set_tx_gain(myConf.txgain);
+ }
+ else if (parameter == "rxgain") {
+ ss >> myConf.rxgain;
+ myUsrp->set_rx_gain(myConf.rxgain);
+ }
+ else if (parameter == "freq") {
+ ss >> myConf.frequency;
+ tune_usrp_to(myUsrp, myConf.lo_offset, myConf.frequency);
+ myConf.frequency = myUsrp->get_tx_freq();
+ }
+ else if (parameter == "muting") {
+ ss >> myConf.muting;
+ }
+ else if (parameter == "underruns" or
+ parameter == "latepackets" or
+ parameter == "frames") {
+ throw ParameterError("Parameter " + parameter + " is read-only.");
+ }
+ else {
+ stringstream ss;
+ ss << "Parameter '" << parameter
+ << "' is not exported by controllable " << get_rc_name();
+ throw ParameterError(ss.str());
+ }
+}
+
+const string OutputUHD::get_parameter(const string& parameter) const
+{
+ stringstream ss;
+ if (parameter == "txgain") {
+ ss << myConf.txgain;
+ }
+ else if (parameter == "rxgain") {
+ ss << myConf.rxgain;
+ }
+ else if (parameter == "freq") {
+ ss << myConf.frequency;
+ }
+ else if (parameter == "muting") {
+ ss << myConf.muting;
+ }
+ else if (parameter == "underruns") {
+ ss << num_underflows;
+ }
+ else if (parameter == "latepackets") {
+ ss << num_late_packets;
+ }
+ else if (parameter == "frames") {
+ ss << num_frames_modulated;
+ }
+ else {
+ ss << "Parameter '" << parameter <<
+ "' is not exported by controllable " << get_rc_name();
+ throw ParameterError(ss.str());
+ }
+ return ss.str();
+}
+
+#endif // HAVE_OUTPUT_UHD
+
diff --git a/src/output/UHD.h b/src/output/UHD.h
new file mode 100644
index 0000000..e3f8174
--- /dev/null
+++ b/src/output/UHD.h
@@ -0,0 +1,244 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+ Queen in Right of Canada (Communications Research Center Canada)
+
+ Copyright (C) 2017
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://opendigitalradio.org
+
+DESCRIPTION:
+ It is an output driver for the USRP family of devices, and uses the UHD
+ library.
+*/
+
+/*
+ This file is part of ODR-DabMod.
+
+ ODR-DabMod 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 3 of the
+ License, or (at your option) any later version.
+
+ ODR-DabMod 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 ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#ifdef HAVE_OUTPUT_UHD
+
+#include <uhd/utils/thread_priority.hpp>
+#include <uhd/utils/safe_main.hpp>
+#include <uhd/usrp/multi_usrp.hpp>
+#include <boost/thread.hpp>
+#include <deque>
+#include <chrono>
+#include <memory>
+#include <string>
+#include <atomic>
+
+#include "Log.h"
+#include "output/SDR.h"
+#include "TimestampDecoder.h"
+#include "RemoteControl.h"
+#include "ThreadsafeQueue.h"
+#include "OutputUHDFeedback.h"
+
+#include <stdio.h>
+#include <sys/types.h>
+
+//#define MDEBUG(fmt, args...) fprintf(LOG, fmt , ## args)
+#define MDEBUG(fmt, args...)
+
+// If the timestamp is further in the future than
+// 100 seconds, abort
+#define TIMESTAMP_ABORT_FUTURE 100
+
+// Add a delay to increase buffers when
+// frames are too far in the future
+#define TIMESTAMP_MARGIN_FUTURE 0.5
+
+namespace Output {
+
+enum refclk_lock_loss_behaviour_t { CRASH, IGNORE };
+
+/* This structure is used as initial configuration for OutputUHD.
+ * It must also contain all remote-controllable settings, otherwise
+ * they will get lost on a modulator restart. */
+struct OutputUHDConfig {
+ std::string device;
+ std::string usrpType; // e.g. b100, b200, usrp2
+
+ // The USRP1 can accept two daughterboards
+ std::string subDevice; // e.g. A:0
+
+ long masterClockRate = 32768000;
+ unsigned sampleRate = 2048000;
+ double frequency = 0.0;
+ double lo_offset = 0.0;
+ double txgain = 0.0;
+ double rxgain = 0.0;
+ bool enableSync = false;
+
+ // When working with timestamps, mute the frames that
+ // do not have a timestamp
+ bool muteNoTimestamps = false;
+ unsigned dabMode = 0;
+ unsigned maxGPSHoldoverTime = 0;
+
+ /* allowed values : auto, int, sma, mimo */
+ std::string refclk_src;
+
+ /* allowed values : int, sma, mimo */
+ std::string pps_src;
+
+ /* allowed values : pos, neg */
+ std::string pps_polarity;
+
+ /* What to do when the reference clock PLL loses lock */
+ refclk_lock_loss_behaviour_t refclk_lock_loss_behaviour;
+
+ // muting can only be changed using the remote control
+ bool muting = false;
+
+ // TCP port on which to serve TX and RX samples for the
+ // digital pre distortion learning tool
+ uint16_t dpdFeedbackServerPort = 0;
+};
+
+class OutputUHD: public ModOutput, public RemoteControllable {
+ public:
+ OutputUHD(OutputUHDConfig& config);
+ OutputUHD(const OutputUHD& other) = delete;
+ OutputUHD operator=(const OutputUHD& other) = delete;
+ ~OutputUHD();
+
+ int process(Buffer* dataIn);
+
+ const char* name() { return "OutputUHD"; }
+
+ void setETISource(EtiSource *etiSource);
+
+ /*********** REMOTE CONTROL ***************/
+
+ /* Base function to set parameters. */
+ virtual void set_parameter(const std::string& parameter,
+ const std::string& value);
+
+ /* Getting a parameter always returns a string. */
+ virtual const std::string get_parameter(
+ const std::string& parameter) const;
+
+ protected:
+ EtiSource *myEtiSource = nullptr;
+ OutputUHDConfig& myConf;
+ uhd::usrp::multi_usrp::sptr myUsrp;
+ std::shared_ptr<boost::barrier> mySyncBarrier;
+ bool first_run = true;
+ bool gps_fix_verified = false;
+ std::shared_ptr<OutputUHDFeedback> uhdFeedback;
+
+ private:
+ // Each frame contains one OFDM frame, and its
+ // associated timestamp
+ struct UHDWorkerFrameData {
+ // Buffer holding frame data
+ std::vector<uint8_t> buf;
+
+ // A full timestamp contains a TIST according to standard
+ // and time information within MNSC with tx_second.
+ struct frame_timestamp ts;
+ };
+
+ // Resize the internal delay buffer according to the dabMode and
+ // the sample rate.
+ void SetDelayBuffer(unsigned int dabMode);
+
+ // data
+ // The remote-controllable static delay is in the OutputUHDConfig
+ int myTFDurationMs; // TF duration in milliseconds
+ std::vector<complexf> myDelayBuf;
+ size_t lastLen = 0;
+
+ // GPS Fix check variables
+ int num_checks_without_gps_fix = 1;
+ struct timespec first_gps_fix_check;
+ struct timespec last_gps_fix_check;
+ struct timespec time_last_frame;
+ boost::packaged_task<bool> gps_fix_pt;
+ boost::unique_future<bool> gps_fix_future;
+ boost::thread gps_fix_task;
+
+ // Wait time in seconds to get fix
+ static const int initial_gps_fix_wait = 180;
+
+ // Interval for checking the GPS at runtime
+ static constexpr double gps_fix_check_interval = 10.0; // seconds
+
+ // Asynchronous message statistics
+ size_t num_underflows = 0;
+ size_t num_late_packets = 0;
+ size_t num_underflows_previous = 0;
+ size_t num_late_packets_previous = 0;
+
+ size_t num_frames_modulated = 0;
+
+ uhd::tx_metadata_t md;
+ bool last_tx_time_initialised = false;
+ uint32_t last_tx_second = 0;
+ uint32_t last_tx_pps = 0;
+
+ // Used to print statistics once a second
+ std::chrono::steady_clock::time_point last_print_time;
+
+ bool sourceContainsTimestamp = false;
+
+ ThreadsafeQueue<UHDWorkerFrameData> frames;
+
+ // Returns true if we want to verify loss of refclk
+ bool refclk_loss_needs_check(void) const;
+ bool suppress_refclk_loss_check = false;
+
+ // Returns true if we want to check for the gps_timelock sensor
+ bool gpsfix_needs_check(void) const;
+
+ // Return true if the gpsdo is from ettus, false if it is the ODR
+ // LEA-M8F board is used
+ bool gpsdo_is_ettus(void) const;
+
+ std::atomic<bool> running;
+ boost::thread uhd_thread;
+ boost::thread async_rx_thread;
+ void stop_threads(void);
+
+ uhd::tx_streamer::sptr myTxStream;
+
+ // The worker thread decouples the modulator from UHD
+ void workerthread();
+ void handle_frame(const struct UHDWorkerFrameData *frame);
+ void tx_frame(const struct UHDWorkerFrameData *frame, bool ts_update);
+
+ // Poll asynchronous metadata from UHD
+ void print_async_thread(void);
+
+ void check_gps();
+
+ void set_usrp_time();
+
+ void initial_gps_check();
+};
+
+} // namespace Output
+
+#endif // HAVE_OUTPUT_UHD
+