diff options
author | Matthias P. Braendli <matthias.braendli@mpb.li> | 2018-12-05 11:19:07 +0100 |
---|---|---|
committer | Matthias P. Braendli <matthias.braendli@mpb.li> | 2018-12-05 11:19:07 +0100 |
commit | 31b65e41043900c0cadd80961f4b22cdfc171e7d (patch) | |
tree | cdeceac026a2d1e0fe8c00af5d0f867767d17ef4 /python | |
parent | 5cf52c74e9eb6bf8a82af4509ff3eb5106f928f9 (diff) | |
download | dabmod-31b65e41043900c0cadd80961f4b22cdfc171e7d.tar.gz dabmod-31b65e41043900c0cadd80961f4b22cdfc171e7d.tar.bz2 dabmod-31b65e41043900c0cadd80961f4b22cdfc171e7d.zip |
Get GUI to communicate with DPDCE
Diffstat (limited to 'python')
-rw-r--r-- | python/dpd/GlobalConfig.py | 9 | ||||
-rw-r--r-- | python/dpd/RX_Agc.py | 45 | ||||
-rwxr-xr-x | python/dpdce.py | 48 | ||||
-rwxr-xr-x | python/gui.py | 7 | ||||
-rwxr-xr-x | python/gui/api.py | 56 | ||||
-rw-r--r-- | python/gui/static/js/odr-predistortion.js | 17 | ||||
-rw-r--r-- | python/gui/static/js/odr.js | 16 | ||||
-rw-r--r-- | python/gui/templates/predistortion.html | 10 | ||||
-rw-r--r-- | python/lib/yamlrpc.py | 58 | ||||
-rw-r--r-- | python/poly.coef | 12 |
10 files changed, 220 insertions, 58 deletions
diff --git a/python/dpd/GlobalConfig.py b/python/dpd/GlobalConfig.py index abd7442..873b6ac 100644 --- a/python/dpd/GlobalConfig.py +++ b/python/dpd/GlobalConfig.py @@ -10,9 +10,9 @@ import numpy as np class GlobalConfig: - def __init__(self, args, plot_location: str): - self.sample_rate = args['samplerate'] - assert self.sample_rate == 8192000 # By now only constants for 8192000 + def __init__(self, samplerate, plot_location: str): + self.sample_rate = samplerate + assert self.sample_rate == 8192000, "We only support constants for 8192000 sample rate: {}".format(self.sample_rate) self.plot_location = plot_location plot = len(plot_location) > 0 @@ -77,12 +77,13 @@ class GlobalConfig: # Constants for RX_AGC self.RAGC_min_rxgain = 25 # USRP B200 specific + self.RAGC_max_rxgain = 65 # USRP B200 specific self.RAGC_rx_median_target = 0.05 # The MIT License (MIT) # # Copyright (c) 2017 Andreas Steger -# Copyright (c) 2017 Matthias P. Braendli +# 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/RX_Agc.py b/python/dpd/RX_Agc.py index 0cc18b8..2a2f548 100644 --- a/python/dpd/RX_Agc.py +++ b/python/dpd/RX_Agc.py @@ -13,6 +13,7 @@ import numpy as np import matplotlib matplotlib.use('agg') import matplotlib.pyplot as plt +from typing import Tuple import dpd.Adapt as Adapt import dpd.Measure as Measure @@ -39,31 +40,39 @@ class Agc: self.measure = measure self.adapt = adapt self.min_rxgain = c.RAGC_min_rxgain + self.max_rxgain = c.RAGC_max_rxgain self.rxgain = self.min_rxgain self.peak_to_median = 1./c.RAGC_rx_median_target - def run(self): + def run(self) -> Tuple[bool, str]: self.adapt.set_rxgain(self.rxgain) - for i in range(2): - # Measure - txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median= \ - self.measure.get_samples() - - # Estimate Maximum - rx_peak = self.peak_to_median * rx_median - correction_factor = 20*np.log10(1/rx_peak) - self.rxgain = self.rxgain + correction_factor - - assert self.min_rxgain <= self.rxgain, ("Desired RX Gain is {} which is smaller than the minimum of {}".format( - self.rxgain, self.min_rxgain)) - - logging.info("RX Median {:1.4f}, estimated peak {:1.4f}, correction factor {:1.4f}, new RX gain {:1.4f}".format( - rx_median, rx_peak, correction_factor, self.rxgain - )) - + # Measure + txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median=self.measure.get_samples() + + # Estimate Maximum + rx_peak = self.peak_to_median * rx_median + correction_factor = 20*np.log10(1/rx_peak) + self.rxgain = self.rxgain + correction_factor + + measurements = "RX Median {:1.4f}, estimated peak {:1.4f}, correction factor {:1.4f}, new RX gain {:1.4f}".format( + rx_median, rx_peak, correction_factor, self.rxgain) + logging.info(measurements) + + if self.rxgain < self.min_rxgain: + w = "Warning: calculated RX Gain={} is lower than minimum={}. RX feedback power is too high!".format( + self.rxgain, self.min_rxgain) + logging.warning(w) + return (False, "\n".join([measurements, w])) + elif self.rxgain > self.max_rxgain: + w = "Warning: calculated RX Gain={} is higher than maximum={}. RX feedback power should be increased.".format( + self.rxgain, self.max_rxgain) + logging.warning(w) + return (False, "\n".join([measurements, w])) + else: self.adapt.set_rxgain(self.rxgain) time.sleep(0.5) + 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 da1b6fb..efc69ef 100755 --- a/python/dpdce.py +++ b/python/dpdce.py @@ -37,11 +37,11 @@ config = allconfig['dpdce'] # removed options: # txgain, rxgain, digital_gain, target_median, iterations, lut, enable-txgain-agc, plot, measure -control_port = config['control_port'] -dpd_port = config['dpd_port'] -rc_port = config['rc_port'] -samplerate = config['samplerate'] -samps = config['samps'] +control_port = config.getint('control_port') +dpd_port = config.getint('dpd_port') +rc_port = config.getint('rc_port') +samplerate = config.getint('samplerate') +samps = config.getint('samps') coef_file = config['coef_file'] log_folder = config['log_folder'] @@ -96,7 +96,7 @@ from dpd.GlobalConfig import GlobalConfig from dpd.MER import MER from dpd.Measure_Shoulders import Measure_Shoulders -c = GlobalConfig(config, logging_path) +c = GlobalConfig(samplerate, logging_path) symbol_align = Symbol_align(c) mer = MER(c) meas_shoulders = Measure_Shoulders(c) @@ -125,7 +125,7 @@ if cli_args.reset: logging.info("DPD Settings were reset to default values.") sys.exit(0) -cmd_socket = yamlrpc.Socket(bind_port=config.getint(control_port)) +cmd_socket = yamlrpc.Socket(bind_port=control_port) # The following is accessed by both threads and need to be locked settings = { @@ -138,6 +138,7 @@ results = { 'tx_median': 0, 'rx_median': 0, 'state': 'idle', + 'summary': 'DPD has not been calibrated yet', } lock = Lock() command_queue = Queue(maxsize=1) @@ -156,7 +157,11 @@ def engine_worker(): with lock: results['state'] = 'rx agc' - agc.run() + agc_success, agc_summary = agc.run() + summary = ["First calibration run: " + agc_summary] + if agc_success: + agc_success, agc_summary = agc.run() + summary.append("Second calibration run: " + agc_summary) txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median = self.measure.get_samples() @@ -166,6 +171,7 @@ def engine_worker(): results['tx_median'] = tx_median results['rx_median'] = rx_median results['state'] = 'idle' + results['summary'] = "Calibration was done:\n" + "\n".join(agc_summary) finally: with lock: @@ -177,9 +183,27 @@ engine.start() try: while True: - addr, msg_id, method, params = cmd_socket.receive_request() - - if method == 'get_settings': + try: + addr, msg_id, method, params = cmd_socket.receive_request() + except ValueError as e: + logging.warning('YAML-RPC request error: {}'.format(e)) + continue + except TimeoutError: + continue + except: + logging.error('YAML-RPC unknown error') + break + + logging.info('YAML-RPC request : {}'.format(method)) + + if method == 'trigger_run': + command_queue.put('trigger_run') + elif method == 'reset': + command_queue.put('reset') + elif method == 'set_setting': + # params == {'setting': ..., 'value': ...} + pass + elif method == 'get_settings': with lock: cmd_socket.send_success_response(addr, msg_id, settings) elif method == 'get_results': @@ -187,6 +211,8 @@ try: cmd_socket.send_success_response(addr, msg_id, results) elif method == 'calibrate': command_queue.put('calibrate') + elif method == "get_calibration_result": + pass else: cmd_socket.send_error_response(addr, msg_id, "request not understood") finally: diff --git a/python/gui.py b/python/gui.py index 512afef..ce7948c 100755 --- a/python/gui.py +++ b/python/gui.py @@ -35,9 +35,9 @@ env = Environment(loader=FileSystemLoader('gui/templates')) base_js = ["js/odr.js"] class Root: - def __init__(self): + def __init__(self, dpd_port): self.mod_rc = zmqrc.ModRemoteControl("localhost") - self.api = API(self.mod_rc) + self.api = API(self.mod_rc, dpd_port) @cherrypy.expose def index(self): @@ -81,6 +81,7 @@ if __name__ == '__main__': allconfig = configparser.ConfigParser() allconfig.read(cli_args.config) config = allconfig['gui'] + dpd_port = allconfig['dpdce'].getint('control_port') daemon = False if daemon: @@ -105,7 +106,7 @@ if __name__ == '__main__': staticdir = os.path.realpath(config['static_directory']) cherrypy.tree.mount( - Root(), config={ + Root(dpd_port), config={ '/': { }, '/dpd': { 'tools.staticdir.on': True, diff --git a/python/gui/api.py b/python/gui/api.py index ae54e8b..0d24bac 100755 --- a/python/gui/api.py +++ b/python/gui/api.py @@ -46,8 +46,10 @@ def send_error(reason=""): return {'status' : 'error'} class API: - def __init__(self, mod_rc): + def __init__(self, mod_rc, dpd_port): self.mod_rc = mod_rc + self.dpd_port = dpd_port + self.dpd_rpc = yamlrpc.Socket(bind_port=0) @cherrypy.expose def index(self): @@ -71,38 +73,66 @@ class API: try: self.mod_rc.set_param_value(params['controllable'], params['param'], params['value']) except IOError as e: + cherrypy.response.status = 503 return send_error(str(e)) except ValueError as e: - cherrypy.response.status = 400 + cherrypy.response.status = 503 return send_error(str(e)) return send_ok() else: cherrypy.response.status = 400 return send_error("POST only") + def _wrap_dpd(self, method, data=None): + try: + reply = self.dpd_rpc.call_rpc_method(self.dpd_port, method, data) + return send_ok(reply) + except ValueError as e: + cherrypy.response.status = 503 + return send_error("YAML-RPC call error: {}".format(e)) + except TimeoutError as e: + cherrypy.response.status = 503 + return send_error("YAML-RPC timeout: {}".format(e)) + cherrypy.response.status = 500 + return send_error("YAML-RPC unknown error") + @cherrypy.expose @cherrypy.tools.json_out() - def trigger_capture(self, **kwargs): + def dpd_trigger_run(self, **kwargs): if cherrypy.request.method == 'POST': - # TODO dpd send capture - return send_ok() + return self._wrap_dpd("trigger_run") else: cherrypy.response.status = 400 return send_error("POST only") @cherrypy.expose @cherrypy.tools.json_out() - def dpd_status(self, **kwargs): - # TODO Request DPD state - return send_error("DPD state unknown") + def dpd_reset(self, **kwargs): + if cherrypy.request.method == 'POST': + return self._wrap_dpd("reset") + else: + cherrypy.response.status = 400 + return send_error("POST only") @cherrypy.expose @cherrypy.tools.json_out() - def calibrate(self, **kwargs): + def dpd_settings(self, setting: str, value: str, **kwargs): if cherrypy.request.method == 'POST': - # TODO dpd send capture - return send_ok() + data = {'setting': setting, 'value': value} + return self._wrap_dpd("set_setting", data) + else: + return self._wrap_dpd("get_settings") + + @cherrypy.expose + @cherrypy.tools.json_out() + def dpd_results(self, **kwargs): + return self._wrap_dpd("get_results") + + @cherrypy.expose + @cherrypy.tools.json_out() + def dpd_calibrate(self, **kwargs): + if cherrypy.request.method == 'POST': + return self._wrap_dpd("calibrate") else: - # Fetch dpd status - return send_error("DPD calibration result unknown") + return self._wrap_dpd("get_calibration_result") diff --git a/python/gui/static/js/odr-predistortion.js b/python/gui/static/js/odr-predistortion.js index 7a93b2b..b2f1d22 100644 --- a/python/gui/static/js/odr-predistortion.js +++ b/python/gui/static/js/odr-predistortion.js @@ -18,6 +18,21 @@ // You should have received a copy of the GNU General Public License // along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>. +function resultrefresh() { + var jqxhr = doApiRequestGET("/api/dpd_results", function(data) { + $('#dpdresults').text(data['summary']); + }); + + jqxhr.always(function() { + setTimeout(resultrefresh, 2000); + }); +} + +$(function(){ + setTimeout(resultrefresh, 2000); +}); + +/* function calibraterefresh() { doApiRequestGET("/api/calibrate", function(data) { var text = "Captured TX signal and feedback." + @@ -92,6 +107,8 @@ $(function(){ }); }); +*/ + // ToolTip init $(function(){ diff --git a/python/gui/static/js/odr.js b/python/gui/static/js/odr.js index ecb02c5..0bf7729 100644 --- a/python/gui/static/js/odr.js +++ b/python/gui/static/js/odr.js @@ -20,7 +20,7 @@ function doApiRequestGET(uri, callback) { - $.ajax({ + return $.ajax({ type: "GET", url: uri, contentType: 'application/json', @@ -32,6 +32,8 @@ function doApiRequestGET(uri, callback) { errorWindow.document.write(data.responseText); } else { + console.log(data.responseText); + $.gritter.add({ title: 'API', text: "AJAX failed: " + data.statusText, image: '/fonts/warning.png', @@ -56,7 +58,7 @@ function doApiRequestGET(uri, callback) { } function doApiRequestPOST(uri, data, callback) { - $.ajax({ + return $.ajax({ type: "POST", url: uri, contentType: 'application/json', @@ -65,10 +67,14 @@ function doApiRequestPOST(uri, data, callback) { error: function(data) { if (data.status == 500) { - var errorWindow = window.open("", "_self"); + var windowObjectReference; + var winFeatures = "menubar=yes,location=yes,resizable=yes,scrollbars=yes,status=yes"; + var errorWindow = window.open("", "Error 500", winFeatures); errorWindow.document.write(data.responseText); } else { + console.log(data.responseText); + $.gritter.add({ title: 'API', text: "AJAX failed: " + data.statusText, @@ -100,10 +106,10 @@ function setRc(controllable, param, value, callback) { param: param, value: value }; - doApiRequestPOST("/api/parameter/", data, callback); + return doApiRequestPOST("/api/parameter/", data, callback); } function getRc(callback) { - doApiRequestGET("/api/rc_parameters", callback); + return doApiRequestGET("/api/rc_parameters", callback); } diff --git a/python/gui/templates/predistortion.html b/python/gui/templates/predistortion.html index 2ebf7ea..ac68537 100644 --- a/python/gui/templates/predistortion.html +++ b/python/gui/templates/predistortion.html @@ -29,6 +29,15 @@ along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>. <div class="container-fluid"> <div class="panel-group"> <div class="panel panel-default"> + <div class="panel-heading">Status</div> + <div class="panel-body"> + <div>Current DPDCE status: + <div class="well well-sm" id="dpdresults">N/A<div> + </div> + </div> + </div> + <!-- + <div class="panel panel-default"> <div class="panel-heading">Calibration</div> <div class="panel-body"> <div>Calibration needs to be done once before the PA model @@ -55,7 +64,6 @@ along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>. </div> </div> - <!-- <div class="panel panel-default"> <div class="panel-heading">Capture</div> <div class="panel-body"> diff --git a/python/lib/yamlrpc.py b/python/lib/yamlrpc.py index bd61569..d963601 100644 --- a/python/lib/yamlrpc.py +++ b/python/lib/yamlrpc.py @@ -30,6 +30,11 @@ import yaml import socket import struct +class ResponseError(Exception): + """The response contains an error""" + def __init__(self, message): + self.message = message + def request(request_id: int, method: str, params) -> bytes: r = { 'yamlrpc': YAMLRPC_VERSION, @@ -42,14 +47,12 @@ def response_success(request_id: int, result) -> bytes: r = { 'yamlrpc': YAMLRPC_VERSION, 'result': result, - 'error': None, 'id': request_id} return yaml.dump(r).encode() def response_error(request_id: int, error) -> bytes: r = { 'yamlrpc': YAMLRPC_VERSION, - 'result': None, 'error': error, 'id': request_id} return yaml.dump(r).encode() @@ -66,9 +69,58 @@ class Socket: self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) if bind_port > 0: self.socket.bind(('127.0.0.1', bind_port)) + self.socket.settimeout(3) + self._last_request_id = 0 + + def send_request(self, dest_port: int, method: str, params) -> int: + addr = ("127.0.0.1", dest_port) + self._last_request_id += 1 + self.socket.sendto(request(self._last_request_id, method, params), addr) + return self._last_request_id + + def receive_response(self, expected_msg_id: int): + try: + data, addr = self.socket.recvfrom(512) + except socket.timeout as to: + raise TimeoutError("Timeout: " + str(to)) + + y = yaml.load(data.decode()) + + if 'yamlrpc' not in y: + raise ValueError("Message is not yamlrpc") + if y['yamlrpc'] != YAMLRPC_VERSION: + raise ValueError("Invalid yamlrpc version") + + # expect a response, with either 'error' or 'result' non-null + try: + msg_id = y['id'] + except KeyError: + raise ValueError("Response is missing id") + + if msg_id != expected_msg_id: + raise ValueError("Response id does not match request") + + try: + result = y['result'] + except KeyError: + try: + error = y['error'] + raise ResponseError(error) + except KeyError: + raise ValueError("response is null") + return result + + def call_rpc_method(self, dest_port: int, method: str, params): + msg_id = self.send_request(dest_port, method, params) + return self.receive_response(msg_id) + def receive_request(self): - data, addr = self.socket.recvfrom(512) + try: + data, addr = self.socket.recvfrom(512) + except socket.timeout as to: + raise TimeoutError("Timeout: " + str(to)) + y = yaml.load(data.decode()) if 'yamlrpc' not in y: diff --git a/python/poly.coef b/python/poly.coef new file mode 100644 index 0000000..248d316 --- /dev/null +++ b/python/poly.coef @@ -0,0 +1,12 @@ +1 +5 +1.0 +0.0 +0.0 +0.0 +0.0 +0.0 +0.0 +0.0 +0.0 +0.0 |