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

Issue:
Refactor package structure



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index fce7eb52..eb2be176 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -1,14 +1,7 @@
-name: Publish Packages
+name: PublishPackages
 
 on:
   workflow_dispatch:
-    inputs:
-      version_name:
-        description: "Version name"
-        required: true
-
-env:
-  INPUT_VERSION_NAME: ${{ github.event.inputs.version_name }}
 
 permissions:
   contents: read
@@ -25,45 +18,49 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v4
         with:
-          python-version: "3.7"
+          python-version: "3.x"
 
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          pip install build wheel cma rpyc==4.1.5 click plumbum pylpsd numpy scipy paramiko packaging appdirs superqt pyqtgraph PyQt5
+          pip install build wheel
 
-      - name: create VERSION file
-        run: echo "$INPUT_VERSION_NAME" > linien/VERSION
+      - name: Build linien-common
+        working-directory: linien-common
+        run: python -m build
 
-      - name: Build client
-        run: |
-          rm -R build --force
-          rm -R dist --force
-          python3 setup_client.py sdist bdist_wheel
+      - name: Build linien-server
+        working-directory: linien-server
+        run: python -m build
+
+      - name: Build linien-client
+        working-directory: linien-client
+        run: python -m build
 
-      - name: Publish client to PyPI
+      - name: Build linien-gui
+        working-directory: linien-gui
+        run: python -m build
+
+      - name: Publish linien-common
         uses: pypa/gh-action-pypi-publish@release/v1
         with:
-          password: ${{ secrets.PYPI_API_TOKEN }}
-
-      - name: Build GUI
-        run: |
-          rm -R build --force
-          rm -R dist --force
-          python3 setup_gui.py sdist bdist_wheel
+          packages_dir: linien-common/dist/
+          password: ${{ secrets.PYPI_API_TOKEN_COMMON }}
 
-      - name: Publish GUI to PyPI
+      - name: Publish linien-server
         uses: pypa/gh-action-pypi-publish@release/v1
         with:
-          password: ${{ secrets.PYPI_API_TOKEN }}
+          packages_dir: linien-server/dist/
+          password: ${{ secrets.PYPI_API_TOKEN_SERVER }}
 
-      - name: Build server
-        run: |
-          rm -R build --force
-          rm -R dist --force
-          python3 setup_server.py sdist bdist_wheel
+      - name: Publish linien-client
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          packages_dir: linien-client/dist/
+          password: ${{ secrets.PYPI_API_TOKEN_CLIENT }}
 
-      - name: Publish server to PyPI
+      - name: Publish linien-gui
         uses: pypa/gh-action-pypi-publish@release/v1
         with:
-          password: ${{ secrets.PYPI_API_TOKEN_SERVER }}
+          packages_dir: linien-gui/dist/
+          password: ${{ secrets.PYPI_API_TOKEN_GUI }}
diff --git a/.github/workflows/pyinstaller-linux.yml b/.github/workflows/pyinstaller-linux.yml
index 2ab03f92..8d380cae 100644
--- a/.github/workflows/pyinstaller-linux.yml
+++ b/.github/workflows/pyinstaller-linux.yml
@@ -1,43 +1,47 @@
 name: Pyinstaller Linux
 
 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 }}
+  QT_DEBUG_PLUGINS: 1
+  DISPLAY: ":99"
 
-# 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
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+
+      - name: Set up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: "3.10"
 
-      - name: create VERSION file
-        run: echo "$INPUT_VERSION_NAME" > linien/VERSION
+      - 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
 
-      # 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
+      - run: pip install pyinstaller
 
-      - name: PyInstaller Linux
-        uses: JackMcKew/pyinstaller-action-linux@main
-        with:
-          path: .
+      - 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@v2
+      - uses: actions/upload-artifact@v3
         with:
