Open RF Prototyping

Red Pitaya RF Front End

Note that this page is automatically generated from an org-mode source file. The Python code for this reference design is also automatically generated from this file using org-babel-tangle.

The Script

"""Package for the sa.py application."""

__app_name__ = 'rprf'
__version__ = '0.1.0'
"""rprf application entry point."""

import rprf


if __name__ == '__main__':
    rprf.main()
# Generated from rp-rf.org
#

<<imports>>

<<rprf-service>>

<<main-func>>

Run the resulting scripts as follows:

cd scripts/rp
python frontend -h

Frontend hardware address.

Frontend hardware configuration format.

The main Function

The asyncio framework is used to coordinate communications over the serial device without blocking user interface events. For this purpose we make use of QEventLoop and QThreadExecutor from the qasync package.

The event loop is equipped with an instance of QThreadExecutor which executes a single thread. The idea here is to ensure that requests to the ECApp serial device will execute one by one in a manner which will not block the operation of the Qt event loop.

def main():

    <<process-cmdline-args>>

    # If specified, read HW config from file
    hw_config = None
    if args.frontend_config is not None:
        try:
            with open(args.frontend_config) as fd:
                hw_config = json.load(fd)
        except FileNotFoundError:
            print(f'No such file "{args.frontend_config}" for front end HW config.')
            print('Using default front end HW config. instead.')

    # This ensures that Cntl-C will work as expected:
    signal.signal(signal.SIGINT, signal.SIG_DFL)

    app = QCoreApplication(sys.argv)
    loop = QEventLoop(app)
    loop.set_default_executor(QThreadExecutor(1))
    asyncio.set_event_loop(loop)

    if hw_config is not None:
        fe_app = FrontEndApp(
            hw_config=hw_config['hw_config'],
            app_config=hw_config['app_config']
        )
    else:
        fe_app = FrontEndApp()

    if hw_config is not None and 'capabilities' in hw_config:
        front_end = FrontEndService(
            fe_app,
            serial_device=args.device,
            baudrate=args.baudrate,
            capabilities=hw_config['capabilities']
        )
    else:
        front_end = FrontEndService(
            fe_app,
            serial_device=args.device,
            baudrate=args.baudrate
        )

    server_thread = QThread()
    server = RPyCServer(
        front_end,
        args.ipaddr,
        args.port)
    server.moveToThread(server_thread)
    server_thread.started.connect(server.run)
    server.finished.connect(server_thread.quit)
    server.finished.connect(server.deleteLater)
    server_thread.finished.connect(server_thread.deleteLater)
    server_thread.start()

    try:
        loop.set_debug(True)
        loop.run_forever()
    except KeyboardInterrupt:
        pass

    loop.close()

Process command line arguments

defaultBaud = 0

parser = ArgumentParser(description="Red Pitaya RF Front End")

parser.add_argument("-d", "--device", default=None, required=True,
                    help="The front end hardware serial device")
parser.add_argument("-b", "--baudrate", default=defaultBaud, type=int,
                    help="Baud rate (default: {})".format(defaultBaud))
parser.add_argument("-A", "--ipaddr", default="127.0.0.1",
                    help="IP address for the RPyC server instance")
parser.add_argument("-P", "--port", default=18870, type=int,
                    help="TCP port for the RPyC server instance")
parser.add_argument("-F", "--frontend_config", default=None,
                    help="Frontend hardware configuration.")
args = parser.parse_args()

ADC Channel Class

from typing import (
    List, Dict, Callable
)
import serial
from scipy import interpolate

from rfblocks import (
    pe43711, hmc833, PE43711Controller, HMC833Controller
)


