diff options
| -rw-r--r-- | .gitignore | 119 | ||||
| -rw-r--r-- | INSTALL | 20 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | dpd/README.md | 48 | ||||
| -rw-r--r-- | dpd/dpd.ini | 25 | ||||
| -rwxr-xr-x | dpd/iq_file_server.py | 120 | ||||
| -rwxr-xr-x | dpd/main.py | 129 | ||||
| -rw-r--r-- | dpd/poly.coef | 11 | ||||
| -rwxr-xr-x | dpd/show_spectrum.py | 132 | ||||
| -rw-r--r-- | dpd/src/Adapt.py | 174 | ||||
| -rw-r--r-- | dpd/src/Agc.py | 165 | ||||
| -rw-r--r-- | dpd/src/Dab_Util.py | 232 | ||||
| -rw-r--r-- | dpd/src/Measure.py | 123 | ||||
| -rw-r--r-- | dpd/src/Model.py | 336 | ||||
| -rw-r--r-- | dpd/src/__init__.py | 0 | ||||
| -rw-r--r-- | dpd/src/phase_align.py | 74 | ||||
| -rwxr-xr-x | dpd/src/subsample_align.py | 83 | ||||
| -rw-r--r-- | dpd/src/test_dab_Util.py | 62 | ||||
| -rw-r--r-- | dpd/src/test_measure.py | 33 | ||||
| -rw-r--r-- | src/ConfigParser.cpp | 5 | ||||
| -rw-r--r-- | src/ConfigParser.h | 2 | ||||
| -rw-r--r-- | src/DabModulator.cpp | 9 | ||||
| -rw-r--r-- | src/MemlessPoly.cpp | 203 | ||||
| -rw-r--r-- | src/MemlessPoly.h | 12 | ||||
| -rw-r--r-- | src/OutputUHDFeedback.cpp | 275 | ||||
| -rw-r--r-- | src/OutputUHDFeedback.h | 1 | 
26 files changed, 2156 insertions, 239 deletions
| @@ -19,3 +19,122 @@ odr-dabmod  .*.swp  *~ + + +# Created by https://www.gitignore.io/api/vim,python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +#  Usually these files are written by a python script from a template +#  before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +### Vim ### +# swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags + +# End of https://www.gitignore.io/api/vim,python @@ -28,6 +28,10 @@ The configure script can be launch with a variety of options:   --disable-output-uhd   Disable the binding to the UHD driver for USRPs   --enable-fast-math     Compile using the -ffast-math option that gives a substantial                          speedup at the cost of correctness. + --disable-native       Do not compile ODR-DabMod with -march=native compiler option. +                        This is meant for distribution package maintainers who want to +                        use their own march option, and for people running into compilation +                        issues due to -march=native. (e.g. GCC bug 70132 on ARM systems)  Debugging options: You should not enable debug if you need good performance.  By default, debug is disabled. @@ -38,6 +42,22 @@ For more information, call:      % ./configure --help +Performance optimisation +------------------------ +While the performance of modern systems is in most cases good enough to +run ODR-DabMod, it is sometimes necessary to increase the compilation +optimisation if all features are used or on slow systems. + +Tricks for best performance: + +    * Do not use --disable-native +    * Use --enable-fast-math +    * Add -O3 to compiler flags + +Applying all together: + +    % CFLAGS="-O3" CXXFLAGS="-O3" ./configure --enable-fast-math +  Nearly as simple install procedure using repository:  ==================================================== @@ -31,6 +31,8 @@ Short list of features:  - A Telnet and ZeroMQ remote-control that can be used to change    some parameters during runtime  - ZeroMQ PUB and REP output. +- Ongoing work about digital predistortion for PA linearisation. +  See dpd/README.md  The src/ directory contains the source code of ODR-DabMod. diff --git a/dpd/README.md b/dpd/README.md index ec7cec2..b5a6b81 100644 --- a/dpd/README.md +++ b/dpd/README.md @@ -1,20 +1,56 @@ -Digital Predistortion for ODR-DabMod -==================================== +Digital Predistortion Calculation Engine for ODR-DabMod +======================================================= -This folder contains work in progress for digital predistortion. It requires: +This folder contains work in progress for digital predistortion. + +Concept +------- + +ODR-DabMod makes outgoing TX samples and feedback RX samples available for an external tool. This +external tool can request a buffer of samples for analysis, can calculate coefficients for the +polynomial predistorter in ODR-DabMod and load the new coefficients using the remote control. + +The *dpd/main.py* script is the entry point for the *DPD Calculation Engine* into which these +features will be implemented. The tool uses modules from the *dpd/src/* folder: + +- Sample transfer and time alignment with subsample accuracy is done by *Measure.py* +- Estimating the effects of the PA using some model and calculation of the updated +  polynomial coefficients is done in *Model.py* +- Finally, *Adapt.py* loads them into ODR-DabMod. + +These modules themselves use additional helper scripts in the *dpd/src/* folder. + +Requirements +------------  - USRP B200.  - Power amplifier.  - A feedback connection from the power amplifier output, at an appropriate power level for the B200. -  Usually this is done with a directional coupler. -- ODR-DabMod with enabled dpd_port, and with a samplerate of 8192000 samples per second. +  Usually this is done with a directional coupler and additional attenuators. +- ODR-DabMod with enabled *dpd_port*, and with a samplerate of 8192000 samples per second.  - Synchronous=1 so that the USRP has the timestamping set properly, internal refclk and pps    are sufficient for this example.  - A live mux source with TIST enabled.  See dpd/dpd.ini for an example. +The DPD server port can be tested with the *dpd/show_spectrum.py* helper tool, which can also display +a constellation diagram. + +File format for coefficients +---------------------------- +The coef file contains the polynomial coefficients used in the predistorter. The file format is +very similar to the filtertaps file used in the FIR filter. It is a text-based format that can +easily be inspected and edited in a text editor. + +The first line contains the number of coefficients as an integer. The second and third lines contain +the real, respectively the imaginary parts of the first coefficient. Fourth and fifth lines give the +second coefficient, and so on. The file therefore contains 2xN + 1 lines if it contains N +coefficients. +  TODO  ---- -Implement a PA model that updates the predistorter. +Implement a PA model. +Implement cases for different oversampling for FFT bin choice. +Fix loads of missing and buggy aspects of the implementation. diff --git a/dpd/dpd.ini b/dpd/dpd.ini index 5e809e5..1bc51de 100644 --- a/dpd/dpd.ini +++ b/dpd/dpd.ini @@ -6,35 +6,48 @@ zmqctrlendpoint=tcp://127.0.0.1:9400  [log]  syslog=0 -filelog=0 -filename=/dev/stderr +filelog=1 +filename=/tmp/dabmod.log  [input]  transport=tcp  source=localhost:9200  [modulator] -digital_gain=0.8 +gainmode=var  rate=8192000  [firfilter] -enabled=0 +enabled=1 + +[poly] +enabled=1 +polycoeffile=dpd/poly.coef + +# How many threads to use for the predistorter. +# If not set, detect automatically. +#num_threads=2  [output] +# to prepare a file for the dpd/iq_file_server.py script, +# use output=file  output=uhd +[fileoutput] +filename=dpd.iq +  [uhdoutput]  device=  master_clock_rate=32768000  type=b200 -txgain=75 +txgain=15  channel=13C  refclk_source=internal  pps_source=none  behaviour_refclk_lock_lost=ignore  max_gps_holdover_time=600  dpd_port=50055 -rxgain=0 +rxgain=15  [delaymanagement]  ; Use synchronous=1 so that the USRP time is set. This works diff --git a/dpd/iq_file_server.py b/dpd/iq_file_server.py new file mode 100755 index 0000000..7a4e570 --- /dev/null +++ b/dpd/iq_file_server.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# This example server simulates the ODR-DabMod's +# DPD server, taking samples from an IQ file +# +# http://www.opendigitalradio.org +# Licence: The MIT License, see notice at the end of this file + +import sys +import socket +import struct +import argparse +import numpy as np +from datetime import datetime + +SIZEOF_SAMPLE = 8 # complex floats +# Constants for TM 1 +NbSymbols = 76 +NbCarriers = 1536 +Spacing = 2048 +NullSize = 2656 +SymSize = 2552 +FicSizeOut = 288 +FrameSize = NullSize + NbSymbols*SymSize + +def main(): +    parser = argparse.ArgumentParser(description="Simulate ODR-DabMod DPD server") +    parser.add_argument('--port', default='50055', +            help='port to listen on (default: 50055)', +            required=False) +    parser.add_argument('--file', help='I/Q File to read from (complex floats)', +            required=True) +    parser.add_argument('--samplerate', default='8192000', help='Sample rate', +            required=False) + +    cli_args = parser.parse_args() + +    serve_file(cli_args) + +def recv_exact(sock, num_bytes): +    bufs = [] +    while num_bytes > 0: +        b = sock.recv(num_bytes) +        if len(b) == 0: +            break +        num_bytes -= len(b) +        bufs.append(b) +    return b''.join(bufs) + +def serve_file(options): +    oversampling = int(int(options.samplerate) / 2048000) +    consumesamples = 8*FrameSize * oversampling +    iq_data = np.fromfile(options.file, count=consumesamples, dtype=np.complex64) + +    print("Loaded {} samples of IQ data".format(len(iq_data))) + +    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +    s.bind(('localhost', int(options.port))) +    s.listen() + +    try: +        while True: +            sock, addr_info = s.accept() +            print("Got a connection from {}".format(addr_info)) + +            ver = recv_exact(sock, 1) +            (num_samps,) = struct.unpack("=I", recv_exact(sock, 4)) +            num_bytes = num_samps * SIZEOF_SAMPLE + +            if num_bytes > len(iq_data): +                print("Truncating length to {} samples".format(len(iq_data))) +                num_samps = len(iq_data) +                num_bytes = num_samps * 4 + +            tx_sec = datetime.now().timestamp() +            tx_pps = int(16384000 * (tx_sec - int(tx_sec))) +            tx_second = int(tx_sec) + +            # TX metadata and data +            sock.sendall(struct.pack("=III", num_samps, tx_second, tx_pps)) +            sock.sendall(iq_data[-num_samps:].tobytes()) + +            # RX metadata and data +            rx_second = tx_second + 1 +            rx_pps = tx_pps +            sock.sendall(struct.pack("=III", num_samps, rx_second, rx_pps)) +            sock.sendall(iq_data[-num_samps:].tobytes()) + +            print("Sent {} samples".format(num_samps)) + +            sock.close() +    finally: +        s.close() +        raise + +main() + + +# The MIT License (MIT) +# +# Copyright (c) 2017 Matthias P. Braendli +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/dpd/main.py b/dpd/main.py new file mode 100755 index 0000000..8af6700 --- /dev/null +++ b/dpd/main.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# DPD Calculation Engine main file. +# +# http://www.opendigitalradio.org +# Licence: The MIT License, see notice at the end of this file + +"""This Python script is the main file for ODR-DabMod's DPD Computation Engine. +This engine calculates and updates the parameter of the digital +predistortion module of ODR-DabMod.""" + +import datetime +import os + +import logging +dt = datetime.datetime.now().isoformat() +logging_path = "/tmp/dpd_{}".format(dt).replace(".","_").replace(":","-") +os.makedirs(logging_path) +logging.basicConfig(format='%(asctime)s - %(module)s - %(levelname)s - %(message)s', +                    datefmt='%Y-%m-%d %H:%M:%S', +                    filename='{}/dpd.log'.format(logging_path), +                    filemode='w', +                    level=logging.DEBUG) + +import traceback +import src.Measure as Measure +import src.Model as Model +import src.Adapt as Adapt +import src.Agc as Agc +import argparse + +parser = argparse.ArgumentParser(description="DPD Computation Engine for ODR-DabMod") +parser.add_argument('--port', default='50055', +        help='port of DPD server to connect to (default: 50055)', +        required=False) +parser.add_argument('--rc-port', default='9400', +        help='port of ODR-DabMod ZMQ Remote Control to connect to (default: 9400)', +        required=False) +parser.add_argument('--samplerate', default='8192000', +        help='Sample rate', +        required=False) +parser.add_argument('--coefs', default='poly.coef', +        help='File with DPD coefficients, which will be read by ODR-DabMod', +        required=False) +parser.add_argument('--txgain', default=65, +                    help='TX Gain', +                    required=False, +                    type=int) +parser.add_argument('--rxgain', default=30, +                    help='TX Gain', +                    required=False, +                    type=int) +parser.add_argument('--samps', default='10240', +        help='Number of samples to request from ODR-DabMod', +        required=False) +parser.add_argument('-i', '--iterations', default='1', +        help='Number of iterations to run', +        required=False) +parser.add_argument('-l', '--load-poly', +        help='Load existing polynomial', +        action="store_true") + +cli_args = parser.parse_args() + +port = int(cli_args.port) +port_rc = int(cli_args.rc_port) +coef_path = cli_args.coefs +txgain = cli_args.txgain +rxgain = cli_args.rxgain +num_req = int(cli_args.samps) +samplerate = int(cli_args.samplerate) +num_iter = int(cli_args.iterations) + +meas = Measure.Measure(samplerate, port, num_req) + +adapt = Adapt.Adapt(port_rc, coef_path) +coefs_am, coefs_pm = adapt.get_coefs() +if cli_args.load_poly: +    model = Model.Model(coefs_am, coefs_pm) +else: +    model = Model.Model([1, 0, 0, 0, 0], [0, 0, 0, 0, 0]) +adapt.set_txgain(txgain) +adapt.set_rxgain(rxgain) + +tx_gain = adapt.get_txgain() +rx_gain = adapt.get_rxgain() +dpd_coefs_am, dpd_coefs_pm = adapt.get_coefs() +logging.info( +    "TX gain {}, RX gain {}, dpd_coefs_am {}, dpd_coefs_pm {}".format( +        tx_gain, rx_gain, dpd_coefs_am, dpd_coefs_pm +    ) +) + +# Automatic Gain Control +agc = Agc.Agc(meas, adapt) +agc.run() + +for i in range(num_iter): +    try: +        txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median = meas.get_samples() +        logging.debug("tx_ts {}, rx_ts {}".format(tx_ts, rx_ts)) +        coefs_am, coefs_pm = model.get_next_coefs(txframe_aligned, rxframe_aligned) +        adapt.set_coefs(coefs_am, coefs_pm) +    except Exception as e: +        logging.warning("Iteration {} failed.".format(i)) +        logging.warning(traceback.format_exc()) + +# The MIT License (MIT) +# +# Copyright (c) 2017 Andreas Steger, Matthias P. Braendli +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/dpd/poly.coef b/dpd/poly.coef new file mode 100644 index 0000000..f65e369 --- /dev/null +++ b/dpd/poly.coef @@ -0,0 +1,11 @@ +5 +1 +0 +0 +0 +0 +0 +0 +0 +0 +0 diff --git a/dpd/show_spectrum.py b/dpd/show_spectrum.py index e92c1d0..f23dba2 100755 --- a/dpd/show_spectrum.py +++ b/dpd/show_spectrum.py @@ -1,16 +1,15 @@  #!/usr/bin/env python  # -*- coding: utf-8 -*-  # -# This is an example tool that shows how to connect to ODR-DabMod's dpd TCP server -# and get samples from there. +# This is an example tool that shows how to connect to ODR-DabMod's dpd TCP +# server and get samples from there.  # -# Since the TX and RX samples are not perfectly aligned, the tool has to align them properly, -# which is done in two steps: First on sample-level using a correlation, then with subsample -# accuracy using a FFT approach. +# Since the TX and RX samples are not perfectly aligned, the tool has to align +# them properly, which is done in two steps: First on sample-level using a +# correlation, then with subsample accuracy using a FFT approach.  #  # It requires SciPy and matplotlib.  # -# Copyright (C) 2017 Matthias P. Braendli  # http://www.opendigitalradio.org  # Licence: The MIT License, see notice at the end of this file @@ -21,9 +20,18 @@ import numpy as np  import matplotlib.pyplot as pp  from matplotlib.animation import FuncAnimation  import argparse +from scipy.misc import imsave  SIZEOF_SAMPLE = 8 # complex floats +# Constants for TM 1 +NbSymbols = 76 +NbCarriers = 1536 +Spacing = 2048 +NullSize = 2656 +SymSize = 2552 +FicSizeOut = 288 +  def main():      parser = argparse.ArgumentParser(description="Plot the spectrum of ODR-DabMod's DPD feedback")      parser.add_argument('--samps', default='10240', help='Number of samples to request at once', @@ -31,18 +39,19 @@ def main():      parser.add_argument('--port', default='50055',              help='port to connect to ODR-DabMod DPD (default: 50055)',              required=False) -      parser.add_argument('--animated', action='store_true', help='Enable real-time animation') +    parser.add_argument('--constellation', action='store_true', help='Draw constellaton plot') +    parser.add_argument('--samplerate', default='8192000', help='Sample rate', +            required=False)      cli_args = parser.parse_args() -    port = int(cli_args.port) -    num_samps_to_request = int(cli_args.samps) - -    if cli_args.animated: -        plot_spectrum_animated(port, num_samps_to_request) +    if cli_args.constellation: +        plot_constellation_once(cli_args) +    elif cli_args.animated: +        plot_spectrum_animated(cli_args)      else: -        plot_spectrum_once(port, num_samps_to_request) +        plot_spectrum_once(cli_args)  def recv_exact(sock, num_bytes):      bufs = [] @@ -82,6 +91,7 @@ def get_samples(port, num_samps_to_request):      else:          txframe = np.array([], dtype=np.complex64) +      print("Wait for RX metadata")      rx_second, rx_pps = struct.unpack("=II", recv_exact(s, 8))      rx_ts = rx_second + rx_pps / 16384000.0 @@ -98,7 +108,7 @@ def get_samples(port, num_samps_to_request):      return (tx_ts, txframe, rx_ts, rxframe) -def get_spectrum(port, num_samps_to_request): +def recv_rxtx(port, num_samps_to_request):      tx_ts, txframe, rx_ts, rxframe = get_samples(port, num_samps_to_request)      # convert to complex doubles for more dynamic range @@ -107,6 +117,10 @@ def get_spectrum(port, num_samps_to_request):      print("Received {} & {} frames at {} and {}".format(          len(txframe), len(rxframe), tx_ts, rx_ts)) +    return tx_ts, txframe, rx_ts, rxframe + +def get_spectrum(port, num_samps_to_request): +    tx_ts, txframe, rx_ts, rxframe = recv_rxtx(port, num_samps_to_request)      print("Calculate TX and RX spectrum assuming 8192000 samples per second")      tx_spectrum = np.fft.fftshift(np.fft.fft(txframe, fft_size)) @@ -116,12 +130,90 @@ def get_spectrum(port, num_samps_to_request):      rx_power = 20*np.log10(np.abs(rx_spectrum))      return tx_power, rx_power +def remove_guard_intervals(frame, options): +    """Remove the cyclic prefix. The frame needs to be aligned to the +    end of the transmission frame. Transmission Mode 1 is assumed""" +    oversample = int(int(options.samplerate) / 2048000) + +    # From the end, take 2048 samples, then skip 504 samples +    frame = frame[::-1] + +    stride_len = Spacing * oversample +    stride_advance = SymSize * oversample + +    # Truncate the frame to an integer length of strides +    newlen = len(frame) - (len(frame) % stride_advance) +    print("Truncating frame from {} to {}".format(len(frame), newlen)) +    frame = frame[:newlen] + +    # Remove the cyclic prefix +    frame = frame.reshape(-1, stride_advance)[:,:stride_len].reshape(-1) + +    # Reverse again +    return frame[::-1] + + +def plot_constellation_once(options): +    port = int(options.port) +    num_samps_to_request = int(options.samps) + +    tx_ts, txframe, rx_ts, rxframe = recv_rxtx(port, num_samps_to_request) + +    frame = remove_guard_intervals(txframe, options) + +    oversample = int(int(options.samplerate) / 2048000) + +    n = Spacing * oversample # is also number of samples per symbol +    if len(frame) % n != 0: +        raise ValueError("Frame length doesn't contain exact number of symbols") +    num_syms = int(len(frame) / n) +    print("frame {} has {} symbols".format(len(frame), num_syms)) +    spectrums = np.array([np.fft.fftshift(np.fft.fft(frame[n*i:n*(i+1)], n)) for i in range(num_syms)]) + +    def normalise(x): +        """Normalise a real-valued array x to the range [0,1]""" +        y = x + np.min(x) +        return x / np.max(x) + +    imsave("spectrums.png", np.concatenate([ +        normalise(np.abs(spectrums)), +        normalise(np.angle(spectrums))])) + +    # Only take bins that are supposed to contain energy +    # i.e. the middle 1536 bins, excluding the bin at n/2 +    assert(n % 2 == 0) +    n_half = int(n/2) +    spectrums = np.concatenate( +            [spectrums[...,n_half-768:n_half], +             spectrums[...,n_half + 1:n_half + 769]], axis=1) + +    sym_indices = (np.tile(np.arange(num_syms-1).reshape(num_syms-1,1), (1,NbCarriers)) + +                   np.tile(np.linspace(-0.4, 0.4, NbCarriers), (num_syms-1, 1) ) ) +    sym_indices = sym_indices.reshape(-1) +    diff_angles = np.mod(np.diff(np.angle(spectrums, deg=1), axis=0), 360) +    #sym_points = spectrums[:-1].reshape(-1) +    # Set amplitude and phase of low power points to zero, avoid cluttering diagram +    #sym_points[np.abs(sym_points) < np.mean(np.abs(sym_points)) * 0.1] = 0 + +    print("ix {}  spec {} da {}".format( +        sym_indices.shape, spectrums.shape, diff_angles.shape)) + +    fig = pp.figure() + +    fig.suptitle("Constellation") +    ax1 = fig.add_subplot(111) +    ax1.set_title("TX") +    ax1.scatter(sym_indices, diff_angles.reshape(-1), alpha=0.1) + +    pp.show() -sampling_rate = 8192000  fft_size = 4096 -freqs = np.fft.fftshift(np.fft.fftfreq(fft_size, d=1./sampling_rate)) -def plot_spectrum_once(port, num_samps_to_request): +def plot_spectrum_once(options): +    port = int(options.port) +    num_samps_to_request = int(options.samps) +    freqs = np.fft.fftshift(np.fft.fftfreq(fft_size, d=1./int(options.samplerate))) +      tx_power, rx_power = get_spectrum(port, num_samps_to_request)      fig = pp.figure() @@ -134,7 +226,11 @@ def plot_spectrum_once(port, num_samps_to_request):      ax2.plot(freqs, rx_power, 'b')      pp.show() -def plot_spectrum_animated(port, num_samps_to_request): +def plot_spectrum_animated(options): +    port = int(options.port) +    num_samps_to_request = int(options.samps) +    freqs = np.fft.fftshift(np.fft.fftfreq(fft_size, d=1./int(options.samplerate))) +      fig, axes = pp.subplots(2, sharex=True)      line1, = axes[0].plot(freqs, np.ones(len(freqs)), 'r', animated=True)      axes[0].set_title("TX") diff --git a/dpd/src/Adapt.py b/dpd/src/Adapt.py new file mode 100644 index 0000000..2fb596f --- /dev/null +++ b/dpd/src/Adapt.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# +# DPD Calculation Engine: updates ODR-DabMod's predistortion block. +# +# http://www.opendigitalradio.org +# Licence: The MIT License, see notice at the end of this file +""" +This module is used to change settings of ODR-DabMod using +the ZMQ remote control socket. +""" + +import zmq +import logging +import numpy as np + +class Adapt: +    """Uses the ZMQ remote control to change parameters of the DabMod + +    Parameters +    ---------- +    port : int +        Port at which the ODR-DabMod is listening to connect the +        ZMQ remote control. +    """ + +    def __init__(self, port, coef_path): +        logging.info("Instantiate Adapt object") +        self.port = port +        self.coef_path = coef_path +        self.host = "localhost" +        self._context = zmq.Context() + +    def _connect(self): +        """Establish the connection to ODR-DabMod using +        a ZMQ socket that is in request mode (Client). +        Returns a socket""" +        sock = self._context.socket(zmq.REQ) +        sock.connect("tcp://%s:%d" % (self.host, self.port)) + +        sock.send(b"ping") +        data = [el.decode() for el in sock.recv_multipart()] + +        if data != ['ok']: +            raise RuntimeError( +                    "Could not ping server at %s %d: %s" % +                    (self.host, self.port, data)) + +        return sock + +    def send_receive(self, message): +        """Send a message to ODR-DabMod. It always +        returns the answer ODR-DabMod sends back. + +        An example message could be +        "get uhd txgain" or "set uhd txgain 50" + +        Parameter +        --------- +        message : str +            The message string that will be sent to the receiver. +        """ +        sock = self._connect() +        logging.info("Send message: %s" % message) +        msg_parts = message.split(" ") +        for i, part in enumerate(msg_parts): +            if i == len(msg_parts) - 1: +                f = 0 +            else: +                f = zmq.SNDMORE + +            sock.send(part.encode(), flags=f) + +        data = [el.decode() for el in sock.recv_multipart()] +        logging.info("Received message: %s" % message) +        return data + +    def set_txgain(self, gain): +        """Set a new txgain for the ODR-DabMod. + +        Parameters +        ---------- +        gain : int +            new TX gain, in the same format as ODR-DabMod's config file +        """ +        # TODO this is specific to the B200 +        if gain < 0 or gain > 89: +            raise ValueError("Gain has to be in [0,89]") +        return self.send_receive("set uhd txgain %d" % gain) + +    def get_txgain(self): +        """Get the txgain value in dB for the ODR-DabMod.""" +        # TODO handle failure +        return self.send_receive("get uhd txgain") + +    def set_rxgain(self, gain): +        """Set a new rxgain for the ODR-DabMod. + +        Parameters +        ---------- +        gain : int +            new RX gain, in the same format as ODR-DabMod's config file +        """ +        # TODO this is specific to the B200 +        if gain < 0 or gain > 89: +            raise ValueError("Gain has to be in [0,89]") +        return self.send_receive("set uhd rxgain %d" % gain) + +    def get_rxgain(self): +        """Get the rxgain value in dB for the ODR-DabMod.""" +        # TODO handle failure +        return self.send_receive("get uhd rxgain") + +    def _read_coef_file(self, path): +        """Load the coefficients from the file in the format given in the README, +        return ([AM coef], [PM coef])""" +        coefs_am_out = [] +        coefs_pm_out = [] +        f = open(path, 'r') +        lines = f.readlines() +        n_coefs = int(lines[0]) +        coefs = [float(l) for l in lines[1:]] +        i = 0 +        for c in coefs: +            if i < n_coefs: +                coefs_am_out.append(c) +            elif i < 2*n_coefs: +                coefs_pm_out.append(c) +            else: +                raise ValueError( +                    "Incorrect coef file format: too many coefficients in {}, should be {}, coefs are {}" +                        .format(path, n_coefs, coefs)) +            i += 1 +        f.close() +        return (coefs_am_out, coefs_pm_out) + +    def get_coefs(self): +        return self._read_coef_file(self.coef_path) + +    def _write_coef_file(self, coefs_am, coefs_pm, path): +        assert(len(coefs_am) == len(coefs_pm)) + +        f = open(path, 'w') +        f.write("{}\n".format(len(coefs_am))) +        for coef in coefs_am: +            f.write("{}\n".format(coef)) +        for coef in coefs_pm: +            f.write("{}\n".format(coef)) +        f.close() + +    def set_coefs(self, coefs_am, coefs_pm): +        self._write_coef_file(coefs_am, coefs_pm, self.coef_path) +        self.send_receive("set memlesspoly coeffile {}".format(self.coef_path)) + +# The MIT License (MIT) +# +# Copyright (c) 2017 Andreas Steger, Matthias P. Braendli +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/dpd/src/Agc.py b/dpd/src/Agc.py new file mode 100644 index 0000000..1fd11c8 --- /dev/null +++ b/dpd/src/Agc.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# +# Automatic Gain Control +# +# http://www.opendigitalradio.org +# Licence: The MIT License, see notice at the end of this file + +import datetime +import os +import logging +import time +logging_path = os.path.dirname(logging.getLoggerClass().root.handlers[0].baseFilename) + +import numpy as np +import matplotlib +matplotlib.use('agg') +import matplotlib.pyplot as plt + +import src.Adapt as Adapt +import src.Measure as Measure + +class Agc: +    """The goal of the automatic gain control is to set the  +    RX gain to a value at which all received amplitudes can  +    be detected. This means that the maximum possible amplitude  +    should be quantized at the highest possible digital value. + +    A problem we have to face, is that the estimation of the  +    maximum amplitude by applying the max() function is very  +    unstable. This is due to the maximum’s rareness. Therefore  +    we estimate a far more robust value, such as the median,  +    and then approximate the maximum amplitude from it. + +    Given this, we tune the RX gain in such a way, that the  +    received signal fulfills our desired property, of having  +    all amplitudes properly quantized.""" + +    def __init__(self, measure, adapt, min_rxgain=25, peak_to_median=20): +        assert isinstance(measure, Measure.Measure) +        assert isinstance(adapt, Adapt.Adapt) +        self.measure = measure +        self.adapt = adapt +        self.min_rxgain = min_rxgain +        self.rxgain = self.min_rxgain +        self.peak_to_median = peak_to_median + +    def run(self): +        self.adapt.set_rxgain(self.rxgain) + +        for i in range(3): +            # Measure +            txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median= \ +                self.measure.get_samples() + +            # Estimate Maximum +            rx_peak = self.peak_to_median * rx_median +            correction_factor = 20*np.log10(1/rx_peak) +            self.rxgain = self.rxgain + correction_factor + +            assert self.min_rxgain <= self.rxgain, ("Desired RX Gain is {} which is smaller than the minimum of {}".format( +                self.rxgain, self.min_rxgain)) + +            logging.info("RX Median {:1.4f}, estimated peak {:1.4f}, correction factor {:1.4f}, new RX gain {:1.4f}".format( +            rx_median, rx_peak, correction_factor, self.rxgain +            )) + +            self.adapt.set_rxgain(self.rxgain) +            time.sleep(1) + +    def plot_estimates(self): +        """Plots the estimate of for Max, Median, Mean for different +        number of samples.""" +        self.adapt.set_rxgain(self.min_rxgain) +        time.sleep(1) + +        dt = datetime.datetime.now().isoformat() +        fig_path = logging_path + "/" + dt + "_agc.pdf" +        fig, axs = plt.subplots(2, 2, figsize=(3*6,1*6)) +        axs = axs.ravel() + +        for j in range(5): +            txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median =\ +                self.measure.get_samples() + +            rxframe_aligned_abs = np.abs(rxframe_aligned) + +            x = np.arange(100, rxframe_aligned_abs.shape[0], dtype=int) +            rx_max_until = [] +            rx_median_until = [] +            rx_mean_until = [] +            for i in x: +                rx_max_until.append(np.max(rxframe_aligned_abs[:i])) +                rx_median_until.append(np.median(rxframe_aligned_abs[:i])) +                rx_mean_until.append(np.mean(rxframe_aligned_abs[:i])) + +            axs[0].plot(x, +                    rx_max_until, +                    label="Run {}".format(j+1), +                    color=matplotlib.colors.hsv_to_rgb((1./(j+1.),0.8,0.7)), +                    linestyle="-", linewidth=0.25) +            axs[0].set_xlabel("Samples") +            axs[0].set_ylabel("Amplitude") +            axs[0].set_title("Estimation for Maximum RX Amplitude") +            axs[0].legend() + +            axs[1].plot(x, +                    rx_median_until, +                    label="Run {}".format(j+1), +                    color=matplotlib.colors.hsv_to_rgb((1./(j+1.),0.9,0.7)), +                    linestyle="-", linewidth=0.25) +            axs[1].set_xlabel("Samples") +            axs[1].set_ylabel("Amplitude") +            axs[1].set_title("Estimation for Median RX Amplitude") +            axs[1].legend() +            ylim_1 = axs[1].get_ylim() + +            axs[2].plot(x, +                    rx_mean_until, +                    label="Run {}".format(j+1), +                    color=matplotlib.colors.hsv_to_rgb((1./(j+1.),0.9,0.7)), +                    linestyle="-", linewidth=0.25) +            axs[2].set_xlabel("Samples") +            axs[2].set_ylabel("Amplitude") +            axs[2].set_title("Estimation for Mean RX Amplitude") +            ylim_2 = axs[2].get_ylim() +            axs[2].legend() + +            axs[1].set_ylim(min(ylim_1[0], ylim_2[0]), +                            max(ylim_1[1], ylim_2[1])) + +            fig.tight_layout() +            fig.savefig(fig_path) + +        axs[3].hist(rxframe_aligned_abs, bins=60) +        axs[3].set_xlabel("Amplitude") +        axs[3].set_ylabel("Frequency") +        axs[3].set_title("Histogram of Amplitudes") +        axs[3].legend() + +        fig.tight_layout() +        fig.savefig(fig_path) +        fig.clf() + + +# The MIT License (MIT) +# +# Copyright (c) 2017 Andreas Steger +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/dpd/src/Dab_Util.py b/dpd/src/Dab_Util.py new file mode 100644 index 0000000..175b744 --- /dev/null +++ b/dpd/src/Dab_Util.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- + +import datetime +import os +import logging +logging_path = os.path.dirname(logging.getLoggerClass().root.handlers[0].baseFilename) + +import numpy as np +import scipy +import matplotlib +matplotlib.use('agg') +import matplotlib.pyplot as plt +import src.subsample_align as sa +import src.phase_align as pa +from scipy import signal + +class Dab_Util: +    """Collection of methods that can be applied to an array +     complex IQ samples of a DAB signal +     """ +    def __init__(self, sample_rate): +        """ +        :param sample_rate: sample rate [sample/sec] to use for calculations +        """ +        self.sample_rate = sample_rate +        self.dab_bandwidth = 1536000 #Bandwidth of a dab signal +        self.frame_ms = 96           #Duration of a Dab frame + +    def lag(self, sig_orig, sig_rec): +        """ +        Find lag between two signals +        Args: +            sig_orig: The signal that has been sent +            sig_rec: The signal that has been recored +        """ +        off = sig_rec.shape[0] +        c = np.abs(signal.correlate(sig_orig, sig_rec)) + +        if logging.getLogger().getEffectiveLevel() == logging.DEBUG: +            dt = datetime.datetime.now().isoformat() +            corr_path = (logging_path + "/" + dt + "_tx_rx_corr.pdf") +            plt.plot(c, label="corr") +            plt.legend() +            plt.savefig(corr_path) +            plt.clf() + +        return np.argmax(c) - off + 1 + +    def lag_upsampling(self, sig_orig, sig_rec, n_up): +        if n_up != 1: +            sig_orig_up = signal.resample(sig_orig, sig_orig.shape[0] * n_up) +            sig_rec_up  = signal.resample(sig_rec, sig_rec.shape[0] * n_up) +        else: +            sig_orig_up = sig_orig +            sig_rec_up  = sig_rec +        l = self.lag(sig_orig_up, sig_rec_up) +        l_orig = float(l) / n_up +        return l_orig + +    def subsample_align_upsampling(self, sig1, sig2, n_up=32): +        """ +        Returns an aligned version of sig1 and sig2 by cropping and subsample alignment +        Using upsampling +        """ +        assert(sig1.shape[0] == sig2.shape[0]) + +        if sig1.shape[0] % 2 == 1: +            sig1 = sig1[:-1] +            sig2 = sig2[:-1] + +        sig1_up = signal.resample(sig1, sig1.shape[0] * n_up) +        sig2_up = signal.resample(sig2, sig2.shape[0] * n_up) + +        off_meas = self.lag_upsampling(sig2_up, sig1_up, n_up=1) +        off = int(abs(off_meas)) + +        if off_meas > 0: +            sig1_up = sig1_up[:-off] +            sig2_up = sig2_up[off:] +        elif off_meas < 0: +            sig1_up = sig1_up[off:] +        sig2_up = sig2_up[:-off] + +        sig1 = signal.resample(sig1_up, sig1_up.shape[0] / n_up).astype(np.complex64) +        sig2 = signal.resample(sig2_up, sig2_up.shape[0] / n_up).astype(np.complex64) +        return sig1, sig2 + +    def subsample_align(self, sig1, sig2): +        """ +        Returns an aligned version of sig1 and sig2 by cropping and subsample alignment +        """ + +        if logging.getLogger().getEffectiveLevel() == logging.DEBUG: +            dt = datetime.datetime.now().isoformat() +            fig_path = logging_path + "/" + dt + "_sync_raw.pdf" + +            fig, axs = plt.subplots(2) +            axs[0].plot(np.abs(sig1[:128]), label="TX Frame") +            axs[0].plot(np.abs(sig2[:128]), label="RX Frame") +            axs[0].set_title("Raw Data") +            axs[0].set_ylabel("Amplitude") +            axs[0].set_xlabel("Samples") +            axs[0].legend(loc=4) + +            axs[1].plot(np.real(sig1[:128]), label="TX Frame") +            axs[1].plot(np.real(sig2[:128]), label="RX Frame") +            axs[1].set_title("Raw Data") +            axs[1].set_ylabel("Real Part") +            axs[1].set_xlabel("Samples") +            axs[1].legend(loc=4) + +            fig.tight_layout() +            fig.savefig(fig_path) +            fig.clf() + +        logging.debug("Sig1_orig: %d %s, Sig2_orig: %d %s" % (len(sig1), sig1.dtype, len(sig2), sig2.dtype)) +        off_meas = self.lag_upsampling(sig2, sig1, n_up=1) +        off = int(abs(off_meas)) + +        if off_meas > 0: +            sig1 = sig1[:-off] +            sig2 = sig2[off:] +        elif off_meas < 0: +            sig1 = sig1[off:] +            sig2 = sig2[:-off] + +        if off % 2 == 1: +            sig1 = sig1[:-1] +            sig2 = sig2[:-1] + +        if logging.getLogger().getEffectiveLevel() == logging.DEBUG: +            dt = datetime.datetime.now().isoformat() +            fig_path = logging_path + "/" + dt + "_sync_sample_aligned.pdf" + +            fig, axs = plt.subplots(2) +            axs[0].plot(np.abs(sig1[:128]), label="TX Frame") +            axs[0].plot(np.abs(sig2[:128]), label="RX Frame") +            axs[0].set_title("Sample Aligned Data") +            axs[0].set_ylabel("Amplitude") +            axs[0].set_xlabel("Samples") +            axs[0].legend(loc=4) + +            axs[1].plot(np.real(sig1[:128]), label="TX Frame") +            axs[1].plot(np.real(sig2[:128]), label="RX Frame") +            axs[1].set_ylabel("Real Part") +            axs[1].set_xlabel("Samples") +            axs[1].legend(loc=4) + +            fig.tight_layout() +            fig.savefig(fig_path) +            fig.clf() + + +        sig2 = sa.subsample_align(sig2, sig1) + +        if logging.getLogger().getEffectiveLevel() == logging.DEBUG: +            dt = datetime.datetime.now().isoformat() +            fig_path = logging_path + "/" + dt + "_sync_subsample_aligned.pdf" + +            fig, axs = plt.subplots(2) +            axs[0].plot(np.abs(sig1[:128]), label="TX Frame") +            axs[0].plot(np.abs(sig2[:128]), label="RX Frame") +            axs[0].set_title("Subsample Aligned") +            axs[0].set_ylabel("Amplitude") +            axs[0].set_xlabel("Samples") +            axs[0].legend(loc=4) + +            axs[1].plot(np.real(sig1[:128]), label="TX Frame") +            axs[1].plot(np.real(sig2[:128]), label="RX Frame") +            axs[1].set_ylabel("Real Part") +            axs[1].set_xlabel("Samples") +            axs[1].legend(loc=4) + +            fig.tight_layout() +            fig.savefig(fig_path) +            fig.clf() + +        sig2 = pa.phase_align(sig2, sig1) + +        if logging.getLogger().getEffectiveLevel() == logging.DEBUG: +            dt = datetime.datetime.now().isoformat() +            fig_path = logging_path + "/" + dt + "_sync_phase_aligned.pdf" + +            fig, axs = plt.subplots(2) +            axs[0].plot(np.abs(sig1[:128]), label="TX Frame") +            axs[0].plot(np.abs(sig2[:128]), label="RX Frame") +            axs[0].set_title("Phase Aligned") +            axs[0].set_ylabel("Amplitude") +            axs[0].set_xlabel("Samples") +            axs[0].legend(loc=4) + +            axs[1].plot(np.real(sig1[:128]), label="TX Frame") +            axs[1].plot(np.real(sig2[:128]), label="RX Frame") +            axs[1].set_ylabel("Real Part") +            axs[1].set_xlabel("Samples") +            axs[1].legend(loc=4) + +            fig.tight_layout() +            fig.savefig(fig_path) +            fig.clf() + +        logging.debug("Sig1_cut: %d %s, Sig2_cut: %d %s, off: %d" % (len(sig1), sig1.dtype, len(sig2), sig2.dtype, off)) +        return sig1, sig2 + +    def fromfile(self, filename, offset=0, length=None): +        if length is None: +            return np.memmap(filename, dtype=np.complex64, mode='r', offset=64/8*offset) +        else: +            return np.memmap(filename, dtype=np.complex64, mode='r', offset=64/8*offset, shape=length) + + +# The MIT License (MIT) +# +# Copyright (c) 2017 Andreas Steger +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/dpd/src/Measure.py b/dpd/src/Measure.py new file mode 100644 index 0000000..e4fa8a2 --- /dev/null +++ b/dpd/src/Measure.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- + +import sys +import socket +import struct +import numpy as np +import logging +import src.Dab_Util as DU + +class Measure: +    """Collect Measurement from DabMod""" +    def __init__(self, samplerate, port, num_samples_to_request): +        logging.info("Instantiate Measure object") +        self.samplerate = samplerate +        self.sizeof_sample = 8 # complex floats +        self.port = port +        self.num_samples_to_request = num_samples_to_request + +    def _recv_exact(self, sock, num_bytes): +        """Receive an exact number of bytes from a socket. This is +        a wrapper around sock.recv() that can return less than the number +        of requested bytes. + +        Args: +            sock (socket): Socket to receive data from. +            num_bytes (int): Number of bytes that will be returned. +        """ +        bufs = [] +        while num_bytes > 0: +            b = sock.recv(num_bytes) +            if len(b) == 0: +                break +            num_bytes -= len(b) +            bufs.append(b) +        return b''.join(bufs) + +    def get_samples(self): +        """Connect to ODR-DabMod, retrieve TX and RX samples, load +        into numpy arrays, and return a tuple +        (tx_timestamp, tx_samples, rx_timestamp, rx_samples) +        where the timestamps are doubles, and the samples are numpy +        arrays of complex floats, both having the same size +        """ +        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +        s.connect(('localhost', self.port)) + +        logging.debug("Send version") +        s.sendall(b"\x01") + +        logging.debug("Send request for {} samples".format(self.num_samples_to_request)) +        s.sendall(struct.pack("=I", self.num_samples_to_request)) + +        logging.debug("Wait for TX metadata") +        num_samps, tx_second, tx_pps = struct.unpack("=III", self._recv_exact(s, 12)) +        tx_ts = tx_second + tx_pps / 16384000.0 + +        if num_samps > 0: +            logging.debug("Receiving {} TX samples".format(num_samps)) +            txframe_bytes = self._recv_exact(s, num_samps * self.sizeof_sample) +            txframe = np.fromstring(txframe_bytes, dtype=np.complex64) +        else: +            txframe = np.array([], dtype=np.complex64) + +        logging.debug("Wait for RX metadata") +        rx_second, rx_pps = struct.unpack("=II", self._recv_exact(s, 8)) +        rx_ts = rx_second + rx_pps / 16384000.0 + +        if num_samps > 0: +            logging.debug("Receiving {} RX samples".format(num_samps)) +            rxframe_bytes = self._recv_exact(s, num_samps * self.sizeof_sample) +            rxframe = np.fromstring(rxframe_bytes, dtype=np.complex64) +        else: +            rxframe = np.array([], dtype=np.complex64) + +        # Normalize received signal with sent signal +        rx_median = np.median(np.abs(rxframe)) +        rxframe = rxframe / rx_median * np.median(np.abs(txframe)) + +        if logging.getLogger().getEffectiveLevel() == logging.DEBUG: +            logging.debug("txframe: min %f, max %f, median %f" % +                (np.min(np.abs(txframe)), +                 np.max(np.abs(txframe)), +                 np.median(np.abs(txframe)))) + +            logging.debug("rxframe: min %f, max %f, median %f" % +                (np.min(np.abs(rxframe)), +                 np.max(np.abs(rxframe)), +                 np.median(np.abs(rxframe)))) + +        logging.debug("Disconnecting") +        s.close() + +        du = DU.Dab_Util(self.samplerate) +        txframe_aligned, rxframe_aligned = du.subsample_align(txframe, rxframe) + +        logging.info( +            "Measurement done, tx %d %s, rx %d %s, tx aligned %d %s, rx aligned %d %s" +            % (len(txframe), txframe.dtype, len(rxframe), rxframe.dtype, +            len(txframe_aligned), txframe_aligned.dtype, len(rxframe_aligned), rxframe_aligned.dtype) ) + +        return txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median + +# The MIT License (MIT) +# +# Copyright (c) 2017 Andreas Steger +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/dpd/src/Model.py b/dpd/src/Model.py new file mode 100644 index 0000000..ae9f7b3 --- /dev/null +++ b/dpd/src/Model.py @@ -0,0 +1,336 @@ +# -*- coding: utf-8 -*- + +import datetime +import os +import logging +logging_path = os.path.dirname(logging.getLoggerClass().root.handlers[0].baseFilename) + +from pynverse import inversefunc +import numpy as np +import matplotlib +matplotlib.use('agg') +import matplotlib.pyplot as plt +from sklearn.linear_model import Ridge + +class Model: +    """Calculates new coefficients using the measurement and the old +    coefficients""" + +    def __init__(self, coefs_am, coefs_pm): +        self.coefs_am = coefs_am +        self.coefs_history = [coefs_am, ] +        self.mses = [0, ] +        self.errs = [0, ] + +        self.coefs_pm = coefs_pm +        self.coefs_pm_history = [coefs_pm, ] +        self.errs_phase = [0, ] + +    def sample_uniformly(self, txframe_aligned, rxframe_aligned, n_bins=4): +        """This function returns tx and rx samples in a way +        that the tx amplitudes have an approximate uniform  +        distribution with respect to the txframe_aligned amplitudes""" +        txframe_aligned_abs = np.abs(txframe_aligned) +        ccdf_min = 0 +        ccdf_max = np.max(txframe_aligned_abs) +        tx_hist, ccdf_edges = np.histogram(txframe_aligned_abs, +                                           bins=n_bins, +                                           range=(ccdf_min, ccdf_max)) +        n_choise  = np.min(tx_hist) +        tx_choice = np.zeros(n_choise * n_bins, dtype=np.complex64) +        rx_choice = np.zeros(n_choise * n_bins, dtype=np.complex64) + +        for idx, bin in enumerate(tx_hist): +            indices = np.where((txframe_aligned_abs >= ccdf_edges[idx]) & +                               (txframe_aligned_abs <= ccdf_edges[idx+1]))[0] +            indices_choise = np.random.choice(indices, n_choise, replace=False) +            rx_choice[idx*n_choise:(idx+1)*n_choise] = rxframe_aligned[indices_choise] +            tx_choice[idx*n_choise:(idx+1)*n_choise] = txframe_aligned[indices_choise] +        return tx_choice, rx_choice + +    def get_next_coefs(self, txframe_aligned, rxframe_aligned): +        tx_choice, rx_choice = self.sample_uniformly(txframe_aligned, rxframe_aligned) + +        # Calculate new coefficients for AM/AM correction +        rx_abs = np.abs(rx_choice) +        rx_A = np.vstack([rx_abs, +                          rx_abs ** 3, +                          rx_abs ** 5, +                          rx_abs ** 7, +                          rx_abs ** 9, +                          ]).T +        rx_dpd = np.sum(rx_A * self.coefs_am, axis=1) +        rx_dpd = rx_dpd * ( +            np.median(np.abs(tx_choice)) / np.median(np.abs(rx_dpd))) + +        err = rx_dpd - np.abs(tx_choice) +        self.errs.append(np.mean(np.abs(err ** 2))) + +        a_delta = np.linalg.lstsq(rx_A, err)[0] +        new_coefs = self.coefs_am - 0.1 * a_delta +        new_coefs = new_coefs * (self.coefs_am[0] / new_coefs[0]) +        logging.debug("a_delta {}".format(a_delta)) +        logging.debug("new coefs_am {}".format(new_coefs)) + +        # Calculate new coefficients for AM/PM correction +        phase_diff_rad = (( +                              (np.angle(tx_choice) - +                               np.angle(rx_choice) + +                               np.pi) % (2 * np.pi)) - +                          np.pi +                          ) + +        tx_abs = np.abs(tx_choice) +        tx_abs_A = np.vstack([tx_abs, +                             tx_abs ** 2, +                             tx_abs ** 3, +                             tx_abs ** 4, +                             tx_abs ** 5, +                             ]).T +        phase_dpd = np.sum(tx_abs_A * self.coefs_pm, axis=1) + +        err_phase = phase_dpd - phase_diff_rad +        self.errs_phase.append(np.mean(np.abs(err_phase ** 2))) +        a_delta = np.linalg.lstsq(tx_abs_A, err_phase)[0] +        new_coefs_pm = self.coefs_pm - 0.1 * a_delta +        logging.debug("a_delta {}".format(a_delta)) +        logging.debug("new new_coefs_pm {}".format(new_coefs_pm)) + +        def dpd_phase(tx): +            tx_abs = np.abs(tx) +            tx_A_complex = np.vstack([tx, +                                      tx * tx_abs ** 1, +                                      tx * tx_abs ** 2, +                                      tx * tx_abs ** 3, +                                      tx * tx_abs ** 4, +                                      ]).T +            tx_dpd = np.sum(tx_A_complex * self.coefs_pm, axis=1) +            return tx_dpd + +        tx_range = np.linspace(0, 2) +        phase_range_dpd = dpd_phase(tx_range) + +        rx_A_complex = np.vstack([rx_choice, +                                  rx_choice * rx_abs ** 2, +                                  rx_choice * rx_abs ** 4, +                                  rx_choice * rx_abs ** 6, +                                  rx_choice * rx_abs ** 8, +                                  ]).T +        rx_post_distored = np.sum(rx_A_complex * self.coefs_am, axis=1) +        rx_post_distored = rx_post_distored * ( +            np.median(np.abs(tx_choice)) / +            np.median(np.abs(rx_post_distored))) +        mse = np.mean(np.abs((tx_choice - rx_post_distored) ** 2)) +        logging.debug("MSE: {}".format(mse)) +        self.mses.append(mse) + +        def dpd(tx): +            tx_abs = np.abs(tx) +            tx_A_complex = np.vstack([tx, +                                      tx * tx_abs ** 2, +                                      tx * tx_abs ** 4, +                                      tx * tx_abs ** 6, +                                      tx * tx_abs ** 8, +                                      ]).T +            tx_dpd = np.sum(tx_A_complex * self.coefs_am, axis=1) +            return tx_dpd + +        rx_range = np.linspace(0, 1, num=100) +        rx_range_dpd = dpd(rx_range) +        rx_range = rx_range[(rx_range_dpd > 0) & (rx_range_dpd < 2)] +        rx_range_dpd = rx_range_dpd[(rx_range_dpd > 0) & (rx_range_dpd < 2)] + +        if logging.getLogger().getEffectiveLevel() == logging.DEBUG: +            logging.debug("txframe: min %f, max %f, median %f" % +                          (np.min(np.abs(txframe_aligned)), +                           np.max(np.abs(txframe_aligned)), +                           np.median(np.abs(txframe_aligned)) +                           )) + +            logging.debug("rxframe: min %f, max %f, median %f" % +                          (np.min(np.abs(rx_choice)), +                           np.max(np.abs(rx_choice)), +                           np.median(np.abs(rx_choice)) +                           )) + +            dt = datetime.datetime.now().isoformat() +            fig_path = logging_path + "/" + dt + "_Model.pdf" + +            fig = plt.figure(figsize=(3*6, 1.5 * 6)) + +            ax = plt.subplot(3,3,1) +            ax.plot(np.abs(txframe_aligned[:128]), +                    label="TX sent", +                    linestyle=":") +            ax.plot(np.abs(rxframe_aligned[:128]), +                    label="RX received", +                    color="red") +            ax.set_title("Synchronized Signals of Iteration {}".format(len(self.coefs_history))) +            ax.set_xlabel("Samples") +            ax.set_ylabel("Amplitude") +            ax.text(0, 0, "TX (max {:01.3f}, mean {:01.3f}, median {:01.3f})".format( +                np.max(np.abs(txframe_aligned)), +                np.mean(np.abs(txframe_aligned)), +                np.median(np.abs(txframe_aligned)) +            ), size = 8) +            ax.legend(loc=4) + +            ax = plt.subplot(3,3,2) +            ax.plot(np.real(txframe_aligned[:128]), +                    label="TX sent", +                    linestyle=":") +            ax.plot(np.real(rxframe_aligned[:128]), +                    label="RX received", +                    color="red") +            ax.set_title("Synchronized Signals") +            ax.set_xlabel("Samples") +            ax.set_ylabel("Real Part") +            ax.legend(loc=4) + +            ax = plt.subplot(3,3,3) +            ax.plot(np.abs(txframe_aligned[:128]), +                    label="TX Frame", +                    linestyle=":", +                    linewidth=0.5) +            ax.plot(np.abs(rxframe_aligned[:128]), +                    label="RX Frame", +                    linestyle="--", +                    linewidth=0.5) + +            rx_abs = np.abs(rxframe_aligned) +            rx_A = np.vstack([rx_abs, +                              rx_abs ** 3, +                              rx_abs ** 5, +                              rx_abs ** 7, +                              rx_abs ** 9, +                              ]).T +            rx_dpd = np.sum(rx_A * self.coefs_am, axis=1) +            rx_dpd = rx_dpd * ( +                np.median(np.abs(tx_choice)) / np.median(np.abs(rx_dpd))) + +            ax.plot(np.abs(rx_dpd[:128]), +                    label="RX DPD Frame", +                    linestyle="-.", +                    linewidth=0.5) + +            tx_abs = np.abs(np.abs(txframe_aligned[:128])) +            tx_A = np.vstack([tx_abs, +                              tx_abs ** 3, +                              tx_abs ** 5, +                              tx_abs ** 7, +                              tx_abs ** 9, +                              ]).T +            tx_dpd = np.sum(tx_A * new_coefs, axis=1) +            tx_dpd_norm = tx_dpd * ( +                np.median(np.abs(tx_choice)) / np.median(np.abs(tx_dpd))) + +            ax.plot(np.abs(tx_dpd_norm[:128]), +                    label="TX DPD Frame Norm", +                    linestyle="-.", +                    linewidth=0.5) +            ax.legend(loc=4) +            ax.set_title("RX DPD") +            ax.set_xlabel("Samples") +            ax.set_ylabel("Amplitude") + +            ax = plt.subplot(3,3,4) +            ax.scatter( +                np.abs(tx_choice[:1024]), +                np.abs(rx_choice[:1024]), +                s=0.1) +            ax.plot(rx_range_dpd / self.coefs_am[0], rx_range, linewidth=0.25) +            ax.set_title("Amplifier Characteristic") +            ax.set_xlabel("TX Amplitude") +            ax.set_ylabel("RX Amplitude") + +            ax = plt.subplot(3,3,5) +            ax.scatter( +                np.abs(tx_choice[:1024]), +                phase_diff_rad[:1024] * 180 / np.pi, +                s=0.1 +            ) +            ax.plot(tx_range, phase_range_dpd * 180 / np.pi, linewidth=0.25) +            ax.set_title("Amplifier Characteristic") +            ax.set_xlabel("TX Amplitude") +            ax.set_ylabel("Phase Difference [deg]") + +            ax = plt.subplot(3,3,6) +            ccdf_min, ccdf_max = 0, 1 +            tx_hist, ccdf_edges = np.histogram(np.abs(txframe_aligned), +                                      bins=60, +                                      range=(ccdf_min, ccdf_max)) +            tx_hist_normalized = tx_hist.astype(float)/np.sum(tx_hist) +            ccdf = 1.0 - np.cumsum(tx_hist_normalized) +            ax.semilogy(ccdf_edges[:-1], ccdf, label="CCDF") +            ax.semilogy(ccdf_edges[:-1], +                        tx_hist_normalized, +                        label="Histogram", +                        drawstyle='steps') +            ax.legend(loc=4) +            ax.set_ylim(1e-5,2) +            ax.set_title("Complementary Cumulative Distribution Function") +            ax.set_xlabel("TX Amplitude") +            ax.set_ylabel("Ratio of Samples larger than x") + +            ax = plt.subplot(3,3,7) +            coefs_history = np.array(self.coefs_history) +            for idx, coef_hist in enumerate(coefs_history.T): +                ax.plot(coef_hist, +                        label="Coef {}".format(idx), +                        linewidth=0.5) +            ax.legend(loc=4) +            ax.set_title("AM/AM Coefficient History") +            ax.set_xlabel("Iterations") +            ax.set_ylabel("Coefficient Value") + +            ax = plt.subplot(3,3,8) +            coefs_history = np.array(self.coefs_pm_history) +            for idx, coef_hist in enumerate(coefs_history.T): +                ax.plot(coef_hist, +                        label="Coef {}".format(idx), +                        linewidth=0.5) +            ax.legend(loc=4) +            ax.set_title("AM/PM Coefficient History") +            ax.set_xlabel("Iterations") +            ax.set_ylabel("Coefficient Value") + +            ax = plt.subplot(3,3,9) +            coefs_history = np.array(self.coefs_history) +            ax.plot(self.mses, label="MSE") +            ax.plot(self.errs, label="ERR") +            ax.legend(loc=4) +            ax.set_title("MSE History") +            ax.set_xlabel("Iterations") +            ax.set_ylabel("MSE") + +            fig.tight_layout() +            fig.savefig(fig_path) +            fig.clf() + +        self.coefs_am = new_coefs +        self.coefs_history.append(self.coefs_am) +        self.coefs_pm = new_coefs_pm +        self.coefs_pm_history.append(self.coefs_pm) +        return self.coefs_am, self.coefs_pm + +# The MIT License (MIT) +# +# Copyright (c) 2017 Andreas Steger +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/dpd/src/__init__.py b/dpd/src/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/dpd/src/__init__.py diff --git a/dpd/src/phase_align.py b/dpd/src/phase_align.py new file mode 100644 index 0000000..f03184b --- /dev/null +++ b/dpd/src/phase_align.py @@ -0,0 +1,74 @@ +import datetime +import os +import logging +logging_path = os.path.dirname(logging.getLoggerClass().root.handlers[0].baseFilename) + +import numpy as np +from scipy import signal, optimize +import sys +import matplotlib.pyplot as plt + + +def phase_align(sig, ref_sig): +    """Do phase alignment for sig relative to the reference signal +    ref_sig. + +    Returns the aligned signal""" + +    angle_diff = (np.angle(sig) - np.angle(ref_sig)) % (2. * np.pi) + +    real_diffs = np.cos(angle_diff) +    imag_diffs = np.sin(angle_diff) + +    if logging.getLogger().getEffectiveLevel() == logging.DEBUG: +        dt = datetime.datetime.now().isoformat() +        fig_path = logging_path + "/" + dt + "_phase_align.pdf" + +        plt.subplot(511) +        plt.hist(angle_diff, bins=60, label="Angle Diff") +        plt.xlabel("Angle") +        plt.ylabel("Count") +        plt.legend(loc=4) + +        plt.subplot(512) +        plt.hist(real_diffs, bins=60, label="Real Diff") +        plt.xlabel("Real Part") +        plt.ylabel("Count") +        plt.legend(loc=4) + +        plt.subplot(513) +        plt.hist(imag_diffs, bins=60, label="Imaginary Diff") +        plt.xlabel("Imaginary Part") +        plt.ylabel("Count") +        plt.legend(loc=4) + +        plt.subplot(514) +        plt.plot(np.angle(ref_sig[:128]), label="ref_sig") +        plt.plot(np.angle(sig[:128]), label="sig") +        plt.xlabel("Angle") +        plt.ylabel("Sample") +        plt.legend(loc=4) + +    real_diff = np.median(real_diffs) +    imag_diff = np.median(imag_diffs) + +    angle = np.angle(real_diff + 1j * imag_diff) + +    logging.debug( +        "Compensating phase by {} rad, {} degree. real median {}, imag median {}".format( +        angle, angle*180./np.pi, real_diff, imag_diff +    )) +    sig = sig * np.exp(1j * -angle) + +    if logging.getLogger().getEffectiveLevel() == logging.DEBUG: +        plt.subplot(515) +        plt.plot(np.angle(ref_sig[:128]), label="ref_sig") +        plt.plot(np.angle(sig[:128]), label="sig") +        plt.xlabel("Angle") +        plt.ylabel("Sample") +        plt.legend(loc=4) +        plt.tight_layout() +        plt.savefig(fig_path) +        plt.clf() + +    return sig diff --git a/dpd/src/subsample_align.py b/dpd/src/subsample_align.py new file mode 100755 index 0000000..0a51593 --- /dev/null +++ b/dpd/src/subsample_align.py @@ -0,0 +1,83 @@ +import datetime +import os +import logging +logging_path = os.path.dirname(logging.getLoggerClass().root.handlers[0].baseFilename) + +import numpy as np +from scipy import signal, optimize +import matplotlib.pyplot as plt + +def gen_omega(length): +    if (length % 2) == 1: +        raise ValueError("Needs an even length array.") + +    halflength = int(length/2) +    factor = 2.0 * np.pi / length + +    omega = np.zeros(length, dtype=np.float) +    for i in range(halflength): +        omega[i] = factor * i + +    for i in range(halflength, length): +        omega[i] = factor * (i - length) + +    return omega + +def subsample_align(sig, ref_sig): +    """Do subsample alignment for sig relative to the reference signal +    ref_sig. The delay between the two must be less than sample + +    Returns the aligned signal""" + +    n = len(sig) +    if (n % 2) == 1: +        raise ValueError("Needs an even length signal.") +    halflen = int(n/2) + +    fft_sig = np.fft.fft(sig) + +    omega = gen_omega(n) + +    def correlate_for_delay(tau): +        # A subsample offset between two signals corresponds, in the frequency +        # domain, to a linearly increasing phase shift, whose slope +        # corresponds to the delay. +        # +        # Here, we build this phase shift in rotate_vec, and multiply it with +        # our signal. + +        rotate_vec = np.exp(1j * tau * omega) +        # zero-frequency is rotate_vec[0], so rotate_vec[N/2] is the +        # bin corresponding to the [-1, 1, -1, 1, ...] time signal, which +        # is both the maximum positive and negative frequency. +        # I don't remember why we handle it differently. +        rotate_vec[halflen] = np.cos(np.pi * tau) + +        corr_sig = np.fft.ifft(rotate_vec * fft_sig) + +        return -np.abs(np.sum(np.conj(corr_sig) * ref_sig)) + +    optim_result = optimize.minimize_scalar(correlate_for_delay, bounds=(-1,1), method='bounded', options={'disp': True}) + +    if optim_result.success: +        best_tau = optim_result.x + +        if 1: +            corr = np.vectorize(correlate_for_delay) +            ixs = np.linspace(-1, 1, 100) +            taus = corr(ixs) + +            dt = datetime.datetime.now().isoformat() +            tau_path = (logging_path + "/" + dt + "_tau.pdf") +            plt.plot(ixs, taus) +            plt.title("Subsample correlation, minimum is best: {}".format(best_tau)) +            plt.savefig(tau_path) +            plt.clf() + +        # Prepare rotate_vec = fft_sig with rotated phase +        rotate_vec = np.exp(1j * best_tau * omega) +        rotate_vec[halflen] = np.cos(np.pi * best_tau) +        return np.fft.ifft(rotate_vec * fft_sig).astype(np.complex64) +    else: +        #print("Could not optimize: " + optim_result.message) +        return np.zeros(0, dtype=np.complex64) diff --git a/dpd/src/test_dab_Util.py b/dpd/src/test_dab_Util.py new file mode 100644 index 0000000..0b2fa4f --- /dev/null +++ b/dpd/src/test_dab_Util.py @@ -0,0 +1,62 @@ +from unittest import TestCase + +import numpy as np +import pandas as pd +import src.Dab_Util as DU + +class TestDab_Util(TestCase): + +    def test_subsample_align(self, sample_orig=r'../test_data/orig_rough_aligned.dat', +                             sample_rec =r'../test_data/recored_rough_aligned.dat', +                             length = 10240, max_size = 1000000): +        du = DU.Dab_Util(8196000) +        res1 = [] +        res2 = [] +        for i in range(10): +            start = np.random.randint(50, max_size) +            r = np.random.randint(-50, 50) + +            s1 = du.fromfile(sample_orig, offset=start+r, length=length) +            s2 = du.fromfile(sample_rec, offset=start, length=length) + +            res1.append(du.lag_upsampling(s2, s1, 32)) + +            s1_aligned, s2_aligned = du.subsample_align(s1, s2) + +            res2.append(du.lag_upsampling(s2_aligned, s1_aligned, 32)) + +        error_rate = np.mean(np.array(res2) != 0) +        self.assertEqual(error_rate, 0.0, "The error rate for aligning was %.2f%%" +                         % error_rate * 100) + +#def test_using_aligned_pair(sample_orig=r'../data/orig_rough_aligned.dat', sample_rec =r'../data/recored_rough_aligned.dat', length = 10240, max_size = 1000000): +#    res = [] +#    for i in tqdm(range(100)): +#        start = np.random.randint(50, max_size) +#        r = np.random.randint(-50, 50) +# +#        s1 = du.fromfile(sample_orig, offset=start+r, length=length) +#        s2 = du.fromfile(sample_rec, offset=start, length=length) +# +#        res.append({'offset':r, +#                    '1':r - du.lag_upsampling(s2, s1, n_up=1), +#                    '2':r - du.lag_upsampling(s2, s1, n_up=2), +#                    '3':r - du.lag_upsampling(s2, s1, n_up=3), +#                    '4':r - du.lag_upsampling(s2, s1, n_up=4), +#                    '8':r - du.lag_upsampling(s2, s1, n_up=8), +#                    '16':r - du.lag_upsampling(s2, s1, n_up=16), +#                    '32':r - du.lag_upsampling(s2, s1, n_up=32), +#                    }) +#    df = pd.DataFrame(res) +#    df = df.reindex_axis(sorted(df.columns), axis=1) +#    print(df.describe()) +# +# +#print("Align using upsampling") +#for n_up in [1, 2, 3, 4, 7, 8, 16]: +#   correct_ratio = test_phase_offset(lambda x,y: du.lag_upsampling(x,y,n_up), tol=1./n_up) +#   print("%.1f%% of the tested offsets were measured within tolerance %.4f for n_up = %d" % (correct_ratio * 100, 1./n_up, n_up)) +#test_using_aligned_pair() +# +#print("Phase alignment") +#test_subsample_alignment() diff --git a/dpd/src/test_measure.py b/dpd/src/test_measure.py new file mode 100644 index 0000000..b695721 --- /dev/null +++ b/dpd/src/test_measure.py @@ -0,0 +1,33 @@ +from unittest import TestCase +from Measure import Measure +import socket + + +class TestMeasure(TestCase): + +    def _open_socks(self): +        sock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +        sock_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +        sock_server.bind(('localhost', 1234)) +        sock_server.listen(1) + +        sock_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +        sock_client.connect(('localhost', 1234)) + +        conn_server, addr_server = sock_server.accept() +        return conn_server, sock_client + +    def test__recv_exact(self): +        m = Measure(1234, 1) +        payload = b"test payload" + +        conn_server, sock_client = self._open_socks() +        conn_server.send(payload) +        rec = m._recv_exact(sock_client, len(payload)) + +        self.assertEqual(rec, payload, +                "Did not receive the same message as sended. (%s, %s)" % +                (rec, payload)) + +    def test_get_samples(self): +        self.fail() diff --git a/src/ConfigParser.cpp b/src/ConfigParser.cpp index 459811f..9ac1280 100644 --- a/src/ConfigParser.cpp +++ b/src/ConfigParser.cpp @@ -171,7 +171,10 @@ static void parse_configfile(      // Poly coefficients:      if (pt.get("poly.enabled", 0) == 1) {          mod_settings.polyCoefFilename = -            pt.get<std::string>("poly.polycoeffile", "default"); +            pt.get<std::string>("poly.polycoeffile", "dpd/poly.coef"); + +        mod_settings.polyNumThreads = +            pt.get<int>("poly.num_threads", 0);      }      // Output options diff --git a/src/ConfigParser.h b/src/ConfigParser.h index 22a4fc5..89f0fb7 100644 --- a/src/ConfigParser.h +++ b/src/ConfigParser.h @@ -75,7 +75,7 @@ struct mod_settings_t {      std::string filterTapsFilename = "";      std::string polyCoefFilename = ""; - +    unsigned polyNumThreads = 0;  #if defined(HAVE_OUTPUT_UHD)      OutputUHDConfig outputuhd_conf; diff --git a/src/DabModulator.cpp b/src/DabModulator.cpp index 34d8e66..cc2642a 100644 --- a/src/DabModulator.cpp +++ b/src/DabModulator.cpp @@ -205,13 +205,8 @@ int DabModulator::process(Buffer* dataOut)          shared_ptr<MemlessPoly> cifPoly;          if (not m_settings.polyCoefFilename.empty()) { -            cifPoly = make_shared<MemlessPoly>(m_settings.polyCoefFilename); -            etiLog.level(debug) << m_settings.polyCoefFilename << "\n"; -            etiLog.level(debug) << cifPoly->m_coefs[0] << " " << -                cifPoly->m_coefs[1] << " "<< cifPoly->m_coefs[2] << " "<< -                cifPoly->m_coefs[3] << " "<< cifPoly->m_coefs[4] << " "<< -                cifPoly->m_coefs[5] << " "<< cifPoly->m_coefs[6] << " "<< -                cifPoly->m_coefs[7] << "\n"; +            cifPoly = make_shared<MemlessPoly>(m_settings.polyCoefFilename, +                                               m_settings.polyNumThreads);              rcs.enrol(cifPoly.get());          } diff --git a/src/MemlessPoly.cpp b/src/MemlessPoly.cpp index 7e074eb..d5188f2 100644 --- a/src/MemlessPoly.cpp +++ b/src/MemlessPoly.cpp @@ -29,6 +29,8 @@     along with ODR-DabMod.  If not, see <http://www.gnu.org/licenses/>.   */ +#pragma GCC optimize ("O3") +  #include "MemlessPoly.h"  #include "PcDebug.h"  #include "Utils.h" @@ -36,31 +38,43 @@  #include <stdio.h>  #include <stdexcept> +#include <future>  #include <array>  #include <iostream>  #include <fstream>  #include <memory> +#include <complex>  using namespace std; +// Number of AM/AM coefs, identical to number of AM/PM coefs +#define NUM_COEFS 5 -// By default the signal is unchanged -static const std::array<float, 8> default_coefficients({ -        1, 0.0, 0.0, 0.0, -        0.0, 0.0, 0.0, 0.0 -        }); - - -MemlessPoly::MemlessPoly(const std::string& coefs_file) : +MemlessPoly::MemlessPoly(const std::string& coefs_file, unsigned int num_threads) :      PipelinedModCodec(),      RemoteControllable("memlesspoly"), -    m_coefs_file(coefs_file) +    m_num_threads(num_threads), +    m_coefs_am(), +    m_coefs_pm(), +    m_coefs_file(coefs_file), +    m_coefs_mutex()  {      PDEBUG("MemlessPoly::MemlessPoly(%s) @ %p\n",              coefs_file.c_str(), this); +    if (m_num_threads == 0) { +        const unsigned int hw_concurrency = std::thread::hardware_concurrency(); +        etiLog.level(info) << "Polynomial Predistorter will use " << +            hw_concurrency << " threads (auto detected)"; +    } +    else { +        etiLog.level(info) << "Polynomial Predistorter will use " << +            m_num_threads << " threads (set in config file)"; +    } +      RC_ADD_PARAMETER(ncoefs, "(Read-only) number of coefficients."); -    RC_ADD_PARAMETER(coeffile, "Filename containing coefficients. When written to, the new file gets automatically loaded."); +    RC_ADD_PARAMETER(coeffile, "Filename containing coefficients. " +            "When set, the file gets loaded.");      load_coefficients(m_coefs_file); @@ -69,76 +83,141 @@ MemlessPoly::MemlessPoly(const std::string& coefs_file) :  void MemlessPoly::load_coefficients(const std::string &coefFile)  { -    std::vector<float> coefs; -    if (coefFile == "default") { -        std::copy(default_coefficients.begin(), default_coefficients.end(), -                std::back_inserter(coefs)); +    std::vector<float> coefs_am; +    std::vector<float> coefs_pm; +    std::ifstream coef_fstream(coefFile.c_str()); +    if (!coef_fstream) { +        throw std::runtime_error("MemlessPoly: Could not open file with coefs!");      } -    else { -        std::ifstream coef_fstream(coefFile.c_str()); -        if(!coef_fstream) { -            fprintf(stderr, "MemlessPoly: file %s could not be opened !\n", coefFile.c_str()); -            throw std::runtime_error("MemlessPoly: Could not open file with coefs! "); -        } -        int n_coefs; -        coef_fstream >> n_coefs; +    int n_coefs; +    coef_fstream >> n_coefs; -        if (n_coefs <= 0) { -            fprintf(stderr, "MemlessPoly: warning: coefs file has invalid format\n"); -            throw std::runtime_error("MemlessPoly: coefs file has invalid format."); -        } +    if (n_coefs <= 0) { +        throw std::runtime_error("MemlessPoly: coefs file has invalid format."); +    } +    else if (n_coefs != NUM_COEFS) { +        throw std::runtime_error("MemlessPoly: invalid number of coefs: " + +                std::to_string(n_coefs) + " expected " + std::to_string(NUM_COEFS)); +    } -        if (n_coefs != 8) { -            throw std::runtime_error( "MemlessPoly: error: coefs file does not have 8 coefs\n"); -        } +    const size_t n_entries = 2 * n_coefs; -        fprintf(stderr, "MemlessPoly: Reading %d coefs...\n", n_coefs); +    etiLog.log(debug, "MemlessPoly: Reading %d coefs...", n_entries); -        coefs.resize(n_coefs); +    coefs_am.resize(n_coefs); +    coefs_pm.resize(n_coefs); -        int n; -        for (n = 0; n < n_coefs; n++) { -            coef_fstream >> coefs[n]; -            PDEBUG("MemlessPoly: coef: %f\n",  coefs[n] ); -            if (coef_fstream.eof()) { -                fprintf(stderr, "MemlessPoly: file %s should contains %d coefs, but EOF reached "\ -                        "after %d coefs !\n", coefFile.c_str(), n_coefs, n); -                throw std::runtime_error("MemlessPoly: coefs file invalid ! "); -            } +    for (int n = 0; n < n_entries; n++) { +        float a; +        coef_fstream >> a; + +        if (n < n_coefs) { +            coefs_am[n] = a; +        } +        else { +            coefs_pm[n - n_coefs] = a; +        } + +        if (coef_fstream.eof()) { +            etiLog.log(error, "MemlessPoly: file %s should contains %d coefs, " +                    "but EOF reached after %d coefs !", +                    coefFile.c_str(), n_entries, n); +            throw std::runtime_error("MemlessPoly: coefs file invalid !");          }      }      {          std::lock_guard<std::mutex> lock(m_coefs_mutex); -        m_coefs = coefs; +        m_coefs_am = coefs_am; +        m_coefs_pm = coefs_pm;      }  } +/* The restrict keyword is C99, g++ and clang++ however support __restrict + * instead, and this allows the compiler to auto-vectorize the loop. + */ +static void apply_coeff( +        const vector<float> &coefs_am, const vector<float> &coefs_pm, +        const complexf *__restrict in, size_t start, size_t stop, +        complexf *__restrict out) +{ +    for (size_t i = start; i < stop; i+=1) { + +        float in_mag_sq = in[i].real() * in[i].real() + in[i].imag() * in[i].imag(); + +        float amplitude_correction = +            ( coefs_am[0] + in_mag_sq * +              ( coefs_am[1] + in_mag_sq * +                ( coefs_am[2] + in_mag_sq * +                  ( coefs_am[3] + in_mag_sq * +                    coefs_am[4])))); + +        float phase_correction = -1 * +            ( coefs_pm[0] + in_mag_sq * +              ( coefs_pm[1] + in_mag_sq * +                ( coefs_pm[2] + in_mag_sq * +                  ( coefs_pm[3] + in_mag_sq * +                    coefs_pm[4])))); + +        float phase_correction_sq = phase_correction * phase_correction; + +        // Approximation for Cosinus 1 - 1/2 x^2 + 1/24 x^4 - 1/720 x^6 +        float re = (1.0f - phase_correction_sq * +                ( -0.5f + phase_correction_sq * +                    ( 0.486666f  + phase_correction_sq * +                        ( -0.00138888f)))); + +        // Approximation for Sinus x + 1/6 x^3 + 1/120 x^5 +        float im = phase_correction * +                (1.0f + phase_correction_sq * +                    (0.166666f + phase_correction_sq * +                        (0.00833333f))); + +        out[i] = in[i] * amplitude_correction * complex<float>(re, im); +    } +}  int MemlessPoly::internal_process(Buffer* const dataIn, Buffer* dataOut)  { -        const float* in = reinterpret_cast<const float*>(dataIn->getData()); -        float* out      = reinterpret_cast<float*>(dataOut->getData()); -        size_t sizeIn   = dataIn->getLength() / sizeof(float); - -        { -             std::lock_guard<std::mutex> lock(m_coefs_mutex); -             for (size_t i = 0; i < sizeIn; i += 1) { -                 float mag = std::abs(in[i]); -                 //out[i] = in[i]; -                 out[i] = in[i] * ( -                     m_coefs[0] + -                     m_coefs[1] * mag + -                     m_coefs[2] * mag*mag + -                     m_coefs[3] * mag*mag*mag + -                     m_coefs[4] * mag*mag*mag*mag + -                     m_coefs[5] * mag*mag*mag*mag*mag + -                     m_coefs[6] * mag*mag*mag*mag*mag*mag + -                     m_coefs[7] * mag*mag*mag*mag*mag*mag*mag -                     ); +    dataOut->setLength(dataIn->getLength()); + +    const complexf* in = reinterpret_cast<const complexf*>(dataIn->getData()); +    complexf* out = reinterpret_cast<complexf*>(dataOut->getData()); +    size_t sizeOut = dataOut->getLength() / sizeof(complexf); + +    { +        std::lock_guard<std::mutex> lock(m_coefs_mutex); +        const unsigned int hw_concurrency = std::thread::hardware_concurrency(); + +        const unsigned int num_threads = +            (m_num_threads > 0) ? m_num_threads : hw_concurrency; + +        if (num_threads) { +            const size_t step = sizeOut / num_threads; +            vector<future<void> > flags; + +            size_t start = 0; +            for (size_t i = 0; i < num_threads - 1; i++) { +                flags.push_back(async(launch::async, apply_coeff, +                            m_coefs_am, m_coefs_pm, +                            in, start, start + step, out)); + +                start += step;              } + +            // Do the last in this thread +            apply_coeff(m_coefs_am, m_coefs_pm, in, start, sizeOut, out); + +            // Wait for completion of the tasks +            for (auto& f : flags) { +                f.get(); +            } +        } +        else { +            apply_coeff(m_coefs_am, m_coefs_pm, in, 0, sizeOut, out);          } +    }      return dataOut->getLength();  } @@ -172,9 +251,9 @@ const string MemlessPoly::get_parameter(const string& parameter) const  {      stringstream ss;      if (parameter == "ncoefs") { -        ss << m_coefs.size(); +        ss << m_coefs_am.size();      } -    else if (parameter == "coefFile") { +    else if (parameter == "coeffile") {          ss << m_coefs_file;      }      else { diff --git a/src/MemlessPoly.h b/src/MemlessPoly.h index 210b4b4..4dcd44a 100644 --- a/src/MemlessPoly.h +++ b/src/MemlessPoly.h @@ -52,7 +52,7 @@ typedef std::complex<float> complexf;  class MemlessPoly : public PipelinedModCodec, public RemoteControllable  {  public: -    MemlessPoly(const std::string& coefs_file); +    MemlessPoly(const std::string& coefs_file, unsigned int num_threads);      virtual const char* name() { return "MemlessPoly"; } @@ -63,16 +63,14 @@ public:      virtual const std::string get_parameter(              const std::string& parameter) const; -//TODO to protected -    std::vector<float> m_coefs; - - -protected: +private:      int internal_process(Buffer* const dataIn, Buffer* dataOut);      void load_coefficients(const std::string &coefFile); +    unsigned int m_num_threads; +    std::vector<float> m_coefs_am; // AM/AM coefficients +    std::vector<float> m_coefs_pm; // AM/PM coefficients      std::string m_coefs_file; -      mutable std::mutex m_coefs_mutex;  }; diff --git a/src/OutputUHDFeedback.cpp b/src/OutputUHDFeedback.cpp index 2a99e6b..a8f2c2e 100644 --- a/src/OutputUHDFeedback.cpp +++ b/src/OutputUHDFeedback.cpp @@ -43,6 +43,7 @@ DESCRIPTION:  #include <sys/socket.h>  #include <errno.h>  #include <poll.h> +#include <boost/date_time/posix_time/posix_time.hpp>  #include "OutputUHDFeedback.h"  #include "Utils.h" @@ -218,152 +219,164 @@ static ssize_t sendall(int socket, const void *buffer, size_t buflen)      return buflen;  } -void OutputUHDFeedback::ServeFeedbackThread() +void OutputUHDFeedback::ServeFeedback()  { -    set_thread_name("uhdservefeedback"); +    if ((m_server_sock = socket(PF_INET, SOCK_STREAM, 0)) < 0) { +        throw std::runtime_error("Can't create TCP socket"); +    } + +    struct sockaddr_in addr; +    addr.sin_family = AF_INET; +    addr.sin_port = htons(m_port); +    addr.sin_addr.s_addr = htonl(INADDR_ANY); + +    if (bind(m_server_sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { +        close(m_server_sock); +        throw std::runtime_error("Can't bind TCP socket"); +    } + +    if (listen(m_server_sock, 1) < 0) { +        close(m_server_sock); +        throw std::runtime_error("Can't listen TCP socket"); +    } + +    etiLog.level(info) << "DPD Feedback server listening on port " << m_port; + +    while (m_running) { +        struct sockaddr_in client; +        int client_sock = accept_with_timeout(m_server_sock, 1000, &client); -    try { -        if ((m_server_sock = socket(PF_INET, SOCK_STREAM, 0)) < 0) { -            throw std::runtime_error("Can't create TCP socket"); +        if (client_sock == -1) { +            close(m_server_sock); +            throw runtime_error("Could not establish new connection"); +        } +        else if (client_sock == -2) { +            continue;          } -        struct sockaddr_in addr; -        addr.sin_family = AF_INET; -        addr.sin_port = htons(m_port); -        addr.sin_addr.s_addr = htonl(INADDR_ANY); +        uint8_t request_version = 0; +        ssize_t read = recv(client_sock, &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 (bind(m_server_sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { -            throw std::runtime_error("Can't bind TCP socket"); +        if (request_version != 1) { +            etiLog.level(info) << "DPD Feedback Server wrong request version"; +            break;          } -        if (listen(m_server_sock, 1) < 0) { -            throw std::runtime_error("Can't listen TCP socket"); +        uint32_t num_samples = 0; +        read = recv(client_sock, &num_samples, 4, 0); +        if (!read) break; // done reading +        if (read < 0) { +            etiLog.level(info) << +                "DPD Feedback Server Client read num samples failed"; +            break;          } -        etiLog.level(info) << "DPD Feedback server listening on port " << m_port; - -        while (m_running) { -            struct sockaddr_in client; -            int client_sock = accept_with_timeout(m_server_sock, 1000, &client); - -            if (client_sock == -1) { -                throw runtime_error("Could not establish new connection"); -            } -            else if (client_sock == -2) { -                continue; -            } - -            uint8_t request_version = 0; -            ssize_t read = recv(client_sock, &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 = recv(client_sock, &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 +        // We are ready to issue the request now +        {              boost::mutex::scoped_lock lock(burstRequest.mutex); -            while (burstRequest.state != BurstRequestState::Acquired) { -                if (not m_running) break; -                burstRequest.mutex_notification.wait(lock); -            } +            burstRequest.num_samples = num_samples; +            burstRequest.state = BurstRequestState::SaveTransmitFrame; -            burstRequest.state = BurstRequestState::None;              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))); -            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 (sendall(client_sock, &num_samples_32, sizeof(num_samples_32)) < 0) { -                etiLog.level(info) << -                    "DPD Feedback Server Client send num_samples failed"; -                break; -            } - -            if (sendall(client_sock, -                        &burstRequest.tx_second, -                        sizeof(burstRequest.tx_second)) < 0) { -                etiLog.level(info) << -                    "DPD Feedback Server Client send tx_second failed"; -                break; -            } - -            if (sendall(client_sock, -                        &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); - -            assert(burstRequest.tx_samples.size() >= frame_bytes); -            if (sendall(client_sock, -                        &burstRequest.tx_samples[0], -                        frame_bytes) < 0) { -                etiLog.level(info) << -                    "DPD Feedback Server Client send tx_frame failed"; -                break; -            } - -            if (sendall(client_sock, -                        &burstRequest.rx_second, -                        sizeof(burstRequest.rx_second)) < 0) { -                etiLog.level(info) << -                    "DPD Feedback Server Client send rx_second failed"; -                break; -            } - -            if (sendall(client_sock, -                        &burstRequest.rx_pps, -                        sizeof(burstRequest.rx_pps)) < 0) { -                etiLog.level(info) << -                    "DPD Feedback Server Client send rx_pps failed"; -                break; -            } - -            assert(burstRequest.rx_samples.size() >= frame_bytes); -            if (sendall(client_sock, -                        &burstRequest.rx_samples[0], -                        frame_bytes) < 0) { -                etiLog.level(info) << -                    "DPD Feedback Server Client send rx_frame failed"; -                break; -            } - -            close(client_sock); +        uint32_t num_samples_32 = burstRequest.num_samples; +        if (sendall(client_sock, &num_samples_32, sizeof(num_samples_32)) < 0) { +            etiLog.level(info) << +                "DPD Feedback Server Client send num_samples failed"; +            break;          } + +        if (sendall(client_sock, +                    &burstRequest.tx_second, +                    sizeof(burstRequest.tx_second)) < 0) { +            etiLog.level(info) << +                "DPD Feedback Server Client send tx_second failed"; +            break; +        } + +        if (sendall(client_sock, +                    &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); + +        assert(burstRequest.tx_samples.size() >= frame_bytes); +        if (sendall(client_sock, +                    &burstRequest.tx_samples[0], +                    frame_bytes) < 0) { +            etiLog.level(info) << +                "DPD Feedback Server Client send tx_frame failed"; +            break; +        } + +        if (sendall(client_sock, +                    &burstRequest.rx_second, +                    sizeof(burstRequest.rx_second)) < 0) { +            etiLog.level(info) << +                "DPD Feedback Server Client send rx_second failed"; +            break; +        } + +        if (sendall(client_sock, +                    &burstRequest.rx_pps, +                    sizeof(burstRequest.rx_pps)) < 0) { +            etiLog.level(info) << +                "DPD Feedback Server Client send rx_pps failed"; +            break; +        } + +        assert(burstRequest.rx_samples.size() >= frame_bytes); +        if (sendall(client_sock, +                    &burstRequest.rx_samples[0], +                    frame_bytes) < 0) { +            etiLog.level(info) << +                "DPD Feedback Server Client send rx_frame failed"; +            break; +        } + +        close(client_sock);      } -    catch (runtime_error &e) { -        etiLog.level(error) << "DPD Feedback Server fault: " << e.what(); +} + +void OutputUHDFeedback::ServeFeedbackThread() +{ +    set_thread_name("uhdservefeedback"); + +    while (m_running) { +        try { +            ServeFeedback(); +        } +        catch (runtime_error &e) { +            etiLog.level(error) << "DPD Feedback Server fault: " << e.what(); +        } + +        boost::this_thread::sleep(boost::posix_time::seconds(5));      }      m_running = false; diff --git a/src/OutputUHDFeedback.h b/src/OutputUHDFeedback.h index 32668b6..c68f4c2 100644 --- a/src/OutputUHDFeedback.h +++ b/src/OutputUHDFeedback.h @@ -101,6 +101,7 @@ class OutputUHDFeedback {          // 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; | 
