From 1d082c28c332ae25b9ed372d1dee8040ccc1ebc1 Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Thu, 5 Aug 2021 15:05:55 +0200 Subject: [PATCH] Reorganizing miniconf, adding loopback test --- hitl/loopback.py | 148 ++++++++++++++++++ miniconf-py/README.md | 6 + miniconf-py/miniconf.egg-info/PKG-INFO | 10 ++ miniconf-py/miniconf.egg-info/SOURCES.txt | 8 + .../miniconf.egg-info/dependency_links.txt | 1 + miniconf-py/miniconf.egg-info/requires.txt | 1 + miniconf-py/miniconf.egg-info/top_level.txt | 1 + miniconf-py/miniconf/__init__.py | 4 + miniconf-py/miniconf/__main__.py | 53 +++++++ .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 233 bytes .../__pycache__/miniconf.cpython-38.pyc | Bin 0 -> 4837 bytes .../miniconf/miniconf.py | 52 +----- miniconf-py/setup.py | 14 ++ 13 files changed, 247 insertions(+), 51 deletions(-) create mode 100644 hitl/loopback.py create mode 100644 miniconf-py/README.md create mode 100644 miniconf-py/miniconf.egg-info/PKG-INFO create mode 100644 miniconf-py/miniconf.egg-info/SOURCES.txt create mode 100644 miniconf-py/miniconf.egg-info/dependency_links.txt create mode 100644 miniconf-py/miniconf.egg-info/requires.txt create mode 100644 miniconf-py/miniconf.egg-info/top_level.txt create mode 100644 miniconf-py/miniconf/__init__.py create mode 100644 miniconf-py/miniconf/__main__.py create mode 100644 miniconf-py/miniconf/__pycache__/__init__.cpython-38.pyc create mode 100644 miniconf-py/miniconf/__pycache__/miniconf.cpython-38.pyc rename miniconf.py => miniconf-py/miniconf/miniconf.py (60%) create mode 100644 miniconf-py/setup.py diff --git a/hitl/loopback.py b/hitl/loopback.py new file mode 100644 index 0000000..df768be --- /dev/null +++ b/hitl/loopback.py @@ -0,0 +1,148 @@ +#!/usr/bin/python3 +""" +Author: Vertigo Designs, Ryan Summers + +Description: Loop-back integration tests for Stabilizer hardware +""" +import argparse +import asyncio +import json +import sys + +from gmqtt import Client as MqttClient +from miniconf import Miniconf + +# The minimum allowably loopback voltage error (difference between output set point and input +# measured value). +MINIMUM_VOLTAGE_ERROR = 0.010 + +def _voltage_to_machine_units(voltage): + """ Convert a voltage to IIR machine units. + + Args: + voltage: The voltage to convert + + Returns: + The IIR machine-units associated with the voltage. + """ + dac_range = 4.096 * 2.5 + assert abs(voltage) <= dac_range, 'Voltage out-of-range' + return voltage / dac_range * 0x7FFF + + +def static_iir_output(output_voltage): + """ Generate IIR configuration for a static output voltage. + + Args: + output_voltage: The desired static IIR output voltage. + + Returns + The IIR configuration to send over Miniconf. + """ + machine_units = _voltage_to_machine_units(output_voltage) + return { + 'y_min': machine_units, + 'y_max': machine_units, + 'y_offset': 0, + 'ba': [1, 0, 0, 0, 0], + } + + +class TelemetryReader: + """ Helper utility to read Stabilizer telemetry. """ + + @classmethod + async def create(cls, prefix, broker): + """Create a connection to the broker and an MQTT device using it.""" + client = MqttClient(client_id='') + await client.connect(broker) + return cls(client, prefix) + + + def __init__(self, client, prefix): + """ Constructor. """ + self.client = client + self._telemetry = [] + self.client.on_message = self.handle_telemetry + self._telemetry_topic = f'{prefix}/telemetry' + self.client.subscribe(self._telemetry_topic) + + + def handle_telemetry(self, _client, topic, payload, _qos, _properties): + """ Handle incoming telemetry messages over MQTT. """ + assert topic == self._telemetry_topic + self._telemetry.append(json.loads(payload)) + + + def get_latest(self): + """ Get the latest telemetry message received. """ + return self._telemetry[-1] + + +async def test_loopback(miniconf, telemetry, set_point): + """ Test loopback operation of Stabilizer. + + Note: + Loopback is tested by configuring DACs for static output and verifying telemetry reports the + ADCs are measuring those values. Output OUTx should be connected in a loopback configuration + to INx on the device. + + Args: + miniconf: The miniconf configuration interface. + telemetry: a helper utility to read inbound telemetry. + set_point: The desired output voltage to test. + """ + print(f'Testing loopback for Vout = {set_point:.2f}') + print('---------------------------------') + # Configure the IIRs to output at the set point + assert (await miniconf.command('iir_ch/0/0', static_iir_output(set_point)))['code'] == 0 + assert (await miniconf.command('iir_ch/1/0', static_iir_output(set_point)))['code'] == 0 + assert (await miniconf.command('telemetry_period', 1))['code'] == 0 + + # Wait for telemetry values to update. + await asyncio.sleep(2.0) + + # Verify the ADCs are receiving the setpoint voltage. + tolerance = max(0.05 * set_point, MINIMUM_VOLTAGE_ERROR) + latest_values = telemetry.get_latest() + print(f'Latest telemtry: {latest_values}') + + assert abs(latest_values['adcs'][0] - set_point) < tolerance + assert abs(latest_values['adcs'][1] - set_point) < tolerance + print('PASS') + print('') + + +def main(): + """ Main program entry point. """ + parser = argparse.ArgumentParser(description='Loopback tests for Stabilizer HITL testing',) + parser.add_argument('prefix', type=str, + help='The MQTT topic prefix of the target') + parser.add_argument('--broker', '-b', default='mqtt', type=str, + help='The MQTT broker address') + + args = parser.parse_args() + + async def test(): + """ The actual testing being completed. """ + interface = await Miniconf.create(args.prefix, args.broker) + telemetry = await TelemetryReader.create(args.prefix, args.broker) + + # Test loopback with a static 1V output of the DACs. + await test_loopback(interface, telemetry, 1.0) + + # Repeat test with AFE = 2x + print('Configuring AFEs to 2x input') + assert (await interface.command('afe/0', "G2"))['code'] == 0 + assert (await interface.command('afe/1', "G2"))['code'] == 0 + await test_loopback(interface, telemetry, 1.0) + + # Test with 0V output + await test_loopback(interface, telemetry, 0.0) + + loop = asyncio.get_event_loop() + sys.exit(loop.run_until_complete(test())) + + +if __name__ == '__main__': + main() diff --git a/miniconf-py/README.md b/miniconf-py/README.md new file mode 100644 index 0000000..ebfa798 --- /dev/null +++ b/miniconf-py/README.md @@ -0,0 +1,6 @@ +# Miniconf Python Utility + +This directory contains a Python package for interacting with Miniconf utilities. + +## Installation +Run `pip install .` from this directory to install the `miniconf` package. diff --git a/miniconf-py/miniconf.egg-info/PKG-INFO b/miniconf-py/miniconf.egg-info/PKG-INFO new file mode 100644 index 0000000..5d1def7 --- /dev/null +++ b/miniconf-py/miniconf.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: miniconf +Version: 1.0.0 +Summary: Utilities for configuring Miniconf-configurable devices +Home-page: https://github.com/quartiq/miniconf +Author: Ryan Summers, Robert Jördens +Author-email: UNKNOWN +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN diff --git a/miniconf-py/miniconf.egg-info/SOURCES.txt b/miniconf-py/miniconf.egg-info/SOURCES.txt new file mode 100644 index 0000000..76eb2ab --- /dev/null +++ b/miniconf-py/miniconf.egg-info/SOURCES.txt @@ -0,0 +1,8 @@ +setup.py +miniconf/__init__.py +miniconf/miniconf.py +miniconf.egg-info/PKG-INFO +miniconf.egg-info/SOURCES.txt +miniconf.egg-info/dependency_links.txt +miniconf.egg-info/requires.txt +miniconf.egg-info/top_level.txt \ No newline at end of file diff --git a/miniconf-py/miniconf.egg-info/dependency_links.txt b/miniconf-py/miniconf.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/miniconf-py/miniconf.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/miniconf-py/miniconf.egg-info/requires.txt b/miniconf-py/miniconf.egg-info/requires.txt new file mode 100644 index 0000000..a3d7fef --- /dev/null +++ b/miniconf-py/miniconf.egg-info/requires.txt @@ -0,0 +1 @@ +gmqtt diff --git a/miniconf-py/miniconf.egg-info/top_level.txt b/miniconf-py/miniconf.egg-info/top_level.txt new file mode 100644 index 0000000..6919ef8 --- /dev/null +++ b/miniconf-py/miniconf.egg-info/top_level.txt @@ -0,0 +1 @@ +miniconf diff --git a/miniconf-py/miniconf/__init__.py b/miniconf-py/miniconf/__init__.py new file mode 100644 index 0000000..1d47069 --- /dev/null +++ b/miniconf-py/miniconf/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/python3 +from .miniconf import Miniconf + +__version__ = '1.0.0' diff --git a/miniconf-py/miniconf/__main__.py b/miniconf-py/miniconf/__main__.py new file mode 100644 index 0000000..fcc7078 --- /dev/null +++ b/miniconf-py/miniconf/__main__.py @@ -0,0 +1,53 @@ +#!/usr/bin/python3 + +import sys +import argparse +from .miniconf import Miniconf + + +def main(): + """ Main program entry point. """ + parser = argparse.ArgumentParser( + description='Miniconf command line interface.', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog='''Examples: +%(prog)s dt/sinara/dual-iir/00-11-22-33-aa-bb iir_ch/0/0=\ +'{"y_min":-32767,"y_max":32767,"y_offset":0,"ba":[1.0,0,0,0,0]}' +%(prog)s dt/sinara/lockin/00-11-22-33-aa-bb afe/0='"G2"'\ +''') + parser.add_argument('-v', '--verbose', action='count', default=0, + help='Increase logging verbosity') + parser.add_argument('--broker', '-b', default='mqtt', type=str, + help='The MQTT broker address') + parser.add_argument('--no-retain', '-n', default=False, + action='store_true', + help='Do not retain the affected settings') + parser.add_argument('prefix', type=str, + help='The MQTT topic prefix of the target') + parser.add_argument('settings', metavar="PATH=VALUE", nargs='+', + help='JSON encoded values for settings path keys.') + + args = parser.parse_args() + + logging.basicConfig( + format='%(asctime)s [%(levelname)s] %(name)s: %(message)s', + level=logging.WARN - 10*args.verbose) + + loop = asyncio.get_event_loop() + + async def configure_settings(): + interface = await Miniconf.create(args.prefix, args.broker) + for setting in args.settings: + path, value = setting.split("=", 1) + response = await interface.command(path, json.loads(value), + not args.no_retain) + print(f'{path}: {response}') + if response['code'] != 0: + return response['code'] + return 0 + + sys.exit(loop.run_until_complete(configure_settings())) + + +if __name__ == '__main__': + main() diff --git a/miniconf-py/miniconf/__pycache__/__init__.cpython-38.pyc b/miniconf-py/miniconf/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e3ce58f0aecb3d21ee2b058f44250db3e6184d9 GIT binary patch literal 233 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 r5KGiZ5MLM)u|EKO`