class ADCChan(object):

    def __init__(self,
                 chan_id: str,
                 hw_config: Dict,
                 chan_config: Dict) -> None:
        """
        """
        self._chan_id: str = chan_id
        self._freq_resp_cal_data: Dict[float, float] = {}
        self._noise_floor_cal_data: Dict[float, float] = {}
        self._stepatten_controller = None
        self._lo1_controller = None
        # self._lo2_controller = None

        if 'stepatten' in hw_config:
            self._stepatten_controller = PE43711Controller(
                f'stepatten_{chan_id[-1]}',
                pe43711(**hw_config['stepatten']))
            self._stepatten_controller.attenuation = chan_config['stepatten']['atten']
        if 'lo1' in hw_config:
            self._lo1_controller = HMC833Controller(
                f'lo1_{chan_id[-1]}',
                hmc833(**hw_config['lo1']),
                chan_config['lo1'])

    @property
    def stepatten_ctl(self) -> PE43711Controller:
        """Return a reference to the channel's front end step attenuator
        controller.
        """
        return self._stepatten_controller

    @property
    def lo1_synth_ctl(self) -> HMC833Controller:
        """Return a reference to the channel's LO1 synthesizer
        controller.
        """
        return self._lo1_controller

    @property
    def freq_resp_cal_data(self) -> Dict[float, float]:
        return self._freq_resp_cal_data

    @freq_resp_cal_data.setter
    def freq_resp_cal_data(self, fr: Dict[float, float]) -> None:
        self._freq_resp_cal_data = dict(fr)

    @property
    def noise_floor_cal_data(self) -> Dict[float, float]:
        return self._noise_floor_cal_data

    @noise_floor_cal_data.setter
    def noise_floor_cal_data(self, nf: Dict[float, float]) -> None:
        self._noise_floor_cal_data = dict(nf)

    def initialize_hw(self, ser: serial.Serial) -> None:
        """Initialize the ADC attenuator hardware.
        """
        if self.stepatten_ctl:
            self.stepatten_ctl.initialize(ser)
        if self.lo1_synth_ctl:
            self.lo1_synth_ctl.initialize(ser)

    def initialize(self, ser: serial.Serial):
        self.configure_lo1(ser)

    def configure_stepatten(self, ser, att) -> None:
        if self.stepatten_ctl:
            self.stepatten_ctl.attenuation = att
            self.stepatten_ctl.configure(ser)

    def configure_lo1(self, ser) -> None:
        if self.lo1_synth_ctl:
            self.lo1_synth_ctl.configure(ser)

    def configure_lo1_freq(self, ser, freq) -> None:
        if self.lo1_synth_ctl:
            self.lo1_synth_ctl.freq = freq
            self.lo1_synth_ctl.configure_freq(ser)

App Class

It's possible to implement different types of front end depending on the hardware. Initially, the front end type will be frequency conversion (more properly, frequency down conversion). This may be done using direct conversion via a single LO and mixer stage or using a super heterodyne approach. Base band must be somewhere within the ADC sampling range which is approximately 0.5 to 55 MHz.

from typing import (
    Optional, List, Dict
)
from time import sleep
from rfblocks import (
    hmc833,
    DEFAULT_BAUDRATE, create_serial, write_cmd
)

from tam import (
    RP_ADC_MIN_FREQUENCY, RP_ADC_MAX_FREQUENCY, RP_ADC_MIN_SPAN,
    RP_ADC_MAX_REF_LEVEL, RP_ADC_MIN_REF_LEVEL, RP_ADC_REF_LEVEL_STEP,
    RP_ADC_MIN_ATT, RP_ADC_MAX_ATT, RP_ADC_ATT_STEP,
    Capability, ChannelCapabilities
)

from PyQt5.QtCore import (
    Qt, QObject, QCoreApplication, QThread, QTimer,
    pyqtSignal, pyqtSlot
)

from adcchan import ADCChan


