From ee435c029eac59e0399dc3ae765cc74d66b9442e Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Wed, 28 Nov 2018 09:38:05 +0100 Subject: GUI: Use cherry bus to communicate internally --- gui/api/__init__.py | 55 +++++++++++++++++++++++++------------- gui/dpd/Capture.py | 30 ++++++++++++++++++++- gui/dpd/__init__.py | 20 +++++++++----- gui/run.py | 21 +++++++++++++-- gui/static/js/odr-predistortion.js | 29 +++++++++++++++++--- gui/templates/predistortion.html | 17 +++++++----- 6 files changed, 134 insertions(+), 38 deletions(-) diff --git a/gui/api/__init__.py b/gui/api/__init__.py index cef81c6..a535eb3 100755 --- a/gui/api/__init__.py +++ b/gui/api/__init__.py @@ -20,8 +20,8 @@ # You should have received a copy of the GNU General Public License # along with ODR-DabMod. If not, see . -import json import cherrypy +from cherrypy.process import wspbus, plugins from cherrypy.lib.httputil import parse_query_string import urllib @@ -30,30 +30,47 @@ import os import io import datetime -def send_ok(data): - return json.dumps({'status' : 'ok', 'data': data}).encode() +def send_ok(data=None): + if data is not None: + return {'status' : 'ok', 'data': data} + else: + return {'status': 'ok'} def send_error(reason=""): - return json.dumps({'status' : 'error', 'reason': reason}).encode() + if reason: + return {'status' : 'error', 'reason': reason} + else: + return {'status' : 'error'} -class API: - def __init__(self, mod_rc, dpd): +class API(plugins.SimplePlugin): + def __init__(self, mod_rc, bus): + plugins.SimplePlugin.__init__(self, bus) self.mod_rc = mod_rc - self.dpd = dpd + self.dpd_state = None + + def start(self): + self.bus.subscribe("dpd-state", self.dpd_state) + + def stop(self): + self.bus.unsubscribe("dpd-state", self.dpd_state) + + def dpd_state(self, new_state): + print("API got new dpd-state {}".format(new_state)) + self.dpd_state = new_state @cherrypy.expose def index(self): return """This is the api area.""" @cherrypy.expose + @cherrypy.tools.json_out() def rc_parameters(self): - cherrypy.response.headers["Content-Type"] = "application/json" return send_ok(self.mod_rc.get_modules()) @cherrypy.expose + @cherrypy.tools.json_out() def parameter(self, **kwargs): if cherrypy.request.method == 'POST': - cherrypy.response.headers["Content-Type"] = "application/json" cl = cherrypy.request.headers['Content-Length'] rawbody = cherrypy.request.body.read(int(cl)) params = json.loads(rawbody) @@ -62,26 +79,26 @@ class API: except ValueError as e: cherrypy.response.status = 400 return send_error(str(e)) - return send_ok(None) + return send_ok() else: - cherrypy.response.headers["Content-Type"] = "application/json" cherrypy.response.status = 400 return send_error("POST only") @cherrypy.expose + @cherrypy.tools.json_out() def trigger_capture(self, **kwargs): if cherrypy.request.method == 'POST': - cherrypy.response.headers["Content-Type"] = "application/json" - try: - return send_ok(self.dpd.capture_samples()) - except ValueError as e: - return send_error(str(e)) + cherrypy.engine.publish('dpd-capture', None) + return send_ok() else: - cherrypy.response.headers["Content-Type"] = "application/json" cherrypy.response.status = 400 return send_error("POST only") @cherrypy.expose + @cherrypy.tools.json_out() def dpd_status(self, **kwargs): - cherrypy.response.headers["Content-Type"] = "application/json" - return send_ok(self.dpd.status()) + if self.dpd_state is not None: + return send_ok(self.dpd_state) + else: + return send_error("DPD state unknown") + diff --git a/gui/dpd/Capture.py b/gui/dpd/Capture.py index de428cb..e2ac63d 100644 --- a/gui/dpd/Capture.py +++ b/gui/dpd/Capture.py @@ -29,6 +29,10 @@ import os import logging import numpy as np from scipy import signal +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +import io from . import Align as sa @@ -77,10 +81,11 @@ class Capture: self.binning_n_bins = 64 # Number of bins between binning_start and binning_end self.binning_n_per_bin = 128 # Number of measurements pre bin - self.target_median = 0.01 + self.target_median = 0.05 self.median_max = self.target_median * 1.4 self.median_min = self.target_median / 1.4 + # axis 0: bins # axis 1: 0=tx, 1=rx self.accumulated_bins = [np.zeros((0, 2), dtype=np.complex64) for i in range(self.binning_n_bins)] @@ -179,6 +184,29 @@ class Capture: def bin_histogram(self): return [b.shape[0] for b in self.accumulated_bins] + def pointcloud_png(self): + fig = plt.figure() + ax = plt.subplot(1, 1, 1) + for b in self.accumulated_bins: + if b: + ax.scatter( + np.abs(b[0]), + np.abs(b[1]), + s=0.1, + color="black") + ax.set_title("Captured and Binned Samples") + ax.set_xlabel("TX Amplitude") + ax.set_ylabel("RX Amplitude") + ax.set_ylim(0, 0.8) + ax.set_xlim(0, 1.1) + ax.legend(loc=4) + fig.tight_layout() + buf = io.BytesIO() + fig.savefig(buf) + plt.close(fig) + + return buf.getvalue() + def _bin_and_accumulate(self, txframe, rxframe): """Bin the samples and extend the accumulated samples""" diff --git a/gui/dpd/__init__.py b/gui/dpd/__init__.py index 8dd0807..06d180d 100644 --- a/gui/dpd/__init__.py +++ b/gui/dpd/__init__.py @@ -47,19 +47,25 @@ class DPD: r['capture'] = self.last_capture_info return r + def pointcloud_png(self): + return self.capture.pointcloud_png() + def capture_samples(self): """Captures samples and store them in the accumulated samples, returns a dict with some info""" + result = {} try: txframe_aligned, tx_ts, tx_median, rxframe_aligned, rx_ts, rx_median = self.capture.get_samples() - self.last_capture_info['length'] = len(txframe_aligned) - self.last_capture_info['tx_median'] = float(tx_median) - self.last_capture_info['rx_median'] = float(rx_median) - self.last_capture_info['tx_ts'] = tx_ts - self.last_capture_info['rx_ts'] = rx_ts - return self.last_capture_info + result['status'] = "ok" + result['length'] = len(txframe_aligned) + result['tx_median'] = float(tx_median) + result['rx_median'] = float(rx_median) + result['tx_ts'] = tx_ts + result['rx_ts'] = rx_ts except ValueError as e: - raise ValueError("Capture failed: {}".format(e)) + result['status'] = "Capture failed: {}".format(e) + + self.last_capture_info = result # tx, rx, phase_diff, n_per_bin = extStat.extract(txframe_aligned, rxframe_aligned) # off = SA.calc_offset(txframe_aligned) diff --git a/gui/run.py b/gui/run.py index 3646b2c..b4af742 100755 --- a/gui/run.py +++ b/gui/run.py @@ -24,6 +24,7 @@ import configuration import os.path import cherrypy +from cherrypy.process import wspbus, plugins import argparse from jinja2 import Environment, FileSystemLoader from api import API @@ -39,8 +40,8 @@ class Root: self.config_file = config_file self.conf = configuration.Configuration(self.config_file) self.mod_rc = zmqrc.ModRemoteControl("localhost") - self.dpd = dpd.DPD() - self.api = API(self.mod_rc, self.dpd) + self.api = API(self.mod_rc, cherrypy.engine) + self.api.subscribe() @cherrypy.expose def index(self): @@ -74,6 +75,20 @@ class Root: js = base_js + ["js/odr-predistortion.js"] return tmpl.render(tab='predistortion', js=js, is_login=False) +class DPDPlugin(plugins.SimplePlugin): + def __init__(self, bus): + plugins.SimplePlugin.__init__(self, bus) + self.dpd = dpd.DPD() + + def start(self): + self.bus.subscribe("dpd-capture", self.trigger_capture) + + def stop(self): + self.bus.unsubscribe("dpd-capture", self.trigger_capture) + + def trigger_capture(self, param): + print("trigger_capture({})".format(param)) + if __name__ == '__main__': parser = argparse.ArgumentParser(description='ODR-DabMod Web GUI') parser.add_argument('-c', '--config', @@ -107,6 +122,8 @@ if __name__ == '__main__': staticdir = os.path.realpath(config.config['global']['static_directory']) + DPDPlugin(cherrypy.engine).subscribe() + cherrypy.tree.mount( Root(cli_args.config), config={ '/': { }, diff --git a/gui/static/js/odr-predistortion.js b/gui/static/js/odr-predistortion.js index 6b09202..4e86f1c 100644 --- a/gui/static/js/odr-predistortion.js +++ b/gui/static/js/odr-predistortion.js @@ -22,9 +22,6 @@ $(function(){ $('#capturebutton').click(function() { doApiRequestPOST("/api/trigger_capture", {}, function(data) { console.log("trigger_capture succeeded: " + JSON.stringify(data)); - $('#capturelength').text(data.length); - $('#tx_median').text(data.tx_median); - $('#rx_median').text(data.rx_median); }); }); @@ -32,7 +29,33 @@ $(function(){ doApiRequestGET("/api/dpd_status", function(data) { console.log("dpd_status succeeded: " + JSON.stringify(data)); $('#histogram').text(data.histogram); + $('#capturestatus').text(data.capture.status); + $('#capturelength').text(data.capture.length); + $('#tx_median').text(data.capture.tx_median); + $('#rx_median').text(data.capture.rx_median); }); + + $.ajax({ + type: "GET", + url: "/api/dpd_capture_pointcloud", + + error: function(data) { + if (data.status == 500) { + var errorWindow = window.open("", "_self"); + errorWindow.document.write(data.responseText); + } + else { + $.gritter.add({ title: 'API', + text: "AJAX failed: " + data.statusText, + image: '/fonts/warning.png', + sticky: true, + }); + } + }, + success: function(data) { + $('#dpd_pointcloud').value(data) + } + }) }); }); diff --git a/gui/templates/predistortion.html b/gui/templates/predistortion.html index 8d5f1a5..ed224d8 100644 --- a/gui/templates/predistortion.html +++ b/gui/templates/predistortion.html @@ -31,19 +31,24 @@ along with ODR-DabMod. If not, see .

Capture

-
Number of samples captured: None
-
TX median: N/A
-
RX median: N/A
-
On pressing this button, the DPDCE will trigger a capture and a quick data analysis, without updating any DPD models.
- +

Status

- +
Histogram: N/A
+
Capture status + N/A
+
Number of samples captured: + None
+
TX median: N/A
+
RX median: N/A
+
Point cloud:
-- cgit v1.2.3