diff options
| author | Balint Seeber <balint@ettus.com> | 2014-10-01 17:20:20 -0700 | 
|---|---|---|
| committer | Martin Braun <martin.braun@ettus.com> | 2014-10-09 10:50:37 +0200 | 
| commit | 15740bcc3b3e1d5adff8c77306d84741a26ebdad (patch) | |
| tree | 29aba02634aaf58079064084d8bb2cc3fa5c319e | |
| parent | ea5c8a39e43913e29936acde89745a7cdf5b18a8 (diff) | |
| download | uhd-15740bcc3b3e1d5adff8c77306d84741a26ebdad.tar.gz uhd-15740bcc3b3e1d5adff8c77306d84741a26ebdad.tar.bz2 uhd-15740bcc3b3e1d5adff8c77306d84741a26ebdad.zip | |
utils: Added latency measurement utility
| -rw-r--r-- | host/utils/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | host/utils/latency/CMakeLists.txt | 46 | ||||
| -rwxr-xr-x | host/utils/latency/graph.py | 376 | ||||
| -rw-r--r-- | host/utils/latency/include/Responder.hpp | 299 | ||||
| -rw-r--r-- | host/utils/latency/lib/Responder.cpp | 1465 | ||||
| -rwxr-xr-x | host/utils/latency/pci_hwdata.py | 231 | ||||
| -rw-r--r-- | host/utils/latency/responder.cpp | 133 | ||||
| -rwxr-xr-x | host/utils/latency/run_tests.py | 222 | 
8 files changed, 2774 insertions, 0 deletions
| 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() | 