class FrontEndApp(QObject):

    <<frontend-config>>

    NOISE_FLOOR: Dict = {
        'channels': {
            ADCCH1_NAME: {
                400000: (-91.238, 5.062), 200000: (-97.886, 5.586),
                100000: (-100.887, 5.566), 50000: (-104.222, 5.531),
                20000: (-106.397, 5.562), 10000: (-110.599, 5.494),
                5000: (-114.466, 5.605), 2000: (-118.074, 5.581),
                1000: (-120.604, 5.547), 500: (-123.662, 5.647),
                200: (-127.920, 5.609), 100: (-130.131, 5.466), 50: (-134.579, 5.560),
                20: (-137.322, 5.585), 10: (-140.600, 5.524), 5: (-144.054, 5.567),
                2: (-148.833, 5.588), 1: (-149.271, 5.558)
            },
            ADCCH2_NAME: {
                400000: (-96.159, 5.025), 200000: (-100.022, 5.578),
                100000: (-103.787, 5.623), 50000: (-104.291, 5.557),
                20000: (-109.207, 5.540), 10000: (-111.787, 5.507),
                5000: (-114.801, 5.553), 2000: (-117.145, 5.547),
                1000: (-122.124, 5.590), 500: (-124.722, 5.503),
                200: (-129.797, 5.479), 100: (-132.126, 5.532), 50: (-134.208, 5.563),
                20: (-138.224, 5.507), 10: (-141.068, 5.564), 5: (-143.290, 5.563),
                2: (-146.779, 5.515), 1: (-151.426, 5.648)
            }
        }
    }

    FREQ_RESPONSE: Dict = {
        'channels': {
            ADCCH1_NAME: {
                54.90: 2.083, 104.90: 1.571, 154.90: 1.569, 204.90: 1.587,
                254.90: 1.473, 304.90: 1.568, 354.90: 1.499, 404.90: 1.558,
                454.90: 1.667, 504.90: 1.604, 554.90: 1.596, 604.90: 1.629,
                654.90: 1.647, 704.90: 1.718, 754.90: 1.767, 804.90: 1.729,
                854.90: 1.575, 904.90: 1.653, 954.90: 1.614, 1004.90: 1.723,
                1054.90: 1.863, 1104.90: 1.686, 1154.90: 1.743, 1204.90: 1.777,
                1254.90: 1.724, 1304.90: 1.804, 1354.90: 1.905, 1404.90: 1.826,
                1454.90: 1.865, 1504.90: 1.898, 1554.90: 2.013, 1604.90: 2.069,
                1654.90: 1.991, 1704.90: 2.091, 1754.90: 2.122, 1804.91: 2.275,
                1854.91: 2.407, 1904.91: 2.286, 1954.91: 2.005, 2004.91: 2.128,
                2054.91: 2.316, 2104.91: 2.654, 2154.91: 2.628, 2204.91: 2.457,
                2254.91: 2.424, 2304.91: 2.471, 2354.91: 2.619, 2404.91: 2.668,
                2454.91: 2.936, 2504.91: 2.841, 2554.91: 2.912, 2604.91: 3.341,
                2654.91: 3.317, 2704.91: 3.565, 2754.91: 3.584, 2804.91: 3.355,
                2854.91: 3.127, 2904.91: 2.965, 2954.91: 2.903, 3004.91: 2.897,
                3054.91: 3.119, 3104.91: 3.231, 3154.91: 3.020, 3204.91: 2.970,
                3254.91: 3.002, 3304.91: 3.280, 3354.91: 3.434, 3404.91: 3.331,
                3454.91: 3.277, 3504.91: 3.489, 3554.91: 4.341, 3604.91: 4.161,
                3654.91: 4.148, 3704.91: 4.320, 3754.91: 5.010, 3804.91: 5.177,
                3854.91: 4.701, 3904.91: 5.072, 3954.91: 5.529, 4004.91: 6.070,
                4054.91: 6.081
            },
            ADCCH2_NAME: {
                54.90: -0.156, 104.90: -0.336, 154.90: -0.118, 204.90: -0.074,
                254.90: 0.019, 304.90: 0.091, 354.90: 0.087, 404.90: 0.017,
                454.90: 0.163, 504.90: 0.161, 554.90: 0.092, 604.90: 0.211,
                654.90: 0.280, 704.90: 0.495, 754.90: 0.625, 804.90: 0.568,
                854.90: 0.640, 904.90: 0.779, 954.90: 0.804, 1004.90: 0.914,
                1054.90: 1.008, 1104.90: 0.957, 1154.90: 0.810, 1204.90: 0.936,
                1254.90: 0.897, 1304.91: 1.135, 1354.91: 1.161, 1404.91: 1.299,
                1454.91: 1.441, 1504.91: 1.434, 1554.91: 1.468, 1604.91: 1.550,
                1654.91: 1.413, 1704.91: 1.558, 1754.91: 1.613, 1804.91: 1.898,
                1854.91: 1.951, 1904.91: 1.794, 1954.91: 1.750, 2004.91: 1.864,
                2054.91: 2.024, 2104.91: 2.408, 2154.91: 2.413, 2204.91: 2.260,
                2254.91: 2.397, 2304.91: 2.517, 2354.91: 2.783, 2404.91: 2.988,
                2454.91: 3.396, 2504.91: 3.155, 2554.91: 3.539, 2604.91: 3.746,
                2654.91: 4.130, 2704.91: 4.902, 2754.91: 4.936, 2804.91: 4.791,
                2854.91: 4.636, 2904.91: 4.678, 2954.91: 4.406, 3004.91: 4.420,
                3054.91: 5.162, 3104.91: 5.126, 3154.91: 5.248, 3204.91: 5.481,
                3254.91: 5.828, 3304.91: 5.657, 3354.91: 5.623, 3404.91: 5.538,
                3454.91: 5.348, 3504.91: 5.125, 3554.91: 5.667, 3604.91: 5.922,
                3654.91: 5.920, 3704.91: 6.600, 3754.91: 7.614, 3804.91: 9.127,
                3854.91: 8.324, 3904.91: 8.188, 3954.91: 8.687, 4004.92: 9.284,
                4054.92: 9.173
            }
        }
    }

    # Frequency response cal. with GRF2013 400-5000 MHz amp. on ADC CH1 and
    # GRF2013 50-3000 MHz amp on ADC CH2.
    #
    # FREQ_RESPONSE: Dict = {
    #     'channels': {
    #         ADCCH1_NAME: {
    #             54.90: -18.281, 104.90: -18.987, 154.90: -18.954, 204.90: -18.935,
    #             254.90: -18.964, 304.90: -18.924, 354.90: -18.870, 404.90: -18.862,
    #             454.90: -18.484, 504.90: -18.497, 554.90: -18.552, 604.90: -18.332,
    #             654.90: -18.360, 704.90: -18.042, 754.90: -17.796, 804.90: -18.062,
    #             854.90: -18.034, 904.90: -17.858, 954.90: -17.973, 1004.90: -17.645,
    #             1054.90: -17.580, 1104.90: -17.628, 1154.90: -17.392, 1204.90: -16.994,
    #             1254.90: -16.764, 1304.90: -16.525, 1354.90: -16.388, 1404.90: -16.429,
    #             1454.90: -16.071, 1504.90: -15.557, 1554.90: -15.353, 1604.90: -15.390,
    #             1654.90: -15.375, 1704.90: -15.080, 1754.90: -14.864, 1804.90: -14.425,
    #             1854.90: -14.245, 1904.90: -14.511, 1954.90: -14.481, 2004.90: -14.325,
    #             2054.90: -14.033, 2104.90: -13.949, 2154.90: -14.041, 2204.90: -14.069,
    #             2254.90: -13.615, 2304.90: -13.448, 2354.90: -13.281, 2404.90: -13.194,
    #             2454.90: -13.830, 2504.90: -13.866, 2554.90: -13.612, 2604.90: -13.348,
    #             2654.90: -12.700, 2704.90: -12.100, 2754.90: -12.321, 2804.90: -12.155,
    #             2854.90: -12.258, 2904.90: -12.570, 2954.90: -12.436, 3004.90: -12.256,
    #             3054.90: -11.983, 3104.90: -11.599, 3154.90: -11.253, 3204.90: -11.096,
    #             3254.90: -11.096, 3304.90: -11.207, 3354.90: -11.236, 3404.90: -11.226,
    #             3454.90: -11.180, 3504.90: -10.545, 3554.90: -9.761, 3604.90: -9.401,
    #             3654.90: -8.822, 3704.90: -8.643, 3754.90: -8.645, 3804.90: -7.951,
    #             3854.90: -7.568, 3904.90: -6.816, 3954.90: -5.756, 4004.91: -5.527,
    #             4054.90: -4.872
    #         },
    #         ADCCH2_NAME: {
    #             54.90: -20.956, 104.90: -20.845, 154.90: -20.719, 204.90: -20.535,
    #             254.90: -20.417, 304.90: -20.259, 354.90: -20.233, 404.90: -20.295,
    #             454.90: -20.253, 504.90: -20.153, 554.90: -20.120, 604.90: -19.829,
    #             654.90: -19.696, 704.90: -19.299, 754.90: -19.259, 804.90: -19.172,
    #             854.90: -18.994, 904.90: -18.899, 954.90: -18.778, 1004.90: -18.518,
    #             1054.90: -18.496, 1104.90: -18.438, 1154.90: -18.326, 1204.90: -18.399,
    #             1254.91: -18.353, 1304.91: -18.113, 1354.91: -17.750, 1404.91: -17.507,
    #             1454.91: -17.075, 1504.91: -16.908, 1554.91: -16.971, 1604.91: -17.005,
    #             1654.91: -16.855, 1704.91: -16.439, 1754.91: -16.097, 1804.91: -15.762,
    #             1854.91: -15.809, 1904.91: -15.839, 1954.91: -15.539, 2004.91: -15.317,
    #             2054.91: -15.134, 2104.91: -14.870, 2154.91: -15.066, 2204.91: -14.902,
    #             2254.91: -14.567, 2304.91: -14.303, 2354.91: -13.757, 2404.91: -13.620,
    #             2454.91: -14.006, 2504.91: -14.047, 2554.91: -13.920, 2604.91: -13.163,
    #             2654.91: -12.214, 2704.91: -11.596, 2754.91: -11.410, 2804.91: -11.388,
    #             2854.91: -11.843, 2904.91: -12.211, 2954.91: -11.917, 3004.91: -11.041,
    #             3054.91: -9.934
    #         }
    #     }
    # }

    def __init__(self,
                 hw_config: List = APP_HWCONF,
                 app_config: Dict = DEFAULT_APP_CONFIG) -> None:
        super().__init__()

        d: Dict = app_config['channels']
        z = zip(d.keys(), hw_config, d.values())
        self._channels: Dict = {
            chan_id: ADCChan(chan_id, hw_conf, chan_config)
            for chan_id, hw_conf, chan_config in z}
        for chan_id, nf in FrontEndApp.NOISE_FLOOR['channels'].items():
            self._channels[chan_id].noise_floor_cal_data = nf
        for chan_id, fr in FrontEndApp.FREQ_RESPONSE['channels'].items():
            self._channels[chan_id].freq_resp_cal_data = fr

    <<initialize>>

    @property
    def channels(self):
        return self._channels

