ms-trx support

This is basically a trxcon that includes a transceiver, and can just
be used with existing and future apps supporting the trxcon interface,
i.e. mobile or ccch_scan.

Supports bladerf and uhd, binaries are automatically built if those libs
are detected + the osmocom-bb submodule exists.
Currently using hardcoded sched/prios aimed at a setup with working,
reliable usb and reserved cores, for example a raspi 4 (ONLY 4, not 3,
not 2, not any other version)

Additionally builds test tools used for development: osmo-trx-syncthing*

Change-Id: I36c65a8c725c4da76dc70006cd96b0a2b6878e84
diff --git a/.gitignore b/.gitignore
index 35a1b46..0b33e0f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,13 @@
 Transceiver52M/osmo-trx-lms
 Transceiver52M/osmo-trx-ipc
 Transceiver52M/osmo-trx-blade
+Transceiver52M/osmo-trx-ipc2
+Transceiver52M/osmo-trx-syncthing-blade
+Transceiver52M/osmo-trx-syncthing-uhd
+Transceiver52M/osmo-trx-syncthing-ipc
+Transceiver52M/osmo-trx-ms-blade
+Transceiver52M/osmo-trx-ms-uhd
+Transceiver52M/osmo-trx-ms-ipc
 Transceiver52M/device/ipc/uhddev_ipc.cpp
 
 .clang-format
diff --git a/Makefile.am b/Makefile.am
index 6a3a955..7636b46 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -26,8 +26,15 @@
 #AM_CXXFLAGS = -Wall -O2 -NDEBUG -pthread
 #AM_CFLAGS = -Wall -O2 -NDEBUG -pthread
 
+SUBDIRS =
+
+if ENABLE_MS_TRX
+	SUBDIRS += osmocom-bb/src/host/trxcon
+endif
+
 # Order must be preserved
-SUBDIRS = \
+SUBDIRS += \
+	osmocom-bb/src/host/trxcon \
 	CommonLibs \
 	GSM \
 	Transceiver52M \
diff --git a/Transceiver52M/Makefile.am b/Transceiver52M/Makefile.am
index 3f84d3d..5649d15 100644
--- a/Transceiver52M/Makefile.am
+++ b/Transceiver52M/Makefile.am
@@ -23,9 +23,9 @@
 
 SUBDIRS = arch device
 
-AM_CPPFLAGS = -Wall $(STD_DEFINES_AND_INCLUDES) -I${srcdir}/arch/common -I${srcdir}/device/common
-AM_CXXFLAGS = -lpthread $(LIBOSMOCORE_CFLAGS) $(LIBOSMOCTRL_CFLAGS) $(LIBOSMOVTY_CFLAGS)
-AM_CFLAGS = -lpthread $(LIBOSMOCORE_CFLAGS) $(LIBOSMOCTRL_CFLAGS) $(LIBOSMOVTY_CFLAGS)
+AM_CPPFLAGS = -Wall $(STD_DEFINES_AND_INCLUDES) -I${srcdir}/arch/common -I${srcdir}/device/common -I$(top_srcdir)/osmocom-bb/src/host/trxcon/include/
+AM_CXXFLAGS = -lpthread $(LIBOSMOCORE_CFLAGS) $(LIBOSMOCTRL_CFLAGS) $(LIBOSMOVTY_CFLAGS) -I$(top_srcdir)/osmocom-bb/src/host/trxcon/include/
+AM_CFLAGS = -lpthread $(LIBOSMOCORE_CFLAGS) $(LIBOSMOCTRL_CFLAGS) $(LIBOSMOVTY_CFLAGS) -I$(top_srcdir)/osmocom-bb/src/host/trxcon/include/
 
 noinst_LTLIBRARIES = libtransceiver_common.la
 
@@ -40,7 +40,9 @@
 	ChannelizerBase.cpp \
 	Channelizer.cpp \
 	Synthesis.cpp \
-	proto_trxd.c
+	proto_trxd.c \
+	grgsm_vitac/grgsm_vitac.cpp \
+	grgsm_vitac/viterbi_detector.cc
 
 libtransceiver_common_la_SOURCES = \
 	$(COMMON_SOURCES) \
@@ -61,7 +63,9 @@
 	ChannelizerBase.h \
 	Channelizer.h \
 	Synthesis.h \
-	proto_trxd.h
+	proto_trxd.h \
+	grgsm_vitac/viterbi_detector.h \
+	grgsm_vitac/constants.h
 
 COMMON_LDADD = \
 	libtransceiver_common.la \
@@ -70,9 +74,26 @@
 	$(COMMON_LA) \
 	$(FFTWF_LIBS) \
 	$(LIBOSMOCORE_LIBS) \
+	$(LIBOSMOCODING_LIBS) \
 	$(LIBOSMOCTRL_LIBS) \
 	$(LIBOSMOVTY_LIBS)
 
+TRXCON_LDADD = \
+	$(top_builddir)/osmocom-bb/src/host/trxcon/src/.libs/libl1sched.a \
+	$(top_builddir)/osmocom-bb/src/host/trxcon/src/.libs/libtrxcon.a
+
+MS_SOURCES = \
+	ms/sch.c \
+	ms/ms.cpp \
+	ms/ms_rx_lower.cpp
+
+noinst_HEADERS += \
+	ms/ms.h \
+	ms/bladerf_specific.h \
+	ms/uhd_specific.h \
+	ms/ms_upper.h \
+	ms/itrq.h
+
 bin_PROGRAMS =
 
 if DEVICE_UHD
@@ -83,6 +104,26 @@
 	$(COMMON_LDADD) \
 	$(UHD_LIBS)
 osmo_trx_uhd_CPPFLAGS  = $(AM_CPPFLAGS) $(UHD_CFLAGS)
+
+if ENABLE_MS_TRX
+bin_PROGRAMS += osmo-trx-ms-uhd
+osmo_trx_ms_uhd_SOURCES = $(MS_SOURCES) ms/ms_upper.cpp ms/l1ctl_server.c ms/logging.cpp ms/l1ctl_server_cb.cpp
+osmo_trx_ms_uhd_LDADD = \
+	$(builddir)/device/uhd/libdevice.la \
+	$(COMMON_LDADD) \
+	$(UHD_LIBS) \
+	$(TRXCON_LDADD)
+osmo_trx_ms_uhd_CPPFLAGS  = $(AM_CPPFLAGS) $(UHD_CFLAGS) -DBUILDUHD
+
+bin_PROGRAMS += osmo-trx-syncthing-uhd
+osmo_trx_syncthing_uhd_SOURCES = $(MS_SOURCES) ms/ms_rx_burst_test.cpp
+osmo_trx_syncthing_uhd_LDADD = \
+	$(builddir)/device/uhd/libdevice.la \
+	$(COMMON_LDADD) \
+	$(UHD_LIBS) \
+	$(TRXCON_LDADD)
+osmo_trx_syncthing_uhd_CPPFLAGS  = $(AM_CPPFLAGS) $(UHD_CFLAGS) -DSYNCTHINGONLY -DBUILDUHD
+endif
 endif
 
 if DEVICE_USRP1
@@ -114,6 +155,25 @@
 	$(BLADE_LIBS)
 osmo_trx_blade_CPPFLAGS  = $(AM_CPPFLAGS) $(LMS_CFLAGS)
 
+if ENABLE_MS_TRX
+bin_PROGRAMS += osmo-trx-ms-blade
+osmo_trx_ms_blade_SOURCES = $(MS_SOURCES) ms/ms_upper.cpp ms/l1ctl_server.c ms/logging.cpp ms/l1ctl_server_cb.cpp
+osmo_trx_ms_blade_LDADD = \
+	$(builddir)/device/bladerf/libdevice.la \
+	$(COMMON_LDADD) \
+	$(BLADE_LIBS) \
+	$(TRXCON_LDADD)
+osmo_trx_ms_blade_CPPFLAGS  = $(AM_CPPFLAGS) $(BLADE_CFLAGS) -DBUILDBLADE
+
+bin_PROGRAMS += osmo-trx-syncthing-blade
+osmo_trx_syncthing_blade_SOURCES = $(MS_SOURCES) ms/ms_rx_burst_test.cpp
+osmo_trx_syncthing_blade_LDADD =  \
+	$(builddir)/device/bladerf/libdevice.la \
+	$(COMMON_LDADD) \
+	$(BLADE_LIBS) \
+	$(TRXCON_LDADD)
+osmo_trx_syncthing_blade_CPPFLAGS  = $(AM_CPPFLAGS) $(BLADE_CFLAGS) -DSYNCTHINGONLY -DBUILDBLADE -I../device/ipc
+endif
 endif
 
 if DEVICE_IPC
@@ -123,5 +183,24 @@
 	$(builddir)/device/ipc/libdevice.la \
 	$(COMMON_LDADD)
 osmo_trx_ipc_CPPFLAGS  = $(AM_CPPFLAGS)
-endif
 
