From 8ddc109a649899ab6a0b673908186a39c75c8f71 Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Wed, 12 Dec 2018 08:37:20 +0100 Subject: GUI: add progress bar --- python/dpd/Measure.py | 16 +++++++++------- python/dpd/RX_Agc.py | 2 +- python/dpdce.py | 22 ++++++++++++++++------ python/gui/static/js/odr-predistortion.js | 6 ++++++ python/gui/templates/predistortion.html | 6 +++++- 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/python/dpd/Measure.py b/python/dpd/Measure.py index 489c4c0..eb3c199 100644 --- a/python/dpd/Measure.py +++ b/python/dpd/Measure.py @@ -15,7 +15,7 @@ import logging class Measure: """Collect Measurement from DabMod""" - def __init__(self, config, samplerate, port, num_samples_to_request): + def __init__(self, config, samplerate : int, port : int, num_samples_to_request : int): logging.info("Instantiate Measure object") self.c = config self.samplerate = samplerate @@ -23,7 +23,7 @@ class Measure: self.port = port self.num_samples_to_request = num_samples_to_request - def _recv_exact(self, sock, num_bytes): + def _recv_exact(self, sock : socket.socket, num_bytes : int) -> bytes: """Receive an exact number of bytes from a socket. This is a wrapper around sock.recv() that can return less than the number of requested bytes. @@ -41,7 +41,7 @@ class Measure: bufs.append(b) return b''.join(bufs) - def receive_tcp(self): + def receive_tcp(self, num_samples_to_request : int): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(4) s.connect(('localhost', self.port)) @@ -49,8 +49,8 @@ class Measure: logging.debug("Send version") s.sendall(b"\x01") - logging.debug("Send request for {} samples".format(self.num_samples_to_request)) - s.sendall(struct.pack("=I", self.num_samples_to_request)) + logging.debug("Send request for {} samples".format(num_samples_to_request)) + s.sendall(struct.pack("=I", num_samples_to_request)) logging.debug("Wait for TX metadata") num_samps, tx_second, tx_pps = struct.unpack("=III", self._recv_exact(s, 12)) @@ -91,13 +91,14 @@ class Measure: return txframe, tx_ts, rxframe, rx_ts - def get_samples(self): + def get_samples(self, short=False): """Connect to ODR-DabMod, retrieve TX and RX samples, load into numpy arrays, and return a tuple (txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median, tx_median) """ - txframe, tx_ts, rxframe, rx_ts = self.receive_tcp() + n_samps = int(self.num_samples_to_request / 4) if short else self.num_samples_to_request + txframe, tx_ts, rxframe, rx_ts = self.receive_tcp(n_samps) # Normalize received signal with sent signal rx_median = np.median(np.abs(rxframe)) @@ -116,6 +117,7 @@ class Measure: # The MIT License (MIT) # +# Copyright (c) 2018 Matthias P. Braendli # Copyright (c) 2017 Andreas Steger # # Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/python/dpd/RX_Agc.py b/python/dpd/RX_Agc.py index 48ef7f3..bca9643 100644 --- a/python/dpd/RX_Agc.py +++ b/python/dpd/RX_Agc.py @@ -48,7 +48,7 @@ class Agc: self.adapt.set_rxgain(self.rxgain) # Measure - txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median, tx_median = self.measure.get_samples() + txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median, tx_median = self.measure.get_samples(short=True) # Estimate Maximum rx_peak = self.peak_to_median * rx_median diff --git a/python/dpdce.py b/python/dpdce.py index 838d265..03bc907 100755 --- a/python/dpdce.py +++ b/python/dpdce.py @@ -138,6 +138,7 @@ results = { 'tx_median': 0, 'rx_median': 0, 'state': 'Idle', + 'stateprogress': 0, # in percent 'summary': ['DPD has not been calibrated yet'], } lock = Lock() @@ -155,13 +156,20 @@ def engine_worker(): break elif cmd == "calibrate": with lock: - results['state'] = 'rx gain calibration' + results['state'] = 'RX Gain Calibration' + results['stateprogress'] = 0 - agc_success, agc_summary = agc.run() - summary = ["First calibration run:"] + agc_summary.split("\n") - if agc_success: + summary = [] + N_ITER = 5 + for i in range(N_ITER): agc_success, agc_summary = agc.run() - summary += ["Second calibration run: "] + agc_summary.split("\n") + summary = ["calibration run {}:".format(i)] + agc_summary.split("\n") + + with lock: + results['stateprogress'] = int((i + 1) * 100/N_ITER) + + if not agc_success: + break txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median, tx_median = meas.get_samples() @@ -171,11 +179,13 @@ def engine_worker(): results['tx_median'] = float(tx_median) results['rx_median'] = float(rx_median) results['state'] = 'Idle' + results['stateprogress'] = 0 results['summary'] = ["Calibration was done:"] + summary finally: with lock: - results['state'] = 'terminated' + results['state'] = 'Terminated' + results['stateprogress'] = 0 engine = Thread(target=engine_worker) diff --git a/python/gui/static/js/odr-predistortion.js b/python/gui/static/js/odr-predistortion.js index 04d2773..739e0ee 100644 --- a/python/gui/static/js/odr-predistortion.js +++ b/python/gui/static/js/odr-predistortion.js @@ -29,6 +29,12 @@ function resultrefresh() { $('#dpdresults').html(summary); $('#dpdstatus').text(data['state']); + var percentage = data['stateprogress']; + if (percentage > 100) { + percentage = 100; + } + $('#dpdprogress').css('width', percentage + '%'); + $('#dpdprogresstext').text(percentage + '%'); }); jqxhr.always(function() { diff --git a/python/gui/templates/predistortion.html b/python/gui/templates/predistortion.html index cc5ecb0..f9af2f1 100644 --- a/python/gui/templates/predistortion.html +++ b/python/gui/templates/predistortion.html @@ -31,7 +31,11 @@ along with ODR-DabMod. If not, see .
Status and calibration
-
Current DPDCE status: N/A +
Current DPDCE status: + N/A +
+ +
N/A
-- cgit v1.2.3 From fb17763e266d8cea1569d3e51404b3e62835f7f4 Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Wed, 12 Dec 2018 09:16:25 +0100 Subject: GUI: Try to get RC params working again --- python/gui/static/js/odr-rcvalues.js | 23 ++++++++++++++--------- python/gui/templates/predistortion.html | 10 +++++++--- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/python/gui/static/js/odr-rcvalues.js b/python/gui/static/js/odr-rcvalues.js index f49674c..a3a5e07 100644 --- a/python/gui/static/js/odr-rcvalues.js +++ b/python/gui/static/js/odr-rcvalues.js @@ -31,18 +31,21 @@ function requestStatus() { doApiRequestGET("/api/rc_parameters", function(data) { console.log(data); - let keys = Object.keys(data); - keys.sort(); + let controllable_names = Object.keys(data); + controllable_names.sort(); var key1; - for (key1 in keys) { - let keys2 = Object.keys(data[keys[key1]]); - keys2.sort(); + for (key1 in controllable_names) { + let param_names = Object.keys(data[controllable_names[key1]]); + param_names.sort(); var key2; - for (key2 in keys2) { - var param = data[keys[key1]][keys2[key2]]; - var key = keys[key1] + "_" + keys2[key2]; + for (key2 in param_names) { + var name_controllable = controllable_names[key1]; + var name_param = param_names[key2]; + var key = name_controllable + "_" + name_param; + + var param = data[name_controllable][name_param]; var valueentry = '' + '
+ -- cgit v1.2.3 From 823c57c747c77bda86c670c446d6f857bd2d1585 Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Wed, 12 Dec 2018 10:22:06 +0100 Subject: Fix rc values button function --- python/gui/static/js/odr-rcvalues.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/python/gui/static/js/odr-rcvalues.js b/python/gui/static/js/odr-rcvalues.js index a3a5e07..486a8a7 100644 --- a/python/gui/static/js/odr-rcvalues.js +++ b/python/gui/static/js/odr-rcvalues.js @@ -49,17 +49,21 @@ function requestStatus() { var valueentry = '' + ''; + 'id="button'+key+'" ' + + 'data-controllable="'+name_controllable+'" ' + + 'data-param="'+name_param+'" ' + + '>upd'; $('#rctable > tbody:last').append( ''+key+''+ ''+valueentry+''+ ''+param['help']+''); - $('#button'+key).click(function() { - console.log("trigger " + key + " with " + name_controllable + " " + name_param); - buttonSetRc("input"+key, name_controllable, name_param); + var attr_c = this.getAttribute('data-controllable'); + var attr_p = this.getAttribute('data-param'); + var k = attr_c + "_" + attr_p; + buttonSetRc("input"+k, attr_c, attr_p); }); } } -- cgit v1.2.3 From 47242ec7ccbfc60fd14276e102abe119f1766d95 Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Wed, 12 Dec 2018 10:55:59 +0100 Subject: Increase YAML-RPC packet size --- python/lib/yamlrpc.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/python/lib/yamlrpc.py b/python/lib/yamlrpc.py index d963601..d4e744a 100644 --- a/python/lib/yamlrpc.py +++ b/python/lib/yamlrpc.py @@ -23,6 +23,9 @@ """yamlrpc is json-rpc, except that it's yaml and not json.""" +# This maybe won't work over ethernet, but for localhost it's ok +UDP_PACKETSIZE = 2048 + # Same as jsonrpc version we're aiming to mirror in YAML YAMLRPC_VERSION = "2.0" @@ -80,7 +83,7 @@ class Socket: def receive_response(self, expected_msg_id: int): try: - data, addr = self.socket.recvfrom(512) + data, addr = self.socket.recvfrom(UDP_PACKETSIZE) except socket.timeout as to: raise TimeoutError("Timeout: " + str(to)) @@ -117,7 +120,7 @@ class Socket: def receive_request(self): try: - data, addr = self.socket.recvfrom(512) + data, addr = self.socket.recvfrom(UDP_PACKETSIZE) except socket.timeout as to: raise TimeoutError("Timeout: " + str(to)) -- cgit v1.2.3 From f409d342c5a314f4aa35382bbdd8daa3882172d1 Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Wed, 12 Dec 2018 10:56:33 +0100 Subject: Improve DPD calibration --- python/dpd/Measure.py | 20 ++++++++++++++++++++ python/dpd/RX_Agc.py | 16 ++++++++++++---- python/dpdce.py | 3 ++- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/python/dpd/Measure.py b/python/dpd/Measure.py index eb3c199..e5a72c7 100644 --- a/python/dpd/Measure.py +++ b/python/dpd/Measure.py @@ -90,6 +90,26 @@ class Measure: return txframe, tx_ts, rxframe, rx_ts + def get_samples_unaligned(self, short=False): + """Connect to ODR-DabMod, retrieve TX and RX samples, load + into numpy arrays, and return a tuple + (txframe, tx_ts, rxframe, rx_ts, rx_median, tx_median) + """ + + n_samps = int(self.num_samples_to_request / 4) if short else self.num_samples_to_request + txframe, tx_ts, rxframe, rx_ts = self.receive_tcp(n_samps) + + # Normalize received signal with sent signal + rx_median = np.median(np.abs(rxframe)) + tx_median = np.median(np.abs(txframe)) + rxframe = rxframe / rx_median * tx_median + + + logging.info( + "Measurement done, tx %d %s, rx %d %s" % + (len(txframe), txframe.dtype, len(rxframe), rxframe.dtype)) + + return txframe, tx_ts, rxframe, rx_ts, rx_median, tx_median def get_samples(self, short=False): """Connect to ODR-DabMod, retrieve TX and RX samples, load diff --git a/python/dpd/RX_Agc.py b/python/dpd/RX_Agc.py index bca9643..4700e68 100644 --- a/python/dpd/RX_Agc.py +++ b/python/dpd/RX_Agc.py @@ -45,10 +45,15 @@ class Agc: self.peak_to_median = 1./c.RAGC_rx_median_target def run(self) -> Tuple[bool, str]: - self.adapt.set_rxgain(self.rxgain) + try: + self.adapt.set_rxgain(self.rxgain) + except ValueError as e: + return (False, "Setting RX gain to {} failed: {}".format(self.rxgain, e)) + time.sleep(0.5) + # Measure - txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median, tx_median = self.measure.get_samples(short=True) + txframe, tx_ts, rxframe, rx_ts, rx_median, tx_median = self.measure.get_samples_unaligned(short=False) # Estimate Maximum rx_peak = self.peak_to_median * rx_median @@ -70,9 +75,12 @@ class Agc: logging.warning(w) return (False, "\n".join([measurements, w])) else: - self.adapt.set_rxgain(self.rxgain) + try: + self.adapt.set_rxgain(self.rxgain) + except ValueError as e: + return (False, "Setting RX gain to {} failed: {}".format(self.rxgain, e)) time.sleep(0.5) - return (True, measurements) + return (True, measurements) def plot_estimates(self): """Plots the estimate of for Max, Median, Mean for different diff --git a/python/dpdce.py b/python/dpdce.py index 03bc907..1ceac46 100755 --- a/python/dpdce.py +++ b/python/dpdce.py @@ -163,10 +163,11 @@ def engine_worker(): N_ITER = 5 for i in range(N_ITER): agc_success, agc_summary = agc.run() - summary = ["calibration run {}:".format(i)] + agc_summary.split("\n") + summary += ["calibration run {}:".format(i)] + agc_summary.split("\n") with lock: results['stateprogress'] = int((i + 1) * 100/N_ITER) + results['summary'] = ["Calibration ongoing:"] + summary if not agc_success: break -- cgit v1.2.3 From e7a2a62e99c9c140664cfbfaa38402983fa9ff1a Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Wed, 12 Dec 2018 10:56:56 +0100 Subject: GUI: return command confirmation to browser to avoid HTTP 503 --- python/dpdce.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python/dpdce.py b/python/dpdce.py index 1ceac46..379f3d0 100755 --- a/python/dpdce.py +++ b/python/dpdce.py @@ -211,13 +211,15 @@ try: if method == 'trigger_run': logging.info('YAML-RPC request : {}'.format(method)) command_queue.put('trigger_run') + cmd_socket.send_success_response(addr, msg_id, None) elif method == 'reset': logging.info('YAML-RPC request : {}'.format(method)) command_queue.put('reset') + cmd_socket.send_success_response(addr, msg_id, None) elif method == 'set_setting': logging.info('YAML-RPC request : {} -> {}'.format(method, params)) # params == {'setting': ..., 'value': ...} - pass + cmd_socket.send_success_response(addr, msg_id, None) elif method == 'get_settings': with lock: cmd_socket.send_success_response(addr, msg_id, settings) @@ -227,6 +229,7 @@ try: elif method == 'calibrate': logging.info('YAML-RPC request : {}'.format(method)) command_queue.put('calibrate') + cmd_socket.send_success_response(addr, msg_id, None) else: cmd_socket.send_error_response(addr, msg_id, "request not understood") finally: -- cgit v1.2.3 From 6be53e16ae994e20857ec92568f9ad125c73c18f Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Mon, 17 Dec 2018 14:47:07 +0100 Subject: Soapy: do not set master clock rate if it is 0 --- src/output/Soapy.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/output/Soapy.cpp b/src/output/Soapy.cpp index 86ed3e0..8c84b84 100644 --- a/src/output/Soapy.cpp +++ b/src/output/Soapy.cpp @@ -71,8 +71,10 @@ Soapy::Soapy(SDRDeviceConfig& config) : throw std::runtime_error("Cannot create SoapySDR output"); } - m_device->setMasterClockRate(m_conf.masterClockRate); - etiLog.level(info) << "SoapySDR master clock rate set to " << + if (m_conf.masterClockRate != 0) { + m_device->setMasterClockRate(m_conf.masterClockRate); + } + etiLog.level(info) << "SoapySDR:Actual master clock rate: " << std::fixed << std::setprecision(4) << m_device->getMasterClockRate()/1000.0 << " kHz"; -- cgit v1.2.3 From 154234871e06d6943d7e79a05ba10b37eb7e9198 Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Mon, 17 Dec 2018 15:13:41 +0100 Subject: Only start DPD feedback if port is set --- src/output/SDR.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/output/SDR.cpp b/src/output/SDR.cpp index 068b5af..23a947b 100644 --- a/src/output/SDR.cpp +++ b/src/output/SDR.cpp @@ -66,10 +66,12 @@ SDR::SDR(SDRDeviceConfig& config, std::shared_ptr device) : m_device_thread = std::thread(&SDR::process_thread_entry, this); - m_dpd_feedback_server = make_shared( - m_device, - m_config.dpdFeedbackServerPort, - m_config.sampleRate); + if (m_config.dpdFeedbackServerPort > 0) { + m_dpd_feedback_server = make_shared( + m_device, + m_config.dpdFeedbackServerPort, + m_config.sampleRate); + } RC_ADD_PARAMETER(txgain, "TX gain"); RC_ADD_PARAMETER(rxgain, "RX gain for DPD feedback"); -- cgit v1.2.3 From e83e1324a50055a4b972b78e26383df7ee290fee Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Tue, 18 Dec 2018 16:26:17 +0100 Subject: GUI: add capture and plot to DPD --- python/dpd/ExtractStatistic.py | 17 ++--- python/dpd/GlobalConfig.py | 2 +- python/dpdce.py | 100 +++++++++++++++++++++++++++--- python/gui-dpdce.ini | 5 +- python/gui/static/js/odr-predistortion.js | 17 ++++- python/gui/templates/predistortion.html | 31 ++++----- 6 files changed, 134 insertions(+), 38 deletions(-) diff --git a/python/dpd/ExtractStatistic.py b/python/dpd/ExtractStatistic.py index 639513a..1aa4391 100644 --- a/python/dpd/ExtractStatistic.py +++ b/python/dpd/ExtractStatistic.py @@ -41,6 +41,8 @@ class ExtractStatistic: def __init__(self, c): self.c = c + self._plot_data = None + # Number of measurements used to extract the statistic self.n_meas = 0 @@ -58,12 +60,10 @@ class ExtractStatistic: for i in range(c.ES_n_bins): self.tx_values_lists.append([]) - self.plot = c.ES_plot + def plot(self, plot_path, title): + if self._plot_data is not None: + tx_values, rx_values, phase_diffs_values, phase_diffs_values_lists = self._plot_data - def _plot_and_log(self, tx_values, rx_values, phase_diffs_values, phase_diffs_values_lists): - if self.plot and self.c.plot_location is not None: - dt = datetime.datetime.now().isoformat() - fig_path = self.c.plot_location + "/" + dt + "_ExtractStatistic.png" sub_rows = 3 sub_cols = 1 fig = plt.figure(figsize=(sub_cols * 6, sub_rows / 2. * 6)) @@ -80,7 +80,7 @@ class ExtractStatistic: np.abs(rx_values_list), s=0.1, color="black") - ax.set_title("Extracted Statistic") + ax.set_title("Extracted Statistic {}".format(title)) ax.set_xlabel("TX Amplitude") ax.set_ylabel("RX Amplitude") ax.set_ylim(0, 0.8) @@ -116,7 +116,7 @@ class ExtractStatistic: ax.set_ylim(0, self.n_per_bin * 1.2) fig.tight_layout() - fig.savefig(fig_path) + fig.savefig(plot_path) plt.close(fig) def _rx_value_per_bin(self): @@ -166,7 +166,7 @@ class ExtractStatistic: phase_diffs_values_lists = self._phase_diff_list_per_bin() phase_diffs_values = _phase_diff_value_per_bin(phase_diffs_values_lists) - self._plot_and_log(tx_values, rx_values, phase_diffs_values, phase_diffs_values_lists) + self._plot_data = (tx_values, rx_values, phase_diffs_values, phase_diffs_values_lists) tx_values_crop = np.array(tx_values, dtype=np.float32)[:idx_end] rx_values_crop = np.array(rx_values, dtype=np.float32)[:idx_end] @@ -176,6 +176,7 @@ class ExtractStatistic: # The MIT License (MIT) # # Copyright (c) 2017 Andreas Steger +# Copyright (c) 2018 Matthias P. Braendli # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/python/dpd/GlobalConfig.py b/python/dpd/GlobalConfig.py index 873b6ac..99280f2 100644 --- a/python/dpd/GlobalConfig.py +++ b/python/dpd/GlobalConfig.py @@ -10,7 +10,7 @@ import numpy as np class GlobalConfig: - def __init__(self, samplerate, plot_location: str): + def __init__(self, samplerate: int, plot_location: str): self.sample_rate = samplerate assert self.sample_rate == 8192000, "We only support constants for 8192000 sample rate: {}".format(self.sample_rate) diff --git a/python/dpdce.py b/python/dpdce.py index 379f3d0..a9ed140 100755 --- a/python/dpdce.py +++ b/python/dpdce.py @@ -43,7 +43,8 @@ rc_port = config.getint('rc_port') samplerate = config.getint('samplerate') samps = config.getint('samps') coef_file = config['coef_file'] -log_folder = config['log_folder'] +logs_directory = config['logs_directory'] +plot_directory = config['plot_directory'] import logging import datetime @@ -52,7 +53,7 @@ save_logs = False # Simple usage scenarios don't need to clutter /tmp if save_logs: - dt = datetime.datetime.now().isoformat() + dt = datetime.datetime.utcnow().isoformat() logging_path = '/tmp/dpd_{}'.format(dt).replace('.', '_').replace(':', '-') print("Logs and plots written to {}".format(logging_path)) os.makedirs(logging_path) @@ -71,7 +72,7 @@ if save_logs: # add the handler to the root logger logging.getLogger('').addHandler(console) else: - dt = datetime.datetime.now().isoformat() + dt = datetime.datetime.utcnow().isoformat() logging.basicConfig(format='%(asctime)s - %(module)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level=logging.INFO) @@ -83,6 +84,7 @@ import socket from lib import yamlrpc import numpy as np import traceback +import os.path from threading import Thread, Lock from queue import Queue from dpd.Model import Poly @@ -96,13 +98,14 @@ from dpd.GlobalConfig import GlobalConfig from dpd.MER import MER from dpd.Measure_Shoulders import Measure_Shoulders -c = GlobalConfig(samplerate, logging_path) +plot_path = os.path.realpath(plot_directory) + +c = GlobalConfig(samplerate, plot_path) symbol_align = Symbol_align(c) mer = MER(c) meas_shoulders = Measure_Shoulders(c) meas = Measure(c, samplerate, dpd_port, samps) -extStat = ExtractStatistic(c) -adapt = Adapt(rc_port, coef_file, logging_path) +adapt = Adapt(rc_port, coef_file, plot_path) model = Poly(c) @@ -134,7 +137,11 @@ settings = { 'digital_gain': digital_gain, 'dpddata': dpddata, } +internal_data = { + 'n_runs': 0, + } results = { + 'statplot': None, 'tx_median': 0, 'rx_median': 0, 'state': 'Idle', @@ -148,6 +155,7 @@ command_queue = Queue(maxsize=1) agc = Agc(meas, adapt, c) def engine_worker(): + extStat = ExtractStatistic(c) try: while True: cmd = command_queue.get() @@ -180,8 +188,86 @@ def engine_worker(): results['tx_median'] = float(tx_median) results['rx_median'] = float(rx_median) results['state'] = 'Idle' - results['stateprogress'] = 0 + results['stateprogress'] = 100 results['summary'] = ["Calibration was done:"] + summary + elif cmd == "reset": + with lock: + internal_data['n_runs'] = 0 + results['state'] = 'Idle' + results['stateprogress'] = 0 + results['summary'] = ["Reset"] + extStat = ExtractStatistic(c) + elif cmd == "trigger_run": + with lock: + results['state'] = 'Capture + Model' + results['stateprogress'] = 0 + n_runs = internal_data['n_runs'] + + # Get Samples and check gain + txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median, tx_median = meas.get_samples() + # TODO Check TX median + + with lock: + results['stateprogress'] = 20 + results['summary'] = ["Captured {} samples".format(len(txframe_aligned)), + "TX/RX median: {} / {}".format(tx_median, rx_median)] + + # Extract usable data from measurement + tx, rx, phase_diff, n_per_bin = extStat.extract(txframe_aligned, rxframe_aligned) + + time = datetime.datetime.utcnow() + + plot_file = "stats_{}.png".format(time.strftime("%s")) + extStat.plot(os.path.join(plot_path, plot_file), time.strftime("%Y-%m-%dT%H%M%S")) + + with lock: + results['statplot'] = "dpd/" + plot_file + results['stateprogress'] = 30 + results['summary'] += ["Extracted Statistics".format(tx_median, rx_median)] + + n_meas = Heuristics.get_n_meas(n_runs) + if extStat.n_meas >= n_meas: # Use as many measurements nr of runs + if any(x is None for x in [tx, rx, phase_diff]): + with lock: + results['summary'] += ["Error! No data to calculate model"] + results['state'] = 'Idle' + results['stateprogress'] = 0 + else: + with lock: + results['state'] = 'Capture + Model' + results['stateprogress'] = 40 + results['summary'] += ["Training model"] + + model.train(tx, rx, phase_diff, lr=Heuristics.get_learning_rate(n_runs)) + + with lock: + results['state'] = 'Capture + Model' + results['stateprogress'] = 60 + results['summary'] += ["Getting DPD data"] + + dpddata = model.get_dpd_data() + with lock: + internal_data['dpddata'] = dpddata + internal_data['n_runs'] = 0 + + results['state'] = 'Capture + Model' + results['stateprogress'] = 80 + results['summary'] += ["Reset statistics"] + + extStat = ExtractStatistic(c) + + with lock: + results['state'] = 'Idle' + results['stateprogress'] = 100 + results['summary'] += ["New DPD coefficients calculated"] + + with lock: + internal_data['n_runs'] += 1 + else: + with lock: + results['state'] = 'Idle' + results['stateprogress'] = 100 + results['summary'] += ["More data required to train model"] finally: with lock: diff --git a/python/gui-dpdce.ini b/python/gui-dpdce.ini index 48c6abf..4385c80 100644 --- a/python/gui-dpdce.ini +++ b/python/gui-dpdce.ini @@ -19,7 +19,10 @@ samps=81920 coef_file=poly.coef # Write logs to this folder, or leave empty for no logs -log_folder= +logs_directory= + +# Saving plots to the static directory makes them accessible to the browser +plot_directory=gui/static/dpd [gui] diff --git a/python/gui/static/js/odr-predistortion.js b/python/gui/static/js/odr-predistortion.js index 739e0ee..e9f7c96 100644 --- a/python/gui/static/js/odr-predistortion.js +++ b/python/gui/static/js/odr-predistortion.js @@ -35,6 +35,10 @@ function resultrefresh() { } $('#dpdprogress').css('width', percentage + '%'); $('#dpdprogresstext').text(percentage + '%'); + + if (data['statplot']) { + $('#dpdcapturestats').attr('src', data['statplot']); + } }); jqxhr.always(function() { @@ -43,7 +47,7 @@ function resultrefresh() { } $(function(){ - setTimeout(resultrefresh, 2000); + setTimeout(resultrefresh, 20); $('#calibratebtn').click(function() { doApiRequestPOST("/api/dpd_calibrate", {}, function(data) { @@ -51,6 +55,17 @@ $(function(){ }); }); + $('#resetbtn').click(function() { + doApiRequestPOST("/api/dpd_reset", {}, function(data) { + console.log("reset succeeded: " + JSON.stringify(data)); + }); + }); + + $('#triggerbtn').click(function() { + doApiRequestPOST("/api/dpd_trigger_run", {}, function(data) { + console.log("run succeeded: " + JSON.stringify(data)); + }); + }); }); /* diff --git a/python/gui/templates/predistortion.html b/python/gui/templates/predistortion.html index edb2f04..adbba7e 100644 --- a/python/gui/templates/predistortion.html +++ b/python/gui/templates/predistortion.html @@ -26,35 +26,26 @@
Calibration needs to be done once before the PA model can be trained. Every time calibration is changed, the predistortion parameters are invalidated!
+
Once calibration succeeded and correct RX gain is set, you + can trigger a capture and model the PA. Usually, several capture + runs are needed before the model can be trained.
-
-
- +

This list contains previously used predistortion settings that you + can recall.

+

+ +

+

+ + +

+

Summary

Calibration needs to be done once before the PA model @@ -36,6 +51,8 @@ runs are needed before the model can be trained.

The capture and model analysis will calculate a new set of DPD model data, that you can apply using the Update Predistorter button.

+

The reset button allows you to reset the computation engine. It does not + modify the currently active predistorter.

@@ -47,7 +64,7 @@ + Reset Capture and Model @@ -97,7 +114,7 @@