Hardware configuration

{
    "hw_config": [
        {"adcatten": {"le": "D0"},
         "stepatten": {"le": "D4"},
         "lo1": {"sen": "D2", "ld_sdo": "D3"}},
        {"adcatten": {"le": "D1"},
         "stepatten": {"le": "C6"},
         "lo1": {"sen": "D5", "ld_sdo": "D6"}}
    ],
    "app_config": {
        "channels": {
            "Ch 1": {"adcatten": {"atten": 0.0},
                     "stepatten": {"atten": 10.0},
                     "lo1": {
                         "freq": 100.0,
                         "ref_freq": 50.0,
                         "refsrc": 0,
                         "bufgain": 0,
                         "divgain": 0,
                         "vco_mute": false
                     }},
            "Ch 2": {"adcatten": {"atten": 0.0},
                     "stepatten": {"atten": 10.0},
                     "lo1": {
                         "freq": 100.0,
                         "ref_freq": 50.0,
                         "refsrc": 0,
                         "bufgain": 0,
                         "divgain": 0,
                         "vco_mute": false
                     }}
        }
    }
}
ADCCH1_NAME: str = 'Ch 1'
ADCCH2_NAME: str = 'Ch 2'

LO1_CH1_HWCONF: Dict = {'sen': 'D2', 'ld_sdo': 'D3'}
LO2_CH1_HWCONF: Dict = {'sen': 'D5', 'ld_sdo': 'D6'}
STEPATTEN_CH1_HWCONF: Dict = {'le': 'D4'}

