From 251c8b20335756794668a34176e5c2a762ef3c98 Mon Sep 17 00:00:00 2001 From: f4exb Date: Fri, 26 Nov 2021 15:21:01 +0100 Subject: [PATCH] Scripts API: added dump.py and updated config.py to be able to do basic instance save and restore --- scriptsapi/Readme.md | 24 ++++ scriptsapi/config.py | 62 ++++++++++ scriptsapi/dump.py | 274 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 360 insertions(+) create mode 100755 scriptsapi/dump.py diff --git a/scriptsapi/Readme.md b/scriptsapi/Readme.md index 5073bae39..423d74010 100644 --- a/scriptsapi/Readme.md +++ b/scriptsapi/Readme.md @@ -70,6 +70,28 @@ Options are: - `-f` or `--frequency` device center frequency (kHz). Mandatory. Ex: 491500 - `-s` or `--symbol-rate` symbol rate (kS/s). Mandatory. Ex: 1500 +

dump.py

+ +Dumps an instance setup to a sequence of commands in a JSON file that can be used as input to `config.py` (see next). The setup commands comprise so far: + + - Deviceset instantiations (with default device) + - Deviceset device change (will get the first device of the type) + - Deviceset device settings + - Deviceset main spectrum settings + - Deviceset channels instantiations + - Deviceset channels settings + - Featureset instantiations + - Featureset features instantiations + - Featureset features settings + +Of course the exact scope of settings that can be manipulated depends of the settings covered by the API. + +Options are: + + - `-h` or `--help` show help message and exit + - `-a` or `--address` address and port of SDRangel instance. Default is `127.0.0.1:8091` + - `-j` or `--json-file` JSON output file where commands are stored - mandatory. +

config.py

