aboutsummaryrefslogtreecommitdiffstats
path: root/src/output
diff options
context:
space:
mode:
authorMatthias P. Braendli <matthias.braendli@mpb.li>2018-01-20 01:20:41 +0100
committerMatthias P. Braendli <matthias.braendli@mpb.li>2018-01-20 01:20:41 +0100
commit28ddaa742d1a815c8c07d17b2a79fbfb964fdc1d (patch)
treef22941e989bb775aacda52876c97ada7b899a7dd /src/output
parent95f556cf0797ab4c23f431e5c8c5accfa7f4c30b (diff)
parentf52b0e13f61a947c26236504ffb4b072352abc04 (diff)
downloaddabmod-28ddaa742d1a815c8c07d17b2a79fbfb964fdc1d.tar.gz
dabmod-28ddaa742d1a815c8c07d17b2a79fbfb964fdc1d.tar.bz2
dabmod-28ddaa742d1a815c8c07d17b2a79fbfb964fdc1d.zip
Merge branch 'outputRefactoring' into next
Diffstat (limited to 'src/output')
-rw-r--r--src/output/Feedback.cpp356
-rw-r--r--src/output/Feedback.h116
-rw-r--r--src/output/SDR.cpp425
-rw-r--r--src/output/SDR.h95
-rw-r--r--src/output/SDRDevice.h137
-rw-r--r--src/output/Soapy.cpp274
-rw-r--r--src/output/Soapy.h98
-rw-r--r--src/output/UHD.cpp500
-rw-r--r--src/output/UHD.h127
-rw-r--r--src/output/USRPTime.cpp283
-rw-r--r--src/output/USRPTime.h116
11 files changed, 2527 insertions, 0 deletions
diff --git a/src/output/Feedback.cpp b/src/output/Feedback.cpp
new file mode 100644
index 0000000..f0bbd98
--- /dev/null
+++ b/src/output/Feedback.cpp
@@ -0,0 +1,356 @@
+/*
+ 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:
+ This presents a TCP socket to an external tool which calculates
+ a Digital Predistortion model from a short sequence of transmit
+ samples and corresponding receive samples.
+*/
+
+/*
+ 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/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#include <vector>
+#include <complex>
+#include <cstring>
+#include <sys/socket.h>
+#include <errno.h>
+#include <poll.h>
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include "output/Feedback.h"
+#include "Utils.h"
+#include "Socket.h"
+
+using namespace std;
+
+namespace Output {
+
+DPDFeedbackServer::DPDFeedbackServer(
+ std::shared_ptr<SDRDevice> device,
+ uint16_t port,
+ uint32_t sampleRate) :
+ m_port(port),
+ m_sampleRate(sampleRate),
+ m_device(device)
+{
+ if (m_port) {
+ m_running.store(true);
+
+ rx_burst_thread = boost::thread(
+ &DPDFeedbackServer::ReceiveBurstThread, this);
+
+ burst_tcp_thread = boost::thread(
+ &DPDFeedbackServer::ServeFeedbackThread, this);
+ }
+}
+
+DPDFeedbackServer::~DPDFeedbackServer()
+{
+ m_running.store(false);
+
+ rx_burst_thread.interrupt();
+ if (rx_burst_thread.joinable()) {
+ rx_burst_thread.join();
+ }
+
+ burst_tcp_thread.interrupt();
+ if (burst_tcp_thread.joinable()) {
+ burst_tcp_thread.join();
+ }
+}
+
+void DPDFeedbackServer::set_tx_frame(
+ const std::vector<uint8_t> &buf,
+ const struct frame_timestamp &buf_ts)
+{
+ if (not m_running) {
+ throw runtime_error("DPDFeedbackServer not running");
+ }
+
+ boost::mutex::scoped_lock lock(burstRequest.mutex);
+
+ if (buf.size() % sizeof(complexf) != 0) {
+ throw std::logic_error("Buffer for tx frame has incorrect size");
+ }
+
+ if (burstRequest.state == BurstRequestState::SaveTransmitFrame) {
+ const size_t n = std::min(
+ burstRequest.num_samples * sizeof(complexf), buf.size());
+
+ burstRequest.num_samples = n / sizeof(complexf);
+
+ burstRequest.tx_samples.clear();
+ burstRequest.tx_samples.resize(n);
+ // A frame will always begin with the NULL symbol, which contains
+ // no power. Instead of taking n samples at the beginning of the
+ // frame, we take them at the end and adapt the timestamp accordingly.
+
+ const size_t start_ix = buf.size() - n;
+ copy(buf.begin() + start_ix, buf.end(), burstRequest.tx_samples.begin());
+
+ frame_timestamp ts = buf_ts;
+ ts += (1.0 * start_ix) / (sizeof(complexf) * m_sampleRate);
+
+ burstRequest.tx_second = ts.timestamp_sec;
+ burstRequest.tx_pps = ts.timestamp_pps;
+
+ // Prepare the next state
+ burstRequest.rx_second = ts.timestamp_sec;
+ burstRequest.rx_pps = ts.timestamp_pps;
+ burstRequest.state = BurstRequestState::SaveReceiveFrame;
+
+ lock.unlock();
+ burstRequest.mutex_notification.notify_one();
+ }
+ else {
+ lock.unlock();
+ }
+}
+
+void DPDFeedbackServer::ReceiveBurstThread()
+{
+ try {
+ set_thread_name("dpdreceiveburst");
+
+ while (m_running) {
+ boost::mutex::scoped_lock lock(burstRequest.mutex);
+ while (burstRequest.state != BurstRequestState::SaveReceiveFrame) {
+ if (not m_running) break;
+ burstRequest.mutex_notification.wait(lock);
+ }
+
+ if (not m_running) break;
+
+ const size_t num_samps = burstRequest.num_samples;
+
+ frame_timestamp ts;
+ ts.timestamp_sec = burstRequest.rx_second;
+ ts.timestamp_pps = burstRequest.rx_pps;
+ ts.timestamp_valid = true;
+
+ // We need to free the mutex while we recv(), because otherwise we block the
+ // TX thread
+ lock.unlock();
+
+ const double device_time = m_device->get_real_secs();
+ const double cmd_time = ts.get_real_secs();
+
+ std::vector<uint8_t> buf(num_samps * sizeof(complexf));
+
+ const double timeout = 60;
+ size_t samples_read = m_device->receive_frame(
+ reinterpret_cast<complexf*>(buf.data()),
+ num_samps, ts, timeout);
+
+ lock.lock();
+ burstRequest.rx_samples = std::move(buf);
+ burstRequest.rx_samples.resize(samples_read * sizeof(complexf));
+
+ // The recv might have happened at another time than requested
+ burstRequest.rx_second = ts.timestamp_sec;
+ burstRequest.rx_pps = ts.timestamp_pps;
+
+ etiLog.level(debug) << "DPD: acquired " << samples_read <<
+ " RX feedback samples " <<
+ "at time " << burstRequest.tx_second << " + " <<
+ std::fixed << burstRequest.tx_pps / 16384000.0 <<
+ " Delta=" << cmd_time - device_time;
+
+ burstRequest.state = BurstRequestState::Acquired;
+
+ lock.unlock();
+ burstRequest.mutex_notification.notify_one();
+ }
+ }
+ catch (const runtime_error &e) {
+ etiLog.level(error) << "DPD Feedback RX runtime error: " << e.what();
+ }
+ catch (const std::exception &e) {
+ etiLog.level(error) << "DPD Feedback RX exception: " << e.what();
+ }
+ catch (...) {
+ etiLog.level(error) << "DPD Feedback RX unknown exception!";
+ }
+
+ m_running.store(false);
+}
+
+void DPDFeedbackServer::ServeFeedback()
+{
+ TCPSocket m_server_sock;
+ m_server_sock.listen(m_port);
+
+ etiLog.level(info) << "DPD Feedback server listening on port " << m_port;
+
+ while (m_running) {
+ struct sockaddr_in client;
+ TCPSocket client_sock = m_server_sock.accept_with_timeout(1000, &client);
+
+ if (not client_sock.valid()) {
+ // No connection request received
+ continue;
+ }
+
+ uint8_t request_version = 0;
+ ssize_t read = client_sock.recv(&request_version, 1, 0);
+ if (!read) break; // done reading
+ if (read < 0) {
+ etiLog.level(info) <<
+ "DPD Feedback Server Client read request version failed: " << strerror(errno);
+ break;
+ }
+
+ if (request_version != 1) {
+ etiLog.level(info) << "DPD Feedback Server wrong request version";
+ break;
+ }
+
+ uint32_t num_samples = 0;
+ read = client_sock.recv(&num_samples, 4, 0);
+ if (!read) break; // done reading
+ if (read < 0) {
+ etiLog.level(info) <<
+ "DPD Feedback Server Client read num samples failed";
+ break;
+ }
+
+ // We are ready to issue the request now
+ {
+ boost::mutex::scoped_lock lock(burstRequest.mutex);
+ burstRequest.num_samples = num_samples;
+ burstRequest.state = BurstRequestState::SaveTransmitFrame;
+
+ lock.unlock();
+ }
+
+ // Wait for the result to be ready
+ boost::mutex::scoped_lock lock(burstRequest.mutex);
+ while (burstRequest.state != BurstRequestState::Acquired) {
+ if (not m_running) break;
+ burstRequest.mutex_notification.wait(lock);
+ }
+
+ burstRequest.state = BurstRequestState::None;
+ lock.unlock();
+
+ burstRequest.num_samples = std::min(burstRequest.num_samples,
+ std::min(
+ burstRequest.tx_samples.size() / sizeof(complexf),
+ burstRequest.rx_samples.size() / sizeof(complexf)));
+
+ uint32_t num_samples_32 = burstRequest.num_samples;
+ if (client_sock.sendall(&num_samples_32, sizeof(num_samples_32)) < 0) {
+ etiLog.level(info) <<
+ "DPD Feedback Server Client send num_samples failed";
+ break;
+ }
+
+ if (client_sock.sendall(
+ &burstRequest.tx_second,
+ sizeof(burstRequest.tx_second)) < 0) {
+ etiLog.level(info) <<
+ "DPD Feedback Server Client send tx_second failed";
+ break;
+ }
+
+ if (client_sock.sendall(
+ &burstRequest.tx_pps,
+ sizeof(burstRequest.tx_pps)) < 0) {
+ etiLog.level(info) <<
+ "DPD Feedback Server Client send tx_pps failed";
+ break;
+ }
+
+ const size_t frame_bytes = burstRequest.num_samples * sizeof(complexf);
+
+ if (burstRequest.tx_samples.size() < frame_bytes) {
+ throw logic_error("DPD Feedback burstRequest invalid: not enough TX samples");
+ }
+
+ if (client_sock.sendall(
+ &burstRequest.tx_samples[0],
+ frame_bytes) < 0) {
+ etiLog.level(info) <<
+ "DPD Feedback Server Client send tx_frame failed";
+ break;
+ }
+
+ if (client_sock.sendall(
+ &burstRequest.rx_second,
+ sizeof(burstRequest.rx_second)) < 0) {
+ etiLog.level(info) <<
+ "DPD Feedback Server Client send rx_second failed";
+ break;
+ }
+
+ if (client_sock.sendall(
+ &burstRequest.rx_pps,
+ sizeof(burstRequest.rx_pps)) < 0) {
+ etiLog.level(info) <<
+ "DPD Feedback Server Client send rx_pps failed";
+ break;
+ }
+
+ if (burstRequest.rx_samples.size() < frame_bytes) {
+ throw logic_error("DPD Feedback burstRequest invalid: not enough RX samples");
+ }
+
+ if (client_sock.sendall(
+ &burstRequest.rx_samples[0],
+ frame_bytes) < 0) {
+ etiLog.level(info) <<
+ "DPD Feedback Server Client send rx_frame failed";
+ break;
+ }
+ }
+}
+
+void DPDFeedbackServer::ServeFeedbackThread()
+{
+ set_thread_name("dpdfeedbackserver");
+
+ while (m_running) {
+ try {
+ ServeFeedback();
+ }
+ catch (const runtime_error &e) {
+ etiLog.level(error) << "DPD Feedback Server runtime error: " << e.what();
+ }
+ catch (const std::exception &e) {
+ etiLog.level(error) << "DPD Feedback Server exception: " << e.what();
+ }
+ catch (...) {
+ etiLog.level(error) << "DPD Feedback Server unknown exception!";
+ }
+
+ boost::this_thread::sleep(boost::posix_time::seconds(5));
+ }
+
+ m_running.store(false);
+}
+
+} // namespace Output
diff --git a/src/output/Feedback.h b/src/output/Feedback.h
new file mode 100644
index 0000000..2cad508
--- /dev/null
+++ b/src/output/Feedback.h
@@ -0,0 +1,116 @@
+/*
+ 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:
+ This presents a TCP socket to an external tool which calculates
+ a Digital Predistortion model from a short sequence of transmit
+ samples and corresponding receive samples.
+*/
+
+/*
+ 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 <boost/thread.hpp>
+#include <memory>
+#include <string>
+#include <atomic>
+
+#include "Log.h"
+#include "TimestampDecoder.h"
+#include "output/SDRDevice.h"
+
+namespace Output {
+
+enum class BurstRequestState {
+ None, // To pending request
+ SaveTransmitFrame, // The TX thread has to save an outgoing frame
+ SaveReceiveFrame, // The RX thread has to save an incoming frame
+ Acquired, // Both TX and RX frames are ready
+};
+
+struct FeedbackBurstRequest {
+ // All fields in this struct are protected
+ mutable boost::mutex mutex;
+ boost::condition_variable mutex_notification;
+
+ BurstRequestState state = BurstRequestState::None;
+
+ // In the SaveTransmit states, num_samples complexf samples are saved into
+ // the vectors
+ size_t num_samples = 0;
+
+ // The timestamp of the first sample of the TX buffers
+ uint32_t tx_second = 0;
+ uint32_t tx_pps = 0; // in units of 1/16384000s
+
+ // Samples contain complexf, but since our internal representation is uint8_t
+ // we keep it like that
+ std::vector<uint8_t> tx_samples;
+
+ // The timestamp of the first sample of the RX buffers
+ uint32_t rx_second = 0;
+ uint32_t rx_pps = 0;
+
+ std::vector<uint8_t> rx_samples; // Also, actually complexf
+};
+
+// Serve TX samples and RX feedback samples over a TCP connection
+class DPDFeedbackServer {
+ public:
+ DPDFeedbackServer(
+ std::shared_ptr<SDRDevice> device,
+ uint16_t port, // Set to 0 to disable the Feedbackserver
+ uint32_t sampleRate);
+ DPDFeedbackServer(const DPDFeedbackServer& other) = delete;
+ DPDFeedbackServer& operator=(const DPDFeedbackServer& other) = delete;
+ ~DPDFeedbackServer();
+
+ void set_tx_frame(const std::vector<uint8_t> &buf,
+ const struct frame_timestamp& ts);
+
+ private:
+ // Thread that reacts to burstRequests and receives from the SDR device
+ void ReceiveBurstThread(void);
+
+ // Thread that listens for requests over TCP to get TX and RX feedback
+ void ServeFeedbackThread(void);
+ void ServeFeedback(void);
+
+ boost::thread rx_burst_thread;
+ boost::thread burst_tcp_thread;
+
+ FeedbackBurstRequest burstRequest;
+
+ std::atomic_bool m_running;
+ uint16_t m_port = 0;
+ uint32_t m_sampleRate = 0;
+ std::shared_ptr<SDRDevice> m_device;
+};
+
+} // namespace Output
diff --git a/src/output/SDR.cpp b/src/output/SDR.cpp
new file mode 100644
index 0000000..34341bd
--- /dev/null
+++ b/src/output/SDR.cpp
@@ -0,0 +1,425 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+ Queen in Right of Canada (Communications Research Center Canada)
+
+ Copyright (C) 2018
+ 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(), ModMetadata(), RemoteControllable("sdr"),
+ m_config(config),
+ m_device(device)
+{
+ // muting is remote-controllable
+ m_config.muting = false;
+
+ m_device_thread = std::thread(&SDR::process_thread_entry, this);
+
+ m_dpd_feedback_server = make_shared<DPDFeedbackServer>(
+ m_device,
+ m_config.dpdFeedbackServerPort,
+ m_config.sampleRate);
+}
+
+SDR::~SDR()
+{
+ m_running.store(false);
+
+ FrameData end_marker;
+ end_marker.buf.clear();
+ m_queue.push(end_marker);
+
+ if (m_device_thread.joinable()) {
+ m_device_thread.join();
+ }
+}
+
+int SDR::process(Buffer *dataIn)
+{
+ if (not m_running) {
+ throw std::runtime_error("SDR thread failed");
+ }
+
+ const uint8_t* pDataIn = (uint8_t*)dataIn->getData();
+ m_frame.resize(dataIn->getLength());
+ std::copy(pDataIn, pDataIn + dataIn->getLength(),
+ m_frame.begin());
+
+ // We will effectively transmit the frame once we got the metadata.
+
+ return dataIn->getLength();
+}
+
+meta_vec_t SDR::process_metadata(const meta_vec_t& metadataIn)
+{
+ if (m_device and m_running) {
+ FrameData frame;
+ frame.buf = std::move(m_frame);
+
+ if (metadataIn.empty()) {
+ etiLog.level(info) <<
+ "SDR output: dropping one frame with invalid FCT";
+ }
+ else {
+ /* In transmission modes where several ETI frames are needed to
+ * build one transmission frame (like in TM 1), we will have
+ * several entries in metadataIn. Take the first one, which
+ * comes from the earliest ETI frame.
+ * This behaviour is different to earlier versions of ODR-DabMod,
+ * which took the timestamp from the latest ETI frame.
+ */
+ frame.ts = *(metadataIn[0].ts);
+
+ // TODO check device running
+
+ try {
+ if (m_dpd_feedback_server) {
+ m_dpd_feedback_server->set_tx_frame(frame.buf, frame.ts);
+ }
+ }
+ catch (const runtime_error& e) {
+ etiLog.level(warn) <<
+ "SDR output: Feedback server failed, restarting...";
+
+ m_dpd_feedback_server = std::make_shared<DPDFeedbackServer>(
+ m_device,
+ m_config.dpdFeedbackServerPort,
+ m_config.sampleRate);
+ }
+
+ 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 {};
+}
+
+
+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;
+
+ m_running.store(true);
+
+ try {
+ 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;
+ }
+ }
+ }
+ catch (const runtime_error& e) {
+ etiLog.level(error) << "SDR output thread caught runtime error: " << e.what();
+ }
+
+ m_running.store(false);
+}
+
+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::sleep_through_frame()
+{
+ using namespace std::chrono;
+
+ const auto now = steady_clock::now();
+
+ if (not t_last_frame_initialised) {
+ t_last_frame = now;
+ t_last_frame_initialised = true;
+ }
+
+ const auto delta = now - t_last_frame;
+ const auto wait_time = transmission_frame_duration(m_config.dabMode);
+
+ if (wait_time > delta) {
+ this_thread::sleep_for(wait_time - delta);
+ }
+
+ t_last_frame += wait_time;
+}
+
+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()) {
+ sleep_through_frame();
+ return;
+ }
+
+ double device_time = m_device->get_real_secs();
+ const auto& time_spec = frame.ts;
+
+ if (m_config.enableSync and m_config.muteNoTimestamps and
+ not time_spec.timestamp_valid) {
+ sleep_through_frame();
+ etiLog.log(info,
+ "OutputSDR: Muting sample %d : no timestamp\n",
+ frame.ts.fct);
+ return;
+ }
+
+ if (m_config.enableSync and time_spec.timestamp_valid) {
+ // 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 << ")";
+
+ frame.ts.timestamp_refresh = 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.");
+ }
+ }
+
+ if (m_config.muting) {
+ etiLog.log(info,
+ "OutputSDR: Muting sample %d requested\n",
+ frame.ts.fct);
+ return;
+ }
+
+ 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;
+ m_device->set_rxgain(m_config.rxgain);
+ }
+ 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_err;
+ ss_err << "Parameter '" << parameter
+ << "' is not exported by controllable " << get_rc_name();
+ throw ParameterError(ss_err.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..a55f7c0
--- /dev/null
+++ b/src/output/SDR.h
@@ -0,0 +1,95 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+ Queen in Right of Canada (Communications Research Center Canada)
+
+ Copyright (C) 2018
+ 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 <chrono>
+#include "ModPlugin.h"
+#include "EtiReader.h"
+#include "output/SDRDevice.h"
+#include "output/Feedback.h"
+
+namespace Output {
+
+using complexf = std::complex<float>;
+
+class SDR : public ModOutput, public ModMetadata, 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 meta_vec_t process_metadata(const meta_vec_t& metadataIn) override;
+
+ virtual const char* name() override;
+
+ /*********** 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 process_thread_entry(void);
+ void handle_frame(struct FrameData &frame);
+ void sleep_through_frame(void);
+
+ SDRDeviceConfig& m_config;
+
+ std::atomic<bool> m_running = ATOMIC_VAR_INIT(false);
+ std::thread m_device_thread;
+ std::vector<uint8_t> m_frame;
+ ThreadsafeQueue<FrameData> m_queue;
+
+ std::shared_ptr<SDRDevice> m_device;
+ std::string m_name;
+
+ std::shared_ptr<DPDFeedbackServer> m_dpd_feedback_server;
+
+ bool last_tx_time_initialised = false;
+ uint32_t last_tx_second = 0;
+ uint32_t last_tx_pps = 0;
+
+ bool t_last_frame_initialised = false;
+ std::chrono::steady_clock::time_point t_last_frame;
+};
+
+}
+
diff --git a/src/output/SDRDevice.h b/src/output/SDRDevice.h
new file mode 100644
index 0000000..bd1a518
--- /dev/null
+++ b/src/output/SDRDevice.h
@@ -0,0 +1,137 @@
+/*
+ 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 <cstdint>
+#include <string>
+#include <vector>
+#include <complex>
+
+#include "TimestampDecoder.h"
+
+namespace Output {
+
+enum refclk_lock_loss_behaviour_t { CRASH, IGNORE };
+
+using complexf = std::complex<float>;
+
+/* 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;
+ std::string subDevice; // For UHD
+ std::string tx_antenna;
+ std::string rx_antenna;
+
+ 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;
+ };
+
+ virtual void tune(double lo_offset, double frequency) = 0;
+ virtual double get_tx_freq(void) const = 0;
+ virtual void set_txgain(double txgain) = 0;
+ virtual double get_txgain(void) const = 0;
+ virtual void transmit_frame(const struct FrameData& frame) = 0;
+ virtual RunStatistics get_run_statistics(void) const = 0;
+ virtual double get_real_secs(void) const = 0;
+ virtual void set_rxgain(double rxgain) = 0;
+ virtual double get_rxgain(void) const = 0;
+ virtual size_t receive_frame(
+ complexf *buf,
+ size_t num_samples,
+ struct frame_timestamp& ts,
+ double timeout_secs) = 0;
+
+
+ // Return true if GPS and reference clock inputs are ok
+ virtual bool is_clk_source_ok(void) const = 0;
+
+ virtual const char* device_name(void) const = 0;
+};
+
+} // namespace Output
diff --git a/src/output/Soapy.cpp b/src/output/Soapy.cpp
new file mode 100644
index 0000000..8ee420e
--- /dev/null
+++ b/src/output/Soapy.cpp
@@ -0,0 +1,274 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+ Queen in Right of Canada (Communications Research Center Canada)
+
+ Copyright (C) 2018
+ 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 " <<
+ std::fixed << std::setprecision(4) <<
+ m_device->getMasterClockRate()/1000.0 << " kHz";
+
+ m_device->setSampleRate(SOAPY_SDR_TX, 0, m_conf.sampleRate);
+ etiLog.level(info) << "SoapySDR:Actual TX rate: " <<
+ std::fixed << std::setprecision(4) <<
+ 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: " <<
+ std::fixed << std::setprecision(3) <<
+ m_conf.frequency / 1000.0 << " kHz.";
+
+ m_device->setGain(SOAPY_SDR_TX, 0, m_conf.txgain);
+ etiLog.level(info) << "SoapySDR:Actual tx gain: " <<
+ std::fixed << std::setprecision(2) <<
+ m_device->getGain(SOAPY_SDR_TX, 0);
+
+ if (not m_conf.tx_antenna.empty()) {
+ m_device->setAntenna(SOAPY_SDR_TX, 0, m_conf.tx_antenna);
+ }
+ etiLog.level(info) << "SoapySDR:Actual tx antenna: " <<
+ m_device->getAntenna(SOAPY_SDR_TX, 0);
+
+ const std::vector<size_t> channels({0});
+ m_tx_stream = m_device->setupStream(SOAPY_SDR_TX, "CF32", channels);
+ m_device->activateStream(m_tx_stream);
+
+ m_rx_stream = m_device->setupStream(SOAPY_SDR_RX, "CF32", channels);
+}
+
+Soapy::~Soapy()
+{
+ if (m_device != nullptr) {
+ if (m_tx_stream != nullptr) {
+ m_device->closeStream(m_tx_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) const
+{
+ 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) const
+{
+ 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) const
+{
+ 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) const
+{
+ if (m_device) {
+ long long time_ns = m_device->getHardwareTime();
+ return time_ns / 1e9;
+ }
+ else {
+ return 0.0;
+ }
+}
+
+void Soapy::set_rxgain(double rxgain)
+{
+ m_device->setGain(SOAPY_SDR_RX, 0, m_conf.rxgain);
+ m_conf.rxgain = m_device->getGain(SOAPY_SDR_RX, 0);
+}
+
+double Soapy::get_rxgain(void) const
+{
+ return m_device->getGain(SOAPY_SDR_RX, 0);
+}
+
+size_t Soapy::receive_frame(
+ complexf *buf,
+ size_t num_samples,
+ struct frame_timestamp& ts,
+ double timeout_secs)
+{
+ int flags = 0;
+ long long timeNs = ts.get_ns();
+ const size_t numElems = num_samples;
+
+ void *buffs[1];
+ buffs[0] = buf;
+
+ m_device->activateStream(m_rx_stream, flags, timeNs, numElems);
+
+ auto ret = m_device->readStream(m_tx_stream, buffs, num_samples, flags, timeNs);
+
+ m_device->deactivateStream(m_rx_stream);
+
+ // TODO update effective receive ts
+
+ if (ret < 0) {
+ throw runtime_error("Soapy readStream error: " + to_string(ret));
+ }
+
+ return ret;
+}
+
+
+bool Soapy::is_clk_source_ok() const
+{
+ // TODO
+ return true;
+}
+
+const char* Soapy::device_name(void) const
+{
+ 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_tx_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_tx_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;
+ }
+ num_frames_modulated++;
+}
+
+} // namespace Output
+
+#endif // HAVE_SOAPYSDR
+
+
diff --git a/src/output/Soapy.h b/src/output/Soapy.h
new file mode 100644
index 0000000..67b280d
--- /dev/null
+++ b/src/output/Soapy.h
@@ -0,0 +1,98 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+ Queen in Right of Canada (Communications Research Center Canada)
+
+ Copyright (C) 2018
+ 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) const override;
+ virtual void set_txgain(double txgain) override;
+ virtual double get_txgain(void) const override;
+ virtual void transmit_frame(const struct FrameData& frame) override;
+ virtual RunStatistics get_run_statistics(void) const override;
+ virtual double get_real_secs(void) const override;
+
+ virtual void set_rxgain(double rxgain) override;
+ virtual double get_rxgain(void) const override;
+ virtual size_t receive_frame(
+ complexf *buf,
+ size_t num_samples,
+ struct frame_timestamp& ts,
+ double timeout_secs) override;
+
+ // Return true if GPS and reference clock inputs are ok
+ virtual bool is_clk_source_ok(void) const override;
+ virtual const char* device_name(void) const override;
+
+ private:
+ SDRDeviceConfig& m_conf;
+ SoapySDR::Device *m_device = nullptr;
+ SoapySDR::Stream *m_tx_stream = nullptr;
+ SoapySDR::Stream *m_rx_stream = nullptr;
+
+ size_t underflows = 0;
+ size_t overflows = 0;
+ size_t late_packets = 0;
+ size_t num_frames_modulated = 0;
+};
+
+} // namespace Output
+
+#endif //HAVE_SOAPYSDR
+
diff --git a/src/output/UHD.cpp b/src/output/UHD.cpp
new file mode 100644
index 0000000..2c571fd
--- /dev/null
+++ b/src/output/UHD.cpp
@@ -0,0 +1,500 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+ Queen in Right of Canada (Communications Research Center Canada)
+
+ Copyright (C) 2018
+ 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/UHD.h"
+
+#ifdef HAVE_OUTPUT_UHD
+
+//#define MDEBUG(fmt, args...) fprintf(LOG, fmt , ## args)
+#define MDEBUG(fmt, args...)
+
+#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;
+
+namespace Output {
+
+// Maximum number of frames that can wait in frames
+static const size_t FRAMES_MAX_SIZE = 8;
+
+static 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());
+}
+
+static 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;
+ }
+ }
+}
+
+
+
+UHD::UHD(SDRDeviceConfig& config) :
+ SDRDevice(),
+ m_conf(config),
+ m_running(false)
+{
+ std::stringstream device;
+ device << m_conf.device;
+
+ if (m_conf.masterClockRate != 0) {
+ if (device.str() != "") {
+ device << ",";
+ }
+ device << "master_clock_rate=" << m_conf.masterClockRate;
+ }
+
+ MDEBUG("OutputUHD::OutputUHD(device: %s) @ %p\n",
+ device.str().c_str(), this);
+
+ 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());
+
+ m_usrp = uhd::usrp::multi_usrp::make(device.str());
+
+ etiLog.log(info, "OutputUHD:Using device: %s...",
+ m_usrp->get_pp_string().c_str());
+
+ if (m_conf.masterClockRate != 0.0) {
+ double master_clk_rate = m_usrp->get_master_clock_rate();
+ etiLog.log(debug, "OutputUHD:Checking master clock rate: %f...",
+ master_clk_rate);
+
+ if (fabs(master_clk_rate - m_conf.masterClockRate) >
+ (m_conf.masterClockRate * 1e-6)) {
+ throw std::runtime_error("Cannot set USRP master_clock_rate. Aborted.");
+ }
+ }
+
+ MDEBUG("OutputUHD:Setting REFCLK and PPS input...\n");
+
+ if (m_conf.refclk_src == "gpsdo-ettus") {
+ m_usrp->set_clock_source("gpsdo");
+ }
+ else {
+ m_usrp->set_clock_source(m_conf.refclk_src);
+ }
+ m_usrp->set_time_source(m_conf.pps_src);
+
+ m_device_time = std::make_shared<USRPTime>(m_usrp, m_conf);
+
+ if (m_conf.subDevice != "") {
+ m_usrp->set_tx_subdev_spec(uhd::usrp::subdev_spec_t(m_conf.subDevice),
+ uhd::usrp::multi_usrp::ALL_MBOARDS);
+ }
+
+ etiLog.level(debug) << "UHD clock source is " << m_usrp->get_clock_source(0);
+
+ etiLog.level(debug) << "UHD time source is " << m_usrp->get_time_source(0);
+
+ m_usrp->set_tx_rate(m_conf.sampleRate);
+ etiLog.log(debug, "OutputUHD:Set rate to %d. Actual TX Rate: %f sps...",
+ m_conf.sampleRate, m_usrp->get_tx_rate());
+
+ if (fabs(m_usrp->get_tx_rate() / m_conf.sampleRate) >
+ m_conf.sampleRate * 1e-6) {
+ throw std::runtime_error("Cannot set USRP sample rate. Aborted.");
+ }
+
+ tune(m_conf.lo_offset, m_conf.frequency);
+
+ m_conf.frequency = m_usrp->get_tx_freq();
+ etiLog.level(debug) << std::fixed << std::setprecision(3) <<
+ "OutputUHD:Actual TX frequency: " << m_conf.frequency;
+
+ etiLog.level(debug) << std::fixed << std::setprecision(3) <<
+ "OutputUHD:Actual RX frequency: " << m_usrp->get_tx_freq();
+
+ m_usrp->set_tx_gain(m_conf.txgain);
+ m_conf.txgain = m_usrp->get_tx_gain();
+ etiLog.log(debug, "OutputUHD:Actual TX Gain: %f", m_conf.txgain);
+
+ etiLog.log(debug, "OutputUHD:Mute on missing timestamps: %s",
+ m_conf.muteNoTimestamps ? "enabled" : "disabled");
+
+ m_usrp->set_rx_rate(m_conf.sampleRate);
+ etiLog.log(debug, "OutputUHD:Actual RX Rate: %f sps.", m_usrp->get_rx_rate());
+
+ if (not m_conf.rx_antenna.empty()) {
+ m_usrp->set_rx_antenna(m_conf.rx_antenna);
+ }
+ etiLog.log(debug, "OutputUHD:Actual RX Antenna: %s",
+ m_usrp->get_rx_antenna().c_str());
+
+ if (not m_conf.tx_antenna.empty()) {
+ m_usrp->set_tx_antenna(m_conf.tx_antenna);
+ }
+ etiLog.log(debug, "OutputUHD:Actual TX Antenna: %s",
+ m_usrp->get_tx_antenna().c_str());
+
+ m_usrp->set_rx_gain(m_conf.rxgain);
+ etiLog.log(debug, "OutputUHD:Actual RX Gain: %f", m_usrp->get_rx_gain());
+
+ const uhd::stream_args_t stream_args("fc32"); //complex floats
+ m_rx_stream = m_usrp->get_rx_stream(stream_args);
+ m_tx_stream = m_usrp->get_tx_stream(stream_args);
+
+ m_running.store(true);
+ m_async_rx_thread = boost::thread(&UHD::print_async_thread, this);
+
+ MDEBUG("OutputUHD:UHD ready.\n");
+}
+
+UHD::~UHD()
+{
+ stop_threads();
+}
+
+void UHD::tune(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 = m_usrp->set_tx_freq(tr);
+
+ etiLog.level(debug) << "OutputUHD: TX freq" <<
+ 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;
+
+ uhd::tune_result_t result_rx = m_usrp->set_rx_freq(tr);
+
+ etiLog.level(debug) << "OutputUHD: RX freq" <<
+ std::fixed << std::setprecision(0) <<
+ " Target RF: " << result_rx.target_rf_freq <<
+ " Actual RF: " << result_rx.actual_rf_freq <<
+ " Target DSP: " << result_rx.target_dsp_freq <<
+ " Actual DSP: " << result_rx.actual_dsp_freq;
+ }
+ else {
+ //set the centre frequency
+ etiLog.level(info) << std::fixed << std::setprecision(3) <<
+ "OutputUHD:Setting freq to " << frequency << "...";
+ m_usrp->set_tx_freq(frequency);
+
+ m_usrp->set_rx_freq(frequency);
+ }
+}
+
+double UHD::get_tx_freq(void) const
+{
+ return m_usrp->get_tx_freq();
+}
+
+void UHD::set_txgain(double txgain)
+{
+ m_usrp->set_tx_gain(txgain);
+ m_conf.txgain = m_usrp->get_tx_gain();
+}
+
+double UHD::get_txgain(void) const
+{
+ return m_usrp->get_tx_gain();
+}
+
+void UHD::transmit_frame(const struct FrameData& frame)
+{
+ 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]);
+
+ uhd::tx_metadata_t md_tx;
+
+ bool tx_allowed = true;
+
+ // muting and mutenotimestamp is handled by SDR
+ if (m_conf.enableSync and frame.ts.timestamp_valid) {
+ uhd::time_spec_t timespec(
+ frame.ts.timestamp_sec, frame.ts.pps_offset());
+ md_tx.time_spec = timespec;
+ md_tx.has_time_spec = true;
+ }
+ else {
+ md_tx.has_time_spec = false;
+ }
+
+ size_t usrp_max_num_samps = m_tx_stream->get_max_num_samps();
+ size_t num_acc_samps = 0; //number of accumulated samples
+ while (tx_allowed and m_running.load() and (num_acc_samps < sizeIn)) {
+ size_t samps_to_send = std::min(sizeIn - num_acc_samps, usrp_max_num_samps);
+
+ const bool eob_because_muting = m_conf.muting;
+
+ // ensure the the last packet has EOB set if the timestamps has been
+ // refreshed and need to be reconsidered. If muting was set, set the
+ // EOB and quit the loop afterwards, to avoid an underrun.
+ md_tx.end_of_burst = eob_because_muting or (
+ frame.ts.timestamp_valid and
+ frame.ts.timestamp_refresh and
+ samps_to_send <= usrp_max_num_samps );
+
+ //send a single packet
+ size_t num_tx_samps = m_tx_stream->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_tx.time_spec +
+ uhd::time_spec_t(0, num_tx_samps/m_conf.sampleRate);
+
+ if (num_tx_samps == 0) {
+ etiLog.log(warn,
+ "OutputUHD unable to write to device, skipping frame!");
+ break;
+ }
+
+ if (eob_because_muting) {
+ break;
+ }
+ }
+
+ num_frames_modulated++;
+}
+
+
+SDRDevice::RunStatistics UHD::get_run_statistics(void) const
+{
+ RunStatistics rs;
+ rs.num_underruns = num_underflows;
+ rs.num_overruns = num_overflows;
+ rs.num_late_packets = num_late_packets;
+ rs.num_frames_modulated = num_frames_modulated;
+ return rs;
+}
+
+double UHD::get_real_secs(void) const
+{
+ return m_usrp->get_time_now().get_real_secs();
+}
+
+void UHD::set_rxgain(double rxgain)
+{
+ m_usrp->set_rx_gain(m_conf.rxgain);
+ m_conf.rxgain = m_usrp->get_rx_gain();
+}
+
+double UHD::get_rxgain() const
+{
+ return m_usrp->get_rx_gain();
+}
+
+size_t UHD::receive_frame(
+ complexf *buf,
+ size_t num_samples,
+ struct frame_timestamp& ts,
+ double timeout_secs)
+{
+ uhd::stream_cmd_t cmd(
+ uhd::stream_cmd_t::stream_mode_t::STREAM_MODE_NUM_SAMPS_AND_DONE);
+ cmd.num_samps = num_samples;
+ cmd.stream_now = false;
+ cmd.time_spec = uhd::time_spec_t(ts.timestamp_sec, ts.pps_offset());
+
+ m_rx_stream->issue_stream_cmd(cmd);
+
+ uhd::rx_metadata_t md_rx;
+
+ constexpr double timeout = 60;
+ size_t samples_read = m_rx_stream->recv(buf, num_samples, md_rx, timeout);
+
+ // Update the ts with the effective receive TS
+ ts.timestamp_sec = md_rx.time_spec.get_full_secs();
+ ts.timestamp_pps = md_rx.time_spec.get_frac_secs() * 16384000.0;
+ return samples_read;
+}
+
+// Return true if GPS and reference clock inputs are ok
+bool UHD::is_clk_source_ok(void) const
+{
+ bool ok = true;
+
+ if (refclk_loss_needs_check()) {
+ try {
+ if (not m_usrp->get_mboard_sensor("ref_locked", 0).to_bool()) {
+ ok = false;
+
+ etiLog.level(alert) <<
+ "OutputUHD: External reference clock lock lost !";
+
+ if (m_conf.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.");
+ }
+ }
+
+ if (m_device_time) {
+ ok |= m_device_time->verify_time();
+ }
+
+ return ok;
+}
+
+const char* UHD::device_name(void) const
+{
+ return "UHD";
+}
+
+
+bool UHD::refclk_loss_needs_check() const
+{
+ if (suppress_refclk_loss_check) {
+ return false;
+ }
+ return m_conf.refclk_src != "internal";
+}
+
+void UHD::stop_threads()
+{
+ m_running.store(false);
+ if (m_async_rx_thread.joinable()) {
+ m_async_rx_thread.join();
+ }
+}
+
+
+
+void UHD::print_async_thread()
+{
+ while (m_running.load()) {
+ uhd::async_metadata_t async_md;
+ if (m_usrp->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 " <<
+ async_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 =
+ m_usrp->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_underflows_previous,
+ num_late_packets - num_late_packets_previous);
+ }
+
+ num_underflows_previous = num_underflows;
+ num_late_packets_previous = num_late_packets;
+
+ last_print_time = time_now;
+ }
+ }
+}
+
+} // namespace Output
+
+#endif // HAVE_OUTPUT_UHD
+
diff --git a/src/output/UHD.h b/src/output/UHD.h
new file mode 100644
index 0000000..b34455c
--- /dev/null
+++ b/src/output/UHD.h
@@ -0,0 +1,127 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+ Queen in Right of Canada (Communications Research Center Canada)
+
+ Copyright (C) 2018
+ 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 <chrono>
+#include <memory>
+#include <string>
+#include <atomic>
+
+#include "Log.h"
+#include "output/SDR.h"
+#include "output/USRPTime.h"
+#include "TimestampDecoder.h"
+#include "RemoteControl.h"
+#include "ThreadsafeQueue.h"
+
+#include <stdio.h>
+#include <sys/types.h>
+
+// 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 {
+
+class UHD : public Output::SDRDevice
+{
+ public:
+ UHD(SDRDeviceConfig& config);
+ UHD(const UHD& other) = delete;
+ UHD& operator=(const UHD& other) = delete;
+ ~UHD();
+
+ virtual void tune(double lo_offset, double frequency) override;
+ virtual double get_tx_freq(void) const override;
+ virtual void set_txgain(double txgain) override;
+ virtual double get_txgain(void) const override;
+ virtual void transmit_frame(const struct FrameData& frame) override;
+ virtual RunStatistics get_run_statistics(void) const override;
+ virtual double get_real_secs(void) const override;
+
+ virtual void set_rxgain(double rxgain) override;
+ virtual double get_rxgain(void) const override;
+ virtual size_t receive_frame(
+ complexf *buf,
+ size_t num_samples,
+ struct frame_timestamp& ts,
+ double timeout_secs) override;
+
+ // Return true if GPS and reference clock inputs are ok
+ virtual bool is_clk_source_ok(void) const override;
+ virtual const char* device_name(void) const override;
+
+ private:
+ SDRDeviceConfig& m_conf;
+ uhd::usrp::multi_usrp::sptr m_usrp;
+ uhd::tx_streamer::sptr m_tx_stream;
+ uhd::rx_streamer::sptr m_rx_stream;
+ std::shared_ptr<USRPTime> m_device_time;
+
+ size_t num_underflows = 0;
+ size_t num_overflows = 0;
+ size_t num_late_packets = 0;
+ size_t num_frames_modulated = 0;
+ size_t num_underflows_previous = 0;
+ size_t num_late_packets_previous = 0;
+
+ // Used to print statistics once a second
+ std::chrono::steady_clock::time_point last_print_time;
+
+ // Returns true if we want to verify loss of refclk
+ bool refclk_loss_needs_check(void) const;
+ mutable bool suppress_refclk_loss_check = false;
+
+ // Poll asynchronous metadata from UHD
+ std::atomic<bool> m_running;
+ boost::thread m_async_rx_thread;
+ void stop_threads(void);
+ void print_async_thread(void);
+};
+
+} // namespace Output
+
+#endif // HAVE_OUTPUT_UHD
+
diff --git a/src/output/USRPTime.cpp b/src/output/USRPTime.cpp
new file mode 100644
index 0000000..935d56b
--- /dev/null
+++ b/src/output/USRPTime.cpp
@@ -0,0 +1,283 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+ Queen in Right of Canada (Communications Research Center Canada)
+
+ Copyright (C) 2018
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://opendigitalradio.org
+
+DESCRIPTION:
+ The part of the UHD output that takes care of the GPSDO.
+*/
+
+/*
+ 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/USRPTime.h"
+
+#ifdef HAVE_OUTPUT_UHD
+
+//#define MDEBUG(fmt, args...) fprintf(LOG, fmt , ## args)
+#define MDEBUG(fmt, args...)
+
+namespace Output {
+
+using namespace std;
+
+
+// Check function for GPS TIMELOCK sensor from the ODR LEA-M8F board GPSDO
+static bool check_gps_timelock(uhd::usrp::multi_usrp::sptr& usrp)
+{
+ try {
+ const string sensor_value =
+ usrp->get_mboard_sensor("gps_timelock", 0).to_pp_string();
+
+ if (sensor_value.find("TIME LOCKED") == string::npos) {
+ etiLog.level(warn) << "OutputUHD: gps_timelock " << sensor_value;
+ return false;
+ }
+
+ return true;
+ }
+ catch (const uhd::lookup_error &e) {
+ etiLog.level(warn) << "OutputUHD: no gps_timelock sensor";
+ return false;
+ }
+}
+
+// Check function for GPS LOCKED sensor from the Ettus GPSDO
+static bool check_gps_locked(uhd::usrp::multi_usrp::sptr& usrp)
+{
+ try {
+ const 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 (const uhd::lookup_error &e) {
+ etiLog.level(warn) << "OutputUHD: no gps_locked sensor";
+ return false;
+ }
+}
+
+
+USRPTime::USRPTime(
+ uhd::usrp::multi_usrp::sptr usrp,
+ SDRDeviceConfig& conf) :
+ m_usrp(usrp),
+ m_conf(conf),
+ time_last_check(timepoint_t::clock::now())
+{
+ if (m_conf.pps_src == "none") {
+ if (m_conf.enableSync) {
+ etiLog.level(warn) <<
+ "OutputUHD: WARNING:"
+ " you are using synchronous transmission without PPS input!";
+ }
+
+ set_usrp_time_from_localtime();
+ }
+ else if (m_conf.pps_src == "pps" or m_conf.pps_src == "gpsdo") {
+ set_usrp_time_from_pps();
+ }
+ else {
+ throw std::runtime_error("USRPTime not implemented yet: " +
+ m_conf.pps_src);
+ }
+}
+
+bool USRPTime::verify_time()
+{
+ if (not gpsfix_needs_check()) {
+ return true;
+ }
+
+ /* During bootup, we say the gpsdo is not ok, and we poll the GPSDO until
+ * we reach lock. Then we sync time. If we do not reach lock in time, we
+ * crash.
+ *
+ * Once we are synced and we have lock, everything ok. If we lose lock for
+ * a number of seconds, we switch to the lost_fix state.
+ *
+ * In the lost fix state, we return false to get the TX muted, and we monitor.
+ * If the fix comes back, we unmute. If we reach the timeout, we crash.
+ */
+
+ check_gps();
+
+ const auto duration_without_fix =
+ gps_fix_check_interval * num_checks_without_gps_fix;
+
+ switch (gps_state) {
+ case gps_state_e::bootup:
+ if (duration_without_fix > initial_gps_fix_wait) {
+ throw runtime_error("GPS did not fix in " +
+ to_string(initial_gps_fix_wait) + " seconds");
+ }
+
+ if (num_checks_without_gps_fix == 0) {
+ if (m_conf.pps_src != "none") {
+ set_usrp_time_from_pps();
+ }
+ gps_state = gps_state_e::monitor_fix;
+ return true;
+ }
+
+ return false;
+
+ case gps_state_e::monitor_fix:
+ if (duration_without_fix > m_conf.maxGPSHoldoverTime) {
+ throw runtime_error("Lost GPS Fix for " +
+ to_string(duration_without_fix) + " seconds");
+ }
+
+ return true;
+ }
+
+ throw logic_error("End of USRPTime::verify_time() reached");
+}
+
+void USRPTime::check_gps()
+{
+ timepoint_t time_now = timepoint_t::clock::now();
+
+ // Divide interval by two because we alternate between
+ // launch and check
+ const auto checkinterval = chrono::seconds(lrint(gps_fix_check_interval/2.0));
+
+ if (gpsfix_needs_check() and time_last_check + checkinterval < time_now) {
+ time_last_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;
+ }
+ }
+ }
+ 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, m_usrp) );
+ }
+ else {
+ gps_fix_pt = boost::packaged_task<bool>(
+ boost::bind(check_gps_timelock, m_usrp) );
+ }
+ gps_fix_future = gps_fix_pt.get_future();
+
+ gps_fix_task = boost::thread(boost::move(gps_fix_pt));
+ }
+ }
+}
+
+bool USRPTime::gpsfix_needs_check() const
+{
+ if (m_conf.refclk_src == "internal") {
+ return false;
+ }
+ else if (m_conf.refclk_src == "gpsdo") {
+ return (m_conf.maxGPSHoldoverTime != 0);
+ }
+ else if (m_conf.refclk_src == "gpsdo-ettus") {
+ return (m_conf.maxGPSHoldoverTime != 0);
+ }
+ else {
+ return false;
+ }
+}
+
+bool USRPTime::gpsdo_is_ettus() const
+{
+ return (m_conf.refclk_src == "gpsdo-ettus");
+}
+
+/* Return a uhd:time_spec representing current system time
+ * with 1ms granularity. */
+static uhd::time_spec_t uhd_timespec_now(void)
+{
+ using namespace std::chrono;
+ auto n = system_clock::now();
+ const long long ticks = duration_cast<milliseconds>(n.time_since_epoch()).count();
+ return uhd::time_spec_t::from_ticks(ticks, 1000);
+}
+
+void USRPTime::set_usrp_time_from_localtime()
+{
+ const auto t = uhd_timespec_now();
+ m_usrp->set_time_now(t);
+
+ etiLog.level(info) << "OutputUHD: Setting USRP time to " <<
+ std::fixed << t.get_real_secs();
+}
+
+void USRPTime::set_usrp_time_from_pps()
+{
+ using namespace std::chrono;
+
+ /* handling time for synchronisation: wait until the next full
+ * second, and set the USRP time at next PPS */
+ auto now = uhd_timespec_now();
+ const time_t secs_since_epoch = now.get_full_secs();
+
+ while (secs_since_epoch + 1 > now.get_full_secs()) {
+ this_thread::sleep_for(milliseconds(1));
+ now = uhd_timespec_now();
+ }
+ /* We are now shortly after the second change.
+ * Wait 200ms to ensure the PPS comes later. */
+ this_thread::sleep_for(milliseconds(200));
+
+ const auto time_set = uhd::time_spec_t(secs_since_epoch + 2);
+ etiLog.level(info) << "OutputUHD: Setting USRP time next pps to " <<
+ std::fixed << time_set.get_real_secs();
+ m_usrp->set_time_next_pps(time_set);
+
+ // The UHD doc says we need to give the USRP one second to update
+ // all the internal registers.
+ this_thread::sleep_for(seconds(1));
+ etiLog.level(info) << "OutputUHD: USRP time " <<
+ std::fixed << m_usrp->get_time_now().get_real_secs();
+}
+
+} // namespace Output
+
+#endif // HAVE_OUTPUT_UHD
diff --git a/src/output/USRPTime.h b/src/output/USRPTime.h
new file mode 100644
index 0000000..7527f21
--- /dev/null
+++ b/src/output/USRPTime.h
@@ -0,0 +1,116 @@
+/*
+ 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:
+ The part of the UHD output that takes care of the GPSDO and setting device
+ time.
+*/
+
+/*
+ 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/usrp/multi_usrp.hpp>
+#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 <stdio.h>
+#include <sys/types.h>
+
+namespace Output {
+
+class USRPTime {
+ public:
+ USRPTime( uhd::usrp::multi_usrp::sptr usrp,
+ SDRDeviceConfig& conf);
+
+ // Verifies the GPSDO state, that the device time is ok.
+ // Returns true if all ok.
+ // Should be called more often than the gps_fix_check_interval
+ bool verify_time(void);
+
+ // 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
+
+ private:
+ enum class gps_state_e {
+ /* At startup, the LEA-M8F GPSDO gets issued a hotstart request to
+ * make sure we will not sync time on a PPS edge that is generated
+ * while the GPSDO is in holdover. In the bootup state, we wait for
+ * the first PPS after hotstart, and then sync time.
+ */
+ bootup,
+
+ /* Once the system is up, we check lock every now and then. If the
+ * fix is lost for too long, we crash.
+ */
+ monitor_fix,
+ };
+
+ void check_gps();
+
+ uhd::usrp::multi_usrp::sptr m_usrp;
+ SDRDeviceConfig& m_conf;
+
+ gps_state_e gps_state = gps_state_e::bootup;
+ int num_checks_without_gps_fix = 1;
+
+ using timepoint_t = std::chrono::time_point<std::chrono::steady_clock>;
+ timepoint_t time_last_check;
+
+ boost::packaged_task<bool> gps_fix_pt;
+ boost::unique_future<bool> gps_fix_future;
+ boost::thread gps_fix_task;
+
+ // 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;
+
+ void set_usrp_time_from_localtime(void);
+ void set_usrp_time_from_pps(void);
+};
+
+} // namespace Output
+
+#endif // HAVE_OUTPUT_UHD