LO1_CH2_HWCONF: Dict = {'sen': 'C4', 'ld_sdo': 'C5'}
LO2_CH2_HWCONF: Dict = {'sen': 'C7', 'ld_sdo': 'B7'}
STEPATTEN_CH2_HWCONF: Dict = {'le': 'C6'}

# The channel hardware is configured to match the hardware
# currently in place.

CH1_HWCONF: Dict = {'stepatten': STEPATTEN_CH1_HWCONF,
                    'lo1': LO1_CH1_HWCONF}
CH2_HWCONF: Dict = {'stepatten': STEPATTEN_CH2_HWCONF,
                    'lo1': LO1_CH2_HWCONF}
APP_HWCONF: List = [CH1_HWCONF, CH2_HWCONF]

DEFAULT_ATTEN: float = 10.0

DEFAULT_LO1_FREQ: float = 100.0
DEFAULT_LO2_FREQ: float = 1000.0
DEFAULT_REF_FREQ: float = 50.0

DEFAULT_STEPATTEN_CONFIG: Dict = {
    'atten': DEFAULT_ATTEN
}
DEFAULT_LO1_CONFIG: Dict = {
    'freq': DEFAULT_LO1_FREQ,
    'ref_freq': DEFAULT_REF_FREQ,
    'refsrc': hmc833.DEFAULT_REFSRC,
    'bufgain': hmc833.OutputBufferGain.MAXGAIN_MINUS_6DB,
    'divgain': hmc833.DividerGain.MAXGAIN_MINUS_3DB,
    'vco_mute': False
}
DEFAULT_LO2_CONFIG: Dict = {
    'freq': DEFAULT_LO2_FREQ,
    'ref_freq': DEFAULT_REF_FREQ,
    'refsrc': hmc833.DEFAULT_REFSRC,
    'bufgain': hmc833.OutputBufferGain.MAXGAIN_MINUS_6DB,
    'divgain': hmc833.DividerGain.MAXGAIN_MINUS_3DB,
    'vco_mute': False
}
DEFAULT_CHAN_CONFIG: Dict = {
    'stepatten': DEFAULT_STEPATTEN_CONFIG,
    'lo1': DEFAULT_LO1_CONFIG,
    'lo2': DEFAULT_LO2_CONFIG
}

