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;