-        

+        
         
-            

-            

+            

+            
             
                 
                 
@@ -13,10 +13,10 @@
                 
              
             
-                
-                
-                
-                
Map
+                
+                
+                
+                
Map
                 ${settingslink}
             
         
diff --git a/htdocs/lib/DemodulatorPanel.js b/htdocs/lib/DemodulatorPanel.js
index bfd3619..439a8e5 100644
--- a/htdocs/lib/DemodulatorPanel.js
+++ b/htdocs/lib/DemodulatorPanel.js
@@ -222,7 +222,7 @@ DemodulatorPanel.prototype.transformHashParams = function(params) {
     var ret = {
         mod: params.secondary_mod || params.mod
     };
-    if (params.offset_frequency) ret.offset_frequency = params.offset_frequency;
+    if (typeof(params.offset_frequency) !== 'undefined') ret.offset_frequency = params.offset_frequency;
     if (typeof(params.sql) !== 'undefined') ret.squelch_level = parseInt(params.sql);
     return ret;
 };
diff --git a/htdocs/lib/Header.js b/htdocs/lib/Header.js
index d1c6a30..cced6b6 100644
--- a/htdocs/lib/Header.js
+++ b/htdocs/lib/Header.js
@@ -6,6 +6,7 @@ function Header(el) {
     });
 
     this.init_rx_photo();
+    this.download_details();
 };
 
 Header.prototype.setDetails = function(details) {
@@ -57,6 +58,13 @@ Header.prototype.toggle_rx_photo = function(ev) {
     }
 };
 