+# bin_PROGRAMS += osmo-trx-ipc2
+# osmo_trx_ipc2_SOURCES = osmo-trx.cpp
+# osmo_trx_ipc2_LDADD = \
+# 	$(builddir)/device/ipc2/libdevice.la \
+# 	$(COMMON_LDADD)
+# osmo_trx_ipc2_CPPFLAGS  = $(AM_CPPFLAGS)
+# if ENABLE_MS_TRX
+# bin_PROGRAMS += osmo-trx-ms-ipc
+# osmo_trx_ms_ipc_SOURCES = $(MS_SOURCES) ms/ms_upper.cpp
+# osmo_trx_ms_ipc_LDADD = \
+# 	$(COMMON_LDADD) \
+# 	$(TRXCON_LDADD)
+# osmo_trx_ms_ipc_CPPFLAGS  = $(AM_CPPFLAGS) -DBUILDIPC  -I./device/ipc2 -I../device/ipc2
+
+# bin_PROGRAMS += osmo-trx-syncthing-ipc
+# osmo_trx_syncthing_ipc_SOURCES = $(MS_SOURCES) ms/ms_rx_burst_test.cpp
+# osmo_trx_syncthing_ipc_LDADD =  $(COMMON_LDADD)
+# osmo_trx_syncthing_ipc_CPPFLAGS  = $(AM_CPPFLAGS) -DSYNCTHINGONLY -DBUILDIPC  -I./device/ipc2 -I../device/ipc2
+# endif
+endif
diff --git a/Transceiver52M/ms/bladerf_specific.h b/Transceiver52M/ms/bladerf_specific.h
new file mode 100644
index 0000000..edfb40f
--- /dev/null
+++ b/Transceiver52M/ms/bladerf_specific.h
@@ -0,0 +1,445 @@
+#pragma once
+
+/*
+ * (C) 2022 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Eric Wild <ewild@sysmocom.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "itrq.h"
+#include <atomic>
+#include <complex>
+#include <cstdint>
+#include <functional>
+#include <iostream>
+#include <cassert>
+#include <cstring>
+
+#include <libbladeRF.h>
+#include <Timeval.h>
+#include <unistd.h>
+
+const size_t BLADE_BUFFER_SIZE = 1024 * 1;
+const size_t BLADE_NUM_BUFFERS = 32 * 1;
+const size_t NUM_TRANSFERS = 16 * 2;
+const int SAMPLE_SCALE_FACTOR = 15; // actually 16 but sigproc complains about clipping..
+
+template <typename Arg, typename... Args> void expand_args(std::ostream &out, Arg &&arg, Args &&...args)
+{
+	out << '(' << std::forward<Arg>(arg);
+	(void)(int[]){ 0, (void((out << "," << std::forward<Args>(args))), 0)... };
+	out << ')' << std::endl;
+}
+
+template <class R, class... Args> using RvalFunc = R (*)(Args...);
+
+// specialisation for funcs which return a value
+template <class R, class... Args>
+R exec_and_check(RvalFunc<R, Args...> func, const char *fname, const char *finame, const char *funcname, int line,
+		 Args... args)
+{
+	R rval = func(std::forward<Args>(args)...);
+	if (rval != 0) {
+		std::cerr << ((rval >= 0) ? "OK:" : bladerf_strerror(rval)) << ':' << finame << ':' << line << ':'
+			  << funcname << ':' << fname;
+		expand_args(std::cerr, args...);
+	}
+	return rval;
+}
+
+// only macros can pass a func name string
+#define blade_check(func, ...) exec_and_check(func, #func, __FILE__, __FUNCTION__, __LINE__, __VA_ARGS__)
+
+#pragma pack(push, 1)
+using blade_sample_type = std::complex<int16_t>;
+enum class blade_speed_buffer_type { HS, SS };
+template <blade_speed_buffer_type T> struct blade_usb_message {
+	uint32_t reserved;
+	uint64_t ts;
+	uint32_t meta_flags;
+	blade_sample_type d[(T == blade_speed_buffer_type::SS ? 512 : 256) - 4];
+};
+
+static_assert(sizeof(blade_usb_message<blade_speed_buffer_type::SS>) == 2048, "blade buffer mismatch!");
+static_assert(sizeof(blade_usb_message<blade_speed_buffer_type::HS>) == 1024, "blade buffer mismatch!");
+template <unsigned int SZ, blade_speed_buffer_type T> struct blade_otw_buffer {
+	static_assert((SZ >= 2 && !(SZ % 2)), "min size is 2x usb buffer!");
+	blade_usb_message<T> m[SZ];
+	int actual_samples_per_msg()
+	{
+		return sizeof(blade_usb_message<T>::d) / sizeof(typeof(blade_usb_message<T>::d[0]));
+	}
+	int actual_samples_per_buffer()
+	{
+		return SZ * actual_samples_per_msg();
+	}
+	int samples_per_buffer()
+	{
+		return SZ * sizeof(blade_usb_message<T>) / sizeof(typeof(blade_usb_message<T>::d[0]));
+	}
+	int num_msgs_per_buffer()
+	{
+		return SZ;
+	}
+	auto get_first_ts()
+	{
+		return m[0].ts;
+	}
+	constexpr auto *getsampleoffset(int ofs)
+	{
+		auto full = ofs / actual_samples_per_msg();
+		auto rem = ofs % actual_samples_per_msg();
+		return &m[full].d[rem];
+	}
+	int readall(blade_sample_type *outaddr)
+	{
+		blade_sample_type *addr = outaddr;
+		for (int i = 0; i < SZ; i++) {
+			memcpy(addr, &m[i].d[0], actual_samples_per_msg() * sizeof(blade_sample_type));
+			addr += actual_samples_per_msg();
+		}
+		return actual_samples_per_buffer();
+	}
+	int read_n(blade_sample_type *outaddr, int start, int num)
+	{
+		assert((start + num) <= actual_samples_per_buffer());
+		assert(start >= 0);
+
+		if (!num)
+			return 0;
+
+		// which buffer?
+		int start_buf_idx = (start > 0) ? start / actual_samples_per_msg() : 0;
+		// offset from actual buffer start
+		auto start_offset_in_buf = (start - (start_buf_idx * actual_samples_per_msg()));
+		auto samp_rem_in_first_buf = actual_samples_per_msg() - start_offset_in_buf;
+		auto remaining_first_buf = num > samp_rem_in_first_buf ? samp_rem_in_first_buf : num;
+
+		memcpy(outaddr, &m[start_buf_idx].d[start_offset_in_buf],
+		       remaining_first_buf * sizeof(blade_sample_type));
+		outaddr += remaining_first_buf;
+
+		auto remaining = num - remaining_first_buf;
+
+		if (!remaining)
+			return num;
+
+		start_buf_idx++;
+
+		auto rem_full_bufs = remaining / actual_samples_per_msg();
+		remaining -= rem_full_bufs * actual_samples_per_msg();
+
+		for (int i = 0; i < rem_full_bufs; i++) {
+			memcpy(outaddr, &m[start_buf_idx++].d[0], actual_samples_per_msg() * sizeof(blade_sample_type));
+			outaddr += actual_samples_per_msg();
+		}
+
+		if (remaining)
+			memcpy(outaddr, &m[start_buf_idx].d[0], remaining * sizeof(blade_sample_type));
+		return num;
+	}
+	int write_n_burst(blade_sample_type *in, int num, uint64_t first_ts)
+	{
+		assert(num <= actual_samples_per_buffer());
+		int len_rem = num;
+		for (int i = 0; i < SZ; i++) {
+			m[i] = {};
+			m[i].ts = first_ts + i * actual_samples_per_msg();
+			if (len_rem) {
+				int max_to_copy =
+					len_rem > actual_samples_per_msg() ? actual_samples_per_msg() : len_rem;
+				memcpy(&m[i].d[0], in, max_to_copy * sizeof(blade_sample_type));
+				len_rem -= max_to_copy;
+				in += actual_samples_per_msg();
+			}
+		}
+		return num;
+	}
+};
+#pragma pack(pop)
+
+template <unsigned int SZ, blade_speed_buffer_type T> struct blade_otw_buffer_helper {
+	static_assert((SZ >= 1024 && ((SZ & (SZ - 1)) == 0)), "only buffer size multiples of 1024 allowed!");
+	static blade_otw_buffer<SZ / 512, T> x;
+};
+
+using dev_buf_t = typeof(blade_otw_buffer_helper<BLADE_BUFFER_SIZE, blade_speed_buffer_type::SS>::x);
+// using buf_in_use = blade_otw_buffer<2, blade_speed_buffer_type::SS>;
+using bh_fn_t = std::function<int(dev_buf_t *)>;
+
+template <typename T> struct blade_hw {
+	struct bladerf *dev;
+	struct bladerf_stream *rx_stream;
+	struct bladerf_stream *tx_stream;
+	// using pkt2buf = blade_otw_buffer<2, blade_speed_buffer_type::SS>;
+	using tx_buf_q_type = spsc_cond<BLADE_NUM_BUFFERS, dev_buf_t *, true, false>;
+	const unsigned int rxFullScale, txFullScale;
+	const int rxtxdelay;
+
+	float rxgain, txgain;
+
+	struct ms_trx_config {
+		int tx_freq;
+		int rx_freq;
+		int sample_rate;
+		int bandwidth;
+
+	    public:
+		ms_trx_config() : tx_freq(881e6), rx_freq(926e6), sample_rate(((1625e3 / 6) * 4)), bandwidth(1e6)
+		{
+		}
+	} cfg;
+
+	struct buf_mgmt {
+		void **rx_samples;
+		void **tx_samples;
+		tx_buf_q_type bufptrqueue;
+
+	} buf_mgmt;
+
+	virtual ~blade_hw()
+	{
+		close_device();
+	}
+	blade_hw() : rxFullScale(2047), txFullScale(2047), rxtxdelay(-60)
+	{
+	}
+
+	void close_device()
+	{
+		if (dev) {
+			if (rx_stream) {
+				bladerf_deinit_stream(rx_stream);
+			}
+
+			if (tx_stream) {
+				bladerf_deinit_stream(tx_stream);
+			}
+
+			bladerf_enable_module(dev, BLADERF_MODULE_RX, false);
+			bladerf_enable_module(dev, BLADERF_MODULE_TX, false);
+
+			bladerf_close(dev);
+			dev = NULL;
+		}
+	}
+
+	int init_device(bh_fn_t rxh, bh_fn_t txh)
+	{
+		struct bladerf_rational_rate rate = { 0, static_cast<uint64_t>((1625e3 * 4)) * 64, 6 * 64 }, actual;
+
+		bladerf_log_set_verbosity(BLADERF_LOG_LEVEL_DEBUG);
+		bladerf_set_usb_reset_on_open(true);
+		blade_check(bladerf_open, &dev, "");
+		if (!dev) {
+			std::cerr << "open failed, device missing?" << std::endl;
+			exit(0);
+		}
+		if (bladerf_device_speed(dev) != bladerf_dev_speed::BLADERF_DEVICE_SPEED_SUPER) {
+			std::cerr << "open failed, only superspeed (usb3) supported!" << std::endl;
+			return -1;
+		}
+
+		blade_check(bladerf_set_tuning_mode, dev, bladerf_tuning_mode::BLADERF_TUNING_MODE_FPGA);
+
+		bool is_locked;
+		blade_check(bladerf_set_pll_enable, dev, true);
+		blade_check(bladerf_set_pll_refclk, dev, 10000000UL);
+		for (int i = 0; i < 20; i++) {
+			usleep(50 * 1000);
+			bladerf_get_pll_lock_state(dev, &is_locked);
+
+			if (is_locked)
+				break;
+		}
+		if (!is_locked) {
+			std::cerr << "unable to lock refclk!" << std::endl;
+			return -1;
+		}
+
+		blade_check(bladerf_set_rational_sample_rate, dev, BLADERF_CHANNEL_RX(0), &rate, &actual);
+		blade_check(bladerf_set_rational_sample_rate, dev, BLADERF_CHANNEL_TX(0), &rate, &actual);
+
+		blade_check(bladerf_set_frequency, dev, BLADERF_CHANNEL_RX(0), (bladerf_frequency)cfg.rx_freq);
+		blade_check(bladerf_set_frequency, dev, BLADERF_CHANNEL_TX(0), (bladerf_frequency)cfg.tx_freq);
+
+		blade_check(bladerf_set_bandwidth, dev, BLADERF_CHANNEL_RX(0), (bladerf_bandwidth)cfg.bandwidth,
+			    (bladerf_bandwidth *)NULL);
+		blade_check(bladerf_set_bandwidth, dev, BLADERF_CHANNEL_TX(0), (bladerf_bandwidth)cfg.bandwidth,
+			    (bladerf_bandwidth *)NULL);
+
+		blade_check(bladerf_set_gain_mode, dev, BLADERF_CHANNEL_RX(0), BLADERF_GAIN_MGC);
+		blade_check(bladerf_set_gain, dev, BLADERF_CHANNEL_RX(0), (bladerf_gain)30);
+		blade_check(bladerf_set_gain, dev, BLADERF_CHANNEL_TX(0), (bladerf_gain)30);
+		usleep(1000);
+		blade_check(bladerf_enable_module, dev, BLADERF_MODULE_RX, true);
+		usleep(1000);
+		blade_check(bladerf_enable_module, dev, BLADERF_MODULE_TX, true);
+		usleep(1000);
+		blade_check(bladerf_init_stream, &rx_stream, dev, getrxcb(rxh), &buf_mgmt.rx_samples, BLADE_NUM_BUFFERS,
+			    BLADERF_FORMAT_SC16_Q11_META, BLADE_BUFFER_SIZE, NUM_TRANSFERS, (void *)this);
+
+		blade_check(bladerf_init_stream, &tx_stream, dev, gettxcb(txh), &buf_mgmt.tx_samples, BLADE_NUM_BUFFERS,
+			    BLADERF_FORMAT_SC16_Q11_META, BLADE_BUFFER_SIZE, NUM_TRANSFERS, (void *)this);
+
+		for (int i = 0; i < BLADE_NUM_BUFFERS; i++) {
+			auto cur_buffer = reinterpret_cast<tx_buf_q_type::elem_t *>(buf_mgmt.tx_samples);
+			buf_mgmt.bufptrqueue.spsc_push(&cur_buffer[i]);
+		}
+
+		setRxGain(20);
+		setTxGain(30);
+
+		usleep(1000);
+
+		// bladerf_set_stream_timeout(dev, BLADERF_TX, 4);
+		// bladerf_set_stream_timeout(dev, BLADERF_RX, 4);
+
+		return 0;
+	}
+
+	bool tuneTx(double freq, size_t chan = 0)
+	{
+		msleep(15);
+		blade_check(bladerf_set_frequency, dev, BLADERF_CHANNEL_TX(0), (bladerf_frequency)freq);
+		msleep(15);
+		return true;
+	};
+	bool tuneRx(double freq, size_t chan = 0)
+	{
+		msleep(15);
+		blade_check(bladerf_set_frequency, dev, BLADERF_CHANNEL_RX(0), (bladerf_frequency)freq);
+		msleep(15);
+		return true;
+	};
+	bool tuneRxOffset(double offset, size_t chan = 0)
+	{
+		return true;
+	};
+
+	double setRxGain(double dB, size_t chan = 0)
+	{
+		rxgain = dB;
+		msleep(15);
+		blade_check(bladerf_set_gain, dev, BLADERF_CHANNEL_RX(0), (bladerf_gain)dB);
+		msleep(15);
+		return dB;
+	};
+	double setTxGain(double dB, size_t chan = 0)
+	{
+		txgain = dB;
+		msleep(15);
+		blade_check(bladerf_set_gain, dev, BLADERF_CHANNEL_TX(0), (bladerf_gain)dB);
+		msleep(15);
+		return dB;
+	};
+	int setPowerAttenuation(int atten, size_t chan = 0)
+	{
+		return atten;
+	};
+
+	static void check_timestamp(dev_buf_t *rcd)
+	{
+		static bool first = true;
+		static uint64_t last_ts;
+		if (first) {
+			first = false;
+			last_ts = rcd->m[0].ts;
+		} else if (last_ts + rcd->actual_samples_per_buffer() != rcd->m[0].ts) {
+			std::cerr << "RX Overrun!" << last_ts << " " << rcd->actual_samples_per_buffer() << " "
+				  << last_ts + rcd->actual_samples_per_buffer() << " " << rcd->m[0].ts << std::endl;
+			last_ts = rcd->m[0].ts;
+		} else {
+			last_ts = rcd->m[0].ts;
+		}
+	}
+
+	bladerf_stream_cb getrxcb(bh_fn_t rxbh)
+	{
+		// C cb -> no capture!
+		static auto rxbhfn = rxbh;
+		return [](struct bladerf *dev, struct bladerf_stream *stream, struct bladerf_metadata *meta,
+			  void *samples, size_t num_samples, void *user_data) -> void * {
+			// struct blade_hw *trx = (struct blade_hw *)user_data;
+			static int to_skip = 0;
+			dev_buf_t *rcd = (dev_buf_t *)samples;
+
+			if (to_skip < 120) // prevents weird overflows on startup
+				to_skip++;
+			else {
+				check_timestamp(rcd);
+				rxbhfn(rcd);
+			}
+
+			return samples;
+		};
+	}
+	bladerf_stream_cb gettxcb(bh_fn_t txbh)
+	{
+		// C cb -> no capture!
+		static auto txbhfn = txbh;
+		return [](struct bladerf *dev, struct bladerf_stream *stream, struct bladerf_metadata *meta,
+			  void *samples, size_t num_samples, void *user_data) -> void * {
+			struct blade_hw *trx = (struct blade_hw *)user_data;
+			auto ptr = reinterpret_cast<tx_buf_q_type::elem_t>(samples);
+
+			if (samples) // put buffer address back into queue, ready to be reused
+				trx->buf_mgmt.bufptrqueue.spsc_push(&ptr);
+
+			return BLADERF_STREAM_NO_DATA;
+		};
+	}
+
+	auto get_rx_burst_handler_fn(bh_fn_t burst_handler)
+	{
+		auto fn = [this] {
+			int status;
+			status = bladerf_stream(rx_stream, BLADERF_RX_X1);
+			if (status < 0)
+				std::cerr << "rx stream error! " << bladerf_strerror(status) << std::endl;
+
+			return NULL;
+		};
+		return fn;
+	}
+	auto get_tx_burst_handler_fn(bh_fn_t burst_handler)
+	{
+		auto fn = [this] {
+			int status;
+			status = bladerf_stream(tx_stream, BLADERF_TX_X1);
+			if (status < 0)
+				std::cerr << "rx stream error! " << bladerf_strerror(status) << std::endl;
+
+			return NULL;
+		};
+		return fn;
+	}
+
+	void submit_burst_ts(blade_sample_type *buffer, int len, uint64_t ts)
+	{
+		//get empty bufer from list
+		tx_buf_q_type::elem_t rcd;
+
+		while (!buf_mgmt.bufptrqueue.spsc_pop(&rcd))
+			buf_mgmt.bufptrqueue.spsc_prep_pop();
+		assert(rcd != nullptr);
+
+		rcd->write_n_burst(buffer, len, ts + rxtxdelay); // blade xa4 specific delay!
+		blade_check(bladerf_submit_stream_buffer_nb, tx_stream, (void *)rcd);
+	}
+};
diff --git a/Transceiver52M/ms/ipc_specific.h b/Transceiver52M/ms/ipc_specific.h
new file mode 100644
index 0000000..c79e743
--- /dev/null
+++ b/Transceiver52M/ms/ipc_specific.h
@@ -0,0 +1,197 @@
+#pragma once
+
+/*
+ * (C) 2022 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Eric Wild <ewild@sysmocom.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <cassert>
+#include <complex>
+#include <cstring>
+#include <functional>
+#include <iostream>
+#include <thread>
+
+#include <Timeval.h>
+#include <vector>
+
+// #define MTX_LOG_ENABLED
+#include <ipcif.h>
+
+// typedef unsigned long long TIMESTAMP;
+using blade_sample_type = std::complex<int16_t>;
+const int SAMPLE_SCALE_FACTOR = 1;
+
+struct uhd_buf_wrap {
+	uint64_t ts;
+	uint32_t num_samps;
+	blade_sample_type *buf;
+	auto actual_samples_per_buffer()
+	{
+		return num_samps;
+	}
+	long get_first_ts()
+	{
+		return ts; //md->time_spec.to_ticks(rxticks);
+	}
+	int readall(blade_sample_type *outaddr)
+	{
+		memcpy(outaddr, buf, num_samps * sizeof(blade_sample_type));
+		return num_samps;
+	}
+	int read_n(blade_sample_type *outaddr, int start, int num)
+	{
+		// assert(start >= 0);
+		auto to_read = std::min((int)num_samps - start, num);
+		// assert(to_read >= 0);
+		memcpy(outaddr, buf + start, to_read * sizeof(blade_sample_type));
+		return to_read;
+	}
+};
+
+using dev_buf_t = uhd_buf_wrap;
+using bh_fn_t = std::function<int(dev_buf_t *)>;
+
+template <typename T> struct ipc_hw {
+	// uhd::usrp::multi_usrp::sptr dev;
+	// uhd::rx_streamer::sptr rx_stream;
+	// uhd::tx_streamer::sptr tx_stream;
+	blade_sample_type *one_pkt_buf;
+	std::vector<blade_sample_type *> pkt_ptrs;
+	size_t rx_spp;
+	double rxticks;
+	const unsigned int rxFullScale, txFullScale;
+	const int rxtxdelay;
+	float rxgain, txgain;
+	trxmsif m;
+
+	virtual ~ipc_hw()
+	{
+		delete[] one_pkt_buf;
+	}
+	ipc_hw() : rxFullScale(32767), txFullScale(32767), rxtxdelay(0)
+	{
+	}
+
+	bool tuneTx(double freq, size_t chan = 0)
+	{
+		msleep(25);
+		// dev->set_tx_freq(freq, chan);
+		msleep(25);
+		return true;
+	};
+	bool tuneRx(double freq, size_t chan = 0)
+	{
+		msleep(25);
+		// dev->set_rx_freq(freq, chan);
+		msleep(25);
+		return true;
+	};
+	bool tuneRxOffset(double offset, size_t chan = 0)
+	{
+		return true;
+	};
+
+	double setRxGain(double dB, size_t chan = 0)
+	{
+		rxgain = dB;
+		msleep(25);
+		// dev->set_rx_gain(dB, chan);
+		msleep(25);
+		return dB;
+	};
+	double setTxGain(double dB, size_t chan = 0)
+	{
+		txgain = dB;
+		msleep(25);
+		// dev->set_tx_gain(dB, chan);
+		msleep(25);
+		return dB;
+	};
+	int setPowerAttenuation(int atten, size_t chan = 0)
+	{
+		return atten;
+	};
+
+	int init_device(bh_fn_t rxh, bh_fn_t txh)
+	{
+		return m.connect() ? 0 : -1;
+	}
+
+	void *rx_cb(bh_fn_t burst_handler)
+	{
+		void *ret;
+		static int to_skip = 0;
+		static uint64_t last_ts;
+
+		blade_sample_type pbuf[508 * 2];
+
+		uint64_t t;
+
+		int len = 508 * 2;
+		m.read_dl(508 * 2, &t, pbuf);
+		dev_buf_t rcd = { t, static_cast<uint32_t>(len), pbuf };
+
+		if (to_skip < 120) // prevents weird overflows on startup
+			to_skip++;
+		else {
+			assert(last_ts != rcd.get_first_ts());
+			burst_handler(&rcd);
+			last_ts = rcd.get_first_ts();
+		}
+
+		m.drive_tx();
+
+		return ret;
+	}
+
+	auto get_rx_burst_handler_fn(bh_fn_t burst_handler)
+	{
+		auto fn = [this, burst_handler] {
+			pthread_setname_np(pthread_self(), "rxrun");
+
+			while (1) {
+				rx_cb(burst_handler);
+			}
+		};
+		return fn;
+	}
+	auto get_tx_burst_handler_fn(bh_fn_t burst_handler)
+	{
+		auto fn = [] {
+			// wait_for_shm_open();
+			// dummy
+		};
+		return fn;
+	}
+	void submit_burst_ts(blade_sample_type *buffer, int len, uint64_t ts)
+	{
+		// FIXME: missing
+	}
+
+	void set_name_aff_sched(const char *name, int cpunum, int schedtype, int prio)
+	{
+		pthread_setname_np(pthread_self(), name);
+
+	}
+	void signal_start()
+	{
+		m.signal_read_start();
+	}
+};
diff --git a/Transceiver52M/ms/itrq.h b/Transceiver52M/ms/itrq.h
new file mode 100644
index 0000000..62ee848
--- /dev/null
+++ b/Transceiver52M/ms/itrq.h
@@ -0,0 +1,213 @@
+#pragma once
+/*
+ * (C) 2022 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Eric Wild <ewild@sysmocom.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <atomic>
+
+#include <condition_variable>
+#include <mutex>
+#include <sys/eventfd.h>
+#include <unistd.h>
+
+#include <stdatomic.h>
+#include <stdbool.h>
+#include <stdlib.h>
+
+/*
+classic lamport circular lockfree spsc queue:
+every "side" only writes its own ptr, but may read the other sides ptr
+
+notify reader using eventfd as soon as element is added, reader then reads until
+read fails
+-> reader pops in a loop until FALSE and might get spurious events because it
+read before it was notified, which is fine
+-> writing pushes *the same data* in a loop until TRUE, blocks
+
+shutting this down requires
+1) to stop reading and pushing
+2) ONE side to take care of the eventfds
+*/
+
+namespace spsc_detail
+{
+template <bool block_read, bool block_write> class spsc_cond_detail {
+	std::condition_variable cond_r, cond_w;
+	std::mutex lr, lw;
+	std::atomic_int r_flag, w_flag;
+
+    public:
+	explicit spsc_cond_detail() : r_flag(0), w_flag(0)
+	{
+	}
+
+	~spsc_cond_detail()
+	{
+	}
+
+	ssize_t spsc_check_r()
+	{
+		std::unique_lock<std::mutex> lk(lr);
+		while (r_flag == 0)
+			cond_r.wait(lk);
+		r_flag--;
+		return 1;
+	}
+	ssize_t spsc_check_w()
+	{
+		std::unique_lock<std::mutex> lk(lw);
+		while (w_flag == 0)
+			cond_w.wait(lk);
+		w_flag--;
+		return 1;
+	}
+	void spsc_notify_r()
+	{
+		std::unique_lock<std::mutex> lk(lr);
+		r_flag++;
+		cond_r.notify_one();
+	}
+	void spsc_notify_w()
+	{
+		std::unique_lock<std::mutex> lk(lw);
+		w_flag++;
+		cond_w.notify_one();
+	}
+};
+
+// originally designed for select loop integration
+template <bool block_read, bool block_write> class spsc_efd_detail {
+	int efd_r, efd_w; /* eventfds used to block/notify readers/writers */
+
+    public:
+	explicit spsc_efd_detail()
+		: efd_r(eventfd(0, block_read ? 0 : EFD_NONBLOCK)), efd_w(eventfd(1, block_write ? 0 : EFD_NONBLOCK))
+	{
+	}
+
+	~spsc_efd_detail()
+	{
+		close(efd_r);
+		close(efd_w);
+	}
+
+	ssize_t spsc_check_r()
+	{
+		uint64_t efdr;
+		return read(efd_r, &efdr, sizeof(uint64_t));
+	}
+	ssize_t spsc_check_w()
+	{
+		uint64_t efdr;
+		return read(efd_w, &efdr, sizeof(uint64_t));
+	}
+	void spsc_notify_r()
+	{
+		uint64_t efdu = 1;
+		write(efd_r, &efdu, sizeof(uint64_t));
+	}
+	void spsc_notify_w()
+	{
+		uint64_t efdu = 1;
+		write(efd_w, &efdu, sizeof(uint64_t));
+	}
+	int get_r_efd()
+	{
+		return efd_r;
+	}
+	int get_w_efd()
+	{
+		return efd_w;
+	}
+};
+
+template <unsigned int SZ, typename ELEM, bool block_read, bool block_write, template <bool, bool> class T>
+class spsc : public T<block_read, block_write> {
+	static_assert(SZ > 0, "queues need a size...");
+	std::atomic<unsigned int> readptr;
+	std::atomic<unsigned int> writeptr;
+
+	ELEM buf[SZ];
+
+    public:
+	using base_t = T<block_read, block_write>;
+	using elem_t = ELEM;
+	explicit spsc() : readptr(0), writeptr(0)
+	{
+	}
+
+	~spsc()
+	{
+	}
+
+	/*! Adds element to the queue by copying the data.
+ *  \param[in] elem input buffer, must match the originally configured queue buffer size!.
+ *  \returns true if queue was not full and element was successfully pushed */
+	bool spsc_push(const ELEM *elem)
+	{
+		size_t cur_wp, cur_rp;
+		cur_wp = writeptr.load(std::memory_order_relaxed);
+		cur_rp = readptr.load(std::memory_order_acquire);
+		if ((cur_wp + 1) % SZ == cur_rp) {
+			if (block_write)
+				base_t::spsc_check_w(); /* blocks, ensures next (!) call succeeds */
+			return false;
+		}
+		buf[cur_wp] = *elem;
+		writeptr.store((cur_wp + 1) % SZ, std::memory_order_release);
+		if (block_read)
+			base_t::spsc_notify_r(); /* fine after release */
+		return true;
+	}
+
+	/*! Removes element from the queue by copying the data.
+ *  \param[in] elem output buffer, must match the originally configured queue buffer size!.
+ *  \returns true if queue was not empty and element was successfully removed */
+	bool spsc_pop(ELEM *elem)
+	{
+		size_t cur_wp, cur_rp;
+		cur_wp = writeptr.load(std::memory_order_acquire);
+		cur_rp = readptr.load(std::memory_order_relaxed);
+
+		if (cur_wp == cur_rp) /* blocks via prep_pop */
+			return false;
+
+		*elem = buf[cur_rp];
+		readptr.store((cur_rp + 1) % SZ, std::memory_order_release);
+		if (block_write)
+			base_t::spsc_notify_w();
+		return true;
+	}
+
+	/*! Reads the read-fd of the queue, which, depending on settings passed on queue creation, blocks.
+    * This function can be used to deliberately wait for a non-empty queue on the read side.
+    *  \returns result of reading the fd. */
+	ssize_t spsc_prep_pop()
+	{
+		return base_t::spsc_check_r();
+	}
+};
+
+} // namespace spsc_detail
+
+template <unsigned int SZ, typename ELEM, bool block_read, bool block_write>
+class spsc_evfd : public spsc_detail::spsc<SZ, ELEM, block_read, block_write, spsc_detail::spsc_efd_detail> {};
+template <unsigned int SZ, typename ELEM, bool block_read, bool block_write>
+class spsc_cond : public spsc_detail::spsc<SZ, ELEM, block_read, block_write, spsc_detail::spsc_cond_detail> {};
\ No newline at end of file
diff --git a/Transceiver52M/ms/l1ctl_server.c b/Transceiver52M/ms/l1ctl_server.c
new file mode 100644
index 0000000..9e9083f
--- /dev/null
+++ b/Transceiver52M/ms/l1ctl_server.c
@@ -0,0 +1,275 @@
+/*
+ * OsmocomBB <-> SDR connection bridge
+ * UNIX socket server for L1CTL
+ *
+ * (C) 2013 by Sylvain Munaut <tnt@246tNt.com>
+ * (C) 2016-2017 by Vadim Yanitskiy <axilirator@gmail.com>
+ * (C) 2022 by by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * This program 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <sys/un.h>
+#include <arpa/inet.h>
+#include <sys/socket.h>
+
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/select.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/core/write_queue.h>
+
+#include <osmocom/bb/trxcon/logging.h>
+#include <osmocom/bb/trxcon/l1ctl_server.h>
+
+#define LOGP_CLI(cli, cat, level, fmt, args...) LOGP(cat, level, "%s" fmt, (cli)->log_prefix, ##args)
+
+static int l1ctl_client_read_cb(struct osmo_fd *ofd)
+{
+	struct l1ctl_client *client = (struct l1ctl_client *)ofd->data;
+	struct msgb *msg;
+	uint16_t len;
+	int rc;
+
+	/* Attempt to read from socket */
+	rc = read(ofd->fd, &len, L1CTL_MSG_LEN_FIELD);
+	if (rc != L1CTL_MSG_LEN_FIELD) {
+		if (rc <= 0) {
+			LOGP_CLI(client, DL1D, LOGL_NOTICE, "L1CTL connection error: read() failed (rc=%d): %s\n", rc,
+				 strerror(errno));
+		} else {
+			LOGP_CLI(client, DL1D, LOGL_NOTICE, "L1CTL connection error: short read\n");
+			rc = -EIO;
+		}
+		l1ctl_client_conn_close(client);
+		return rc;
+	}
+
+	/* Check message length */
+	len = ntohs(len);
+	if (len > L1CTL_LENGTH) {
+		LOGP_CLI(client, DL1D, LOGL_ERROR, "Length is too big: %u\n", len);
+		return -EINVAL;
+	}
+
+	/* Allocate a new msg */
+	msg = msgb_alloc_headroom(L1CTL_LENGTH + L1CTL_HEADROOM, L1CTL_HEADROOM, "l1ctl_rx_msg");
+	if (!msg) {
+		LOGP_CLI(client, DL1D, LOGL_ERROR, "Failed to allocate msg\n");
+		return -ENOMEM;
+	}
+
+	msg->l1h = msgb_put(msg, len);
+	rc = read(ofd->fd, msg->l1h, msgb_l1len(msg));
+	if (rc != len) {
+		LOGP_CLI(client, DL1D, LOGL_ERROR, "Can not read data: len=%d < rc=%d: %s\n", len, rc, strerror(errno));
+		msgb_free(msg);
+		return rc;
+	}
+
+	/* Debug print */
+	LOGP_CLI(client, DL1D, LOGL_DEBUG, "RX: '%s'\n", osmo_hexdump(msg->data, msg->len));
+
+	/* Call L1CTL handler */
+	client->server->cfg->conn_read_cb(client, msg);
+
+	return 0;
+}
+
+static int l1ctl_client_write_cb(struct osmo_fd *ofd, struct msgb *msg)
+{
+	struct l1ctl_client *client = (struct l1ctl_client *)ofd->data;
+	int len;
+
+	if (ofd->fd <= 0)
+		return -EINVAL;
+
+	len = write(ofd->fd, msg->data, msg->len);
+	if (len != msg->len) {
+		LOGP_CLI(client, DL1D, LOGL_ERROR, "Failed to write data: written (%d) < msg_len (%d)\n", len,
+			 msg->len);
+		return -1;
+	}
+
+	return 0;
+}
+
+/* Connection handler */
+static int l1ctl_server_conn_cb(struct osmo_fd *sfd, unsigned int flags)
+{
+	struct l1ctl_server *server = (struct l1ctl_server *)sfd->data;
+	struct l1ctl_client *client;
+	int rc, client_fd;
+
+	client_fd = accept(sfd->fd, NULL, NULL);
+	if (client_fd < 0) {
+		LOGP(DL1C, LOGL_ERROR,
+		     "Failed to accept() a new connection: "
+		     "%s\n",
+		     strerror(errno));
+		return client_fd;
+	}
+
+	if (server->cfg->num_clients_max > 0 /* 0 means unlimited */ &&
+	    server->num_clients >= server->cfg->num_clients_max) {
+		LOGP(DL1C, LOGL_NOTICE,
+		     "L1CTL server cannot accept more "
+		     "than %u connection(s)\n",
+		     server->cfg->num_clients_max);
+		close(client_fd);
+		return -ENOMEM;
+	}
+
+	client = talloc_zero(server, struct l1ctl_client);
+	if (client == NULL) {
+		LOGP(DL1C, LOGL_ERROR, "Failed to allocate an L1CTL client\n");
+		close(client_fd);
+		return -ENOMEM;
+	}
+
+	/* Init the client's write queue */
+	osmo_wqueue_init(&client->wq, 100);
+	INIT_LLIST_HEAD(&client->wq.bfd.list);
+
+	client->wq.write_cb = &l1ctl_client_write_cb;
+	client->wq.read_cb = &l1ctl_client_read_cb;
+	osmo_fd_setup(&client->wq.bfd, client_fd, OSMO_FD_READ, &osmo_wqueue_bfd_cb, client, 0);
+
+	/* Register the client's write queue */
+	rc = osmo_fd_register(&client->wq.bfd);
+	if (rc != 0) {
+		LOGP(DL1C, LOGL_ERROR, "Failed to register a new connection fd\n");
+		close(client->wq.bfd.fd);
+		talloc_free(client);
+		return rc;
+	}
+
+	llist_add_tail(&client->list, &server->clients);
+	client->id = server->next_client_id++;
+	client->server = server;
+	server->num_clients++;
+
+	LOGP(DL1C, LOGL_NOTICE, "L1CTL server got a new connection (id=%u)\n", client->id);
+
+	if (client->server->cfg->conn_accept_cb != NULL)
+		client->server->cfg->conn_accept_cb(client);
+
+	return 0;
+}
+
+int l1ctl_client_send(struct l1ctl_client *client, struct msgb *msg)
+{
+	uint8_t *len;
+
+	/* Debug print */
+	LOGP_CLI(client, DL1D, LOGL_DEBUG, "TX: '%s'\n", osmo_hexdump(msg->data, msg->len));
+
+	if (msg->l1h != msg->data)
+		LOGP_CLI(client, DL1D, LOGL_INFO, "Message L1 header != Message Data\n");
+
+	/* Prepend 16-bit length before sending */
+	len = msgb_push(msg, L1CTL_MSG_LEN_FIELD);
+	osmo_store16be(msg->len - L1CTL_MSG_LEN_FIELD, len);
+
+	if (osmo_wqueue_enqueue(&client->wq, msg) != 0) {
+		LOGP_CLI(client, DL1D, LOGL_ERROR, "Failed to enqueue msg!\n");
+		msgb_free(msg);
+		return -EIO;
+	}
+
+	return 0;
+}
+
+void l1ctl_client_conn_close(struct l1ctl_client *client)
+{
+	struct l1ctl_server *server = client->server;
+
+	LOGP_CLI(client, DL1C, LOGL_NOTICE, "Closing L1CTL connection\n");
+
+	if (server->cfg->conn_close_cb != NULL)
+		server->cfg->conn_close_cb(client);
+
+	/* Close connection socket */
+	osmo_fd_unregister(&client->wq.bfd);
+	close(client->wq.bfd.fd);
+	client->wq.bfd.fd = -1;
+
+	/* Clear pending messages */
+	osmo_wqueue_clear(&client->wq);
+
+	client->server->num_clients--;
+	llist_del(&client->list);
+	talloc_free(client);
+
+	/* If this was the last client, reset the client IDs generator to 0.
+	 * This way avoid assigning huge unreadable client IDs like 26545. */
+	if (llist_empty(&server->clients))
+		server->next_client_id = 0;
+}
+
+struct l1ctl_server *l1ctl_server_alloc(void *ctx, const struct l1ctl_server_cfg *cfg)
+{
+	struct l1ctl_server *server;
+	int rc;
+
+	LOGP(DL1C, LOGL_NOTICE, "Init L1CTL server (sock_path=%s)\n", cfg->sock_path);
+
+	server = talloc(ctx, struct l1ctl_server);
+	OSMO_ASSERT(server != NULL);
+
+	*server = (struct l1ctl_server){
+		.clients = LLIST_HEAD_INIT(server->clients),
+		.cfg = cfg,
+	};
+
+	/* conn_read_cb shall not be NULL */
+	OSMO_ASSERT(cfg->conn_read_cb != NULL);
+
+	/* Bind connection handler */
+	osmo_fd_setup(&server->ofd, -1, OSMO_FD_READ, &l1ctl_server_conn_cb, server, 0);
+
+	rc = osmo_sock_unix_init_ofd(&server->ofd, SOCK_STREAM, 0, cfg->sock_path, OSMO_SOCK_F_BIND);
+	if (rc < 0) {
+		LOGP(DL1C, LOGL_ERROR, "Could not create UNIX socket: %s\n", strerror(errno));
+		talloc_free(server);
+		return NULL;
+	}
+
+	return server;
+}
+
+void l1ctl_server_free(struct l1ctl_server *server)
+{
+	LOGP(DL1C, LOGL_NOTICE, "Shutdown L1CTL server\n");
+
+	/* Close all client connections */
+	while (!llist_empty(&server->clients)) {
+		struct l1ctl_client *client = llist_entry(server->clients.next, struct l1ctl_client, list);
+		l1ctl_client_conn_close(client);
+	}
+
+	/* Unbind listening socket */
+	if (server->ofd.fd != -1) {
+		osmo_fd_unregister(&server->ofd);
+		close(server->ofd.fd);
+		server->ofd.fd = -1;
+	}
+
+	talloc_free(server);
+}
diff --git a/Transceiver52M/ms/l1ctl_server_cb.cpp b/Transceiver52M/ms/l1ctl_server_cb.cpp
new file mode 100644
index 0000000..5013871
--- /dev/null
+++ b/Transceiver52M/ms/l1ctl_server_cb.cpp
@@ -0,0 +1,77 @@
+/*
+ * (C) 2022 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Eric Wild <ewild@sysmocom.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+extern "C" {
+#include <osmocom/bb/trxcon/trxcon.h>
+#include <osmocom/bb/trxcon/trxcon_fsm.h>
+#include <osmocom/bb/trxcon/l1ctl_server.h>
+}
+
+static struct l1ctl_server_cfg server_cfg;
+static struct l1ctl_server *server = NULL;
+namespace trxcon
+{
+extern struct trxcon_inst *g_trxcon;
+}
+
+static int l1ctl_rx_cb(struct l1ctl_client *l1c, struct msgb *msg)
+{
+	struct trxcon_inst *trxcon = (struct trxcon_inst *)l1c->priv;
+
+	return trxcon_l1ctl_receive(trxcon, msg);
+}
+
+static void l1ctl_conn_accept_cb(struct l1ctl_client *l1c)
+{
+	l1c->log_prefix = talloc_strdup(l1c, trxcon::g_trxcon->log_prefix);
+	l1c->priv = trxcon::g_trxcon;
+	trxcon::g_trxcon->l2if = l1c;
+}
+
+static void l1ctl_conn_close_cb(struct l1ctl_client *l1c)
+{
+	struct trxcon_inst *trxcon = (struct trxcon_inst *)l1c->priv;
+
+	if (trxcon == NULL || trxcon->fi == NULL)
+		return;
+
+	osmo_fsm_inst_dispatch(trxcon->fi, TRXCON_EV_L2IF_FAILURE, NULL);
+}
+
+namespace trxcon
+{
+void trxc_l1ctl_init(void *tallctx)
+{
+	/* Start the L1CTL server */
+	server_cfg = (struct l1ctl_server_cfg){
+		.sock_path = "/tmp/osmocom_l2",
+		.num_clients_max = 1,
+		.conn_read_cb = &l1ctl_rx_cb,
+		.conn_accept_cb = &l1ctl_conn_accept_cb,
+		.conn_close_cb = &l1ctl_conn_close_cb,
+	};
+
+	server = l1ctl_server_alloc(tallctx, &server_cfg);
+	if (server == NULL) {
+		return;
+	}
+}
+} // namespace trxcon
\ No newline at end of file
diff --git a/Transceiver52M/ms/logging.cpp b/Transceiver52M/ms/logging.cpp
new file mode 100644
index 0000000..0857c65
--- /dev/null
+++ b/Transceiver52M/ms/logging.cpp
@@ -0,0 +1,103 @@
+/*
+ * (C) 2022 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Eric Wild <ewild@sysmocom.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+extern "C" {
+#include <osmocom/core/application.h>
+#include <osmocom/bb/trxcon/logging.h>
+#include <osmocom/bb/trxcon/trxcon.h>
+#include <osmocom/bb/l1sched/l1sched.h>
+
+}
+static const int trxcon_log_cfg[] = {
+    [TRXCON_LOGC_FSM] = DAPP,
+    [TRXCON_LOGC_L1C] = DL1C,
+    [TRXCON_LOGC_L1D] = DL1D,
+    [TRXCON_LOGC_SCHC] = DSCH,
+    [TRXCON_LOGC_SCHD] = DSCHD,
+};
+
+static struct log_info_cat trxcon_log_info_cat[] = {
+	[DAPP] = {
+		.name = "DAPP",
+		.color = "\033[1;35m",
+		.description = "Application",
+		.loglevel = LOGL_NOTICE, .enabled = 1,
+	},
+	[DL1C] = {
+		.name = "DL1C",
+		.color = "\033[1;31m",
+		.description = "Layer 1 control interface",
+		.loglevel = LOGL_NOTICE, .enabled = 1,
+	},
+	[DL1D] = {
+		.name = "DL1D",
+		.color = "\033[1;31m",
+		.description = "Layer 1 data",
+        .loglevel = LOGL_NOTICE,
+		.enabled = 1,
+	},
+	[DTRXC] = {
+		.name = "DTRXC",
+		.color = "\033[1;33m",
+		.description = "Transceiver control interface",
+        .loglevel = LOGL_NOTICE,
+		.enabled = 1,
+	},
+	[DTRXD] = {
+		.name = "DTRXD",
+		.color = "\033[1;33m",
+		.description = "Transceiver data interface",
+        .loglevel = LOGL_NOTICE,
+		.enabled = 1,
+	},
+	[DSCH] = {
+		.name = "DSCH",
+		.color = "\033[1;36m",
+		.description = "Scheduler management",
+        .loglevel = LOGL_NOTICE,
+		.enabled = 0,
+	},
+	[DSCHD] = {
+		.name = "DSCHD",
+		.color = "\033[1;36m",
+		.description = "Scheduler data",
+        .loglevel = LOGL_NOTICE,
+		.enabled = 0, 
+	},
+};
+
+static struct log_info trxcon_log_info = {
+	.cat = trxcon_log_info_cat,
+	.num_cat = ARRAY_SIZE(trxcon_log_info_cat),
+};
+
+namespace trxcon
+{
+
+void trxc_log_init(void *tallctx)
+{
+	osmo_init_logging2(tallctx, &trxcon_log_info);
+	// log_parse_category_mask(osmo_stderr_target, "");
+
+	trxcon_set_log_cfg(&trxcon_log_cfg[0], ARRAY_SIZE(trxcon_log_cfg));
+}
+
+} // namespace trxcon
diff --git a/Transceiver52M/ms/ms.cpp b/Transceiver52M/ms/ms.cpp
new file mode 100644
index 0000000..3a62ed0
--- /dev/null
+++ b/Transceiver52M/ms/ms.cpp
@@ -0,0 +1,339 @@
+
+/*
+ * (C) 2022 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Eric Wild <ewild@sysmocom.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include "GSMCommon.h"
+#include <atomic>
+#include <cassert>
+#include <complex>
+#include <iostream>
+#include <cstdlib>
+#include <cstdio>
+#include <thread>
+#include <fstream>
+
+#include "sigProcLib.h"
+
+#include "ms.h"
+#include "ms_rx_burst.h"
+#include "grgsm_vitac/grgsm_vitac.h"
+
+extern "C" {
+#include "sch.h"
+#include "convolve.h"
+#include "convert.h"
+}
+
+dummylog ms_trx::dummy_log;
+
+#ifdef DBGXX
+const int offsetrange = 200;
+const int offset_start = -15;
+static int offset_ctr = 0;
+#endif
+
+void tx_test(ms_trx *t, ts_hitter_q_t *q, unsigned int *tsc)
+{
+	sched_param sch_params;
+	sch_params.sched_priority = sched_get_priority_max(SCHED_FIFO);
+	pthread_setschedparam(pthread_self(), SCHED_FIFO, &sch_params);
+
+	auto burst = genRandAccessBurst(0, 4, 0);
+	scaleVector(*burst, t->txFullScale * 0.7);
+
+	// float -> int16
+	blade_sample_type burst_buf[burst->size()];
+	convert_and_scale<int16_t, float>(burst_buf, burst->begin(), burst->size() * 2, 1);
+
+	while (1) {
+		GSM::Time target;
+		while (!q->spsc_pop(&target)) {
+			q->spsc_prep_pop();
+		}
+
+		std::cerr << std::endl << "\x1B[32m hitting " << target.FN() << "\033[0m" << std::endl;
+
+		int timing_advance = 0;
+		int64_t now_ts;
+		GSM::Time now_time;
+		target.incTN(3); // ul dl offset
+		int target_fn = target.FN();
+		int target_tn = target.TN();
+		t->timekeeper.get_both(&now_time, &now_ts);
+
+		auto diff_fn = GSM::FNDelta(target_fn, now_time.FN());
+		int diff_tn = (target_tn - (int)now_time.TN()) % 8;
+		auto tosend = GSM::Time(diff_fn, 0);
+
+		if (diff_tn > 0)
+			tosend.incTN(diff_tn);
+		else if (diff_tn < 0)
+			tosend.decTN(-diff_tn);
+
+		// in thory fn equal and tn+3 equal is also a problem...
+		if (diff_fn < 0 || (diff_fn == 0 && (now_time.TN() - target_tn < 1))) {
+			std::cerr << "## TX too late?! fn DIFF:" << diff_fn << " tn LOCAL: " << now_time.TN()
+				  << " tn OTHER: " << target_tn << std::endl;
+			return;
+		}
+
+		auto check = now_time + tosend;
+		int64_t send_ts =
+			now_ts + tosend.FN() * 8 * ONE_TS_BURST_LEN + tosend.TN() * ONE_TS_BURST_LEN - timing_advance;
+
+#ifdef DBGXX
+		std::cerr << "## fn DIFF: " << diff_fn << " ## tn DIFF: " << diff_tn << " tn LOCAL: " << now_time.TN()
+			  << " tn OTHER: " << target_tn << " tndiff" << diff_tn << " tosend:" << tosend.FN() << ":"
+			  << tosend.TN() << " calc: " << check.FN() << ":" << check.TN() << " target: " << target.FN()
+			  << ":" << target.TN() << " ts now: " << now_ts << " target ts:" << send_ts << std::endl;
+#endif
+
+		unsigned int pad = 4 * 25;
+		blade_sample_type buf2[burst->size() + pad];
+		memset(buf2, 0, pad * sizeof(blade_sample_type));
+		memcpy(&buf2[pad], burst_buf, burst->size() * sizeof(blade_sample_type));
+
+		assert(target.FN() == check.FN());
+		assert(target.TN() == check.TN());
+		assert(target.FN() % 51 == 21);
+#ifdef DBGXX
+		auto this_offset = offset_start + (offset_ctr++ % offsetrange);
+		std::cerr << "-- O " << this_offset << std::endl;
+		send_ts = now_ts - timing_advance +
+			  ((target.FN() * 8 + (int)target.TN()) - (now_time.FN() * 8 + (int)now_time.TN())) *
+				  ONE_TS_BURST_LEN;
+#endif
+		t->submit_burst_ts(buf2, burst->size() + pad, send_ts - pad);
+#ifdef DBGXX
+		signalVector test(burst->size() + pad);
+		convert_and_scale<float, int16_t>(test.begin(), buf2, burst->size() * 2 + pad, 1.f / float(scale));
+		estim_burst_params ebp;
+		auto det = detectAnyBurst(test, 0, 4, 4, CorrType::RACH, 40, &ebp);
+		if (det > 0)
+			std::cerr << "## Y " << ebp.toa << std::endl;
+		else
+			std::cerr << "## NOOOOOOOOO " << ebp.toa << std::endl;
+#endif
+	}
+}
+#ifdef SYNCTHINGONLY
+template <typename A> auto parsec(std::vector<std::string> &v, A &itr, std::string arg, bool *rv)
+{
+	if (*itr == arg) {
+		*rv = true;
+		return true;
+	}
+	return false;
+}
+
+template <typename A, typename B, typename C>
+bool parsec(std::vector<std::string> &v, A &itr, std::string arg, B f, C *rv)
+{
+	if (*itr == arg) {
+		itr++;
+		if (itr != v.end()) {
+			*rv = f(itr->c_str());
+			return true;
+		}
+	}
+	return false;
+}
+template <typename A> bool parsec(std::vector<std::string> &v, A &itr, std::string arg, int scale, int *rv)
+{
+	return parsec(
+		v, itr, arg, [scale](const char *v) -> auto{ return atoi(v) * scale; }, rv);
+}
+template <typename A> bool parsec(std::vector<std::string> &v, A &itr, std::string arg, int scale, unsigned int *rv)
+{
+	return parsec(
+		v, itr, arg, [scale](const char *v) -> auto{ return atoi(v) * scale; }, rv);
+}
+
+int main(int argc, char *argv[])
+{
+	cpu_set_t cpuset;
+
+	CPU_ZERO(&cpuset);
+	CPU_SET(2, &cpuset);
+
+	auto rv = pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);
+	if (rv < 0) {
+		std::cerr << "affinity: errreur! " << std::strerror(errno);
+		return 0;
+	}
+
+	unsigned int default_tx_freq(881000 * 1000), default_rx_freq(926000 * 1000);
+	unsigned int grx = 20, gtx = 20;
+	bool tx_flag = false;
+	pthread_setname_np(pthread_self(), "main");
+	convolve_init();
+	convert_init();
+	sigProcLibSetup();
+	initvita();
+
+	int status = 0;
+	auto trx = new ms_trx();
+	trx->do_auto_gain = true;
+
+	std::vector<std::string> args(argv + 1, argv + argc);
+	for (auto i = args.begin(); i != args.end(); ++i) {
+		parsec(args, i, "-r", 1000, &default_rx_freq);
+		parsec(args, i, "-t", 1000, &default_tx_freq);
+		parsec(args, i, "-gr", 1, &grx);
+		parsec(args, i, "-gt", 1, &gtx);
+		parsec(args, i, "-tx", &tx_flag);
+	}
+
+	std::cerr << "usage: " << argv[0] << " <rxfreq in khz, i.e. 926000> [txfreq in khz, i.e. 881000] [TX]"
+		  << std::endl
+		  << "rx" << (argc == 1 ? " (default) " : " ") << default_rx_freq << "hz, tx " << default_tx_freq
+		  << "hz" << std::endl
+		  << "gain rx " << grx << " gain tx " << gtx << std::endl
+		  << (tx_flag ? "##!!## RACH TX ACTIVE ##!!##" : "-- no rach tx --") << std::endl;
+
+	status = trx->init_dev_and_streams();
+	if (status < 0)
+		return status;
+	trx->tuneRx(default_rx_freq);
+	trx->tuneTx(default_tx_freq);
+	trx->setRxGain(grx);
+	trx->setTxGain(gtx);
+
+	if (status == 0) {
+		// FIXME: hacks! needs exit flag for detached threads!
+
+		std::thread(rcv_bursts_test, &trx->rxqueue, &trx->mTSC, trx->rxFullScale).detach();
+		if (tx_flag)
+			std::thread(tx_test, trx, &trx->ts_hitter_q, &trx->mTSC).detach();
+		trx->start();
+		do {
+			sleep(1);
+		} while (1);
+
+		trx->stop_threads();
+	}
+	delete trx;
+
+	return status;
+}
+#endif
+
+int ms_trx::init_dev_and_streams()
+{
+	int status = 0;
+	status = base::init_device(rx_bh(), tx_bh());
+	if (status < 0) {
+		std::cerr << "failed to init dev!" << std::endl;
+		return -1;
+	}
+	return status;
+}
+
+bh_fn_t ms_trx::rx_bh()
+{
+	return [this](dev_buf_t *rcd) -> int {
+		if (this->search_for_sch(rcd) == SCH_STATE::FOUND)
+			this->grab_bursts(rcd);
+		return 0;
+	};
+}
+
+bh_fn_t ms_trx::tx_bh()
+{
+	return [this](dev_buf_t *rcd) -> int {
+#pragma unused(rcd)
+		auto y = this;
+#pragma unused(y)
+		/* nothing to do here */
+		return 0;
+	};
+}
+
+void ms_trx::start()
+{
+	auto fn = get_rx_burst_handler_fn(rx_bh());
+	rx_task = std::thread(fn);
+	set_name_aff_sched(rx_task.native_handle(), "rxrun", 2, SCHED_FIFO, sched_get_priority_max(SCHED_FIFO) - 2);
+
+	usleep(1000);
+	auto fn2 = get_tx_burst_handler_fn(tx_bh());
+	tx_task = std::thread(fn2);
+	set_name_aff_sched(tx_task.native_handle(), "txrun", 2, SCHED_FIFO, sched_get_priority_max(SCHED_FIFO) - 1);
+}
+
+void ms_trx::set_upper_ready(bool is_ready)
+{
+	upper_is_ready = is_ready;
+}
+
+void ms_trx::stop_threads()
+{
+	std::cerr << "killing threads...\r\n" << std::endl;
+	close_device();
+	rx_task.join();
+	tx_task.join();
+}
+
+void ms_trx::submit_burst(blade_sample_type *buffer, int len, GSM::Time target)
+{
+	int64_t now_ts;
+	GSM::Time now_time;
+	target.incTN(3); // ul dl offset
+	int target_fn = target.FN();
+	int target_tn = target.TN();
+	timekeeper.get_both(&now_time, &now_ts);
+
+	auto diff_fn = GSM::FNDelta(target_fn, now_time.FN());
+	int diff_tn = (target_tn - (int)now_time.TN()) % 8;
+	auto tosend = GSM::Time(diff_fn, 0);
+
+	if (diff_tn > 0)
+		tosend.incTN(diff_tn);
+	else
+		tosend.decTN(-diff_tn);
+
+	// in thory fn equal and tn+3 equal is also a problem...
+	if (diff_fn < 0 || (diff_fn == 0 && (now_time.TN() - target_tn < 1))) {
+		std::cerr << "## TX too late?! fn DIFF:" << diff_fn << " tn LOCAL: " << now_time.TN()
+			  << " tn OTHER: " << target_tn << std::endl;
+		return;
+	}
+
+	auto check = now_time + tosend;
+	int64_t send_ts = now_ts + tosend.FN() * 8 * ONE_TS_BURST_LEN + tosend.TN() * ONE_TS_BURST_LEN - timing_advance;
+#ifdef DBGXX
+	std::cerr << "## fn DIFF: " << diff_fn << " ## tn DIFF: " << diff_tn << " tn LOCAL/OTHER: " << now_time.TN()
+		  << "/" << target_tn << " tndiff" << diff_tn << " tosend:" << tosend.FN() << ":" << tosend.TN()
+		  << " check: " << check.FN() << ":" << check.TN() << " target: " << target.FN() << ":" << target.TN()
+		  << " ts now: " << now_ts << " target ts:" << send_ts << std::endl;
+#endif
+#if 1
+	unsigned int pad = 4 * 4;
+	blade_sample_type buf2[len + pad];
+	memset(buf2, 0, pad * sizeof(blade_sample_type));
+	memcpy(&buf2[pad], buffer, len * sizeof(blade_sample_type));
+
+	assert(target.FN() == check.FN());
+	assert(target.TN() == check.TN());
+	submit_burst_ts(buf2, len + pad, send_ts - pad);
+#else
+	submit_burst_ts(buffer, len, send_ts);
+#endif
+}
diff --git a/Transceiver52M/ms/ms.h b/Transceiver52M/ms/ms.h
new file mode 100644
index 0000000..8b781a3
--- /dev/null
+++ b/Transceiver52M/ms/ms.h
@@ -0,0 +1,264 @@
+#pragma once
+
+/*
+ * (C) 2022 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Eric Wild <ewild@sysmocom.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <atomic>
+#include <cassert>
+#include <complex>
+#include <cstdint>
+#include <mutex>
+#include <iostream>
+#include <thread>
+
+#if defined(BUILDBLADE)
+#include "bladerf_specific.h"
+#define BASET blade_hw<ms_trx>
+#elif defined(BUILDUHD)
+#include "uhd_specific.h"
+#define BASET uhd_hw<ms_trx>
+#elif defined(BUILDIPC)
+#include "ipc_specific.h"
+#define BASET ipc_hw<ms_trx>
+#else
+#error wat? no device..
+#endif
+
+#include "GSMCommon.h"
+#include "itrq.h"
+
+const unsigned int ONE_TS_BURST_LEN = (3 + 58 + 26 + 58 + 3 + 8.25) * 4 /*sps*/;
+const unsigned int NUM_RXQ_FRAMES = 1; // rx thread <-> upper rx queue
+const unsigned int SCH_LEN_SPS = (ONE_TS_BURST_LEN * 8 /*ts*/ * 12 /*frames*/);
+
+template <typename T> void clamp_array(T *start2, unsigned int len, T max)
+{
+	for (int i = 0; i < len; i++) {
+		const T t1 = start2[i] < -max ? -max : start2[i];
+		const T t2 = t1 > max ? max : t1;
+		start2[i] = t2;
+	}
+}
+template <typename DST_T, typename SRC_T, typename ST>
+void convert_and_scale(void *dst, void *src, unsigned int src_len, ST scale)
+{
+	for (unsigned int i = 0; i < src_len; i++)
+		reinterpret_cast<DST_T *>(dst)[i] = static_cast<DST_T>((reinterpret_cast<SRC_T *>(src)[i])) * scale;
+}
+template <typename DST_T, typename SRC_T> void convert_and_scale_default(void *dst, void *src, unsigned int src_len)
+{
+	return convert_and_scale<DST_T, SRC_T>(dst, src, src_len, SAMPLE_SCALE_FACTOR);
+}
+
+struct one_burst {
+	one_burst()
+	{
+	}
+	GSM::Time gsmts;
+	union {
+		blade_sample_type burst[ONE_TS_BURST_LEN];
+		char sch_bits[148];
+	};
+};
+
+using rx_queue_t = spsc_cond<8 * NUM_RXQ_FRAMES, one_burst, true, true>;
+
+enum class SCH_STATE { SEARCHING, FOUND };
+
+class dummylog : private std::streambuf {
+	std::ostream null_stream;
+
+    public:
+	dummylog() : null_stream(this){};
+	~dummylog() override{};
+	std::ostream &operator()()
+	{
+		return null_stream;
+	}
+	int overflow(int c) override
+	{
+		return c;
+	}
+};
+
+// keeps relationship between gsm time and (continuously adjusted) ts
+class time_keeper {
+	GSM::Time global_time_keeper;
+	int64_t global_ts_keeper;
+	std::mutex m;
+
+    public:
+	time_keeper() : global_time_keeper(0), global_ts_keeper(0)
+	{
+	}
+
+	void set(GSM::Time t, int64_t ts)
+	{
+		std::lock_guard<std::mutex> g(m);
+		global_time_keeper = t;
+		global_ts_keeper = ts;
+	}
+	void inc_both()
+	{
+		std::lock_guard<std::mutex> g(m);
+		global_time_keeper.incTN(1);
+		global_ts_keeper += ONE_TS_BURST_LEN;
+	}
+	void inc_and_update(int64_t new_ts)
+	{
+		std::lock_guard<std::mutex> g(m);
+		global_time_keeper.incTN(1);
+		global_ts_keeper = new_ts;
+		// std::cerr << "u " << new_ts << std::endl;
+	}
+	void inc_and_update_safe(int64_t new_ts)
+	{
+		std::lock_guard<std::mutex> g(m);
+		auto diff = new_ts - global_ts_keeper;
+		assert(diff < 1.5 * ONE_TS_BURST_LEN);
+		assert(diff > 0.5 * ONE_TS_BURST_LEN);
+		global_time_keeper.incTN(1);
+		global_ts_keeper = new_ts;
+		// std::cerr << "s " << new_ts << std::endl;
+	}
+	void dec_by_one()
+	{
+		std::lock_guard<std::mutex> g(m);
+		global_time_keeper.decTN(1);
+		global_ts_keeper -= ONE_TS_BURST_LEN;
+	}
+	auto get_ts()
+	{
+		std::lock_guard<std::mutex> g(m);
+		return global_ts_keeper;
+	}
+	auto gsmtime()
+	{
+		std::lock_guard<std::mutex> g(m);
+		return global_time_keeper;
+	}
+	void get_both(GSM::Time *t, int64_t *ts)
+	{
+		std::lock_guard<std::mutex> g(m);
+		*t = global_time_keeper;
+		*ts = global_ts_keeper;
+	}
+};
+
+using ts_hitter_q_t = spsc_cond<64, GSM::Time, true, false>;
+
+struct ms_trx : public BASET {
+	using base = BASET;
+	static dummylog dummy_log;
+	unsigned int mTSC;
+	unsigned int mBSIC;
+	int timing_advance;
+	bool do_auto_gain;
+
+	std::thread rx_task;
+	std::thread tx_task;
+	std::thread *calcrval_task;
+
+	// provides bursts to upper rx thread
+	rx_queue_t rxqueue;
+#ifdef SYNCTHINGONLY
+	ts_hitter_q_t ts_hitter_q;
+#endif
+	blade_sample_type *first_sch_buf;
+	blade_sample_type *burst_copy_buffer;
+
+	uint64_t first_sch_buf_rcv_ts;
+	std::atomic<bool> rcv_done;
+	std::atomic<bool> sch_thread_done;
+
+	int64_t temp_ts_corr_offset = 0;
+	int64_t first_sch_ts_start = -1;
+
+	time_keeper timekeeper;
+
+	void start();
+	std::atomic<bool> upper_is_ready;
+	void set_upper_ready(bool is_ready);
+
+	bool handle_sch_or_nb();
+	bool handle_sch(bool first = false);
+	bool decode_sch(float *bits, bool update_global_clock);
+	SCH_STATE search_for_sch(dev_buf_t *rcd);
+	void grab_bursts(dev_buf_t *rcd) __attribute__((optnone));
+
+	int init_device();
+	int init_dev_and_streams();
+	void stop_threads();
+	void *rx_cb(ms_trx *t);
+	void *tx_cb();
+	void maybe_update_gain(one_burst &brst);
+
+	ms_trx()
+		: timing_advance(0), do_auto_gain(false), rxqueue(), first_sch_buf(new blade_sample_type[SCH_LEN_SPS]),
+		  burst_copy_buffer(new blade_sample_type[ONE_TS_BURST_LEN]), rcv_done{ false }, sch_thread_done{ false }
+	{
+	}
+
+	virtual ~ms_trx()
+	{
+		delete[] burst_copy_buffer;
+		delete[] first_sch_buf;
+	}
+	bh_fn_t rx_bh();
+	bh_fn_t tx_bh();
+
+	void submit_burst(blade_sample_type *buffer, int len, GSM::Time);
+	void set_ta(int val)
+	{
+		assert(val > -127 && val < 128);
+		timing_advance = val * 4;
+	}
+
+	void set_name_aff_sched(const char *name, int cpunum, int schedtype, int prio)
+	{
+		set_name_aff_sched(pthread_self(), name, cpunum, schedtype, prio);
+	}
+
+	void set_name_aff_sched(std::thread::native_handle_type h, const char *name, int cpunum, int schedtype,
+				int prio)
+	{
+		pthread_setname_np(h, name);
+
+		cpu_set_t cpuset;
+
+		CPU_ZERO(&cpuset);
+		CPU_SET(cpunum, &cpuset);
+
+		auto rv = pthread_setaffinity_np(h, sizeof(cpuset), &cpuset);
+		if (rv < 0) {
+			std::cerr << name << " affinity: errreur! " << std::strerror(errno);
+			return exit(0);
+		}
+
+		sched_param sch_params;
+		sch_params.sched_priority = prio;
+		rv = pthread_setschedparam(h, schedtype, &sch_params);
+		if (rv < 0) {
+			std::cerr << name << " sched: errreur! " << std::strerror(errno);
+			return exit(0);
+		}
+	}
+};
diff --git a/Transceiver52M/ms/ms_rx_burst.h b/Transceiver52M/ms/ms_rx_burst.h
new file mode 100644
index 0000000..764a69e
--- /dev/null
+++ b/Transceiver52M/ms/ms_rx_burst.h
@@ -0,0 +1,25 @@
+#pragma once
+/*
+ * (C) 2022 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Eric Wild <ewild@sysmocom.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "ms.h"
+
+void rcv_bursts_test(rx_queue_t *q, unsigned int *tsc, int scale);
\ No newline at end of file
diff --git a/Transceiver52M/ms/ms_rx_burst_test.cpp b/Transceiver52M/ms/ms_rx_burst_test.cpp
new file mode 100644
index 0000000..69fca5e
--- /dev/null
+++ b/Transceiver52M/ms/ms_rx_burst_test.cpp
@@ -0,0 +1,207 @@
+/*
+ * (C) 2022 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Eric Wild <ewild@sysmocom.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include "ms.h"
+#include "sigProcLib.h"
+#include "signalVector.h"
+#include "grgsm_vitac/grgsm_vitac.h"
+extern "C" {
+#include "sch.h"
+}
+
+#if !defined(SYNCTHINGONLY) || !defined(NODAMNLOG)
+#define DBGLG(...) ms_trx::dummy_log()
+#else
+#define DBGLG(...) std::cerr
+#endif
+
+#if !defined(SYNCTHINGONLY)
+#define DBGLG2(...) ms_trx::dummy_log()
+#else
+#define DBGLG2(...) std::cerr
+#endif
+
+static bool decode_sch(float *bits, bool update_global_clock)
+{
+	struct sch_info sch;
+	ubit_t info[GSM_SCH_INFO_LEN];
+	sbit_t data[GSM_SCH_CODED_LEN];
+
+	float_to_sbit(&bits[3], &data[0], 62, 39);
+	float_to_sbit(&bits[106], &data[39], 62, 39);
+
+	if (!gsm_sch_decode(info, data)) {
+		gsm_sch_parse(info, &sch);
+
+		DBGLG() << "SCH : Decoded values" << std::endl;
+		DBGLG() << "    BSIC: " << sch.bsic << std::endl;
+		DBGLG() << "    TSC: " << (sch.bsic & 0x7) << std::endl;
+		DBGLG() << "    T1  : " << sch.t1 << std::endl;
+		DBGLG() << "    T2  : " << sch.t2 << std::endl;
+		DBGLG() << "    T3p : " << sch.t3p << std::endl;
+		DBGLG() << "    FN  : " << gsm_sch_to_fn(&sch) << std::endl;
+		return true;
+	}
+	return false;
+}
+
+static void check_rcv_fn(GSM::Time t, bool first, unsigned int &lastfn, unsigned int &fnbm)
+{
+	if (first && t.TN() == 0) {
+		lastfn = t.FN();
+		fnbm = 1 << 0;
+		first = false;
+	}
+	if (!first && t.FN() != lastfn) {
+		if (fnbm != 255)
+			std::cerr << "rx " << lastfn << ":" << fnbm << " " << __builtin_popcount(fnbm) << std::endl;
+		lastfn = t.FN();
+		fnbm = 1 << t.TN();
+	}
+
+	fnbm |= 1 << t.TN();
+}
+
+static void handle_it(one_burst &e, signalVector &burst, unsigned int tsc, int scale)
+{
+	memset(burst.begin(), 0, burst.size() * sizeof(std::complex<float>));
+	const auto is_sch = gsm_sch_check_ts(e.gsmts.TN(), e.gsmts.FN());
+	const auto is_fcch = gsm_fcch_check_ts(e.gsmts.TN(), e.gsmts.FN());
+
+	if (is_fcch)
+		return;
+
+	if (is_sch) {
+		char outbin[148];
+		convert_and_scale_default<float, int16_t>(burst.begin(), e.burst, ONE_TS_BURST_LEN * 2);
+		std::stringstream dbgout;
+#if 0
+		{
+			struct estim_burst_params ebp;
+			auto rv2 = detectSCHBurst(burst, 4, 4, sch_detect_type::SCH_DETECT_FULL, &ebp);
+			auto bits = demodAnyBurst(burst, SCH, 4, &ebp);
+			// clamp_array(bits->begin(), 148, 1.5f);
+			for (auto &i : *bits)
+				i = (i > 0 ? 1 : -1);
+
+			auto rv = decode_sch(bits->begin(), false);
+			dbgout << "U DET@" << (rv2 ? "yes " : "   ") << "Timing offset     " << ebp.toa
+			       << " symbols, DECODE: " << (rv ? "yes" : "---") << " ";
+
+			delete bits;
+		}
+#endif
+		{
+			convert_and_scale<float, float>(burst.begin(), burst.begin(), ONE_TS_BURST_LEN * 2,
+							1.f / float(scale));
+
+			std::complex<float> channel_imp_resp[CHAN_IMP_RESP_LENGTH * d_OSR];
+			auto ss = reinterpret_cast<std::complex<float> *>(burst.begin());
+			int d_c0_burst_start = get_sch_chan_imp_resp(ss, &channel_imp_resp[0]);
+			detect_burst(ss, &channel_imp_resp[0], d_c0_burst_start, outbin);
+
+			SoftVector bits;
+			bits.resize(148);
+			for (int i = 0; i < 148; i++) {
+				bits[i] = (!outbin[i]); // < 1 ? -1 : 1;
+			}
+
+			auto rv = decode_sch(bits.begin(), false);
+			dbgout << "U SCH@"
+			       << " " << e.gsmts.FN() << ":" << e.gsmts.TN() << " " << d_c0_burst_start
+			       << " DECODE:" << (rv ? "yes" : "---") << std::endl;
+		}
+
+		DBGLG() << dbgout.str();
+		return;
+	}
+#if 1
+	convert_and_scale<float, int16_t>(burst.begin(), e.burst, ONE_TS_BURST_LEN * 2, 1.f / float(scale));
+	// std::cerr << "@" << tsc << " " << e.gsmts.FN() << ":" << e.gsmts.TN() << " " << ebp.toa << " "
+	// 	  << std::endl;
+
+	char outbin[148];
+	auto ss = reinterpret_cast<std::complex<float> *>(burst.begin());
+	float ncmax, dcmax;
+	std::complex<float> chan_imp_resp[CHAN_IMP_RESP_LENGTH * d_OSR], chan_imp_resp2[CHAN_IMP_RESP_LENGTH * d_OSR];
+	auto normal_burst_start = get_norm_chan_imp_resp(ss, &chan_imp_resp[0], &ncmax, tsc);
+	auto dummy_burst_start = get_norm_chan_imp_resp(ss, &chan_imp_resp2[0], &dcmax, TS_DUMMY);
+	auto is_nb = ncmax > dcmax;
+
+	DBGLG() << " U " << (is_nb ? "NB" : "DB") << "@ o nb: " << normal_burst_start << " o db: " << dummy_burst_start
+		<< std::endl;
+
+	if (is_nb)
+		detect_burst(ss, &chan_imp_resp[0], normal_burst_start, outbin);
+	else
+		detect_burst(ss, &chan_imp_resp2[0], dummy_burst_start, outbin);
+	;
+#ifdef DBGXX
+	// auto bits = SoftVector(148);
+	// for (int i = 0; i < 148; i++)
+	// 	(bits)[i] = outbin[i] < 1 ? -1 : 1;
+#endif
+#endif
+}
+
+void rcv_bursts_test(rx_queue_t *q, unsigned int *tsc, int scale)
+{
+	static bool first = true;
+	unsigned int lastfn = 0;
+	unsigned int fnbm = 0;
+	signalVector burst(ONE_TS_BURST_LEN, 100, 100);
+
+	cpu_set_t cpuset;
+
+	CPU_ZERO(&cpuset);
+	CPU_SET(1, &cpuset);
+
+	auto rv = pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);
+	if (rv < 0) {
+		std::cerr << "affinity: errreur! " << std::strerror(errno);
+		exit(0);
+	}
+
+	int prio = sched_get_priority_max(SCHED_RR);
+	struct sched_param param;
+	param.sched_priority = prio;
+	rv = sched_setscheduler(0, SCHED_RR, &param);
+	if (rv < 0) {
+		std::cerr << "scheduler: errreur! " << std::strerror(errno);
+		exit(0);
+	}
+
+	while (1) {
+		one_burst e;
+		while (!q->spsc_pop(&e)) {
+			q->spsc_prep_pop();
+		}
+
+		check_rcv_fn(e.gsmts, first, lastfn, fnbm);
+
+		handle_it(e, burst, *tsc, scale);
+#ifdef DBGXX
+		rv = detectSCHBurst(*burst, 4, 4, sch_detect_type::SCH_DETECT_FULL, &ebp);
+		if (rv > 0)
+			std::cerr << "#" << e.gsmts.FN() << ":" << e.gsmts.TN() << " " << ebp.toa << std::endl;
+		sched_yield();
+#endif
+	}
+}
\ No newline at end of file
diff --git a/Transceiver52M/ms/ms_rx_lower.cpp b/Transceiver52M/ms/ms_rx_lower.cpp
new file mode 100644
index 0000000..780ab42
--- /dev/null
+++ b/Transceiver52M/ms/ms_rx_lower.cpp
@@ -0,0 +1,343 @@
+/*
+ * (C) 2022 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Eric Wild <ewild@sysmocom.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "sigProcLib.h"
+#include "signalVector.h"
+#include <atomic>
+#include <cassert>
+#include <complex>
+#include <iostream>
+#include <future>
+
+#include "ms.h"
+#include "grgsm_vitac/grgsm_vitac.h"
+
+extern "C" {
+#include "sch.h"
+}
+
+#ifdef LOG
+#undef LOG
+#endif
+
+#if !defined(SYNCTHINGONLY) //|| !defined(NODAMNLOG)
+#define DBGLG(...) ms_trx::dummy_log()
+#else
+#define DBGLG(...) std::cerr
+#endif
+
+#if !defined(SYNCTHINGONLY) || !defined(NODAMNLOG)
+#define DBGLG2(...) ms_trx::dummy_log()
+#else
+#define DBGLG2(...) std::cerr
+#endif
+
+#define PRINT_Q_OVERFLOW
+bool ms_trx::decode_sch(float *bits, bool update_global_clock)
+{
+	int fn;
+	struct sch_info sch;
+	ubit_t info[GSM_SCH_INFO_LEN];
+	sbit_t data[GSM_SCH_CODED_LEN];
+
+	float_to_sbit(&bits[3], &data[0], 1, 39);
+	float_to_sbit(&bits[106], &data[39], 1, 39);
+
+	if (!gsm_sch_decode(info, data)) {
+		gsm_sch_parse(info, &sch);
+
+		if (update_global_clock) {
+			DBGLG() << "SCH : Decoded values" << std::endl;
+			DBGLG() << "    BSIC: " << sch.bsic << std::endl;
+			DBGLG() << "    TSC: " << (sch.bsic & 0x7) << std::endl;
+			DBGLG() << "    T1  : " << sch.t1 << std::endl;
+			DBGLG() << "    T2  : " << sch.t2 << std::endl;
+			DBGLG() << "    T3p : " << sch.t3p << std::endl;
+			DBGLG() << "    FN  : " << gsm_sch_to_fn(&sch) << std::endl;
+		}
+
+		fn = gsm_sch_to_fn(&sch);
+		if (fn < 0) { // how? wh?
+			DBGLG() << "SCH : Failed to convert FN " << std::endl;
+			return false;
+		}
+
+		if (update_global_clock) {
+			mBSIC = sch.bsic;
+			mTSC = sch.bsic & 0x7;
+			timekeeper.set(fn, 0);
+			// global_time_keeper.FN(fn);
+			// global_time_keeper.TN(0);
+		}
+#ifdef SYNCTHINGONLY
+		else {
+			int t3 = sch.t3p * 10 + 1;
+			if (t3 == 11) {
+				// timeslot hitter attempt @ fn 21 in mf
+				DBGLG2() << "sch @ " << t3 << std::endl;
+				auto e = GSM::Time(fn, 0);
+				e += 10;
+				ts_hitter_q.spsc_push(&e);
+			}
+		}
+#endif
+
+		return true;
+	}
+	return false;
+}
+
+void ms_trx::maybe_update_gain(one_burst &brst)
+{
+	static_assert((sizeof(brst.burst) / sizeof(brst.burst[0])) == ONE_TS_BURST_LEN, "wtf, buffer size mismatch?");
+	const int avgburst_num = 8 * 20; // ~ 50*4.5ms = 90ms?
+	static_assert(avgburst_num * 577 > (50 * 1000), "can't update faster then blade wait time?");
+	const unsigned int rx_max_cutoff = (rxFullScale * 2) / 3;
+	static int gain_check = 0;
+	static float runmean = 0;
+	float sum = 0;
+	for (auto i : brst.burst)
+		sum += abs(i.real()) + abs(i.imag());
+	sum /= ONE_TS_BURST_LEN * 2;
+
+	runmean = gain_check ? (runmean * (gain_check + 2) - 1 + sum) / (gain_check + 2) : sum;
+
+	if (gain_check == avgburst_num - 1) {
+		DBGLG2() << "\x1B[32m #RXG \033[0m" << rxgain << " " << runmean << " " << sum << std::endl;
+		auto gainoffset = runmean < (rxFullScale / 4 ? 4 : 2);
+		gainoffset = runmean < (rxFullScale / 2 ? 2 : 1);
+		float newgain = runmean < rx_max_cutoff ? rxgain + gainoffset : rxgain - gainoffset;
+		// FIXME: gian cutoff
+		if (newgain != rxgain && newgain <= 60)
+			std::thread([this, newgain] { setRxGain(newgain); }).detach();
+		runmean = 0;
+	}
+	gain_check = (gain_check + 1) % avgburst_num;
+}
+
+static char sch_demod_bits[148];
+
+bool ms_trx::handle_sch_or_nb()
+{
+	one_burst brst;
+	const auto current_gsm_time = timekeeper.gsmtime();
+	const auto is_sch = gsm_sch_check_ts(current_gsm_time.TN(), current_gsm_time.FN());
+	const auto is_fcch = gsm_fcch_check_ts(current_gsm_time.TN(), current_gsm_time.FN());
+#pragma unused(is_fcch)
+
+	//either pass burst to upper layer for demod, OR pass demodded SCH to upper layer so we don't waste time processing it twice
+	brst.gsmts = current_gsm_time;
+
+	if (!is_sch) {
+		memcpy(brst.burst, burst_copy_buffer, sizeof(blade_sample_type) * ONE_TS_BURST_LEN);
+	} else {
+		handle_sch(false);
+		memcpy(brst.sch_bits, sch_demod_bits, sizeof(sch_demod_bits));
+	}
+#ifndef SYNCTHINGONLY
+	if (upper_is_ready) { // this is blocking, so only submit if there is a reader - only if upper exists!
+#endif
+		while (!rxqueue.spsc_push(&brst))
+			;
+#ifndef SYNCTHINGONLY
+	}
+#endif
+
+	if (do_auto_gain)
+		maybe_update_gain(brst);
+
+	return false;
+}
+
+static float sch_acq_buffer[SCH_LEN_SPS * 2];
+
+bool ms_trx::handle_sch(bool is_first_sch_acq)
+{
+	auto current_gsm_time = timekeeper.gsmtime();
+	const auto buf_len = is_first_sch_acq ? SCH_LEN_SPS : ONE_TS_BURST_LEN;
+	const auto which_in_buffer = is_first_sch_acq ? first_sch_buf : burst_copy_buffer;
+	const auto which_out_buffer = is_first_sch_acq ? sch_acq_buffer : &sch_acq_buffer[40 * 2];
+	const auto ss = reinterpret_cast<std::complex<float> *>(which_out_buffer);
+	std::complex<float> channel_imp_resp[CHAN_IMP_RESP_LENGTH * d_OSR];
+
+	int start;
+	memset((void *)&sch_acq_buffer[0], 0, sizeof(sch_acq_buffer));
+	if (is_first_sch_acq) {
+		float max_corr = 0;
+		convert_and_scale<float, int16_t>(which_out_buffer, which_in_buffer, buf_len * 2,
+						  1.f / float(rxFullScale));
+		start = get_sch_buffer_chan_imp_resp(ss, &channel_imp_resp[0], buf_len, &max_corr);
+		detect_burst(&ss[start], &channel_imp_resp[0], 0, sch_demod_bits);
+	} else {
+		convert_and_scale<float, int16_t>(which_out_buffer, which_in_buffer, buf_len * 2,
+						  1.f / float(rxFullScale));
+		start = get_sch_chan_imp_resp(ss, &channel_imp_resp[0]);
+		start = start < 39 ? start : 39;
+		start = start > -39 ? start : -39;
+		detect_burst(&ss[start], &channel_imp_resp[0], 0, sch_demod_bits);
+	}
+
+	SoftVector bitss(148);
+	for (int i = 0; i < 148; i++) {
+		bitss[i] = (sch_demod_bits[i]);
+	}
+
+	auto sch_decode_success = decode_sch(bitss.begin(), is_first_sch_acq);
+
+	if (sch_decode_success) {
+		const auto ts_offset_symb = 0;
+		if (is_first_sch_acq) {
+			// update ts to first sample in sch buffer, to allow delay calc for current ts
+			first_sch_ts_start = first_sch_buf_rcv_ts + start - (ts_offset_symb * 4) - 1;
+		} else if (abs(start) > 1) {
+			// continuous sch tracking, only update if off too much
+			temp_ts_corr_offset += -start;
+			std::cerr << "offs: " << start << " " << temp_ts_corr_offset << std::endl;
+		}
+
+		return true;
+	} else {
+		DBGLG2() << "L SCH : \x1B[31m decode fail \033[0m @ toa:" << start << " " << current_gsm_time.FN()
+			 << ":" << current_gsm_time.TN() << std::endl;
+	}
+	return false;
+}
+
+__attribute__((xray_never_instrument)) SCH_STATE ms_trx::search_for_sch(dev_buf_t *rcd)
+{
+	static unsigned int sch_pos = 0;
+	if (sch_thread_done)
+		return SCH_STATE::FOUND;
+
+	if (rcv_done)
+		return SCH_STATE::SEARCHING;
+
+	auto to_copy = SCH_LEN_SPS - sch_pos;
+
+	if (SCH_LEN_SPS == to_copy) // first time
+		first_sch_buf_rcv_ts = rcd->get_first_ts();
+
+	if (!to_copy) {
+		sch_pos = 0;
+		rcv_done = true;
+		std::thread([this] {
+			set_name_aff_sched("sch_search", 1, SCHED_FIFO, sched_get_priority_max(SCHED_FIFO) - 5);
+
+			auto ptr = reinterpret_cast<const int16_t *>(first_sch_buf);
+			const auto target_val = rxFullScale / 8;
+			float sum = 0;
+			for (int i = 0; i < SCH_LEN_SPS * 2; i++)
+				sum += std::abs(ptr[i]);
+			sum /= SCH_LEN_SPS * 2;
+
+			//FIXME: arbitrary value, gain cutoff
+			if (sum > target_val || rxgain >= 60) // enough ?
+				sch_thread_done = this->handle_sch(true);
+			else {
+				std::cerr << "\x1B[32m #RXG \033[0m gain " << rxgain << " -> " << rxgain + 4
+					  << " sample avg:" << sum << " target: >=" << target_val << std::endl;
+				setRxGain(rxgain + 4);
+			}
+
+			if (!sch_thread_done)
+				rcv_done = false; // retry!
+			return (bool)sch_thread_done;
+		}).detach();
+	}
+
+	auto spsmax = rcd->actual_samples_per_buffer();
+	if (to_copy > spsmax)
+		sch_pos += rcd->readall(first_sch_buf + sch_pos);
+	else
+		sch_pos += rcd->read_n(first_sch_buf + sch_pos, 0, to_copy);
+
+	return SCH_STATE::SEARCHING;
+}
+
+void ms_trx::grab_bursts(dev_buf_t *rcd)
+{
+	// partial burst samples read from the last buffer
+	static int partial_rdofs = 0;
+	static bool first_call = true;
+	int to_skip = 0;
+
+	// round up to next burst by calculating the time between sch detection and now
+	if (first_call) {
+		const auto next_burst_start = rcd->get_first_ts() - first_sch_ts_start;
+		const auto fullts = next_burst_start / ONE_TS_BURST_LEN;
+		const auto fracts = next_burst_start % ONE_TS_BURST_LEN;
+		to_skip = ONE_TS_BURST_LEN - fracts;
+
+		for (int i = 0; i < fullts; i++)
+			timekeeper.inc_and_update(first_sch_ts_start + i * ONE_TS_BURST_LEN);
+
+		if (fracts)
+			timekeeper.inc_both();
+		// timekeeper.inc_and_update(first_sch_ts_start + 1 * ONE_TS_BURST_LEN);
+
+		timekeeper.dec_by_one(); // oops, off by one?
+
+		timekeeper.set(timekeeper.gsmtime(), rcd->get_first_ts() - ONE_TS_BURST_LEN + to_skip);
+
+		DBGLG() << "this ts: " << rcd->get_first_ts() << " diff full TN: " << fullts << " frac TN: " << fracts
+			<< " GSM now: " << timekeeper.gsmtime().FN() << ":" << timekeeper.gsmtime().TN() << " is sch? "
+			<< gsm_sch_check_fn(timekeeper.gsmtime().FN()) << std::endl;
+		first_call = false;
+	}
+
+	if (partial_rdofs) {
+		auto first_remaining = ONE_TS_BURST_LEN - partial_rdofs;
+		auto rd = rcd->read_n(burst_copy_buffer + partial_rdofs, 0, first_remaining);
+		if (rd != first_remaining) {
+			partial_rdofs += rd;
+			return;
+		}
+
+		timekeeper.inc_and_update_safe(rcd->get_first_ts() - partial_rdofs);
+		handle_sch_or_nb();
+		to_skip = first_remaining;
+	}
+
+	// apply sample rate slippage compensation
+	to_skip -= temp_ts_corr_offset;
+
+	// FIXME: happens rarely, read_n start -1 blows up
+	// this is fine: will just be corrected one buffer later
+	if (to_skip < 0)
+		to_skip = 0;
+	else
+		temp_ts_corr_offset = 0;
+
+	const auto left_after_burst = rcd->actual_samples_per_buffer() - to_skip;
+
+	const int full = left_after_burst / ONE_TS_BURST_LEN;
+	const int frac = left_after_burst % ONE_TS_BURST_LEN;
+
+	for (int i = 0; i < full; i++) {
+		rcd->read_n(burst_copy_buffer, to_skip + i * ONE_TS_BURST_LEN, ONE_TS_BURST_LEN);
+		timekeeper.inc_and_update_safe(rcd->get_first_ts() + to_skip + i * ONE_TS_BURST_LEN);
+		handle_sch_or_nb();
+	}
+
+	if (frac)
+		rcd->read_n(burst_copy_buffer, to_skip + full * ONE_TS_BURST_LEN, frac);
+	partial_rdofs = frac;
+}
diff --git a/Transceiver52M/ms/ms_upper.cpp b/Transceiver52M/ms/ms_upper.cpp
new file mode 100644
index 0000000..dd25fd2
--- /dev/null
+++ b/Transceiver52M/ms/ms_upper.cpp
@@ -0,0 +1,451 @@
+/*
+ * (C) 2022 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Eric Wild <ewild@sysmocom.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "sigProcLib.h"
+#include "ms.h"
+#include <signalVector.h>
+#include <radioVector.h>
+#include <radioInterface.h>
+#include "grgsm_vitac/grgsm_vitac.h"
+
+extern "C" {
+#include <osmocom/core/select.h>
+#include "sch.h"
+#include "convolve.h"
+#include "convert.h"
+#include "proto_trxd.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <getopt.h>
+#include <unistd.h>
+#include <signal.h>
+#include <errno.h>
+#include <time.h>
+
+#ifdef LSANDEBUG
+void __lsan_do_recoverable_leak_check();
+#endif
+}
+
+#include "ms_upper.h"
+
+namespace trxcon
+{
+extern "C" {
+#include <osmocom/core/fsm.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/signal.h>
+#include <osmocom/core/select.h>
+#include <osmocom/core/gsmtap_util.h>
+#include <osmocom/core/gsmtap.h>
+
+// #include <osmocom/core/application.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/bb/trxcon/logging.h>
+
+#include <osmocom/bb/trxcon/trxcon.h>
+#include <osmocom/bb/trxcon/trxcon_fsm.h>
+#include <osmocom/bb/trxcon/phyif.h>
+#include <osmocom/bb/trxcon/trx_if.h>
+#include <osmocom/bb/trxcon/l1ctl_server.h>
+
+#include <osmocom/bb/l1sched/l1sched.h>
+// #include <osmocom/bb/l1sched/logging.h>
+}
+struct trxcon_inst *g_trxcon;
+// trx_instance *trxcon_instance; // local handle
+struct internal_q_tx_buf {
+	trxcon_phyif_burst_req r;
+	uint8_t buf[148];
+};
+using tx_queue_t = spsc_cond<8 * 1, internal_q_tx_buf, true, false>;
+using cmd_queue_t = spsc_cond<8 * 1, trxcon_phyif_cmd, true, false>;
+using cmdr_queue_t = spsc_cond<8 * 1, trxcon_phyif_rsp, false, false>;
+static tx_queue_t txq;
+static cmd_queue_t cmdq_to_phy;
+static cmdr_queue_t cmdq_from_phy;
+
+extern void trxc_log_init(void *tallctx);
+extern void trxc_l1ctl_init(void *tallctx);
+
+} // namespace trxcon
+
+#ifdef LOG
+#undef LOG
+#define LOG(...) upper_trx::dummy_log()
+#endif
+
+#define DBGLG(...) upper_trx::dummy_log()
+
+void upper_trx::start_threads()
+{
+	thr_control = std::thread([this] {
+		set_name_aff_sched("upper_ctrl", 1, SCHED_RR, sched_get_priority_max(SCHED_RR));
+		while (1) {
+			driveControl();
+		}
+	});
+	msleep(1);
+	thr_tx = std::thread([this] {
+		set_name_aff_sched("upper_tx", 1, SCHED_FIFO, sched_get_priority_max(SCHED_FIFO) - 1);
+		while (1) {
+			driveTx();
+		}
+	});
+
+	// atomic ensures data is not written to q until loop reads
+	start_lower_ms();
+
+	set_name_aff_sched("upper_rx", 1, SCHED_FIFO, sched_get_priority_max(SCHED_RR) - 5);
+	while (1) {
+		// set_upper_ready(true);
+		driveReceiveFIFO();
+		osmo_select_main(1);
+
+		trxcon::trxcon_phyif_rsp r;
+		if (trxcon::cmdq_from_phy.spsc_pop(&r)) {
+			DBGLG() << "HAVE RESP:" << r.type << std::endl;
+			trxcon_phyif_handle_rsp(trxcon::g_trxcon, &r);
+		}
+	}
+
+#ifdef LSANDEBUG
+	std::thread([this] {
+		set_name_aff_sched("leakcheck", 1, SCHED_FIFO, sched_get_priority_max(SCHED_FIFO) - 10);
+
+		while (1) {
+			std::this_thread::sleep_for(std::chrono::seconds{ 5 });
+			__lsan_do_recoverable_leak_check();
+		}
+	}).detach();
+#endif
+}
+
+void upper_trx::start_lower_ms()
+{
+	ms_trx::start();
+}
+
+bool upper_trx::pullRadioVector(GSM::Time &wTime, int &RSSI, int &timingOffset)
+{
+	float pow, avg = 1.0;
+	static complex workbuf[40 + 625 + 40];
+	static signalVector sv(workbuf, 40, 625);
+	one_burst e;
+	auto ss = reinterpret_cast<std::complex<float> *>(&workbuf[40]);
+	memset((void *)&workbuf[0], 0, sizeof(workbuf));
+	// assert(sv.begin() == &workbuf[40]);
+
+	while (!rxqueue.spsc_pop(&e)) {
+		rxqueue.spsc_prep_pop();
+	}
+
+	wTime = e.gsmts;
+
+	const auto is_sch = gsm_sch_check_ts(wTime.TN(), wTime.FN());
+	const auto is_fcch = gsm_fcch_check_ts(wTime.TN(), wTime.FN());
+
+	trxcon::trxcon_phyif_rtr_ind i = { static_cast<uint32_t>(wTime.FN()), static_cast<uint8_t>(wTime.TN()) };
+	trxcon::trxcon_phyif_rtr_rsp r = {};
+	trxcon_phyif_handle_rtr_ind(trxcon::g_trxcon, &i, &r);
+	if (!(r.flags & TRXCON_PHYIF_RTR_F_ACTIVE))
+		return false;
+
+	if (is_fcch) {
+		// return trash
+		return true;
+	}
+
+	if (is_sch) {
+		for (int i = 0; i < 148; i++)
+			(demodded_softbits)[i] = (e.sch_bits[i]);
+		RSSI = 10;
+		timingOffset = 0;
+		return true;
+	}
+
+	convert_and_scale<float, int16_t>(ss, e.burst, ONE_TS_BURST_LEN * 2, 1.f / float(rxFullScale));
+
+	pow = energyDetect(sv, 20 * 4 /*sps*/);
+	if (pow < -1) {
+		LOG(ALERT) << "Received empty burst";
+		return false;
+	}
+
+	avg = sqrt(pow);
+	{
+		float ncmax;
+		std::complex<float> chan_imp_resp[CHAN_IMP_RESP_LENGTH * d_OSR];
+		auto normal_burst_start = get_norm_chan_imp_resp(ss, &chan_imp_resp[0], &ncmax, mTSC);
+#ifdef DBGXX
+		float dcmax;
+		std::complex<float> chan_imp_resp2[CHAN_IMP_RESP_LENGTH * d_OSR];
+		auto dummy_burst_start = get_norm_chan_imp_resp(ss, &chan_imp_resp2[0], &dcmax, TS_DUMMY);
+		auto is_nb = ncmax > dcmax;
+		// DBGLG() << " U " << (is_nb ? "NB" : "DB") << "@ o nb: " << normal_burst_start
+		// 	  << " o db: " << dummy_burst_start << std::endl;
+#endif
+		normal_burst_start = normal_burst_start < 39 ? normal_burst_start : 39;
+		normal_burst_start = normal_burst_start > -39 ? normal_burst_start : -39;
+#ifdef DBGXX
+		// fprintf(stderr, "%s %d\n", (is_nb ? "N":"D"), burst_time.FN());
+		// if (is_nb)
+#endif
+		detect_burst(ss, &chan_imp_resp[0], normal_burst_start, demodded_softbits);
+#ifdef DBGXX
+		// else
+		// 	detect_burst(ss, &chan_imp_resp2[0], dummy_burst_start, outbin);
+#endif
+	}
+	RSSI = (int)floor(20.0 * log10(rxFullScale / avg));
+	timingOffset = (int)round(0);
+
+	return true;
+}
+
+void upper_trx::driveReceiveFIFO()
+{
+	int RSSI;
+	int TOA; // in 1/256 of a symbol
+	GSM::Time burstTime;
+
+	if (!mOn)
+		return;
+
+	if (pullRadioVector(burstTime, RSSI, TOA)) {
+		// trxcon::trx_data_rx_handler(trxcon::trxcon_instance, (uint8_t *)&response);
+		trxcon::trxcon_phyif_burst_ind bi;
+		bi.fn = burstTime.FN();
+		bi.tn = burstTime.TN();
+		bi.rssi = RSSI;
+		bi.toa256 = TOA;
+		bi.burst = (sbit_t *)demodded_softbits;
+		bi.burst_len = sizeof(demodded_softbits);
+		// trxcon_phyif_handle_clock_ind(trxcon::g_trxcon, bi.fn);
+		trxcon_phyif_handle_burst_ind(trxcon::g_trxcon, &bi);
+	}
+
+	struct trxcon::trxcon_phyif_rts_ind rts {
+		static_cast<uint32_t>(burstTime.FN()), static_cast<uint8_t>(burstTime.TN())
+	};
+	trxcon_phyif_handle_rts_ind(trxcon::g_trxcon, &rts);
+}
+
+void upper_trx::driveTx()
+{
+	trxcon::internal_q_tx_buf e;
+	while (!trxcon::txq.spsc_pop(&e)) {
+		trxcon::txq.spsc_prep_pop();
+	}
+
+	trxcon::internal_q_tx_buf *burst = &e;
+
+#ifdef TXDEBUG
+	DBGLG() << "got burst!" << burst->r.fn << ":" << burst->ts << " current: " << timekeeper.gsmtime().FN()
+		<< " dff: " << (int64_t)((int64_t)timekeeper.gsmtime().FN() - (int64_t)burst->r.fn) << std::endl;
+#endif
+
+	auto currTime = GSM::Time(burst->r.fn, burst->r.tn);
+	int RSSI = (int)burst->r.pwr;
+
+	static BitVector newBurst(gSlotLen);
+	BitVector::iterator itr = newBurst.begin();
+	auto *bufferItr = burst->buf;
+	while (itr < newBurst.end())
+		*itr++ = *bufferItr++;
+
+	auto txburst = modulateBurst(newBurst, 8 + (currTime.TN() % 4 == 0), 4);
+	scaleVector(*txburst, txFullScale * pow(10, -RSSI / 10));
+
+	// float -> int16
+	blade_sample_type burst_buf[txburst->size()];
+	convert_and_scale<int16_t, float>(burst_buf, txburst->begin(), txburst->size() * 2, 1);
+#ifdef TXDEBUG
+	auto check = signalVector(txburst->size(), 40);
+	convert_and_scale<float, int16_t, 1>(check.begin(), burst_buf, txburst->size() * 2);
+	estim_burst_params ebp;
+	auto d = detectAnyBurst(check, 2, 4, 4, CorrType::RACH, 40, &ebp);
+	if (d)
+		DBGLG() << "RACH D! " << ebp.toa << std::endl;
+	else
+		DBGLG() << "RACH NOOOOOOOOOO D! " << ebp.toa << std::endl;
+
+		// memory read --binary --outfile /tmp/mem.bin &burst_buf[0] --count 2500 --force
+#endif
+	submit_burst(burst_buf, txburst->size(), currTime);
+	delete txburst;
+}
+
+static const char *cmd2str(trxcon::trxcon_phyif_cmd_type c)
+{
+	switch (c) {
+	case trxcon::TRXCON_PHYIF_CMDT_RESET:
+		return "TRXCON_PHYIF_CMDT_RESET";
+	case trxcon::TRXCON_PHYIF_CMDT_POWERON:
+		return "TRXCON_PHYIF_CMDT_POWERON";
+	case trxcon::TRXCON_PHYIF_CMDT_POWEROFF:
+		return "TRXCON_PHYIF_CMDT_POWEROFF";
+	case trxcon::TRXCON_PHYIF_CMDT_MEASURE:
+		return "TRXCON_PHYIF_CMDT_MEASURE";
+	case trxcon::TRXCON_PHYIF_CMDT_SETFREQ_H0:
+		return "TRXCON_PHYIF_CMDT_SETFREQ_H0";
+	case trxcon::TRXCON_PHYIF_CMDT_SETFREQ_H1:
+		return "TRXCON_PHYIF_CMDT_SETFREQ_H1";
+	case trxcon::TRXCON_PHYIF_CMDT_SETSLOT:
+		return "TRXCON_PHYIF_CMDT_SETSLOT";
+	case trxcon::TRXCON_PHYIF_CMDT_SETTA:
+		return "TRXCON_PHYIF_CMDT_SETTA";
+	default:
+		return "UNKNOWN COMMAND!";
+	}
+}
+
+static void print_cmd(trxcon::trxcon_phyif_cmd_type c)
+{
+	DBGLG() << cmd2str(c) << std::endl;
+}
+
+bool upper_trx::driveControl()
+{
+	trxcon::trxcon_phyif_rsp r;
+	trxcon::trxcon_phyif_cmd cmd;
+	while (!trxcon::cmdq_to_phy.spsc_pop(&cmd)) {
+		trxcon::cmdq_to_phy.spsc_prep_pop();
+	}
+	print_cmd(cmd.type);
+
+	switch (cmd.type) {
+	case trxcon::TRXCON_PHYIF_CMDT_RESET:
+		break;
+	case trxcon::TRXCON_PHYIF_CMDT_POWERON:
+
+		if (!mOn) {
+			// start_ms();
+			set_upper_ready(true);
+			mOn = true;
+		}
+		break;
+	case trxcon::TRXCON_PHYIF_CMDT_POWEROFF:
+		// set_upper_ready(false);
+		set_ta(0);
+		break;
+	case trxcon::TRXCON_PHYIF_CMDT_MEASURE:
+		r.type = trxcon::trxcon_phyif_cmd_type::TRXCON_PHYIF_CMDT_MEASURE;
+		r.param.measure.band_arfcn = cmd.param.measure.band_arfcn;
+		r.param.measure.dbm = -80;
+		tuneRx(trxcon::gsm_arfcn2freq10(cmd.param.measure.band_arfcn, 0) * 1000 * 100);
+		tuneTx(trxcon::gsm_arfcn2freq10(cmd.param.measure.band_arfcn, 1) * 1000 * 100);
+		trxcon::cmdq_from_phy.spsc_push(&r);
+		break;
+	case trxcon::TRXCON_PHYIF_CMDT_SETFREQ_H0:
+		// gsm_arfcn2band_rc(uint16_t arfcn, enum gsm_band *band)
+		tuneRx(trxcon::gsm_arfcn2freq10(cmd.param.setfreq_h0.band_arfcn, 0) * 1000 * 100);
+		tuneTx(trxcon::gsm_arfcn2freq10(cmd.param.setfreq_h0.band_arfcn, 1) * 1000 * 100);
+
+		break;
+	case trxcon::TRXCON_PHYIF_CMDT_SETFREQ_H1:
+		break;
+	case trxcon::TRXCON_PHYIF_CMDT_SETSLOT:
+		break;
+	case trxcon::TRXCON_PHYIF_CMDT_SETTA:
+		set_ta(cmd.param.setta.ta);
+		break;
+	}
+	return false;
+}
+
+// trxcon C call(back) if
+extern "C" {
+int trxcon_phyif_handle_burst_req(void *phyif, const struct trxcon::trxcon_phyif_burst_req *br)
+{
+	if (br->burst_len == 0) // dummy/nope
+		return 0;
+	assert(br->burst != 0);
+
+	trxcon::internal_q_tx_buf b;
+	b.r = *br;
+	memcpy(b.buf, (void *)br->burst, br->burst_len);
+	trxcon::txq.spsc_push(&b);
+	return 0;
+}
+
+int trxcon_phyif_handle_cmd(void *phyif, const struct trxcon::trxcon_phyif_cmd *cmd)
+{
+	DBGLG() << "TOP C: " << cmd2str(cmd->type) << std::endl;
+	trxcon::cmdq_to_phy.spsc_push(cmd);
+	// q for resp polling happens in main loop
+	return 0;
+}
+
+void trxcon_phyif_close(void *phyif)
+{
+}
+
+void trxcon_l1ctl_close(struct trxcon::trxcon_inst *trxcon)
+{
+	/* Avoid use-after-free: both *fi and *trxcon are children of
+	 * the L2IF (L1CTL connection), so we need to re-parent *fi
+	 * to NULL before calling l1ctl_client_conn_close(). */
+	talloc_steal(NULL, trxcon->fi);
+	trxcon::l1ctl_client_conn_close((struct trxcon::l1ctl_client *)trxcon->l2if);
+}
+
+int trxcon_l1ctl_send(struct trxcon::trxcon_inst *trxcon, struct trxcon::msgb *msg)
+{
+	struct trxcon::l1ctl_client *l1c = (struct trxcon::l1ctl_client *)trxcon->l2if;
+
+	return trxcon::l1ctl_client_send(l1c, msg);
+}
+}
+
+int main(int argc, char *argv[])
+{
+	auto tall_trxcon_ctx = talloc_init("trxcon context");
+	trxcon::msgb_talloc_ctx_init(tall_trxcon_ctx, 0);
+	trxcon::trxc_log_init(tall_trxcon_ctx);
+
+	trxcon::g_trxcon = trxcon::trxcon_inst_alloc(tall_trxcon_ctx, 0, 3);
+	trxcon::g_trxcon->gsmtap = 0;
+	trxcon::g_trxcon->phyif = (void *)0x1234;
+
+	pthread_setname_np(pthread_self(), "main_trxc");
+	convolve_init();
+	convert_init();
+	sigProcLibSetup();
+	initvita();
+
+	int status = 0;
+	auto trx = new upper_trx();
+	trx->do_auto_gain = true;
+
+	status = trx->init_dev_and_streams();
+	trx->set_name_aff_sched("main", 3, SCHED_FIFO, sched_get_priority_max(SCHED_FIFO) - 5);
+
+	trxcon::trxc_l1ctl_init(tall_trxcon_ctx);
+
+	trx->start_threads();
+
+	return status;
+}
diff --git a/Transceiver52M/ms/ms_upper.h b/Transceiver52M/ms/ms_upper.h
new file mode 100644
index 0000000..a8a86a4
--- /dev/null
+++ b/Transceiver52M/ms/ms_upper.h
@@ -0,0 +1,57 @@
+
+#pragma once
+
+/*
+ * (C) 2022 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Eric Wild <ewild@sysmocom.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include <netdb.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+
+#include "GSMCommon.h"
+#include "radioClock.h"
+#include "ms.h"
+
+namespace trxcon
+{
+extern "C" {
+#include <osmocom/bb/trxcon/phyif.h>
+#include <osmocom/bb/trxcon/trx_if.h>
+}
+} // namespace trxcon
+class upper_trx : public ms_trx {
+	bool mOn;
+	char demodded_softbits[444];
+
+	// void driveControl();
+	bool driveControl();
+	void driveReceiveFIFO();
+	void driveTx();
+
+	bool pullRadioVector(GSM::Time &wTime, int &RSSI, int &timingOffset) __attribute__((optnone));
+
+	std::thread thr_control, thr_rx, thr_tx;
+
+    public:
+	void start_threads();
+	void start_lower_ms();
+
+	upper_trx(){};
+};
diff --git a/Transceiver52M/ms/sch.c b/Transceiver52M/ms/sch.c
new file mode 100644
index 0000000..d05c4e2
--- /dev/null
+++ b/Transceiver52M/ms/sch.c
@@ -0,0 +1,324 @@
+/*
+ * (C) 2013 by Andreas Eversberg <jolly@eversberg.eu>
+ * (C) 2015 by Alexander Chemeris <Alexander.Chemeris@fairwaves.co>
+ * (C) 2016 by Tom Tsou <tom.tsou@ettus.com>
+ * (C) 2017 by Harald Welte <laforge@gnumonks.org>
+ * (C) 2022 by 2022 by sysmocom s.f.m.c. GmbH <info@sysmocom.de> / Eric Wild <ewild@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#include <complex.h>
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+
+#include <osmocom/core/bits.h>
+#include <osmocom/core/conv.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/crcgen.h>
+#include <osmocom/coding/gsm0503_coding.h>
+#include <osmocom/coding/gsm0503_parity.h>
+
+#include "sch.h"
+
+/* GSM 04.08, 9.1.30 Synchronization channel information */
+struct sch_packed_info {
+	ubit_t t1_hi[2];
+	ubit_t bsic[6];
+	ubit_t t1_md[8];
+	ubit_t t3p_hi[2];
+	ubit_t t2[5];
+	ubit_t t1_lo[1];
+	ubit_t t3p_lo[1];
+} __attribute__((packed));
+
+struct sch_burst {
+	sbit_t tail0[3];
+	sbit_t data0[39];
+	sbit_t etsc[64];
+	sbit_t data1[39];
+	sbit_t tail1[3];
+	sbit_t guard[8];
+} __attribute__((packed));
+
+static const uint8_t sch_next_output[][2] = {
+	{ 0, 3 }, { 1, 2 }, { 0, 3 }, { 1, 2 },
+	{ 3, 0 }, { 2, 1 }, { 3, 0 }, { 2, 1 },
+	{ 3, 0 }, { 2, 1 }, { 3, 0 }, { 2, 1 },
+	{ 0, 3 }, { 1, 2 }, { 0, 3 }, { 1, 2 },
+};
+
+static const uint8_t sch_next_state[][2] = {
+	{  0,  1 }, {  2,  3 }, {  4,  5 }, {  6,  7 },
+	{  8,  9 }, { 10, 11 }, { 12, 13 }, { 14, 15 },
+	{  0,  1 }, {  2,  3 }, {  4,  5 }, {  6,  7 },
+	{  8,  9 }, { 10, 11 }, { 12, 13 }, { 14, 15 },
+};
+
+static const struct osmo_conv_code gsm_conv_sch = {
+	.N = 2,
+	.K = 5,
+	.len = GSM_SCH_UNCODED_LEN,
+	.next_output = sch_next_output,
+	.next_state  = sch_next_state,
+};
+
+#define GSM_MAX_BURST_LEN	157 * 4
+#define GSM_SYM_RATE		(1625e3 / 6) * 4
+
+/* Pre-generated FCCH measurement tone */
+static complex float fcch_ref[GSM_MAX_BURST_LEN];
+
+int float_to_sbit(const float *in, sbit_t *out, float scale, int len)
+{
+	int i;
+
+	for (i = 0; i < len; i++) {
+		out[i] = (in[i] - 0.5f) * scale;
+	}
+
+	return 0;
+}
+
+/* Check if FN contains a FCCH burst */
+int gsm_fcch_check_fn(int fn)
+{
+	int fn51 = fn % 51;
+
+	switch (fn51) {
+	case 0:
+	case 10:
+	case 20:
+	case 30:
+	case 40:
+		return 1;
+	}
+
+	return 0;
+}
+
+/* Check if FN contains a SCH burst */
+int gsm_sch_check_fn(int fn)
+{
+	int fn51 = fn % 51;
+
+	switch (fn51) {
+	case 1:
+	case 11:
+	case 21:
+	case 31:
+	case 41:
+		return 1;
+	}
+
+	return 0;
+}
+
+int gsm_fcch_check_ts(int ts, int fn) {
+	return ts == 0 && gsm_fcch_check_fn(fn);
+}
+
+int gsm_sch_check_ts(int ts, int fn) {
+	return ts == 0 && gsm_sch_check_fn(fn);
+}
+
+/* SCH (T1, T2, T3p) to full FN value */
+int gsm_sch_to_fn(struct sch_info *sch)
+{
+	int t1 = sch->t1;
+	int t2 = sch->t2;
+	int t3p = sch->t3p;
+
+	if ((t1 < 0) || (t2 < 0) || (t3p < 0))
+		return -1;
+	int tt;
+	int t3 = t3p * 10 + 1;
+
+	if (t3 < t2)
+		tt = (t3 + 26) - t2;
+	else
+		tt = (t3 - t2) % 26;
+
+	return t1 * 51 * 26 + tt * 51 + t3;
+}
+
+/* Parse encoded SCH message */
+int gsm_sch_parse(const uint8_t *info, struct sch_info *desc)
+{
+	struct sch_packed_info *p = (struct sch_packed_info *) info;
+
+	desc->bsic = (p->bsic[0] << 0) | (p->bsic[1] << 1) |
+		     (p->bsic[2] << 2) | (p->bsic[3] << 3) |
+		     (p->bsic[4] << 4) | (p->bsic[5] << 5);
+
+	desc->t1 = (p->t1_lo[0] << 0) | (p->t1_md[0] << 1) |
+		   (p->t1_md[1] << 2) | (p->t1_md[2] << 3) |
+		   (p->t1_md[3] << 4) | (p->t1_md[4] << 5) |
+		   (p->t1_md[5] << 6) | (p->t1_md[6] << 7) |
+		   (p->t1_md[7] << 8) | (p->t1_hi[0] << 9) |
+		   (p->t1_hi[1] << 10);
+
+	desc->t2 = (p->t2[0] << 0) | (p->t2[1] << 1) |
+		   (p->t2[2] << 2) | (p->t2[3] << 3) |
+		   (p->t2[4] << 4);
+
+	desc->t3p = (p->t3p_lo[0] << 0) | (p->t3p_hi[0] << 1) |
+		    (p->t3p_hi[1] << 2);
+
+	return 0;
+}
+
+/* From osmo-bts */
+int gsm_sch_decode(uint8_t *info, sbit_t *data)
+{
+	int rc;
+	ubit_t uncoded[GSM_SCH_UNCODED_LEN];
+
+	osmo_conv_decode(&gsm_conv_sch, data, uncoded);
+
+	rc = osmo_crc16gen_check_bits(&gsm0503_sch_crc10,
+				      uncoded, GSM_SCH_INFO_LEN,
+				      uncoded + GSM_SCH_INFO_LEN);
+	if (rc)
+		return -1;
+
+	memcpy(info, uncoded, GSM_SCH_INFO_LEN * sizeof(ubit_t));
+
+	return 0;
+}
+
+#define FCCH_TAIL_BITS_LEN	3*4
+#define FCCH_DATA_LEN	100*4//	142
+#if 1
+/* Compute FCCH frequency offset */
+double org_gsm_fcch_offset(float *burst, int len)
+{
+	int i, start, end;
+	float a, b, c, d, ang, avg = 0.0f;
+	double freq;
+
+	if (len > GSM_MAX_BURST_LEN)
+		len = GSM_MAX_BURST_LEN;
+
+	for (i = 0; i < len; i++) {
+		a = burst[2 * i + 0];
+		b = burst[2 * i + 1];
+		c = crealf(fcch_ref[i]);
+		d = cimagf(fcch_ref[i]);
+
+		burst[2 * i + 0] = a * c - b * d;
+		burst[2 * i + 1] = a * d + b * c;
+	}
+
+	start = FCCH_TAIL_BITS_LEN;
+	end = start + FCCH_DATA_LEN;
+
+	for (i = start; i < end; i++) {
+		a = cargf(burst[2 * (i - 1) + 0] +
+			  burst[2 * (i - 1) + 1] * I);
+		b = cargf(burst[2 * i + 0] +
+			  burst[2 * i + 1] * I);
+
+		ang = b - a;
+
+		if (ang > M_PI)
+			ang -= 2 * M_PI;
+		else if (ang < -M_PI)
+			ang += 2 * M_PI;
+
+		avg += ang;
+	}
+
+	avg /= (float) (end - start);
+	freq = avg / (2 * M_PI) * GSM_SYM_RATE;
+
+	return freq;
+}
+
+
+static const int L1 = 3;
+static const int L2 = 32;
+static const int N1 = 92;
+static const int N2 = 92;
+
+static struct { int8_t r; int8_t s; } P_inv_table[3+32];
+
+void pinv(int P, int8_t* r, int8_t* s, int L1, int L2) {
+	for (int i = 0; i < L1; i++)
+		for (int j = 0; j < L2; j++)
+			if (P == L2 * i - L1 * j) {
+				*r = i;
+				*s = j;
+				return;
+			}
+}
+
+
+float ac_sum_with_lag( complex float* in, int lag, int offset, int N) {
+	complex float v = 0 + 0*I;
+	int total_offset = offset + lag;
+	for (int s = 0; s < N; s++)
+		v += in[s + total_offset] * conjf(in[s + total_offset - lag]);
+	return cargf(v);
+}
+
+
+double gsm_fcch_offset(float *burst, int len)
+{
+	int start;
+
+	const float fs = 13. / 48. * 1e6 * 4;
+	const float expected_fcch_val = ((2 * M_PI) / (fs)) * 67700;
+
+	if (len > GSM_MAX_BURST_LEN)
+		len = GSM_MAX_BURST_LEN;
+
+	start = FCCH_TAIL_BITS_LEN+10 * 4;
+	float alpha_one = ac_sum_with_lag((complex float*)burst, L1, start, N1);
+	float alpha_two = ac_sum_with_lag((complex float*)burst, L2, start, N2);
+
+	float P_unrounded = (L1 * alpha_two - L2 * alpha_one) / (2 * M_PI);
+	int P = roundf(P_unrounded);
+
+	int8_t r = 0, s = 0;
+	pinv(P, &r, &s, L1, L2);
+
+	float omegal1 = (alpha_one + 2 * M_PI * r) / L1;
+	float omegal2 = (alpha_two + 2 * M_PI * s) / L2;
+
+	float rv = org_gsm_fcch_offset(burst, len);
+	//return rv;
+
+	float reval = GSM_SYM_RATE / (2 * M_PI) * (expected_fcch_val - (omegal1+omegal2)/2);
+	//fprintf(stderr, "XX rv %f %f %f %f\n", rv, reval, omegal1 / (2 * M_PI) * fs, omegal2 / (2 * M_PI) * fs);
+
+	//fprintf(stderr, "XX rv %f %f\n", rv, reval);
+
+	return -reval;
+}
+#endif
+/* Generate FCCH measurement tone */
+static __attribute__((constructor)) void init()
+{
+	int i;
+	double freq = 0.25;
+
+	for (i = 0; i < GSM_MAX_BURST_LEN; i++) {
+		fcch_ref[i] = sin(2 * M_PI * freq * (double) i) +
+			      cos(2 * M_PI * freq * (double) i) * I;
+	}
+
+}
diff --git a/Transceiver52M/ms/sch.h b/Transceiver52M/ms/sch.h
new file mode 100644
index 0000000..1af2114
--- /dev/null
+++ b/Transceiver52M/ms/sch.h
@@ -0,0 +1,48 @@
+#pragma once
+/*
+ * (C) 2013 by Andreas Eversberg <jolly@eversberg.eu>
+ * (C) 2015 by Alexander Chemeris <Alexander.Chemeris@fairwaves.co>
+ * (C) 2016 by Tom Tsou <tom.tsou@ettus.com>
+ * (C) 2017 by Harald Welte <laforge@gnumonks.org>
+ * (C) 2022 by 2022 by sysmocom s.f.m.c. GmbH <info@sysmocom.de> / Eric Wild <ewild@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#include <osmocom/core/bits.h>
+
+struct sch_info  {
+	int bsic;
+	int t1;
+	int t2;
+	int t3p;
+};
+
+#define GSM_SCH_INFO_LEN		25
+#define GSM_SCH_UNCODED_LEN		35
+#define GSM_SCH_CODED_LEN		78
+
+int gsm_sch_decode(uint8_t *sb_info, sbit_t *burst);
+int gsm_sch_parse(const uint8_t *sb_info, struct sch_info *desc);
+int gsm_sch_to_fn(struct sch_info *sch);
+int gsm_sch_check_fn(int fn);
+int gsm_fcch_check_fn(int fn);
+int gsm_fcch_check_ts(int ts, int fn);
+int gsm_sch_check_ts(int ts, int fn);
+
+double gsm_fcch_offset(float *burst, int len);
+
+int float_to_sbit(const float *in, sbit_t *out, float scale, int len);
+
diff --git a/Transceiver52M/ms/uhd_specific.h b/Transceiver52M/ms/uhd_specific.h
new file mode 100644
index 0000000..ed1a72a
--- /dev/null
+++ b/Transceiver52M/ms/uhd_specific.h
@@ -0,0 +1,251 @@
+#pragma once
+
+/*
+ * (C) 2022 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Eric Wild <ewild@sysmocom.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <uhd/version.hpp>
+#include <uhd/usrp/multi_usrp.hpp>
+#include <uhd/types/metadata.hpp>
+#include <complex>
+#include <cstring>
+#include <iostream>
+#include <thread>
+
+#include <Timeval.h>
+#include <vector>
+
+using blade_sample_type = std::complex<int16_t>;
+const int SAMPLE_SCALE_FACTOR = 1;
+
+struct uhd_buf_wrap {
+	double rxticks;
+	size_t num_samps;
+	uhd::rx_metadata_t *md;
+	blade_sample_type *buf;
+	auto actual_samples_per_buffer()
+	{
+		return num_samps;
+	}
+	long get_first_ts()
+	{
+		return md->time_spec.to_ticks(rxticks);
+	}
+	int readall(blade_sample_type *outaddr)
+	{
+		memcpy(outaddr, buf, num_samps * sizeof(blade_sample_type));
+		return num_samps;
+	}
+	int read_n(blade_sample_type *outaddr, int start, int num)
+	{
+		assert(start >= 0);
+		auto to_read = std::min((int)num_samps - start, num);
+		assert(to_read >= 0);
+		memcpy(outaddr, buf + start, to_read * sizeof(blade_sample_type));
+		return to_read;
+	}
+};
+
+using dev_buf_t = uhd_buf_wrap;
+using bh_fn_t = std::function<int(dev_buf_t *)>;
+
+template <typename T> struct uhd_hw {
+	uhd::usrp::multi_usrp::sptr dev;
+	uhd::rx_streamer::sptr rx_stream;
+	uhd::tx_streamer::sptr tx_stream;
+	blade_sample_type *one_pkt_buf;
+	std::vector<blade_sample_type *> pkt_ptrs;
+	size_t rx_spp;
+	double rxticks;
+	const unsigned int rxFullScale, txFullScale;
+	const int rxtxdelay;
+	float rxgain, txgain;
+	volatile bool stop_me_flag;
+
+	virtual ~uhd_hw()
+	{
+		delete[] one_pkt_buf;
+	}
+	uhd_hw() : rxFullScale(32767), txFullScale(32767), rxtxdelay(-67), stop_me_flag(false)
+	{
+	}
+
+	void close_device()
+	{
+		stop_me_flag = true;
+	}
+
+	bool tuneTx(double freq, size_t chan = 0)
+	{
+		msleep(25);
+		dev->set_tx_freq(freq, chan);
+		msleep(25);
+		return true;
+	};
+	bool tuneRx(double freq, size_t chan = 0)
+	{
+		msleep(25);
+		dev->set_rx_freq(freq, chan);
+		msleep(25);
+		return true;
+	};
+	bool tuneRxOffset(double offset, size_t chan = 0)
+	{
+		return true;
+	};
+
+	double setRxGain(double dB, size_t chan = 0)
+	{
+		rxgain = dB;
+		msleep(25);
+		dev->set_rx_gain(dB, chan);
+		msleep(25);
+		return dB;
+	};
+	double setTxGain(double dB, size_t chan = 0)
+	{
+		txgain = dB;
+		msleep(25);
+		dev->set_tx_gain(dB, chan);
+		msleep(25);
+		return dB;
+	};
+	int setPowerAttenuation(int atten, size_t chan = 0)
+	{
+		return atten;
+	};
+
+	int init_device(bh_fn_t rxh, bh_fn_t txh)
+	{
+		auto const lock_delay_ms = 500;
+		auto const mcr = 26e6;
+		auto const rate = (1625e3 / 6) * 4;
+		auto const ref = "external";
+		auto const gain = 35;
+		auto const freq = 931.4e6; // 936.8e6
+		auto bw = 0.5e6;
+		auto const channel = 0;
+		std::string args = {};
+
+		dev = uhd::usrp::multi_usrp::make(args);
+		std::cout << "Using Device: " << dev->get_pp_string() << std::endl;
+		dev->set_clock_source(ref);
+		dev->set_master_clock_rate(mcr);
+		dev->set_rx_rate(rate, channel);
+		dev->set_tx_rate(rate, channel);
+		uhd::tune_request_t tune_request(freq, 0);
+		dev->set_rx_freq(tune_request, channel);
+		dev->set_rx_gain(gain, channel);
+		dev->set_tx_gain(60, channel);
+		dev->set_rx_bandwidth(bw, channel);
+		dev->set_tx_bandwidth(bw, channel);
+
+		while (!(dev->get_rx_sensor("lo_locked", channel).to_bool() &&
+			 dev->get_mboard_sensor("ref_locked").to_bool()))
+			std::this_thread::sleep_for(std::chrono::milliseconds(lock_delay_ms));
+
+		uhd::stream_args_t stream_args("sc16", "sc16");
+		rx_stream = dev->get_rx_stream(stream_args);
+		uhd::stream_args_t stream_args2("sc16", "sc16");
+		tx_stream = dev->get_tx_stream(stream_args2);
+
+		rx_spp = rx_stream->get_max_num_samps();
+		rxticks = dev->get_rx_rate();
+		assert(rxticks == dev->get_tx_rate());
+		one_pkt_buf = new blade_sample_type[rx_spp];
+		pkt_ptrs = { 1, &one_pkt_buf[0] };
+		return 0;
+	}
+
+	void *rx_cb(bh_fn_t burst_handler)
+	{
+		void *ret;
+		static int to_skip = 0;
+
+		uhd::rx_metadata_t md;
+		auto num_rx_samps = rx_stream->recv(pkt_ptrs.front(), rx_spp, md, 1.0, true);
+
+		if (md.error_code == uhd::rx_metadata_t::ERROR_CODE_TIMEOUT) {
+			std::cerr << boost::format("Timeout while streaming") << std::endl;
+			exit(0);
+		}
+		if (md.error_code == uhd::rx_metadata_t::ERROR_CODE_OVERFLOW) {
+			std::cerr << boost::format("Got an overflow indication\n") << std::endl;
+			exit(0);
+		}
+		if (md.error_code != uhd::rx_metadata_t::ERROR_CODE_NONE) {
+			std::cerr << str(boost::format("Receiver error: %s") % md.strerror());
+			exit(0);
+		}
+
+		dev_buf_t rcd = { rxticks, num_rx_samps, &md, &one_pkt_buf[0] };
+
+		if (to_skip < 120) // prevents weird overflows on startup
+			to_skip++;
+		else {
+			burst_handler(&rcd);
+		}
+
+		return ret;
+	}
+
+	auto get_rx_burst_handler_fn(bh_fn_t burst_handler)
+	{
+		auto fn = [this, burst_handler] {
+			pthread_setname_np(pthread_self(), "rxrun");
+
+			uhd::stream_cmd_t stream_cmd(uhd::stream_cmd_t::STREAM_MODE_START_CONTINUOUS);
+			stream_cmd.stream_now = true;
+			stream_cmd.time_spec = uhd::time_spec_t();
+			rx_stream->issue_stream_cmd(stream_cmd);
+
+			while (!stop_me_flag) {
+				rx_cb(burst_handler);
+			}
+		};
+		return fn;
+	}
+	auto get_tx_burst_handler_fn(bh_fn_t burst_handler)
+	{
+		auto fn = [] {
+			// dummy
+		};
+		return fn;
+	}
+	void submit_burst_ts(blade_sample_type *buffer, int len, uint64_t ts)
+	{
+		uhd::tx_metadata_t m = {};
+		m.end_of_burst = true;
+		m.start_of_burst = true;
+		m.has_time_spec = true;
+		m.time_spec = m.time_spec.from_ticks(ts + rxtxdelay, rxticks); // uhd specific b210 delay!
+		std::vector<void *> ptrs(1, buffer);
+
+		tx_stream->send(ptrs, len, m, 1.0);
+#ifdef DBGXX
+		uhd::async_metadata_t async_md;
+		bool tx_ack = false;
+		while (!tx_ack && tx_stream->recv_async_msg(async_md)) {
+			tx_ack = (async_md.event_code == uhd::async_metadata_t::EVENT_CODE_BURST_ACK);
+		}
+		std::cout << (tx_ack ? "yay" : "nay") << " " << async_md.time_spec.to_ticks(rxticks) << std::endl;
+#endif
+	}
+};
\ No newline at end of file
diff --git a/configure.ac b/configure.ac
index 020f7e3..6ce9ad4 100644
--- a/configure.ac
+++ b/configure.ac
@@ -327,6 +327,14 @@
 AC_MSG_RESULT([CXXFLAGS="$CXXFLAGS"])
 AC_MSG_RESULT([LDFLAGS="$LDFLAGS"])
 
+AM_CONDITIONAL(ENABLE_MS_TRX, [test -d osmocom-bb])
+
+if ENABLE_MS_TRX; then
+	AC_MSG_NOTICE(["Enabling ms-trx..."])
+    AC_CONFIG_SUBDIRS([osmocom-bb/src/host/trxcon])
+fi
+
+
 dnl Output files
 AC_CONFIG_FILES([\
     Makefile \
@@ -353,8 +361,7 @@
     doc/examples/Makefile \
     contrib/Makefile \
     contrib/systemd/Makefile \
+    doc/manuals/Makefile \
+    contrib/osmo-trx.spec \
 ])
-
-AC_OUTPUT(
-	doc/manuals/Makefile
-	contrib/osmo-trx.spec)
+AC_OUTPUT