-          name: my-exe
-          path: dist/linux
+          name: linien-gui-linux
+          path: linien-gui/dist/*
diff --git a/.github/workflows/pyinstaller-windows.yml b/.github/workflows/pyinstaller-windows.yml
index f6eebef0..1aabe0f2 100644
--- a/.github/workflows/pyinstaller-windows.yml
+++ b/.github/workflows/pyinstaller-windows.yml
@@ -1,43 +1,40 @@
 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
+    runs-on: windows-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
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
 
-      - name: create VERSION file
-        run: echo "$INPUT_VERSION_NAME" > linien/VERSION
+      - name: Set up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: "3.10"
 
-      # 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
+      - run: pip install pyinstaller
 
-      - name: PyInstaller Windows
-        uses: JackMcKew/pyinstaller-action-windows@main
-        with:
-          path: .
+      - 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@v2
+      - uses: actions/upload-artifact@v3
         with:
-          name: my-exe
-          path: dist/windows
+          name: linien-gui-windows
+          path: linien-gui/dist/*
diff --git a/.gitignore b/.gitignore
index 6f99f7bd..c29f0273 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,7 +9,6 @@ __pycache__
 /*.v
 /dist
 .eggs
-linien-*
 
 .mypy_cache
 .vscode
@@ -23,4 +22,24 @@ 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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 88f190b0..f4aff56b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,7 @@
-## V 0.5.3.post1
+## V 0.6.0
+- Refactor package structure by @bleykauf in https://github.com/linien-org/linien/pull/277
+
+## V 0.5.3.post2
 - Pin specific commits for installation of  `pyrp3 and `mdio-tool` in `linien_install_requirements.sh`
 - Fix file extension in `linien_start_server.sh`, see #291. Thank you, @doronbehar!
 - Bump future from 0.18.2 to 0.18.3 by @dependabot in https://github.com/linien-org/linien/pull/305
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 5f5b081f..924e9533 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
-Linien
-======
+![GitHub release (latest by date)](https://img.shields.io/github/v/release/linien-org/linien)
+[![PyPI](https://img.shields.io/pypi/v/linien?color=blue)](https://pypi.org/project/linien/)
+[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
+
+Linien ‒ User-friendly locking of lasers using RedPitaya (STEMlab 125-14) that just works
+=========================================================================================
 
 <img align="right" src="https://raw.githubusercontent.com/linien-org/linien/master/docs/icon.png" width="20%">
 
@@ -19,8 +23,8 @@ Features
     automatically lock to it. This algorithm is built to be noise and jitter tolerant.
 -   **IQ demodulation**: Optimize demodulation phase in an instant
 -   **Noise analysis**: Record power spectral density (PSD) of the error signal for analyzing noise of the locked laser and for optimizing PID parameters
--   **Lock detection**: Linien is capable of detecting loss of lock.
--   **Automatic relocking**: in that case, it relocks autonomously (temporarily disabled, use [v0.3.2](https://github.com/linien-org/linien/releases/tag/v0.3.2) if you rely in this feature)
+-   **Lock detection**: Linien is capable of detecting loss of lock (temporarily disabled, use [v0.3.2](https://github.com/linien-org/linien/releases/tag/v0.3.2) if you rely in this feature
+-   **Automatic relocking**: if lock is lost, it relocks autonomously (temporarily disabled, use [v0.3.2](https://github.com/linien-org/linien/releases/tag/v0.3.2) if you rely in this feature)
 -   **Machine learning** is used to tune the spectroscopy parameters in order to optimize the signal
 -   **Remote-controllable**: the client libraries can be used to control or monitor the spectroscopy lock with python.
 -   **Combined FMS+MTS**: Linien supports dual-channel spectroscopy that can be
@@ -38,7 +42,7 @@ Features
 Getting started: install Linien
 ---------------
 
-Linien runs on Windows and Linux. For most users the [standalone
+Linien runs on Windows and Linux. For Windows users the [standalone
 binaries](#standalone-binary) containing the graphical user interface
 are recommended.
 These binaries run on your lab PC and contain everything to get Linien running on your RedPitaya.
@@ -47,45 +51,30 @@ If you want to use the python interface you should [install it using pip](#insta
 
 ### Standalone binary
 
-You can download standalone binaries for windows and linux on [the
+You can download standalone binaries for Windows on [the
 releases
-page](https://github.com/linien-org/linien/releases) (download the corresponding binary in the assets section of the latest version). On linux mark it as executable before executing:
-
-```bash
-chmod +x linien-linux*
-./linien-linux*
-```
+page](https://github.com/linien-org/linien/releases) (download the binary in the assets section of the latest version). For Linux users, we recommend installation via pip.
 
 ### Installation with pip
 
-Linien is written for python 3 and can be installed using python\'s
-package manager pip:
+Linien is written for python 3 and can be installed using python\'s package manager pip:
 
 ```bash
-pip3 install linien
+pip install linien-gui
 ```
 
-On Linux, you may run the application by calling
+The GUI can be started by calling 
 
 ```bash
 linien
 ```
 
-in a terminal.
-
-If this doesn\'t work, your local bin directory (e.g. \~/.local/bin) is
-probably missing in your PATH. In this case you can open Linien with
-python:
-
-```python
-from linien.gui.app import run_application
-run_application()
-```
+in a terminal (on both Linux and Windows).
 
-In case you're only interested in the python client and don't want to install the graphical application, you may use the `linien-python-client`, a subset of the `linien` package:
+In case you're only interested in the python client and don't want to install the graphical application, you may use the `linien-client` package:
 
 ```bash
-pip3 install linien-python-client
+pip install linien-client
 ```
 
 
@@ -196,7 +185,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
@@ -284,8 +274,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
@@ -366,7 +356,7 @@ Before installing a new version of Linien, open the previously installed client
 Development
 -----------
 
-Information about ddevelopment can be found in the [wiki](https://github.com/linien-org/linien/wiki/Development).
+Information about development can be found in the [wiki](https://github.com/linien-org/linien/wiki/Development).
 
 FAQs
 ----
@@ -395,23 +385,30 @@ Troubleshooting
 
 ### Connection problems
 
-If the client fails to connect to a RedPitaya, first check whether you
-can ping it by executing
+If the client fails to connect to a RedPitaya, first check whether you can ping it by
+executing
 
 ```bash
 ping rp-f0xxxx.local
 ```
 
-in a command line. If this works, check whether you can connect via SSH.
-On Windows, you have to [install a SSH client](https://www.putty.org),
-on linux you can execute
+in a command line. If this works, check whether you can connect via SSH:
 
 ```bash
 ssh rp-f0xxxx.local
 ```
 
-on the command line.
+on the command line. If this is successful, in order to to  check whether the
+`linien-server` is running, first confirm that there is a running `screen` session with
+the name `linien-server` by  executing `screen -ls`. If that is the case attach it by
+running `screen -r linien-server`. If any errors occurred on the server side, they will
+be displayed here. Please provide the output if you are reporting an
+[issue](https://github.com/linien-org/linien/issues)  related to connection problems.
+
+### Possible conflict with openSSH
 
+Note that there might be issues if openSSH server is running on the client, see
+[here](https://github.com/orgs/linien-org/discussions/286).
 
 ### Updating or installing fails
 
@@ -421,19 +418,21 @@ on the command line.
 Citation
 ----
 
-If you are using Linien, please cite us as follows:
+If you are using Linien for your scientific work, please cite us as follows:
 
 ```
 @article{Wiegand2022,
-archivePrefix = {arXiv},
-arxivId = {2203.02947},
-author = {Wiegand, Benjamin and Leykauf, Bastian and J{\"{o}}rdens, Robert and Krutzik, Markus},
-eprint = {2203.02947},
-title = {{Linien: A versatile, user-friendly, open-source FPGA-based tool for frequency stabilization and spectroscopy parameter optimization}},
-url = {http://arxiv.org/abs/2203.02947},
-year = {2022}
+   author = {B. Wiegand and B. Leykauf and R. Jördens and M. Krutzik},
+   doi = {10.1063/5.0090384},
+   issn = {10897623},
+   issue = {6},
+   journal = {Review of Scientific Instruments},
+   month = {6},
+   pmid = {35778046},
+   title = {Linien: A versatile, user-friendly, open-source FPGA-based tool for frequency stabilization and spectroscopy parameter optimization},
+   volume = {93},
+   year = {2022},
 }
-
 ```
 
 License
@@ -442,7 +441,7 @@ Linien ‒ User-friendly locking of lasers using RedPitaya (STEMlab 125-14) that
 
 Copyright © 2014-2015 Robert Jördens
 Copyright © 2018-2022 Benjamin Wiegand
-Copyright © 2021-2022 Bastian Leykauf
+Copyright © 2021-2023 Bastian Leykauf
 
 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.
 
@@ -450,7 +449,15 @@ Linien is distributed in the hope that it will be useful, but WITHOUT ANY WARRAN
 
 You should have received a copy of the GNU General Public License along with Linien. If not, see <https://www.gnu.org/licenses/>.
 
-Development takes place at Humboldt University of Berlin.
+Acknowledgements
+----------------
+
+Development mainly takes place at Humboldt University of Berlin.
+
+This work is supported by the German Space Agency
+(DLR) with funds provided by the Federal Ministry of
+Economics and Technology (BMWi) under grant number
+DLR50WM2066.
 
 See Also
 --------
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 89%
rename from gateware/platform.py
rename to gateware/hw_platform.py
index bfa9ee00..ca8f2579 100644
--- a/gateware/platform.py
+++ b/gateware/hw_platform.py
@@ -16,21 +16,6 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
-#
-# 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 migen.build.generic_platform import (
     ConstraintError,
@@ -187,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
 
@@ -203,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 98%
rename from gateware/linien.py
rename to gateware/linien_module.py
index 6c1a2493..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
@@ -274,8 +273,7 @@ def connect_everything(self, width, signal_width, coeff_width, chain_factor_bits
             self.fast_b.adc.eq(self.analog.adc_b),
         ]
 
-        # now, we combine the output of the two paths, with a variable
-        # factor each.
+        # now, we combine the output of the two paths, with a variable factor each.
         mixed = Signal(
             (2 + ((signal_width + 1) + self.logic.chain_a_factor.size), True)
         )
@@ -330,9 +328,8 @@ def connect_everything(self, width, signal_width, coeff_width, chain_factor_bits
 
         for analog_idx in range(4):
             if analog_idx == 0:
-                # first analog out gets a special treatment bc it may
-                # contain signal of slow pid or sweep
-
+                # first analog out gets a special treatment bc it may contain signal of
+                # slow pid or sweep
                 self.comb += self.slow.pid.running.eq(
                     self.logic.autolock.lock_running.status
                 )
@@ -382,8 +379,8 @@ def connect_everything(self, width, signal_width, coeff_width, chain_factor_bits
 
         self.sync += [
             self.logic.autolock.robust.input.eq(self.scopegen.scope_written_data),
-            # `writing_data_now` is intentionally delayed by one cycle here
-            # in order to prevent glitches
+            # `writing_data_now` is intentionally delayed by one cycle here in order to
+            # prevent glitches
             self.logic.autolock.robust.writing_data_now.eq(
                 self.scopegen.writing_data_now
             ),
@@ -409,6 +406,7 @@ def connect_everything(self, width, signal_width, coeff_width, chain_factor_bits
             self.logic.slow_value.status.eq(self.slow.limit.y),
         ]
 
+        # Having this in a comb statement caused errors. See PR #251.
         self.sync += [
             self.logic.limit_fast1.x.eq(fast_outs[0]),
             self.logic.limit_fast2.x.eq(fast_outs[1]),
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/gateware/logic/cordic.py b/gateware/logic/cordic.py
index cf525ca0..659b59aa 100644
--- a/gateware/logic/cordic.py
+++ b/gateware/logic/cordic.py
@@ -16,21 +16,6 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
-#
-# 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 math import atan, atanh, log, pi, sqrt
 
@@ -298,7 +283,7 @@ def __init__(
     def _constants(self, stages, bits):
         if self.func_mode == "circular":
             s = range(stages)
-            a = [atan(2 ** -i) for i in s]
+            a = [atan(2**-i) for i in s]
             g = [sqrt(1 + 2 ** (-2 * i)) for i in s]
             # zmax = sum(a)
             # use pi anyway as the input z can cause overflow
@@ -306,7 +291,7 @@ def _constants(self, stages, bits):
             zmax = pi
         elif self.func_mode == "linear":
             s = range(stages)
-            a = [2 ** -i for i in s]
+            a = [2**-i for i in s]
             g = [1 for i in s]
             # zmax = sum(a)
             # use 2 anyway as this simplifies a and scaling
@@ -320,7 +305,7 @@ def _constants(self, stages, bits):
                     s.append(j)
                     j = 3 * j + 1
                 s.append(i + 1)
-            a = [atanh(2 ** -i) for i in s]
+            a = [atanh(2**-i) for i in s]
             g = [sqrt(1 - 2 ** (-2 * i)) for i in s]
             zmax = sum(a) * 2
         # round here helps the width=2**i - 1 case but hurts the
diff --git a/gateware/logic/delta_sigma.py b/gateware/logic/delta_sigma.py
index 7c0836c5..7fa70fc8 100644
--- a/gateware/logic/delta_sigma.py
+++ b/gateware/logic/delta_sigma.py
@@ -16,21 +16,6 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
-#
-# 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 migen import Module, Signal
 from misoc.interconnect.csr import AutoCSR, CSRStorage
diff --git a/gateware/logic/filter.py b/gateware/logic/filter.py
index 242dbd93..62a0dcdb 100644
--- a/gateware/logic/filter.py
+++ b/gateware/logic/filter.py
@@ -16,21 +16,6 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
-#
-# 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 migen import Module, Signal
 from misoc.interconnect.csr import AutoCSR
diff --git a/gateware/logic/iir.py b/gateware/logic/iir.py
index dcaa894e..2380dd79 100644
--- a/gateware/logic/iir.py
+++ b/gateware/logic/iir.py
@@ -16,21 +16,6 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
-#
-# 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 migen import Constant, If, Signal
 from migen.genlib.misc import timeline
diff --git a/gateware/logic/limit.py b/gateware/logic/limit.py
index 970594cd..f02435d3 100644
--- a/gateware/logic/limit.py
+++ b/gateware/logic/limit.py
@@ -16,21 +16,6 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
-#
-# 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 migen import Cat, If, Module, Replicate, Signal
 from misoc.interconnect.csr import CSRStorage
diff --git a/gateware/logic/modulate.py b/gateware/logic/modulate.py
index c324b8ac..c7503060 100644
--- a/gateware/logic/modulate.py
+++ b/gateware/logic/modulate.py
@@ -16,21 +16,6 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with Linien.  If not, see <http://www.gnu.org/licenses/>.
-#
-# 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 migen import If, Module, Signal
 from misoc.interconnect.csr import AutoCSR, CSRStorage
diff --git a/gateware/logic/sweep.py b/gateware/logic/sweep.py
index d70c6e6a..fd0b75d1 100644
--- a/gateware/logic/sweep.py
+++ b/gateware/logic/sweep.py
@@ -16,24 +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/>.
-#
-# 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 migen import Cat, If, Module, Signal, bits_for
-from misoc.interconnect.csr import AutoCSR, CSRConstant, CSRStorage
+from migen import Cat, If, Module, Signal
+from misoc.interconnect.csr import AutoCSR, CSRStorage
 
 from .limit import Limit
 
@@ -56,23 +41,16 @@ def __init__(self, width):
         self.comb += [
             If(
                 self.run,
-                If(self.turn & ~turning,
-                    self.up.eq(~dir))
-                .Else(
-                    self.up.eq(dir)),)
-            .Else(
-                self.up.eq(1))
+                If(self.turn & ~turning, self.up.eq(~dir)).Else(self.up.eq(dir)),
+            ).Else(self.up.eq(1))
         ]
         self.sync += [
             self.trigger.eq(self.turn & self.up),
             turning.eq(self.turn),
             dir.eq(self.up),
-            If(~self.run,
-                self.y.eq(0))
-            .Elif(~self.hold,
-                If(self.up,
-                    self.y.eq(self.y + self.step),)
-                    .Else(
+            If(~self.run, self.y.eq(0)).Elif(
+                ~self.hold,
+                If(self.up, self.y.eq(self.y + self.step),).Else(
                     self.y.eq(self.y - self.step),
                 ),
             ),
@@ -116,9 +94,8 @@ def __init__(self, width, step_width=None, step_shift=0):
             self.limit.min.eq(Cat(self.min.storage, self.min.storage[-1])),
             self.limit.max.eq(Cat(self.max.storage, self.max.storage[-1])),
             self.sweep.turn.eq(self.limit.railed),
-            If(self.pause.storage,
+            If(
+                self.pause.storage,
                 self.y.eq(0),
-            ).Else(
-                self.y.eq(self.limit.y)
-            )
+            ).Else(self.y.eq(self.limit.y)),
         ]
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..e30132e1
--- /dev/null
+++ b/linien-client/linien_client/__init__.py
@@ -0,0 +1,3 @@
+import importlib.metadata
+
+__version__ = importlib.metadata.version("linien-client")  # noqa: F401
diff --git a/linien/client/connection.py b/linien-client/linien_client/connection.py
similarity index 50%
rename from linien/client/connection.py
rename to linien-client/linien_client/connection.py
index 7ae26775..db49743c 100644
--- a/linien/client/connection.py
+++ b/linien-client/linien_client/connection.py
@@ -23,33 +23,27 @@
 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
 
 
 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 +58,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,32 +105,27 @@ 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:
-                        print(
-                            """
-                            Server still not running, waiting (this may take some time).
-                            """
-                        )
+                        print("Server still not running, waiting (may take some time).")
                         sleep(1)
                     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 +133,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 != client_version:
-            raise InvalidServerVersionException(client_version, remote_version)
+        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)
 
-        print(colors.green | "connected established!")
+        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("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..6f043278
--- /dev/null
+++ b/linien-client/linien_client/deploy.py
@@ -0,0 +1,108 @@
+# 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 sys
+
+import linien_client
+from fabric import Connection
+from linien_client.exceptions import (
+    InvalidServerVersionException,
+    ServerNotInstalledException,
+)
+
+
+def read_remote_version(
+    host: str, user: str, password: str, port: int = 22, out_stream=sys.stdout
+) -> str:
+    """Read the remote version of linien."""
+
+    if not out_stream:
+        # sys.stdout is not available in the pyinstaller build, redirect it to avoid
+        # AttributeError: 'NoneType' object has no attribute 'write'
+        out_stream = open(os.devnull, "w")
+
+    with Connection(
+        host, user=user, port=port, connect_kwargs={"password": password}
+    ) as conn:
+        result = conn.run(
+            'python3 -c "import linien_server; print(linien_server.__version__);"',
+            out_stream=out_stream,
+            err_stream=out_stream,
+            warn=True,
+        )
+    if result.ok:
+        return result.stdout.strip()
+    else:
+        raise ServerNotInstalledException()
+
+
+def start_remote_server(
+    host: str, user: str, password: str, port: int = 22, out_stream=sys.stdout
+) -> None:
+    """Start the remote linien server."""
+
+    if not out_stream:
+        # sys.stdout is not available in the pyinstaller build, redirect it to avoid
+        # AttributeError: 'NoneType' object has no attribute 'write'
+        out_stream = open(os.devnull, "w")
+
+    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(host, user, password, port).split("+")[0]
+
+        if (local_version != remote_version) and not ("dev" in local_version):
+            raise InvalidServerVersionException(local_version, remote_version)
+
+        conn.run(
+            "linien_start_server.sh",
+            out_stream=out_stream,
+            err_stream=out_stream,
+            warn=True,
+        )
+
+
+def install_remote_server(
+    host: str, user: str, password: str, port: int = 22, out_stream=sys.stdout
+):
+    """Install the remote linien server."""
+
+    if not out_stream:
+        # sys.stdout is not available in the pyinstaller build
+        out_stream = open(os.devnull, "w")
+
+    with Connection(
+        host, user=user, port=port, connect_kwargs={"password": password}
+    ) as conn:
+        local_version = linien_client.__version__.split("+")[0]
+        cmds = [
+            "linien_stop_server.sh",
+            "pip3 uninstall linien-server -y",
+            "pip3 uninstall linien-common -y",
+            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, err_stream=out_stream, warn=True
+            )
+            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 54%
rename from setup_client.py
rename to linien-client/setup.py
index c3e10af4..4f01d11b 100644
--- a/setup_client.py
+++ b/linien-client/setup.py
@@ -16,57 +16,34 @@
 # 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
 
-import linien
+version = "0.6.0"
 
-assert linien.__version__ != "dev"
-
-long_description = """
-
----
-**DEPRICATION NOTE**
-
-This package has been moved to [linien-client](https://pypi.org/project/linien-client/) as of Linien v0.6.0. Please have a look at the [project repository](https://github.com/linien-org/linien) for up to date installation instructions.
-
----
-
-"""
-with open("README.md", "r") as fh:
-    long_description += fh.read()
-
-setuptools.setup(
-    name="linien-python-client",
-    version=linien.__version__,
+setup(
+    name="linien-client",
+    version=version,
     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,
+    description="Client components of the Linien spectroscopy lock application.",
+    long_description="Have a look at the [project repository](https://github.com/linien-org/linien) for installation instructions.",  # noqa: E501
     long_description_content_type="text/markdown",
     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",
     ],
+    python_requires=">=3.8",
     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",
+        f"linien-common=={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-common/linien_common/__init__.py b/linien-common/linien_common/__init__.py
new file mode 100644
index 00000000..255301d5
--- /dev/null
+++ b/linien-common/linien_common/__init__.py
@@ -0,0 +1,3 @@
+import importlib_metadata
+
+__version__ = importlib_metadata.version("linien-common")  # noqa: F401
diff --git a/linien/common.py b/linien-common/linien_common/common.py
similarity index 100%
rename from linien/common.py
rename to linien-common/linien_common/common.py
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..da92a79e
--- /dev/null
+++ b/linien-common/setup.py
@@ -0,0 +1,43 @@
+# Copyright 2023 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 setuptools import find_packages, setup
+
+setup(
+    name="linien-common",
+    version="0.6.0",
+    author="Benjamin Wiegand",
+    author_email="highwaychile@posteo.de",
+    maintainer="Bastian Leykauf",
+    maintainer_email="leykauf@physik.hu-berlin.de",
+    description="Shared components of the Linien spectroscopy lock application.",
+    long_description="Have a look at the [project repository](https://github.com/linien-org/linien) for installation instructions.",  # noqa: E501
+    long_description_content_type="text/markdown",
+    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",
+    ],
+    python_requires=">=3.5",
+    install_requires=[
+        "numpy>=1.11.0",
+        "scipy>=0.17.0",
+        "importlib_metadata>=2.1.3",
+    ],
+)
diff --git a/linien-gui/linien_gui/__init__.py b/linien-gui/linien_gui/__init__.py
new file mode 100644
index 00000000..17246ad0
--- /dev/null
+++ b/linien-gui/linien_gui/__init__.py
@@ -0,0 +1,3 @@
+import importlib.metadata
+
+__version__ = importlib.metadata.version("linien-gui")  # noqa: F401
diff --git a/linien/gui/app.py b/linien-gui/linien_gui/app.py
similarity index 82%
rename from linien/gui/app.py
rename to linien-gui/linien_gui/app.py
index ecb039da..98680b55 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
 
-from plumbum import colors
+import click
+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 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
@@ -95,7 +88,7 @@ def call_listeners(self):
             try:
                 self.parameters.call_listeners()
             except Exception:
-                print(colors.red | "call_listeners() failed")
+                print("call_listeners() failed")
                 print_exc()
 
             QtCore.QTimer.singleShot(50, self.call_listeners)
@@ -135,15 +128,16 @@ def check_for_new_version(self):
 
     def new_version_available(self, new_version_available):
         if new_version_available:
-            print("new version available")
+            print("New version available")
             self.main_window.show_new_version_available()
         else:
-            print("no new version available")
+            print("No new version available")
             QtCore.QTimer.singleShot(1000 * 60 * 60, self.check_for_new_version)
 
 
+@click.command()
+@click.version_option(linien_gui.__version__)
 def run_application():
-    print("Linien spectroscopy lock version " + (colors.bold | linien.__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 50%
rename from linien/gui/dialogs.py
rename to linien-gui/linien_gui/dialogs.py
index 9e75946b..fd16c4b1 100644
--- a/linien/gui/dialogs.py
+++ b/linien-gui/linien_gui/dialogs.py
@@ -15,88 +15,69 @@
 # 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 plumbum import colors
-from PyQt5.QtCore import pyqtSignal
-from PyQt5.QtWidgets import QDialog, QListWidget, QMessageBox, QVBoxLayout
+from time import sleep
+from typing import Callable
+
+from linien_gui.threads import RemoteServerInstallationThread
+from PyQt5.QtCore import QThread, pyqtSignal
+from PyQt5.QtWidgets import (
+    QDialog,
+    QListWidget,
+    QMessageBox,
+    QPushButton,
+    QVBoxLayout,
+    QWidget,
+)
 from pyqtgraph import QtCore
 
-from linien.client.utils import connect_ssh
-
 
 class SSHCommandOutputWidget(QListWidget):
-    command_ended = pyqtSignal()
+    command_finished = pyqtSignal()
+    enable_button = 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()
-        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()
-
-        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 run(self, thread: QThread):
+        self.thread = thread
+        self.thread.out_stream.new_item.connect(self.on_new_item_in_out_stream)
+        self.thread.finished.connect(self.on_thread_finished)
+        self.thread.start()
+
+    def on_new_item_in_out_stream(self, line: str):
+        self.addItem(line.rstrip())
+        self.scrollToBottom()
 
+    def on_thread_finished(self):
+        self.addItem("\nFinished.")
+        self.scrollToBottom()
+        self.command_finished.emit()
+
+
+def show_installation_progress_widget(
+    parent: QWidget, device: dict, callback: Callable
+):
     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)
+    button = QPushButton("Continue")
+    button.setEnabled(False)
     window_layout.addWidget(widget)
+    window_layout.addWidget(button)
     window.setLayout(window_layout)
     window.setModal(True)
     window.setWindowModality(QtCore.Qt.WindowModal)
     window.show()
 
-    def after_command():
-        window.hide()
-        callback()
-
-    widget.command_ended.connect(after_command)
+    widget.command_finished.connect(lambda: button.setEnabled(True))
+    button.clicked.connect(callback)
+    button.clicked.connect(window.close)
 
-    widget.execute(ssh, command)
+    thread = RemoteServerInstallationThread(device)
+    widget.run(thread)
 
     return window
 
@@ -104,11 +85,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 +105,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..9b221e40
--- /dev/null
+++ b/linien-gui/linien_gui/threads.py
@@ -0,0 +1,182 @@
+# 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 traceback
+
+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 QObject, QThread, pyqtSignal
+
+
+class RemoteOutStream(QObject):
+    new_item = pyqtSignal(str)
+
+    def write(self, item):
+        self.new_item.emit(item)
+
+    def read(self):
+        pass
+
+    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 = RemoteOutStream()
+
+    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()
+    other_exception_raised = pyqtSignal(str)
+    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)
+
+            # Check for locally cached settings for this server
+            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()
+
+        except ServerNotInstalledException:
+            self.server_not_installed_exception_raised.emit()
+
+        except InvalidServerVersionException as e:
+            self.invalid_server_version_exception_raised.emit(
+                e.remote_version, e.client_version
+            )
+
+        except RPYCAuthenticationException:
+            self.authentication_exception_raised.emit()
+
+        except GeneralConnectionErrorException:
+            self.general_connection_exception_raised.emit()
+
+        except Exception:
+            traceback.print_exc()
+            self.other_exception_raised.emit(traceback.format_exc())
+
+    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/client/__init__.py b/linien-gui/linien_gui/ui/__init__.py
similarity index 100%
rename from linien/client/__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..96277f8f
--- /dev/null
+++ b/linien-gui/linien_gui/ui/device_manager.py
@@ -0,0 +1,276 @@
+# 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(exception):
+            loading_dialog.hide()
+            if not aborted:
+                display_error = (
+                    f"Exception occured when connecting to the device:\n\n {exception}"
+                )
+                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.other_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 3753cca2..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):
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 55%
rename from setup_gui.py
rename to linien-gui/setup.py
index 410de3cf..4fb93d5f 100644
--- a/setup_gui.py
+++ b/linien-gui/setup.py
@@ -16,62 +16,40 @@
 # 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
 
-import linien
+version = "0.6.0"
 
-assert linien.__version__ != "dev"
 
-long_description = """
-
----
-**DEPRICATION NOTE**
-
-This package has been moved to [linien-gui](https://pypi.org/project/linien-gui/) as of Linien v0.6.0. Please have a look at the [project repository](https://github.com/linien-org/linien) for up to date installation instructions.
-
----
-
-"""
-with open("README.md", "r") as fh:
-    long_description += fh.read()
-
-setuptools.setup(
-    name="linien",
-    version=linien.__version__,
+setup(
+    name="linien-gui",
+    version=version,
     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,
+    description="Graphical user interface of the Linien spectroscopy lock application.",
+    long_description="Have a look at the [project repository](https://github.com/linien-org/linien) for installation instructions.",  # noqa: E501
     long_description_content_type="text/markdown",
     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"]},
+    python_requires=">=3.8",
     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__,
+        f"linien_client=={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..0b904c58
--- /dev/null
+++ b/linien-server/linien_server/__init__.py
@@ -0,0 +1,3 @@
+import importlib_metadata
+
+__version__ = importlib_metadata.version("linien-server")  # noqa: F401
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..792741cc 100644
--- a/linien/server/acquisition_process.py
+++ b/linien-server/linien_server/acquisition_process.py
@@ -19,21 +19,18 @@
 import _thread
 import os
 import pickle
-import sys
 import threading
 from random import random
 from time import sleep
 
 import numpy as np
-from PyRedPitaya.board import RedPitaya
+from linien_common.common import DECIMATION, MAX_N_POINTS, N_POINTS
+from linien_common.config import ACQUISITION_PORT
+from pyrp3.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/__init__.py b/linien-server/linien_server/autolock/__init__.py
similarity index 100%
rename from linien/gui/__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_server/linien_install_requirements.sh b/linien-server/linien_server/linien_install_requirements.sh
new file mode 100644
index 00000000..c25f4b74
--- /dev/null
+++ b/linien-server/linien_server/linien_install_requirements.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+if [[ $EUID -ne 0 ]]; then
+   echo "This script must be run as root"
+   exit 1
+fi
+
+# the server is started in a screen session
+echo 'installing screen...'
+apt-get install screen
+
+# 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
+git checkout 72bd5a915ff046a59ce4303c8de672e77622a86c
+cmake .
+make
+mv -f mdio-tool /usr/bin
\ No newline at end of file
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/gui/ui/__init__.py b/linien-server/linien_server/optimization/__init__.py
similarity index 100%
rename from linien/gui/ui/__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/autolock/__init__.py b/linien-server/linien_server/pid_optimization/__init__.py
similarity index 100%
rename from linien/server/autolock/__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 c7301bc6..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))
 
 
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 93%
rename from linien/server/server.py
rename to linien-server/linien_server/server.py
index e9076c35..11a75d4c 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,25 @@
 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 import __version__
+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 +93,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 +244,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 +328,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
@@ -340,19 +340,21 @@ def continue_acquisition(self):
 
 
 @click.command()
+@click.version_option(__version__)
 @click.argument("port", default=DEFAULT_SERVER_PORT, type=int, required=False)
 @click.option(
     "--fake", is_flag=True, help="Runs a fake server that just returns random data"
 )
 @click.option(
     "--remote-rp",
-    help="Allows to run the server locally for development and "
-    "connects to a RedPitaya. Specify the RP's credentials "
-    "as follows: "
-    "--remote-rp=root:myPassword@rp-f0xxxx.local",
+    help=(
+        "Allows to run the server locally for development and connects to a RedPitaya. "
+        "Specify the RP's credentials as follows: "
+        "--remote-rp=root:myPassword@rp-f0xxxx.local"
+    ),
 )
 def run_server(port, fake=False, remote_rp=False):
-    print("start server at port", port)
+    print("Start server on port", port)
 
     if fake:
         print("starting fake server")
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/setup_server.py b/linien-server/setup.py
similarity index 61%
rename from setup_server.py
rename to linien-server/setup.py
index 87ad50b6..f9312f1e 100644
--- a/setup_server.py
+++ b/linien-server/setup.py
@@ -16,61 +16,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 setuptools
+from setuptools import find_packages, setup
 
-import linien
+version = "0.6.0"
 
-assert linien.__version__ != "dev"
-
-setuptools.setup(
+setup(
     name="linien-server",
-    version=linien.__version__,
+    version=version,
     author="Benjamin Wiegand",
     author_email="highwaychile@posteo.de",
     maintainer="Bastian Leykauf",
     maintainer_email="leykauf@physik.hu-berlin.de",
-    description="Server of linien that runs on RedPitaya.",
-    long_description="Have a look at the 'linien' package for installation instructions.",  # noqa: E501
-    long_description_content_type="text/x-rst",
+    description="Server components of the Linien spectroscopy lock application.",
+    long_description="Have a look at the [project repository](https://github.com/linien-org/linien) for installation instructions.",  # noqa: E501
+    long_description_content_type="text/markdown",
     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"]},
+    python_requires=">=3.5",
     install_requires=[
-        "rpyc>=4.0,<5.0",
-        "myhdl>=0.11",
         "click>=7.1.2",
         "cma>=3.0.3",
-        "plumbum>=1.6.9",
         "pylpsd>=0.1.4",
-        "numpy>=1.11.0",
-        "scipy>=0.17.0",
+        "pyrp3>=1.1.0;platform_machine=='armv7l'",  # only install on RedPitaya
+        "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_install_requirements.sh b/linien/server/linien_install_requirements.sh
deleted file mode 100644
index d05b5bf8..00000000
--- a/linien/server/linien_install_requirements.sh
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/bin/bash
-
-if [[ $EUID -ne 0 ]]; then
-   echo "This script must be run as root"
-   exit 1
-fi
-
-while ! ping -c 1 -W 1 8.8.4.4; do
-   echo "Not connected to the internet! RedPitaya needs to access the internet in order to proceed with the installation."
-   sleep 1
-done
-
-echo 'installing dependencies...'
-# 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
-git checkout f13da68d825ede3091a082edf99339c5ed736bd2
-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
-
-# https://github.com/RedPitaya/RedPitaya/issues/205
-echo 'building ethernet blinking fix'
-git clone https://github.com/linien-org/mdio-tool.git
-cd mdio-tool
-git checkout 72bd5a915ff046a59ce4303c8de672e77622a86c
-cmake .
-make
-rm -f /usr/bin/mdio-tool
-mv mdio-tool /usr/bin
\ No newline at end of file
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 04734f9e..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.sh
-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/optimization/__init__.py b/linien/server/optimization/__init__.py
deleted file mode 100644
index e69de29b..00000000
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 1388ecec..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
-numpy==1.21.0
-future==0.18.3
-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/requirements_server.txt b/requirements_server.txt
deleted file mode 100644
index 03f86420..00000000
--- a/requirements_server.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-cma==3.0.3
-rpyc==4.1.5
-myhdl==0.11
-click==7.1.2
-plumbum==1.6.9
-pylpsd==0.1.4
-numpy==1.11.0
-scipy==0.17.0
\ 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/version-info.json b/version-info.json
index 80ef68e5..cc12d961 100644
--- a/version-info.json
+++ b/version-info.json
@@ -1,3 +1,3 @@
 {
-    "latest": "0.5.3"
+    "latest": "0.6.0"
 }
\ No newline at end of file


Test patch:
diff --git a/.github/workflows/publish-test-pypi.yml b/.github/workflows/publish-test-pypi.yml
index 75d0a088..7e3ab8b7 100644
--- a/.github/workflows/publish-test-pypi.yml
+++ b/.github/workflows/publish-test-pypi.yml
@@ -2,13 +2,6 @@ name: Upload Packages to TestPyPI
 
 on:
   workflow_dispatch:
-    inputs:
-      version_name:
-        description: "Version name"
-        required: true
-
-env:
-  INPUT_VERSION_NAME: ${{ github.event.inputs.version_name }}
 
 permissions:
   contents: read
@@ -25,48 +18,53 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v4
         with:
-          python-version: "3.7"
+          python-version: "3.x"
 
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          pip install build wheel cma rpyc==4.1.5 click plumbum pylpsd numpy scipy paramiko packaging appdirs superqt pyqtgraph PyQt5
+          pip install build wheel
 
-      - name: create VERSION file
-        run: echo "$INPUT_VERSION_NAME" > linien/VERSION
+      - name: Build linien-common
+        working-directory: linien-common
+        run: python -m build
 
-      - name: Build client
-        run: |
-          rm -R build --force
-          rm -R dist --force
-          python3 setup_client.py sdist bdist_wheel
+      - name: Build linien-server
+        working-directory: linien-server
+        run: python -m build
+
+      - name: Build linien-client
+        working-directory: linien-client
+        run: python -m build
 
-      - name: Publish client to TestPyPI
+      - name: Build linien-gui
+        working-directory: linien-gui
+        run: python -m build
+
+      - name: Publish linien-common to TestPyPI
         uses: pypa/gh-action-pypi-publish@release/v1
         with:
-          password: ${{ secrets.TEST_PYPI_API_TOKEN }}
+          packages_dir: linien-common/dist/
+          password: ${{ secrets.TEST_PYPI_API_TOKEN_COMMON }}
           repository_url: https://test.pypi.org/legacy/
 
-      - name: Build GUI
-        run: |
-          rm -R build --force
-          rm -R dist --force
-          python3 setup_gui.py sdist bdist_wheel
-
-      - name: Publish GUI to TestPyPI
+      - name: Publish linien-server to TestPyPI
         uses: pypa/gh-action-pypi-publish@release/v1
         with:
-          password: ${{ secrets.TEST_PYPI_API_TOKEN }}
+          packages_dir: linien-server/dist/
+          password: ${{ secrets.TEST_PYPI_API_TOKEN_SERVER }}
           repository_url: https://test.pypi.org/legacy/
 
-      - name: Build server
-        run: |
-          rm -R build --force
-          rm -R dist --force
-          python3 setup_server.py sdist bdist_wheel
+      - name: Publish linien-client to TestPyPI
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          packages_dir: linien-client/dist/
+          password: ${{ secrets.TEST_PYPI_API_TOKEN_CLIENT }}
+          repository_url: https://test.pypi.org/legacy/
 
-      - name: Publish server to TestPyPI
+      - name: Publish linien-gui to TestPyPI
         uses: pypa/gh-action-pypi-publish@release/v1
         with:
-          password: ${{ secrets.TEST_PYPI_API_TOKEN_SERVER }}
+          packages_dir: linien-gui/dist/
+          password: ${{ secrets.TEST_PYPI_API_TOKEN_GUI }}
           repository_url: https://test.pypi.org/legacy/
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index e22483fe..5cf9b819 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -28,7 +28,7 @@ jobs:
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          pip install pytest migen git+https://github.com/m-labs/misoc
+          pip install pytest migen pytest-plt git+https://github.com/m-labs/misoc
 
       - name: Test with pytest
         run: |
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_pid.py b/tests/test_pid.py
index 012a8884..ed745aba 100644
--- a/tests/test_pid.py
+++ b/tests/test_pid.py
@@ -116,7 +116,7 @@ def test_d():
             ys = []
             for offset in (0, 2000):
                 x = offset + (np.sin(np.linspace(-np.pi, np.pi, 100)) * 4000).astype(
-                    np.int
+                    int
                 )
                 y = []
 
diff --git a/tests/test_pid_transfer.py b/tests/test_pid_transfer.py
index 3b47ae17..bf640fff 100644
--- a/tests/test_pid_transfer.py
+++ b/tests/test_pid_transfer.py
@@ -36,7 +36,7 @@ def pid_testbench(pid):
 
         x = np.random.uniform(-amplitude, amplitude, samples)
         scale = 2 ** (len(pid.input) - 1) - 1
-        x = (scale * np.array(x)).astype(np.int)
+        x = (scale * np.array(x)).astype(int)
         y = np.array([0] * len(x))
 
         def plot_transfer(x, y, label=None):
@@ -90,9 +90,7 @@ def do_test(p=0, i=0, d=0):
                 out = yield pid.pid_out
                 y[_] = out
 
-            f, plot_color = plot_transfer(
-                x.astype(np.float), y.astype(np.float), label=label
-            )
+            f, plot_color = plot_transfer(x.astype(float), y.astype(float), label=label)
             plot_theory(f, p, i, d, plot_color)
 
         yield from do_test(p=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