From 15740bcc3b3e1d5adff8c77306d84741a26ebdad Mon Sep 17 00:00:00 2001
From: Balint Seeber <balint@ettus.com>
Date: Wed, 1 Oct 2014 17:20:20 -0700
Subject: utils: Added latency measurement utility

---
 host/utils/CMakeLists.txt                |    2 +
 host/utils/latency/CMakeLists.txt        |   46 +
 host/utils/latency/graph.py              |  376 ++++++++
 host/utils/latency/include/Responder.hpp |  299 ++++++
 host/utils/latency/lib/Responder.cpp     | 1465 ++++++++++++++++++++++++++++++
 host/utils/latency/pci_hwdata.py         |  231 +++++
 host/utils/latency/responder.cpp         |  133 +++
 host/utils/latency/run_tests.py          |  222 +++++
 8 files changed, 2774 insertions(+)
 create mode 100644 host/utils/latency/CMakeLists.txt
 create mode 100755 host/utils/latency/graph.py
 create mode 100644 host/utils/latency/include/Responder.hpp
 create mode 100644 host/utils/latency/lib/Responder.cpp
 create mode 100755 host/utils/latency/pci_hwdata.py
 create mode 100644 host/utils/latency/responder.cpp
 create mode 100755 host/utils/latency/run_tests.py

(limited to 'host/utils')

diff --git a/host/utils/CMakeLists.txt b/host/utils/CMakeLists.txt
index f693ee7a6..3db28fa3c 100644
--- a/host/utils/CMakeLists.txt
+++ b/host/utils/CMakeLists.txt
@@ -168,3 +168,5 @@ IF(ENABLE_USRP2)
     ENDFOREACH(burner ${burners})
 
 ENDIF(ENABLE_USRP2)
