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 | |
parent | d5cbe10c0e2298b0e40161607a3da158249bdb82 (diff) | |
download | dabmod-5cf52c74e9eb6bf8a82af4509ff3eb5106f928f9.tar.gz dabmod-5cf52c74e9eb6bf8a82af4509ff3eb5106f928f9.tar.bz2 dabmod-5cf52c74e9eb6bf8a82af4509ff3eb5106f928f9.zip |
Rework GUI and DPDCE
Diffstat (limited to 'python')
-rw-r--r-- | python/README.md (renamed from python/dpd/README.md) | 80 | ||||
-rw-r--r-- | python/dpd.ini (renamed from python/dpd/dpd.ini) | 0 | ||||
-rw-r--r-- | python/dpd/Adapt.py (renamed from python/dpd/src/Adapt.py) | 8 | ||||
-rw-r--r-- | python/dpd/Dab_Util.py (renamed from python/dpd/src/Dab_Util.py) | 4 | ||||
-rw-r--r-- | python/dpd/ExtractStatistic.py (renamed from python/dpd/src/ExtractStatistic.py) | 0 | ||||
-rw-r--r-- | python/dpd/GlobalConfig.py (renamed from python/dpd/src/GlobalConfig.py) | 21 | ||||
-rw-r--r-- | python/dpd/Heuristics.py (renamed from python/dpd/src/Heuristics.py) | 0 | ||||
-rw-r--r-- | python/dpd/MER.py (renamed from python/dpd/src/MER.py) | 0 | ||||
-rw-r--r-- | python/dpd/Measure.py (renamed from python/dpd/src/Measure.py) | 2 | ||||
-rw-r--r-- | python/dpd/Measure_Shoulders.py (renamed from python/dpd/src/Measure_Shoulders.py) | 0 | ||||
-rw-r--r-- | python/dpd/Model.py (renamed from python/dpd/src/Model.py) | 4 | ||||
-rw-r--r-- | python/dpd/Model_AM.py (renamed from python/dpd/src/Model_AM.py) | 0 | ||||
-rw-r--r-- | python/dpd/Model_Lut.py (renamed from python/dpd/src/Model_Lut.py) | 0 | ||||
-rw-r--r-- | python/dpd/Model_PM.py (renamed from python/dpd/src/Model_PM.py) | 0 | ||||
-rw-r--r-- | python/dpd/Model_Poly.py (renamed from python/dpd/src/Model_Poly.py) | 4 | ||||
-rw-r--r-- | python/dpd/RX_Agc.py (renamed from python/dpd/src/RX_Agc.py) | 6 | ||||
-rw-r--r-- | python/dpd/Symbol_align.py (renamed from python/dpd/src/Symbol_align.py) | 0 | ||||
-rw-r--r-- | python/dpd/TX_Agc.py (renamed from python/dpd/src/TX_Agc.py) | 0 | ||||
-rw-r--r-- | python/dpd/__init__.py | 1 | ||||
-rw-r--r-- | python/dpd/lut.coef | 64 | ||||
-rwxr-xr-x | python/dpd/old/apply_adapt_dumps.py (renamed from python/dpd/apply_adapt_dumps.py) | 0 | ||||
-rwxr-xr-x | python/dpd/old/iq_file_server.py (renamed from python/dpd/iq_file_server.py) | 0 | ||||
-rwxr-xr-x | python/dpd/old/main.py (renamed from python/dpd/main.py) | 0 | ||||
-rwxr-xr-x | python/dpd/old/show_spectrum.py (renamed from python/dpd/show_spectrum.py) | 0 | ||||
-rwxr-xr-x | python/dpd/old/store_received.py (renamed from python/dpd/store_received.py) | 0 | ||||
-rw-r--r-- | python/dpd/phase_align.py (renamed from python/dpd/src/phase_align.py) | 0 | ||||
-rw-r--r-- | python/dpd/poly.coef | 12 | ||||
-rw-r--r-- | python/dpd/src/__init__.py | 0 | ||||
-rwxr-xr-x | python/dpd/subsample_align.py (renamed from python/dpd/src/subsample_align.py) | 0 | ||||
-rwxr-xr-x | python/dpdce.py | 337 | ||||
-rw-r--r-- | python/gui-dpdce.ini | 30 | ||||
-rwxr-xr-x | python/gui.py | 135 | ||||
-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/lib/__init__.py | 1 | ||||
-rw-r--r-- | python/lib/yamlrpc.py | 93 | ||||
-rw-r--r-- | python/lib/zmqrc.py (renamed from python/gui/zmqrc.py) | 0 |
41 files changed, 678 insertions, 443 deletions
diff --git a/python/dpd/README.md b/python/README.md index 307a2f5..2933923 100644 --- a/python/dpd/README.md +++ b/python/README.md @@ -1,10 +1,36 @@ +GUI and DPDCE +============= + +This folder contains a web-based GUI and a DPD computation engine. +The Digital Predistortion Computation Engine and the web GUI can +run independently, and communicate through UDP socket. + +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. + +The Web GUI presents a control interface that connects to ODR-DabMod and the +DPD computation engine. It is the main frontend for the DPDCE. + +Prerequisites: python 3 with CherryPy, Jinja2, `python-zeromq`, `python-yaml` + + Digital Predistortion Computation Engine for ODR-DabMod -======================================================= +------------------------------------------------------- This folder contains a digital predistortion prototype. It was only tested in a laboratory system, and is not ready for production usage. +Prerequisites: python 3 with SciPy, Matplotlib, `python-zeromq`, `python-yaml` + Concept ------- @@ -20,16 +46,16 @@ efficient computation. Its sources reside in the *dpd* folder. The predistorter in ODR-DabMod supports two modes: polynomial and lookup table. In the DPDCE, only the polynomial model is implemented at the moment. -The *dpd/main.py* script is the entry point for the *DPD Computation Engine* -into which these features will be implemented. The tool uses modules from the -*dpd/src/* folder: - - Sample transfer and time alignment with subsample accuracy is done by *Measure.py* - Estimating the effects of the PA using some model and calculation of the updated polynomial coefficients is done in *Model.py* and other specific *Model_XXX.py* files - Finally, *Adapt.py* updates the ODR-DabMod predistortion setting and digital gain -These modules themselves use additional helper scripts in the *dpd/src/* folder. +The DPDCE can be controlled through a UDP interface from the web GUI. + +The *old/main.py* script was the entry point for the *DPD Computation Engine* +stand-alone prototype, used to develop the DPDCE, and is not functional anymore. + Requirements ------------ @@ -44,16 +70,16 @@ Requirements are sufficient (not GPSDO necessary). - A live mux source with TIST enabled. -See dpd/dpd.ini for an example. +See `dpd.ini` for an example. -The DPD server port can be tested with the *dpd/show_spectrum.py* helper tool, which can also display +The DPD server port can be tested with the *show_spectrum.py* helper tool, which can also display a constellation diagram. Hardware Setup -------------- -![setup diagram](img/setup_diagram.svg) -![setup photo](img/setup_photo.svg) +![setup diagram](dpd/img/setup_diagram.svg) +![setup photo](dpd/img/setup_photo.svg) Our setup is depicted in the Figure above. We used components with the following properties: 1. USRP TX (max +20dBm) @@ -94,7 +120,7 @@ Make sure you have a ODR-DabMux running with a TCP output on port 9200. Then run the modulator, with the example dpd configuration file. ``` -./odr-dabmod dpd/dpd.ini +./odr-dabmod dpd.ini ``` This configuration file is different from usual defaults in several respects: @@ -103,30 +129,17 @@ This configuration file is different from usual defaults in several respects: * 4x oversampling: 8192000 sample rate * a very small digital gain, which will be overridden by the DPDCE * predistorter enabled + * enables zmq rc The TX gain should be chosen so that you can drive your amplifier into saturation with a digital gain of 0.1, so that there is margin for the DPD to operate. -You should *not modify txgain, rxgain, digital gain or coefficient settings manually!* +You should *not modify txgain, rxgain, digital gain or coefficient settings in the dpd.ini file!* When the DPDCE is used, it controls these settings, and there are command line options for you to define initial values. -To verify that the communication between the DPDCE and ODR-DabMod is ok, -you can use the status and reset options: - -``` -cd dpd -python main.py --status -python main.py --reset -``` - -The reset option sets all DPD-related settings to the defaults (as shown in the -`--help` usage screen) and stops. - -When neither `--status` nor `--reset` is given, the DPDCE will run the predistortion -algorithm. As a first test you should run the DPDCE with the `--plot` -parameter. It preserves the output power and generates all available +When plotting is enabled, it generates all available visualisation plots in the newly created logging directory `/tmp/dpd_<time_stamp>`. As the predistortion should increase the peak to shoulder ratio, you should select a *txgain* in the ODR-DabMod configuration @@ -134,11 +147,6 @@ file such that the initial peak-to-soulder ratio visible on your spectrum analyser. This way, you will be able to see a the change. -``` -cd dpd -python main.py --plot -``` - The DPDCE now does 10 iterations, and tries to improve the predistortion effectiveness. In each step the learning rate is decreased. The learning rate is the factor with which new coefficients are weighted in a weighted mean with the old @@ -160,16 +168,13 @@ difference has increased on your spectrum analyzer, similar to the figure below. Without digital predistortion: -![shoulder_measurement_before](img/shoulder_measurement_before.png) +![shoulder_measurement_before](dpd/img/shoulder_measurement_before.png) With digital predistortion, computed by the DPDCE: -![shoulder_measurement_after](img/shoulder_measurement_after.png) +![shoulder_measurement_after](dpd/img/shoulder_measurement_after.png) Now see what happens if you apply the predistortions for different TX gains. -You can either set the TX gain before you start the predistortion or using the -command line option `--txgain gain`. You can also try to adjust other -parameters. To see their documentation run `python main.py --help`. File format for coefficients ---------------------------- @@ -262,3 +267,4 @@ Models with memory: Taken from slide 36 of [ECE218C Lecture 15](http://www.ece.ucsb.edu/Faculty/rodwell/Classes/ece218c/notes/Lecture15_Digital%20Predistortion_and_Future%20Challenges.pdf) + diff --git a/python/dpd/dpd.ini b/python/dpd.ini index 31d6140..31d6140 100644 --- a/python/dpd/dpd.ini +++ b/python/dpd.ini diff --git a/python/dpd/src/Adapt.py b/python/dpd/Adapt.py index a57602f..840aee9 100644 --- a/python/dpd/src/Adapt.py +++ b/python/dpd/Adapt.py @@ -66,11 +66,11 @@ class Adapt: ZMQ remote control. """ - def __init__(self, config, port, coef_path): + def __init__(self, port, coef_path, plot_location): logging.debug("Instantiate Adapt object") - self.c = config self.port = port self.coef_path = coef_path + self.plot_location = plot_location self.host = "localhost" self._context = zmq.Context() @@ -238,8 +238,8 @@ class Adapt: """Backup current settings to a file""" dt = datetime.datetime.now().isoformat() if path is None: - if self.c.plot_location is not None: - path = self.c.plot_location + "/" + dt + "_adapt.pkl" + if self.plot_location is not None: + path = self.plot_location + "/" + dt + "_adapt.pkl" else: raise Exception("Cannot dump Adapt without either plot_location or path set") d = { diff --git a/python/dpd/src/Dab_Util.py b/python/dpd/Dab_Util.py index bc89a39..f9ae59f 100644 --- a/python/dpd/src/Dab_Util.py +++ b/python/dpd/Dab_Util.py @@ -13,8 +13,8 @@ import matplotlib matplotlib.use('agg') import matplotlib.pyplot as plt -import src.subsample_align as sa -import src.phase_align as pa +import dpd.subsample_align as sa +import dpd.phase_align as pa from scipy import signal diff --git a/python/dpd/src/ExtractStatistic.py b/python/dpd/ExtractStatistic.py index 639513a..639513a 100644 --- a/python/dpd/src/ExtractStatistic.py +++ b/python/dpd/ExtractStatistic.py diff --git a/python/dpd/src/GlobalConfig.py b/python/dpd/GlobalConfig.py index 56839fc..abd7442 100644 --- a/python/dpd/src/GlobalConfig.py +++ b/python/dpd/GlobalConfig.py @@ -10,11 +10,12 @@ import numpy as np class GlobalConfig: - def __init__(self, cli_args, plot_location): - self.sample_rate = cli_args.samplerate + def __init__(self, args, plot_location: str): + self.sample_rate = args['samplerate'] assert self.sample_rate == 8192000 # By now only constants for 8192000 self.plot_location = plot_location + plot = len(plot_location) > 0 # DAB frame # Time domain @@ -39,7 +40,7 @@ class GlobalConfig: self.phase_offset_per_sample = 1. / self.sample_rate * 2 * np.pi * 1000 # Constants for ExtractStatistic - self.ES_plot = cli_args.plot + self.ES_plot = plot self.ES_start = 0.0 self.ES_end = 1.0 self.ES_n_bins = 64 # Number of bins between ES_start and ES_end @@ -47,7 +48,7 @@ class GlobalConfig: # Constants for Measure_Shoulder self.MS_enable = False - self.MS_plot = cli_args.plot + self.MS_plot = plot meas_offset = 976 # Offset from center frequency to measure shoulder [kHz] meas_width = 100 # Size of frequency delta to measure shoulder [kHz] @@ -65,24 +66,18 @@ class GlobalConfig: self.MS_n_proc = 4 # Constants for MER - self.MER_plot = cli_args.plot + self.MER_plot = plot # Constants for Model - self.MDL_plot = cli_args.plot + self.MDL_plot = plot # Constants for Model_PM # Set all phase offsets to zero for TX amplitude < MPM_tx_min self.MPM_tx_min = 0.1 - # Constants for TX_Agc - self.TAGC_max_txgain = 89 # USRP B200 specific - self.TAGC_tx_median_target = cli_args.target_median - self.TAGC_tx_median_max = self.TAGC_tx_median_target * 1.4 - self.TAGC_tx_median_min = self.TAGC_tx_median_target / 1.4 - # Constants for RX_AGC self.RAGC_min_rxgain = 25 # USRP B200 specific - self.RAGC_rx_median_target = cli_args.target_median + self.RAGC_rx_median_target = 0.05 # The MIT License (MIT) # diff --git a/python/dpd/src/Heuristics.py b/python/dpd/Heuristics.py index 21d400b..21d400b 100644 --- a/python/dpd/src/Heuristics.py +++ b/python/dpd/Heuristics.py diff --git a/python/dpd/src/MER.py b/python/dpd/MER.py index 693058d..693058d 100644 --- a/python/dpd/src/MER.py +++ b/python/dpd/MER.py diff --git a/python/dpd/src/Measure.py b/python/dpd/Measure.py index 6d8007d..36b1888 100644 --- a/python/dpd/src/Measure.py +++ b/python/dpd/Measure.py @@ -9,7 +9,7 @@ import socket import struct import numpy as np -import src.Dab_Util as DU +import dpd.Dab_Util as DU import os import logging diff --git a/python/dpd/src/Measure_Shoulders.py b/python/dpd/Measure_Shoulders.py index fd90050..fd90050 100644 --- a/python/dpd/src/Measure_Shoulders.py +++ b/python/dpd/Measure_Shoulders.py diff --git a/python/dpd/src/Model.py b/python/dpd/Model.py index b2c303f..a8aedde 100644 --- a/python/dpd/src/Model.py +++ b/python/dpd/Model.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from src.Model_Poly import Poly -from src.Model_Lut import Lut +from dpd.Model_Poly import Poly +from dpd.Model_Lut import Lut def select_model_from_dpddata(dpddata): if dpddata[0] == 'lut': diff --git a/python/dpd/src/Model_AM.py b/python/dpd/Model_AM.py index 75b226f..75b226f 100644 --- a/python/dpd/src/Model_AM.py +++ b/python/dpd/Model_AM.py diff --git a/python/dpd/src/Model_Lut.py b/python/dpd/Model_Lut.py index e70fdb0..e70fdb0 100644 --- a/python/dpd/src/Model_Lut.py +++ b/python/dpd/Model_Lut.py diff --git a/python/dpd/src/Model_PM.py b/python/dpd/Model_PM.py index 7b80bf3..7b80bf3 100644 --- a/python/dpd/src/Model_PM.py +++ b/python/dpd/Model_PM.py diff --git a/python/dpd/src/Model_Poly.py b/python/dpd/Model_Poly.py index cdfd319..c8f6135 100644 --- a/python/dpd/src/Model_Poly.py +++ b/python/dpd/Model_Poly.py @@ -9,8 +9,8 @@ import os import logging import numpy as np -import src.Model_AM as Model_AM -import src.Model_PM as Model_PM +import dpd.Model_AM as Model_AM +import dpd.Model_PM as Model_PM def assert_np_float32(x): diff --git a/python/dpd/src/RX_Agc.py b/python/dpd/RX_Agc.py index f778dee..0cc18b8 100644 --- a/python/dpd/src/RX_Agc.py +++ b/python/dpd/RX_Agc.py @@ -14,8 +14,8 @@ import matplotlib matplotlib.use('agg') import matplotlib.pyplot as plt -import src.Adapt as Adapt -import src.Measure as Measure +import dpd.Adapt as Adapt +import dpd.Measure as Measure class Agc: """The goal of the automatic gain control is to set the @@ -59,7 +59,7 @@ class Agc: 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 + rx_median, rx_peak, correction_factor, self.rxgain )) self.adapt.set_rxgain(self.rxgain) diff --git a/python/dpd/src/Symbol_align.py b/python/dpd/Symbol_align.py index 2a17a65..2a17a65 100644 --- a/python/dpd/src/Symbol_align.py +++ b/python/dpd/Symbol_align.py diff --git a/python/dpd/src/TX_Agc.py b/python/dpd/TX_Agc.py index 309193d..309193d 100644 --- a/python/dpd/src/TX_Agc.py +++ b/python/dpd/TX_Agc.py diff --git a/python/dpd/__init__.py b/python/dpd/__init__.py new file mode 100644 index 0000000..9c33361 --- /dev/null +++ b/python/dpd/__init__.py @@ -0,0 +1 @@ +# DPDCE main module diff --git a/python/dpd/lut.coef b/python/dpd/lut.coef deleted file mode 100644 index a198d56..0000000 --- a/python/dpd/lut.coef +++ /dev/null @@ -1,64 +0,0 @@ -2 -4294967296 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 -1 -0 diff --git a/python/dpd/apply_adapt_dumps.py b/python/dpd/old/apply_adapt_dumps.py index 20bc013..20bc013 100755 --- a/python/dpd/apply_adapt_dumps.py +++ b/python/dpd/old/apply_adapt_dumps.py diff --git a/python/dpd/iq_file_server.py b/python/dpd/old/iq_file_server.py index 7a4e570..7a4e570 100755 --- a/python/dpd/iq_file_server.py +++ b/python/dpd/old/iq_file_server.py diff --git a/python/dpd/main.py b/python/dpd/old/main.py index 9ea5a39..9ea5a39 100755 --- a/python/dpd/main.py +++ b/python/dpd/old/main.py diff --git a/python/dpd/show_spectrum.py b/python/dpd/old/show_spectrum.py index f23dba2..f23dba2 100755 --- a/python/dpd/show_spectrum.py +++ b/python/dpd/old/show_spectrum.py diff --git a/python/dpd/store_received.py b/python/dpd/old/store_received.py index 19b735e..19b735e 100755 --- a/python/dpd/store_received.py +++ b/python/dpd/old/store_received.py diff --git a/python/dpd/src/phase_align.py b/python/dpd/phase_align.py index 8654333..8654333 100644 --- a/python/dpd/src/phase_align.py +++ b/python/dpd/phase_align.py diff --git a/python/dpd/poly.coef b/python/dpd/poly.coef deleted file mode 100644 index 248d316..0000000 --- a/python/dpd/poly.coef +++ /dev/null @@ -1,12 +0,0 @@ -1 -5 -1.0 -0.0 -0.0 -0.0 -0.0 -0.0 -0.0 -0.0 -0.0 -0.0 diff --git a/python/dpd/src/__init__.py b/python/dpd/src/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/python/dpd/src/__init__.py +++ /dev/null diff --git a/python/dpd/src/subsample_align.py b/python/dpd/subsample_align.py index 20ae56b..20ae56b 100755 --- a/python/dpd/src/subsample_align.py +++ b/python/dpd/subsample_align.py diff --git a/python/dpdce.py b/python/dpdce.py new file mode 100755 index 0000000..da1b6fb --- /dev/null +++ b/python/dpdce.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# DPD Computation Engine standalone main file. +# +# http://www.opendigitalradio.org +# Licence: The MIT License, see notice at the end of this file + +"""This Python script is the main file for ODR-DabMod's DPD Computation Engine running +in server mode. + +This engine calculates and updates the parameter of the digital +predistortion module of ODR-DabMod.""" + +import sys +import os +import argparse +import configparser +import matplotlib +matplotlib.use('Agg') + +parser = argparse.ArgumentParser( + description="DPD Computation Engine for ODR-DabMod") +parser.add_argument('--config', default="gui-dpdce.ini", type=str, + help='Location of configuration filename (default: gui-dpdce.ini)', + required=False) +parser.add_argument('-s', '--status', action="store_true", + help='Display the currently running DPD settings.') +parser.add_argument('-r', '--reset', action="store_true", + help='Reset the DPD settings to the defaults, and set digital gain to 0.01') + +cli_args = parser.parse_args() +allconfig = configparser.ConfigParser() +allconfig.read(cli_args.config) +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'] +coef_file = config['coef_file'] +log_folder = config['log_folder'] + +import logging +import datetime + +save_logs = False + +# Simple usage scenarios don't need to clutter /tmp +if save_logs: + dt = datetime.datetime.now().isoformat() + logging_path = '/tmp/dpd_{}'.format(dt).replace('.', '_').replace(':', '-') + print("Logs and plots written to {}".format(logging_path)) + os.makedirs(logging_path) + logging.basicConfig(format='%(asctime)s - %(module)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + filename='{}/dpd.log'.format(logging_path), + filemode='w', + level=logging.DEBUG) + # also log up to INFO to console + console = logging.StreamHandler() + console.setLevel(logging.INFO) + # set a format which is simpler for console use + formatter = logging.Formatter('%(asctime)s - %(module)s - %(levelname)s - %(message)s') + # tell the handler to use this format + console.setFormatter(formatter) + # add the handler to the root logger + logging.getLogger('').addHandler(console) +else: + dt = datetime.datetime.now().isoformat() + logging.basicConfig(format='%(asctime)s - %(module)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + level=logging.INFO) + logging_path = "" + +logging.info("DPDCE starting up"); + +import socket +from lib import yamlrpc +import numpy as np +import traceback +from threading import Thread, Lock +from queue import Queue +from dpd.Model import Poly +import dpd.Heuristics as Heuristics +from dpd.Measure import Measure +from dpd.ExtractStatistic import ExtractStatistic +from dpd.Adapt import Adapt, dpddata_to_str +from dpd.RX_Agc import Agc +from dpd.Symbol_align import Symbol_align +from dpd.GlobalConfig import GlobalConfig +from dpd.MER import MER +from dpd.Measure_Shoulders import Measure_Shoulders + +c = GlobalConfig(config, logging_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) + +model = Poly(c) + +# Do not touch settings on startup +tx_gain = adapt.get_txgain() +rx_gain = adapt.get_rxgain() +digital_gain = adapt.get_digital_gain() +dpddata = adapt.get_predistorter() + +logging.info("ODR-DabMod currently running with TXGain {}, RXGain {}, digital gain {} and {}".format( + tx_gain, rx_gain, digital_gain, dpddata_to_str(dpddata))) + +if cli_args.status: + sys.exit(0) + +if cli_args.reset: + adapt.set_digital_gain(0.01) + adapt.set_rxgain(0) + adapt.set_predistorter(model.get_dpd_data()) + logging.info("DPD Settings were reset to default values.") + sys.exit(0) + +cmd_socket = yamlrpc.Socket(bind_port=config.getint(control_port)) + +# The following is accessed by both threads and need to be locked +settings = { + 'rx_gain': rx_gain, + 'tx_gain': tx_gain, + 'digital_gain': digital_gain, + 'dpddata': dpddata, + } +results = { + 'tx_median': 0, + 'rx_median': 0, + 'state': 'idle', + } +lock = Lock() +command_queue = Queue(maxsize=1) + +# Automatic Gain Control for the RX gain +agc = Agc(meas, adapt, c) + +def engine_worker(): + try: + while True: + cmd = command_queue.get() + + if cmd == "quit": + break + elif cmd == "calibrate": + with lock: + results['state'] = 'rx agc' + + agc.run() + + txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median = self.measure.get_samples() + + with lock: + settings['rx_gain'] = adapt.get_rxgain() + settings['digital_gain'] = adapt.get_digital_gain() + results['tx_median'] = tx_median + results['rx_median'] = rx_median + results['state'] = 'idle' + + finally: + with lock: + results['state'] = 'terminated' + + +engine = Thread(target=engine_worker) +engine.start() + +try: + while True: + addr, msg_id, method, params = cmd_socket.receive_request() + + if method == 'get_settings': + with lock: + cmd_socket.send_success_response(addr, msg_id, settings) + elif method == 'get_results': + with lock: + cmd_socket.send_success_response(addr, msg_id, results) + elif method == 'calibrate': + command_queue.put('calibrate') + else: + cmd_socket.send_error_response(addr, msg_id, "request not understood") +finally: + command_queue.put('quit') + engine.join() + +# Make code below unreachable +sys.exit(0) + +def measure_once(): + txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median = meas.get_samples() + + print("TX signal median {}".format(np.median(np.abs(txframe_aligned)))) + print("RX signal median {}".format(rx_median)) + + tx, rx, phase_diff, n_per_bin = extStat.extract(txframe_aligned, rxframe_aligned) + + off = symbol_align.calc_offset(txframe_aligned) + print("off {}".format(off)) + tx_mer = mer.calc_mer(txframe_aligned[off:off + c.T_U], debug_name='TX') + print("tx_mer {}".format(tx_mer)) + rx_mer = mer.calc_mer(rxframe_aligned[off:off + c.T_U], debug_name='RX') + print("rx_mer {}".format(rx_mer)) + + mse = np.mean(np.abs((txframe_aligned - rxframe_aligned) ** 2)) + print("mse {}".format(mse)) + + digital_gain = adapt.get_digital_gain() + print("digital_gain {}".format(digital_gain)) + + #rx_shoulder_tuple = meas_shoulders.average_shoulders(rxframe_aligned) + #tx_shoulder_tuple = meas_shoulders.average_shoulders(txframe_aligned) + +state = 'report' +i = 0 +n_meas = None +num_iter = 10 +while i < num_iter: + try: + # Measure + if state == 'measure': + # Get Samples and check gain + txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median = meas.get_samples() + # TODO Check TX median + + # Extract usable data from measurement + tx, rx, phase_diff, n_per_bin = extStat.extract(txframe_aligned, rxframe_aligned) + + n_meas = Heuristics.get_n_meas(i) + if extStat.n_meas >= n_meas: # Use as many measurements nr of runs + state = 'model' + else: + state = 'measure' + + # Model + elif state == 'model': + # Calculate new model parameters and delete old measurements + if any(x is None for x in [tx, rx, phase_diff]): + logging.error("No data to calculate model") + state = 'measure' + continue + + model.train(tx, rx, phase_diff, lr=Heuristics.get_learning_rate(i)) + dpddata = model.get_dpd_data() + extStat = ExtractStatistic(c) + state = 'adapt' + + # Adapt + elif state == 'adapt': + adapt.set_predistorter(dpddata) + state = 'report' + + # Report + elif state == 'report': + try: + txframe_aligned, tx_ts, rxframe_aligned, rx_ts, rx_median = meas.get_samples() + + # Store all settings for pre-distortion, tx and rx + adapt.dump() + + # Collect logging data + off = symbol_align.calc_offset(txframe_aligned) + tx_mer = mer.calc_mer(txframe_aligned[off:off + c.T_U], debug_name='TX') + rx_mer = mer.calc_mer(rxframe_aligned[off:off + c.T_U], debug_name='RX') + mse = np.mean(np.abs((txframe_aligned - rxframe_aligned) ** 2)) + tx_gain = adapt.get_txgain() + rx_gain = adapt.get_rxgain() + digital_gain = adapt.get_digital_gain() + tx_median = np.median(np.abs(txframe_aligned)) + rx_shoulder_tuple = meas_shoulders.average_shoulders(rxframe_aligned) + tx_shoulder_tuple = meas_shoulders.average_shoulders(txframe_aligned) + + lr = Heuristics.get_learning_rate(i) + + # Generic logging + logging.info(list((name, eval(name)) for name in + ['i', 'tx_mer', 'tx_shoulder_tuple', 'rx_mer', + 'rx_shoulder_tuple', 'mse', 'tx_gain', + 'digital_gain', 'rx_gain', 'rx_median', + 'tx_median', 'lr', 'n_meas'])) + + # Model specific logging + if dpddata[0] == 'poly': + coefs_am = dpddata[1] + coefs_pm = dpddata[2] + logging.info('It {}: coefs_am {}'. + format(i, coefs_am)) + logging.info('It {}: coefs_pm {}'. + format(i, coefs_pm)) + elif dpddata[0] == 'lut': + scalefactor = dpddata[1] + lut = dpddata[2] + logging.info('It {}: LUT scalefactor {}, LUT {}'. + format(i, scalefactor, lut)) + except: + logging.error('Iteration {}: Report failed.'.format(i)) + logging.error(traceback.format_exc()) + i += 1 + state = 'measure' + + except: + logging.error('Iteration {} failed.'.format(i)) + logging.error(traceback.format_exc()) + i += 1 + state = 'measure' + +# 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 +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/python/gui-dpdce.ini b/python/gui-dpdce.ini new file mode 100644 index 0000000..48c6abf --- /dev/null +++ b/python/gui-dpdce.ini @@ -0,0 +1,30 @@ +# Configuration file for DPDCE and the web GUI + +[dpdce] +# UDP port for dpdce commands +control_port=50056 + +# ODR-DabMod DPD server port +dpd_port=50055 + +# ODR-DabMod ZMQ RC port +rc_port=9400 + +samplerate=8192000 + +# number of samples to collect per capture +samps=81920 + +# Filename of coefficients, must match ODR-DabMod configuration file +coef_file=poly.coef + +# Write logs to this folder, or leave empty for no logs +log_folder= + + +[gui] +host=127.0.0.1 +port=8099 + +logs_directory=gui/logs +static_directory=gui/static diff --git a/python/gui.py b/python/gui.py new file mode 100755 index 0000000..512afef --- /dev/null +++ b/python/gui.py @@ -0,0 +1,135 @@ +#!/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/>. + +from multiprocessing import Process, Pipe +import os.path +import cherrypy +import configparser +import argparse +from jinja2 import Environment, FileSystemLoader +from gui.api import API +from lib import zmqrc + +env = Environment(loader=FileSystemLoader('gui/templates')) + +base_js = ["js/odr.js"] + +class Root: + def __init__(self): + self.mod_rc = zmqrc.ModRemoteControl("localhost") + self.api = API(self.mod_rc) + + @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) + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='ODR-DabMod Web GUI') + parser.add_argument('-c', '--config', + default="gui-dpdce.ini", + help='configuration filename') + cli_args = parser.parse_args() + + allconfig = configparser.ConfigParser() + allconfig.read(cli_args.config) + config = allconfig['gui'] + + daemon = False + if daemon: + cherrypy.process.plugins.Daemonizer(cherrypy.engine).subscribe() + + accesslog = os.path.realpath(os.path.join(config['logs_directory'], 'access.log')) + errorlog = os.path.realpath(os.path.join(config['logs_directory'], 'error.log')) + + cherrypy.config.update({ + 'engine.autoreload.on': True, + 'server.socket_host': config['host'], + 'server.socket_port': config.getint('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['static_directory']) + + cherrypy.tree.mount( + Root(), 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/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/lib/__init__.py b/python/lib/__init__.py new file mode 100644 index 0000000..738b3a1 --- /dev/null +++ b/python/lib/__init__.py @@ -0,0 +1 @@ +# lib module diff --git a/python/lib/yamlrpc.py b/python/lib/yamlrpc.py new file mode 100644 index 0000000..bd61569 --- /dev/null +++ b/python/lib/yamlrpc.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# -*- 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/>. + +"""yamlrpc is json-rpc, except that it's yaml and not json.""" + +# Same as jsonrpc version we're aiming to mirror in YAML +YAMLRPC_VERSION = "2.0" + +import yaml +import socket +import struct + +def request(request_id: int, method: str, params) -> bytes: + r = { + 'yamlrpc': YAMLRPC_VERSION, + 'method': method, + 'params': params, + 'id': request_id} + return yaml.dump(r).encode() + +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() + +def notification(method: str, params) -> bytes: + r = { + 'yamlrpc': YAMLRPC_VERSION, + 'method': method, + 'params': params} + return yaml.dump(r).encode() + +class Socket: + def __init__(self, bind_port: int): + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + if bind_port > 0: + self.socket.bind(('127.0.0.1', bind_port)) + + def receive_request(self): + data, addr = self.socket.recvfrom(512) + 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 request + try: + method = y['method'] + msg_id = y['id'] + params = y['params'] + except KeyError: + raise ValueError("Incomplete message") + return addr, msg_id, method, params + + def send_success_response(self, addr, msg_id: int, result): + self.socket.sendto(response_success(msg_id, result), addr) + + def send_error_response(self, addr, msg_id: int, error): + self.socket.sendto(response_error(msg_id, error), addr) + diff --git a/python/gui/zmqrc.py b/python/lib/zmqrc.py index 3897d7a..3897d7a 100644 --- a/python/gui/zmqrc.py +++ b/python/lib/zmqrc.py |