Repository: linien-org/linien (language: python)

Issue:
Use setuptools-scm
Currently not working, running `setup.py` returns:

```
root@rp-f07734:/linien-dev# server.py
/usr/local/bin/server.py: line 18: $'\r': command not found
/usr/local/bin/server.py: line 19: import: command not found
/usr/local/bin/server.py: line 20: import: command not found
/usr/local/bin/server.py: line 21: import: command not found
/usr/local/bin/server.py: line 22: import: command not found
/usr/local/bin/server.py: line 23: import: command not found
/usr/local/bin/server.py: line 24: from: command not found
/usr/local/bin/server.py: line 25: from: command not found
/usr/local/bin/server.py: line 26: $'\r': command not found
/usr/local/bin/server.py: line 27: import: command not found
/usr/local/bin/server.py: line 28: import: command not found
/usr/local/bin/server.py: line 29: import: command not found
/usr/local/bin/server.py: line 30: syntax error near unexpected token `('
'usr/local/bin/server.py: line 30: `from linien_common.common import (
```

Guess this is due to the packaging. Could maybe be worked around if the whole repo (including `.git`) is transferred to the RP.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/pyinstaller-linux2.yml b/.github/workflows/pyinstaller-linux2.yml
new file mode 100644
index 00000000..774c4f33
--- /dev/null
+++ b/.github/workflows/pyinstaller-linux2.yml
@@ -0,0 +1,47 @@
+name: Pyinstaller Linux (new)
+
+on:
+  workflow_dispatch:
+
+env:
+  QT_DEBUG_PLUGINS: 1
+  DISPLAY: ":99"
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+
+      - name: Set up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: "3.7"
+
+      - name: Install dependencies
+        run: sudo apt-get install -y qt5-default qttools5-dev-tools binutils libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 libegl1-mesa
+
+      - run: pip install pyinstaller
+
+      - name: Install linien-common
+        working-directory: linien-common
+        run: pip install .
+
+      - name: Install linien-client
+        working-directory: linien-client
+        run: pip install .
+
+      - name: Install linien-gui
+        working-directory: linien-gui
+        run: pip install .
+
+      - run: pyinstaller pyinstaller.spec
+        working-directory: linien-gui
+
+      - uses: actions/upload-artifact@v3
+        with:
+          name: linien-gui-linux
+          path: linien-gui/dist/*
diff --git a/.github/workflows/pyinstaller-windows.yml b/.github/workflows/pyinstaller-windows.yml
index f6eebef0..3cc0ced0 100644
--- a/.github/workflows/pyinstaller-windows.yml
+++ b/.github/workflows/pyinstaller-windows.yml
@@ -1,43 +1,30 @@
 name: Pyinstaller Windows
 
 on:
-  # Allows you to run this workflow manually from the Actions tab
   workflow_dispatch:
-    inputs:
-      version_name:
-        description: "Version name"
-        required: true
 
-env:
-  INPUT_VERSION_NAME: ${{ github.event.inputs.version_name }}
-
-# A workflow run is made up of one or more jobs that can run sequentially or in parallel
 jobs:
-  # This workflow contains a single job called "build"
   build:
-    # The type of runner that the job will run on
     runs-on: ubuntu-latest
 
-    # Steps represent a sequence of tasks that will be executed as part of the job
     steps:
-      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
       - uses: actions/checkout@v2
 
-      - name: create VERSION file
-        run: echo "$INPUT_VERSION_NAME" > linien/VERSION
-
-      # NOTE: we hardcode the version of requirements here because the new pip
-      #       resolver otherwise takes ages
-      # ANOTHER NOTE: scipy is fixed to 1.4.1 because of problems with pyinstaller
-      - name: create requirements file
-        run: cp requirements_gui.txt requirements.txt
+      - name: Set up Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: "3.x"
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install setuptools wheel twine
 
       - name: PyInstaller Windows
         uses: JackMcKew/pyinstaller-action-windows@main
         with:
-          path: .
+          path: linien-gui
 
       - uses: actions/upload-artifact@v2
         with:
           name: my-exe
-          path: dist/windows
+          path: linien-gui/dist/windows
diff --git a/.github/workflows/pyinstaller-windows2.yml b/.github/workflows/pyinstaller-windows2.yml
new file mode 100644
index 00000000..572f322b
--- /dev/null
+++ b/.github/workflows/pyinstaller-windows2.yml
@@ -0,0 +1,40 @@
+name: Pyinstaller Windows (new)
+
+on:
+  workflow_dispatch:
+
+jobs:
+  build:
+    runs-on: windows-latest
+
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+
+      - name: Set up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: "3.7"
+
+      - run: pip install pyinstaller
+
+      - name: Install linien-common
+        working-directory: linien-common
+        run: pip install .
+
+      - name: Install linien-client
+        working-directory: linien-client
+        run: pip install .
+
+      - name: Install linien-gui
+        working-directory: linien-gui
+        run: pip install .
+
+      - run: pyinstaller pyinstaller.spec
+        working-directory: linien-gui
+
+      - uses: actions/upload-artifact@v3
+        with:
+          name: linien-gui-windows
+          path: linien-gui/dist/*
diff --git a/.gitignore b/.gitignore
index 6f99f7bd..f9d31089 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,7 +9,6 @@ __pycache__
 /*.v
 /dist
 .eggs
-linien-*
 
 .mypy_cache
 .vscode
@@ -23,4 +22,26 @@ VERSION
 
 # results of pytest-html
 tests/*.html
-tests/assets/*
\ No newline at end of file
+tests/assets/*
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+**/_version.py
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5d5bfa1c..af8e7c59 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,13 +1,12 @@
 repos:
   - repo: https://github.com/psf/black
-    rev: 22.10.0
+    rev: 23.1.0
     hooks:
       - id: black
-        language_version: python3.7
         exclude: ^(gateware/logic/|gateware/lowlevel/|linien/server/csrmap.py)
 
   - repo: https://github.com/pycqa/isort
-    rev: 5.10.1
+    rev: 5.12.0
     hooks:
       - id: isort
         name: isort (python)
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index c47af667..00000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,2 +0,0 @@
-recursive-exclude test *
-recursive-exclude gateware *
\ No newline at end of file
diff --git a/README.md b/README.md
index 5e9b64a7..839d592e 100644
--- a/README.md
+++ b/README.md
@@ -200,7 +200,8 @@ Then, you should start the Linien server on your RedPitaya. This can be done by
 
 Once the server is up and running, you can connect using python:
 ```python
-from linien.client.connection import LinienClient, MHz, Vpp
+from linien_client.connection import LinienClient
+from linien_common.common import  MHz, Vpp
 c = LinienClient(
     {'host': 'rp-XXXXXX.local', 'username': 'root', 'password': 'change-it-to-something-else!'},
     # starts the server if it is not running
@@ -288,8 +289,8 @@ The script below shows an example of how to run the autolock using the scripting
 import pickle
 import numpy as np
 
-from linien.client.connection import LinienClient
-from linien.common import FAST_AUTOLOCK
+from linien_client.connection import LinienClient
+from linien_common.common import FAST_AUTOLOCK
 
 from matplotlib import pyplot as plt
 from time import sleep
diff --git a/scripts/bit2bin.py b/gateware/bit2bin.py
old mode 100755
new mode 100644
similarity index 100%
rename from scripts/bit2bin.py
rename to gateware/bit2bin.py
diff --git a/scripts/build_fpga_image.sh b/gateware/build_fpga_image.sh
old mode 100755
new mode 100644
similarity index 72%
rename from scripts/build_fpga_image.sh
rename to gateware/build_fpga_image.sh
index c314a6a1..e8486544
--- a/scripts/build_fpga_image.sh
+++ b/gateware/build_fpga_image.sh
@@ -14,7 +14,5 @@ fi
 
 export PATH=$VIVADOPATH:$PATH
 
-python3 scripts/fpga_image_helper.py
-
-rm linien/server/linien.bin -f
-cp fpga_build/linien.bin linien/server
\ No newline at end of file
+rm linien-server/linien_server/linien.bin -f
+python3 gateware/fpga_image_helper.py
\ No newline at end of file
diff --git a/scripts/fpga_image_helper.py b/gateware/fpga_image_helper.py
old mode 100755
new mode 100644
similarity index 80%
rename from scripts/fpga_image_helper.py
rename to gateware/fpga_image_helper.py
index 08d032f0..b50b1559
--- a/scripts/fpga_image_helper.py
+++ b/gateware/fpga_image_helper.py
@@ -18,19 +18,13 @@
 
 # this file compiles the FPGA image. You shouldn't call it directly though but
 # use `build_fpga_image.sh`
-import os
-import sys
+from pathlib import Path
 
-LINIEN_FOLDER = "/".join(os.path.abspath(__file__).split("/")[:-2])
-
-
-os.chdir(LINIEN_FOLDER)
-sys.path.append(LINIEN_FOLDER)
+REPO_ROOT_DIR = Path(__file__).resolve().parents[1]
 
 from bit2bin import bit2bin
-
-from gateware.linien import RootModule
-from gateware.platform import Platform
+from hw_platform import Platform
+from linien_module import RootModule
 
 
 def py_csrconstants(map, fil):
@@ -66,14 +60,20 @@ def py_csrmap(it, fil):
     platform = Platform()
     root = RootModule(platform)
 
-    with open("linien/server/csrmap.py", "w") as fil:
+    with open(
+        REPO_ROOT_DIR / "linien-server" / "linien_server" / "csrmap.py", "w"
+    ) as fil:
         py_csrconstants(root.linien.csrbanks.constants, fil)
         csr = get_csrmap(root.linien.csrbanks.banks)
         py_csrmap(csr, fil)
         fil.write("states = {}\n".format(repr(root.linien.state_names)))
         fil.write("signals = {}\n".format(repr(root.linien.signal_names)))
 
-    platform.add_source_dir("verilog")
-    build_dir = "fpga_build"
+    platform.add_source_dir(REPO_ROOT_DIR / "gateware" / "verilog")
+    build_dir = REPO_ROOT_DIR / "gateware" / "build"
     platform.build(root, build_name="top", build_dir=build_dir)
-    bit2bin("%s/top.bit" % build_dir, "%s/linien.bin" % build_dir, flip=True)
+    bit2bin(
+        build_dir / "top.bit",
+        REPO_ROOT_DIR / "linien-server" / "linien_server" / "linien.bin",
+        flip=True,
+    )
diff --git a/gateware/platform.py b/gateware/hw_platform.py
similarity index 98%
rename from gateware/platform.py
rename to gateware/hw_platform.py
index 48654d18..ca8f2579 100644
--- a/gateware/platform.py
+++ b/gateware/hw_platform.py
@@ -172,7 +172,7 @@ class Platform(XilinxPlatform):
     def __init__(self):
         XilinxPlatform.__init__(self, "xc7z010-clg400-1", _io, toolchain="vivado")
         self.toolchain.pre_synthesis_commands.append(
-            "read_xdc -ref processing_system7_v5_4_processing_system7 ../verilog/system_processing_system7_0_0.xdc"
+            "read_xdc -ref processing_system7_v5_4_processing_system7 ../verilog/system_processing_system7_0_0.xdc"  # noqa: E501
         )
         self.toolchain.with_phys_opt = True
 
@@ -188,7 +188,7 @@ def do_finalize(self, fragment):
             )
             for i in range(2):
                 try:
-                    adc = self.lookup_request("adc", i)
+                    _ = self.lookup_request("adc", i)
                     # self.add_platform_command("set_input_delay "
                     #    "-clock {clk} 3.4 [get_ports {data}]",
                     #    clk=clk125, data=adc[0])
diff --git a/gateware/linien.py b/gateware/linien_module.py
similarity index 99%
rename from gateware/linien.py
rename to gateware/linien_module.py
index d6ec1b2e..290b819b 100644
--- a/gateware/linien.py
+++ b/gateware/linien_module.py
@@ -17,6 +17,7 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
+from linien_common.common import ANALOG_OUT0
 from migen import (
     Array,
     Cat,
@@ -31,9 +32,7 @@
 from misoc.interconnect import csr_bus
 from misoc.interconnect.csr import AutoCSR, CSRStatus, CSRStorage
 
-from gateware.logic.autolock import FPGAAutolock
-from linien.common import ANALOG_OUT0
-
+from .logic.autolock import FPGAAutolock
 from .logic.chains import FastChain, SlowChain, cross_connect
 from .logic.decimation import Decimate
 from .logic.delta_sigma import DeltaSigma
diff --git a/gateware/logic/autolock.py b/gateware/logic/autolock.py
index 665fbec4..d060730a 100644
--- a/gateware/logic/autolock.py
+++ b/gateware/logic/autolock.py
@@ -16,11 +16,15 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
+from linien_common.common import (
+    AUTOLOCK_MAX_N_INSTRUCTIONS,
+    FAST_AUTOLOCK,
+    ROBUST_AUTOLOCK,
+)
 from migen import Array, If, Module, Signal, bits_for
 from misoc.interconnect.csr import AutoCSR, CSRStatus, CSRStorage
 
-from gateware.logic.autolock_utils import SumDiffCalculator
-from linien.common import AUTOLOCK_MAX_N_INSTRUCTIONS, FAST_AUTOLOCK, ROBUST_AUTOLOCK
+from .autolock_utils import SumDiffCalculator
 
 ROBUST_AUTOLOCK_FPGA_DELAY = 3
 
diff --git a/verilog/axi_slave.v b/gateware/verilog/axi_slave.v
similarity index 100%
rename from verilog/axi_slave.v
rename to gateware/verilog/axi_slave.v
diff --git a/verilog/bus_clk_bridge.v b/gateware/verilog/bus_clk_bridge.v
similarity index 100%
rename from verilog/bus_clk_bridge.v
rename to gateware/verilog/bus_clk_bridge.v
diff --git a/verilog/processing_system7_v5_4_processing_system7.v b/gateware/verilog/processing_system7_v5_4_processing_system7.v
similarity index 100%
rename from verilog/processing_system7_v5_4_processing_system7.v
rename to gateware/verilog/processing_system7_v5_4_processing_system7.v
diff --git a/verilog/red_pitaya_scope.v b/gateware/verilog/red_pitaya_scope.v
similarity index 100%
rename from verilog/red_pitaya_scope.v
rename to gateware/verilog/red_pitaya_scope.v
diff --git a/verilog/system_processing_system7_0_0.v b/gateware/verilog/system_processing_system7_0_0.v
similarity index 100%
rename from verilog/system_processing_system7_0_0.v
rename to gateware/verilog/system_processing_system7_0_0.v
diff --git a/verilog/system_processing_system7_0_0.xdc b/gateware/verilog/system_processing_system7_0_0.xdc
similarity index 100%
rename from verilog/system_processing_system7_0_0.xdc
rename to gateware/verilog/system_processing_system7_0_0.xdc
diff --git a/linien-client/linien_client/__init__.py b/linien-client/linien_client/__init__.py
new file mode 100644
index 00000000..3a8d6d5c
--- /dev/null
+++ b/linien-client/linien_client/__init__.py
@@ -0,0 +1,1 @@
+from ._version import __version__  # noqa: F401
diff --git a/linien/client/connection.py b/linien-client/linien_client/connection.py
similarity index 54%
rename from linien/client/connection.py
rename to linien-client/linien_client/connection.py
index 7ae26775..032bb5e6 100644
--- a/linien/client/connection.py
+++ b/linien-client/linien_client/connection.py
@@ -23,33 +23,28 @@
 from traceback import print_exc
 from typing import Callable
 
+import linien_client
 import rpyc
-from plumbum import colors
-
-import linien
-from linien.client.exceptions import (
+from linien_client.deploy import start_remote_server
+from linien_client.exceptions import (
     GeneralConnectionErrorException,
     InvalidServerVersionException,
     RPYCAuthenticationException,
     ServerNotRunningException,
 )
-from linien.client.remote_parameters import RemoteParameters
-from linien.client.utils import run_server
-
-# IMPORTANT: keep this import, because it eases interfacing with the python client
-from linien.common import ANALOG_OUT_V, MHz, Vpp, hash_username_and_password
-from linien.config import DEFAULT_SERVER_PORT
-
-assert MHz
-assert Vpp
-assert ANALOG_OUT_V
+from linien_client.remote_parameters import RemoteParameters
+from linien_common.common import hash_username_and_password
+from linien_common.config import DEFAULT_SERVER_PORT
+from plumbum import colors
 
 
 class RPYCClientWithAuthentication(rpyc.Service):
-    """An rpyc client that authenticates using a hash.
+    """
+    An rpyc client that authenticates using a hash.
 
-    This class is run on the client side and exposes the client's unique id
-    to the server."""
+    This class is run on the client side and exposes the client's unique id to the
+    server.
+    """
 
     def __init__(self, uuid, user, password):
         super().__init__()
@@ -64,159 +59,46 @@ def _connect(self, channel, config):
         return super()._connect(channel, config)
 
 
-class RawRPYCClient:
-    """This class implements the basic functionality for connecting to a Linien
-    server using rpyc. See `LinienClient` for higher-level functionality.
-
-    Once connected, communication between server and client mainly takes place
-    using the `parameters` attribute."""
-
+class LinienClient:
     def __init__(
         self,
-        server: str,
-        port: int,
+        host: str,
         user: str,
         password: str,
-        use_parameter_cache: bool = False,
-        call_on_error: Callable = None,
+        port: int = DEFAULT_SERVER_PORT,
+        name: str = "",
     ):
-        self.use_parameter_cache = use_parameter_cache
-        self.uuid = "".join(random.choice(string.ascii_lowercase) for i in range(10))
+        """Connect to a RedPitaya that runs linien server."""
+        self.host = host
+        self.user = user
+        self.password = password
+        self.port = port
+        self.name = name
+
+        if self.host in ("localhost", "127.0.0.1"):
+            # RP is configured such that "localhost" doesn't point to 127.0.0.1 in all
+            # cases
+            self.host = "127.0.0.1"
+
+        self.uuid = "".join(random.choice(string.ascii_lowercase) for _ in range(10))
 
         # for exposing client's uuid to server
         self.client_service = RPYCClientWithAuthentication(self.uuid, user, password)
 
-        self._connect(
-            server,
-            port,
-            user,
-            password,
-            use_parameter_cache,
-            call_on_error=call_on_error,
-        )
-        self.connected = True
-
-    def _connect(
+    def connect(
         self,
-        server: str,
-        port: int,
-        user: str,
-        password: str,
+        autostart_server: bool,
         use_parameter_cache: bool,
         call_on_error: Callable = None,
-    ):
-        """This method just redirects to `connect_rpyc` and is intended to be
-        overridden in inheriting classes."""
-        return self._connect_rpyc(
-            server,
-            port,
-            user,
-            password,
-            use_parameter_cache,
-            call_on_error=call_on_error,
-        )
-
-    def _connect_rpyc(
-        self, server, port, user, password, use_parameter_cache, call_on_error=None
-    ):
-        """Connect to the server using rpyc and instanciate `RemoteParameters`."""
-        self.connection = rpyc.connect(
-            server, port, service=self.client_service, config={"allow_pickle": True}
-        )
-
-        cls = RemoteParameters
-        if call_on_error:
-            cls = self._catch_network_errors(cls, call_on_error)
-
-        self.parameters = cls(self.connection.root, self.uuid, use_parameter_cache)
-
-    def _catch_network_errors(self, cls, call_on_error):
-        """This method can be used for patching RemoteParameters such
-        that network errors are redirected to `call_on_error`"""
-        function_type = type(lambda x: x)
-
-        for attr_name in dir(cls):
-            # patch all methods that don't start with __
-            if not attr_name.startswith("__"):
-                attr = getattr(cls, attr_name)
-
-                if isinstance(attr, function_type):
-                    method = attr
-
-                    def wrapped(*args, method=method, **kwargs):
-                        try:
-                            return method(*args, **kwargs)
-                        except (EOFError,):
-                            print(colors.red | "Connection lost")
-                            self.connected = False
-                            call_on_error()
-                            raise
-
-                    setattr(cls, attr_name, wrapped)
-
-        return cls
-
-
-class LinienClient(RawRPYCClient):
-    def __init__(
-        self,
-        device,
-        autostart_server=True,
-        use_parameter_cache=False,
-        on_connection_lost: Callable = None,
-    ):
-        """Connect to a RedPitaya that runs linien server.
-
-        Takes the following arguments:
-            * `device` should be a dictionary:
-                {
-                    "host": "rp-XXXXXX.local",
-                    "username": "root",
-                    "password": "your-username"
-                }
-            * `autostart_server`: A bool indicating whether this call
-              should automatically start a linien server on redpitaya if it
-              doesn't run already
-        """
-        self.device = device
-        self.host = device["host"]
-        user = device.get("username")
-        password = device.get("password")
-
-        if self.host in ("localhost", "127.0.0.1"):
-            # RP is configured such that "localhost" doesn't point to
-            # 127.0.0.1 in all cases
-            self.host = "127.0.0.1"
-        else:
-            assert user and password, "username and passwort are required"
-
-        self.autostart_server = autostart_server
-
-        super().__init__(
-            self.host,
-            device.get("port", DEFAULT_SERVER_PORT),
-            user,
-            password,
-            use_parameter_cache=use_parameter_cache,
-            call_on_error=on_connection_lost,
-        )
-
-    def _connect(
-        self, host, port, user, password, use_parameter_cache, call_on_error=None
-    ):
+    ) -> None:
         self.connection = None
 
         i = -1
         while True:
             i += 1
-
             try:
-                print("try to connect to %s:%s" % (host, port))
+                print(f"Try to connect to {self.host}:{self.port}")
                 self._connect_rpyc(
-                    host,
-                    port,
-                    user,
-                    password,
                     use_parameter_cache,
                     call_on_error=call_on_error,
                 )
@@ -224,18 +106,18 @@ def _connect(
                 break
             except gaierror:
                 # host not found
-                print(colors.red | "Error: host %s not found" % host)
+                print(f"Error: host {self.host} not found")
                 break
             except EOFError:
                 print("EOFError! Probably authentication failed")
                 raise RPYCAuthenticationException()
             except Exception:
-                if not self.autostart_server:
+                if not autostart_server:
                     raise ServerNotRunningException()
 
                 if i == 0:
-                    print("server is not running. Launching it!")
-                    run_server(host, user, password, port)
+                    print("Server is not running. Launching it!")
+                    start_remote_server(self.host, self.user, self.password)
                     sleep(3)
                 else:
                     if i < 20:
@@ -248,8 +130,7 @@ def _connect(
                     else:
                         print_exc()
                         print(
-                            colors.red
-                            | "Error: connection to the server could not be established"
+                            "Error: connection to the server could not be established"
                         )
                         break
 
@@ -257,14 +138,58 @@ def _connect(
             raise GeneralConnectionErrorException()
 
         # now check that the remote version is the same as ours
-        remote_version = self.connection.root.exposed_get_server_version()
-        client_version = linien.__version__
+        remote_version = self.connection.root.exposed_get_server_version().split("+")[0]
+        local_version = linien_client.__version__.split("+")[0]
+
+        if (remote_version != local_version) and not ("dev" in local_version):
+            raise InvalidServerVersionException(local_version, remote_version)
+
+        self.connected = True
+        print("Connection established!")
+
+    def _connect_rpyc(self, use_parameter_cache, call_on_error=None):
+        """Connect to the server using rpyc and instanciate `RemoteParameters`."""
+        self.connection = rpyc.connect(
+            self.host,
+            self.port,
+            service=self.client_service,
+            config={"allow_pickle": True},
+        )
+
+        cls = RemoteParameters
+        if call_on_error:
+            cls = self._catch_network_errors(cls, call_on_error)
+
+        self.parameters = cls(self.connection.root, self.uuid, use_parameter_cache)
+
+    def _catch_network_errors(self, cls, call_on_error):
+        """
+        This method can be used for patching RemoteParameters such that network errors
+        are redirected to `call_on_error`
+        """
+        function_type = type(lambda x: x)
 
-        if remote_version != client_version:
-            raise InvalidServerVersionException(client_version, remote_version)
+        for attr_name in dir(cls):
+            # patch all methods that don't start with __
+            if not attr_name.startswith("__"):
+                attr = getattr(cls, attr_name)
 
-        print(colors.green | "connected established!")
+                if isinstance(attr, function_type):
+                    method = attr
+
+                    def wrapped(*args, method=method, **kwargs):
+                        try:
+                            return method(*args, **kwargs)
+                        except (EOFError,):
+                            print(colors.red | "Connection lost")
+                            self.connected = False
+                            call_on_error()
+                            raise
+
+                    setattr(cls, attr_name, wrapped)
+
+        return cls
 
-    def disconnect(self):
+    def disconnect(self) -> None:
         self.connection.close()
         self.connected = False
diff --git a/linien-client/linien_client/deploy.py b/linien-client/linien_client/deploy.py
new file mode 100644
index 00000000..3ab90a24
--- /dev/null
+++ b/linien-client/linien_client/deploy.py
@@ -0,0 +1,69 @@
+# Copyright 2018-2022 Benjamin Wiegand <benjamin.wiegand@physik.hu-berlin.de>
+# Copyright 2021-2022 Bastian Leykauf <leykauf@physik.hu-berlin.de>
+#
+# This file is part of Linien and based on redpid.
+#
+# Linien is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Linien 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Linien.  If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+
+import linien_client
+from fabric import Connection
+from linien_client.exceptions import (
+    InvalidServerVersionException,
+    ServerNotInstalledException,
+)
+
+
+def read_remote_version(conn: Connection) -> str:
+    """Read the remote version of linien."""
+    result = conn.run(
+        'python3 -c "import linien_server; print(linien_server.__version__);"'
+    )
+    if result.ok:
+        return result.stdout.strip()
+    else:
+        raise ServerNotInstalledException()
+
+
+def start_remote_server(host: str, user: str, password: str, port: int = 22):
+    with Connection(
+        host, user=user, port=port, connect_kwargs={"password": password}
+    ) as conn:
+        local_version = linien_client.__version__.split("+")[0]
+        remote_version = read_remote_version(conn).split("+")[0]
+
+        if (local_version != remote_version) and not ("dev" in local_version):
+            raise InvalidServerVersionException(local_version, remote_version)
+
+        # start the server process
+        conn.run("linien_start_server.sh")
+
+
+def install_remote_server(
+    host: str, user: str, password: str, port: int = 22, out_stream=sys.stdout
+):
+    with Connection(
+        host, user=user, port=port, connect_kwargs={"password": password}
+    ) as conn:
+        local_version = linien_client.__version__.split("+")[0]
+        cmds = [
+            f"pip3 install linien-server=={local_version} --no-cache-dir",
+            "linien_install_requirements.sh",
+        ]
+        for cmd in cmds:
+            out_stream.write(f">> {cmd}\n")
+            result = conn.run(cmd, out_stream=out_stream)
+        if result.ok:
+            print(f"Sucesfully executed '{result.command}'")
diff --git a/linien/client/exceptions.py b/linien-client/linien_client/exceptions.py
similarity index 91%
rename from linien/client/exceptions.py
rename to linien-client/linien_client/exceptions.py
index 1a4dfbf3..c345546b 100644
--- a/linien/client/exceptions.py
+++ b/linien-client/linien_client/exceptions.py
@@ -20,14 +20,16 @@
 class RPYCAuthenticationException(Exception):
     def __init__(self):
         super().__init__(
-            "Invalid credentials passed to LinienClient. Be sure to use the same username and password as when connecting via SSH."
+            "Invalid credentials passed to LinienClient. Be sure to use the same "
+            "username and password as when connecting via SSH."
         )
 
 
 class ServerNotRunningException(Exception):
     def __init__(self):
         super().__init__(
-            "The host was reached but no linien server is running. Use `autostart_server` if you want to change this."
+            "The host was reached but no linien server is running. Use "
+            "`autostart_server` if you want to change this."
         )
 
 
diff --git a/linien/client/remote_parameters.py b/linien-client/linien_client/remote_parameters.py
similarity index 98%
rename from linien/client/remote_parameters.py
rename to linien-client/linien_client/remote_parameters.py
index d15e65b6..65fa5afc 100644
--- a/linien/client/remote_parameters.py
+++ b/linien-client/linien_client/remote_parameters.py
@@ -19,13 +19,13 @@
 from typing import Callable
 
 import rpyc
+from linien_common.common import pack, unpack
 from rpyc import async_
 
-from linien.common import pack, unpack
-
 
 class RemoteParameters:
-    """A class that provides access to a remote `parameters.Parameters` instance.
+    """
+    A class that provides access to a remote `parameters.Parameters` instance.
 
     It clones the functionality of the remote `Parameters` instance. E.g.:
 
diff --git a/setup_client.py b/linien-client/setup.py
similarity index 62%
rename from setup_client.py
rename to linien-client/setup.py
index 970168f6..e3c9d57f 100644
--- a/setup_client.py
+++ b/linien-client/setup.py
@@ -16,47 +16,39 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
-import setuptools
+from setuptools import find_packages, setup
+from setuptools_scm import get_version
 
-import linien
+version = get_version(root="..", relative_to=__file__).split("+")[0]
 
-assert linien.__version__ != "dev"
-
-with open("README.md", "r") as fh:
-    long_description = fh.read()
-
-setuptools.setup(
-    name="linien-python-client",
-    version=linien.__version__,
+setup(
+    name="linien-client",
+    use_scm_version={
+        "write_to": "linien-client/linien_client/_version.py",
+        "root": "..",
+        "relative_to": __file__,
+    },
+    setup_requires=["setuptools_scm"],
     author="Benjamin Wiegand",
     author_email="highwaychile@posteo.de",
     maintainer="Bastian Leykauf",
     maintainer_email="leykauf@physik.hu-berlin.de",
     description="Python client for linien spectroscopy lock",
-    long_description=long_description,
-    long_description_content_type="text/markdown",
+    long_description="Have a look at the 'linien' package for installation instructions.",  # noqa: E501
+    long_description_content_type="text/x-rst",
     url="https://github.com/linien-org/linien/",
-    # IMPORTANT: any changes have to be made in setup_client_and_gui.py
-    # of flathub repo as well
-    packages=["linien", "linien.client"],
+    packages=find_packages(),
     classifiers=[
         "Programming Language :: Python :: 3",
         "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
         "Operating System :: OS Independent",
     ],
     install_requires=[
-        "numpy>=1.19.1",
-        "paramiko>=2.10.1",
-        "plumbum>=1.6.9",
+        "fabric>=2.7.0",
+        "numpy>=1.22",
+        "patchwork>=1.0.1",
         "rpyc>=4.0,<5.0",
         "scipy>=1.4.1",
-        "uuid>=1.30",
+        "linien-common=={}".format(version),
     ],
-    package_data={
-        # IMPORTANT: any changes have to be made in pyinstaller.spec, too
-        # (for the standalone installer)
-        # IMPORTANT: any changes have to be made in setup_client_and_gui.py
-        # of flathub repo as well
-        "": ["VERSION"]
-    },
 )
diff --git a/linien/client/__init__.py b/linien-common/linien_common/__init__.py
similarity index 100%
rename from linien/client/__init__.py
rename to linien-common/linien_common/__init__.py
diff --git a/linien/common.py b/linien-common/linien_common/common.py
similarity index 99%
rename from linien/common.py
rename to linien-common/linien_common/common.py
index b4c51ba3..e88b9363 100644
--- a/linien/common.py
+++ b/linien-common/linien_common/common.py
@@ -265,7 +265,6 @@ def convert_channel_mixing_value(value):
 def combine_error_signal(
     error_signals, dual_channel, channel_mixing, combined_offset, chain_factor_width=8
 ):
-
     if not dual_channel:
         signal = error_signals[0]
     else:
diff --git a/linien/config.py b/linien-common/linien_common/config.py
similarity index 97%
rename from linien/config.py
rename to linien-common/linien_common/config.py
index 5462dc98..309e1cdf 100644
--- a/linien/config.py
+++ b/linien-common/linien_common/config.py
@@ -17,7 +17,6 @@
 
 ACQUISITION_PORT = 19321
 DEFAULT_SERVER_PORT = 18862
-REMOTE_BASE_PATH = "/linien"
 DEFAULT_SWEEP_SPEED = (125 * 2048) << 6
 # IMPORTANT: DEFAULT_COLORS and N_COLORS have to be here, not in client.config
 # because the server needs them and shouldn't import client config as it requires
diff --git a/linien-common/setup.py b/linien-common/setup.py
new file mode 100644
index 00000000..67679b40
--- /dev/null
+++ b/linien-common/setup.py
@@ -0,0 +1,27 @@
+from setuptools import find_packages, setup
+
+setup(
+    name="linien-common",
+    use_scm_version={"root": "..", "relative_to": __file__},
+    setup_requires=["setuptools_scm"],
+    author="Benjamin Wiegand",
+    author_email="highwaychile@posteo.de",
+    maintainer="Bastian Leykauf",
+    maintainer_email="leykauf@physik.hu-berlin.de",
+    description="Part of linien that is used by all components.",
+    long_description="Have a look at the 'linien' package for installation instructions.",  # noqa: E501
+    long_description_content_type="text/x-rst",
+    url="https://github.com/linien-org/linien",
+    packages=find_packages(),
+    classifiers=[
+        "Programming Language :: Python :: 3",
+        "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
+        "Operating System :: OS Independent",
+    ],
+    install_requires=[
+        "numpy>=1.11.0",
+        "plumbum>=1.6.9",
+        "scipy>=0.17.0",
+        "setuptools_scm>=5.0.2",
+    ],
+)
diff --git a/linien-gui/linien_gui/__init__.py b/linien-gui/linien_gui/__init__.py
new file mode 100644
index 00000000..3a8d6d5c
--- /dev/null
+++ b/linien-gui/linien_gui/__init__.py
@@ -0,0 +1,1 @@
+from ._version import __version__  # noqa: F401
diff --git a/linien/gui/app.py b/linien-gui/linien_gui/app.py
similarity index 87%
rename from linien/gui/app.py
rename to linien-gui/linien_gui/app.py
index 8a6de152..c8607ea7 100644
--- a/linien/gui/app.py
+++ b/linien-gui/linien_gui/app.py
@@ -15,29 +15,22 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
-import os
 import signal
 import sys
 from traceback import print_exc
 
+import linien_gui
+from linien_gui.ui.device_manager import DeviceManager
+from linien_gui.ui.main_window import MainWindow
+from linien_gui.ui.psd_window import PSDWindow
+from linien_gui.ui.version_checker import VersionCheckerThread
+from linien_gui.utils_gui import set_window_icon
+from linien_gui.widgets import UI_PATH, CustomWidget
 from plumbum import colors
 from PyQt5 import QtWidgets
 from pyqtgraph.Qt import QtCore
 
-# it may seem odd to include '.', but for some reason this is needed for
-# standalone windows executable
-sys.path += ["../", os.path.join("..", ".."), "."]
-
-import linien
-from linien.gui.widgets import CustomWidget, ui_path
-
-sys.path += [ui_path]
-
-from linien.gui.ui.device_manager import DeviceManager
-from linien.gui.ui.main_window import MainWindow
-from linien.gui.ui.psd_window import PSDWindow
-from linien.gui.ui.version_checker import VersionCheckerThread
-from linien.gui.utils_gui import set_window_icon
+sys.path += [str(UI_PATH)]
 
 
 class QTApp(QtCore.QObject):
@@ -63,7 +56,7 @@ def __init__(self):
 
     def client_connected(self, client):
         self.device_manager.hide()
-        self.main_window.show(client.host, client.device["name"])
+        self.main_window.show(client.host, client.name)
 
         self.client = client
         self.control = client.control
@@ -73,7 +66,6 @@ def client_connected(self, client):
         self.ready.emit(True)
 
     def init(self):
-
         for instance in CustomWidget.instances:
             try:
                 instance.connection_established()
@@ -144,7 +136,7 @@ def new_version_available(self, new_version_available):
 
 
 def run_application():
-    print("Linien spectroscopy lock version " + (colors.bold | linien.__version__))
+    print("Linien spectroscopy lock version " + (colors.bold | linien_gui.__version__))
     gui = QTApp()
 
     # catch ctrl-c and shutdown
diff --git a/linien/gui/config.py b/linien-gui/linien_gui/config.py
similarity index 94%
rename from linien/gui/config.py
rename to linien-gui/linien_gui/config.py
index f8d42813..4fcefbc1 100644
--- a/linien/gui/config.py
+++ b/linien-gui/linien_gui/config.py
@@ -68,10 +68,12 @@ def save_parameter(device_key, param, value, delete=False):
     device.setdefault("params", {})
 
     if not delete:
+        # FIXME: This is the only part where rpyc is used in linien-gui. Remove it if
+        # possible.
         # rpyc obtain is for ensuring that we don't try to save a netref here
         try:
             device["params"][param] = rpyc.classic.obtain(value)
-        except:
+        except Exception:
             print("unable to obtain and save parameter", param)
     else:
         try:
diff --git a/linien/gui/dialogs.py b/linien-gui/linien_gui/dialogs.py
similarity index 53%
rename from linien/gui/dialogs.py
rename to linien-gui/linien_gui/dialogs.py
index 9e75946b..66a6088b 100644
--- a/linien/gui/dialogs.py
+++ b/linien-gui/linien_gui/dialogs.py
@@ -15,74 +15,48 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
-import re
+from time import sleep
+from typing import Callable
 
-from plumbum import colors
-from PyQt5.QtCore import pyqtSignal
-from PyQt5.QtWidgets import QDialog, QListWidget, QMessageBox, QVBoxLayout
+from linien_gui.threads import RemoteServerInstallationThread
+from PyQt5.QtCore import QThread, pyqtSignal
+from PyQt5.QtWidgets import QDialog, QListWidget, QMessageBox, QVBoxLayout, QWidget
 from pyqtgraph import QtCore
 
-from linien.client.utils import connect_ssh
-
 
 class SSHCommandOutputWidget(QListWidget):
     command_ended = pyqtSignal()
 
-    def __init__(self, *args):
-        super().__init__(*args)
+    def __init__(self, parent: QWidget):
+        super().__init__(parent)
         self.setSelectionMode(self.NoSelection)
 
-    def execute(self, ssh, command):
-        self.stdin, self.stdout, self.stderr = ssh.exec_command(
-            command, bufsize=0, get_pty=True
-        )
-        self.addItem(">>> %s" % command)
-        self.show_output()
-
-    def show_output(self):
-        if self.stdout.channel.exit_status_ready():
-            return self.command_ended.emit()
+    def run(self, thread: QThread):
+        self.thread = thread
+        self.thread.start()
+        self.read_output_and_update_widget()
+
+    def read_output_and_update_widget(self):
+        if self.thread.isFinished() and self.thread.out_stream.empty():
+            self.addItem("Finished.")
+            self.scrollToBottom()
+            # Show widget for some time before proceeding
+            QtCore.QTimer.singleShot(3000, self.command_ended.emit)
         else:
-            for output in (self.stdout, self.stderr):
-                toread = len(output.channel.in_buffer)
-                if toread == 0:
-                    continue
-                buf = output.read(toread).decode("utf8").rstrip("\n")
-
-                for part in buf.split("\n"):
-                    for subpart_i, subpart in enumerate(part.split("\r")):
-                        subpart = subpart.strip("\n").strip("\r").strip("\r\n")
-                        if subpart:
-                            print(
-                                (colors.red if output == self.stderr else colors.reset)
-                                | subpart
-                            )
-                            if subpart_i > 0:
-                                # delete previous item if \r is found
-                                self.takeItem(self.count() - 1)
-
-                            self.addItem(
-                                # filter out special things like color codes etc.
-                                re.sub(
-                                    r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))", "", subpart
-                                )
-                            )
-
-                self.scrollToBottom()
+            self.addItem(self.thread.out_stream.read().rstrip())
+            self.scrollToBottom()
+            # update widget every 100 ms
+            QtCore.QTimer.singleShot(100, self.read_output_and_update_widget)
 
-        QtCore.QTimer.singleShot(1000, self.show_output)
-
-
-def execute_command_and_show_output(parent, host, user, password, command, callback):
-    print((colors.bold | "Execute command: ") + command)
-
-    ssh = connect_ssh(host, user, password)
 
+def show_installation_progress_widget(
+    parent: QWidget, device: dict, callback: Callable
+):
+    # Define and open dialog window
     window = QDialog(parent)
-    window.setWindowTitle("Installing Linien server component")
+    window.setWindowTitle("Deploying Linien Server")
     window.resize(800, 600)
     window_layout = QVBoxLayout(window)
-
     widget = SSHCommandOutputWidget(parent)
     window_layout.addWidget(widget)
     window.setLayout(window_layout)
@@ -90,13 +64,11 @@ def execute_command_and_show_output(parent, host, user, password, command, callb
     window.setWindowModality(QtCore.Qt.WindowModal)
     window.show()
 
-    def after_command():
-        window.hide()
-        callback()
-
-    widget.command_ended.connect(after_command)
+    widget.command_ended.connect(callback)
+    widget.command_ended.connect(window.close)
 
-    widget.execute(ssh, command)
+    thread = RemoteServerInstallationThread(device)
+    widget.run(thread)
 
     return window
 
@@ -104,11 +76,11 @@ def after_command():
 class LoadingDialog(QMessageBox):
     aborted = pyqtSignal()
 
-    def __init__(self, parent, host):
+    def __init__(self, parent: QWidget, host: str):
         super().__init__(parent)
 
         self.setIcon(QMessageBox.Information)
-        self.setText("Connecting to %s" % host)
+        self.setText(f"Connecting to {host}")
         self.setWindowTitle("Connecting")
         self.setModal(True)
         self.setWindowModality(QtCore.Qt.WindowModal)
@@ -124,7 +96,7 @@ def keyPressEvent(self, event):
             self.close()
 
 
-def error_dialog(parent, error):
+def error_dialog(parent: QWidget, error):
     return QMessageBox.question(parent, "Error", error, QMessageBox.Ok, QMessageBox.Ok)
 
 
diff --git a/linien/gui/icon.ico b/linien-gui/linien_gui/icon.ico
similarity index 100%
rename from linien/gui/icon.ico
rename to linien-gui/linien_gui/icon.ico
diff --git a/linien-gui/linien_gui/threads.py b/linien-gui/linien_gui/threads.py
new file mode 100644
index 00000000..99aeb975
--- /dev/null
+++ b/linien-gui/linien_gui/threads.py
@@ -0,0 +1,189 @@
+# Copyright 2018-2022 Benjamin Wiegand <benjamin.wiegand@physik.hu-berlin.de>
+# Copyright 2021-2022 Bastian Leykauf <leykauf@physik.hu-berlin.de>
+#
+# This file is part of Linien and based on redpid.
+#
+# Linien is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Linien 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Linien.  If not, see <http://www.gnu.org/licenses/>.
+
+from queue import Queue
+from traceback import print_exc
+
+from linien_client.connection import LinienClient
+from linien_client.deploy import install_remote_server
+from linien_client.exceptions import (
+    GeneralConnectionErrorException,
+    InvalidServerVersionException,
+    RPYCAuthenticationException,
+    ServerNotInstalledException,
+)
+from linien_common.config import DEFAULT_SERVER_PORT
+from linien_gui.config import get_saved_parameters, save_parameter
+from PyQt5.QtCore import QThread, pyqtSignal
+
+
+class FileLikeQueue(Queue):
+    """
+    A queue with the interface of a file-like object used to redirect output stream of
+    SSH calls to the GUI.
+    """
+
+    def write(self, data):
+        self.put(data)
+
+    def read(self):
+        return self.get()
+
+    def flush(self):
+        pass
+
+
+class RemoteServerInstallationThread(QThread):
+    def __init__(self, device):
+        """A thread that installs the linien server on a remote machine."""
+        super().__init__()
+        self.device = device
+
+    out_stream = FileLikeQueue()
+
+    def run(self):
+        install_remote_server(
+            host=self.device["host"],
+            user=self.device["username"],
+            password=self.device["password"],
+            out_stream=self.out_stream,
+        )
+
+
+class ConnectionThread(QThread):
+    def __init__(self, device):
+        super().__init__()
+        self.device = device
+
+    client_connected = pyqtSignal(object)
+    server_not_installed_exception_raised = pyqtSignal()
+    invalid_server_version_exception_raised = pyqtSignal(str, str)
+    authentication_exception_raised = pyqtSignal()
+    general_connection_exception_raised = pyqtSignal()
+    exception_raised = pyqtSignal()
+    connection_lost = pyqtSignal()
+    ask_for_parameter_restore = pyqtSignal()
+
+    def run(self):
+        try:
+            self.client = LinienClient(
+                host=self.device["host"],
+                user=self.device["username"],
+                password=self.device["password"],
+                port=self.device.get("port", DEFAULT_SERVER_PORT),
+                name=self.device.get("name", ""),
+            )
+            self.client.connect(
+                autostart_server=True,
+                use_parameter_cache=True,
+                call_on_error=self.on_connection_lost,
+            )
+            self.client_connected.emit(self.client)
+
+        except ServerNotInstalledException:
+            return self.server_not_installed_exception_raised.emit()
+
+        except InvalidServerVersionException as e:
+            return self.invalid_server_version_exception_raised.emit(
+                e.remote_version, e.client_version
+            )
+
+        except RPYCAuthenticationException:
+            return self.authentication_exception_raised.emit()
+
+        except GeneralConnectionErrorException:
+            return self.general_connection_exception_raised.emit()
+
+        except Exception:
+            print_exc()
+            return self.exception_raised.emit()
+
+        # now, we are connected to the server. Check whether we have cached settings for
+        # this server. If yes, check whether they match with what is currentlyrunning.
+        # If there is a mismatch, ask the user whether the settings should be restored.
+
+        parameters_differ = self.restore_parameters(dry_run=True)
+        if parameters_differ:
+            self.ask_for_parameter_restore.emit()
+        else:
+            # if parameters don't differ, we can start monitoring remote parameter
+            # changes and write them to disk. We don't do this if parameters differ
+            # because we don't want to override our local settings with the remote one
+            # --> we wait until user has answered whether local parameters or remote
+            # ones should be used.
+            self.continuously_write_parameters_to_disk()
+
+    def on_connection_lost(self):
+        self.connection_lost.emit()
+
+    def answer_whether_to_restore_parameters(self, should_restore):
+        if should_restore:
+            self.restore_parameters(dry_run=False)
+
+        self.continuously_write_parameters_to_disk()
+
+    def restore_parameters(self, dry_run=False):
+        """
+        Reads settings for a server that were cached locally. Sends them to the server.
+        If `dry_run` is...
+
+            * `True`, this function returns a boolean indicating whether the
+              local parameters differ from the ones on the server
+            * `False`, the local parameters are uploaded to the server
+        """
+        device_key = self.device["key"]
+        params = get_saved_parameters(device_key)
+        print("restoring parameters")
+
+        differences = False
+
+        for k, v in params.items():
+            if hasattr(self.client.parameters, k):
+                param = getattr(self.client.parameters, k)
+                if param.value != v:
+                    if dry_run:
+                        print("parameter", k, "differs")
+                        differences = True
+                        break
+                    else:
+                        param.value = v
+            else:
+                # This may happen if the settings were written with a different version
+                # of linien.
+                print(f"Unable to restore parameter {k}. Delete the cached value.")
+                save_parameter(device_key, k, None, delete=True)
+
+        if not dry_run:
+            self.client.control.write_registers()
+
+        return differences
+
+    def continuously_write_parameters_to_disk(self):
+        """
+        Listens for changes of some parameters and permanently saves their values on the
+        client's disk. This data can be used to restore the status later, if the client
+        tries to connect to the server but it doesn't run anymore.
+        """
+        params = self.client.parameters.remote.exposed_get_restorable_parameters()
+
+        for param in params:
+
+            def on_change(value, param=param):
+                save_parameter(self.device["key"], param, value)
+
+            getattr(self.client.parameters, param).on_change(on_change)
diff --git a/linien/gui/__init__.py b/linien-gui/linien_gui/ui/__init__.py
similarity index 100%
rename from linien/gui/__init__.py
rename to linien-gui/linien_gui/ui/__init__.py
diff --git a/linien-gui/linien_gui/ui/device_manager.py b/linien-gui/linien_gui/ui/device_manager.py
new file mode 100644
index 00000000..9f3eb864
--- /dev/null
+++ b/linien-gui/linien_gui/ui/device_manager.py
@@ -0,0 +1,274 @@
+# Copyright 2018-2022 Benjamin Wiegand <benjamin.wiegand@physik.hu-berlin.de>
+# Copyright 2021-2022 Bastian Leykauf <leykauf@physik.hu-berlin.de>
+#
+# This file is part of Linien and based on redpid.
+#
+# Linien is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Linien 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Linien.  If not, see <http://www.gnu.org/licenses/>.
+
+import linien_gui
+from linien_gui.config import load_device_data, save_device_data
+from linien_gui.dialogs import (
+    LoadingDialog,
+    ask_for_parameter_restore_dialog,
+    error_dialog,
+    question_dialog,
+    show_installation_progress_widget,
+)
+from linien_gui.threads import ConnectionThread
+from linien_gui.ui.new_device_dialog import NewDeviceDialog
+from linien_gui.utils_gui import set_window_icon
+from linien_gui.widgets import CustomWidget
+from PyQt5 import QtCore, QtWidgets
+
+
+class DeviceManager(QtWidgets.QMainWindow, CustomWidget):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.load_ui("device_manager.ui")
+        self.setWindowTitle(f"Linien spectroscopy lock v{linien_gui.__version__}")
+        set_window_icon(self)
+
+    def ready(self):
+        self.load_device_data(autoload=True)
+
+    def keyPressEvent(self, event):
+        key = event.key()
+
+        if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
+            self.connect()
+
+    def load_device_data(self, autoload=False):
+        devices = load_device_data()
+        lst = self.ids.deviceList
+        lst.clear()
+
+        for device in devices:
+            lst.addItem("{} ({})".format(device["name"], device["host"]))
+
+        if autoload and len(devices) == 1:
+            self.connect_to_device(devices[0])
+
+    def connect(self):
+        devices = load_device_data()
+
+        if not devices:
+            return
+        else:
+            self.connect_to_device(devices[self.get_list_index()])
+
+    def connect_to_device(self, device):
+        loading_dialog = LoadingDialog(self, device["host"])
+        loading_dialog.show()
+
+        aborted = {}
+
+        self.connection_thread = ConnectionThread(device)
+
+        def was_aborted(*args):
+            aborted["aborted"] = True
+
+        loading_dialog.aborted.connect(was_aborted)
+
+        # Define slot functions to be connected ----------------------------------------
+        def on_client_connected(client):
+            loading_dialog.hide()
+            if not aborted:
+                self.app.client_connected(client)
+
+        def handle_server_not_installed():
+            loading_dialog.hide()
+            if not aborted:
+                display_question = (
+                    "The server is not yet installed on the device. Should it be "
+                    "installed? (Requires internet connection on RedPitaya)"
+                )
+                if question_dialog(self, display_question, "Install server?"):
+                    show_installation_progress_widget(
+                        parent=self,
+                        device=device,
+                        callback=lambda: self.connect_to_device(device),
+                    )
+
+        def handle_invalid_server_version(
+            remote_version: str, client_version: str
+        ) -> None:
+            loading_dialog.hide()
+            if not aborted:
+                display_question = (
+                    f"Server version ({remote_version}) does not match the client "
+                    f"({client_version}) version. Should the corresponding server "
+                    f"version be installed?"
+                )
+                if question_dialog(
+                    self, display_question, "Install corresponding version?"
+                ):
+                    show_installation_progress_widget(
+                        parent=self,
+                        device=device,
+                        callback=lambda: self.connect_to_device(device),
+                    )
+
+        def handle_authentication_exception():
+            loading_dialog.hide()
+            if not aborted:
+                display_error = (
+                    "Error at authentication.\n"
+                    "Check username and password (by default both are 'root')  and "
+                    "verify that you don't have any offending SSH keys in your known "
+                    "hosts file."
+                )
+                error_dialog(self, display_error)
+
+        def handle_general_connection_error():
+            loading_dialog.hide()
+            if not aborted:
+                display_error = (
+                    "Unable to connect to device. If you are connecting by hostname "
+                    "(i.e. `rp-xxxxxx.local`), try using IP address instead."
+                )
+                error_dialog(self, display_error)
+
+        def handle_other_exception():
+            loading_dialog.hide()
+            if not aborted:
+                display_error = "Exception occured when connecting to the device."
+                error_dialog(self, display_error)
+
+        def ask_for_parameter_restore():
+            question = (
+                "Linien on RedPitaya is running with different parameters than the "
+                "ones saved locally on this machine. Do you want to upload the local "
+                "parameters or keep the remote ones? Note that remote parameters are "
+                "only saved if Linien server was shut down properly, not when "
+                "unplugging the power plug. In this case, you should update your local "
+                "parameters."
+            )
+            should_restore = ask_for_parameter_restore_dialog(
+                self, question, "Restore parameters?"
+            )
+            self.connection_thread.answer_whether_to_restore_parameters(should_restore)
+
+        def handle_connection_lost():
+            error_dialog(self, "Lost connection to the server!")
+            self.app.close()
+
+        # Connect slots to signals -----------------------------------------------------
+        self.connection_thread.client_connected.connect(on_client_connected)
+
+        self.connection_thread.server_not_installed_exception_raised.connect(
+            handle_server_not_installed
+        )
+        self.connection_thread.invalid_server_version_exception_raised.connect(
+            handle_invalid_server_version
+        )
+        self.connection_thread.authentication_exception_raised.connect(
+            handle_authentication_exception
+        )
+        self.connection_thread.general_connection_exception_raised.connect(
+            handle_general_connection_error
+        )
+        self.connection_thread.exception_raised.connect(handle_other_exception)
+        self.connection_thread.ask_for_parameter_restore.connect(
+            ask_for_parameter_restore
+        )
+        self.connection_thread.connection_lost.connect(handle_connection_lost)
+
+        # Start the worker -------------------------------------------------------------
+        self.connection_thread.start()
+
+    def new_device(self):
+        self.dialog = NewDeviceDialog()
+        self.dialog.setModal(True)
+        self.dialog.show()
+
+        def reload_device_data():
+            # not very elegant...
+            QtCore.QTimer.singleShot(100, self.load_device_data)
+
+        self.dialog.accepted.connect(reload_device_data)
+
+    def edit_device(self):
+        devices = load_device_data()
+
+        if not devices:
+            return
+
+        device = devices[self.get_list_index()]
+
+        self.dialog = NewDeviceDialog(device)
+        self.dialog.setModal(True)
+        self.dialog.show()
+
+        def reload_device_data():
+            # not very elegant...
+            QtCore.QTimer.singleShot(100, self.load_device_data)
+
+        self.dialog.accepted.connect(reload_device_data)
+
+    def move_device_up(self):
+        self.move_device(-1)
+
+    def move_device_down(self):
+        self.move_device(1)
+
+    def move_device(self, direction):
+        devices = load_device_data()
+
+        if not devices:
+            return
+
+        current_index = self.get_list_index()
+        new_index = current_index + direction
+
+        if new_index < 0 or new_index > len(devices) - 1:
+            return
+
+        device = devices.pop(current_index)
+        devices = devices[:new_index] + [device] + devices[new_index:]
+        save_device_data(devices)
+        self.load_device_data()
+        self.ids.deviceList.setCurrentRow(new_index)
+
+    def get_list_index(self):
+        return self.ids.deviceList.currentIndex().row()
+
+    def remove_device(self):
+        devices = load_device_data()
+
+        if not devices:
+            return
+
+        devices.pop(self.get_list_index())
+        save_device_data(devices)
+        self.load_device_data()
+
+    def selected_device_changed(self):
+        idx = self.get_list_index()
+
+        disable_buttons = True
+
+        if idx >= 0:
+            devices = load_device_data()
+
+            if devices:
+                disable_buttons = False
+
+        for btn in [
+            self.ids.connectButton,
+            self.ids.removeButton,
+            self.ids.editButton,
+            self.ids.moveUpButton,
+            self.ids.moveDownButton,
+        ]:
+            btn.setEnabled(not disable_buttons)
diff --git a/linien/gui/ui/device_manager.ui b/linien-gui/linien_gui/ui/device_manager.ui
similarity index 100%
rename from linien/gui/ui/device_manager.ui
rename to linien-gui/linien_gui/ui/device_manager.ui
diff --git a/linien/gui/ui/general_panel.py b/linien-gui/linien_gui/ui/general_panel.py
similarity index 98%
rename from linien/gui/ui/general_panel.py
rename to linien-gui/linien_gui/ui/general_panel.py
index c4d983e5..3d225001 100644
--- a/linien/gui/ui/general_panel.py
+++ b/linien-gui/linien_gui/ui/general_panel.py
@@ -16,17 +16,16 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
-from PyQt5 import QtWidgets
-
-from linien.common import (
+from linien_common.common import (
     ANALOG_OUT0,
     ANALOG_OUT_V,
     FAST_OUT1,
     FAST_OUT2,
     convert_channel_mixing_value,
 )
-from linien.gui.utils_gui import param2ui
-from linien.gui.widgets import CustomWidget
+from linien_gui.utils_gui import param2ui
+from linien_gui.widgets import CustomWidget
+from PyQt5 import QtWidgets
 
 
 class GeneralPanel(QtWidgets.QWidget, CustomWidget):
diff --git a/linien/gui/ui/general_panel.ui b/linien-gui/linien_gui/ui/general_panel.ui
similarity index 100%
rename from linien/gui/ui/general_panel.ui
rename to linien-gui/linien_gui/ui/general_panel.ui
diff --git a/linien/gui/ui/lock_status_panel.py b/linien-gui/linien_gui/ui/lock_status_panel.py
similarity index 97%
rename from linien/gui/ui/lock_status_panel.py
rename to linien-gui/linien_gui/ui/lock_status_panel.py
index c5c107d7..554e3f21 100644
--- a/linien/gui/ui/lock_status_panel.py
+++ b/linien-gui/linien_gui/ui/lock_status_panel.py
@@ -16,11 +16,10 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
+from linien_gui.utils_gui import param2ui
+from linien_gui.widgets import CustomWidget
 from PyQt5 import QtWidgets
 
-from linien.gui.utils_gui import param2ui
-from linien.gui.widgets import CustomWidget
-
 
 class LockStatusPanel(QtWidgets.QWidget, CustomWidget):
     def __init__(self, *args, **kwargs):
diff --git a/linien/gui/ui/locking_panel.py b/linien-gui/linien_gui/ui/locking_panel.py
similarity index 98%
rename from linien/gui/ui/locking_panel.py
rename to linien-gui/linien_gui/ui/locking_panel.py
index e0a65b5d..7b1b3989 100644
--- a/linien/gui/ui/locking_panel.py
+++ b/linien-gui/linien_gui/ui/locking_panel.py
@@ -16,12 +16,11 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
+from linien_common.common import FAST_AUTOLOCK
+from linien_gui.utils_gui import param2ui
+from linien_gui.widgets import CustomWidget
 from PyQt5 import QtWidgets
 
-from linien.common import FAST_AUTOLOCK
-from linien.gui.utils_gui import param2ui
-from linien.gui.widgets import CustomWidget
-
 
 class LockingPanel(QtWidgets.QWidget, CustomWidget):
     def __init__(self, *args, **kwargs):
diff --git a/linien/gui/ui/locking_panel.ui b/linien-gui/linien_gui/ui/locking_panel.ui
similarity index 100%
rename from linien/gui/ui/locking_panel.ui
rename to linien-gui/linien_gui/ui/locking_panel.ui
diff --git a/linien/gui/ui/main_window.py b/linien-gui/linien_gui/ui/main_window.py
similarity index 95%
rename from linien/gui/ui/main_window.py
rename to linien-gui/linien_gui/ui/main_window.py
index 21455653..5b4151c8 100644
--- a/linien/gui/ui/main_window.py
+++ b/linien-gui/linien_gui/ui/main_window.py
@@ -21,17 +21,16 @@
 from math import log
 from time import time
 
+import linien_gui
 import numpy as np
+from linien_common.common import check_plot_data
+from linien_common.config import N_COLORS
+from linien_gui.config import COLORS
+from linien_gui.ui.plot_widget import INVALID_POWER
+from linien_gui.utils_gui import color_to_hex
+from linien_gui.widgets import CustomWidget
 from PyQt5 import QtWidgets
 
-import linien
-from linien.common import check_plot_data
-from linien.config import N_COLORS
-from linien.gui.config import COLORS
-from linien.gui.ui.plot_widget import INVALID_POWER
-from linien.gui.utils_gui import color_to_hex
-from linien.gui.widgets import CustomWidget
-
 ZOOM_STEP = 0.9
 MAX_ZOOM = 50
 MIN_ZOOM = 0
@@ -50,7 +49,8 @@ def __init__(self, *args, **kwargs):
 
     def show(self, host, name):
         self.setWindowTitle(
-            "Linien spectroscopy lock %s: %s (%s)" % (linien.__version__, name, host)
+            "Linien spectroscopy lock %s: %s (%s)"
+            % (linien_gui.__version__, name, host)
         )
         super().show()
 
@@ -114,7 +114,7 @@ def export_parameters_select_file(self):
             with open(fn, "w") as f:
                 json.dump(
                     {
-                        "linien-version": linien.__version__,
+                        "linien-version": linien_gui.__version__,
                         "time": time(),
                         "parameters": dict(
                             (k, getattr(self.parameters, k).value)
diff --git a/linien/gui/ui/main_window.ui b/linien-gui/linien_gui/ui/main_window.ui
similarity index 100%
rename from linien/gui/ui/main_window.ui
rename to linien-gui/linien_gui/ui/main_window.ui
diff --git a/linien/gui/ui/modulation_sweep_panel.py b/linien-gui/linien_gui/ui/modulation_sweep_panel.py
similarity index 96%
rename from linien/gui/ui/modulation_sweep_panel.py
rename to linien-gui/linien_gui/ui/modulation_sweep_panel.py
index b83c6059..5849ec1f 100644
--- a/linien/gui/ui/modulation_sweep_panel.py
+++ b/linien-gui/linien_gui/ui/modulation_sweep_panel.py
@@ -16,12 +16,11 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
+from linien_common.common import MHz, Vpp
+from linien_gui.utils_gui import param2ui
+from linien_gui.widgets import CustomWidget
 from PyQt5 import QtWidgets
 
-from linien.client.connection import MHz, Vpp
-from linien.gui.utils_gui import param2ui
-from linien.gui.widgets import CustomWidget
-
 
 class ModulationAndSweepPanel(QtWidgets.QWidget, CustomWidget):
     def __init__(self, *args, **kwargs):
diff --git a/linien/gui/ui/modulation_sweep_panel.ui b/linien-gui/linien_gui/ui/modulation_sweep_panel.ui
similarity index 100%
rename from linien/gui/ui/modulation_sweep_panel.ui
rename to linien-gui/linien_gui/ui/modulation_sweep_panel.ui
diff --git a/linien/gui/ui/new_device_dialog.py b/linien-gui/linien_gui/ui/new_device_dialog.py
similarity index 92%
rename from linien/gui/ui/new_device_dialog.py
rename to linien-gui/linien_gui/ui/new_device_dialog.py
index 4d86ea16..51f5eb1d 100644
--- a/linien/gui/ui/new_device_dialog.py
+++ b/linien-gui/linien_gui/ui/new_device_dialog.py
@@ -18,12 +18,11 @@
 import random
 import string
 
+from linien_common.config import DEFAULT_SERVER_PORT
+from linien_gui.config import load_device_data, save_device_data
+from linien_gui.widgets import CustomWidget
 from PyQt5 import QtWidgets
 
-from linien.config import DEFAULT_SERVER_PORT
-from linien.gui.config import load_device_data, save_device_data
-from linien.gui.widgets import CustomWidget
-
 
 class NewDeviceDialog(QtWidgets.QDialog, CustomWidget):
     def __init__(self, initial_device=None):
diff --git a/linien/gui/ui/new_device_dialog.ui b/linien-gui/linien_gui/ui/new_device_dialog.ui
similarity index 100%
rename from linien/gui/ui/new_device_dialog.ui
rename to linien-gui/linien_gui/ui/new_device_dialog.ui
diff --git a/linien/gui/ui/optimization_panel.py b/linien-gui/linien_gui/ui/optimization_panel.py
similarity index 96%
rename from linien/gui/ui/optimization_panel.py
rename to linien-gui/linien_gui/ui/optimization_panel.py
index 6dec8ab9..b1604402 100644
--- a/linien/gui/ui/optimization_panel.py
+++ b/linien-gui/linien_gui/ui/optimization_panel.py
@@ -16,12 +16,11 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
+from linien_common.common import MHz, Vpp
+from linien_gui.utils_gui import param2ui
+from linien_gui.widgets import CustomWidget
 from PyQt5 import QtWidgets
 
-from linien.client.connection import MHz, Vpp
-from linien.gui.utils_gui import param2ui
-from linien.gui.widgets import CustomWidget
-
 
 class OptimizationPanel(QtWidgets.QWidget, CustomWidget):
     def __init__(self, *args, **kwargs):
@@ -163,13 +162,13 @@ def dual_channel_changed(value):
         self.parameters.dual_channel.on_change(dual_channel_changed)
 
         def fast_mode_changed(fast_mode_enabled):
-            """Disables this panel if fast mode is enabled (nothing to optimize)"""
+            """Disable this panel if fast mode is enabled (nothing to optimize)."""
             self.setEnabled(not fast_mode_enabled)
 
         self.parameters.fast_mode.on_change(fast_mode_changed)
 
         def fast_mode_changed(fast_mode_enabled):
-            """Disables this panel if fast mode is enabled (nothing to optimize)"""
+            """Disable this panel if fast mode is enabled (nothing to optimize)."""
             self.setEnabled(not fast_mode_enabled)
 
         self.parameters.fast_mode.on_change(fast_mode_changed)
diff --git a/linien/gui/ui/optimization_panel.ui b/linien-gui/linien_gui/ui/optimization_panel.ui
similarity index 100%
rename from linien/gui/ui/optimization_panel.ui
rename to linien-gui/linien_gui/ui/optimization_panel.ui
diff --git a/linien/gui/ui/plot_widget.py b/linien-gui/linien_gui/ui/plot_widget.py
similarity index 99%
rename from linien/gui/ui/plot_widget.py
rename to linien-gui/linien_gui/ui/plot_widget.py
index 4cc5c092..c22aaaf6 100644
--- a/linien/gui/ui/plot_widget.py
+++ b/linien-gui/linien_gui/ui/plot_widget.py
@@ -21,12 +21,7 @@
 
 import numpy as np
 import pyqtgraph as pg
-from PyQt5 import QtGui, QtWidgets
-from PyQt5.QtCore import pyqtSignal
-from pyqtgraph.Qt import QtCore
-
-from linien.client.utils import peak_voltage_to_dBm
-from linien.common import (
+from linien_common.common import (
     DECIMATION,
     N_POINTS,
     SpectrumUncorrelatedException,
@@ -37,9 +32,12 @@
     get_signal_strength_from_i_q,
     update_signal_history,
 )
-from linien.config import N_COLORS
-from linien.gui.config import COLORS, DEFAULT_PLOT_RATE_LIMIT
-from linien.gui.widgets import CustomWidget
+from linien_common.config import N_COLORS
+from linien_gui.config import COLORS, DEFAULT_PLOT_RATE_LIMIT
+from linien_gui.widgets import CustomWidget
+from PyQt5 import QtGui, QtWidgets
+from PyQt5.QtCore import pyqtSignal
+from pyqtgraph.Qt import QtCore
 
 # NOTE: this is required for using a pen_width > 1.
 # There is a bug though that causes the plot to be way too small. Therefore,
@@ -59,6 +57,10 @@
 INVALID_POWER = -1000
 
 
+def peak_voltage_to_dBm(voltage):
+    return 10 + 20 * np.log10(voltage)
+
+
 class TimeXAxis(pg.AxisItem, CustomWidget):
     """Plots x axis as time in seconds instead of point number."""
 
diff --git a/linien/gui/ui/psd_plot_widget.py b/linien-gui/linien_gui/ui/psd_plot_widget.py
similarity index 98%
rename from linien/gui/ui/psd_plot_widget.py
rename to linien-gui/linien_gui/ui/psd_plot_widget.py
index a7fb26d6..704b0d10 100644
--- a/linien/gui/ui/psd_plot_widget.py
+++ b/linien-gui/linien_gui/ui/psd_plot_widget.py
@@ -18,9 +18,8 @@
 
 import numpy as np
 import pyqtgraph as pg
-
-from linien.gui.ui.plot_widget import V
-from linien.gui.widgets import CustomWidget
+from linien_gui.ui.plot_widget import V
+from linien_gui.widgets import CustomWidget
 
 
 class CustomLogAxis(pg.AxisItem):
diff --git a/linien/gui/ui/psd_table_widget.py b/linien-gui/linien_gui/ui/psd_table_widget.py
similarity index 97%
rename from linien/gui/ui/psd_table_widget.py
rename to linien-gui/linien_gui/ui/psd_table_widget.py
index 19554999..8f9b6ec9 100644
--- a/linien/gui/ui/psd_table_widget.py
+++ b/linien-gui/linien_gui/ui/psd_table_widget.py
@@ -18,12 +18,11 @@
 
 from datetime import datetime
 
+from linien_gui.utils_gui import color_to_hex
+from linien_gui.widgets import CustomWidget
 from PyQt5 import QtCore, QtWidgets
 from PyQt5.QtCore import pyqtSignal
 
-from linien.gui.utils_gui import color_to_hex
-from linien.gui.widgets import CustomWidget
-
 
 class PSDTableWidget(QtWidgets.QTableWidget, CustomWidget):
     show_or_hide_curve = pyqtSignal(str, bool)
diff --git a/linien/gui/ui/psd_window.py b/linien-gui/linien_gui/ui/psd_window.py
similarity index 95%
rename from linien/gui/ui/psd_window.py
rename to linien-gui/linien_gui/ui/psd_window.py
index 0cb81653..0967c688 100644
--- a/linien/gui/ui/psd_window.py
+++ b/linien-gui/linien_gui/ui/psd_window.py
@@ -19,14 +19,13 @@
 import pickle
 from time import time
 
+import linien_gui
+from linien_common.common import PSD_ALGORITHM_LPSD, PSD_ALGORITHM_WELCH
+from linien_gui.dialogs import error_dialog
+from linien_gui.utils_gui import RandomColorChoser, param2ui, set_window_icon
+from linien_gui.widgets import CustomWidget
 from PyQt5 import QtWidgets
 
-import linien
-from linien.common import PSD_ALGORITHM_LPSD, PSD_ALGORITHM_WELCH
-from linien.gui.dialogs import error_dialog
-from linien.gui.utils_gui import RandomColorChoser, param2ui, set_window_icon
-from linien.gui.widgets import CustomWidget
-
 
 class PSDWindow(QtWidgets.QMainWindow, CustomWidget):
     def __init__(self, *args, **kwargs):
@@ -174,7 +173,7 @@ def export_psd(self):
             with open(fn, "wb") as f:
                 pickle.dump(
                     {
-                        "linien-version": linien.__version__,
+                        "linien-version": linien_gui.__version__,
                         "time": time(),
                         "psd-data": self.data,
                     },
diff --git a/linien/gui/ui/psd_window.ui b/linien-gui/linien_gui/ui/psd_window.ui
similarity index 100%
rename from linien/gui/ui/psd_window.ui
rename to linien-gui/linien_gui/ui/psd_window.ui
diff --git a/linien/gui/ui/right_panel.py b/linien-gui/linien_gui/ui/right_panel.py
similarity index 98%
rename from linien/gui/ui/right_panel.py
rename to linien-gui/linien_gui/ui/right_panel.py
index 9b1e3d13..a6a6de04 100644
--- a/linien/gui/ui/right_panel.py
+++ b/linien-gui/linien_gui/ui/right_panel.py
@@ -16,10 +16,9 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
+from linien_gui.widgets import CustomWidget
 from PyQt5 import QtWidgets
 
-from linien.gui.widgets import CustomWidget
-
 
 class RightPanel(QtWidgets.QWidget, CustomWidget):
     def __init__(self, *args, **kwargs):
diff --git a/linien/gui/ui/spectroscopy_panel.py b/linien-gui/linien_gui/ui/spectroscopy_panel.py
similarity index 97%
rename from linien/gui/ui/spectroscopy_panel.py
rename to linien-gui/linien_gui/ui/spectroscopy_panel.py
index b400b37b..4e6e942f 100644
--- a/linien/gui/ui/spectroscopy_panel.py
+++ b/linien-gui/linien_gui/ui/spectroscopy_panel.py
@@ -16,12 +16,11 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
+from linien_common.common import HIGH_PASS_FILTER, LOW_PASS_FILTER
+from linien_gui.utils_gui import param2ui
+from linien_gui.widgets import CustomWidget
 from PyQt5 import QtWidgets
 
-from linien.common import HIGH_PASS_FILTER, LOW_PASS_FILTER
-from linien.gui.utils_gui import param2ui
-from linien.gui.widgets import CustomWidget
-
 
 class SpectroscopyPanel(QtWidgets.QWidget, CustomWidget):
     def __init__(self, *args):
diff --git a/linien/gui/ui/spectroscopy_panel.ui b/linien-gui/linien_gui/ui/spectroscopy_panel.ui
similarity index 100%
rename from linien/gui/ui/spectroscopy_panel.ui
rename to linien-gui/linien_gui/ui/spectroscopy_panel.ui
diff --git a/linien/gui/ui/spin_box.py b/linien-gui/linien_gui/ui/spin_box.py
similarity index 100%
rename from linien/gui/ui/spin_box.py
rename to linien-gui/linien_gui/ui/spin_box.py
diff --git a/linien/gui/ui/sweep_control.py b/linien-gui/linien_gui/ui/sweep_control.py
similarity index 98%
rename from linien/gui/ui/sweep_control.py
rename to linien-gui/linien_gui/ui/sweep_control.py
index bf5ef67d..cce9c217 100644
--- a/linien/gui/ui/sweep_control.py
+++ b/linien-gui/linien_gui/ui/sweep_control.py
@@ -16,10 +16,9 @@
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
 import superqt
+from linien_gui.widgets import CustomWidget
 from PyQt5 import QtWidgets
 
-from linien.gui.widgets import CustomWidget
-
 
 class SweepControlWidget(QtWidgets.QWidget, CustomWidget):
     def __init__(self, *args, **kwargs):
@@ -100,7 +99,6 @@ def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
     def init(self):
-
         # set control boundaries
         self.setMinimum(-1.0)
         self.setMaximum(1.0)
diff --git a/linien/gui/ui/version_checker.py b/linien-gui/linien_gui/ui/version_checker.py
similarity index 95%
rename from linien/gui/ui/version_checker.py
rename to linien-gui/linien_gui/ui/version_checker.py
index 9c7b71dd..e14c2ebb 100644
--- a/linien/gui/ui/version_checker.py
+++ b/linien-gui/linien_gui/ui/version_checker.py
@@ -19,17 +19,16 @@
 import json
 from urllib.request import urlopen
 
+import linien_gui
 from packaging import version
 from PyQt5.QtCore import QThread, pyqtSignal
 
-import linien
-
 
 class VersionCheckerThread(QThread):
     check_done = pyqtSignal(bool)
 
     def run(self):
-        our_version = version.parse(linien.__version__)
+        our_version = version.parse(linien_gui.__version__)
         latest_version = our_version
         print("Check whether new version is available.")
         url = "https://raw.githubusercontent.com/linien-org/linien/master/version-info.json"  # noqa: E501
diff --git a/linien/gui/ui/view_panel.py b/linien-gui/linien_gui/ui/view_panel.py
similarity index 97%
rename from linien/gui/ui/view_panel.py
rename to linien-gui/linien_gui/ui/view_panel.py
index c2498ca3..910bdc0a 100644
--- a/linien/gui/ui/view_panel.py
+++ b/linien-gui/linien_gui/ui/view_panel.py
@@ -21,12 +21,11 @@
 from os import path
 
 import numpy as np
+from linien_common.config import N_COLORS
+from linien_gui.utils_gui import color_to_hex, param2ui
+from linien_gui.widgets import CustomWidget
 from PyQt5 import QtGui, QtWidgets
 
-from linien.config import N_COLORS
-from linien.gui.utils_gui import color_to_hex, param2ui
-from linien.gui.widgets import CustomWidget
-
 
 class ViewPanel(QtWidgets.QWidget, CustomWidget):
     def __init__(self, *args, **kwargs):
diff --git a/linien/gui/ui/view_panel.ui b/linien-gui/linien_gui/ui/view_panel.ui
similarity index 100%
rename from linien/gui/ui/view_panel.ui
rename to linien-gui/linien_gui/ui/view_panel.ui
diff --git a/linien/gui/utils_gui.py b/linien-gui/linien_gui/utils_gui.py
similarity index 100%
rename from linien/gui/utils_gui.py
rename to linien-gui/linien_gui/utils_gui.py
diff --git a/linien/gui/widgets.py b/linien-gui/linien_gui/widgets.py
similarity index 91%
rename from linien/gui/widgets.py
rename to linien-gui/linien_gui/widgets.py
index 1e2daf11..d14effd8 100644
--- a/linien/gui/widgets.py
+++ b/linien-gui/linien_gui/widgets.py
@@ -16,15 +16,13 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
-import os
 import weakref
-from os import path
+from pathlib import Path
 
 from PyQt5 import uic
 from pyqtgraph.Qt import QtCore
 
-# add ui folder to path
-ui_path = os.path.join(*list(os.path.split(os.path.abspath(__file__))[:-1]) + ["ui"])
+UI_PATH = Path(__file__).parents[0].resolve() / "ui"
 
 
 class IDSelector:
@@ -71,4 +69,5 @@ def app(self, app):
 
     def load_ui(self, name):
         assert name.endswith(".ui")
-        uic.loadUi(path.join(ui_path, name), self)
+        path = UI_PATH / name
+        uic.loadUi(str(path), self)
diff --git a/linien-gui/pyinstaller.spec b/linien-gui/pyinstaller.spec
new file mode 100644
index 00000000..6095460f
--- /dev/null
+++ b/linien-gui/pyinstaller.spec
@@ -0,0 +1,39 @@
+# -*- mode: python -*-
+# This spec file is for compiling a standalone version of linien using pyinstaller.
+# Run with `pyinstaller pyinstaller.spec`
+
+block_cipher = None
+
+datas = [("linien_gui/ui/*", "linien_gui/ui"), ("linien_gui/icon.ico", "linien_gui")]
+
+a = Analysis(
+    ["linien_gui/app.py"],
+    pathex=[],
+    binaries=[],
+    datas=datas,
+    hiddenimports=["PyQt5.sip", "superqt"],
+    hookspath=[],
+    runtime_hooks=[],
+    excludes=[],
+    win_no_prefer_redirects=False,
+    win_private_assemblies=False,
+    cipher=block_cipher,
+    noarchive=False,
+)
+pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
+exe = EXE(
+    pyz,
+    a.scripts,
+    a.binaries,
+    a.zipfiles,
+    a.datas,
+    [],
+    name="linien-gui",
+    debug=False,
+    bootloader_ignore_signals=False,
+    strip=False,
+    upx=True,
+    runtime_tmpdir=None,
+    console=False,
+    icon="linien_gui/icon.ico",
+)
diff --git a/setup_gui.py b/linien-gui/setup.py
similarity index 64%
rename from setup_gui.py
rename to linien-gui/setup.py
index b47f0fb6..dc5b9394 100644
--- a/setup_gui.py
+++ b/linien-gui/setup.py
@@ -16,52 +16,45 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
-import setuptools
+from setuptools import find_packages, setup
+from setuptools_scm import get_version
 
-import linien
+version = get_version(root="..", relative_to=__file__).split("+")[0]
 
-assert linien.__version__ != "dev"
-
-with open("README.md", "r") as fh:
-    long_description = fh.read()
-
-setuptools.setup(
-    name="linien",
-    version=linien.__version__,
+setup(
+    name="linien-gui",
+    use_scm_version={
+        "write_to": "linien-gui/linien_gui/_version.py",
+        "root": "..",
+        "relative_to": __file__,
+    },
+    setup_requires=["setuptools_scm"],
     author="Benjamin Wiegand",
     author_email="highwaychile@posteo.de",
     maintainer="Bastian Leykauf",
     maintainer_email="leykauf@physik.hu-berlin.de",
     description="Spectroscopy lock application using RedPitaya",
-    long_description=long_description,
-    long_description_content_type="text/markdown",
+    long_description="Have a look at the 'linien' package for installation instructions.",  # noqa: E501
+    long_description_content_type="text/x-rst",
     url="https://github.com/linien-org/linien",
-    # IMPORTANT: any changes have to be made in setup_client_and_gui.py
-    # of flathub repo as well
-    packages=["linien", "linien.gui", "linien.gui.ui"],
+    packages=find_packages(),
     classifiers=[
         "Programming Language :: Python :: 3",
         "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
         "Operating System :: OS Independent",
     ],
-    entry_points={"console_scripts": ["linien=linien.gui.app:run_application"]},
+    entry_points={"console_scripts": ["linien=linien_gui.app:run_application"]},
     install_requires=[
+        "appdirs>=1.4.4",
         "click>=7.1.2",
-        "numpy>=1.19.1",
-        "paramiko>=2.10.1",
-        "plumbum>=1.6.9",
         "pyqtgraph>=0.10.0",
         "PyQt5>=5.12.0",
-        "rpyc>=4.0,<5.0",
-        "scipy>=1.4.1",
         "superqt>=0.2.3",
-        "linien-python-client==" + linien.__version__,
+        "linien_client=={}".format(version),
     ],
     package_data={
         # IMPORTANT: any changes have to be made in pyinstaller.spec, too
         # (for the standalone installer)
-        # IMPORTANT: any changes have to be made in setup_client_and_gui.py
-        # of flathub repo as well
-        "": ["*.ui", "VERSION", "*.ico"]
+        "": ["*.ui", "*.ico"]
     },
 )
diff --git a/linien-server/linien_server/__init__.py b/linien-server/linien_server/__init__.py
new file mode 100644
index 00000000..f6befe54
--- /dev/null
+++ b/linien-server/linien_server/__init__.py
@@ -0,0 +1,7 @@
+from pkg_resources import DistributionNotFound, get_distribution
+
+try:
+    __version__ = get_distribution("linien-server").version
+except DistributionNotFound:
+    # package is not installed
+    pass
diff --git a/linien/server/acquisition.py b/linien-server/linien_server/acquisition.py
similarity index 97%
rename from linien/server/acquisition.py
rename to linien-server/linien_server/acquisition.py
index ce830db6..fde61323 100644
--- a/linien/server/acquisition.py
+++ b/linien-server/linien_server/acquisition.py
@@ -16,19 +16,16 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
-import sys
-
-sys.path += ["../../"]
 import atexit
+import sys
 import threading
 from enum import Enum
 from multiprocessing import Pipe, Process
 from time import sleep
 
 import rpyc
-
-from linien.config import ACQUISITION_PORT
-from linien.server.utils import flash_fpga, start_nginx, stop_nginx
+from linien_common.config import ACQUISITION_PORT
+from linien_server.utils import flash_fpga, start_nginx, stop_nginx
 
 
 class AcquisitionConnectionError(Exception):
@@ -85,7 +82,7 @@ def connect_acquisition_process(self, pipe, use_ssh, host):
             acquisition = acquisition_rpyc.root
         else:
             # this is what happens in production mode
-            from linien.server.acquisition_process import DataAcquisitionService
+            from linien_server.acquisition_process import DataAcquisitionService
 
             stop_nginx()
             flash_fpga()
diff --git a/linien/server/acquisition_process.py b/linien-server/linien_server/acquisition_process.py
similarity index 98%
rename from linien/server/acquisition_process.py
rename to linien-server/linien_server/acquisition_process.py
index 06b8ad94..bb05bbd8 100644
--- a/linien/server/acquisition_process.py
+++ b/linien-server/linien_server/acquisition_process.py
@@ -25,15 +25,13 @@
 from time import sleep
 
 import numpy as np
+from linien_common.common import DECIMATION, MAX_N_POINTS, N_POINTS
+from linien_common.config import ACQUISITION_PORT
 from PyRedPitaya.board import RedPitaya
 from rpyc import Service
 from rpyc.utils.server import OneShotServer
 
-sys.path += ["../../"]
-from csr import PythonCSR
-
-from linien.common import DECIMATION, MAX_N_POINTS, N_POINTS
-from linien.config import ACQUISITION_PORT
+from .csr import PythonCSR
 
 
 def shutdown():
diff --git a/linien/server/approach_line.py b/linien-server/linien_server/approach_line.py
similarity index 98%
rename from linien/server/approach_line.py
rename to linien-server/linien_server/approach_line.py
index 9641b66f..4be3b000 100644
--- a/linien/server/approach_line.py
+++ b/linien-server/linien_server/approach_line.py
@@ -19,8 +19,7 @@
 from time import time
 
 import numpy as np
-
-from linien.common import determine_shift_by_correlation
+from linien_common.common import determine_shift_by_correlation
 
 ZOOM_STEP = 2
 
diff --git a/linien/gui/ui/__init__.py b/linien-server/linien_server/autolock/__init__.py
similarity index 100%
rename from linien/gui/ui/__init__.py
rename to linien-server/linien_server/autolock/__init__.py
diff --git a/linien/server/autolock/algorithm_selection.py b/linien-server/linien_server/autolock/algorithm_selection.py
similarity index 98%
rename from linien/server/autolock/algorithm_selection.py
rename to linien-server/linien_server/autolock/algorithm_selection.py
index 8c5c2d7a..e975b2da 100644
--- a/linien/server/autolock/algorithm_selection.py
+++ b/linien-server/linien_server/autolock/algorithm_selection.py
@@ -15,7 +15,7 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
-from linien.common import (
+from linien_common.common import (
     AUTO_DETECT_AUTOLOCK_MODE,
     FAST_AUTOLOCK,
     N_POINTS,
diff --git a/linien/server/autolock/autolock.py b/linien-server/linien_server/autolock/autolock.py
similarity index 97%
rename from linien/server/autolock/autolock.py
rename to linien-server/linien_server/autolock/autolock.py
index cbf0d72f..d78419aa 100644
--- a/linien/server/autolock/autolock.py
+++ b/linien-server/linien_server/autolock/autolock.py
@@ -19,10 +19,10 @@
 import pickle
 import traceback
 
-from linien.common import check_plot_data, combine_error_signal, get_lock_point
-from linien.server.autolock.algorithm_selection import AutolockAlgorithmSelector
-from linien.server.autolock.fast import FastAutolock
-from linien.server.autolock.robust import RobustAutolock
+from linien_common.common import check_plot_data, combine_error_signal, get_lock_point
+from linien_server.autolock.algorithm_selection import AutolockAlgorithmSelector
+from linien_server.autolock.fast import FastAutolock
+from linien_server.autolock.robust import RobustAutolock
 
 
 class Autolock:
diff --git a/linien/server/autolock/fast.py b/linien-server/linien_server/autolock/fast.py
similarity index 95%
rename from linien/server/autolock/fast.py
rename to linien-server/linien_server/autolock/fast.py
index e80dace3..67ef1cdc 100644
--- a/linien/server/autolock/fast.py
+++ b/linien-server/linien_server/autolock/fast.py
@@ -16,7 +16,10 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
-from linien.common import SpectrumUncorrelatedException, determine_shift_by_correlation
+from linien_common.common import (
+    SpectrumUncorrelatedException,
+    determine_shift_by_correlation,
+)
 
 
 class FastAutolock:
diff --git a/linien/server/autolock/robust.py b/linien-server/linien_server/autolock/robust.py
similarity index 98%
rename from linien/server/autolock/robust.py
rename to linien-server/linien_server/autolock/robust.py
index 30b8b141..73551d48 100644
--- a/linien/server/autolock/robust.py
+++ b/linien-server/linien_server/autolock/robust.py
@@ -19,13 +19,12 @@
 from time import time
 
 import numpy as np
-
-from linien.common import (
+from linien_common.common import (
     AUTOLOCK_MAX_N_INSTRUCTIONS,
     SpectrumUncorrelatedException,
     determine_shift_by_correlation,
 )
-from linien.server.autolock.utils import (
+from linien_server.autolock.utils import (
     crop_spectra_to_same_view,
     get_all_peaks,
     get_diff_at_time_scale,
@@ -35,7 +34,7 @@
     sign,
     sum_up_spectrum,
 )
-from linien.server.utils import sweep_speed_to_time
+from linien_server.utils import sweep_speed_to_time
 
 
 class LockPositionNotFound(Exception):
diff --git a/linien/server/autolock/utils.py b/linien-server/linien_server/autolock/utils.py
similarity index 100%
rename from linien/server/autolock/utils.py
rename to linien-server/linien_server/autolock/utils.py
diff --git a/linien/server/csr.py b/linien-server/linien_server/csr.py
similarity index 98%
rename from linien/server/csr.py
rename to linien-server/linien_server/csr.py
index 35150d72..e3a6fd9f 100644
--- a/linien/server/csr.py
+++ b/linien-server/linien_server/csr.py
@@ -18,8 +18,8 @@
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
 
-import csrmap
-from iir_coeffs import get_params
+from . import csrmap
+from .iir_coeffs import get_params
 
 
 class PitayaCSR:
diff --git a/linien-server/linien_server/csrmap.py b/linien-server/linien_server/csrmap.py
new file mode 100644
index 00000000..efd3b64e
--- /dev/null
+++ b/linien-server/linien_server/csrmap.py
@@ -0,0 +1,346 @@
+csr_constants = {
+    "fast_a_iir_c_1_shift": 23,
+    "fast_a_iir_c_1_width": 25,
+    "fast_a_iir_c_1_interval": 1,
+    "fast_a_iir_c_1_latency": 2,
+    "fast_a_iir_c_1_order": 1,
+    "fast_a_iir_c_1_iterative": 0,
+    "fast_a_iir_d_1_shift": 23,
+    "fast_a_iir_d_1_width": 25,
+    "fast_a_iir_d_1_interval": 1,
+    "fast_a_iir_d_1_latency": 3,
+    "fast_a_iir_d_1_order": 2,
+    "fast_a_iir_d_1_iterative": 0,
+    "fast_a_iir_c_2_shift": 23,
+    "fast_a_iir_c_2_width": 25,
+    "fast_a_iir_c_2_interval": 1,
+    "fast_a_iir_c_2_latency": 2,
+    "fast_a_iir_c_2_order": 1,
+    "fast_a_iir_c_2_iterative": 0,
+    "fast_a_iir_d_2_shift": 23,
+    "fast_a_iir_d_2_width": 25,
+    "fast_a_iir_d_2_interval": 1,
+    "fast_a_iir_d_2_latency": 3,
+    "fast_a_iir_d_2_order": 2,
+    "fast_a_iir_d_2_iterative": 0,
+    "fast_b_iir_c_1_shift": 23,
+    "fast_b_iir_c_1_width": 25,
+    "fast_b_iir_c_1_interval": 1,
+    "fast_b_iir_c_1_latency": 2,
+    "fast_b_iir_c_1_order": 1,
+    "fast_b_iir_c_1_iterative": 0,
+    "fast_b_iir_d_1_shift": 23,
+    "fast_b_iir_d_1_width": 25,
+    "fast_b_iir_d_1_interval": 1,
+    "fast_b_iir_d_1_latency": 3,
+    "fast_b_iir_d_1_order": 2,
+    "fast_b_iir_d_1_iterative": 0,
+    "fast_b_iir_c_2_shift": 23,
+    "fast_b_iir_c_2_width": 25,
+    "fast_b_iir_c_2_interval": 1,
+    "fast_b_iir_c_2_latency": 2,
+    "fast_b_iir_c_2_order": 1,
+    "fast_b_iir_c_2_iterative": 0,
+    "fast_b_iir_d_2_shift": 23,
+    "fast_b_iir_d_2_width": 25,
+    "fast_b_iir_d_2_interval": 1,
+    "fast_b_iir_d_2_latency": 3,
+    "fast_b_iir_d_2_order": 2,
+    "fast_b_iir_d_2_iterative": 0,
+    "logic_raw_acquisition_iir_shift": 23,
+    "logic_raw_acquisition_iir_width": 25,
+    "logic_raw_acquisition_iir_interval": 1,
+    "logic_raw_acquisition_iir_latency": 6,
+    "logic_raw_acquisition_iir_order": 5,
+    "logic_raw_acquisition_iir_iterative": 0,
+}
+
+csr = {
+    "dna_dna": (28, 0x000, 64, False),
+    "fast_a_y_tap": (0, 0x000, 2, True),
+    "fast_a_invert": (0, 0x001, 1, True),
+    "fast_a_demod_delay": (0, 0x002, 32, True),
+    "fast_a_demod_multiplier": (0, 0x006, 4, True),
+    "fast_a_x_limit_1_min": (0, 0x007, 25, True),
+    "fast_a_x_limit_1_max": (0, 0x00B, 25, True),
+    "fast_a_iir_c_1_z0": (0, 0x00F, 27, True),
+    "fast_a_iir_c_1_a1": (0, 0x013, 25, True),
+    "fast_a_iir_c_1_b0": (0, 0x017, 25, True),
+    "fast_a_iir_c_1_b1": (0, 0x01B, 25, True),
+    "fast_a_iir_d_1_z0": (0, 0x01F, 27, True),
+    "fast_a_iir_d_1_a1": (0, 0x023, 25, True),
+    "fast_a_iir_d_1_a2": (0, 0x027, 25, True),
+    "fast_a_iir_d_1_b0": (0, 0x02B, 25, True),
+    "fast_a_iir_d_1_b1": (0, 0x02F, 25, True),
+    "fast_a_iir_d_1_b2": (0, 0x033, 25, True),
+    "fast_a_y_limit_1_min": (0, 0x037, 25, True),
+    "fast_a_y_limit_1_max": (0, 0x03B, 25, True),
+    "fast_a_x_limit_2_min": (0, 0x03F, 25, True),
+    "fast_a_x_limit_2_max": (0, 0x043, 25, True),
+    "fast_a_iir_c_2_z0": (0, 0x047, 27, True),
+    "fast_a_iir_c_2_a1": (0, 0x04B, 25, True),
+    "fast_a_iir_c_2_b0": (0, 0x04F, 25, True),
+    "fast_a_iir_c_2_b1": (0, 0x053, 25, True),
+    "fast_a_iir_d_2_z0": (0, 0x057, 27, True),
+    "fast_a_iir_d_2_a1": (0, 0x05B, 25, True),
+    "fast_a_iir_d_2_a2": (0, 0x05F, 25, True),
+    "fast_a_iir_d_2_b0": (0, 0x063, 25, True),
+    "fast_a_iir_d_2_b1": (0, 0x067, 25, True),
+    "fast_a_iir_d_2_b2": (0, 0x06B, 25, True),
+    "fast_a_y_limit_2_min": (0, 0x06F, 25, True),
+    "fast_a_y_limit_2_max": (0, 0x073, 25, True),
+    "fast_a_x_clr": (0, 0x077, 1, True),
+    "fast_a_x_max": (0, 0x078, 25, False),
+    "fast_a_x_min": (0, 0x07C, 25, False),
+    "fast_a_out_i_clr": (0, 0x080, 1, True),
+    "fast_a_out_i_max": (0, 0x081, 25, False),
+    "fast_a_out_i_min": (0, 0x085, 25, False),
+    "fast_a_out_q_clr": (0, 0x089, 1, True),
+    "fast_a_out_q_max": (0, 0x08A, 25, False),
+    "fast_a_out_q_min": (0, 0x08E, 25, False),
+    "fast_a_dx_sel": (0, 0x092, 4, True),
+    "fast_a_dy_sel": (0, 0x093, 4, True),
+    "fast_b_y_tap": (1, 0x000, 2, True),
+    "fast_b_invert": (1, 0x001, 1, True),
+    "fast_b_demod_delay": (1, 0x002, 32, True),
+    "fast_b_demod_multiplier": (1, 0x006, 4, True),
+    "fast_b_x_limit_1_min": (1, 0x007, 25, True),
+    "fast_b_x_limit_1_max": (1, 0x00B, 25, True),
+    "fast_b_iir_c_1_z0": (1, 0x00F, 27, True),
+    "fast_b_iir_c_1_a1": (1, 0x013, 25, True),
+    "fast_b_iir_c_1_b0": (1, 0x017, 25, True),
+    "fast_b_iir_c_1_b1": (1, 0x01B, 25, True),
+    "fast_b_iir_d_1_z0": (1, 0x01F, 27, True),
+    "fast_b_iir_d_1_a1": (1, 0x023, 25, True),
+    "fast_b_iir_d_1_a2": (1, 0x027, 25, True),
+    "fast_b_iir_d_1_b0": (1, 0x02B, 25, True),
+    "fast_b_iir_d_1_b1": (1, 0x02F, 25, True),
+    "fast_b_iir_d_1_b2": (1, 0x033, 25, True),
+    "fast_b_y_limit_1_min": (1, 0x037, 25, True),
+    "fast_b_y_limit_1_max": (1, 0x03B, 25, True),
+    "fast_b_x_limit_2_min": (1, 0x03F, 25, True),
+    "fast_b_x_limit_2_max": (1, 0x043, 25, True),
+    "fast_b_iir_c_2_z0": (1, 0x047, 27, True),
+    "fast_b_iir_c_2_a1": (1, 0x04B, 25, True),
+    "fast_b_iir_c_2_b0": (1, 0x04F, 25, True),
+    "fast_b_iir_c_2_b1": (1, 0x053, 25, True),
+    "fast_b_iir_d_2_z0": (1, 0x057, 27, True),
+    "fast_b_iir_d_2_a1": (1, 0x05B, 25, True),
+    "fast_b_iir_d_2_a2": (1, 0x05F, 25, True),
+    "fast_b_iir_d_2_b0": (1, 0x063, 25, True),
+    "fast_b_iir_d_2_b1": (1, 0x067, 25, True),
+    "fast_b_iir_d_2_b2": (1, 0x06B, 25, True),
+    "fast_b_y_limit_2_min": (1, 0x06F, 25, True),
+    "fast_b_y_limit_2_max": (1, 0x073, 25, True),
+    "fast_b_x_clr": (1, 0x077, 1, True),
+    "fast_b_x_max": (1, 0x078, 25, False),
+    "fast_b_x_min": (1, 0x07C, 25, False),
+    "fast_b_out_i_clr": (1, 0x080, 1, True),
+    "fast_b_out_i_max": (1, 0x081, 25, False),
+    "fast_b_out_i_min": (1, 0x085, 25, False),
+    "fast_b_out_q_clr": (1, 0x089, 1, True),
+    "fast_b_out_q_max": (1, 0x08A, 25, False),
+    "fast_b_out_q_min": (1, 0x08E, 25, False),
+    "fast_b_dx_sel": (1, 0x092, 4, True),
+    "fast_b_dy_sel": (1, 0x093, 4, True),
+    "gpio_n_ins": (30, 0x000, 8, False),
+    "gpio_n_outs": (30, 0x001, 8, True),
+    "gpio_n_oes": (30, 0x002, 8, True),
+    "gpio_n_state": (30, 0x003, 14, False),
+    "gpio_n_state_clr": (30, 0x005, 1, True),
+    "gpio_n_do0_en": (30, 0x006, 14, True),
+    "gpio_n_do1_en": (30, 0x008, 14, True),
+    "gpio_n_do2_en": (30, 0x00A, 14, True),
+    "gpio_n_do3_en": (30, 0x00C, 14, True),
+    "gpio_n_do4_en": (30, 0x00E, 14, True),
+    "gpio_n_do5_en": (30, 0x010, 14, True),
+    "gpio_n_do6_en": (30, 0x012, 14, True),
+    "gpio_n_do7_en": (30, 0x014, 14, True),
+    "gpio_p_ins": (31, 0x000, 8, False),
+    "gpio_p_outs": (31, 0x001, 8, True),
+    "gpio_p_oes": (31, 0x002, 8, True),
+    "logic_dual_channel": (8, 0x000, 1, True),
+    "logic_chain_a_factor": (8, 0x001, 9, True),
+    "logic_chain_b_factor": (8, 0x003, 9, True),
+    "logic_chain_a_offset": (8, 0x005, 14, True),
+    "logic_chain_b_offset": (8, 0x007, 14, True),
+    "logic_combined_offset": (8, 0x009, 14, True),
+    "logic_out_offset": (8, 0x00B, 14, True),
+    "logic_mod_channel": (8, 0x00D, 1, True),
+    "logic_control_channel": (8, 0x00E, 1, True),
+    "logic_sweep_channel": (8, 0x00F, 2, True),
+    "logic_fast_mode": (8, 0x010, 1, True),
+    "logic_slow_value": (8, 0x011, 14, False),
+    "logic_slow_decimation": (8, 0x013, 5, True),
+    "logic_analog_out_1": (8, 0x014, 15, True),
+    "logic_analog_out_2": (8, 0x016, 15, True),
+    "logic_analog_out_3": (8, 0x018, 15, True),
+    "logic_mod_amp": (8, 0x01A, 14, True),
+    "logic_mod_freq": (8, 0x01C, 32, True),
+    "logic_sweep_step": (8, 0x020, 30, True),
+    "logic_sweep_min": (8, 0x024, 14, True),
+    "logic_sweep_max": (8, 0x026, 14, True),
+    "logic_sweep_run": (8, 0x028, 1, True),
+    "logic_sweep_pause": (8, 0x029, 1, True),
+    "logic_limit_error_signal_min": (8, 0x02A, 25, True),
+    "logic_limit_error_signal_max": (8, 0x02E, 25, True),
+    "logic_limit_fast1_min": (8, 0x032, 14, True),
+    "logic_limit_fast1_max": (8, 0x034, 14, True),
+    "logic_limit_fast2_min": (8, 0x036, 14, True),
+    "logic_limit_fast2_max": (8, 0x038, 14, True),
+    "logic_pid_setpoint": (8, 0x03A, 25, True),
+    "logic_pid_kp": (8, 0x03E, 14, True),
+    "logic_pid_ki": (8, 0x040, 14, True),
+    "logic_pid_reset": (8, 0x042, 1, True),
+    "logic_pid_kd": (8, 0x043, 14, True),
+    "logic_autolock_robust_time_scale": (8, 0x045, 14, True),
+    "logic_autolock_robust_N_instructions": (8, 0x047, 5, True),
+    "logic_autolock_robust_final_wait_time": (8, 0x048, 14, True),
+    "logic_autolock_robust_peak_height_0": (8, 0x04A, 28, True),
+    "logic_autolock_robust_peak_height_1": (8, 0x04E, 28, True),
+    "logic_autolock_robust_peak_height_2": (8, 0x052, 28, True),
+    "logic_autolock_robust_peak_height_3": (8, 0x056, 28, True),
+    "logic_autolock_robust_peak_height_4": (8, 0x05A, 28, True),
+    "logic_autolock_robust_peak_height_5": (8, 0x05E, 28, True),
+    "logic_autolock_robust_peak_height_6": (8, 0x062, 28, True),
+    "logic_autolock_robust_peak_height_7": (8, 0x066, 28, True),
+    "logic_autolock_robust_peak_height_8": (8, 0x06A, 28, True),
+    "logic_autolock_robust_peak_height_9": (8, 0x06E, 28, True),
+    "logic_autolock_robust_peak_height_10": (8, 0x072, 28, True),
+    "logic_autolock_robust_peak_height_11": (8, 0x076, 28, True),
+    "logic_autolock_robust_peak_height_12": (8, 0x07A, 28, True),
+    "logic_autolock_robust_peak_height_13": (8, 0x07E, 28, True),
+    "logic_autolock_robust_peak_height_14": (8, 0x082, 28, True),
+    "logic_autolock_robust_peak_height_15": (8, 0x086, 28, True),
+    "logic_autolock_robust_peak_height_16": (8, 0x08A, 28, True),
+    "logic_autolock_robust_peak_height_17": (8, 0x08E, 28, True),
+    "logic_autolock_robust_peak_height_18": (8, 0x092, 28, True),
+    "logic_autolock_robust_peak_height_19": (8, 0x096, 28, True),
+    "logic_autolock_robust_peak_height_20": (8, 0x09A, 28, True),
+    "logic_autolock_robust_peak_height_21": (8, 0x09E, 28, True),
+    "logic_autolock_robust_peak_height_22": (8, 0x0A2, 28, True),
+    "logic_autolock_robust_peak_height_23": (8, 0x0A6, 28, True),
+    "logic_autolock_robust_peak_height_24": (8, 0x0AA, 28, True),
+    "logic_autolock_robust_peak_height_25": (8, 0x0AE, 28, True),
+    "logic_autolock_robust_peak_height_26": (8, 0x0B2, 28, True),
+    "logic_autolock_robust_peak_height_27": (8, 0x0B6, 28, True),
+    "logic_autolock_robust_peak_height_28": (8, 0x0BA, 28, True),
+    "logic_autolock_robust_peak_height_29": (8, 0x0BE, 28, True),
+    "logic_autolock_robust_peak_height_30": (8, 0x0C2, 28, True),
+    "logic_autolock_robust_peak_height_31": (8, 0x0C6, 28, True),
+    "logic_autolock_robust_wait_for_0": (8, 0x0CA, 14, True),
+    "logic_autolock_robust_wait_for_1": (8, 0x0CC, 14, True),
+    "logic_autolock_robust_wait_for_2": (8, 0x0CE, 14, True),
+    "logic_autolock_robust_wait_for_3": (8, 0x0D0, 14, True),
+    "logic_autolock_robust_wait_for_4": (8, 0x0D2, 14, True),
+    "logic_autolock_robust_wait_for_5": (8, 0x0D4, 14, True),
+    "logic_autolock_robust_wait_for_6": (8, 0x0D6, 14, True),
+    "logic_autolock_robust_wait_for_7": (8, 0x0D8, 14, True),
+    "logic_autolock_robust_wait_for_8": (8, 0x0DA, 14, True),
+    "logic_autolock_robust_wait_for_9": (8, 0x0DC, 14, True),
+    "logic_autolock_robust_wait_for_10": (8, 0x0DE, 14, True),
+    "logic_autolock_robust_wait_for_11": (8, 0x0E0, 14, True),
+    "logic_autolock_robust_wait_for_12": (8, 0x0E2, 14, True),
+    "logic_autolock_robust_wait_for_13": (8, 0x0E4, 14, True),
+    "logic_autolock_robust_wait_for_14": (8, 0x0E6, 14, True),
+    "logic_autolock_robust_wait_for_15": (8, 0x0E8, 14, True),
+    "logic_autolock_robust_wait_for_16": (8, 0x0EA, 14, True),
+    "logic_autolock_robust_wait_for_17": (8, 0x0EC, 14, True),
+    "logic_autolock_robust_wait_for_18": (8, 0x0EE, 14, True),
+    "logic_autolock_robust_wait_for_19": (8, 0x0F0, 14, True),
+    "logic_autolock_robust_wait_for_20": (8, 0x0F2, 14, True),
+    "logic_autolock_robust_wait_for_21": (8, 0x0F4, 14, True),
+    "logic_autolock_robust_wait_for_22": (8, 0x0F6, 14, True),
+    "logic_autolock_robust_wait_for_23": (8, 0x0F8, 14, True),
+    "logic_autolock_robust_wait_for_24": (8, 0x0FA, 14, True),
+    "logic_autolock_robust_wait_for_25": (8, 0x0FC, 14, True),
+    "logic_autolock_robust_wait_for_26": (8, 0x0FE, 14, True),
+    "logic_autolock_robust_wait_for_27": (8, 0x100, 14, True),
+    "logic_autolock_robust_wait_for_28": (8, 0x102, 14, True),
+    "logic_autolock_robust_wait_for_29": (8, 0x104, 14, True),
+    "logic_autolock_robust_wait_for_30": (8, 0x106, 14, True),
+    "logic_autolock_robust_wait_for_31": (8, 0x108, 14, True),
+    "logic_autolock_fast_target_position": (8, 0x10A, 14, True),
+    "logic_autolock_request_lock": (8, 0x10C, 1, True),
+    "logic_autolock_autolock_mode": (8, 0x10D, 2, True),
+    "logic_autolock_lock_running": (8, 0x10E, 1, False),
+    "logic_raw_acquisition_iir_z0": (8, 0x10F, 27, True),
+    "logic_raw_acquisition_iir_a1": (8, 0x113, 25, True),
+    "logic_raw_acquisition_iir_a2": (8, 0x117, 25, True),
+    "logic_raw_acquisition_iir_a3": (8, 0x11B, 25, True),
+    "logic_raw_acquisition_iir_a4": (8, 0x11F, 25, True),
+    "logic_raw_acquisition_iir_a5": (8, 0x123, 25, True),
+    "logic_raw_acquisition_iir_b0": (8, 0x127, 25, True),
+    "logic_raw_acquisition_iir_b1": (8, 0x12B, 25, True),
+    "logic_raw_acquisition_iir_b2": (8, 0x12F, 25, True),
+    "logic_raw_acquisition_iir_b3": (8, 0x133, 25, True),
+    "logic_raw_acquisition_iir_b4": (8, 0x137, 25, True),
+    "logic_raw_acquisition_iir_b5": (8, 0x13B, 25, True),
+    "logic_control_signal_clr": (8, 0x13F, 1, True),
+    "logic_control_signal_max": (8, 0x140, 25, False),
+    "logic_control_signal_min": (8, 0x144, 25, False),
+    "logic_combined_error_signal_clr": (8, 0x148, 1, True),
+    "logic_combined_error_signal_max": (8, 0x149, 25, False),
+    "logic_combined_error_signal_min": (8, 0x14D, 25, False),
+    "logic_combined_error_signal_filtered_clr": (8, 0x151, 1, True),
+    "logic_combined_error_signal_filtered_max": (8, 0x152, 25, False),
+    "logic_combined_error_signal_filtered_min": (8, 0x156, 25, False),
+    "scopegen_external_trigger": (6, 0x000, 1, True),
+    "scopegen_dac_a_clr": (6, 0x001, 1, True),
+    "scopegen_dac_a_max": (6, 0x002, 25, False),
+    "scopegen_dac_a_min": (6, 0x006, 25, False),
+    "scopegen_dac_b_clr": (6, 0x00A, 1, True),
+    "scopegen_dac_b_max": (6, 0x00B, 25, False),
+    "scopegen_dac_b_min": (6, 0x00F, 25, False),
+    "scopegen_adc_a_sel": (6, 0x013, 4, True),
+    "scopegen_adc_b_sel": (6, 0x014, 4, True),
+    "scopegen_adc_a_q_sel": (6, 0x015, 4, True),
+    "scopegen_adc_b_q_sel": (6, 0x016, 4, True),
+    "slow_pid_setpoint": (2, 0x000, 14, True),
+    "slow_pid_kp": (2, 0x002, 14, True),
+    "slow_pid_ki": (2, 0x004, 14, True),
+    "slow_pid_reset": (2, 0x006, 1, True),
+    "slow_pid_kd": (2, 0x007, 14, True),
+    "slow_limit_min": (2, 0x009, 14, True),
+    "slow_limit_max": (2, 0x00B, 14, True),
+    "slow_out_clr": (2, 0x00D, 1, True),
+    "slow_out_max": (2, 0x00E, 25, False),
+    "slow_out_min": (2, 0x012, 25, False),
+    "xadc_temp": (29, 0x000, 12, False),
+    "xadc_v": (29, 0x002, 12, False),
+    "xadc_a": (29, 0x004, 12, False),
+    "xadc_b": (29, 0x006, 12, False),
+    "xadc_c": (29, 0x008, 12, False),
+    "xadc_d": (29, 0x00A, 12, False),
+}
+states = [
+    "force",
+    "di0",
+    "di1",
+    "di2",
+    "di3",
+    "di4",
+    "di5",
+    "di6",
+    "di7",
+    "robust_watching",
+    "robust_turn_on_lock",
+    "robust_sign_equal",
+    "robust_over_threshold",
+    "robust_waited_long_enough",
+]
+signals = [
+    "zero",
+    "fast_a_x",
+    "fast_a_out_i",
+    "fast_a_out_q",
+    "fast_b_x",
+    "fast_b_out_i",
+    "fast_b_out_q",
+    "slow_out",
+    "scopegen_dac_a",
+    "scopegen_dac_b",
+    "logic_control_signal",
+    "logic_combined_error_signal",
+    "logic_combined_error_signal_filtered",
+]
diff --git a/linien/server/iir_coeffs.py b/linien-server/linien_server/iir_coeffs.py
similarity index 100%
rename from linien/server/iir_coeffs.py
rename to linien-server/linien_server/iir_coeffs.py
diff --git a/linien/server/linien.bin b/linien-server/linien_server/linien.bin
similarity index 100%
rename from linien/server/linien.bin
rename to linien-server/linien_server/linien.bin
diff --git a/linien/server/linien_install_requirements.sh b/linien-server/linien_server/linien_install_requirements.sh
similarity index 70%
rename from linien/server/linien_install_requirements.sh
rename to linien-server/linien_server/linien_install_requirements.sh
index dee6fa61..8d512c41 100644
--- a/linien/server/linien_install_requirements.sh
+++ b/linien-server/linien_server/linien_install_requirements.sh
@@ -11,29 +11,15 @@ while ! ping -c 1 -W 1 8.8.4.4; do
 done
 
 echo 'installing dependencies...'
+echo 'installing screen...'
 # the server is started in a screen session
 apt-get install screen
 
-cd /tmp
-mkdir linien
-
 echo 'installing pyrp3...'
-# install pyrp3
-cd /tmp/linien
-git clone https://github.com/linien-org/pyrp3.git
-cd pyrp3
-python3 setup.py install
-
-echo 'building monitor...'
-# build monitor shared library
-cd monitor
-make
-cp libmonitor.so /usr/lib/
-
-cd /tmp
-rm -R /tmp/linien
+pip3 install git+https://github.com/linien-org/pyrp3.git@7de325f2de2d744d7bd1181003d6aa90a5986fc5
 
 # https://github.com/RedPitaya/RedPitaya/issues/205
+cd /tmp
 echo 'building ethernet blinking fix'
 git clone https://github.com/linien-org/mdio-tool.git
 cd mdio-tool
diff --git a/linien-server/linien_server/linien_start_server.sh b/linien-server/linien_server/linien_start_server.sh
new file mode 100644
index 00000000..d3783397
--- /dev/null
+++ b/linien-server/linien_server/linien_start_server.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+# quit any remaining screen session
+if [ -x "$(command -v linien_stop_server.sh)" ]; then
+    linien_stop_server.sh
+fi
+screen -X -S linien-server quit
+# start a new one
+screen -S linien-server -d -m
+
+# stop ethernet blinking and start the server inside the screen session. Start ethernet blinking again after server stopped.
+# Regarding ethernet blinking, see https://github.com/RedPitaya/RedPitaya/issues/205
+screen -r linien-server -X stuff $"mdio-tool w eth0 0x1b 0x0000; linien-server $1; mdio-tool w eth0 0x1b 0x0f00; \n"
\ No newline at end of file
diff --git a/linien-server/linien_server/linien_stop_server.sh b/linien-server/linien_server/linien_stop_server.sh
new file mode 100644
index 00000000..970a593a
--- /dev/null
+++ b/linien-server/linien_server/linien_stop_server.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+# Close screen session and start ethernet blinking again, see https://github.com/RedPitaya/RedPitaya/issues/205
+screen -X -S linien-server quit
+mdio-tool w eth0 0x1b 0x0f00
\ No newline at end of file
diff --git a/linien/server/autolock/__init__.py b/linien-server/linien_server/optimization/__init__.py
similarity index 100%
rename from linien/server/autolock/__init__.py
rename to linien-server/linien_server/optimization/__init__.py
diff --git a/linien/server/optimization/engine.py b/linien-server/linien_server/optimization/engine.py
similarity index 98%
rename from linien/server/optimization/engine.py
rename to linien-server/linien_server/optimization/engine.py
index 4874d7dc..2f86acd4 100644
--- a/linien/server/optimization/engine.py
+++ b/linien-server/linien_server/optimization/engine.py
@@ -19,9 +19,8 @@
 import math
 
 import cma
-
-from linien.common import MHz, Vpp
-from linien.server.optimization.utils import (
+from linien_common.common import MHz, Vpp
+from linien_server.optimization.utils import (
     FINAL_ZOOM_FACTOR,
     get_max_slope,
     optimize_phase_from_iq,
diff --git a/linien/server/optimization/general.py b/linien-server/linien_server/optimization/general.py
similarity index 100%
rename from linien/server/optimization/general.py
rename to linien-server/linien_server/optimization/general.py
diff --git a/linien/server/optimization/optimization.py b/linien-server/linien_server/optimization/optimization.py
similarity index 96%
rename from linien/server/optimization/optimization.py
rename to linien-server/linien_server/optimization/optimization.py
index 58539544..a2912fac 100644
--- a/linien/server/optimization/optimization.py
+++ b/linien-server/linien_server/optimization/optimization.py
@@ -20,11 +20,10 @@
 import traceback
 
 import numpy as np
-
-from linien.common import determine_shift_by_correlation, get_lock_point
-from linien.server.approach_line import Approacher
-from linien.server.optimization.engine import OptimizerEngine
-from linien.server.optimization.utils import FINAL_ZOOM_FACTOR
+from linien_common.common import determine_shift_by_correlation, get_lock_point
+from linien_server.approach_line import Approacher
+from linien_server.optimization.engine import OptimizerEngine
+from linien_server.optimization.utils import FINAL_ZOOM_FACTOR
 
 
 class OptimizeSpectroscopy:
diff --git a/linien/server/optimization/utils.py b/linien-server/linien_server/optimization/utils.py
similarity index 100%
rename from linien/server/optimization/utils.py
rename to linien-server/linien_server/optimization/utils.py
diff --git a/linien/server/parameter_store.py b/linien-server/linien_server/parameter_store.py
similarity index 97%
rename from linien/server/parameter_store.py
rename to linien-server/linien_server/parameter_store.py
index 82a100ef..21012164 100644
--- a/linien/server/parameter_store.py
+++ b/linien-server/linien_server/parameter_store.py
@@ -20,7 +20,7 @@
 import pickle
 from time import time
 
-import linien
+import linien_server
 
 PARAMETER_STORE_FN = "/linien_parameters.pickle"
 
@@ -71,7 +71,7 @@ def save_parameters(self):
                     {
                         "parameters": parameters,
                         "time": time(),
-                        "version": linien.__version__,
+                        "version": linien_server.__version__,
                     },
                     f,
                 )
diff --git a/linien/server/parameters.py b/linien-server/linien_server/parameters.py
similarity index 98%
rename from linien/server/parameters.py
rename to linien-server/linien_server/parameters.py
index 445cc9a8..e6e99796 100644
--- a/linien/server/parameters.py
+++ b/linien-server/linien_server/parameters.py
@@ -16,15 +16,15 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
-from linien.common import (
+from linien_common.common import (
     AUTO_DETECT_AUTOLOCK_MODE,
     FAST_AUTOLOCK,
     PSD_ALGORITHM_LPSD,
     MHz,
     Vpp,
 )
-from linien.config import DEFAULT_COLORS, N_COLORS
-from linien.server.parameters_base import BaseParameters, Parameter
+from linien_common.config import DEFAULT_COLORS, N_COLORS
+from linien_server.parameters_base import BaseParameters, Parameter
 
 
 class Parameters(BaseParameters):
@@ -304,7 +304,7 @@ def __init__(self):
                     Parameter(start=False),
                 )
                 # Either `LOW_PASS_FILTER` or `HIGH_PASS_FILTER` from
-                # `linien.common` module.
+                # `linien_common.common` module.
                 setattr(
                     self, "filter_%d_type_%s" % (filter_i, channel), Parameter(start=0)
                 )
diff --git a/linien/server/parameters_base.py b/linien-server/linien_server/parameters_base.py
similarity index 99%
rename from linien/server/parameters_base.py
rename to linien-server/linien_server/parameters_base.py
index d3cd3172..a7bef3e8 100644
--- a/linien/server/parameters_base.py
+++ b/linien-server/linien_server/parameters_base.py
@@ -15,7 +15,7 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
-from linien.common import pack
+from linien_common.common import pack
 
 
 class Parameter:
diff --git a/linien/server/optimization/__init__.py b/linien-server/linien_server/pid_optimization/__init__.py
similarity index 100%
rename from linien/server/optimization/__init__.py
rename to linien-server/linien_server/pid_optimization/__init__.py
diff --git a/linien/server/pid_optimization/pid_optimization.py b/linien-server/linien_server/pid_optimization/pid_optimization.py
similarity index 98%
rename from linien/server/pid_optimization/pid_optimization.py
rename to linien-server/linien_server/pid_optimization/pid_optimization.py
index 1dfac29e..b30c4ba9 100644
--- a/linien/server/pid_optimization/pid_optimization.py
+++ b/linien-server/linien_server/pid_optimization/pid_optimization.py
@@ -22,12 +22,11 @@
 from time import sleep, time
 
 import numpy as np
+from linien_common.common import PSD_ALGORITHM_LPSD, PSD_ALGORITHM_WELCH
+from linien_server.optimization.engine import MultiDimensionalOptimizationEngine
 from pylpsd import lpsd
 from scipy import signal
 
-from linien.common import PSD_ALGORITHM_LPSD, PSD_ALGORITHM_WELCH
-from linien.server.optimization.engine import MultiDimensionalOptimizationEngine
-
 ALL_DECIMATIONS = list(range(32))
 
 
@@ -52,12 +51,10 @@ def calculate_psd(sig, fs, algorithm):
     sig = sig.astype(np.float64)
 
     if algorithm == PSD_ALGORITHM_WELCH:
-
         f, Pxx = signal.welch(
             sig, fs, window=window, nperseg=num_pts, scaling="density"
         )
     elif algorithm == PSD_ALGORITHM_LPSD:
-
         fmin = fs / len(sig) * 10  # lowest frequency of interest
         fmax = fs / 20.0  # highest frequency of interest
 
diff --git a/linien/server/registers.py b/linien-server/linien_server/registers.py
similarity index 98%
rename from linien/server/registers.py
rename to linien-server/linien_server/registers.py
index df0a5bfc..a6a09a3b 100644
--- a/linien/server/registers.py
+++ b/linien-server/linien_server/registers.py
@@ -17,19 +17,19 @@
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
 import numpy as np
-from csr import PitayaCSR
-from iir_coeffs import make_filter
-from utils import twos_complement
-
-from linien.common import (
+from linien_common.common import (
     ANALOG_OUT0,
     HIGH_PASS_FILTER,
     LOW_PASS_FILTER,
     MHz,
     convert_channel_mixing_value,
 )
-from linien.config import DEFAULT_SWEEP_SPEED
-from linien.server.acquisition import AcquisitionMaster
+from linien_common.config import DEFAULT_SWEEP_SPEED
+
+from .acquisition import AcquisitionMaster
+from .csr import PitayaCSR
+from .iir_coeffs import make_filter
+from .utils import twos_complement
 
 
 class Registers:
diff --git a/linien/server/server.py b/linien-server/linien_server/server.py
similarity index 94%
rename from linien/server/server.py
rename to linien-server/linien_server/server.py
index 1db59f45..c75723ad 100644
--- a/linien/server/server.py
+++ b/linien-server/linien_server/server.py
@@ -16,16 +16,10 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
-import os
-import sys
-
-# add the two parent directories to the path so linien and autolock can be found
-dir_name = os.path.dirname(os.path.abspath(__file__))
-sys.path += [os.path.join(dir_name, "..", "..")]
-sys.path += [os.path.join(dir_name, "..")]
-
 import _thread
+import os
 import pickle
+import sys
 import threading
 from random import random
 from time import sleep
@@ -33,19 +27,24 @@
 import click
 import numpy as np
 import rpyc
-from autolock.autolock import Autolock
-from parameters import Parameters
-from rpyc.utils.authenticators import AuthenticationError
-from rpyc.utils.server import ThreadedServer
-
-from linien.common import N_POINTS, check_plot_data, pack, unpack, update_signal_history
-from linien.config import DEFAULT_SERVER_PORT
-from linien.server.optimization.optimization import OptimizeSpectroscopy
-from linien.server.parameter_store import ParameterStore
-from linien.server.pid_optimization.pid_optimization import (
+from linien_common.common import (
+    N_POINTS,
+    check_plot_data,
+    pack,
+    unpack,
+    update_signal_history,
+)
+from linien_common.config import DEFAULT_SERVER_PORT
+from linien_server.autolock.autolock import Autolock
+from linien_server.optimization.optimization import OptimizeSpectroscopy
+from linien_server.parameter_store import ParameterStore
+from linien_server.parameters import Parameters
+from linien_server.pid_optimization.pid_optimization import (
     PIDOptimization,
     PSDAcquisition,
 )
+from rpyc.utils.authenticators import AuthenticationError
+from rpyc.utils.server import ThreadedServer
 
 
 class BaseService(rpyc.Service):
@@ -93,7 +92,7 @@ def __init__(self, **kwargs):
 
         super().__init__()
 
-        from registers import Registers
+        from linien_server.registers import Registers
 
         self.registers = Registers(**kwargs)
         self.registers.connect(self, self.parameters)
@@ -244,9 +243,9 @@ def exposed_shutdown(self):
         raise SystemExit
 
     def exposed_get_server_version(self):
-        import linien
+        import linien_server
 
-        return linien.__version__
+        return linien_server.__version__
 
     def exposed_get_restorable_parameters(self):
         return self.parameters._restorable_parameters
@@ -328,9 +327,9 @@ def exposed_get_restorable_parameters(self):
         return self.parameters._restorable_parameters
 
     def exposed_get_server_version(self):
-        import linien
+        import linien_server
 
-        return linien.__version__
+        return linien_server.__version__
 
     def pause_acquisition(self):
         pass
@@ -397,7 +396,6 @@ def username_and_password_authenticator(sock):
         if secret is None:
             print("warning: no authentication set up")
         else:
-
             if received != secret.encode():
                 print("received invalid credentials: ", received)
 
diff --git a/linien/server/utils.py b/linien-server/linien_server/utils.py
similarity index 92%
rename from linien/server/utils.py
rename to linien-server/linien_server/utils.py
index 6c503a72..38c20c5c 100644
--- a/linien/server/utils.py
+++ b/linien-server/linien_server/utils.py
@@ -18,8 +18,7 @@
 
 import shutil
 import subprocess
-
-from linien.config import REMOTE_BASE_PATH
+from pathlib import Path
 
 
 def stop_nginx():
@@ -32,7 +31,8 @@ def start_nginx():
 
 
 def flash_fpga():
-    shutil.copyfile("linien.bin", "/dev/xdevcfg")
+    filepath = Path(__file__).parent / "linien.bin"
+    shutil.copy(str(filepath.resolve()), "/dev/xdevcfg")
 
 
 def twos_complement(num, N_bits):
diff --git a/requirements_server.txt b/linien-server/requirements.txt
similarity index 82%
rename from requirements_server.txt
rename to linien-server/requirements.txt
index 03f86420..cf9d4003 100644
--- a/requirements_server.txt
+++ b/linien-server/requirements.txt
@@ -5,4 +5,5 @@ click==7.1.2
 plumbum==1.6.9
 pylpsd==0.1.4
 numpy==1.11.0
+setuptools_scm==7.0.5
 scipy==0.17.0
\ No newline at end of file
diff --git a/setup_server.py b/linien-server/setup.py
similarity index 70%
rename from setup_server.py
rename to linien-server/setup.py
index 87ad50b6..82305be2 100644
--- a/setup_server.py
+++ b/linien-server/setup.py
@@ -16,15 +16,15 @@
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
-import setuptools
+from setuptools import find_packages, setup
+from setuptools_scm import get_version
 
-import linien
+version = get_version(root="..", relative_to=__file__).split("+")[0]
 
-assert linien.__version__ != "dev"
-
-setuptools.setup(
+setup(
     name="linien-server",
-    version=linien.__version__,
+    use_scm_version={"root": "..", "relative_to": __file__},
+    setup_requires=["setuptools_scm"],
     author="Benjamin Wiegand",
     author_email="highwaychile@posteo.de",
     maintainer="Bastian Leykauf",
@@ -33,44 +33,33 @@
     long_description="Have a look at the 'linien' package for installation instructions.",  # noqa: E501
     long_description_content_type="text/x-rst",
     url="https://github.com/linien-org/linien",
-    packages=[
-        "linien",
-        "linien.server",
-        "linien.server.autolock",
-        "linien.server.optimization",
-        "linien.server.pid_optimization",
-    ],
+    packages=find_packages(),
     classifiers=[
         "Programming Language :: Python :: 3",
         "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
         "Operating System :: OS Independent",
     ],
+    entry_points={"console_scripts": ["linien-server=linien_server.server:run_server"]},
     install_requires=[
-        "rpyc>=4.0,<5.0",
-        "myhdl>=0.11",
         "click>=7.1.2",
         "cma>=3.0.3",
-        "plumbum>=1.6.9",
+        "myhdl>=0.11",
         "pylpsd>=0.1.4",
-        "numpy>=1.11.0",
-        "scipy>=0.17.0",
+        # "pyrp3>=1.1.0", # FIXME: Enable once pyrp3 is on PyPI
+        "rpyc>=4.0,<5.0",
+        "linien-common=={}".format(version),
     ],
     scripts=[
-        "linien/server/linien_start_server.sh",
-        "linien/server/linien_stop_server.sh",
-        "linien/server/linien_install_requirements.sh",
-        "linien/server/linien_start_ethernet_blinking.sh",
-        "linien/server/linien_stop_ethernet_blinking.sh",
+        "linien_server/linien_start_server.sh",
+        "linien_server/linien_stop_server.sh",
+        "linien_server/linien_install_requirements.sh",
     ],
     package_data={
         "": [
             "linien.bin",
             "linien_start_server.sh",
             "linien_stop_server.sh",
-            "linien_start_ethernet_blinking.sh",
-            "linien_stop_ethernet_blinking.sh",
             "linien_install_requirements.sh",
-            "VERSION",
         ]
     },
 )
diff --git a/linien/__init__.py b/linien/__init__.py
deleted file mode 100644
index 58363991..00000000
--- a/linien/__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-import os
-
-name = "linien"
-
-from plumbum import colors
-
-version_fn = os.path.join(
-    *list(os.path.split(os.path.abspath(__file__))[:-1]) + ["VERSION"]
-)
-
-try:
-    with open(version_fn, "r") as fh:
-        __version__ = fh.read().strip()
-        assert __version__ == "dev" or len(__version__.split(".")) == 3, (
-            "invalid version number! Either has to be dev or something like " "1.0.1"
-        )
-except FileNotFoundError:
-    print(
-        colors.red | "Unable to read VERSION file. Create a file called "
-        "VERSION in the checked_out_repo/linien folder. "
-        'The content should be either "dev" for development or a '
-        "version number if you want to release a new production version. "
-        "Consult README for more details."
-    )
-    raise
diff --git a/linien/client/utils.py b/linien/client/utils.py
deleted file mode 100644
index 5ea2e02b..00000000
--- a/linien/client/utils.py
+++ /dev/null
@@ -1,142 +0,0 @@
-# Copyright 2018-2022 Benjamin Wiegand <benjamin.wiegand@physik.hu-berlin.de>
-# Copyright 2021-2022 Bastian Leykauf <leykauf@physik.hu-berlin.de>
-#
-# This file is part of Linien and based on redpid.
-#
-# Linien is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Linien 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 General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Linien.  If not, see <http://www.gnu.org/licenses/>.
-
-import os
-
-import numpy as np
-import paramiko
-from plumbum import colors
-
-import linien
-from linien.client.exceptions import (
-    InvalidServerVersionException,
-    ServerNotInstalledException,
-)
-from linien.common import hash_username_and_password
-from linien.config import REMOTE_BASE_PATH
-
-
-def connect_ssh(host, user, password):
-    ssh = paramiko.SSHClient()
-    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-    ssh.connect(hostname=host, username=user, password=password)
-    return ssh
-
-
-def run_server(host, user, password, port):
-    ssh = connect_ssh(host, user, password)
-
-    version = linien.__version__
-
-    if version == "dev":
-        # if we are in a development version, we upload the files from source
-        # directory directly to the RP.
-        upload_source_code(ssh)
-
-        # start the development code that was just uploaded.
-        # For that, go to the parent directory; an import of "linien" then
-        # points to that directory instead of any globally installed release
-        # version.
-        stdin, stdout, stderr = ssh.exec_command(
-            ("cd %s/../;" % REMOTE_BASE_PATH)
-            + (
-                "export LINIEN_AUTH_HASH=%s;"
-                % hash_username_and_password(user, password)
-            )
-            + ("bash %s/server/linien_start_server.sh %d" % (REMOTE_BASE_PATH, port))
-        )
-        err = stderr.read()
-        if err:
-            print(colors.red | "Error starting the server")
-            print(err)
-    else:
-        # it is a release and not a dev version.
-
-        # read out version number of the server
-        remote_version = read_remote_version(ssh)
-        print("remote version", remote_version, "local version", version)
-
-        if version != remote_version:
-            raise InvalidServerVersionException(version, remote_version)
-
-        # start the server process using the global command
-        ssh.exec_command("linien_start_server.sh")
-        ssh.close()
-
-
-def read_remote_version(ssh):
-    """Reads the remote version of linien using SSH."""
-    stdin, stdout, stderr = ssh.exec_command(
-        'python3 -c "import linien; print(linien.__version__);"'
-    )
-    lines = stdout.readlines()
-
-    if not lines:
-        raise ServerNotInstalledException()
-
-    remote_version = lines[0].strip()
-    return remote_version
-
-
-def upload_source_code(ssh):
-    """Uploads the application's source code to the remote server using SFTP."""
-    print("uploading dev source code...")
-
-    ftp = ssh.open_sftp()
-
-    directory = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
-
-    if not os.path.exists(os.path.join(directory, "server", "linien.bin")):
-        print(
-            colors.red | "Error: In order to run the development version, "
-            "you need the FPGA bitstream in server/linien.bin! "
-            "Consult README to see how you can get one."
-        )
-        raise Exception("FPGA bitstream missing")
-
-    # upload the code required for running the server
-    for dirpath, dirnames, filenames in os.walk(directory):
-        # lstrip / so os.path.join does not think dir_path_rel is an absolute path.
-        dirpath_rel = dirpath.replace(directory, "").lstrip("/")
-        # Change direction of path slashes to work on the RedPitayas Linux system. This
-        # is necessary when deploying the server from a Windows machine.
-        dirpath_rel = dirpath_rel.lstrip("\\")
-        remote_path = os.path.join(REMOTE_BASE_PATH, dirpath_rel)
-
-        remote_path = remote_path.replace("\\", "/")
-
-        if "." in dirpath_rel or "__" in dirpath_rel:
-            continue
-
-        try:
-            ftp.lstat(remote_path)
-        except IOError:
-            ftp.mkdir(os.path.join(remote_path.rstrip("/")))
-
-        for filename in filenames:
-            local_path = os.path.join(dirpath, filename)
-            remote_filepath = os.path.join(remote_path, filename)
-            remote_filepath = remote_filepath.replace("\\", "/")
-            # put file
-            ftp.put(local_path, remote_filepath)
-
-    ftp.close()
-
-
-def peak_voltage_to_dBm(voltage):
-    return 10 + 20 * np.log10(voltage)
diff --git a/linien/gui/ui/device_manager.py b/linien/gui/ui/device_manager.py
deleted file mode 100644
index 45c8889c..00000000
--- a/linien/gui/ui/device_manager.py
+++ /dev/null
@@ -1,437 +0,0 @@
-# Copyright 2018-2022 Benjamin Wiegand <benjamin.wiegand@physik.hu-berlin.de>
-# Copyright 2021-2022 Bastian Leykauf <leykauf@physik.hu-berlin.de>
-#
-# This file is part of Linien and based on redpid.
-#
-# Linien is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Linien 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 General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Linien.  If not, see <http://www.gnu.org/licenses/>.
-
-from traceback import print_exc
-
-from paramiko.ssh_exception import AuthenticationException as SSHAuthenticationException
-from PyQt5 import QtCore, QtWidgets
-from PyQt5.QtCore import QThread, pyqtSignal
-
-import linien
-from linien.client.connection import LinienClient
-from linien.client.exceptions import (
-    GeneralConnectionErrorException,
-    InvalidServerVersionException,
-    RPYCAuthenticationException,
-    ServerNotInstalledException,
-)
-from linien.gui.config import (
-    get_saved_parameters,
-    load_device_data,
-    save_device_data,
-    save_parameter,
-)
-from linien.gui.dialogs import (
-    LoadingDialog,
-    ask_for_parameter_restore_dialog,
-    error_dialog,
-    execute_command_and_show_output,
-    question_dialog,
-)
-from linien.gui.ui.new_device_dialog import NewDeviceDialog
-from linien.gui.utils_gui import set_window_icon
-from linien.gui.widgets import CustomWidget
-
-
-class DeviceManager(QtWidgets.QMainWindow, CustomWidget):
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.load_ui("device_manager.ui")
-        self.setWindowTitle("Linien spectroscopy lock %s" % linien.__version__)
-        set_window_icon(self)
-
-    def ready(self):
-        self.load_device_data(autoload=True)
-
-    def keyPressEvent(self, event):
-        key = event.key()
-
-        if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
-            self.connect()
-
-    def load_device_data(self, autoload=False):
-        devices = load_device_data()
-        lst = self.ids.deviceList
-        lst.clear()
-
-        for device in devices:
-            lst.addItem("%s (%s)" % (device["name"], device["host"]))
-
-        if autoload and len(devices) == 1:
-            self.connect_to_device(devices[0])
-
-    def connect(self):
-        devices = load_device_data()
-
-        if not devices:
-            return
-
-        self.connect_to_device(devices[self.get_list_index()])
-
-    def connect_to_device(self, device):
-        loading_dialog = LoadingDialog(self, device["host"])
-        loading_dialog.show()
-
-        aborted = {}
-
-        def was_aborted(*args):
-            aborted["aborted"] = True
-
-        loading_dialog.aborted.connect(was_aborted)
-
-        self.connection_thread = ConnectionThread(device)
-
-        def client_connected(client):
-            loading_dialog.hide()
-            if not aborted:
-                self.app.client_connected(client)
-
-        self.connection_thread.client_connected.connect(client_connected)
-
-        def server_not_installed():
-            client_version = linien.__version__
-            loading_dialog.hide()
-            if not aborted:
-                display_question = (
-                    "The server is not yet installed on the device. "
-                    "Should it be installed? (Requires internet "
-                    "connection on RedPitaya)"
-                )
-                if question_dialog(self, display_question, "Install server?"):
-                    self.install_linien_server(
-                        device,
-                        version=client_version if client_version != "dev" else None,
-                    )
-
-        self.connection_thread.server_not_installed.connect(server_not_installed)
-
-        def invalid_server_version(remote_version, client_version):
-            loading_dialog.hide()
-            if not aborted:
-                if client_version != "dev":
-                    display_question = (
-                        "Server version (%s) does not match the client (%s) version."
-                        "Should the corresponding server version be installed?"
-                        % (remote_version, client_version)
-                    )
-                    if question_dialog(
-                        self, display_question, "Install corresponding version?"
-                    ):
-                        self.install_linien_server(device, version=client_version)
-                else:
-                    display_error = """
-                        A production version is installed on the RedPitaya, 
-                        but the client uses a development version. Stop the 
-                        server and uninstall the version on the RedPitaya using\n
-                        linien_stop_server.sh;\n
-                        pip3 uninstall linien-server\n
-                        before trying it again.
-                        """  # noqa: W291
-                    error_dialog(self, display_error)
-
-        self.connection_thread.invalid_server_version.connect(invalid_server_version)
-
-        def authentication_exception():
-            loading_dialog.hide()
-            if not aborted:
-                display_error = (
-                    "Error at authentication. "
-                    "Check username and password (by default both are 'root') "
-                    "and verify that you "
-                    "don't have any offending SSH keys in your known hosts file."
-                )
-                error_dialog(self, display_error)
-
-        self.connection_thread.authentication_exception.connect(
-            authentication_exception
-        )
-
-        def general_connection_error():
-            loading_dialog.hide()
-            if not aborted:
-                display_error = (
-                    "Unable to connect to device. If you are connecting by"
-                    " hostname (i.e. rp-xxxxxx.local), try using IP "
-                    "address instead."
-                )
-                error_dialog(self, display_error)
-
-        self.connection_thread.general_connection_error.connect(
-            general_connection_error
-        )
-
-        def exception():
-            loading_dialog.hide()
-            if not aborted:
-                display_error = "Exception occured when connecting to the device."
-                error_dialog(self, display_error)
-
-        self.connection_thread.exception.connect(exception)
-
-        def ask_for_parameter_restore():
-            question = (
-                "Linien on RedPitaya is running with different parameters than "
-                "the ones saved locally on this machine. Do you want to upload "
-                "the local parameters or keep the remote ones? Note that remote"
-                " parameters are only saved if Linien server was shut down "
-                "properly, not when unplugging the power plug. In this case, "
-                "you should update your local parameters."
-            )
-            should_restore = ask_for_parameter_restore_dialog(
-                self, question, "Restore parameters?"
-            )
-            self.connection_thread.answer_whether_to_restore_parameters(should_restore)
-
-        self.connection_thread.ask_for_parameter_restore.connect(
-            ask_for_parameter_restore
-        )
-
-        def connection_lost():
-            error_dialog(self, "Lost connection to the server!")
-            self.app.close()
-
-        self.connection_thread.connection_lost.connect(connection_lost)
-
-        self.connection_thread.start()
-
-    def install_linien_server(self, device, version=None):
-        version_string = ""
-        stop_server_command = ""
-
-        if version is not None:
-            version_string = "==" + version
-            # stop server if another version of linien is installed
-            stop_server_command = "linien_stop_server.sh;"
-
-        self.ssh_command = execute_command_and_show_output(
-            self,
-            device["host"],
-            device["username"],
-            device["password"],
-            (
-                "%s "
-                "pip3 install linien-server%s --no-cache-dir; "
-                "linien_install_requirements.sh; "
-            )
-            % (stop_server_command, version_string),
-            lambda: self.connect_to_device(device),
-        )
-
-    def new_device(self):
-        self.dialog = NewDeviceDialog()
-        self.dialog.setModal(True)
-        self.dialog.show()
-
-        def reload_device_data():
-            # not very elegant...
-            QtCore.QTimer.singleShot(100, self.load_device_data)
-
-        self.dialog.accepted.connect(reload_device_data)
-
-    def edit_device(self):
-        devices = load_device_data()
-
-        if not devices:
-            return
-
-        device = devices[self.get_list_index()]
-
-        self.dialog = NewDeviceDialog(device)
-        self.dialog.setModal(True)
-        self.dialog.show()
-
-        def reload_device_data():
-            # not very elegant...
-            QtCore.QTimer.singleShot(100, self.load_device_data)
-
-        self.dialog.accepted.connect(reload_device_data)
-
-    def move_device_up(self):
-        self.move_device(-1)
-
-    def move_device_down(self):
-        self.move_device(1)
-
-    def move_device(self, direction):
-        devices = load_device_data()
-
-        if not devices:
-            return
-
-        current_index = self.get_list_index()
-        new_index = current_index + direction
-
-        if new_index < 0 or new_index > len(devices) - 1:
-            return
-
-        device = devices.pop(current_index)
-        devices = devices[:new_index] + [device] + devices[new_index:]
-        save_device_data(devices)
-        self.load_device_data()
-        self.ids.deviceList.setCurrentRow(new_index)
-
-    def get_list_index(self):
-        return self.ids.deviceList.currentIndex().row()
-
-    def remove_device(self):
-        devices = load_device_data()
-
-        if not devices:
-            return
-
-        devices.pop(self.get_list_index())
-        save_device_data(devices)
-        self.load_device_data()
-
-    def selected_device_changed(self):
-        idx = self.get_list_index()
-
-        disable_buttons = True
-
-        if idx >= 0:
-            devices = load_device_data()
-
-            if devices:
-                disable_buttons = False
-
-        for btn in [
-            self.ids.connectButton,
-            self.ids.removeButton,
-            self.ids.editButton,
-            self.ids.moveUpButton,
-            self.ids.moveDownButton,
-        ]:
-            btn.setEnabled(not disable_buttons)
-
-
-class ConnectionThread(QThread):
-    client_connected = pyqtSignal(object)
-    server_not_installed = pyqtSignal()
-    invalid_server_version = pyqtSignal(str, str)
-    authentication_exception = pyqtSignal()
-    general_connection_error = pyqtSignal()
-    exception = pyqtSignal()
-    connection_lost = pyqtSignal()
-    ask_for_parameter_restore = pyqtSignal()
-
-    def __init__(self, device):
-        super().__init__()
-
-        self.device = device
-
-    def run(self):
-        try:
-            client = LinienClient(
-                self.device,
-                autostart_server=True,
-                use_parameter_cache=True,
-                on_connection_lost=self.on_connection_lost,
-            )
-            self.client_connected.emit(client)
-
-            self.client = client
-
-        except ServerNotInstalledException:
-            return self.server_not_installed.emit()
-
-        except InvalidServerVersionException as e:
-            return self.invalid_server_version.emit(e.remote_version, e.client_version)
-
-        except (SSHAuthenticationException, RPYCAuthenticationException):
-            return self.authentication_exception.emit()
-
-        except GeneralConnectionErrorException:
-            return self.general_connection_error.emit()
-
-        except Exception:
-            print_exc()
-            return self.exception.emit()
-
-        # now, we are connected to the server. Check whether we have cached settings
-        # for this server. If yes, check whether they match with what is currently
-        # running. If there is a mismatch, ask the user whether the settings should
-        # be restored.
-
-        parameters_differ = self.restore_parameters(dry_run=True)
-        if parameters_differ:
-            self.ask_for_parameter_restore.emit()
-        else:
-            # if parameters don't differ, we can start monitoring remote parameter
-            # changes and write them to disk. We don't do this if parameters
-            # differ because we don't want to override our local settings with
-            # the remote one --> we wait until user has answered whether local
-            # parameters or remote ones should be used.
-            self.continuously_write_parameters_to_disk()
-
-    def on_connection_lost(self):
-        self.connection_lost.emit()
-
-    def answer_whether_to_restore_parameters(self, should_restore):
-        if should_restore:
-            self.restore_parameters(dry_run=False)
-
-        self.continuously_write_parameters_to_disk()
-
-    def restore_parameters(self, dry_run=False):
-        """Reads settings for a server that were cached locally. Sends them to
-        the server. If `dry_run` is...
-
-            * `True`, this function returns a boolean indicating whether the
-              local parameters differ from the ones on the server
-            * `False`, the local parameters are uploaded to the server
-        """
-        device_key = self.device["key"]
-        params = get_saved_parameters(device_key)
-        print("restoring parameters")
-
-        differences = False
-
-        for k, v in params.items():
-            if hasattr(self.client.parameters, k):
-                param = getattr(self.client.parameters, k)
-                if param.value != v:
-                    if dry_run:
-                        print("parameter", k, "differs")
-                        differences = True
-                        break
-                    else:
-                        param.value = v
-            else:
-                # this may happen if the settings were written with a different
-                # version of linien.
-                print("unable to restore parameter %s. Delete the cached value." % k)
-                save_parameter(device_key, k, None, delete=True)
-
-        if not dry_run:
-            self.client.control.write_registers()
-
-        return differences
-
-    def continuously_write_parameters_to_disk(self):
-        """Listens for changes of some parameters and permanently saves their
-        values on the client's disk. This data can be used to restore the status
-        later, if the client tries to connect to the server but it doesn't run
-        anymore."""
-        params = self.client.parameters.remote.exposed_get_restorable_parameters()
-
-        for param in params:
-
-            def on_change(value, param=param):
-                save_parameter(self.device["key"], param, value)
-
-            getattr(self.client.parameters, param).on_change(on_change)
diff --git a/linien/server/csrmap.py b/linien/server/csrmap.py
deleted file mode 100644
index bc45cea1..00000000
--- a/linien/server/csrmap.py
+++ /dev/null
@@ -1,317 +0,0 @@
-csr_constants = {
-    'fast_a_iir_c_1_shift': 23,
-    'fast_a_iir_c_1_width': 25,
-    'fast_a_iir_c_1_interval': 1,
-    'fast_a_iir_c_1_latency': 2,
-    'fast_a_iir_c_1_order': 1,
-    'fast_a_iir_c_1_iterative': 0,
-    'fast_a_iir_d_1_shift': 23,
-    'fast_a_iir_d_1_width': 25,
-    'fast_a_iir_d_1_interval': 1,
-    'fast_a_iir_d_1_latency': 3,
-    'fast_a_iir_d_1_order': 2,
-    'fast_a_iir_d_1_iterative': 0,
-    'fast_a_iir_c_2_shift': 23,
-    'fast_a_iir_c_2_width': 25,
-    'fast_a_iir_c_2_interval': 1,
-    'fast_a_iir_c_2_latency': 2,
-    'fast_a_iir_c_2_order': 1,
-    'fast_a_iir_c_2_iterative': 0,
-    'fast_a_iir_d_2_shift': 23,
-    'fast_a_iir_d_2_width': 25,
-    'fast_a_iir_d_2_interval': 1,
-    'fast_a_iir_d_2_latency': 3,
-    'fast_a_iir_d_2_order': 2,
-    'fast_a_iir_d_2_iterative': 0,
-    'fast_b_iir_c_1_shift': 23,
-    'fast_b_iir_c_1_width': 25,
-    'fast_b_iir_c_1_interval': 1,
-    'fast_b_iir_c_1_latency': 2,
-    'fast_b_iir_c_1_order': 1,
-    'fast_b_iir_c_1_iterative': 0,
-    'fast_b_iir_d_1_shift': 23,
-    'fast_b_iir_d_1_width': 25,
-    'fast_b_iir_d_1_interval': 1,
-    'fast_b_iir_d_1_latency': 3,
-    'fast_b_iir_d_1_order': 2,
-    'fast_b_iir_d_1_iterative': 0,
-    'fast_b_iir_c_2_shift': 23,
-    'fast_b_iir_c_2_width': 25,
-    'fast_b_iir_c_2_interval': 1,
-    'fast_b_iir_c_2_latency': 2,
-    'fast_b_iir_c_2_order': 1,
-    'fast_b_iir_c_2_iterative': 0,
-    'fast_b_iir_d_2_shift': 23,
-    'fast_b_iir_d_2_width': 25,
-    'fast_b_iir_d_2_interval': 1,
-    'fast_b_iir_d_2_latency': 3,
-    'fast_b_iir_d_2_order': 2,
-    'fast_b_iir_d_2_iterative': 0,
-    'logic_raw_acquisition_iir_shift': 23,
-    'logic_raw_acquisition_iir_width': 25,
-    'logic_raw_acquisition_iir_interval': 1,
-    'logic_raw_acquisition_iir_latency': 6,
-    'logic_raw_acquisition_iir_order': 5,
-    'logic_raw_acquisition_iir_iterative': 0,
-}
-
-csr = {
-    'dna_dna': (28, 0x000, 64, False),
-    'fast_a_y_tap': (0, 0x000, 2, True),
-    'fast_a_invert': (0, 0x001, 1, True),
-    'fast_a_demod_delay': (0, 0x002, 32, True),
-    'fast_a_demod_multiplier': (0, 0x006, 4, True),
-    'fast_a_x_limit_1_min': (0, 0x007, 25, True),
-    'fast_a_x_limit_1_max': (0, 0x00b, 25, True),
-    'fast_a_iir_c_1_z0': (0, 0x00f, 27, True),
-    'fast_a_iir_c_1_a1': (0, 0x013, 25, True),
-    'fast_a_iir_c_1_b0': (0, 0x017, 25, True),
-    'fast_a_iir_c_1_b1': (0, 0x01b, 25, True),
-    'fast_a_iir_d_1_z0': (0, 0x01f, 27, True),
-    'fast_a_iir_d_1_a1': (0, 0x023, 25, True),
-    'fast_a_iir_d_1_a2': (0, 0x027, 25, True),
-    'fast_a_iir_d_1_b0': (0, 0x02b, 25, True),
-    'fast_a_iir_d_1_b1': (0, 0x02f, 25, True),
-    'fast_a_iir_d_1_b2': (0, 0x033, 25, True),
-    'fast_a_y_limit_1_min': (0, 0x037, 25, True),
-    'fast_a_y_limit_1_max': (0, 0x03b, 25, True),
-    'fast_a_x_limit_2_min': (0, 0x03f, 25, True),
-    'fast_a_x_limit_2_max': (0, 0x043, 25, True),
-    'fast_a_iir_c_2_z0': (0, 0x047, 27, True),
-    'fast_a_iir_c_2_a1': (0, 0x04b, 25, True),
-    'fast_a_iir_c_2_b0': (0, 0x04f, 25, True),
-    'fast_a_iir_c_2_b1': (0, 0x053, 25, True),
-    'fast_a_iir_d_2_z0': (0, 0x057, 27, True),
-    'fast_a_iir_d_2_a1': (0, 0x05b, 25, True),
-    'fast_a_iir_d_2_a2': (0, 0x05f, 25, True),
-    'fast_a_iir_d_2_b0': (0, 0x063, 25, True),
-    'fast_a_iir_d_2_b1': (0, 0x067, 25, True),
-    'fast_a_iir_d_2_b2': (0, 0x06b, 25, True),
-    'fast_a_y_limit_2_min': (0, 0x06f, 25, True),
-    'fast_a_y_limit_2_max': (0, 0x073, 25, True),
-    'fast_a_x_clr': (0, 0x077, 1, True),
-    'fast_a_x_max': (0, 0x078, 25, False),
-    'fast_a_x_min': (0, 0x07c, 25, False),
-    'fast_a_out_i_clr': (0, 0x080, 1, True),
-    'fast_a_out_i_max': (0, 0x081, 25, False),
-    'fast_a_out_i_min': (0, 0x085, 25, False),
-    'fast_a_out_q_clr': (0, 0x089, 1, True),
-    'fast_a_out_q_max': (0, 0x08a, 25, False),
-    'fast_a_out_q_min': (0, 0x08e, 25, False),
-    'fast_a_dx_sel': (0, 0x092, 4, True),
-    'fast_a_dy_sel': (0, 0x093, 4, True),
-    'fast_b_y_tap': (1, 0x000, 2, True),
-    'fast_b_invert': (1, 0x001, 1, True),
-    'fast_b_demod_delay': (1, 0x002, 32, True),
-    'fast_b_demod_multiplier': (1, 0x006, 4, True),
-    'fast_b_x_limit_1_min': (1, 0x007, 25, True),
-    'fast_b_x_limit_1_max': (1, 0x00b, 25, True),
-    'fast_b_iir_c_1_z0': (1, 0x00f, 27, True),
-    'fast_b_iir_c_1_a1': (1, 0x013, 25, True),
-    'fast_b_iir_c_1_b0': (1, 0x017, 25, True),
-    'fast_b_iir_c_1_b1': (1, 0x01b, 25, True),
-    'fast_b_iir_d_1_z0': (1, 0x01f, 27, True),
-    'fast_b_iir_d_1_a1': (1, 0x023, 25, True),
-    'fast_b_iir_d_1_a2': (1, 0x027, 25, True),
-    'fast_b_iir_d_1_b0': (1, 0x02b, 25, True),
-    'fast_b_iir_d_1_b1': (1, 0x02f, 25, True),
-    'fast_b_iir_d_1_b2': (1, 0x033, 25, True),
-    'fast_b_y_limit_1_min': (1, 0x037, 25, True),
-    'fast_b_y_limit_1_max': (1, 0x03b, 25, True),
-    'fast_b_x_limit_2_min': (1, 0x03f, 25, True),
-    'fast_b_x_limit_2_max': (1, 0x043, 25, True),
-    'fast_b_iir_c_2_z0': (1, 0x047, 27, True),
-    'fast_b_iir_c_2_a1': (1, 0x04b, 25, True),
-    'fast_b_iir_c_2_b0': (1, 0x04f, 25, True),
-    'fast_b_iir_c_2_b1': (1, 0x053, 25, True),
-    'fast_b_iir_d_2_z0': (1, 0x057, 27, True),
-    'fast_b_iir_d_2_a1': (1, 0x05b, 25, True),
-    'fast_b_iir_d_2_a2': (1, 0x05f, 25, True),
-    'fast_b_iir_d_2_b0': (1, 0x063, 25, True),
-    'fast_b_iir_d_2_b1': (1, 0x067, 25, True),
-    'fast_b_iir_d_2_b2': (1, 0x06b, 25, True),
-    'fast_b_y_limit_2_min': (1, 0x06f, 25, True),
-    'fast_b_y_limit_2_max': (1, 0x073, 25, True),
-    'fast_b_x_clr': (1, 0x077, 1, True),
-    'fast_b_x_max': (1, 0x078, 25, False),
-    'fast_b_x_min': (1, 0x07c, 25, False),
-    'fast_b_out_i_clr': (1, 0x080, 1, True),
-    'fast_b_out_i_max': (1, 0x081, 25, False),
-    'fast_b_out_i_min': (1, 0x085, 25, False),
-    'fast_b_out_q_clr': (1, 0x089, 1, True),
-    'fast_b_out_q_max': (1, 0x08a, 25, False),
-    'fast_b_out_q_min': (1, 0x08e, 25, False),
-    'fast_b_dx_sel': (1, 0x092, 4, True),
-    'fast_b_dy_sel': (1, 0x093, 4, True),
-    'gpio_n_ins': (30, 0x000, 8, False),
-    'gpio_n_outs': (30, 0x001, 8, True),
-    'gpio_n_oes': (30, 0x002, 8, True),
-    'gpio_n_state': (30, 0x003, 14, False),
-    'gpio_n_state_clr': (30, 0x005, 1, True),
-    'gpio_n_do0_en': (30, 0x006, 14, True),
-    'gpio_n_do1_en': (30, 0x008, 14, True),
-    'gpio_n_do2_en': (30, 0x00a, 14, True),
-    'gpio_n_do3_en': (30, 0x00c, 14, True),
-    'gpio_n_do4_en': (30, 0x00e, 14, True),
-    'gpio_n_do5_en': (30, 0x010, 14, True),
-    'gpio_n_do6_en': (30, 0x012, 14, True),
-    'gpio_n_do7_en': (30, 0x014, 14, True),
-    'gpio_p_ins': (31, 0x000, 8, False),
-    'gpio_p_outs': (31, 0x001, 8, True),
-    'gpio_p_oes': (31, 0x002, 8, True),
-    'logic_dual_channel': (8, 0x000, 1, True),
-    'logic_chain_a_factor': (8, 0x001, 9, True),
-    'logic_chain_b_factor': (8, 0x003, 9, True),
-    'logic_chain_a_offset': (8, 0x005, 14, True),
-    'logic_chain_b_offset': (8, 0x007, 14, True),
-    'logic_combined_offset': (8, 0x009, 14, True),
-    'logic_out_offset': (8, 0x00b, 14, True),
-    'logic_mod_channel': (8, 0x00d, 1, True),
-    'logic_control_channel': (8, 0x00e, 1, True),
-    'logic_sweep_channel': (8, 0x00f, 2, True),
-    'logic_fast_mode': (8, 0x010, 1, True),
-    'logic_slow_value': (8, 0x011, 14, False),
-    'logic_slow_decimation': (8, 0x013, 5, True),
-    'logic_analog_out_1': (8, 0x014, 15, True),
-    'logic_analog_out_2': (8, 0x016, 15, True),
-    'logic_analog_out_3': (8, 0x018, 15, True),
-    'logic_mod_amp': (8, 0x01a, 14, True),
-    'logic_mod_freq': (8, 0x01c, 32, True),
-    'logic_sweep_step': (8, 0x020, 30, True),
-    'logic_sweep_min': (8, 0x024, 14, True),
-    'logic_sweep_max': (8, 0x026, 14, True),
-    'logic_sweep_run': (8, 0x028, 1, True),
-    'logic_sweep_pause': (8, 0x029, 1, True),
-    'logic_limit_error_signal_min': (8, 0x02a, 25, True),
-    'logic_limit_error_signal_max': (8, 0x02e, 25, True),
-    'logic_limit_fast1_min': (8, 0x032, 14, True),
-    'logic_limit_fast1_max': (8, 0x034, 14, True),
-    'logic_limit_fast2_min': (8, 0x036, 14, True),
-    'logic_limit_fast2_max': (8, 0x038, 14, True),
-    'logic_pid_setpoint': (8, 0x03a, 25, True),
-    'logic_pid_kp': (8, 0x03e, 14, True),
-    'logic_pid_ki': (8, 0x040, 14, True),
-    'logic_pid_reset': (8, 0x042, 1, True),
-    'logic_pid_kd': (8, 0x043, 14, True),
-    'logic_autolock_robust_time_scale': (8, 0x045, 14, True),
-    'logic_autolock_robust_N_instructions': (8, 0x047, 5, True),
-    'logic_autolock_robust_final_wait_time': (8, 0x048, 14, True),
-    'logic_autolock_robust_peak_height_0': (8, 0x04a, 28, True),
-    'logic_autolock_robust_peak_height_1': (8, 0x04e, 28, True),
-    'logic_autolock_robust_peak_height_2': (8, 0x052, 28, True),
-    'logic_autolock_robust_peak_height_3': (8, 0x056, 28, True),
-    'logic_autolock_robust_peak_height_4': (8, 0x05a, 28, True),
-    'logic_autolock_robust_peak_height_5': (8, 0x05e, 28, True),
-    'logic_autolock_robust_peak_height_6': (8, 0x062, 28, True),
-    'logic_autolock_robust_peak_height_7': (8, 0x066, 28, True),
-    'logic_autolock_robust_peak_height_8': (8, 0x06a, 28, True),
-    'logic_autolock_robust_peak_height_9': (8, 0x06e, 28, True),
-    'logic_autolock_robust_peak_height_10': (8, 0x072, 28, True),
-    'logic_autolock_robust_peak_height_11': (8, 0x076, 28, True),
-    'logic_autolock_robust_peak_height_12': (8, 0x07a, 28, True),
-    'logic_autolock_robust_peak_height_13': (8, 0x07e, 28, True),
-    'logic_autolock_robust_peak_height_14': (8, 0x082, 28, True),
-    'logic_autolock_robust_peak_height_15': (8, 0x086, 28, True),
-    'logic_autolock_robust_peak_height_16': (8, 0x08a, 28, True),
-    'logic_autolock_robust_peak_height_17': (8, 0x08e, 28, True),
-    'logic_autolock_robust_peak_height_18': (8, 0x092, 28, True),
-    'logic_autolock_robust_peak_height_19': (8, 0x096, 28, True),
-    'logic_autolock_robust_peak_height_20': (8, 0x09a, 28, True),
-    'logic_autolock_robust_peak_height_21': (8, 0x09e, 28, True),
-    'logic_autolock_robust_peak_height_22': (8, 0x0a2, 28, True),
-    'logic_autolock_robust_peak_height_23': (8, 0x0a6, 28, True),
-    'logic_autolock_robust_peak_height_24': (8, 0x0aa, 28, True),
-    'logic_autolock_robust_peak_height_25': (8, 0x0ae, 28, True),
-    'logic_autolock_robust_peak_height_26': (8, 0x0b2, 28, True),
-    'logic_autolock_robust_peak_height_27': (8, 0x0b6, 28, True),
-    'logic_autolock_robust_peak_height_28': (8, 0x0ba, 28, True),
-    'logic_autolock_robust_peak_height_29': (8, 0x0be, 28, True),
-    'logic_autolock_robust_peak_height_30': (8, 0x0c2, 28, True),
-    'logic_autolock_robust_peak_height_31': (8, 0x0c6, 28, True),
-    'logic_autolock_robust_wait_for_0': (8, 0x0ca, 14, True),
-    'logic_autolock_robust_wait_for_1': (8, 0x0cc, 14, True),
-    'logic_autolock_robust_wait_for_2': (8, 0x0ce, 14, True),
-    'logic_autolock_robust_wait_for_3': (8, 0x0d0, 14, True),
-    'logic_autolock_robust_wait_for_4': (8, 0x0d2, 14, True),
-    'logic_autolock_robust_wait_for_5': (8, 0x0d4, 14, True),
-    'logic_autolock_robust_wait_for_6': (8, 0x0d6, 14, True),
-    'logic_autolock_robust_wait_for_7': (8, 0x0d8, 14, True),
-    'logic_autolock_robust_wait_for_8': (8, 0x0da, 14, True),
-    'logic_autolock_robust_wait_for_9': (8, 0x0dc, 14, True),
-    'logic_autolock_robust_wait_for_10': (8, 0x0de, 14, True),
-    'logic_autolock_robust_wait_for_11': (8, 0x0e0, 14, True),
-    'logic_autolock_robust_wait_for_12': (8, 0x0e2, 14, True),
-    'logic_autolock_robust_wait_for_13': (8, 0x0e4, 14, True),
-    'logic_autolock_robust_wait_for_14': (8, 0x0e6, 14, True),
-    'logic_autolock_robust_wait_for_15': (8, 0x0e8, 14, True),
-    'logic_autolock_robust_wait_for_16': (8, 0x0ea, 14, True),
-    'logic_autolock_robust_wait_for_17': (8, 0x0ec, 14, True),
-    'logic_autolock_robust_wait_for_18': (8, 0x0ee, 14, True),
-    'logic_autolock_robust_wait_for_19': (8, 0x0f0, 14, True),
-    'logic_autolock_robust_wait_for_20': (8, 0x0f2, 14, True),
-    'logic_autolock_robust_wait_for_21': (8, 0x0f4, 14, True),
-    'logic_autolock_robust_wait_for_22': (8, 0x0f6, 14, True),
-    'logic_autolock_robust_wait_for_23': (8, 0x0f8, 14, True),
-    'logic_autolock_robust_wait_for_24': (8, 0x0fa, 14, True),
-    'logic_autolock_robust_wait_for_25': (8, 0x0fc, 14, True),
-    'logic_autolock_robust_wait_for_26': (8, 0x0fe, 14, True),
-    'logic_autolock_robust_wait_for_27': (8, 0x100, 14, True),
-    'logic_autolock_robust_wait_for_28': (8, 0x102, 14, True),
-    'logic_autolock_robust_wait_for_29': (8, 0x104, 14, True),
-    'logic_autolock_robust_wait_for_30': (8, 0x106, 14, True),
-    'logic_autolock_robust_wait_for_31': (8, 0x108, 14, True),
-    'logic_autolock_fast_target_position': (8, 0x10a, 14, True),
-    'logic_autolock_request_lock': (8, 0x10c, 1, True),
-    'logic_autolock_autolock_mode': (8, 0x10d, 2, True),
-    'logic_autolock_lock_running': (8, 0x10e, 1, False),
-    'logic_raw_acquisition_iir_z0': (8, 0x10f, 27, True),
-    'logic_raw_acquisition_iir_a1': (8, 0x113, 25, True),
-    'logic_raw_acquisition_iir_a2': (8, 0x117, 25, True),
-    'logic_raw_acquisition_iir_a3': (8, 0x11b, 25, True),
-    'logic_raw_acquisition_iir_a4': (8, 0x11f, 25, True),
-    'logic_raw_acquisition_iir_a5': (8, 0x123, 25, True),
-    'logic_raw_acquisition_iir_b0': (8, 0x127, 25, True),
-    'logic_raw_acquisition_iir_b1': (8, 0x12b, 25, True),
-    'logic_raw_acquisition_iir_b2': (8, 0x12f, 25, True),
-    'logic_raw_acquisition_iir_b3': (8, 0x133, 25, True),
-    'logic_raw_acquisition_iir_b4': (8, 0x137, 25, True),
-    'logic_raw_acquisition_iir_b5': (8, 0x13b, 25, True),
-    'logic_control_signal_clr': (8, 0x13f, 1, True),
-    'logic_control_signal_max': (8, 0x140, 25, False),
-    'logic_control_signal_min': (8, 0x144, 25, False),
-    'logic_combined_error_signal_clr': (8, 0x148, 1, True),
-    'logic_combined_error_signal_max': (8, 0x149, 25, False),
-    'logic_combined_error_signal_min': (8, 0x14d, 25, False),
-    'logic_combined_error_signal_filtered_clr': (8, 0x151, 1, True),
-    'logic_combined_error_signal_filtered_max': (8, 0x152, 25, False),
-    'logic_combined_error_signal_filtered_min': (8, 0x156, 25, False),
-    'scopegen_external_trigger': (6, 0x000, 1, True),
-    'scopegen_dac_a_clr': (6, 0x001, 1, True),
-    'scopegen_dac_a_max': (6, 0x002, 25, False),
-    'scopegen_dac_a_min': (6, 0x006, 25, False),
-    'scopegen_dac_b_clr': (6, 0x00a, 1, True),
-    'scopegen_dac_b_max': (6, 0x00b, 25, False),
-    'scopegen_dac_b_min': (6, 0x00f, 25, False),
-    'scopegen_adc_a_sel': (6, 0x013, 4, True),
-    'scopegen_adc_b_sel': (6, 0x014, 4, True),
-    'scopegen_adc_a_q_sel': (6, 0x015, 4, True),
-    'scopegen_adc_b_q_sel': (6, 0x016, 4, True),
-    'slow_pid_setpoint': (2, 0x000, 14, True),
-    'slow_pid_kp': (2, 0x002, 14, True),
-    'slow_pid_ki': (2, 0x004, 14, True),
-    'slow_pid_reset': (2, 0x006, 1, True),
-    'slow_pid_kd': (2, 0x007, 14, True),
-    'slow_limit_min': (2, 0x009, 14, True),
-    'slow_limit_max': (2, 0x00b, 14, True),
-    'slow_out_clr': (2, 0x00d, 1, True),
-    'slow_out_max': (2, 0x00e, 25, False),
-    'slow_out_min': (2, 0x012, 25, False),
-    'xadc_temp': (29, 0x000, 12, False),
-    'xadc_v': (29, 0x002, 12, False),
-    'xadc_a': (29, 0x004, 12, False),
-    'xadc_b': (29, 0x006, 12, False),
-    'xadc_c': (29, 0x008, 12, False),
-    'xadc_d': (29, 0x00a, 12, False),
-}
-states = ['force', 'di0', 'di1', 'di2', 'di3', 'di4', 'di5', 'di6', 'di7', 'robust_watching', 'robust_turn_on_lock', 'robust_sign_equal', 'robust_over_threshold', 'robust_waited_long_enough']
-signals = ['zero', 'fast_a_x', 'fast_a_out_i', 'fast_a_out_q', 'fast_b_x', 'fast_b_out_i', 'fast_b_out_q', 'slow_out', 'scopegen_dac_a', 'scopegen_dac_b', 'logic_control_signal', 'logic_combined_error_signal', 'logic_combined_error_signal_filtered']
diff --git a/linien/server/linien_start_ethernet_blinking.sh b/linien/server/linien_start_ethernet_blinking.sh
deleted file mode 100644
index cbac5a6b..00000000
--- a/linien/server/linien_start_ethernet_blinking.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-
-# start ethernet blinking again, cf. https://github.com/RedPitaya/RedPitaya/issues/205
-mdio-tool w eth0 0x1b 0x0f00
diff --git a/linien/server/linien_start_server.sh b/linien/server/linien_start_server.sh
deleted file mode 100644
index 04e0c2a6..00000000
--- a/linien/server/linien_start_server.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/bash
-
-# now, we have to find out which folder linien is installed to.
-# We do this dynamically in order to be able to detect installations done
-# with pip and those that are copied to /linien (in development mode).
-# In dev mode, this script is started in the parent folder of the code,
-# therefore, the dev code is preferred over any global installation.
-pth=`python3 -c 'import linien; print(linien.__path__[0]);'`
-
-# quit any remaining screen session
-if [ -x "$(command -v linien_stop_server.sh)" ]; then
-    linien_stop_server
-fi
-screen -X -S linien-server quit
-# start a new one
-screen -S linien-server -d -m
-
-# start the server inside the screen session
-screen -r linien-server -X stuff $"cd $pth/server;\n"
-
-screen -r linien-server -X stuff $"bash linien_stop_ethernet_blinking.sh; python3 server.py $1; bash linien_start_ethernet_blinking.sh; \n"
\ No newline at end of file
diff --git a/linien/server/linien_stop_ethernet_blinking.sh b/linien/server/linien_stop_ethernet_blinking.sh
deleted file mode 100644
index 70983073..00000000
--- a/linien/server/linien_stop_ethernet_blinking.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-
-# stop ethernet blinking, cf. https://github.com/RedPitaya/RedPitaya/issues/205
-mdio-tool w eth0 0x1b 0x0000
\ No newline at end of file
diff --git a/linien/server/linien_stop_server.sh b/linien/server/linien_stop_server.sh
deleted file mode 100644
index a4c4ade9..00000000
--- a/linien/server/linien_stop_server.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-
-screen -X -S linien-server quit
-bash linien_start_ethernet_blinking.sh
\ No newline at end of file
diff --git a/linien/server/pid_optimization/__init__.py b/linien/server/pid_optimization/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/pyinstaller.spec b/pyinstaller.spec
deleted file mode 100644
index 862a5266..00000000
--- a/pyinstaller.spec
+++ /dev/null
@@ -1,65 +0,0 @@
-# -*- mode: python -*-
-# this spec file is for compiling a standalone version of linien using pyinstaller.
-# use `scripts/build_standalone_client.sh` for this purpose
-
-block_cipher = None
-
-import platform
-
-datas= [
-    ('linien/VERSION', 'linien'),
-    ('linien/gui/ui/*', 'linien/gui/ui'),
-    ('linien/gui/icon.ico', 'linien/gui')
-]
-
-pathex = []
-
-if platform.system().lower() != 'linux':
-    import os
-
-    # IMPORTANT: For some reason the app doesn't work if pyqt is not installed globally
-    # using powershell run with admin privileges.
-    """qt_site_packages = 'C:\\Program Files\\Python38\\lib\\site-packages'
-    qt_bin_folder = qt_site_packages + '\\PyQt5\\Qt\\bin'
-    if not os.path.exists(qt_bin_folder):
-        print('')
-        print('============================================')
-        print('pyinstaller.spec was customized due to a pyinstaller bug that lead to Qt5Core.dll not being bundled.')
-        print('see also https://github.com/pyinstaller/pyinstaller/issues/2152')
-        print('therefore, qt_bin_folder is set to')
-        print(qt_bin_folder)
-        print('However, on your machine this folder does not exist. You probably have to modify the spec file.')
-        input('Proceed anyway? [press enter]')
-
-    datas += [(qt_bin_folder + '\\Qt5Core.dll', 'PyQt5\\Qt\\bin')]
-    pathex.append(qt_site_packages)"""
-
-
-a = Analysis(['linien/gui/app.py'],
-             pathex=pathex,
-             binaries=[],
-             datas=datas,
-             hiddenimports=['linien', 'linien.common', 'PyQt5.sip', 'superqt'],
-             hookspath=[],
-             runtime_hooks=[],
-             excludes=[],
-             win_no_prefer_redirects=False,
-             win_private_assemblies=False,
-             cipher=block_cipher,
-             noarchive=False)
-pyz = PYZ(a.pure, a.zipped_data,
-             cipher=block_cipher)
-exe = EXE(pyz,
-          a.scripts,
-          a.binaries,
-          a.zipfiles,
-          a.datas,
-          [],
-          name='linien-client',
-          debug=False,
-          bootloader_ignore_signals=False,
-          strip=False,
-          upx=True,
-          runtime_tmpdir=None,
-          console=False,
-          icon="linien/gui/icon.ico")
diff --git a/requirements_client.txt b/requirements_client.txt
deleted file mode 100644
index 19fa8e55..00000000
--- a/requirements_client.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-numpy==1.21.0
-paramiko==2.10.1
-plumbum==1.6.9
-rpyc==4.1.5
-scipy==1.4.1
-uuid==1.30
\ No newline at end of file
diff --git a/requirements_dev.txt b/requirements_dev.txt
deleted file mode 100644
index 3b8b0824..00000000
--- a/requirements_dev.txt
+++ /dev/null
@@ -1,14 +0,0 @@
--r requirements_client.txt
--r requirements_gui.txt
-cma==3.0.3
-pylpsd==0.1.3
-pyinstaller==3.6
-migen==0.9.2
-git+https://github.com/m-labs/misoc
-flake8
-black == 22.3.0
-pre-commit
-matplotlib
-pytest
-pytest-plt
-twine
\ No newline at end of file
diff --git a/requirements_gui.txt b/requirements_gui.txt
deleted file mode 100644
index 94ae7622..00000000
--- a/requirements_gui.txt
+++ /dev/null
@@ -1,23 +0,0 @@
-altgraph==0.17
-appdirs==1.4.4
-bcrypt==3.2.0
-cffi==1.14.2
-click==8.1.2
-cryptography==3.3.2
-future==0.18.2
-numpy==1.21.0
-paramiko==2.10.1
-pefile==2019.4.18
-plumbum==1.6.9
-pycparser==2.20
-PyNaCl==1.4.0
-PyQt5==5.15.0
-PyQt5-sip==12.8.0
-pyqtgraph==0.11.0
-pywin32-ctypes==0.2.0
-rpyc==4.1.5
-scipy==1.4.1
-six==1.15.0
-uuid==1.30
-superqt==0.2.3
-packaging==21.3
\ No newline at end of file
diff --git a/scripts/build_standalone_client.sh b/scripts/build_standalone_client.sh
deleted file mode 100755
index d1f239e7..00000000
--- a/scripts/build_standalone_client.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-set -e
-
-while true; do
-    read -p "Did you update your VERSION file? " yn
-    case $yn in
-        [Yy]* ) break;;
-        [Nn]* ) exit;;
-        * ) echo "Please answer yes or no.";;
-    esac
-done
-
-pwd
-
-FILE=/etc/resolv.conf
-if [ -f "linien/VERSION" ]; then
-    pyinstaller pyinstaller.spec
-else
-    echo "VERSION file is missing! See README"
-fi
\ No newline at end of file
diff --git a/scripts/upload_pypi.sh b/scripts/upload_pypi.sh
deleted file mode 100755
index be96e9e6..00000000
--- a/scripts/upload_pypi.sh
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/bin/bash
-
-set -e
-
-read -p "Use production pypi.org? [y/n]" realpypi
-
-while true; do
-    read -p "Did you remember to build a new gateware (if required)? [y/n]" yn
-    case $yn in
-        [Yy]* ) break;;
-        [Nn]* ) exit;;
-        * ) echo "Please answer yes or no.";;
-    esac
-done
-
-while true; do
-    read -p "Did you update your VERSION file? [y/n]" yn
-    case $yn in
-        [Yy]* ) break;;
-        [Nn]* ) exit;;
-        * ) echo "Please answer yes or no.";;
-    esac
-done
-
-SCRIPT=`realpath $0`
-SCRIPTPATH=`dirname $SCRIPT`
-cd $SCRIPTPATH/../
-
-FILE=linien/server/linien.bin
-
-if [ ! -f "$FILE" ]; then
-    echo "FPGA binary is missing. Run build_gateware.sh to build it."
-    exit
-fi
-
-# build client
-rm -R build --force
-rm -R dist --force
-
-read -s -p "Enter your pypi token: " password
-
-
-#               CLIENT
-python3 setup_client.py sdist bdist_wheel
-python3 -m twine check dist/*
-
-case $realpypi in
-    [Yy]* ) python3 -m twine upload dist/* -u __token__ -p $password;;
-    * ) python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* -u __token__ -p $password;;
-esac
-
-
-#               GUI
-rm -R build --force
-rm -R dist --force
-python3 setup_gui.py sdist bdist_wheel
-python3 -m twine check dist/*
-
-case $realpypi in
-    [Yy]* ) python3 -m twine upload dist/* -u __token__ -p $password;;
-    * ) python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* -u __token__ -p $password;;
-esac
-
-#               SERVER
-
-rm -R build --force
-rm -R dist --force
-python3 setup_server.py sdist bdist_wheel
-
-case $realpypi in
-    [Yy]* ) python3 -m twine upload dist/* -u __token__ -p $password;;
-    * ) python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* -u __token__ -p $password;;
-esac
diff --git a/upload_pypi.sh b/upload_pypi.sh
new file mode 100644
index 00000000..31483bfc
--- /dev/null
+++ b/upload_pypi.sh
@@ -0,0 +1,70 @@
+#!/bin/bash
+
+set -e
+
+read -p "Use production pypi.org? [y/n]" realpypi
+
+while true; do
+    read -p "Did you remember to build a new gateware (if required)? [y/n]" yn
+    case $yn in
+        [Yy]* ) break;;
+        [Nn]* ) exit;;
+        * ) echo "Please answer yes or no.";;
+    esac
+done
+
+SCRIPT=`realpath $0`
+SCRIPTPATH=`dirname $SCRIPT`
+cd $SCRIPTPATH
+
+FILE=linien-server/linien_server/linien.bin
+
+if [ ! -f "$FILE" ]; then
+    echo "FPGA binary is missing. Run build_gateware.sh to build it."
+    exit
+fi
+
+read -s -p "Enter your pypi token: " token
+
+# COMMON
+cd $SCRIPTPATH/linien-common
+python3 setup.py sdist bdist_wheel
+python3 -m twine check dist/*
+
+case $realpypi in
+    [Yy]* ) python3 -m twine upload dist/* -u __token__ -p $token;;
+    * ) python3 -m twine upload --repository testpypi dist/* -u __token__ -p $token;;
+esac
+
+
+# CLIENT
+cd $SCRIPTPATH/linien-client
+python3 setup.py sdist bdist_wheel
+python3 -m twine check dist/*
+
+case $realpypi in
+    [Yy]* ) python3 -m twine upload dist/* -u __token__ -p $token;;
+    * ) python3 -m twine upload --repository testpypi dist/* -u __token__ -p $token;;
+esac
+
+
+# GUI
+cd $SCRIPTPATH/linien-gui
+python3 setup.py sdist bdist_wheel
+python3 -m twine check dist/*
+
+case $realpypi in
+    [Yy]* ) python3 -m twine upload dist/* -u __token__ -p $token;;
+    * ) python3 -m twine upload --repository testpypi dist/* -u __token__ -p $token;;
+esac
+
+
+# SERVER
+cd $SCRIPTPATH/linien-server
+python3 setup.py sdist bdist_wheel
+python3 -m twine check dist/*
+
+case $realpypi in
+    [Yy]* ) python3 -m twine upload dist/* -u __token__ -p $token;;
+    * ) python3 -m twine upload --repository testpypi dist/* -u __token__ -p $token;;
+esac


Test patch:
diff --git a/tests/test_algorithm_selection.py b/tests/test_algorithm_selection.py
index 5f2da049..1113a26f 100644
--- a/tests/test_algorithm_selection.py
+++ b/tests/test_algorithm_selection.py
@@ -19,12 +19,15 @@
 import pickle
 
 import numpy as np
-
-from linien.common import AUTO_DETECT_AUTOLOCK_MODE, FAST_AUTOLOCK, ROBUST_AUTOLOCK
-from linien.server.autolock.autolock import Autolock
-from linien.server.autolock.fast import FastAutolock
-from linien.server.autolock.robust import RobustAutolock
-from linien.server.parameters import Parameters
+from linien_common.common import (
+    AUTO_DETECT_AUTOLOCK_MODE,
+    FAST_AUTOLOCK,
+    ROBUST_AUTOLOCK,
+)
+from linien_server.autolock.autolock import Autolock
+from linien_server.autolock.fast import FastAutolock
+from linien_server.autolock.robust import RobustAutolock
+from linien_server.parameters import Parameters
 
 Y_SHIFT = 4000
 
diff --git a/tests/test_approacher.py b/tests/test_approacher.py
index 1fb3b796..6389ca6a 100644
--- a/tests/test_approacher.py
+++ b/tests/test_approacher.py
@@ -17,10 +17,9 @@
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
 import numpy as np
-
-from linien.common import get_lock_point
-from linien.server.approach_line import Approacher
-from linien.server.parameters import Parameters
+from linien_common.common import get_lock_point
+from linien_server.approach_line import Approacher
+from linien_server.parameters import Parameters
 
 Y_SHIFT = 4000
 
diff --git a/tests/test_common.py b/tests/test_common.py
index 4fc0da53..5dcf0918 100644
--- a/tests/test_common.py
+++ b/tests/test_common.py
@@ -17,10 +17,12 @@
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
 
 import numpy as np
+from linien_common.common import (
+    SpectrumUncorrelatedException,
+    determine_shift_by_correlation,
+)
 from pytest import raises
 
-from linien.common import SpectrumUncorrelatedException, determine_shift_by_correlation
-
 Y_SHIFT = 0
 
 
diff --git a/tests/test_fast_autolock_cpu.py b/tests/test_fast_autolock_cpu.py
index ce767a3a..b3e98fd6 100644
--- a/tests/test_fast_autolock_cpu.py
+++ b/tests/test_fast_autolock_cpu.py
@@ -19,10 +19,9 @@
 import pickle
 
 import numpy as np
-
-from linien.common import FAST_AUTOLOCK
-from linien.server.autolock.autolock import Autolock
-from linien.server.parameters import Parameters
+from linien_common.common import FAST_AUTOLOCK
+from linien_server.autolock.autolock import Autolock
+from linien_server.parameters import Parameters
 
 Y_SHIFT = 4000
 
diff --git a/tests/test_fast_autolock_fpga.py b/tests/test_fast_autolock_fpga.py
index e393c9b6..30e89be2 100644
--- a/tests/test_fast_autolock_fpga.py
+++ b/tests/test_fast_autolock_fpga.py
@@ -18,12 +18,12 @@
 
 from pathlib import Path
 
+from linien_common.common import FAST_AUTOLOCK
 from migen import run_simulation
 
-from gateware.linien import LinienLogic
+from gateware.linien_module import LinienLogic
 from gateware.logic.pid import PID
 from gateware.logic.sweep import SweepCSR
-from linien.common import FAST_AUTOLOCK
 
 VCD_DIR = Path(__file__).parent / "vcd"
 
diff --git a/tests/test_optimizer_engines.py b/tests/test_optimizer_engines.py
index 5f258fff..f6d1fc29 100644
--- a/tests/test_optimizer_engines.py
+++ b/tests/test_optimizer_engines.py
@@ -18,13 +18,12 @@
 
 import cma
 import numpy as np
-
-from linien.common import MHz, Vpp
-from linien.server.optimization.engine import (
+from linien_common.common import MHz, Vpp
+from linien_server.optimization.engine import (
     MultiDimensionalOptimizationEngine,
     OptimizerEngine,
 )
-from linien.server.parameters import Parameters
+from linien_server.parameters import Parameters
 
 
 def test_multi():
diff --git a/tests/test_optimizer_utils.py b/tests/test_optimizer_utils.py
index d7c5214c..8dfe41b4 100644
--- a/tests/test_optimizer_utils.py
+++ b/tests/test_optimizer_utils.py
@@ -19,10 +19,9 @@
 from random import randint, random
 
 import numpy as np
+from linien_server.optimization.utils import get_max_slope, optimize_phase_from_iq
 from scipy.optimize import minimize_scalar
 
-from linien.server.optimization.utils import get_max_slope, optimize_phase_from_iq
-
 
 def test_get_max_slope():
     def generate_slope(N=1024, slope=1):
diff --git a/tests/test_robust_autolock.py b/tests/test_robust_autolock.py
index f945a79b..269e15c0 100644
--- a/tests/test_robust_autolock.py
+++ b/tests/test_robust_autolock.py
@@ -20,21 +20,21 @@
 
 import numpy as np
 import pytest
-from migen import run_simulation
-
-from gateware.logic.autolock import RobustAutolock
-from gateware.logic.autolock_utils import DynamicDelay, SumDiffCalculator
-from linien.server.autolock.robust import (
+from linien_server.autolock.robust import (
     calculate_autolock_instructions,
     get_lock_position_from_autolock_instructions,
 )
-from linien.server.autolock.utils import (
+from linien_server.autolock.utils import (
     crop_spectra_to_same_view,
     get_diff_at_time_scale,
     get_lock_region,
     get_time_scale,
     sum_up_spectrum,
 )
+from migen import run_simulation
+
+from gateware.logic.autolock import RobustAutolock
+from gateware.logic.autolock_utils import DynamicDelay, SumDiffCalculator
 
 VCD_DIR = Path(__file__).parent / "vcd"
 FPGA_DELAY_SUMDIFF_CALCULATOR = 2
diff --git a/tests/test_sweep.py b/tests/test_sweep.py
index 568f1b65..bf5f3ced 100644
--- a/tests/test_sweep.py
+++ b/tests/test_sweep.py
@@ -214,7 +214,6 @@ def testbench():
         yield dut.max.storage.eq(1024)
         yield dut.run.storage.eq(1)
         for i in range(n):
-
             if i == 50:
                 # pause sweep
                 yield dut.pause.storage.eq(1)