+
+ADD_SUBDIRECTORY(latency)
diff --git a/host/utils/latency/CMakeLists.txt b/host/utils/latency/CMakeLists.txt
new file mode 100644
index 000000000..2ea996857
--- /dev/null
+++ b/host/utils/latency/CMakeLists.txt
@@ -0,0 +1,46 @@
+#
+# Copyright 2010-2013 Ettus Research LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+FIND_PACKAGE(Curses)
+
+IF(CURSES_FOUND)
+    INCLUDE_DIRECTORIES(${CURSES_INCLUDE_DIR})
+    SET(latency_include_dir ${CMAKE_CURRENT_SOURCE_DIR}/include)
+    INCLUDE_DIRECTORIES(${latency_include_dir})
+    SET(latency_lib_path ${CMAKE_CURRENT_SOURCE_DIR}/lib/Responder.cpp)
+
+    SET(sources
+        responder.cpp
+    )
+
+    SET(latency_comp_name utilities)
+    SET(latency_comp_dest ${PKG_LIB_DIR}/utils/latency)
+
+    #for each source: build an executable and install
+    FOREACH(source ${sources})
+        GET_FILENAME_COMPONENT(name ${source} NAME_WE)
+        ADD_EXECUTABLE(${name} ${source} ${latency_lib_path})
+    	LIBUHD_APPEND_SOURCES(${name})
+        TARGET_LINK_LIBRARIES(${name} uhd ${Boost_LIBRARIES} ${CURSES_LIBRARIES})
+    	UHD_INSTALL(TARGETS ${name} RUNTIME DESTINATION ${latency_comp_dest} COMPONENT ${latency_comp_name})
+    ENDFOREACH(source)
+
+    UHD_INSTALL(PROGRAMS run_tests.py graph.py
+                DESTINATION ${latency_comp_dest}
+                COMPONENT ${latency_comp_name}
+    )
+ENDIF(CURSES_FOUND)
diff --git a/host/utils/latency/graph.py b/host/utils/latency/graph.py
new file mode 100755
index 000000000..6aa2ba4e5
--- /dev/null
+++ b/host/utils/latency/graph.py
@@ -0,0 +1,376 @@
+#!/usr/bin/env python
+#
+# Copyright 2012 Ettus Research LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import sys, re
+from optparse import OptionParser
+
+import matplotlib.pyplot as plt
+import matplotlib.font_manager
+import numpy as np
+
+from gnuradio.eng_option import eng_option
+
+_units = [
+    (3, "k"),
+    (6, "M"),
+    (9, "G")
+]
+
+
+def _format_rate(rate):
+    for (u1, s1), (u2, s2) in zip(_units, _units[1:]):
+        n = pow(10, u1)
+        if rate >= n and rate < pow(10, u2):
+            r = rate % n
+            if r > 0:
+                return str(1.0 * rate / n) + " " + s1
+            else:
+                return str(rate / n) + " " + s1
+    return str(rate) + " "
+
+
+def _sort(series, keys):
+    if len(keys) == 0:
+        return []
+    key = keys[0]
+    rev = False
+    if key[0] == '-':
+        key = key[1:]
+        rev = True
+    l = []
+    for s in series:
+        if s[key] not in l:
+            l += [s[key]]
+    l.sort()
+    if rev:
+        l.reverse()
+    return [(key, l)] + _sort(series, keys[1:])
+
+
+def _order(series, sort_list):
+    if len(sort_list) == 0:
+        return series
+    (sort_key, sort_key_list) = sort_list[0]
+    if len(sort_key_list) == 0:
+        return []
+    #print sort_key, sort_key_list
+    l = []
+    for s in series:
+        if s[sort_key] == sort_key_list[0]:
+            l += [s]
+    #print l
+    return _order(l, sort_list[1:]) + _order(series, [(sort_list[0][0], sort_list[0][1][1:])] + sort_list[1:])
+
+
+def get_option_parser():
+    usage = "%prog: [options]"
+    parser = OptionParser(option_class=eng_option, usage=usage)
+
+    parser.add_option("", "--id", type="string", help="device ID [default: %default]", default=None)
+    parser.add_option("", "--sort", type="string", help="sort order [default: %default]", default="rate -spb -spp")
+    parser.add_option("", "--output", type="string", help="output file [default: %default]", default=None)
+    parser.add_option("", "--output-type", type="string", help="output file type [default: %default]", default="pdf")
+    parser.add_option("", "--output-size", type="string", help="output file size [default: %default pixels]",
+                      default="1600,900")
+    parser.add_option("", "--xrange", type="float", help="X range [default: %default]", default=None)
+    parser.add_option("", "--title", type="string", help="additional title [default: %default]", default=None)
+    parser.add_option("", "--legend", type="string", help="legend position [default: %default]", default="lower right")
+    parser.add_option("", "--diff", action="store_true", help="compare results instead of just plotting them", default=None)
+    return parser
+
+
+def get_sorted_series(args, options):
+    series = []
+
+    if len(args) > 0:
+        with open(args[0]) as f:
+            lines = f.readlines()
+    else:
+        lines = sys.stdin.readlines()
+    if lines is None or len(lines) == 0:
+        return
+
+    for line in lines:
+        line = line.strip()
+        if len(line) == 0:
+            continue
+        x = {'file': line}
+        idx2 = 0
+        idx = line.find("latency-stats")
+        if idx > 0:
+            x['prefix'] = line[0:idx]
+        idx = line.find(".id_")
+        if idx > -1:
+            idx += 4
+            idx2 = line.find("-", idx)
+            x['id'] = line[idx:idx2]
+            if options.id is None:
+                options.id = x['id']
+            elif options.id != x['id']:
+                print "Different IDs:", options.id, x['id']
+        idx = line.find("-rate_")
+        if idx > -1:
+            idx += 6
+            idx2 = line.find("-", idx)
+            x['rate'] = int(line[idx:idx2])
+        idx = line.find("-spb_")
+        if idx > -1:
+            idx += 5
+            idx2 = line.find("-", idx)
+            x['spb'] = int(line[idx:idx2])
+        idx = line.find("-spp_")
+        if idx > -1:
+            idx += 5
+            #idx2 = line.find(".", idx)
+            idx2 = re.search("\D", line[idx:])
+            if idx2:
+                idx2 = idx + idx2.start()
+            else:
+                idx2 = -1
+            x['spp'] = int(line[idx:idx2])
+        idx = line.rfind(".")
+        if idx > -1 and idx >= idx2:
+            idx2 = re.search("\d", line[::-1][len(line) - idx:])
+            if idx2 and (idx2.start() > 0):
+                idx2 = idx2.start()
+                x['suffix'] = line[::-1][len(line) - idx:][0:idx2][::-1]
+        print x
+        series += [x]
+
+    sort_keys = options.sort.split()
+    print sort_keys
+    sorted_key_list = _sort(series, sort_keys)
+    print sorted_key_list
+    series = _order(series, sorted_key_list)
+
+    return series
+
+
+def main():
+    # Create object with all valid options
+    parser = get_option_parser()
+
+    # Read in given command line options and arguments
+    (options, args) = parser.parse_args()
+
+
+    # series contains path and attributes for all data sets given by args.
+    series = get_sorted_series(args, options)
+
+    # Read in actual data sets from file
+    data = read_series_data(series)
+
+    if options.diff:
+        data = calculate_data_diff(data)
+
+
+    # Get all the wanted properties for this plot
+    plt_props = get_plt_props(options)
+    print plt_props
+
+    mpl_plot(data, plt_props)
+
+    return 0
+
+
+def read_series_data(series):
+    result = []
+    for s in series:
+        data = {}
+        [data_x, data_y] = np.loadtxt(s['file'], delimiter=" ", unpack=True)
+        data['x'] = data_x
+        data['y'] = data_y
+        data['metadata'] = s
+        result.append(data)
+    return result
+
+
+def find_values(data, key):
+    result = []
+    for d in data:
+        val = d['metadata'][key]
+        if not val in result:
+            result.append(val)
+    return result
+
+
+def find_match(data, key, val):
+    result = []
+    for d in data:
+        meta = d['metadata']
+        if meta[key] == val:
+            result.append(d)
+    return result
+
+def get_data_diff(data):
+    if not data:
+        return data # just return. User didn't input any data.
+    if len(data) < 2:
+        return data[0] # Single data set. Can't calculate a diff.
+
+    print "diff %d: rate %s, spb %s, spp %s" % (len(data), data[0]['metadata']['rate'], data[0]['metadata']['spb'], data[0]['metadata']['spp'])
+
+    data = align_data(data)
+
+    min_len = len(data[0]['x'])
+    for d in data:
+        min_len = min(min_len, len(d['x']))
+
+    metadiff = ""
+    for d in data:
+        m = d['metadata']['prefix']
+        for r in "/._":
+            m = m.replace(r, "")
+        metadiff += m + "-"
+
+    xd = data[0]['x'][0:min_len]
+    yd = data[0]['y'][0:min_len]
+    meta = data[0]['metadata']
+    meta['diff'] = metadiff
+    other = data[1:]
+    for d in other:
+        y = d['y']
+        for i in range(len(yd)):
+            yd[i] -= y[i]
+
+    result = {}
+    result['x'] = xd
+    result['y'] = yd
+    result['metadata'] = meta
+    return result
+
+def align_data(data):
+    x_start = 0
+    for d in data:
+        x_start = max(x_start, d['x'][0])
+
+    for i in range(len(data)):
+        s = np.where(data[i]['x'] == x_start)[0]
+        data[i]['x'] = data[i]['x'][s:]
+        data[i]['y'] = data[i]['y'][s:]
+
+    return data
+
+
+def calculate_data_diff(data):
+    spps = find_values(data, "spp")
+    spbs = find_values(data, "spb")
+    rates = find_values(data, "rate")
+    print spps, "\t", spbs, "\t", rates
+    result = []
+    for rate in rates:
+        rd = find_match(data, "rate", rate)
+        for spb in spbs:
+            bd = find_match(rd, "spb", spb)
+            for spp in spps:
+                pd = find_match(bd, "spp", spp)
+                if len(pd) > 0:
+                    result.append(get_data_diff(pd))
+
+    return result
+
+
+def get_plt_props(options):
+    plt_props = {}
+    plt_out = None
+    if options.output is not None:
+        try:
+            idx = options.output_size.find(",")
+            x = int(options.output_size[0:idx])
+            y = int(options.output_size[idx + 1:])
+            plt_out = {'name': options.output,
+                       'type': options.output_type,
+                       'size': [x, y]}
+        except:
+            plt_out = None
+
+    plt_props['output'] = plt_out
+
+    plt_title = "Latency (" + options.id + ")"
+    if options.title is not None and len(options.title) > 0:
+        plt_title += " - " + options.title
+    plt_props['title'] = plt_title
+
+    plt_props['xlabel'] = "Latency (us)"
+    plt_props['ylabel'] = "Normalised success of on-time burst transmission"
+
+    plt_legend_loc = None
+    if options.legend is not None:
+        plt_legend_loc = options.legend
+    plt_props['legend'] = plt_legend_loc
+
+    plt_xrange = None
+    if options.xrange is not None:
+        plt_xrange = [0, options.xrange]
+    plt_props['xrange'] = plt_xrange
+    return plt_props
+
+
+def mpl_plot(data, props):
+    plt_out = props['output']
+    plt_title = props['title']
+    plt_xlabel = props['xlabel']
+    plt_ylabel = props['ylabel']
+    plt_legend_loc = props['legend']
+    plt_xrange = props['xrange']
+
+    markers = ['.', ',', 'o', 'v', '^', '<', '>', '1', '2', '3', '4', '8',
+               's', 'p', '*', 'h', 'H', '+', 'D', 'd', '|', '_']
+    colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k', 'w']
+    midx = 0
+
+    # plot available data.
+    mylegend = []
+    for d in data:
+        mylegend.append(get_legend_str(d['metadata']))
+        plt.plot(d['x'], d['y'], marker=markers[midx], markerfacecolor=None)
+        midx = (midx + 1) % len(markers)
+
+    # Set all plot properties
+    plt.title(plt_title)
+    plt.xlabel(plt_xlabel)
+    plt.ylabel(plt_ylabel)
+    plt.grid()
+    fontP = matplotlib.font_manager.FontProperties()
+    fontP.set_size('x-small')
+    plt.legend(mylegend, loc=plt_legend_loc, prop=fontP, ncol=2)
+    if plt_xrange is not None:
+        plt.xlim(plt_xrange)
+
+    # Save plot to file, if option is given.
+    if plt_out is not None:
+        fig = plt.gcf() # get current figure
+        dpi = 100.0 # Could be any value. It exists to convert the input in pixels to inches/dpi.
+        figsize = (plt_out['size'][0] / dpi, plt_out['size'][1] / dpi) # calculate figsize in inches
+        fig.set_size_inches(figsize)
+        name = plt_out['name'] + "." + plt_out['type']
+        plt.savefig(name, dpi=dpi, bbox_inches='tight')
+
+    plt.show()
+
+
+def get_legend_str(meta):
+    lt = ""
+    if meta['diff']:
+        lt += meta['diff'] + " "
+    lt += "%ssps, SPB %d, SPP %d" % (_format_rate(meta['rate']), meta['spb'], meta['spp'])
+    return lt
+
+
+if __name__ == '__main__':
+    main()
diff --git a/host/utils/latency/include/Responder.hpp b/host/utils/latency/include/Responder.hpp
new file mode 100644
index 000000000..a9f616a24
--- /dev/null
+++ b/host/utils/latency/include/Responder.hpp
@@ -0,0 +1,299 @@
+//
+// Copyright 2010-2013 Ettus Research LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+
+#ifndef RESPONDER_H
+#define RESPONDER_H
+
+#include <curses.h>
+#include <map>
+#include <ctime>
+#include <stdint.h>
+
+#include <uhd/usrp/multi_usrp.hpp>
+#include <uhd/utils/msg.hpp>
+
+using namespace std;
+
+
+class Responder
+{
+    public:
+        enum ReturnCodes
+        {
+            RETCODE_OK                  = 0,
+            RETCODE_BAD_ARGS            = -1,
+            RETCODE_RUNTIME_ERROR       = -2,
+            RETCODE_UNKNOWN_EXCEPTION   = -3,
+            RETCODE_RECEIVE_TIMEOUT     = -4,
+            RETCODE_RECEIVE_FAILED      = -5,
+            RETCODE_MANUAL_ABORT        = -6,
+            RETCODE_BAD_PACKET          = -7,
+            RETCODE_OVERFLOW            = -8
+        };
+
+        struct Options
+        {
+            string device_args;
+            double delay;
+            double sample_rate;
+            double trigger_level;
+            float output_scale;
+            double response_duration;
+            double dc_offset_delay;
+            double init_delay;
+            double timeout;
+            size_t samps_per_buff;
+            size_t samps_per_packet;
+            double level_calibration_duration;
+            std::string test_title;
+            std::string stats_filename;
+            std::string stats_filename_prefix;
+            std::string stats_filename_suffix;
+            double delay_min;
+            double delay_max;
+            double delay_step;
+            double pulse_detection_threshold;
+            uint64_t test_iterations;
+            size_t end_test_after_success_count;
+            size_t skip_iterations;
+            double simulate_frequency;
+            double time_mul;
+            size_t flush_count;
+            size_t optimize_padding;
+            double rt_priority;
+            bool ignore_simulation_check;
+            bool test_iterations_is_sample_count;
+            bool skip_eob;
+            bool adjust_simulation_rate;
+            bool optimize_simulation_rate;
+            bool no_stats_file;
+            bool log_file;
+            bool batch_mode;
+            bool skip_if_results_exist;
+            bool skip_send;
+            bool combine_eob;
+            bool pause;
+            bool realtime;
+            bool invert;
+            bool output_value;
+            bool no_delay;
+            bool allow_late_bursts;
+
+            uint64_t level_calibration_count() const
+            {
+                return (uint64_t)(sample_rate * level_calibration_duration);
+            }
+
+            uint64_t response_length() const
+            {
+                return (uint64_t)(sample_rate * response_duration);
+            }
+
+            uint64_t highest_delay_samples(const double delay) const
+            {
+                return (uint64_t)(delay * (double)sample_rate);
+            }
+
+            uint64_t simulate_duration(const double simulate_frequency) const
+            {
+                if(simulate_frequency > 0.0) {
+                    return (uint64_t)((double)sample_rate / simulate_frequency);
+                }
+                return 0;
+            }
+        };
+
+        typedef struct Stats
+        {
+            double delay;
+            uint64_t detected;
+            uint64_t missed;
+            uint64_t skipped;
+        } STATS;
+
+        typedef std::map<uint64_t,STATS> StatsMap;
+
+        struct DebugInfo
+        {
+            time_t start_time;
+            time_t end_time;
+            time_t start_time_test;
+            time_t end_time_test;
+            time_t first_send_timeout;
+        };
+        Responder(Options& opt);
+        virtual ~Responder();
+
+        // Main entry point after constructor.
+        int run();
+
+        // Public method to inject UHD messages in the main ncurses window.
+        void print_uhd_late_handler(uhd::msg::type_t type, const std::string& msg);
+
+        int get_return_code(){return _return_code;}
+
+    protected:
+    private:
+        // These 2 variables are used for ncurses output.
+        WINDOW* _window;
+        std::stringstream _ss;
+
+        // struct which holds all arguments as constants settable from outside the class
+        const Options _opt;
+
+        string _stats_filename; // Specify name of statistics file
+        string _stats_log_filename; // Specify name for log file.
+        double _delay; // may be altered in all modes.
+        size_t _samps_per_packet; // This is one of the options of interest. Find out how well it performs.
+        double _delay_step; // may be altered in interactive mode
+        double _simulate_frequency; // updated during automatic test iterations
+
+        // additional attributes
+        bool _allow_late_bursts; // may be altered in interactive mode
+        bool _no_delay; // may be altered in interactive mode
+
+        // dependent variables
+        uint64_t _response_length;
+        int64_t _init_delay_count;
+        int64_t _dc_offset_countdown;
+        int64_t _level_calibration_countdown;
+        uint64_t _simulate_duration;
+        uint64_t _original_simulate_duration;
+
+        // these variables store test conditions
+        uint64_t _num_total_samps; // printed on exit
+        size_t _overruns; // printed on exit
+        StatsMap _mapStats; // store results
+        uint64_t _max_success; // < 0 --> write results to file
+        int _return_code;
+
+        // Hold USRP, streams and commands
+        uhd::usrp::multi_usrp::sptr _usrp;
+        uhd::tx_streamer::sptr _tx_stream;
+        uhd::rx_streamer::sptr _rx_stream;
+        uhd::stream_cmd_t _stream_cmd;
+
+        // Keep track of number of timeouts.
+        uint64_t _timeout_burst_count;
+        uint64_t _timeout_eob_count;
+
+        // Transmit attributes
+        float* _pResponse;
+
+        // Control print parameters.
+        int _y_delay_pos;
+        int _x_delay_pos; // Remember the cursor position of delay line
+        uint64_t _last_overrun_count;
+
+        // Hold debug info during test. Will be included in log file.
+        DebugInfo _dbginfo;
+
+        /*
+         * Here are the class's member methods.
+         */
+        // These methods are used for ncurses output
+        void create_ncurses_window();
+        void FLUSH_SCREEN();
+        void FLUSH_SCREEN_NL();
+
+        // Variable calculation helpers
+        inline uint64_t get_response_length(double sample_rate, double response_duration)
+                                    {return (uint64_t)(sample_rate * response_duration);}
+        int calculate_dependent_values();
+
+        // make sure existing results are not overwritten accidently
+        bool set_stats_filename(string test_id);
+        bool check_for_existing_results();
+
+        // Functions that may cause Responder to finish
+        void register_stop_signal_handler();
+        bool test_finished(size_t success_count);
+        int test_step_finished(uint64_t trigger_count, uint64_t num_total_samps_test, STATS statsCurrent, size_t success_count);
+
+        // Check if sent burst could be transmitted.
+        bool tx_burst_is_late();
+
+        // Handle receiver errors such as overflows.
+        bool handle_rx_errors(uhd::rx_metadata_t::error_code_t err, size_t num_rx_samps);
+
+        // In interactive mode, handle Responder control and output.
+        bool handle_interactive_control();
+        void print_interactive_msg(std::string msg);
+
+        // calibration important for interactive mode with 2nd USRP connected.
+        float calibrate_usrp_for_test_run();
+
+        // Run actual test
+        void run_test(float threshold = 0.0f );
+
+        // Detect falling edge
+        bool get_new_state(uint64_t total_samps, uint64_t simulate_duration, float val, float threshold);
+        uint64_t detect_respond_pulse_count(STATS &statsCurrent, std::vector<std::complex<float> > &buff, uint64_t trigger_count, size_t num_rx_samps, float threshold, uhd::time_spec_t rx_time);
+
+        // Hold test results till they are printed to a file
+        void add_stats_to_results(STATS statsCurrent, double delay);
+
+        // Control USRP and necessary streamers
+        uhd::usrp::multi_usrp::sptr create_usrp_device();
+        void set_usrp_rx_dc_offset(uhd::usrp::multi_usrp::sptr usrp, bool ena);
+        void stop_usrp_stream();
+        uhd::tx_streamer::sptr create_tx_streamer(uhd::usrp::multi_usrp::sptr usrp);
+        uhd::rx_streamer::sptr create_rx_streamer(uhd::usrp::multi_usrp::sptr usrp);
+
+        // Send burst and handle results.
+        bool send_tx_burst(uhd::time_spec_t rx_time, size_t n);
+        void handle_tx_timeout(int burst, int eob);
+        float* alloc_response_buffer_with_data(uint64_t response_length);
+        uhd::tx_metadata_t get_tx_metadata(uhd::time_spec_t rx_time, size_t n);
+
+        // Control test parameters
+        void update_and_print_parameters(const STATS& statsPrev, const double delay);
+        double get_simulate_frequency(double delay, uint64_t response_length, uint64_t original_simulate_duration);
+        double get_max_possible_frequency(uint64_t highest_delay_samples, uint64_t response_length);
+
+        // Helper methods to print status during test.
+        void print_init_test_status();
+        void print_test_title();
+        void print_usrp_status();
+        void print_create_usrp_msg();
+        void print_tx_stream_status();
+        void print_rx_stream_status();
+        void print_test_parameters();
+        void print_formatted_delay_line(const uint64_t simulate_duration, const uint64_t old_simulate_duration, const STATS& statsPrev, const double delay, const double simulate_frequency);
+        void print_overrun_msg();
+        void print_error_msg(std::string msg);
+        void print_timeout_msg();
+        void print_final_statistics();
+        void print_msg_and_wait(std::string msg);
+        void print_msg(std::string msg);
+
+        // Safe results of test to file.
+        void write_statistics_to_file(StatsMap mapStats);
+        void safe_write_statistics_to_file(StatsMap mapStats, uint64_t max_success, int return_code);
+        void write_log_file();
+
+        // Write debug info to log file if requested.
+        void write_debug_info(ofstream& logs);
+        std::string get_gmtime_string(time_t time);
+        std::string enum2str(int return_code);
+        std::vector<std::map<std::string,std::string> > read_eth_info();
+        uhd::device_addr_t get_usrp_info();
+        std::map<std::string, std::string> get_hw_info();
+        std::string get_ip_subnet_addr(std::string ip);
+};
+
+#endif // RESPONDER_H
diff --git a/host/utils/latency/lib/Responder.cpp b/host/utils/latency/lib/Responder.cpp
new file mode 100644
index 000000000..d265e9dcb
--- /dev/null
+++ b/host/utils/latency/lib/Responder.cpp
@@ -0,0 +1,1465 @@
+//
+// Copyright 2010-2013 Ettus Research LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+
+#include "Responder.hpp"
+
+#include <iostream>
+#include <iomanip>
+#include <fstream>
+#include <complex>
+#include <csignal>
+#include <cmath>
+
+#include <boost/format.hpp>
+#include <boost/algorithm/string.hpp>
+#include <boost/filesystem.hpp>
+#include <uhd/utils/thread_priority.hpp>
+#include <uhd/property_tree.hpp>
+
+const std::string _eth_file("eths_info.txt");
+
+// UHD screen handler during initialization. Error messages will be printed to log file
+static std::string uhd_error_msgs;
+static void screen_handler(uhd::msg::type_t type, const std::string& msg)
+{
+    printw( msg.c_str() );
+    //printw("\n");
+    refresh();
+    if(type == uhd::msg::error){
+        uhd_error_msgs.append(msg);
+        uhd_error_msgs.append("\n");
+    }
+}
+
+// UHD screen handler during test run. Error messages will be printed to log file
+static int s_late_count = 0;
+static Responder* s_responder; // needed here to have a way to inject uhd msg into Responder.
+// function is only called by UHD, if s_responder points to a valid instance.
+// this instance sets the function to be the output callback for UHD.
+static void _late_handler(uhd::msg::type_t type, const std::string& msg)
+{
+    s_responder->print_uhd_late_handler(type, msg);
+}
+
+void Responder::print_uhd_late_handler(uhd::msg::type_t type, const std::string& msg)
+{
+    if (msg == "L") // This is just a test
+    {
+        ++s_late_count;
+    }
+    if(type == uhd::msg::error){
+        uhd_error_msgs.append(msg);
+        uhd_error_msgs.append("\n");
+        // Only print error messages. There will be very many 'L's due to the way the test works.
+        print_msg(msg);
+    }
+}
+
+// Catch keyboard interrupts for clean manual abort
+static bool s_stop_signal_called = false;
+static int s_signal = 0;
+static void sig_int_handler(int signal)
+{
+    s_stop_signal_called = true;
+    s_signal = signal;
+}
+
+// member of Responder to register sig int handler
+void
+Responder::register_stop_signal_handler()
+{
+    std::signal(SIGINT, &sig_int_handler);
+}
+
+// For ncurses. Print everything in stream to screen
+void
+Responder::FLUSH_SCREEN()
+{
+    printw(_ss.str().c_str());
+    refresh();
+    _ss.str("");
+}
+
+// Like FLUSH_SCREEN but with new line
+void
+Responder::FLUSH_SCREEN_NL()
+{
+    do {
+        int y, x;
+        getyx(_window, y, x);
+        if (x > 0){
+            printw("\n");
+            y++;
+        }
+        FLUSH_SCREEN();
+    } while (0);
+}
+
+// Constructor
+Responder::Responder( Options& opt)
+    : _opt(opt),
+    _stats_filename(opt.stats_filename),
+    _delay(opt.delay),
+    _samps_per_packet(opt.samps_per_packet),
+    _delay_step(opt.delay_step),
+    _simulate_frequency(opt.simulate_frequency),
+    _allow_late_bursts(opt.allow_late_bursts),
+    _no_delay(opt.no_delay),
+    //Initialize atributes not given by Options
+    _num_total_samps(0), // printed on exit
+    _overruns(0), // printed on exit
+    _max_success(0), // < 0 --> write results to file
+    _return_code(RETCODE_OK),
+    _stream_cmd(uhd::stream_cmd_t::STREAM_MODE_START_CONTINUOUS),
+    _timeout_burst_count(0),
+    _timeout_eob_count(0),
+    _y_delay_pos(-1),
+    _x_delay_pos(-1), // Remember the cursor position of delay line.
+    _last_overrun_count(0)
+{
+    time( &_dbginfo.start_time ); // for debugging
+    s_responder = this;
+
+    if (uhd::set_thread_priority_safe(_opt.rt_priority, _opt.realtime) == false) // try to set realtime scheduling
+    {
+        cerr << "Failed to set real-time" << endl;
+    }
+
+    _return_code = calculate_dependent_values();
+
+    uhd::msg::register_handler(&screen_handler); // used to print USRP initialization status
+
+    // From this point on, everything is written to a ncurses window!
+    create_ncurses_window();
+
+    print_create_usrp_msg();
+    try
+    {
+        _usrp = create_usrp_device();
+    }
+    catch (const std::runtime_error& e)
+    {
+        print_msg(e.what() );
+        _return_code = RETCODE_RUNTIME_ERROR;
+    }
+    catch(...){
+        print_msg("unhandled ERROR");
+        _return_code = RETCODE_UNKNOWN_EXCEPTION;
+        print_msg_and_wait("create USRP device failed!\nPress key to abort test...");
+        return;
+    }
+
+    // Prepare array with response burst data.
+    _pResponse = alloc_response_buffer_with_data(_response_length);
+
+    // ensure that filename is set
+    string test_id = _usrp->get_mboard_name();
+    if (set_stats_filename(test_id) )
+    {
+        _return_code = RETCODE_BAD_ARGS; // make sure run() does return!
+        FLUSH_SCREEN();
+        if (_opt.batch_mode == false)
+        {
+            print_msg_and_wait("Press any key to end...");
+        }
+        return;
+    }
+
+    // set up handlers for test run
+    uhd::msg::register_handler(&_late_handler); // capture UHD output.
+    register_stop_signal_handler();
+}
+
+int
+Responder::calculate_dependent_values()
+{
+    _response_length = _opt.response_length();
+    _init_delay_count = (int64_t)(_opt.sample_rate * _opt.init_delay);
+    _dc_offset_countdown = (int64_t)(_opt.sample_rate * _opt.dc_offset_delay);
+    _level_calibration_countdown = (int64_t)_opt.level_calibration_count();
+    _original_simulate_duration = _simulate_duration = _opt.simulate_duration(_simulate_frequency);
+
+    if (_simulate_duration > 0)
+    {
+        // Skip settling period and calibration
+        _init_delay_count = 0;
+        _dc_offset_countdown = 0;
+        _level_calibration_countdown = 0;
+
+        double highest_delay = 0.0;
+        if (_opt.test_iterations > 0)
+            highest_delay = max(_opt.delay_max, _opt.delay_min);
+        else if (_no_delay == false)
+            highest_delay = _delay;
+
+        uint64_t highest_delay_samples = _opt.highest_delay_samples(highest_delay);
+        if ((highest_delay_samples + _response_length + _opt.flush_count) > _simulate_duration)
+        {
+            if (_opt.adjust_simulation_rate) // This is now done DURING the simulation based on active delay
+            {
+                //_simulate_frequency = max_possible_rate;
+                //_simulate_duration = (uint64_t)((double)sample_rate / _simulate_frequency);
+            }
+            else
+            {
+                cerr << boost::format("Highest delay and response duration will exceed the pulse simulation rate (%ld + %ld > %ld samples)") % highest_delay_samples % _response_length % _simulate_duration << endl;
+                int max_possible_rate = (int) get_max_possible_frequency(highest_delay_samples, _response_length);
+                double max_possible_delay = (double)(_simulate_duration - (_response_length + _opt.flush_count)) / (double)_opt.sample_rate;
+                cerr << boost::format("Simulation rate must be less than %i Hz, or maximum delay must be less than %f s") % max_possible_rate % max_possible_delay << endl;
+
+                if (_opt.ignore_simulation_check == 0)
+                    return RETCODE_BAD_ARGS;
+            }
+        }
+    }
+    else
+    {
+        boost::format fmt("Simulation frequency too high (%f Hz with sample_rate %f Msps)");
+        fmt % _simulate_frequency % (_opt.sample_rate/1e6);
+        cerr << fmt << endl;
+        return RETCODE_BAD_ARGS;
+    }
+
+    if (_opt.test_iterations > 0)    // Force certain settings during test mode
+    {
+        _no_delay = false;
+        _allow_late_bursts = false;
+        _delay = _opt.delay_min;
+    }
+    return RETCODE_OK; // default return code
+}
+
+// print test title to ncurses window
+void
+Responder::print_test_title()
+{
+    if (_opt.test_title.empty() == false)
+    {
+        std::string title(_opt.test_title);
+        boost::replace_all(title, "%", "%%");
+        print_msg(title + "\n");
+    }
+}
+
+void
+Responder::print_usrp_status()
+{
+    std::string msg;
+    msg += (boost::format("Using device:\n%s\n") % _usrp->get_pp_string() ).str();
+    msg += (boost::format("Setting RX rate: %f Msps\n") % (_opt.sample_rate/1e6)).str();
+    msg += (boost::format("Actual RX rate:  %f Msps\n") % (_usrp->get_rx_rate()/1e6) ).str();
+    msg += (boost::format("Setting TX rate: %f Msps\n") % (_opt.sample_rate/1e6) ).str();
+    msg += (boost::format("Actual TX rate:  %f Msps") % (_usrp->get_tx_rate()/1e6) ).str();
+    print_msg(msg);
+    print_tx_stream_status();
+    print_rx_stream_status();
+}
+
+void
+Responder::print_test_parameters()
+{
+    // Some status output shoud be printed here!
+    size_t rx_max_num_samps = _rx_stream->get_max_num_samps();
+    size_t tx_max_num_samps = _tx_stream->get_max_num_samps();
+    std::string msg;
+
+    msg += (boost::format("Samples per buffer: %d\n") % _opt.samps_per_buff).str();
+    msg += (boost::format("Maximum number of samples: RX = %d, TX = %d\n") % rx_max_num_samps % tx_max_num_samps).str();
+    msg += (boost::format("Response length: %ld samples (%f us)") % _response_length % (_opt.response_duration * 1e6) ).str();
+
+    if (_simulate_duration > 0)
+        msg += (boost::format("\nSimulating pulses at %f Hz (every %ld samples)") % _simulate_frequency % _simulate_duration ).str();
+
+    if (_opt.test_iterations > 0)
+    {
+        msg += (boost::format("\nTest coverage: %f -> %f (%f steps)") % _opt.delay_min % _opt.delay_max % _opt.delay_step ).str();
+
+        if (_opt.end_test_after_success_count > 0)
+            msg += (boost::format("\nTesting will end after %d successful delays") % _opt.end_test_after_success_count ).str();
+    }
+
+    if ((_dc_offset_countdown == 0) && (_simulate_frequency == 0.0))
+    {
+        msg += "\nDC offset disabled";
+    }
+    print_msg(msg);
+}
+
+// e.g. B200 doesn't support this command. Check if possible and only set rx_dc_offset if available
+void
+Responder::set_usrp_rx_dc_offset(uhd::usrp::multi_usrp::sptr usrp, bool ena)
+{
+    uhd::property_tree::sptr tree = usrp->get_device()->get_tree();
+    // FIXME: Path needs to be build in a programmatic way.
+    bool dc_offset_exists = tree->exists( uhd::fs_path("/mboards/0/rx_frontends/A/dc_offset") );
+    if(dc_offset_exists)
+    {
+        usrp->set_rx_dc_offset(ena);
+    }
+}
+
+void
+Responder::print_create_usrp_msg()
+{
+    std::string msg("Creating the USRP device");
+    if (_opt.device_args.empty() == false)
+        msg.append( (boost::format(" with args \"%s\"") % _opt.device_args ).str() );
+    msg.append("...");
+    print_msg(msg);
+}
+
+uhd::usrp::multi_usrp::sptr
+Responder::create_usrp_device()
+{
+    uhd::usrp::multi_usrp::sptr usrp = uhd::usrp::multi_usrp::make(_opt.device_args);
+    usrp->set_rx_rate(_opt.sample_rate); // set the rx sample rate
+    usrp->set_tx_rate(_opt.sample_rate); // set the tx sample rate
+    _tx_stream = create_tx_streamer(usrp);
+    _rx_stream = create_rx_streamer(usrp);
+    if ((_dc_offset_countdown == 0) && (_simulate_frequency == 0.0))
+        set_usrp_rx_dc_offset(usrp, false);
+    return usrp;
+}
+
+uhd::rx_streamer::sptr
+Responder::create_rx_streamer(uhd::usrp::multi_usrp::sptr usrp)
+{
+    uhd::stream_args_t stream_args("fc32"); //complex floats
+    if (_samps_per_packet > 0)
+    {
+        stream_args.args["spp"] = str(boost::format("%d") % _samps_per_packet);
+    }
+    uhd::rx_streamer::sptr rx_stream = usrp->get_rx_stream(stream_args);
+    _samps_per_packet = rx_stream->get_max_num_samps();
+
+    return rx_stream;
+}
+
+void
+Responder::print_rx_stream_status()
+{
+    std::string msg;
+    msg += (boost::format("Samples per packet set to: %d\n") % _samps_per_packet).str();
+    msg += (boost::format("Flushing burst with %d samples") % _opt.flush_count).str();
+    if (_opt.skip_eob)
+        msg += "\nSkipping End-Of-Burst";
+    print_msg(msg);
+}
+
+uhd::tx_streamer::sptr
+Responder::create_tx_streamer(uhd::usrp::multi_usrp::sptr usrp)
+{
+    uhd::stream_args_t tx_stream_args("fc32"); //complex floats
+    if (_allow_late_bursts == false)
+    {
+        tx_stream_args.args["underflow_policy"] = "next_burst";
+    }
+    uhd::tx_streamer::sptr tx_stream = usrp->get_tx_stream(tx_stream_args);
+    return tx_stream;
+}
+
+void
+Responder::print_tx_stream_status()
+{
+    std::string msg;
+    if (_allow_late_bursts == false)
+    {
+        msg += "Underflow policy set to drop late bursts";
+    }
+    else
+        msg += "Underflow policy set to allow late bursts";
+    if (_opt.skip_send)
+        msg += "\nNOT sending bursts";
+    else if (_opt.combine_eob)
+        msg += "\nCombining EOB into first send";
+    print_msg(msg);
+}
+
+// handle transmit timeouts properly
+void
+Responder::handle_tx_timeout(int burst, int eob)
+{
+    if(_timeout_burst_count == 0 && _timeout_eob_count == 0)
+        time( &_dbginfo.first_send_timeout );
+    _timeout_burst_count += burst;
+    _timeout_eob_count += eob;
+    print_timeout_msg();
+}
+
+void
+Responder::print_timeout_msg()
+{
+    move(_y_delay_pos+3, _x_delay_pos);
+    print_msg( (boost::format("Send timeout, burst_count = %ld\teob_count = %ld\n") % _timeout_burst_count % _timeout_eob_count ).str() );
+}
+
+uhd::tx_metadata_t Responder::get_tx_metadata(uhd::time_spec_t rx_time, size_t n)
+{
+    uhd::tx_metadata_t tx_md;
+    tx_md.start_of_burst = true;
+    tx_md.end_of_burst = false;
+    if ((_opt.skip_eob == false) && (_opt.combine_eob)) {
+        tx_md.end_of_burst = true;
+    }
+
+    if (_no_delay == false) {
+        tx_md.has_time_spec = true;
+        tx_md.time_spec = rx_time + uhd::time_spec_t(0, n, _opt.sample_rate) + uhd::time_spec_t(_delay);
+    } else {
+        tx_md.has_time_spec = false;
+    }
+    return tx_md;
+}
+
+bool Responder::send_tx_burst(uhd::time_spec_t rx_time, size_t n)
+{
+    if (_opt.skip_send == true) {
+        return false;
+    }
+    //send a single packet
+    uhd::tx_metadata_t tx_md = get_tx_metadata(rx_time, n);
+    const size_t length_to_send = _response_length + (_opt.flush_count - (tx_md.end_of_burst ? 0 : 1));
+
+    size_t num_tx_samps = _tx_stream->send(_pResponse, length_to_send, tx_md, _opt.timeout); // send pulse!
+    if (num_tx_samps < length_to_send) {
+        handle_tx_timeout(1, 0);
+    }
+    if (_opt.skip_eob == false && _opt.combine_eob == false) {
+        tx_md.start_of_burst = false;
+        tx_md.end_of_burst = true;
+        tx_md.has_time_spec = false;
+
+        const size_t eob_length_to_send = 1;
+
+        size_t eob_num_tx_samps = _tx_stream->send(&_pResponse[length_to_send], eob_length_to_send, tx_md); // send EOB
+        if (eob_num_tx_samps < eob_length_to_send) {
+            handle_tx_timeout(0, 1);
+        }
+    }
+
+    return true;
+}
+
+// ensure that stats_filename is not empty.
+bool
+Responder::set_stats_filename(string test_id)
+{
+    if (_stats_filename.empty())
+    {
+        string file_friendly_test_id(test_id);
+        boost::replace_all(file_friendly_test_id, " ", "_");
+        boost::format fmt = boost::format("%slatency-stats.id_%s-rate_%i-spb_%i-spp_%i%s") % _opt.stats_filename_prefix % file_friendly_test_id % (int)_opt.sample_rate % _opt.samps_per_buff % _samps_per_packet % _opt.stats_filename_suffix;
+        _stats_filename = str(fmt) + ".txt";
+        _stats_log_filename = str(fmt) + ".log";
+    }
+    return check_for_existing_results();
+}
+
+// Check if results file can be overwritten
+bool
+Responder::check_for_existing_results()
+{
+    bool ex = false;
+    if ((_opt.skip_if_results_exist) && (boost::filesystem::exists(_stats_filename)))
+    {
+        print_msg( (boost::format("Skipping invocation as results file already exists: %s") %  _stats_filename).str() );
+        ex = true;
+    }
+    return ex;
+}
+
+// Allocate an array with a burst response
+float*
+Responder::alloc_response_buffer_with_data(uint64_t response_length) // flush_count, output_value, output_scale are const
+{
+    float* pResponse = new float[(response_length + _opt.flush_count) * 2];
+    for (unsigned int i = 0; i < (response_length * 2); ++i)
+        pResponse[i] = _opt.output_value * _opt.output_scale;
+    for (unsigned int i = (response_length * 2); i < ((response_length + _opt.flush_count) * 2); ++i)
+        pResponse[i] = 0.0f;
+    return pResponse;
+}
+
+// print test parameters for current delay time
+void
+Responder::print_formatted_delay_line(const uint64_t simulate_duration, const uint64_t old_simulate_duration, const STATS& statsPrev, const double delay, const double simulate_frequency)
+{
+    if(_y_delay_pos < 0 || _x_delay_pos < 0){ // make sure it gets printed to the same position everytime
+        getyx(_window, _y_delay_pos, _x_delay_pos);
+    }
+    double score = 0.0d;
+    if (statsPrev.detected > 0)
+        score = 100.0 * (double)(statsPrev.detected - statsPrev.missed) / (double)statsPrev.detected;
+    std::string form;
+    boost::format fmt0("Delay now: %.6f (previous delay %.6f scored %.1f%% [%ld / %ld])");
+    fmt0 % delay % statsPrev.delay % score % (statsPrev.detected - statsPrev.missed) % statsPrev.detected;
+    form += fmt0.str();
+    if (old_simulate_duration != simulate_duration)
+    {
+        boost::format fmt1(" [Simulation rate now: %.1f Hz (%ld samples)]");
+        fmt1 % simulate_frequency % simulate_duration;
+        form = form + fmt1.str();
+    }
+    move(_y_delay_pos, _x_delay_pos);
+    print_msg(form);
+}
+
+// print message and wait for user interaction
+void
+Responder::print_msg_and_wait(std::string msg)
+{
+    msg = "\n" + msg;
+    print_msg(msg);
+    timeout(-1);
+    getch();
+    timeout(0);
+}
+
+// print message to ncurses window
+void
+Responder::print_msg(std::string msg)
+{
+    _ss << msg << endl;
+    FLUSH_SCREEN();
+}
+
+// Check if error occured during call to receive
+bool
+Responder::handle_rx_errors(uhd::rx_metadata_t::error_code_t err, size_t num_rx_samps)
+{
+    // handle errors
+    if (err == uhd::rx_metadata_t::ERROR_CODE_TIMEOUT)
+    {
+        std::string msg = (boost::format("Timeout while streaming (received %ld samples)") % _num_total_samps).str();
+        print_error_msg(msg);
+        _return_code = RETCODE_OVERFLOW;
+        return true;
+    }
+    else if (err == uhd::rx_metadata_t::ERROR_CODE_BAD_PACKET)
+    {
+        std::string msg = (boost::format("Bad packet (received %ld samples)") % _num_total_samps).str();
+        print_error_msg(msg);
+        _return_code = RETCODE_BAD_PACKET;
+        return true;
+    }
+    else if ((num_rx_samps == 0) && (err == uhd::rx_metadata_t::ERROR_CODE_NONE))
+    {
+        print_error_msg("Received no samples");
+        _return_code = RETCODE_RECEIVE_FAILED;
+        return true;
+    }
+    else if (err == uhd::rx_metadata_t::ERROR_CODE_OVERFLOW)
+    {
+        ++_overruns;
+        print_overrun_msg(); // update overrun info on console.
+    }
+    else if (err != uhd::rx_metadata_t::ERROR_CODE_NONE)
+    {
+        throw std::runtime_error(str(boost::format(
+            "Unexpected error code 0x%x"
+        ) % err));
+    }
+    return false;
+}
+
+// print overrun status message.
+void
+Responder::print_overrun_msg()
+{
+    if (_num_total_samps > (_last_overrun_count + (uint64_t)(_opt.sample_rate * 1.0)))
+    {
+        int y, x, y_max, x_max;
+        getyx(_window, y, x);
+        getmaxyx(_window, y_max, x_max);
+        move(y_max-1, 0);
+        print_msg( (boost::format("Overruns: %d") % _overruns).str() );
+        move(y, x);
+        _last_overrun_count = _num_total_samps;
+    }
+}
+
+// print error message on last line of ncurses window
+void
+Responder::print_error_msg(std::string msg)
+{
+    int y, x, y_max, x_max;
+    getyx(_window, y, x);
+    getmaxyx(_window, y_max, x_max);
+    move(y_max-2, 0);
+    clrtoeol();
+    print_msg(msg);
+    move(y, x);
+}
+
+// calculate simulate frequency
+double
+Responder::get_simulate_frequency(double delay, uint64_t response_length, uint64_t original_simulate_duration)
+{
+    double simulate_frequency = _simulate_frequency;
+    uint64_t highest_delay_samples = _opt.highest_delay_samples(delay);
+    if ((_opt.optimize_simulation_rate) ||
+        ((highest_delay_samples + response_length + _opt.flush_count) > original_simulate_duration))
+    {
+        simulate_frequency = get_max_possible_frequency(highest_delay_samples, response_length);
+    }
+    return simulate_frequency;
+}
+
+// calculate max possible simulate frequency
+double
+Responder::get_max_possible_frequency(uint64_t highest_delay_samples, uint64_t response_length) // only 2 args, others are all const!
+{
+    return std::floor((double)_opt.sample_rate / (double)(highest_delay_samples + response_length + _opt.flush_count + _opt.optimize_padding));
+}
+
+// Check if conditions to finish test are met.
+bool
+Responder::test_finished(size_t success_count)
+{
+    if (success_count == _opt.end_test_after_success_count)
+    {
+        print_msg( (boost::format("\nTest complete after %d successes.") % success_count).str() );
+        return true;
+    }
+    if (((_opt.delay_min <= _opt.delay_max) && (_delay >= _opt.delay_max)) ||
+        ((_opt.delay_min > _opt.delay_max) && (_delay <= _opt.delay_max)))
+    {
+        print_msg("\nTest complete.");
+        return true;
+    }
+    return false;
+}
+
+// handle keyboard input in interactive mode
+bool
+Responder::handle_interactive_control()
+{
+    std::string msg = "";
+    int c = wgetch(_window);
+    if (c > -1)
+    {
+        // UP/DOWN Keys control delay step width
+        if ((c == KEY_DOWN) || (c == KEY_UP))
+        {
+            double dMag = log10(_delay_step);
+            int iMag = (int)floor(dMag);
+            iMag += ((c == KEY_UP) ? 1 : -1);
+            _delay_step = pow(10.0, iMag);
+            msg += (boost::format("Step: %f") % _delay_step ).str();
+        }
+        // LEFT/RIGHT Keys control absolute delay length
+        if ((c == KEY_LEFT) || (c == KEY_RIGHT))
+        {
+            double step = _delay_step * ((c == KEY_RIGHT) ? 1 : -1);
+            if ((_delay + step) >= 0.0)
+                _delay += step;
+            msg += (boost::format("Delay: %f") % _delay).str();
+        }
+        // Enable/disable fixed delay <--> best effort mode
+        if (c == 'd')
+        {
+            _no_delay = !_no_delay;
+
+            if (_no_delay)
+                msg += "Delay disabled (best effort)";
+            else
+                msg += (boost::format("Delay: %f") % _delay).str();
+        }
+        else if (c == 'q') // exit test
+        {
+            return true; // signal test to stop
+        }
+        else if (c == 'l') // change late burst policy
+        {
+            _allow_late_bursts = !_allow_late_bursts;
+
+            if (_allow_late_bursts)
+                msg += "Allowing late bursts";
+            else
+                msg += "Dropping late bursts";
+        }
+        print_interactive_msg(msg);
+    }
+    return false; // signal test to continue with updated values
+}
+
+// print updated interactive control value
+void
+Responder::print_interactive_msg(std::string msg)
+{
+    if(msg != "")
+    {
+        // move cursor back to beginning of line
+        int y, x;
+        getyx(_window, y, x);
+        if (x > 0)
+        {
+            move(y, 0);
+            clrtoeol();
+        }
+        print_msg(msg);
+        move(y, 0);
+    }
+}
+
+// check if transmit burst is late
+bool
+Responder::tx_burst_is_late()
+{
+    uhd::async_metadata_t async_md;
+    if (_usrp->get_device()->recv_async_msg(async_md, 0))
+    {
+        if (async_md.event_code == uhd::async_metadata_t::EVENT_CODE_TIME_ERROR)
+        {
+            return true;
+        }
+    }
+    return false;
+}
+
+void
+Responder::create_ncurses_window()
+{
+    _window = initscr();
+    cbreak();       // Unbuffered key input, except for signals (cf. 'raw')
+    noecho();
+    nonl();
+    intrflush(_window, FALSE);
+    keypad(_window, TRUE);   // Enable function keys, arrow keys, ...
+    nodelay(_window, 0);
+    timeout(0);
+}
+
+// print all fixed test parameters
+void
+Responder::print_init_test_status()
+{
+    // Clear the window and write new data.
+    erase();
+    refresh();
+    print_test_title();
+    print_usrp_status();
+    print_test_parameters();
+
+    std::string msg("");
+    if (_opt.test_iterations > 0)
+        msg.append("Press Ctrl + C to abort test");
+    else
+        msg.append("Press Q stop streaming");
+    msg.append("\n");
+    print_msg(msg);
+
+    _y_delay_pos = -1; // reset delay display line pos.
+    _x_delay_pos = -1;
+}
+
+// in interactive mode with second usrp sending bursts. calibrate trigger level
+float
+Responder::calibrate_usrp_for_test_run()
+{
+    bool calibration_finished = false;
+    float threshold = 0.0f;
+    double ave_high = 0, ave_low = 0;
+    int ave_high_count = 0, ave_low_count = 0;
+    bool level_calibration_stage_2 = false; // 1. stage = rough calibration ; 2. stage = fine calibration
+
+    std::vector<std::complex<float> > buff(_opt.samps_per_buff);
+    while (not s_stop_signal_called && !calibration_finished && _return_code == RETCODE_OK)
+    {
+        uhd::rx_metadata_t rx_md;
+        size_t num_rx_samps = _rx_stream->recv(&buff.front(), buff.size(), rx_md, _opt.timeout);
+
+        // handle errors
+        if(handle_rx_errors(rx_md.error_code, num_rx_samps) )
+        {
+            break;
+        }
+
+        // Wait for USRP for DC offset calibration
+        if (_dc_offset_countdown > 0)
+        {
+            _dc_offset_countdown -= (int64_t)num_rx_samps;
+            if (_dc_offset_countdown > 0)
+                continue;
+            set_usrp_rx_dc_offset(_usrp, false);
+            print_msg("DC offset calibration complete");
+        }
+
+        // Wait for certain time to minimize POWER UP effects
+        if (_init_delay_count > 0)
+        {
+            _init_delay_count -= (int64_t)num_rx_samps;
+            if (_init_delay_count > 0)
+                continue;
+            print_msg("Initial settling period elapsed");
+        }
+
+        ////////////////////////////////////////////////////////////
+        // detect falling edges and calibrate detection values
+        if (_level_calibration_countdown > 0)
+        {
+            if (level_calibration_stage_2 == false)
+            {
+                float average = 0.0f;
+                for (size_t n = 0; n < num_rx_samps; n++)
+                    average += buff[n].real() * _opt.invert;
+                average /= (float)num_rx_samps;
+
+                if (ave_low_count == 0)
+                {
+                    ave_low = average;
+                    ++ave_low_count;
+                }
+                else if (average < ave_low)
+                {
+                    ave_low = average;
+                    ++ave_low_count;
+                }
+
+                if (ave_high_count == 0)
+                {
+                    ave_high = average;
+                    ++ave_high_count;
+                }
+                else if (average > ave_high)
+                {
+                    ave_high = average;
+                    ++ave_high_count;
+                }
+            }
+            else {
+                for (size_t n = 0; n < num_rx_samps; n++)
+                {
+                    float f = buff[n].real() * _opt.invert;
+                    if (f >= threshold)
+                    {
+                        ave_high += f;
+                        ave_high_count++;
+                    }
+                    else
+                    {
+                        ave_low += f;
+                        ave_low_count++;
+                    }
+                }
+            }
+
+            _level_calibration_countdown -= (int64_t)num_rx_samps;
+
+            if (_level_calibration_countdown <= 0)
+            {
+                if (level_calibration_stage_2 == false)
+                {
+                    level_calibration_stage_2 = true;
+                    _level_calibration_countdown = _opt.level_calibration_count();
+                    threshold = ave_low + ((ave_high - ave_low) / 2.0);
+                    print_msg( (boost::format("Phase #1: Ave low: %.3f (#%d), ave high: %.3f (#%d), threshold: %.3f") % ave_low % ave_low_count % ave_high % ave_high_count % threshold).str() );
+                    ave_low_count = ave_high_count = 0;
+                    ave_low = ave_high = 0.0f;
+                    continue;
+                }
+                else
+                {
+                    ave_low /= (double)ave_low_count;
+                    ave_high /= (double)ave_high_count;
+                    threshold = ave_low + ((ave_high - ave_low) * _opt.trigger_level);
+                    print_msg( (boost::format("Phase #2: Ave low: %.3f (#%d), ave high: %.3f (#%d), threshold: %.3f\n") % ave_low % ave_low_count % ave_high % ave_high_count % threshold).str() );
+
+                    _stream_cmd.stream_mode = uhd::stream_cmd_t::STREAM_MODE_STOP_CONTINUOUS;
+                    _stream_cmd.stream_now = true;
+                    _usrp->issue_stream_cmd(_stream_cmd);
+
+                    double diff = abs(ave_high - ave_low);
+                    if (diff < _opt.pulse_detection_threshold)
+                    {
+                        _return_code = RETCODE_BAD_ARGS;
+                        print_error_msg( (boost::format("Did not detect any pulses (difference %.6f < detection threshold %.6f)") % diff % _opt.pulse_detection_threshold).str() );
+                        break;
+                    }
+
+                    _stream_cmd.stream_mode = uhd::stream_cmd_t::STREAM_MODE_START_CONTINUOUS;
+                    _stream_cmd.stream_now = true;
+                    _usrp->issue_stream_cmd(_stream_cmd);
+                }
+            }
+            else
+                continue;
+        } // calibration finished
+        calibration_finished = true;
+    }
+    return threshold;
+}
+
+// try to stop USRP properly after tests
+void
+Responder::stop_usrp_stream()
+{
+    try
+    {
+        if (_usrp)
+        {
+            _stream_cmd.stream_mode = uhd::stream_cmd_t::STREAM_MODE_STOP_CONTINUOUS;
+            _stream_cmd.stream_now = true;
+            _usrp->issue_stream_cmd(_stream_cmd);
+        }
+    }
+    catch (...)
+    {
+        //
+    }
+}
+
+// after each delay length update test parameters and print them
+void
+Responder::update_and_print_parameters(const STATS& statsPrev, const double delay)
+{
+    uint64_t old_simulate_duration = _simulate_duration;
+    _simulate_frequency = get_simulate_frequency(delay, _response_length, _original_simulate_duration);
+    _simulate_duration = _opt.simulate_duration(_simulate_frequency);
+    print_formatted_delay_line(_simulate_duration, old_simulate_duration, statsPrev, delay, _simulate_frequency);
+    _timeout_burst_count = 0;
+    _timeout_eob_count = 0;
+}
+
+// detect or simulate burst level.
+bool
+Responder::get_new_state(uint64_t total_samps, uint64_t simulate_duration, float val, float threshold)
+{
+    bool new_state = false;
+    if (simulate_duration > 0) // only simulated input bursts!
+        new_state = (((total_samps) % simulate_duration) == 0);
+    else
+        new_state = (val >= threshold);    // TODO: Just measure difference in fall
+    return new_state;
+}
+
+// detect a pulse, respond to it and count number of pulses.
+// statsCurrent holds parameters.
+uint64_t
+Responder::detect_respond_pulse_count(STATS &statsCurrent, std::vector<std::complex<float> > &buff, uint64_t trigger_count, size_t num_rx_samps, float threshold, uhd::time_spec_t rx_time)
+{
+    // buff, threshold
+    bool input_state = false;
+    for (size_t n = 0; n < num_rx_samps; n++)
+    {
+        float f = buff[n].real() * _opt.invert;
+
+        bool new_state = get_new_state(_num_total_samps + n, _simulate_duration, f, threshold);
+
+        if ((new_state == false) && (input_state)) // == falling_edge
+        {
+            trigger_count++;
+            statsCurrent.detected++;
+
+            if ((_opt.test_iterations > 0)
+                    && (_opt.skip_iterations > 0)
+                    && (statsCurrent.skipped == 0)
+                    && (_opt.skip_iterations == statsCurrent.detected))
+            {
+                memset(&statsCurrent, 0x00, sizeof(STATS));
+                statsCurrent.delay = _delay;
+                statsCurrent.detected = 1;
+                statsCurrent.skipped = _opt.skip_iterations;
+
+                trigger_count = 1;
+            }
+
+            if( !send_tx_burst(rx_time, n) )
+            {
+                statsCurrent.missed++;
+            }
+
+            if(tx_burst_is_late() )
+            {
+                statsCurrent.missed++;
+            }
+        }
+
+        input_state = new_state;
+    }
+    return trigger_count;
+}
+
+// this is the actual "work" function. All the fun happens here
+void
+Responder::run_test(float threshold)
+{
+    STATS statsCurrent; //, statsPrev;
+    memset(&statsCurrent, 0x00, sizeof(STATS));
+    if (_opt.test_iterations > 0)
+    {
+        update_and_print_parameters(statsCurrent, _delay);
+        statsCurrent.delay = _opt.delay_min;
+    }
+    ///////////////////////////////////////////////////////////////////////////
+    uint64_t trigger_count = 0;
+    size_t success_count = 0;
+    uint64_t num_total_samps_test = 0;
+
+    std::vector<std::complex<float> > buff(_opt.samps_per_buff);
+    while (not s_stop_signal_called && _return_code == RETCODE_OK)
+    {
+        // get samples from rx stream.
+        uhd::rx_metadata_t rx_md;
+        size_t num_rx_samps = _rx_stream->recv(&buff.front(), buff.size(), rx_md, _opt.timeout);
+        // handle errors
+        if(handle_rx_errors(rx_md.error_code, num_rx_samps) )
+        {
+            break;
+        }
+        // detect falling edges, send respond pulse and check if response could be sent in time
+        trigger_count = detect_respond_pulse_count(statsCurrent, buff, trigger_count, num_rx_samps, threshold, rx_md.time_spec);
+
+        // increase counters for single test and overall test samples count.
+        _num_total_samps += num_rx_samps;
+        num_total_samps_test += num_rx_samps;
+
+        // control section for interactive mode
+        if (_opt.test_iterations == 0) // == "interactive'
+        {
+            if(handle_interactive_control() )
+                break;
+        }
+
+        // control section for test mode
+        if (_opt.test_iterations > 0) // == test mode / batch-mode
+        {
+            int step_return = test_step_finished(trigger_count, num_total_samps_test, statsCurrent, success_count);
+            if(step_return == -2) // == test is finished with all desired delay steps
+                break;
+            else if(step_return == -1) // just continue test
+                continue;
+            else // test with one delay is finished
+            {
+                success_count = (size_t) step_return;
+                trigger_count = 0;
+                num_total_samps_test = 0;
+                memset(&statsCurrent, 0x00, sizeof(STATS)); // reset current stats for next test iteration
+                statsCurrent.delay = _delay;
+            }
+        } // end test mode control section
+    }// exit outer loop after stop signal is called, test is finished or other break condition is met
+
+    if (s_stop_signal_called)
+        _return_code = RETCODE_MANUAL_ABORT;
+}
+
+// check if test with one specific delay is finished
+int
+Responder::test_step_finished(uint64_t trigger_count, uint64_t num_total_samps_test, STATS statsCurrent, size_t success_count)
+{
+    if ( ((_opt.test_iterations_is_sample_count == false) && (trigger_count >= _opt.test_iterations)) ||
+         ((_opt.test_iterations_is_sample_count) && (num_total_samps_test > _opt.test_iterations)) )
+    {
+        add_stats_to_results(statsCurrent, _delay);
+
+        if (statsCurrent.missed == 0) // == NO late bursts
+            ++success_count;
+        else
+            success_count = 0;
+
+        if(test_finished(success_count) )
+            return -2; // test is completely finished
+
+        _delay += _delay_step; // increase delay by one step
+
+        update_and_print_parameters(statsCurrent, _delay);
+        return success_count; // test is finished for one delay step
+    }
+    return -1; // == continue test
+}
+
+// save test results
+void
+Responder::add_stats_to_results(STATS statsCurrent, double delay)
+{
+    _max_success = max(_max_success, (statsCurrent.detected - statsCurrent.missed)); // > 0 --> save results
+    uint64_t key = (uint64_t)(delay * 1e6);
+    _mapStats[key] = statsCurrent;
+}
+
+// run tests and handle errors
+int
+Responder::run()
+{
+    if (_return_code != RETCODE_OK)
+        return _return_code;
+    if (_opt.pause)
+        print_msg_and_wait("Press any key to begin...");
+    time( &_dbginfo.start_time_test );
+
+    // Put some info about the test on the console
+    print_init_test_status();
+    try {
+        //setup streaming
+        _stream_cmd.stream_mode = uhd::stream_cmd_t::STREAM_MODE_START_CONTINUOUS;
+        _stream_cmd.stream_now = true;
+        _usrp->issue_stream_cmd(_stream_cmd);
+
+        if( !_opt.batch_mode ){
+            float threshold = calibrate_usrp_for_test_run();
+            if (_return_code != RETCODE_OK)
+            {
+                return _return_code;
+            }
+            run_test(threshold);
+        }
+        else
+        {
+            run_test();
+        }
+    }
+    catch (const std::runtime_error& e)
+    {
+        print_msg(e.what() );
+        _return_code = RETCODE_RUNTIME_ERROR;
+    }
+    catch (...)
+    {
+        print_msg("Unhandled exception");
+        _return_code = RETCODE_UNKNOWN_EXCEPTION;
+    }
+
+    stop_usrp_stream();
+    time( &_dbginfo.end_time_test );
+    return (_return_code < 0 ? _return_code : _overruns);
+}
+
+/*
+ *  Following functions are intended to be used by destructor only!
+ */
+
+// This method should print statistics after ncurses endwin.
+void
+Responder::print_final_statistics()
+{
+    cout << boost::format("Received %ld samples during test run") % _num_total_samps;
+    if (_overruns > 0)
+        cout << boost::format(" (%d overruns)") % _overruns;
+    cout << endl;
+}
+
+// safe test results to a log file if enabled
+void
+Responder::write_log_file()
+{
+    try
+    {
+        if(_opt.log_file){
+            std::map<std::string, std::string> hw_info = get_hw_info();
+            ofstream logs(_stats_log_filename.c_str());
+
+            logs << boost::format("title=%s") % _opt.test_title << endl;
+            logs << boost::format("device=%s") %  _usrp->get_mboard_name() << endl;
+            logs << boost::format("device_args=%s") % _opt.device_args << endl;
+            logs << boost::format("type=%s") %  hw_info["type"] << endl;
+            if (hw_info.size() > 0)
+            {
+                logs << boost::format("usrp_addr=%s") %  hw_info["usrp_addr"] << endl;
+                logs << boost::format("usrp_name=%s") %  hw_info["name"] << endl;
+                logs << boost::format("serial=%s") %  hw_info["serial"] << endl;
+                logs << boost::format("host_interface=%s") %  hw_info["interface"] << endl;
+                logs << boost::format("host_addr=%s") %  hw_info["host_addr"] << endl;
+                logs << boost::format("host_mac=%s") %  hw_info["mac"] << endl;
+                logs << boost::format("host_vendor=%s (id=%s)") %  hw_info["vendor"] % hw_info["vendor_id"] << endl;
+                logs << boost::format("host_device=%s (id=%s)") %  hw_info["device"] % hw_info["device_id"] << endl;
+            }
+            logs << boost::format("sample_rate=%f") % _opt.sample_rate << endl;
+            logs << boost::format("samps_per_buff=%i") % _opt.samps_per_buff << endl;
+            logs << boost::format("samps_per_packet=%i") % _samps_per_packet << endl;
+            logs << boost::format("delay_min=%f") % _opt.delay_min << endl;
+            logs << boost::format("delay_max=%f") % _opt.delay_max << endl;
+            logs << boost::format("delay_step=%f") % _delay_step << endl;
+            logs << boost::format("delay=%f") % _delay << endl;
+            logs << boost::format("init_delay=%f") % _opt.init_delay << endl;
+            logs << boost::format("response_duration=%f") % _opt.response_duration << endl;
+            logs << boost::format("response_length=%ld") % _response_length << endl;
+            logs << boost::format("timeout=%f") % _opt.timeout << endl;
+            logs << boost::format("timeout_burst_count=%ld") % _timeout_burst_count << endl;
+            logs << boost::format("timeout_eob_count=%f") % _timeout_eob_count << endl;
+            logs << boost::format("allow_late_bursts=%s") % (_allow_late_bursts ? "yes" : "no") << endl;
+            logs << boost::format("skip_eob=%s") % (_opt.skip_eob ? "yes" : "no") << endl;
+            logs << boost::format("combine_eob=%s") % (_opt.combine_eob ? "yes" : "no") << endl;
+            logs << boost::format("skip_send=%s") % (_opt.skip_send ? "yes" : "no") << endl;
+            logs << boost::format("no_delay=%s") % (_no_delay ? "yes" : "no") << endl;
+            logs << boost::format("simulate_frequency=%f") % _simulate_frequency << endl;
+            logs << boost::format("simulate_duration=%ld") % _simulate_duration << endl;
+            logs << boost::format("original_simulate_duration=%ld") % _original_simulate_duration << endl;
+            logs << boost::format("realtime=%s") % (_opt.realtime ? "yes" : "no") << endl;
+            logs << boost::format("rt_priority=%f") % _opt.rt_priority << endl;
+            logs << boost::format("test_iterations=%ld") % _opt.test_iterations << endl;
+            logs << boost::format("end_test_after_success_count=%i") % _opt.end_test_after_success_count << endl;
+            logs << boost::format("skip_iterations=%i") % _opt.skip_iterations << endl;
+            logs << boost::format("overruns=%i") % _overruns << endl;
+            logs << boost::format("num_total_samps=%ld") % _num_total_samps << endl;
+            logs << boost::format("return_code=%i\t(%s)") % _return_code % enum2str(_return_code) << endl;
+            logs << endl;
+
+            write_debug_info(logs);
+
+            if(uhd_error_msgs.length() > 0)
+            {
+                logs << endl << "%% UHD ERROR MESSAGES %%" << endl;
+                logs << uhd_error_msgs;
+            }
+        }
+    }
+    catch(...)
+    {
+        cerr << "Failed to write log file to: " << _stats_log_filename << endl;
+    }
+}
+
+// write debug info to log file
+void
+Responder::write_debug_info(ofstream& logs)
+{
+    logs << endl << "%% DEBUG INFO %%" << endl;
+
+    logs << boost::format("dbg_time_start=%s") % get_gmtime_string(_dbginfo.start_time) << endl;
+    logs << boost::format("dbg_time_end=%s") % get_gmtime_string(_dbginfo.end_time) << endl;
+    logs << boost::format("dbg_time_duration=%d") % difftime( _dbginfo.end_time, _dbginfo.start_time ) << endl;
+    logs << boost::format("dbg_time_start_test=%s") % get_gmtime_string(_dbginfo.start_time_test) << endl;
+    logs << boost::format("dbg_time_end_test=%s") % get_gmtime_string(_dbginfo.end_time_test) << endl;
+    logs << boost::format("dbg_time_duration_test=%d") % difftime( _dbginfo.end_time_test, _dbginfo.start_time_test ) << endl;
+    logs << boost::format("dbg_time_first_send_timeout=%s") % get_gmtime_string(_dbginfo.first_send_timeout) << endl;
+}
+
+// convert a time string to desired format
+std::string
+Responder::get_gmtime_string(time_t time)
+{
+    tm* ftm;
+    ftm = gmtime( &time );
+    std::string strtime;
+    strtime.append( (boost::format("%i") % (ftm->tm_year+1900) ).str() );
+    strtime.append( (boost::format("-%02i") % ftm->tm_mon).str() );
+    strtime.append( (boost::format("-%02i") % ftm->tm_mday).str() );
+    strtime.append( (boost::format("-%02i") % ((ftm->tm_hour)) ).str() );
+    strtime.append( (boost::format(":%02i") % ftm->tm_min).str() );
+    strtime.append( (boost::format(":%02i") % ftm->tm_sec).str() );
+
+    return strtime;
+}
+
+// read hardware info from file if available to include it in log file
+std::map<std::string, std::string>
+Responder::get_hw_info()
+{
+    std::map<std::string, std::string> result;
+    std::vector<std::map<std::string,std::string> > eths = read_eth_info();
+    if(eths.empty()){
+        return result;
+    }
+    uhd::device_addr_t usrp_info = get_usrp_info();
+    std::string uaddr = get_ip_subnet_addr(usrp_info["addr"]);
+
+    for(unsigned int i = 0 ; i < eths.size() ; i++ )
+    {
+        if(get_ip_subnet_addr(eths[i]["addr"]) == uaddr)
+        {
+            result["type"] = usrp_info["type"];
+            result["usrp_addr"] = usrp_info["addr"];
+            result["name"] = usrp_info["name"];
+            result["serial"] = usrp_info["serial"];
+            result["interface"] = eths[i]["interface"];
+            result["host_addr"] = eths[i]["addr"];
+            result["mac"] = eths[i]["mac"];
+            result["vendor"] = eths[i]["vendor"];
+            result["vendor_id"] = eths[i]["vendor_id"];
+            result["device"] = eths[i]["device"];
+            result["device_id"] = eths[i]["device_id"];
+            break; // Use first item found. Imitate device discovery.
+        }
+    }
+
+    return result;
+}
+
+// subnet used to identify used network interface
+std::string
+Responder::get_ip_subnet_addr(std::string ip)
+{
+    return ip.substr(0, ip.rfind(".") + 1);
+}
+
+// get network interface info from file (should include all available interfaces)
+std::vector<std::map<std::string,std::string> >
+Responder::read_eth_info()
+{
+    const std::string eth_file(_eth_file);
+
+    std::vector<std::map<std::string,std::string> > eths;
+    try
+    {
+        ifstream eth_info(eth_file.c_str());
+        if(!eth_info.is_open()){
+            return eths;
+        }
+        const int len = 256;
+        char cline[len];
+        for(; !eth_info.eof() ;)
+        {
+            eth_info.getline(cline, len);
+            std::string line(cline);
+            if(line.find("## ETH Interface") != std::string::npos)
+            {
+                eth_info.getline(cline, len);
+                std::string eth(cline);
+//                cout << "interface=" << eth << endl;
+                std::map<std::string,std::string> iface;
+                iface["interface"] = eth;
+                eths.push_back(iface);
+            }
+            const std::string ipstr("\tip ");
+            if(line.find(ipstr) != std::string::npos)
+            {
+                std::string ip( line.replace(line.begin(), line.begin()+ipstr.length(), "") );
+//                cout << "ip=" << ip << endl;
+                eths.back()["addr"] = ip;
+            }
+            const std::string macstr("\tmac ");
+            if(line.find(macstr) != std::string::npos)
+            {
+                std::string mac( line.replace(line.begin(), line.begin()+macstr.length(), "") );
+//                cout << "mac=" << mac << endl;
+                eths.back()["mac"] = mac;
+            }
+            const std::string vstr("\t\tvendor ");
+            if(line.find(vstr) != std::string::npos)
+            {
+                std::string vendor( line.replace(line.begin(), line.begin()+vstr.length(), "") );
+                std::string vid( vendor.substr(0,6) );
+                vendor.replace(0, 7, "");
+//                cout << "id=" << vid << endl;
+//                cout << "vendor=" << vendor << endl;
+                eths.back()["vendor"] = vendor;
+                eths.back()["vendor_id"] = vid;
+            }
+            const std::string dstr("\t\tdevice ");
+            if(line.find(dstr) != std::string::npos)
+            {
+                std::string device( line.replace(line.begin(), line.begin()+dstr.length(), "") );
+                std::string did( device.substr(0,6) );
+                device.replace(0, 7, "");
+//                cout << "id=" << did << endl;
+//                cout << "device=" << device << endl;
+                eths.back()["device"] = device;
+                eths.back()["device_id"] = did;
+            }
+        }
+
+    }
+    catch(...)
+    {
+        // nothing in yet
+    }
+    return eths;
+}
+
+// get info on used USRP
+uhd::device_addr_t
+Responder::get_usrp_info()
+{
+    uhd::device_addrs_t device_addrs = uhd::device::find(_opt.device_args);
+    uhd::device_addr_t device_addr = device_addrs[0];
+    return device_addr;
+}
+
+// write statistics of test run to file
+void
+Responder::write_statistics_to_file(StatsMap mapStats)
+{
+    try
+    {
+        ofstream results(_stats_filename.c_str());
+
+        for (StatsMap::iterator it = mapStats.begin(); it != mapStats.end(); ++it)
+        {
+            STATS& stats = it->second;
+            double d = 0.0;
+            if (stats.detected > 0)
+                d = 1.0 - ((double)stats.missed / (double)stats.detected);
+            cout << "\t" << setprecision(6) << stats.delay << "\t\t" << setprecision(6) << d << endl;
+
+            results << (stats.delay * _opt.time_mul) << " " << setprecision(6) << d << endl;
+        }
+        cout << "Statistics written to: " << _stats_filename << endl;
+
+    }
+    catch (...)
+    {
+        cout << "Failed to write statistics to: " << _stats_filename << endl;
+    }
+}
+
+// make sure write files is intended
+void
+Responder::safe_write_statistics_to_file(StatsMap mapStats, uint64_t max_success, int return_code)
+{
+    if ((_opt.test_iterations > 0) && (_stats_filename.empty() == false) && (_opt.no_stats_file == false))
+    {
+        if (mapStats.empty())
+        {
+            cout << "No results to output (not writing statistics file)" << endl;
+        }
+        else if ((max_success == 0) && (return_code == RETCODE_MANUAL_ABORT))
+        {
+            cout << "Aborted before a single successful timed burst (not writing statistics file)" << endl;
+        }
+        else
+        {
+            write_statistics_to_file(mapStats);
+        }
+        write_log_file();
+    }
+}
+
+// destructor, handle proper test shutdown
+Responder::~Responder()
+{
+    endwin();
+    if(_pResponse != NULL){
+        delete[] _pResponse;
+    }
+    time( &_dbginfo.end_time );
+    // Print final info about test run
+    print_final_statistics();
+    // check conditions and write statistics to file
+    safe_write_statistics_to_file(_mapStats, _max_success, _return_code);
+    cout << "program exited with code = " << enum2str(_return_code) << endl;
+}
+
+// make test output more helpful
+std::string
+Responder::enum2str(int return_code)
+{
+    switch(return_code)
+    {
+        case RETCODE_OK: return "OK";
+        case RETCODE_BAD_ARGS: return "BAD_ARGS";
+        case RETCODE_RUNTIME_ERROR: return "RUNTIME_ERROR";
+        case RETCODE_UNKNOWN_EXCEPTION: return "UNKNOWN_EXCEPTION";
+        case RETCODE_RECEIVE_TIMEOUT: return "RECEIVE_TIMEOUT";
+        case RETCODE_RECEIVE_FAILED: return "RECEIVE_FAILED";
+        case RETCODE_MANUAL_ABORT: return "MANUAL_ABORT";
+        case RETCODE_BAD_PACKET: return "BAD_PACKET";
+        case RETCODE_OVERFLOW: return "OVERFLOW";
+    }
+    return "UNKNOWN";
+}
+
diff --git a/host/utils/latency/pci_hwdata.py b/host/utils/latency/pci_hwdata.py
new file mode 100755
index 000000000..1ab5056d8
--- /dev/null
+++ b/host/utils/latency/pci_hwdata.py
@@ -0,0 +1,231 @@
+#!/usr/bin/env python
+#
+# Copyright 2013 Ettus Research LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+# This script exists for convenience. Run it once to get your hardware info.
+# Save the created files in the directory where you execute your tests.
+
+import os
+import netifaces
+from netifaces import AF_INET
+from optparse import OptionParser
+
+try:
+    from gnuradio import uhd
+except:
+    print "Can't gather USRP info! gr-uhd not found."
+
+# If other paths for this file are known, add them to this list.
+pci_hwdata_paths = ["/usr/share/hwdata/pci.ids", "/usr/share/misc/pci.ids"]
+
+
+def main():
+    # Just get the file name, where results are supposed to be stored.
+    usage = "%prog: [savefile]"
+    parser = OptionParser(usage=usage)
+    parser.add_option("", "--file", type="string", help="file to save results. [default=%default]", default="usrps_info.txt")
+    (options, args) = parser.parse_args()
+
+    eths_ids = get_eths_with_ids()
+    for eth in eths_ids:
+        print eth
+    save_eth_ids_info_to_file(eths_ids, options.file)
+
+    usrps = []
+    try:
+        usrps = get_usrps_with_device_info()
+        for usrp in usrps:
+            print usrp
+    except Exception as e:
+        print "Can't gather USRP info!"
+        print e.message,
+    try:
+        save_usrp_info(usrps, options.file)
+    except Exception as e:
+        print "Can't save USRP info!"
+        print e.message
+
+
+def get_eths_with_ids():
+    eths = get_eth_interface_with_address()
+    eths_ids = []
+    for eth in eths:
+        vd_id = get_vendor_device_id(eth['interface'])
+        vd_string = get_pci_string_from_id(vd_id)
+        vendor = {'id': vd_id['vendor'], 'name': vd_string['vendor']}
+        device = {'id': vd_id['device'], 'name': vd_string['device']}
+        phys = {'vendor': vendor, 'device': device}
+        eth['physical'] = phys
+        eths_ids.append(eth)
+    return eths_ids
+
+
+def get_eth_interface_with_address():
+    eths = []
+    for iface in netifaces.interfaces():
+        if iface.find("eth") == 0:
+            ips = netifaces.ifaddresses(iface).get(netifaces.AF_INET)
+            macs = netifaces.ifaddresses(iface).get(netifaces.AF_PACKET)
+            if ips and macs:
+                for ip, mac in zip(ips, macs):
+                    eths.append({'interface': iface, 'addr': ip['addr'], 'mac': mac['addr']})
+    if not eths:
+        print "Can't gather Ethernet info. Check if a network based USRP is connected to host and responding to \'uhd_find_devices\'"
+    return eths
+
+
+def get_usrps_with_device_info():
+    devs = uhd.find_devices()
+    devs_infos = []
+    eths_ids = get_eths_with_ids()
+    for dev in devs:
+        if dev['addr']:
+            ridx = dev['addr'].rfind('.')
+            net = dev['addr'][0:ridx + 1]
+            for eth in eths_ids:
+                if eth['addr'].startswith(net):
+                    dev_info = {'type': dev['type'], 'addr': dev['addr'], 'name': dev['name'], 'serial': dev['serial'],
+                                'host': eth}
+                    devs_infos.append(dev_info)
+
+    return devs_infos
+
+
+def save_usrp_info(usrps, filename):
+    if not usrps:
+        print "No USRP data available. Not saving any data."
+        return
+    with open(filename, 'w') as f:
+        if f.closed:
+            print "Warning: Couldn't open", filename, "to save results."
+        f.write("#\n")
+        f.write("#\n")
+        f.write("# This file contains gathered information about USRPs connected to the host\n")
+        f.write("#\n")
+        count = 0
+        for usrp in usrps:
+            f.write("\n## USRP Device " + str(count) + "\n")
+            f.write("type:    " + usrp['type'] + "\n")
+            f.write("address: " + usrp['addr'] + "\n")
+            f.write("name:    " + usrp['name'] + "\n")
+            f.write("serial:  " + usrp['serial'] + "\n")
+            f.write("host\n")
+            f.write("\t" + usrp['host']['interface'] + "\n")
+            f.write("\t" + usrp['host']['addr'] + "\n")
+            f.write("\t" + usrp['host']['mac'] + "\n")
+            f.write("\t\tphysical port info\n")
+            f.write("\t\t\t" + usrp['host']['physical']['vendor']['id'] + " " + usrp['host']['physical']['vendor'][
+                'name'] + "\n")
+            f.write("\t\t\t" + usrp['host']['physical']['device']['id'] + " " + usrp['host']['physical']['device'][
+                'name'] + "\n")
+            f.write("## End USRP Device " + str(count) + "\n\n")
+            count += 1
+
+
+def save_eth_ids_info_to_file(eths, filename):
+    with open(filename, 'w') as f:
+        if f.closed:
+            print "Warning: Couldn't open", filename, "to save results."
+        f.write("#\n")
+        f.write("#\n")
+        f.write("# This file contains infos about the available eth interfaces\n")
+        f.write("#\n")
+        #print eths
+        count = 0
+        for eth in eths:
+            f.write("\n## ETH Interface " + str(count) + "\n")
+            f.write(eth['interface'] + "\n")
+            f.write("\tip " + eth['addr'] + "\n")
+            f.write("\tmac " + eth['mac'] + "\n")
+            f.write("phys_port_info\n")
+            f.write("\t\tvendor " + eth['physical']['vendor']['id'] + " " + eth['physical']['vendor']['name'] + "\n")
+            f.write("\t\tdevice " + eth['physical']['device']['id'] + " " + eth['physical']['device']['name'] + "\n")
+            f.write("## End ETH Interface " + str(count) + "\n\n")
+            count += 1
+
+
+def get_vendor_device_id(eth):
+    path = "/sys/class/net/" + eth + "/device/"
+    vendor_id = get_id(path + "vendor")
+    device_id = get_id(path + "device")
+    return {'vendor': vendor_id, 'device': device_id}
+
+
+def get_id(path):
+    gid = 0
+    with open(path, 'r') as f:
+        if f.closed:
+            print "Warning: Couldn't open", path, "to gather device information."
+        data = f.read()
+        gid = data[0:-1]
+    return gid
+
+
+def get_pci_string_from_id(vid):
+    vendors = get_vendors()
+    vendor_id = vid['vendor'][2:]
+    device_id = vid['device'][2:]
+    vendor = vendors[vendor_id]['vendor']
+    device = vendors[vendor_id]['devices'][device_id]
+
+    return {'vendor': vendor, 'device': device}
+
+
+_g_vendors = {}
+
+
+def get_vendors():
+    global _g_vendors
+    if len(_g_vendors) > 0:
+        return _g_vendors
+
+    path = ""
+    vendors = {}
+    # Check for possible locations of pci.ids on the system.
+    for pci_path in pci_hwdata_paths:
+        if os.path.isfile(pci_path):
+            path = pci_path
+            break
+    if path == "":
+        print "Couldn't find pci.ids file. Vendor data not available!"
+        return vendors
+
+    vendor_id = ''
+    with open(path, 'r') as f:
+        if f.closed:
+            print "Warning: Couldn't open", path, ". Vendor data not available."
+        for line in f.readlines():
+            if line.startswith("#"):
+                if line.startswith("# List of known device classes"):
+                    break
+                else:
+                    continue
+            l = line.split()
+            if len(l) > 1 and not line.startswith("\t"):
+                vendor_id = l[0]
+                vendor = " ".join(l[1:])
+                vendors[vendor_id] = {'vendor': vendor, 'devices': {}}
+            if len(l) > 1 and line.startswith("\t") and not line.startswith("\t\t"):
+                device_id = l[0]
+                device = " ".join(l[1:])
+                vendors[vendor_id]['devices'][device_id] = device
+    _g_vendors = vendors
+    return vendors
+
+
+if __name__ == '__main__':
+    main()
diff --git a/host/utils/latency/responder.cpp b/host/utils/latency/responder.cpp
new file mode 100644
index 000000000..938102fb0
--- /dev/null
+++ b/host/utils/latency/responder.cpp
@@ -0,0 +1,133 @@
+//
+// Copyright 2010-2012 Ettus Research LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+
+#include <boost/program_options.hpp>
+#include <uhd/utils/safe_main.hpp>
+#include <Responder.hpp>
+
+namespace po = boost::program_options;
+
+static Responder::Options prog;
+
+po::options_description
+get_program_options_description()
+{
+    po::options_description desc("Allowed options");
+    desc.add_options()
+        ("help", "help message")
+        ("title", po::value<std::string>(&prog.test_title)->default_value(""), "title to show during test")
+        ("args", po::value<std::string>(&prog.device_args)->default_value(""), "single uhd device address args")
+        ("stats-file", po::value<std::string>(&prog.stats_filename)->default_value(""), "test statistics output filename (empty: auto-generate)")
+        ("stats-file-prefix", po::value<std::string>(&prog.stats_filename_prefix)->default_value(""), "test statistics output filename prefix")
+        ("stats-file-suffix", po::value<std::string>(&prog.stats_filename_suffix)->default_value(""), "test statistics output filename suffix")
+
+        ("rate", po::value<double>(&prog.sample_rate)->default_value(1e6), "rate of outgoing samples")
+        ("level", po::value<double>(&prog.trigger_level)->default_value(0.5), "trigger level as fraction of high level")
+        ("scale", po::value<float>(&prog.output_scale)->default_value(float(0.3)), "output scaling")
+        ("duration", po::value<double>(&prog.response_duration)->default_value(0.001), "duration of response (seconds)")
+        ("dc-offset-delay", po::value<double>(&prog.dc_offset_delay)->default_value(0), "duration of DC offset calibration (seconds)")   // This stage is not necessary
+        ("init-delay", po::value<double>(&prog.init_delay)->default_value(0.5), "initialisation delay (seconds)")
+        ("timeout", po::value<double>(&prog.timeout)->default_value(1.0), "stream timeout (seconds)")
+        ("spb", po::value<size_t>(&prog.samps_per_buff)->default_value(1024), "samples per buffer")
+        ("spp", po::value<size_t>(&prog.samps_per_packet)->default_value(0), "samples per packet (0: use default)")
+        ("calib", po::value<double>(&prog.level_calibration_duration)->default_value(0.5), "level calibration duration (seconds)")
+        ("invert", "input signal inversion")
+        ("invert-output", "output signal inversion")
+        ("no-delay", "disable timed delay")
+        ("allow-late", "allow late bursts")
+        ("delay", po::value<double>(&prog.delay)->default_value(0.005), "number of seconds in the future to reply")
+        ("delay-min", po::value<double>(&prog.delay_min)->default_value(0.0001), "minimum delay")
+        ("delay-max", po::value<double>(&prog.delay_max)->default_value(0.0050), "maximum delay")
+        ("delay-step", po::value<double>(&prog.delay_step)->default_value(0.000001), "delay step")
+        ("pdt", po::value<double>(&prog.pulse_detection_threshold)->default_value(1e-3), "pulse detection threshold")
+        ("iterations", po::value<uint64_t>(&prog.test_iterations)->default_value(0), "test iterations")
+        ("test-duration", "treat test iterations as duration")
+        ("test-success", po::value<size_t>(&prog.end_test_after_success_count)->default_value(0), "end test after multiple successful delays (0: run through all delays)")
+        ("skip-iterations", po::value<size_t>(&prog.skip_iterations)->default_value(50), "skip first iterations for each delay")
+        ("simulate", po::value<double>(&prog.simulate_frequency)->default_value(0.0), "frequency of simulation event (Hz)")
+        ("time-mul", po::value<double>(&prog.time_mul)->default_value(1.0), "statistics output time multiplier")
+        ("ignore-simulation-check", "ignore if simulation rate exceeds maximum delay + response duration")
+        ("flush", po::value<size_t>(&prog.flush_count)->default_value(16), "number of zero samples to add to a burst to flush hardware")
+        ("skip-eob", "disable end-of-burst")
+        ("adjust-simulation-rate", "adjust simulation rate if it will be too fast for maximum delay duration")
+        ("optimize-simulation-rate", "make simulation rate as fast as possible for each delay")
+        ("optimize-padding", po::value<size_t>(&prog.optimize_padding)->default_value(16), "time (as number of samples) to pad optimized simulation rate")
+        ("no-stats-file", "do not output statistics file")
+        ("log-file", "output log file")
+        ("batch-mode", "disable user prompts")
+        ("skip-if-exists", "skip the test if the results file exists")
+        ("disable-send", "skip burst transmission")
+        ("combine-eob", "combine EOB into first send")
+        ("pause", "pause after device creation")
+        ("priority", po::value<double>(&prog.rt_priority)->default_value(1.0), "scheduler priority")
+        ("no-realtime", "don't enable real-time")
+    ;
+    return desc;
+}
+
+void
+read_program_options(po::variables_map vm)
+{
+    // read out given options
+    prog.realtime = (vm.count("no-realtime") == 0);
+
+    prog.delay_step = abs(prog.delay_step);
+    if (prog.delay_min > prog.delay_max)
+    {
+        prog.delay_step *= -1;
+    }
+
+    prog.allow_late_bursts = (vm.count("allow-late") > 0);
+    prog.test_iterations_is_sample_count = (vm.count("test-duration") > 0);
+    prog.invert = ((vm.count("invert") > 0) ? -1.0f : 1.0f);
+    prog.output_value = ((vm.count("invert-output") > 0) ? -1.0f : 1.0f);
+    prog.skip_eob = (vm.count("skip-eob") > 0);
+    prog.no_delay = (vm.count("no-delay") > 0);
+    prog.adjust_simulation_rate = (vm.count("adjust-simulation-rate") > 0);
+    prog.optimize_simulation_rate = (vm.count("optimize-simulation-rate") > 0);
+    prog.no_stats_file = (vm.count("no-stats-file") > 0);
+    prog.log_file = (vm.count("log-file") > 0);
+    prog.batch_mode = (vm.count("batch-mode") > 0);
+    prog.skip_if_results_exist = (vm.count("skip-if-exists") > 0);
+    prog.skip_send = (vm.count("disable-send") > 0);
+    prog.combine_eob = (vm.count("combine-eob") > 0);
+    prog.pause = (vm.count("pause") > 0);
+    prog.ignore_simulation_check = vm.count("ignore-simulation-check");
+}
+
+/*
+ * This is the MAIN function!
+ * UHD_SAFE_MAIN catches all errors and prints them to stderr.
+ */
+int UHD_SAFE_MAIN(int argc, char *argv[]){
+    po::options_description desc = get_program_options_description();
+    po::variables_map vm;
+    po::store(po::parse_command_line(argc, argv, desc), vm);
+    po::notify(vm);
+    read_program_options(vm);
+
+    // Print help message instead of executing Responder.
+    if (vm.count("help")){
+        cout << boost::format("UHD Latency Test %s") % desc;
+        return Responder::RETCODE_OK;
+    }
+
+    //create a new instance of Responder and run it!
+    boost::shared_ptr<Responder> my_responder(new Responder(prog));
+    return my_responder->run();
+}
+
diff --git a/host/utils/latency/run_tests.py b/host/utils/latency/run_tests.py
new file mode 100755
index 000000000..f0cb31ffb
--- /dev/null
+++ b/host/utils/latency/run_tests.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python
+#
+# Copyright 2012 Ettus Research LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import subprocess, time
+from optparse import OptionParser
+from string import split
+import sys
+import os
+
+from gnuradio.eng_option import eng_option
+
+
+def launch_test(args="", rate=None, spb=None, spp=0, prefix="", suffix="", extra=[], verbose=False, title=None):
+    real = os.path.realpath(__file__)
+    basedir = os.path.dirname(real)
+    responder = [
+        os.path.join(basedir, "responder")
+    ]
+
+    if args is not None and len(args) > 0:
+        responder += ["--args=" + args]
+    if rate is not None and rate > 0:
+        responder += ["--rate=%f" % (rate)]
+    if spb is not None and spb > 0:
+        responder += ["--spb=%d" % (spb)]
+    if spp is not None and spp > 0:
+        responder += ["--spp=%d" % (spp)]
+    if prefix is not None and len(prefix) > 0:
+        responder += ["--stats-file-prefix=" + prefix]
+    if suffix is not None and len(suffix) > 0:
+        responder += ["--stats-file-suffix=" + suffix]
+    if extra is not None:
+        responder += extra
+    if title is not None and len(title) > 0:
+        responder += ["--title=\"" + title + "\""]
+    if verbose:
+        print "==> Executing:", " ".join(responder)
+    try:
+        responder += ["--log-file"] # This will produce another output file with logs
+        responder += ["--combine-eob"]
+        p = subprocess.Popen(responder)
+        res = p.wait() # make sure subprocess finishes
+    except KeyboardInterrupt:
+        res = p.wait() # even in CTRL+C case wait till subprocess finishes
+        print "==> Caught CTRL+C"
+        return None
+
+    return res
+
+# These return codes should match the C++ return codes
+class ReturnCode:
+    RETCODE_OK = 0
+    RETCODE_BAD_ARGS = -1
+    RETCODE_RUNTIME_ERROR = -2
+    RETCODE_UNKNOWN_EXCEPTION = -3
+    RETCODE_RECEIVE_TIMEOUT = -4
+    RETCODE_RECEIVE_FAILED = -5
+    RETCODE_MANUAL_ABORT = -6
+    RETCODE_BAD_PACKET = -7
+    RETCODE_OVERFLOW = -8
+
+
+def get_initialized_OptionParser():
+    def_rates = ".25 1 4 8 25"
+    usage = "%prog: [options] -- [extra arguments]"
+    parser = OptionParser(option_class=eng_option, usage=usage)
+
+    parser.add_option("", "--rates", type="string", help="sample rates (Msps) [default: %default]", default=def_rates)
+    parser.add_option("", "--spbs", type="string", help="samples per block [default: %default]",
+                      default="32 64 256 1024")
+    parser.add_option("", "--spps", type="string", help="samples per packet (0: driver default) [default: %default]",
+                      default="0 64 128 256 512")
+    parser.add_option("", "--args", type="string", help="UHD device arguments [default: %default]", default=None)
+    parser.add_option("", "--prefix", type="string", help="Stats filename prefix [default: %default]", default=None)
+    parser.add_option("", "--suffix", type="string", help="Stats filename suffix [default: %default]", default=None)
+    parser.add_option("", "--pause", action="store_true", help="pause between tests [default=%default]", default=False)
+    parser.add_option("", "--interactive", action="store_true", help="enable prompts within test [default=%default]",
+                      default=False)
+    parser.add_option("", "--wait", type="float", help="time to wait between tests (seconds) [default=%default]",
+                      default=0.0)
+    parser.add_option("", "--abort", action="store_true", help="abort on error [default=%default]", default=False)
+    parser.add_option("", "--verbose", action="store_true", help="be verbose [default=%default]", default=False)
+    parser.add_option("", "--title", type="string", help="test title [default: %default]", default=None)
+
+    return parser
+
+
+def set_gen_prefix(prefix, save_dir):
+    if not save_dir[-1] == "/":
+        save_dir = save_dir + "/"
+
+    if prefix == None:
+        if os.path.exists(save_dir) is not True:
+            os.makedirs(save_dir)
+        prefix = save_dir
+    return prefix
+
+
+def get_extra_args(options, args):
+    extra_args = {
+    "adjust-simulation-rate": None,
+    "time-mul": "1e6",
+    "test-success": 5,
+    "simulate": 1000,
+    "iterations": 1000,
+    "delay-min": "50e-6",
+    "delay-max": "5e-3",
+    "delay-step": "50e-6",
+    }
+
+    if options.interactive is not True:
+        extra_args["batch-mode"] = None
+    if options.pause is True:
+        extra_args["pause"] = None
+
+    for arg in args:
+        if len(arg) > 2 and arg[0:2] == "--":
+            arg = arg[2:]
+        idx = arg.find('=')
+        if idx == -1:
+            extra_args[arg] = None
+        else:
+            extra_args[arg[0:idx]] = arg[idx + 1:]
+
+    def _format_arg(d, k):
+        a = "--" + str(k)
+        if d[k] is not None:
+            a += "=" + str(d[k])
+        return a
+
+    extra = map(lambda x: _format_arg(extra_args, x), extra_args)
+
+    print "\n".join(map(lambda x: str(x) + " = " + str(extra_args[x]), extra_args.keys()))
+
+    return extra
+
+
+def wait_for_keyboard():
+    try:
+        print "\nPress ENTER to start..."
+        raw_input()
+        return ReturnCode.RETCODE_OK
+    except KeyboardInterrupt:
+        print "Aborted"
+        return ReturnCode.RETCODE_MANUAL_ABORT
+
+
+def main():
+    parser = get_initialized_OptionParser()
+    (options, args) = parser.parse_args()
+
+    save_dir = "results"
+    options.prefix = set_gen_prefix(options.prefix, save_dir)
+    extra = get_extra_args(options, args)
+
+    rates = map(lambda x: float(x) * 1e6, split(options.rates))
+    spbs = map(int, split(options.spbs))
+    spps = map(int, split(options.spps))
+    total = len(rates) * len(spbs) * len(spps)
+
+    title = options.title or ""
+    if len(title) >= 2 and title[0] == "\"" and title[-1] == "\"":
+        title = title[1:-1]
+
+    count = 0
+    results = {}
+
+    try:
+        for rate in rates:
+            results_rates = results[rate] = {}
+            for spb in spbs:
+                results_spbs = results_rates[spb] = {}
+                for spp in spps:
+                    if count > 0:
+                        if options.pause:
+                            print "Press ENTER to begin next test..."
+                            raw_input()
+                        elif options.wait > 0:
+                            time.sleep(options.wait)
+                    title = "Test #%d of %d (%d%% complete, %d to go)" % (
+                        count + 1, total, int(100 * count / total), total - count - 1)
+                    res = launch_test(options.args, rate, spb, spp, options.prefix, options.suffix, extra,
+                                      options.verbose, title)
+                    sys.stdout.flush()
+                    count += 1
+                    # Break out of loop. Exception thrown if Ctrl + C was pressed.
+                    if res is None:
+                        raise Exception
+                    results_spbs[spp] = res
+                    if res < 0 and (res == ReturnCode.RETCODE_MANUAL_ABORT or options.abort):
+                        raise Exception
+    except:
+        pass
+
+    for rate in results.keys():
+        results_rates = results[rate]
+        for spb in results_rates.keys():
+            results_spbs = results_rates[spb]
+            for spp in results_spbs.keys():
+                res = results_spbs[spp]
+                print res, ":", rate, spb, spp
+    print "Tests finished"
+    return 0
+
+
+if __name__ == '__main__':
+    main()
-- 
cgit v1.2.3