Sends a sequence of commands recorded in a JSON file which is in the form of a list of commands. @@ -79,6 +101,8 @@ Options are: - `-h` or `--help` show help message and exit - `-a` or `--address` address and port of SDRangel instance. Default is `127.0.0.1:8091` - `-j` or `--json-file` JSON file containing description of API commands + - `-i` or `--init` Initialize instance before running script. This is useful when running a sequence of initialization commands + - `-1` or `--ignore-first-posts`. Ignore first deviceset or featureset post in sequence. This is useful when running a sequence of initialization commands for a GUI instance Each command in the JSON file is a JSON document with the following keys: diff --git a/scriptsapi/config.py b/scriptsapi/config.py index 5e2c3f9bc..ed8232cc2 100755 --- a/scriptsapi/config.py +++ b/scriptsapi/config.py @@ -16,6 +16,9 @@ import requests, traceback, sys, json, time from optparse import OptionParser base_url = "http://127.0.0.1:8091/sdrangel" +app_gui = False +nb_devicesets = 0 +nb_featuresets = 0 requests_methods = { "GET": requests.get, @@ -31,6 +34,8 @@ def getInputOptions(): parser = OptionParser(usage="usage: %%prog [-t]\n") parser.add_option("-a", "--address", dest="address", help="Address and port. Default: 127.0.0.1:8091", metavar="ADDRESS", type="string") parser.add_option("-j", "--json-file", dest="json_file", help="JSON file containing commands. Mandatory", metavar="FILE", type="string") + parser.add_option("-i", "--init", dest="initialize", help="Initialize instance before running script", action="store_true") + parser.add_option("-1", "--ignore-first-posts", dest="ignore_first_posts", help="Ignore first deviceset or featureset post in sequence", action="store_true") (options, args) = parser.parse_args() @@ -39,6 +44,45 @@ def getInputOptions(): return options +# ====================================================================== +def get_instance_details(): + global app_gui + global nb_devicesets + global nb_featuresets + r = requests_methods["GET"](url=base_url) + if r.status_code // 100 == 2: + app_gui = r.json()["appname"] == "SDRangel" + nb_devicesets = r.json()["devicesetlist"]["devicesetcount"] + nb_featuresets = r.json()["featuresetlist"]["featuresetcount"] + +# ====================================================================== +def initialize(): + global nb_devicesets + global nb_featuresets + for i_ds in reversed(range(nb_devicesets)): + if app_gui and i_ds == 0: + r_ds = requests_methods["GET"](url=f"{base_url}/deviceset/0") + if r_ds.status_code // 100 == 2: + nb_channels = r_ds.json()["channelcount"] + for i_chan in reversed(range(nb_channels)): + requests_methods["DELETE"](url=f"{base_url}/deviceset/0/channel/{i_chan}") + else: + r_del = requests_methods["DELETE"](url=f"{base_url}/deviceset") + if r_del.status_code // 100 == 2: + nb_devicesets -= 1 + for i_fs in reversed(range(nb_featuresets)): + if app_gui and i_fs == 0: + r_fs = requests_methods["GET"](url=f"{base_url}/featureset/0") + if r_fs.status_code // 100 == 2: + nb_features = r_fs.json()["featurecount"] + for i_feat in reversed(range(nb_features)): + requests_methods["DELETE"](url=f"{base_url}/featureset/0/feature/{i_feat}") + else: + r_del = requests_methods["DELETE"](url=f"{base_url}/featureset") + if r_del.status_code // 100 == 2: + nb_featuresets -= 1 + + # ====================================================================== def main(): try: @@ -47,6 +91,14 @@ def main(): global base_url base_url = "http://%s/sdrangel" % options.address + get_instance_details() + + if options.initialize: + initialize() + + nb_featureset_posts = 0 + nb_deviceset_posts = 0 + with open(options.json_file) as json_file: commands = json.load(json_file) for command in commands: @@ -55,6 +107,16 @@ def main(): continue url = base_url + endpoint http_method = command.get('method', None) + if endpoint == "/deviceset" and http_method == "POST": + nb_deviceset_posts += 1 + if nb_deviceset_posts == 1 and options.ignore_first_posts: + print("First deviceset creation ignored") + continue + if endpoint == "/featureset" and http_method == "POST": + nb_featureset_posts += 1 + if nb_featureset_posts == 1 and options.ignore_first_posts: + print("First featureset creation ignored") + continue method = requests_methods.get(http_method, None) if http_method is not None else None if method is not None: request_params = command.get('params', None) diff --git a/scriptsapi/dump.py b/scriptsapi/dump.py new file mode 100755 index 000000000..eadcc1882 --- /dev/null +++ b/scriptsapi/dump.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +""" +Dumps a SDRangel instance configuration to a sequence of commands in a JSON file +compatible with config.py + +Each command is a JSON document with the following keys: + - endpoint: URL suffix (API function) - mandatory + - method: HTTP method (GET, PATCH, POST, PUT, DELETE) - mandatory + - params: pairs of argument and values - optional + - payload: request body in JSON format - optional + - msg: descriptive message for console display - optional + - delay: delay in milliseconds after command - optional +""" + +import requests, traceback, sys, json, time +from optparse import OptionParser + +base_url = "http://127.0.0.1:8091/sdrangel" + + +# ====================================================================== +class Instance: + def __init__(self): + self.devicesets = [] + self.featuresets = [] + + def add_deviceset(self, json_data): + ds = DeviceSet(len(self.devicesets), json_data["direction"], json_data["deviceHwType"], json_data) + self.devicesets.append(ds) + return ds + + def add_featureset(self, json_data): + fs = FeatureSet(len(self.featuresets)) + self.featuresets.append(fs) + return fs + + def add_deviceset_item(self, direction): + return { + "endpoint": "/deviceset", + "method": "POST", + "params": {"direction": direction}, + "msg": f"Add device set" + } + + def add_featureset_item(self): + return { + "endpoint": "/featureset", + "method": "POST", + "msg": f"Add feature set" + } + + def get_config_items(self): + items = [] + for i_ds, deviceset in enumerate(self.devicesets): + items.append(self.add_deviceset_item(deviceset.direction)) + items.append(deviceset.change_device_item()) + items.append(deviceset.set_device_item()) + for channel in deviceset.channels: + items.append(channel.add_channel_item()) + items.append(channel.set_channel_item()) + items.append(deviceset.set_spectrum_item()) + for i_fs, featureset in enumerate(self.featuresets): + items.append(self.add_featureset_item()) + for feature in featureset.features: + items.append(feature.add_feature_item()) + items.append(feature.set_feature_item()) + return items + +# ====================================================================== +class DeviceSet: + def __init__(self, index, direction, hwType, settings): + self.index = index + self.direction = direction + self.hwType = hwType + self.device_settings = settings + self.spectrum_settings = {} + self.channels = [] + + def set_spectrum_settings(self, settings): + self.spectrum_settings = settings + + def add_channel(self, json_data): + ch = Channel(self.index, len(self.channels), json_data["direction"], json_data["channelType"], json_data) + self.channels.append(ch) + return ch + + def change_device_item(self): + return { + "endpoint": f"/deviceset/{self.index}/device", + "method": "PUT", + "payload": { + "hwType": self.hwType, + "direction": self.direction + }, + "msg": f"Change device to {self.hwType} on device set {self.index}" + } + + def set_device_item(self): + return { + "endpoint": f"/deviceset/{self.index}/device/settings", + "method": "PUT", + "payload": self.device_settings, + "msg": f"Setup device {self.hwType} on device set {self.index}" + } + + def set_spectrum_item(self): + return { + "endpoint": f"/deviceset/{self.index}/spectrum/settings", + "method": "PUT", + "payload": self.spectrum_settings, + "msg": f"Setup spectrum on device set {self.index}" + } + + def delete_all_channels_items(self): + nb_channels = len(self.channels) + items = [] + for i_chan in reversed(range(nb_channels)): + items.append({ + "endpoint": f"/deviceset/{self.index}/channel/{i_chan}", + "method": "DELETE", + "msg": f"Delete channel at index {i_chan} in device set {self.index}" + }) + return items + +# ====================================================================== +class Channel: + def __init__(self, ds_index, index, direction, type, settings): + self.ds_index = ds_index + self.index = index + self.direction = direction + self.type = type + self.channel_settings = settings + + def add_channel_item(self): + return { + "endpoint": f"/deviceset/{self.ds_index}/channel", + "method": "POST", + "payload": { + "channelType": self.type, + "direction": self.direction + }, + "msg": f"Add channel {self.type} in device set index {self.ds_index}" + } + + def set_channel_item(self): + return { + "endpoint": f"/deviceset/{self.ds_index}/channel/{self.index}/settings", + "method": "PUT", + "payload": self.channel_settings, + "msg": f"Setup channel {self.type} at {self.index} in device set {self.ds_index}" + } + +# ====================================================================== +class FeatureSet: + def __init__(self, index): + self.index = index + self.features = [] + + def add_feature(self, json_data): + feat = Feature(self.index, len(self.features), json_data["featureType"], json_data) + self.features.append(feat) + return feat + + def delete_all_features_items(self): + nb_features = len(self.features) + items = [] + for i_feat in reversed(range(nb_features)): + items.append({ + "endpoint": f"/featureset/{self.index}/feature/{i_feat}", + "method": "DELETE", + "msg": f"Delete feature at index {i_feat} in feature set {self.index}" + }) + return items + +# ====================================================================== +class Feature: + def __init__(self, fs_index, index, type, settings): + self.fs_index = fs_index + self.index = index + self.type = type + self.feature_settings = settings + + def add_feature_item(self): + return { + "endpoint": f"/featureset/{self.fs_index}/feature", + "method": "POST", + "payload": { + "featureType": self.type + }, + "msg": f"Add feature {self.type} in feature set index {self.fs_index}" + } + + def set_feature_item(self): + return { + "endpoint": f"/featureset/{self.fs_index}/feature/{self.index}/settings", + "method": "PUT", + "payload": self.feature_settings, + "msg": f"Setup feature {self.type} at {self.index} in feature set {self.fs_index}" + } + + +# ====================================================================== +def getInputOptions(): + + parser = OptionParser(usage="usage: %%prog [-t]\n") + parser.add_option("-a", "--address", dest="address", help="Address and port. Default: 127.0.0.1:8091", metavar="ADDRESS", type="string") + parser.add_option("-j", "--json-file", dest="json_file", help="JSON output file where commands are stored. Mandatory", metavar="FILE", type="string") + + (options, args) = parser.parse_args() + + if (options.address == None): + options.address = "127.0.0.1:8091" + + return options + + +# ====================================================================== +def dump(): + instance = None + r = requests.get(url=f"{base_url}") + if r.status_code // 100 == 2: + instance = Instance() + nb_devicesets = r.json()["devicesetlist"]["devicesetcount"] + for i_deviceset in range(nb_devicesets): + r_ds = requests.get(url=f"{base_url}/deviceset/{i_deviceset}") + if r_ds.status_code // 100 == 2: + nb_channels = r_ds.json()["channelcount"] + r_dev = requests.get(url=f"{base_url}/deviceset/{i_deviceset}/device/settings") + if r_dev.status_code // 100 == 2: + ds = instance.add_deviceset(r_dev.json()) + for i_channel in range(nb_channels): + r_chan = requests.get(url=f"{base_url}/deviceset/{i_deviceset}/channel/{i_channel}/settings") + if r_chan.status_code // 100 == 2: + ds.add_channel(r_chan.json()) + r_spec = requests.get(url=f"{base_url}/deviceset/{i_deviceset}/spectrum/settings") + if r_spec.status_code // 100 == 2: + ds.set_spectrum_settings(r_spec.json()) + nb_featuresets = r.json()["featuresetlist"]["featuresetcount"] + for i_featureset in range(nb_featuresets): + r_fs = requests.get(url=f"{base_url}/featureset/{i_featureset}") + if r_fs.status_code // 100 == 2: + nb_features = r_fs.json()["featurecount"] + fs = instance.add_featureset(r_fs.json()) + for i_feature in range(nb_features): + r_feat = requests.get(url=f"{base_url}/featureset/{i_featureset}/feature/{i_feature}/settings") + if r_feat.status_code // 100 == 2: + fs.add_feature(r_feat.json()) + return instance + + +# ====================================================================== +def main(): + try: + options = getInputOptions() + + global base_url + base_url = "http://%s/sdrangel" % options.address + + instance = dump() + json_items = instance.get_config_items() + + with open(options.json_file,'w') as json_file: + json.dump(json_items, json_file, indent=2) + + print("All done!") + + except Exception as ex: + tb = traceback.format_exc() + print(tb, file=sys.stderr) + + +# ====================================================================== +if __name__ == "__main__": + main()