DEFAULT_APP_CONFIG: Dict = {
    'channels': {
        ADCCH1_NAME: DEFAULT_CHAN_CONFIG,
        ADCCH2_NAME: DEFAULT_CHAN_CONFIG
    }
}
def initialize_hw(self, ser) -> None:
    for chan in self._channels.values():
        chan.initialize_hw(ser)
        sleep(0.1)

def initialize(self, ser) -> None:
    for chan in self._channels.values():
        chan.initialize(ser)

def cleanup(self) -> None:
    pass

The FrontEnd Service

MIN_ATT = 0.0
MAX_ATT = 31.75
ATT_STEP = 0.25


class FrontEndService(rpyc.Service):

    DEFAULT_CAPABILITIES = {
        "channels": [
            FrontEndApp.ADCCH1_NAME,
            FrontEndApp.ADCCH2_NAME],
        FrontEndApp.ADCCH1_NAME: {
            "adcdma_channel": 0,
            "adc_attenuation": {
                "min": RP_ADC_MIN_ATT,
                "max": RP_ADC_MAX_ATT,
                "step": RP_ADC_ATT_STEP
            },
            "fe_attenuation": {
                "min": MIN_ATT,
                "max": MAX_ATT,
                "step": ATT_STEP
            },
            "freq": {
                "min": 55.0,
                "max": 4000.0,
                "step": 0.0
            },
            "cal_freq": {
                "min": 53.0,
                "max": 4055.0,
                "step": 50.0
            },
            "reflevel": {
                "min": RP_ADC_MIN_REF_LEVEL,
                "max": RP_ADC_MAX_REF_LEVEL,
                "step": 1.0
            },
            "min_span": RP_ADC_MIN_SPAN,
            "chan_type": ChannelCapabilities.SINGLE,
            "bandwidth": 45.0
        },
        FrontEndApp.ADCCH2_NAME: {
            "adcdma_channel": 1,
            "adc_attenuation": {
                "min": RP_ADC_MIN_ATT,
                "max": RP_ADC_MAX_ATT,
                "step": RP_ADC_ATT_STEP
            },
            "fe_attenuation": {
                "min": MIN_ATT,
                "max": MAX_ATT,
                "step": ATT_STEP
            },
            "freq": {
                "min": 55.0,
                "max": 4000.0,
                "step": 0.0
            },
            "cal_freq": {
                "min": 53.0,
                "max": 4055.0,
                "step": 50.0
            },
            "reflevel": {
                "min": RP_ADC_MIN_REF_LEVEL,
                "max": RP_ADC_MAX_REF_LEVEL,
                "step": 1.0
            },
            "min_span": RP_ADC_MIN_SPAN,
            "chan_type": ChannelCapabilities.SINGLE,
            "bandwidth": 45.0
        }
    }

    def __init__(self,
                 app,
                 serial_device: str,
                 baudrate: int = 0,
                 capabilities: Dict = DEFAULT_CAPABILITIES) -> None:
        super().__init__()
        self._app = app
        self._ctl_device: str = serial_device
        self._baudrate: int = baudrate
        if baudrate == 0:
            self._baudrate = DEFAULT_BAUDRATE
        self._capabilities = {
            'channels': capabilities['channels'],
            FrontEndApp.ADCCH1_NAME: ChannelCapabilities.from_dict(
                capabilities[FrontEndApp.ADCCH1_NAME]),
            FrontEndApp.ADCCH2_NAME: ChannelCapabilities.from_dict(
                capabilities[FrontEndApp.ADCCH2_NAME])
        }
        self._ser = None
        self._client_count = 0

    def on_connect(self, conn):
        if self._ser is None:
            self._ser = create_serial(self._ctl_device,
                                      self._baudrate)
            self._app.initialize_hw(self._ser)
        self._client_count += 1

    def on_disconnect(self, conn):
        self._client_count -= 1
        if self._client_count == 0:
            self._app.cleanup()
            self._ser.close()
            self._ser = None

    @property
    def capabilities(self):
        return self._capabilities

    @property
    def adc_channels(self):
        return self._app.channels

    def initialize(self):
        self._app.initialize(self._ser)

    def configure_stepatten(self, chan_id, att):
        ch = self.adc_channels[chan_id]
        ch.configure_stepatten(self._ser, att)

    def configure_lo1(self, chan_id):
        ch = self.adc_channels[chan_id]
        ch.configure_lo1(self._ser)

    def configure_lo1_freq(self, chan_id, lo_freq):
        ch = self.adc_channels[chan_id]
        ch.configure_lo1_freq(self._ser, lo_freq)

    @classmethod
    def save_config(cls):
        print(FrontEndApp.DEFAULT_APP_CONFIG)


