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])) - | 
