diff options
author | Matthias P. Braendli <matthias.braendli@mpb.li> | 2018-12-04 16:45:58 +0100 |
---|---|---|
committer | Matthias P. Braendli <matthias.braendli@mpb.li> | 2018-12-04 16:45:58 +0100 |
commit | 5cf52c74e9eb6bf8a82af4509ff3eb5106f928f9 (patch) | |
tree | a7edc1dfd2b2f4469f4dc4d760fdfa83a25fa710 /python/gui | |
parent | d5cbe10c0e2298b0e40161607a3da158249bdb82 (diff) | |
download | dabmod-5cf52c74e9eb6bf8a82af4509ff3eb5106f928f9.tar.gz dabmod-5cf52c74e9eb6bf8a82af4509ff3eb5106f928f9.tar.bz2 dabmod-5cf52c74e9eb6bf8a82af4509ff3eb5106f928f9.zip |
Rework GUI and DPDCE
Diffstat (limited to 'python/gui')
-rw-r--r-- | python/gui/README.md | 45 | ||||
-rw-r--r-- | python/gui/__init__.py | 1 | ||||
-rwxr-xr-x | python/gui/api.py (renamed from python/gui/api/__init__.py) | 54 | ||||
-rw-r--r-- | python/gui/configuration.py | 44 | ||||
-rwxr-xr-x | python/gui/run.py | 166 | ||||
-rw-r--r-- | python/gui/ui-config.json | 9 | ||||
-rw-r--r-- | python/gui/zmqrc.py | 84 |
7 files changed, 16 insertions, 387 deletions
diff --git a/python/gui/README.md b/python/gui/README.md deleted file mode 100644 index ec55bc9..0000000 --- a/python/gui/README.md +++ /dev/null @@ -1,45 +0,0 @@ -ODR-DabMod Web UI -================= - -Goals ------ - -Enable users to play with digital predistortion settings, through a -visualisation of the settings and the parameters. - -Make it easier to discover the tuning possibilities of the modulator. - - -Install -------- - -Install dependencies: cherrypy, jinja2, scipy, matplotlib, zmq python modules - -Run ---- - -1. Execute ODR-DabMod, configured with zmq rc on port 9400 -1. `cd gui` -1. `./run.py` -1. Connect your browser to `http://localhost:8099` - -Todo ----- - -* Integrate DPDCE - * Show DPD settings and effect visually - * Allow load/store of DPD settings - * Make ports configurable -* Use Feedback Server interface and make spectrum and constellation plots -* Get authentication to work -* Read and write config file, and add forms to change ODR-DabMod configuration -* Connect to supervisord to be able to restart ODR-DabMod -* Create a status page - * Is process running? - * Is modulator rate within bounds? - * Are there underruns or late packets? - * Is the GPSDO ok? (needs new RC params) -* Think about how to show PA settings - * Return loss is an important metric - * Some PAs offer serial interfaces for supervision - diff --git a/python/gui/__init__.py b/python/gui/__init__.py new file mode 100644 index 0000000..725f20d --- /dev/null +++ b/python/gui/__init__.py @@ -0,0 +1 @@ +#gui module diff --git a/python/gui/api/__init__.py b/python/gui/api.py index 77faa10..ae54e8b 100755 --- a/python/gui/api/__init__.py +++ b/python/gui/api.py @@ -23,6 +23,8 @@ import cherrypy from cherrypy.lib.httputil import parse_query_string +from lib import yamlrpc + import json import urllib import os @@ -43,36 +45,9 @@ def send_error(reason=""): else: return {'status' : 'error'} -class RXThread(threading.Thread): - def __init__(self, api): - super(RXThread, self).__init__() - self.api = api - self.running = False - self.daemon = True - - def cancel(self): - self.running = False - - def run(self): - self.running = True - while self.running: - if self.api.dpd_pipe.poll(1): - rx = self.api.dpd_pipe.recv() - if rx['cmd'] == "quit": - break - elif rx['cmd'] == "dpd-state": - self.api.dpd_state = rx['data'] - elif rx['cmd'] == "dpd-calibration-result": - self.api.calibration_result = rx['data'] - class API: - def __init__(self, mod_rc, dpd_pipe): + def __init__(self, mod_rc): self.mod_rc = mod_rc - self.dpd_pipe = dpd_pipe - self.dpd_state = None - self.calibration_result = None - self.receive_thread = RXThread(self) - self.receive_thread.start() @cherrypy.expose def index(self): @@ -81,7 +56,10 @@ class API: @cherrypy.expose @cherrypy.tools.json_out() def rc_parameters(self): - return send_ok(self.mod_rc.get_modules()) + try: + return send_ok(self.mod_rc.get_modules()) + except IOError as e: + return send_error(str(e)) @cherrypy.expose @cherrypy.tools.json_out() @@ -92,6 +70,8 @@ class API: params = json.loads(rawbody.decode()) try: self.mod_rc.set_param_value(params['controllable'], params['param'], params['value']) + except IOError as e: + return send_error(str(e)) except ValueError as e: cherrypy.response.status = 400 return send_error(str(e)) @@ -104,7 +84,7 @@ class API: @cherrypy.tools.json_out() def trigger_capture(self, **kwargs): if cherrypy.request.method == 'POST': - self.dpd_pipe.send({'cmd': "dpd-capture"}) + # TODO dpd send capture return send_ok() else: cherrypy.response.status = 400 @@ -113,20 +93,16 @@ class API: @cherrypy.expose @cherrypy.tools.json_out() def dpd_status(self, **kwargs): - if self.dpd_state is not None: - return send_ok(self.dpd_state) - else: - return send_error("DPD state unknown") + # TODO Request DPD state + return send_error("DPD state unknown") @cherrypy.expose @cherrypy.tools.json_out() def calibrate(self, **kwargs): if cherrypy.request.method == 'POST': - self.dpd_pipe.send({'cmd': "dpd-calibrate"}) + # TODO dpd send capture return send_ok() else: - if self.calibration_result is not None: - return send_ok(self.calibration_result) - else: - return send_error("DPD calibration result unknown") + # Fetch dpd status + return send_error("DPD calibration result unknown") diff --git a/python/gui/configuration.py b/python/gui/configuration.py deleted file mode 100644 index 4af1abd..0000000 --- a/python/gui/configuration.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copyright (C) 2018 -# Matthias P. Braendli, matthias.braendli@mpb.li -# -# http://www.opendigitalradio.org -# -# This file is part of ODR-DabMod. -# -# ODR-DabMod 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. -# -# ODR-DabMod 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 ODR-DabMod. If not, see <http://www.gnu.org/licenses/>. -import json - -class ConfigurationNotPresent: - pass - -class Configuration: - def __init__(self, configfilename="ui-config.json"): - self.config = None - - try: - fd = open(configfilename, "r") - self.config = json.load(fd) - except json.JSONDecodeError: - pass - except OSError: - pass - - def get_key(self, key): - if self.config is None: - raise ConfigurationNotPresent() - else: - return self.config[key] diff --git a/python/gui/run.py b/python/gui/run.py deleted file mode 100755 index b83dd14..0000000 --- a/python/gui/run.py +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copyright (C) 2018 -# Matthias P. Braendli, matthias.braendli@mpb.li -# -# http://www.opendigitalradio.org -# -# This file is part of ODR-DabMod. -# -# ODR-DabMod 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. -# -# ODR-DabMod 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 ODR-DabMod. If not, see <http://www.gnu.org/licenses/>. - -import configuration -from multiprocessing import Process, Pipe -import os.path -import cherrypy -import argparse -from jinja2 import Environment, FileSystemLoader -from api import API -import zmqrc -import dpd - -env = Environment(loader=FileSystemLoader('templates')) - -base_js = ["js/odr.js"] - -class Root: - def __init__(self, config_file, dpd_pipe): - self.config_file = config_file - self.conf = configuration.Configuration(self.config_file) - self.mod_rc = zmqrc.ModRemoteControl("localhost") - self.api = API(self.mod_rc, dpd_pipe) - - @cherrypy.expose - def index(self): - raise cherrypy.HTTPRedirect('/home') - - @cherrypy.expose - def about(self): - tmpl = env.get_template("about.html") - return tmpl.render(tab='about', js=base_js, is_login=False) - - @cherrypy.expose - def home(self): - tmpl = env.get_template("home.html") - return tmpl.render(tab='home', js=base_js, is_login=False) - - @cherrypy.expose - def rcvalues(self): - tmpl = env.get_template("rcvalues.html") - js = base_js + ["js/odr-rcvalues.js"] - return tmpl.render(tab='rcvalues', js=js, is_login=False) - - @cherrypy.expose - def modulator(self): - tmpl = env.get_template("modulator.html") - js = base_js + ["js/odr-modulator.js"] - return tmpl.render(tab='modulator', js=js, is_login=False) - - @cherrypy.expose - def predistortion(self): - tmpl = env.get_template("predistortion.html") - js = base_js + ["js/odr-predistortion.js"] - return tmpl.render(tab='predistortion', js=js, is_login=False) - -class DPDRunner: - def __init__(self, static_dir): - self.web_end, self.dpd_end = Pipe() - self.dpd = dpd.DPD(static_dir) - - def __enter__(self): - self.p = Process(target=self._handle_messages) - self.p.start() - return self.web_end - - def _handle_messages(self): - while True: - rx = self.dpd_end.recv() - if rx['cmd'] == "quit": - break - elif rx['cmd'] == "dpd-capture": - self.dpd.capture_samples() - elif rx['cmd'] == "dpd-calibrate": - self.dpd_end.send({'cmd': "dpd-calibration-result", - 'data': self.dpd.capture_calibration()}) - - - def __exit__(self, exc_type, exc_value, traceback): - self.web_end.send({'cmd': "quit"}) - self.p.join() - return False - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='ODR-DabMod Web GUI') - parser.add_argument('-c', '--config', - default="ui-config.json", - help='configuration filename') - cli_args = parser.parse_args() - - config = configuration.Configuration(cli_args.config) - if config.config is None: - print("Configuration file is missing or is not readable - {}".format(cli_args.config)) - sys.exit(1) - - if config.config['global']['daemon']: - cherrypy.process.plugins.Daemonizer(cherrypy.engine).subscribe() - - accesslog = os.path.realpath(os.path.join(config.config['global']['logs_directory'], 'access.log')) - errorlog = os.path.realpath(os.path.join(config.config['global']['logs_directory'], 'error.log')) - - cherrypy.config.update({ - 'engine.autoreload.on': True, - 'server.socket_host': config.config['global']['host'], - 'server.socket_port': int(config.config['global']['port']), - 'request.show_tracebacks' : True, - 'tools.sessions.on': False, - 'tools.encode.on': True, - 'tools.encode.encoding': "utf-8", - 'log.access_file': accesslog, - 'log.error_file': errorlog, - 'log.screen': True, - }) - - staticdir = os.path.realpath(config.config['global']['static_directory']) - - with DPDRunner(os.path.join(staticdir, "dpd")) as dpd_pipe: - cherrypy.tree.mount( - Root(cli_args.config, dpd_pipe), config={ - '/': { }, - '/dpd': { - 'tools.staticdir.on': True, - 'tools.staticdir.dir': os.path.join(staticdir, u"dpd/") - }, - '/css': { - 'tools.staticdir.on': True, - 'tools.staticdir.dir': os.path.join(staticdir, u"css/") - }, - '/js': { - 'tools.staticdir.on': True, - 'tools.staticdir.dir': os.path.join(staticdir, u"js/") - }, - '/fonts': { - 'tools.staticdir.on': True, - 'tools.staticdir.dir': os.path.join(staticdir, u"fonts/") - }, - '/favicon.ico': { - 'tools.staticfile.on': True, - 'tools.staticfile.filename': os.path.join(staticdir, u"fonts/favicon.ico") - }, - } - ) - - cherrypy.engine.start() - cherrypy.engine.block() - diff --git a/python/gui/ui-config.json b/python/gui/ui-config.json deleted file mode 100644 index 070290c..0000000 --- a/python/gui/ui-config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "global": { - "daemon": false, - "host": "127.0.0.1", - "port": "8099", - "logs_directory": "logs", - "static_directory": "static" - } -} diff --git a/python/gui/zmqrc.py b/python/gui/zmqrc.py deleted file mode 100644 index 3897d7a..0000000 --- a/python/gui/zmqrc.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copyright (C) 2018 -# Matthias P. Braendli, matthias.braendli@mpb.li -# -# http://www.opendigitalradio.org -# -# This file is part of ODR-DabMod. -# -# ODR-DabMod 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. -# -# ODR-DabMod 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 ODR-DabMod. If not, see <http://www.gnu.org/licenses/>. -import zmq -import json - -class ModRemoteControl(object): - """Interact with ODR-DabMod using the ZMQ RC""" - def __init__(self, mod_host, mod_port=9400): - self._host = mod_host - self._port = mod_port - self._ctx = zmq.Context() - - def _read(self, message_parts): - sock = zmq.Socket(self._ctx, zmq.REQ) - sock.setsockopt(zmq.LINGER, 0) - sock.connect("tcp://{}:{}".format(self._host, self._port)) - - for i, part in enumerate(message_parts): - if i == len(message_parts) - 1: - f = 0 - else: - f = zmq.SNDMORE - sock.send(part.encode(), flags=f) - - # use poll for timeouts: - poller = zmq.Poller() - poller.register(sock, zmq.POLLIN) - if poller.poll(5*1000): # 5s timeout in milliseconds - recv = sock.recv_multipart() - sock.close() - return [r.decode() for r in recv] - else: - raise IOError("Timeout processing ZMQ request") - - def get_modules(self): - modules = {} - - for mod in [json.loads(j) for j in self._read(['list'])]: - params = {} - pv_list = self._read(['show', mod['name']]) - - for pv in pv_list: - p, _, v = pv.partition(": ") - params[p] = {"value": v.strip()} - - for p in mod['params']: - if p in params: - params[p]["help"] = mod['params'][p] - modules[mod['name']] = params - - return modules - - def get_param_value(self, module, param): - value = self._read(['get', module, param]) - if value[0] == 'fail': - raise ValueError("Error getting param: {}".format(value[1])) - else: - return value[0] - - def set_param_value(self, module, param, value): - ret = self._read(['set', module, param, value]) - if ret[0] == 'fail': - raise ValueError("Error setting param: {}".format(ret[1])) - |