+Header.prototype.download_details = function() {
+    var self = this;
+    $.ajax('api/receiverdetails').done(function(data){
+        self.setDetails(data);
+    });
+};
+
 $.fn.header = function() {
     if (!this.data('header')) {
         this.data('header', new Header(this));
diff --git a/htdocs/lib/settings/Input.js b/htdocs/lib/settings/Input.js
new file mode 100644
index 0000000..f638257
--- /dev/null
+++ b/htdocs/lib/settings/Input.js
@@ -0,0 +1,138 @@
+function Input(name, value, options) {
+    this.name = name;
+    this.value = value;
+    this.options = options;
+    this.label = options && options.label || name;
+};
+
+Input.prototype.getClasses = function() {
+    return ['form-control', 'form-control-sm'];
+}
+
+Input.prototype.bootstrapify = function(input) {
+    this.getClasses().forEach(input.addClass.bind(input));
+    return [
+        '
'
+    ].join('');
+};
+
+function TextInput() {
+    Input.apply(this, arguments);
+};
+
+TextInput.prototype = new Input();
+
+TextInput.prototype.render = function() {
+    return this.bootstrapify($('
'));
+}
+
+function NumberInput() {
+    Input.apply(this, arguments);
+};
+
+NumberInput.prototype = new Input();
+
+NumberInput.prototype.render = function() {
+    return this.bootstrapify($('
'));
+};
+
+function SoapyGainInput() {
+    Input.apply(this, arguments);
+}
+
+SoapyGainInput.prototype = new Input();
+
+SoapyGainInput.prototype.getClasses = function() {
+    return [];
+};
+
+SoapyGainInput.prototype.render = function(){
+    var markup = $(
+        '
' +
+        '
' +
+        this.options.gains.map(function(g){
+            return '
';
+        }).join('')
+    );
+    var el = $(this.bootstrapify(markup))
+    var setMode = function(mode){
+        el.find('select').val(mode);
+        el.find('.option').hide();
+        el.find('.gain-mode-' + mode).show();
+    };
+    el.on('change', 'select', function(){
+        var mode = $(this).val();
+        setMode(mode);
+    });
+    if (typeof(this.value) === 'number') {
+        setMode('single');
+        el.find('.gain-mode-single input').val(this.value);
+    } else if (typeof(this.value) === 'string') {
+        if (this.value === 'auto') {
+            setMode('auto');
+        } else {
+            setMode('separate');
+            values = $.extend.apply($, this.value.split(',').map(function(seg){
+                var split = seg.split('=');
+                if (split.length < 2) return;
+                var res = {};
+                res[split[0]] = parseInt(split[1]);
+                return res;
+            }));
+            el.find('.gain-mode-separate input').each(function(){
+                var $input = $(this);
+                var g = $input.data('gain');
+                $input.val(g in values ? values[g] : 0);
+            });
+        }
+    } else {
+        setMode('auto');
+    }
+    return el;
+};
+
+function ProfileInput() {
+    Input.apply(this, arguments);
+};
+
+ProfileInput.prototype = new Input();
+
+ProfileInput.prototype.render = function() {
+    return $('
Profiles
');
+};
+
+function SchedulerInput() {
+    Input.apply(this, arguments);
+};
+
+SchedulerInput.prototype = new Input();
+
+SchedulerInput.prototype.render = function() {
+    return $('
Scheduler
');
+};
diff --git a/htdocs/lib/settings/SdrDevice.js b/htdocs/lib/settings/SdrDevice.js
new file mode 100644
index 0000000..25f85c9
--- /dev/null
+++ b/htdocs/lib/settings/SdrDevice.js
@@ -0,0 +1,252 @@
+function SdrDevice(el, data) {
+    this.el = el;
+    this.data = data;
+    this.inputs = {};
+    this.render();
+
+    var self = this;
+    el.on('click', '.fieldselector .btn', function() {
+        var key = el.find('.fieldselector select').val();
+        self.data[key] = self.getInitialValue(key);
+        self.render();
+    });
+};
+
+SdrDevice.create = function(el) {
+    var data = JSON.parse(decodeURIComponent(el.data('config')));
+    var type = data.type;
+    var constructor = SdrDevice.types[type] || SdrDevice;
+    return new constructor(el, data);
+};
+
+SdrDevice.prototype.getData = function() {
+    return $.extend(new Object(), this.getDefaults(), this.data);
+};
+
+SdrDevice.prototype.getDefaults = function() {
+    var defaults = {}
+    $.each(this.getMappings(), function(k, v) {
+        if (!v.includeInDefault) return;
+        defaults[k] = 'initialValue' in v ? v['initialValue'] : false;
+    });
+    return defaults;
+};
+
+SdrDevice.prototype.getMappings = function() {
+    return {
+        "name": {
+            constructor: TextInput,
+            inputOptions: {
+                label: "Name"
+            },
+            initialValue: "",
+            includeInDefault: true
+        },
+        "type": {
+            constructor: TextInput,
+            inputOptions: {
+                label: "Type"
+            },
+            initialValue: '',
+            includeInDefault: true
+        },
+        "ppm": {
+            constructor: NumberInput,
+            inputOptions: {
+                label: "PPM"
+            },
+            initialValue: 0
+        },
+        "profiles": {
+            constructor: ProfileInput,
+            inputOptions: {
+                label: "Profiles"
+            },
+            initialValue: [],
+            includeInDefault: true,
+            position: 100
+        },
+        "scheduler": {
+            constructor: SchedulerInput,
+            inputOptions: {
+                label: "Scheduler",
+            },
+            initialValue: {},
+            position: 101
+        },
+        "rf_gain": {
+            constructor: TextInput,
+            inputOptions: {
+                label: "Gain",
+            },
+            initialValue: 0
+        }
+    };
+};
+
+SdrDevice.prototype.getMapping = function(key) {
+    var mappings = this.getMappings();
+    return mappings[key];
+};
+
+SdrDevice.prototype.getInputClass = function(key) {
+    var mapping = this.getMapping(key);
+    return mapping && mapping.constructor || TextInput;
+};
+
+SdrDevice.prototype.getInitialValue = function(key) {
+    var mapping = this.getMapping(key);
+    return mapping && ('initialValue' in mapping) ? mapping['initialValue'] : false;
+};
+
+SdrDevice.prototype.getPosition = function(key) {
+    var mapping = this.getMapping(key);
+    return mapping && mapping.position || 10;
+};
+
+SdrDevice.prototype.getInputOptions = function(key) {
+    var mapping = this.getMapping(key);
+    return mapping && mapping.inputOptions || {};
+};
+
+SdrDevice.prototype.getLabel = function(key) {
+    var options = this.getInputOptions(key);
+    return options && options.label || key;
+};
+
+SdrDevice.prototype.render = function() {
+    var self = this;
+    self.el.empty();
+    var data = this.getData();
+    Object.keys(data).sort(function(a, b){
+        return self.getPosition(a) - self.getPosition(b);
+    }).forEach(function(key){
+        var value = data[key];
+        var inputClass = self.getInputClass(key);
+        var input = new inputClass(key, value, self.getInputOptions(key));
+        self.inputs[key] = input;
+        self.el.append(input.render());
+    });
+    self.el.append(this.renderFieldSelector());
+};
+
+SdrDevice.prototype.renderFieldSelector = function() {
+    var self = this;
+    return '
' +
+        '
Add new configuration options' +
+        '' +
+    '
';
+};
+
+RtlSdrDevice = function() {
+    SdrDevice.apply(this, arguments);
+};
+
+RtlSdrDevice.prototype = Object.create(SdrDevice.prototype);
+RtlSdrDevice.prototype.constructor = RtlSdrDevice;
+
+RtlSdrDevice.prototype.getMappings = function() {
+    var mappings = SdrDevice.prototype.getMappings.apply(this, arguments);
+    return $.extend(new Object(), mappings, {
+        "device": {
+            constructor: TextInput,
+            inputOptions:{
+                label: "Serial number"
+            },
+            initialValue: ""
+        }
+    });
+};
+
+SoapySdrDevice = function() {
+    SdrDevice.apply(this, arguments);
+};
+
+SoapySdrDevice.prototype = Object.create(SdrDevice.prototype);
+SoapySdrDevice.prototype.constructor = SoapySdrDevice;
+
+SoapySdrDevice.prototype.getMappings = function() {
+    var mappings = SdrDevice.prototype.getMappings.apply(this, arguments);
+    return $.extend(new Object(), mappings, {
+        "device": {
+            constructor: TextInput,
+            inputOptions:{
+                label: "Soapy device selector"
+            },
+            initialValue: ""
+        },
+        "rf_gain": {
+            constructor: SoapyGainInput,
+            initialValue: 0,
+            inputOptions: {
+                label: "Gain",
+                gains: this.getGains()
+            }
+        }
+    });
+};
+
+SoapySdrDevice.prototype.getGains = function() {
+    return [];
+};
+
+SdrplaySdrDevice = function() {
+    SoapySdrDevice.apply(this, arguments);
+};
+
+SdrplaySdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
+SdrplaySdrDevice.prototype.constructor = SdrplaySdrDevice;
+
+SdrplaySdrDevice.prototype.getGains = function() {
+    return ['RFGR', 'IFGR'];
+};
+
+AirspyHfSdrDevice = function() {
+    SoapySdrDevice.apply(this, arguments);
+};
+
+AirspyHfSdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
+AirspyHfSdrDevice.prototype.constructor = AirspyHfSdrDevice;
+
+AirspyHfSdrDevice.prototype.getGains = function() {
+    return ['RF', 'VGA'];
+};
+
+HackRfSdrDevice = function() {
+    SoapySdrDevice.apply(this, arguments);
+};
+
+HackRfSdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
+HackRfSdrDevice.prototype.constructor = HackRfSdrDevice;
+
+HackRfSdrDevice.prototype.getGains = function() {
+    return ['LNA', 'VGA', 'AMP'];
+};
+
+SdrDevice.types = {
+    'rtl_sdr': RtlSdrDevice,
+    'sdrplay': SdrplaySdrDevice,
+    'airspyhf': AirspyHfSdrDevice,
+    'hackrf': HackRfSdrDevice
+};
+
+$.fn.sdrdevice = function() {
+    return this.map(function(){
+        var el = $(this);
+        if (!el.data('sdrdevice')) {
+            el.data('sdrdevice', SdrDevice.create(el));
+        }
+        return el.data('sdrdevice');
+    });
+};
diff --git a/htdocs/login.html b/htdocs/login.html
index 9cbeacc..4f4c554 100644
--- a/htdocs/login.html
+++ b/htdocs/login.html
@@ -3,7 +3,7 @@
 
     
OpenWebRX Login
     
-    
+    
     
     
     
@@ -21,7 +21,7 @@
                 
                 
             
-            
+            
         
      
 
\ No newline at end of file
diff --git a/htdocs/sdrsettings.html b/htdocs/sdrsettings.html
index 08664fe..74aa8b7 100644
--- a/htdocs/sdrsettings.html
+++ b/htdocs/sdrsettings.html
@@ -3,11 +3,9 @@
 
     
-                    
-                    
-                        device settings go here
-                    
+        return "".join(self.render_device(key, value) for key, value in Config.get()["sdrs"].items())
+
+    def render_device(self, device_id, config):
+        return """
+            
+                
-            """.format(device_name=config["name"])
-        return "".join(render_devicde(key, value) for key, value in Config.get()["sdrs"].items())
+                
+                    {form}
+                
+            
+        """.format(device_name=config["name"], form=self.render_form(device_id, config))
+
+    def render_form(self, device_id, config):
+        return """
+            
+        """.format(device_id=device_id, formdata=quote(json.dumps(config)))
 
     def indexAction(self):
         self.serve_template("sdrsettings.html", **self.template_variables())
@@ -236,18 +244,6 @@ class GeneralSettingsController(AdminController):
                 infotext="This callsign will be used to send spots to pskreporter.info",
             ),
         ),
-        Section(
-            "sdr.hu",
-            TextInput(
-                "sdrhu_key",
-                "sdr.hu key",
-                infotext='Please obtain your personal key on 
sdr.hu',
-            ),
-            CheckboxInput(
-                "sdrhu_public_listing", "List on sdr.hu", "List my receiver on sdr.hu"
-            ),
-            TextInput("server_hostname", "Hostname"),
-        ),
     ]
 
     def render_sections(self):
diff --git a/owrx/controllers/status.py b/owrx/controllers/status.py
index 61f7102..beded28 100644
--- a/owrx/controllers/status.py
+++ b/owrx/controllers/status.py
@@ -9,26 +9,6 @@ import pkg_resources
 
 
 class StatusController(Controller):
-    def indexAction(self):
-        pm = Config.get()
-        # convert to old format
-        gps = (pm["receiver_gps"]["lat"], pm["receiver_gps"]["lon"])
-        avatar_path = pkg_resources.resource_filename("htdocs", "gfx/openwebrx-avatar.png")
-        # TODO keys that have been left out since they are no longer simple strings: sdr_hw, bands, antenna
-        vars = {
-            "status": "active",
-            "name": pm["receiver_name"],
-            "op_email": pm["receiver_admin"],
-            "users": ClientRegistry.getSharedInstance().clientCount(),
-            "users_max": pm["max_clients"],
-            "gps": gps,
-            "asl": pm["receiver_asl"],
-            "loc": pm["receiver_location"],
-            "sw_version": openwebrx_version,
-            "avatar_ctime": os.path.getctime(avatar_path),
-        }
-        self.send_response("\n".join(["{key}={value}".format(key=key, value=value) for key, value in vars.items()]))
-
     def getProfileStats(self, profile):
         return {
             "name": profile["name"],
@@ -45,7 +25,7 @@ class StatusController(Controller):
         }
         return stats
 
-    def jsonAction(self):
+    def indexAction(self):
         pm = Config.get()
 
         status = {
diff --git a/owrx/controllers/template.py b/owrx/controllers/template.py
index 23c1d17..3d57861 100644
--- a/owrx/controllers/template.py
+++ b/owrx/controllers/template.py
@@ -23,7 +23,7 @@ class WebpageController(TemplateController):
         settingslink = ""
         pm = Config.get()
         if "webadmin_enabled" in pm and pm["webadmin_enabled"]:
-            settingslink = """

Settings"""
+            settingslink = """

Settings"""
         header = self.render_template("include/header.include.html", settingslink=settingslink)
         return {"header": header}
 
diff --git a/owrx/details.py b/owrx/details.py
new file mode 100644
index 0000000..5bc7253
--- /dev/null
+++ b/owrx/details.py
@@ -0,0 +1,21 @@
+from owrx.config import Config
+from owrx.locator import Locator
+from owrx.property import PropertyFilter
+
+
+class ReceiverDetails(PropertyFilter):
+    def __init__(self):
+        super().__init__(
+            Config.get(),
+            "receiver_name",
+            "receiver_location",
+            "receiver_asl",
+            "receiver_gps",
+            "photo_title",
+            "photo_desc",
+        )
+
+    def __dict__(self):
+        receiver_info = super().__dict__()
+        receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"])
+        return receiver_info
diff --git a/owrx/feature.py b/owrx/feature.py
index 6d5b4fa..b6d83b8 100644
--- a/owrx/feature.py
+++ b/owrx/feature.py
@@ -24,12 +24,12 @@ class FeatureDetector(object):
         "rtl_sdr": ["rtl_connector"],
         "rtl_sdr_soapy": ["soapy_connector", "soapy_rtl_sdr"],
         "sdrplay": ["soapy_connector", "soapy_sdrplay"],
-        "hackrf": ["hackrf_transfer"],
+        "hackrf": ["soapy_connector", "soapy_hackrf"],
         "perseussdr": ["perseustest"],
         "airspy": ["soapy_connector", "soapy_airspy"],
         "airspyhf": ["soapy_connector", "soapy_airspyhf"],
         "lime_sdr": ["soapy_connector", "soapy_lime_sdr"],
-        "fifi_sdr": ["alsa"],
+        "fifi_sdr": ["alsa", "rockprog"],
         "pluto_sdr": ["soapy_connector", "soapy_pluto_sdr"],
         "soapy_remote": ["soapy_connector", "soapy_remote"],
         "uhd": ["soapy_connector", "soapy_uhd"],
@@ -128,26 +128,6 @@ class FeatureDetector(object):
         """
         return self.command_is_runnable("nc --help")
 
-    def has_hackrf_transfer(self):
-        """
-        To use a HackRF, compile the HackRF host tools from its "stdout" branch:
-        ```
-         git clone https://github.com/mossmann/hackrf/
-         cd hackrf
-         git fetch
-         git checkout origin/stdout
-         cd host
-         mkdir build
-         cd build
-         cmake .. -DINSTALL_UDEV_RULES=ON
-         make
-         sudo make install
-        ```
-        """
-        # TODO i don't have a hackrf, so somebody doublecheck this.
-        # TODO also check if it has the stdout feature
-        return self.command_is_runnable("hackrf_transfer --help")
-
     def has_perseustest(self):
         """
         To use a Microtelecom Perseus HF receiver, compile and
@@ -273,7 +253,7 @@ class FeatureDetector(object):
         """
         The SoapySDR module for sdrplay devices is required for interfacing with SDRPlay devices (RSP1*, RSP2*, RSPDuo)
 
-        You can get it [here](https://github.com/pothosware/SoapySDRPlay/wiki).
+        You can get it [here](https://github.com/SDRplay/SoapySDRPlay).
         """
         return self._has_soapy_driver("sdrplay")
 
@@ -342,6 +322,14 @@ class FeatureDetector(object):
         """
         return self._has_soapy_driver("radioberry")
 
+    def has_soapy_hackrf(self):
+        """
+        The SoapyHackRF allows HackRF to be used with SoapySDR.
+
+        You can get it [here](https://github.com/pothosware/SoapyHackRF/wiki).
+        """
+        return self._has_soapy_driver("hackrf")
+
     def has_dsd(self):
         """
         The digital voice modes NXDN and D-Star can be decoded by the dsd project. Please note that you need the version
@@ -396,3 +384,11 @@ class FeatureDetector(object):
         on the Alsa library. It is available as a package for most Linux distributions.
         """
         return self.command_is_runnable("arecord --help")
+
+    def has_rockprog(self):
+        """
+        The "rockprog" executable is required to send commands to your FiFiSDR. It needs to be installed separately.
+
+        You can find instructions and downloads [here](https://o28.sischa.net/fifisdr/trac/wiki/De%3Arockprog).
+        """
+        return self.command_is_runnable("rockprog")
diff --git a/owrx/http.py b/owrx/http.py
index 39e2bdf..812bec0 100644
--- a/owrx/http.py
+++ b/owrx/http.py
@@ -89,18 +89,16 @@ class Router(object):
     def __init__(self):
         self.routes = [
             StaticRoute("/", IndexController),
-            StaticRoute("/status", StatusController),
-            StaticRoute("/status.json", StatusController, options={"action": "jsonAction"}),
+            StaticRoute("/status.json", StatusController),
             RegexRoute("/static/(.+)", OwrxAssetsController),
             RegexRoute("/compiled/(.+)", CompiledAssetsController),
             RegexRoute("/aprs-symbols/(.+)", AprsSymbolsController),
             StaticRoute("/ws/", WebSocketController),
             RegexRoute("(/favicon.ico)", OwrxAssetsController),
-            # backwards compatibility for the sdr.hu portal
-            RegexRoute("(/gfx/openwebrx-avatar.png)", OwrxAssetsController),
             StaticRoute("/map", MapController),
             StaticRoute("/features", FeatureController),
             StaticRoute("/api/features", ApiController),
+            StaticRoute("/api/receiverdetails", ApiController, options={"action": "receiverDetails"}),
             StaticRoute("/metrics", MetricsController),
             StaticRoute("/settings", SettingsController),
             StaticRoute("/generalsettings", GeneralSettingsController),
diff --git a/owrx/property/__init__.py b/owrx/property/__init__.py
index 853cca3..f3560fa 100644
--- a/owrx/property/__init__.py
+++ b/owrx/property/__init__.py
@@ -40,6 +40,10 @@ class PropertyManager(ABC):
     def __dict__(self):
         pass
 
+    @abstractmethod
+    def __delitem__(self, key):
+        pass
+
     @abstractmethod
     def keys(self):
         pass
@@ -98,6 +102,9 @@ class PropertyLayer(PropertyManager):
     def __dict__(self):
         return {k: v for k, v in self.properties.items()}
 
+    def __delitem__(self, key):
+        return self.properties.__delitem__(key)
+
     def keys(self):
         return self.properties.keys()
 
@@ -132,6 +139,11 @@ class PropertyFilter(PropertyManager):
     def __dict__(self):
         return {k: v for k, v in self.pm.__dict__().items() if k in self.props}
 
+    def __delitem__(self, key):
+        if key not in self.props:
+            raise KeyError(key)
+        return self.pm.__delitem__(key)
+
     def keys(self):
         return [k for k in self.pm.keys() if k in self.props]
 
@@ -226,5 +238,9 @@ class PropertyStack(PropertyManager):
     def __dict__(self):
         return {k: self.__getitem__(k) for k in self.keys()}
 
+    def __delitem__(self, key):
+        for layer in self.layers:
+            layer["props"].__delitem__(key)
+
     def keys(self):
         return set([key for l in self.layers for key in l["props"].keys()])
diff --git a/owrx/sdrhu.py b/owrx/sdrhu.py
deleted file mode 100644
index 193399f..0000000
--- a/owrx/sdrhu.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import threading
-import time
-from owrx.config import Config
-from urllib import request, parse
-
-import logging
-
-logger = logging.getLogger(__name__)
-
-
-class SdrHuUpdater(threading.Thread):
-    def __init__(self):
-        self.doRun = True
-        super().__init__(daemon=True)
-
-    def update(self):
-        pm = Config.get().filter("server_hostname", "web_port", "sdrhu_key")
-        data = parse.urlencode({
-            "url": "http://{server_hostname}:{web_port}".format(**pm.__dict__()),
-            "apikey": pm["sdrhu_key"]
-        }).encode()
-
-        res = request.urlopen("https://sdr.hu/update", data=data)
-        if res.getcode() < 200 or res.getcode() >= 300:
-            logger.warning('sdr.hu update failed with error code %i', res.getcode())
-            return 2
-
-        returned = res.read().decode("utf-8")
-        if "UPDATE:" not in returned:
-            logger.warning("Update failed, your receiver cannot be listed on sdr.hu!")
-            return 2
-
-        value = returned.split("UPDATE:")[1].split("\n", 1)[0]
-        if value.startswith("SUCCESS"):
-            logger.info("Update succeeded!")
-        else:
-            logger.warning("Update failed, your receiver cannot be listed on sdr.hu! Reason: %s", value)
-        return 20
-
-    def run(self):
-        while self.doRun:
-            retrytime_mins = self.update()
-            time.sleep(60 * retrytime_mins)
diff --git a/owrx/service/__init__.py b/owrx/service/__init__.py
index d4c97df..b4070a1 100644
--- a/owrx/service/__init__.py
+++ b/owrx/service/__init__.py
@@ -265,6 +265,7 @@ class ServiceHandler(object):
         d.set_secondary_demodulator(mode)
         d.set_audio_compression("none")
         d.set_samp_rate(source.getProps()["samp_rate"])
+        d.set_temporary_directory(Config.get()['temporary_directory'])
         d.set_service()
         d.start()
         return d
diff --git a/owrx/source/hackrf.py b/owrx/source/hackrf.py
index f59a448..a103218 100644
--- a/owrx/source/hackrf.py
+++ b/owrx/source/hackrf.py
@@ -1,23 +1,11 @@
-from .direct import DirectSource
-from owrx.command import Option
-import time
+from .soapy import SoapyConnectorSource
 
 
-class HackrfSource(DirectSource):
-    def getCommandMapper(self):
-        return super().getCommandMapper().setBase("hackrf_transfer").setMappings(
-            {
-                "samp_rate": Option("-s"),
-                "tuner_freq": Option("-f"),
-                "rf_gain": Option("-g"),
-                "lna_gain": Option("-l"),
-                "rf_amp": Option("-a"),
-                "ppm": Option("-C"),
-            }
-        ).setStatic("-r-")
+class HackrfSource(SoapyConnectorSource):
+    def getSoapySettingsMappings(self):
+        mappings = super().getSoapySettingsMappings()
+        mappings.update({"bias_tee": "bias_tx"})
+        return mappings
 
-    def getFormatConversion(self):
-        return ["csdr convert_s8_f"]
-
-    def sleepOnRestart(self):
-        time.sleep(1)
+    def getDriver(self):
+        return "hackrf"
\ No newline at end of file
diff --git a/owrx/source/resampler.py b/owrx/source/resampler.py
index 6afe50c..1f6a4e1 100644
--- a/owrx/source/resampler.py
+++ b/owrx/source/resampler.py
@@ -1,10 +1,4 @@
 from .direct import DirectSource
-from . import SdrSource
-import subprocess
-import threading
-import os
-import socket
-import time
 
 import logging
 
@@ -29,7 +23,7 @@ class Resampler(DirectSource):
     def getCommand(self):
         return [
             "nc -v 127.0.0.1 {nc_port}".format(nc_port=self.sdr.getPort()),
-            "csdr shift_addition_cc {shift}".format(shift=self.shift),
+            "csdr shift_addfast_cc {shift}".format(shift=self.shift),
             "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING".format(
                 decimation=self.decimation, ddc_transition_bw=self.transition_bw
             ),
diff --git a/sdrhu.py b/sdrhu.py
deleted file mode 100755
index 9678a9b..0000000
--- a/sdrhu.py
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/usr/bin/python3
-"""
-
-    This file is part of OpenWebRX, 
-    an open-source SDR receiver software with a web UI.
-    Copyright (c) 2013-2015 by Andras Retzler 
-    Copyright (c) 2019-2020 by Jakob Ketterl 
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see .
-
-"""
-
-from owrx.sdrhu import SdrHuUpdater
-from owrx.config import Config
-
-import logging
-logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
-logger = logging.getLogger(__name__)
-
-if __name__ == "__main__":
-    pm = Config.get()
-
-    if "sdrhu_public_listing" not in pm or not pm["sdrhu_public_listing"]:
-        logger.error('Public listing on sdr.hu is not activated. Please check "sdrhu_public_listing" in your config.')
-        exit(1)
-    if "sdrhu_key" not in pm or pm["sdrhu_key"] is None or pm["sdrhu_key"] == "":
-        logger.error('Missing "sdrhu_key" in your config. Aborting')
-        exit(1)
-    SdrHuUpdater().update()