diff --git a/scriptsapi/Readme.md b/scriptsapi/Readme.md new file mode 100644 index 000000000..f596f7c5a --- /dev/null +++ b/scriptsapi/Readme.md @@ -0,0 +1,13 @@ +## Python scripts interfacing with the API ## + +These scripts are designed to work in Python 3 preferably with version 3.6 or higher. Dependencies are installed with pip in a virtual environment. The sequence of operations is the following: + +``` +virtualenv -p /usr/bin/python3 venv # Create virtual environment +. ./venv/bin/activate # Activate virtual environment +pip install -r requirements.txt # Install requirements +``` + +

freqtracking.py

+ +This script is used to achieve frequency tracking with the FreqTracker plugin. \ No newline at end of file diff --git a/scriptsapi/freqtracking.py b/scriptsapi/freqtracking.py new file mode 100644 index 000000000..e0baabf90 --- /dev/null +++ b/scriptsapi/freqtracking.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +''' +Frequency tracker: + +Listens on a TCP port for SDRangel reverse API requests. + - When the request comes from a FreqTracker channel it gets the FreqTracker channel frequency shift + - When the request comes from another channel it records the difference between the FreqTracker frequency shift and +this channel frequency shift. Then it will periodically send a center frequency change request to this channel with +the FreqTracker channel frequency shift plus the difference thus achieving the locking of this channel to the FreqTracker +channel frequency. + - If the reply from the channel returns an error it will un-register the channel. + +In the SDRangel instance you must activate the reverse API of the FreqTracker channel and the controlled channel(s) +reverse API giving the address and port of this instance of the script. You have to click on the small grey box at the +top left of the plugin GUI to open the channel details dialog where the reverse API can be configured. +''' + +import requests +import time +import argparse +from flask import Flask +from flask import request, jsonify + +SDRANGEL_API_ADDR = None +SDRANGEL_API_PORT = 8091 +TRACKER_OFFSET = 0 +TRACKER_DEVICE = 0 +TRACKING_DICT = {} + +app = Flask(__name__) + +# ====================================================================== +def getInputOptions(): + """ This is the argument line parser """ +# ---------------------------------------------------------------------- + parser = argparse.ArgumentParser(description="Manages PTT from an SDRangel instance automatically") + parser.add_argument("-A", "--address", dest="addr", help="listening address (default 0.0.0.0)", metavar="IP", type=str) + parser.add_argument("-P", "--port", dest="port", help="listening port (default 8000)", metavar="PORT", type=int) + parser.add_argument("-a", "--address-sdr", dest="sdrangel_address", help="SDRangel REST API address (defaults to calling address)", metavar="ADDRESS", type=str) + parser.add_argument("-p", "--port-sdr", dest="sdrangel_port", help="SDRangel REST API port (default 8091)", metavar="PORT", type=int) + + options = parser.parse_args() + + if options.addr == None: + options.addr = "0.0.0.0" + if options.port == None: + options.port = 8000 + if options.sdrangel_port == None: + options.sdrangel_port = 8091 + + return options.addr, options.port, options.sdrangel_address, options.sdrangel_port + +# ====================================================================== +def get_sdrangel_ip(request): + """ Extract originator address from request """ +# ---------------------------------------------------------------------- + if SDRANGEL_API_ADDR is not None: + return SDRANGEL_API_ADDR + if request.environ.get('HTTP_X_FORWARDED_FOR') is None: + return request.environ['REMOTE_ADDR'] + else: + return request.environ['HTTP_X_FORWARDED_FOR'] + +# ====================================================================== +def gen_dict_extract(key, var): + """ Gets a key value in a dictionnary or sub-dictionnary structure """ +# ---------------------------------------------------------------------- + if hasattr(var,'items'): + for k, v in var.items(): + if k == key: + yield v + if isinstance(v, dict): + for result in gen_dict_extract(key, v): + yield result + elif isinstance(v, list): + for d in v: + for result in gen_dict_extract(key, d): + yield result + +# ====================================================================== +def update_frequency_setting(request_content, frequency): + """ Finds the channel settings key that contains the inputFrequencyOffset key + and replace it with a single inputFrequencyOffset key with new frequency + """ +# ---------------------------------------------------------------------- + for k in request_content: + setting_item = request_content[k] + if isinstance(setting_item, dict): + if 'inputFrequencyOffset' in setting_item: + setting_item.update({ + 'inputFrequencyOffset': frequency + }) + + +# ====================================================================== +def adjust_channels(sdrangel_ip, sdrangel_port): + """ Adjust registered channels center frequencies + Remove keys for channels returning error + """ +# ---------------------------------------------------------------------- + global TRACKING_DICT + base_url = f'http://{sdrangel_ip}:{sdrangel_port}/sdrangel' + remove_keys = [] + for k in TRACKING_DICT: + device_index = k[0] + channel_index = k[1] + tracking_item = TRACKING_DICT[k] + frequency_correction = TRACKER_OFFSET - tracking_item['trackerFrequency'] + frequency = tracking_item['channelFrequency'] + frequency_correction + update_frequency_setting(tracking_item['requestContent'], frequency) + r = requests.patch(url=base_url + f'/deviceset/{device_index}/channel/{channel_index}/settings', json=tracking_item['requestContent']) + if r.status_code / 100 != 2: + remove_keys.append(k) + for k in remove_keys: + TRACKING_DICT.pop(k, None) + + +# ====================================================================== +def register_channel(device_index, channel_index, channel_frequency, request_content): + """ Register a channel or change its center frequency reference """ +# ---------------------------------------------------------------------- + global TRACKING_DICT + TRACKING_DICT.update({ + (device_index, channel_index) : { + 'channelFrequency': channel_frequency, + 'trackerFrequency': TRACKER_OFFSET, + 'requestContent': request_content + } + }) + +# ====================================================================== +@app.route('/sdrangel') +def hello_sdrangel(): + """ Just to test if it works """ +# ---------------------------------------------------------------------- + sdrangel_ip = get_sdrangel_ip(request) + print(f'SDRangel IP: {sdrangel_ip}') + return 'Hello, SDRangel!' + + +# ====================================================================== +@app.route('/sdrangel/deviceset//channel//settings', methods=['GET', 'PATCH', 'PUT']) +def channel_settings(deviceset_index, channel_index): + """ Receiving channel settings from reverse API """ +# ---------------------------------------------------------------------- + global TRACKER_OFFSET + global TRACKER_DEVICE + orig_device_index = None + orig_channel_index = None + content = request.get_json(silent=True) + if content: + orig_device_index = content.get('originatorDeviceSetIndex') + orig_channel_index = content.get('originatorChannelIndex') + if orig_device_index is None or orig_channel_index is None: + print('device_settings: SDRangel reverse API v4.5.2 or higher required. No or invalid originator information') + return "SDRangel reverse API v4.5.2 or higher required " + sdrangel_ip = get_sdrangel_ip(request) + channel_type = content.get('channelType') + for freq_offset in gen_dict_extract('inputFrequencyOffset', content): + if channel_type == "FreqTracker": + print(f'SDRangel: {sdrangel_ip}:{SDRANGEL_API_PORT} Tracker: {freq_offset} Hz') + TRACKER_OFFSET = freq_offset + TRACKER_DEVICE = orig_device_index + adjust_channels(sdrangel_ip, SDRANGEL_API_PORT) + else: + register_channel(orig_device_index, orig_channel_index, freq_offset, content) + print(f'SDRangel: {sdrangel_ip}:{SDRANGEL_API_PORT} {channel_type} [{orig_device_index}:{orig_channel_index}] at {freq_offset} Hz') + return "OK processed " + + +# ====================================================================== +def main(): + """ This is the main routine """ +# ---------------------------------------------------------------------- + global SDRANGEL_API_ADDR + global SDRANGEL_API_PORT + addr, port, SDRANGEL_API_ADDR, SDRANGEL_API_PORT = getInputOptions() + print(f'main: starting at: {addr}:{port}') + app.run(debug=True, host=addr, port=port) + + +# ====================================================================== +if __name__ == "__main__": + """ When called from command line... """ +# ---------------------------------------------------------------------- + main() diff --git a/scriptsapi/requirements.txt b/scriptsapi/requirements.txt new file mode 100644 index 000000000..b914feac2 --- /dev/null +++ b/scriptsapi/requirements.txt @@ -0,0 +1,2 @@ +requests +Flask \ No newline at end of file diff --git a/sdrbase/resources/res.qrc b/sdrbase/resources/res.qrc index 4b7d46ea0..903d46282 100644 --- a/sdrbase/resources/res.qrc +++ b/sdrbase/resources/res.qrc @@ -17,6 +17,7 @@ webapi/doc/swagger/include/FileSource.yaml webapi/doc/swagger/include/FreeDVDemod.yaml webapi/doc/swagger/include/FreeDVMod.yaml + webapi/doc/swagger/include/FreqTracker.yaml webapi/doc/swagger/include/HackRF.yaml webapi/doc/swagger/include/LimeSdr.yaml webapi/doc/swagger/include/LocalInput.yaml diff --git a/sdrbase/resources/webapi/doc/html2/index.html b/sdrbase/resources/webapi/doc/html2/index.html index 8e2cde445..e4d320a93 100644 --- a/sdrbase/resources/webapi/doc/html2/index.html +++ b/sdrbase/resources/webapi/doc/html2/index.html @@ -5950,7 +5950,7 @@ margin-bottom: 20px;