class RPyCServer(QObject):

    finished = pyqtSignal()

    def __init__(self, serviceInst, host, port):
        super().__init__()
        self._serviceInst = serviceInst
        self._host = host
        self._port = port

    def run(self):
        print("RPRF rpyc service on {}:{}".format(self._host, self._port))
        self._server = ThreadedServer(
            self._serviceInst,
            hostname = self._host,
            port = self._port,
            auto_register = False,
            protocol_config = {
                'allow_all_attrs': True,
                'allow_setattr': True,
                'allow_pickle': True})
        self._server.start()
        self.finished.emit()

Single Channel Configuration

In some circumstances only one channel of the down converter will be in use. The other spectrum analyzer channel either remaining unused or being used to measure a signal in base band.

The spectrum analyzer test bed app can be configured to make use of the single channel configuration by using the -F command line parameter:

$ python sa -F frontend/config/hw-single-chan-config.json
{
    "hw_config": [
        {"adcatten": {"le": "D0"},
         "stepatten": {"le": "D4"},
         "lo1": {"sen": "D2", "ld_sdo": "D3"}},
        {"adcatten": {"le": "D1"}}
    ],
    "app_config": {
        "channels": {
            "Ch 1": {"adcatten": {"atten": 0.0},
                     "stepatten": {"atten": 16.0},
                     "lo1": {
                         "freq": 100.0,
                         "ref_freq": 50.0,
                         "refsrc": 0,
                         "bufgain": 0,
                         "divgain": 0,
                         "vco_mute": false
                     }},
            "Ch 2": {"adcatten": {"atten": 0.0}}
        }
    },
    "capabilities": {
        "channels": ["Ch 1", "Ch 2"],
        "Ch 1": {
            "adcdma_channel": 0,
            "adc_attenuation": {
                "min": 0.0,
                "max": 31.75,
                "step": 0.25
            },
            "fe_attenuation": {
                "min": 0.0,
                "max": 31.75,
                "step": 0.25
            },
            "freq": {
                "min": 55.0,
                "max": 4000.0,
                "step": 0.0
            },
            "cal_freq": {
                "min": 53.0,
                "max": 4055.0,
                "step": 50.0
            },
            "reflevel": {
                "min": -100.0,
                "max": 20.0,
                "step": 1.0
            },
            "min_span": 0.0005,
            "chan_type": 1,
            "bandwidth": 45.0
        },
        "Ch 2": {
            "adcdma_channel": 1,
            "adc_attenuation": {
                "min": 0.0,
                "max": 31.75,
                "step": 0.25
            },
            "fe_attenuation": null,
            "freq": {
                "min": 0.5,
                "max": 55.0,
                "step": 0.0
            },
            "cal_freq": {
                "min": 0.1,
                "max": 56.5,
                "step": 0.5
            },
            "reflevel": {
                "min": -100.0,
                "max": 20.0,
                "step": 1.0
            },
            "min_span": 0.0005,
            "chan_type": 0,
            "bandwidth": 45.0
        }
    }
}

Imports

from typing import (
    Optional, List, Dict
)
import sys
from time import sleep
import signal
import json
from argparse import ArgumentParser
import asyncio
import serial

from rfblocks import (
    hmc833,
    DEFAULT_BAUDRATE, create_serial, write_cmd
)
from tam import (
    RP_ADC_MIN_FREQUENCY, RP_ADC_MAX_FREQUENCY, RP_ADC_MIN_SPAN,
    RP_ADC_MAX_REF_LEVEL, RP_ADC_MIN_REF_LEVEL, RP_ADC_REF_LEVEL_STEP,
    RP_ADC_MIN_ATT, RP_ADC_MAX_ATT, RP_ADC_ATT_STEP,
    Capability, ChannelCapabilities
)
import rpyc
from rpyc.utils.server import ThreadedServer
from qasync import (
    QEventLoop, QThreadExecutor, asyncSlot, asyncClose
)
from PyQt5.QtCore import (
    Qt, QObject, QCoreApplication, QThread, QTimer,
    pyqtSignal, pyqtSlot
)

from adcchan import ADCChan
from frontend import FrontEndApp