new ms

Change-Id: I7c5abe57182e7ef508cac4068c0b41f905d39fd6
diff --git a/.gitignore b/.gitignore
index 0140f9b..79e1531 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,12 @@
 Transceiver52M/osmo-trx-usrp1
 Transceiver52M/osmo-trx-lms
 Transceiver52M/osmo-trx-ipc
+Transceiver52M/osmo-trx-blade
+Transceiver52M/osmo-trx-syncthing-blade
+Transceiver52M/osmo-trx-syncthing-uhd
+Transceiver52M/osmo-trx-ms-blade
+Transceiver52M/osmo-trx-ms-uhd
+
 
 .clang-format
 
diff --git a/GSM/GSMCommon.cpp b/GSM/GSMCommon.cpp
index 59548b2..a9e2bb1 100644
--- a/GSM/GSMCommon.cpp
+++ b/GSM/GSMCommon.cpp
@@ -55,6 +55,7 @@
 };
 
 const BitVector GSM::gDummyBurst("0001111101101110110000010100100111000001001000100000001111100011100010111000101110001010111010010100011001100111001111010011111000100101111101010000");
+const BitVector GSM::gDummyBurstTSC("01110001011100010111000101");
 
 /* 3GPP TS 05.02, section 5.2.7 "Access burst (AB)", synch. sequence bits */
 const BitVector GSM::gRACHSynchSequenceTS0("01001011011111111001100110101010001111000");  /* GSM, GMSK (default) */
diff --git a/GSM/GSMCommon.h b/GSM/GSMCommon.h
index 386365a..aa059c2 100644
--- a/GSM/GSMCommon.h
+++ b/GSM/GSMCommon.h
@@ -52,6 +52,7 @@
 
 /** C0T0 filler burst, GSM 05.02, 5.2.6 */
 extern const BitVector gDummyBurst;
+extern const BitVector gDummyBurstTSC;
 
 /** Random access burst synch. sequence */
 extern const BitVector gRACHSynchSequenceTS0;
diff --git a/Transceiver52M/Makefile.am b/Transceiver52M/Makefile.am
index 0699ffb..78a8a27 100644
--- a/Transceiver52M/Makefile.am
+++ b/Transceiver52M/Makefile.am
@@ -24,8 +24,8 @@
 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_CXXFLAGS = -lpthread $(LIBOSMOCORE_CFLAGS) $(LIBOSMOCTRL_CFLAGS) $(LIBOSMOVTY_CFLAGS) -mcpu=cortex-a72 -mfloat-abi=hard -mfpu=neon-fp-armv8
+AM_CFLAGS = -lpthread $(LIBOSMOCORE_CFLAGS) $(LIBOSMOCTRL_CFLAGS) $(LIBOSMOVTY_CFLAGS) -mcpu=cortex-a72 -mfloat-abi=hard -mfpu=neon-fp-armv8
 
 noinst_LTLIBRARIES = libtransceiver_common.la
 
@@ -38,8 +38,6 @@
 	radioBuffer.cpp \
 	sigProcLib.cpp \
 	signalVector.cpp \
-	Transceiver.cpp \
-	Transceiver2.cpp \
 	ChannelizerBase.cpp \
 	Channelizer.cpp \
 	Synthesis.cpp \
@@ -86,52 +84,47 @@
 bin_PROGRAMS =
 
 if DEVICE_UHD
-bin_PROGRAMS += osmo-trx-uhd
-osmo_trx_uhd_SOURCES = osmo-trx.cpp
-osmo_trx_uhd_LDADD = \
-	$(builddir)/device/uhd/libdevice.la \
+bin_PROGRAMS += osmo-trx-ms-uhd
+osmo_trx_ms_uhd_SOURCES = ms/syncthing.cpp ms/ms_rx_lower.cpp ms/ms_rx_upper.cpp ms/ms_commandhandler.cpp
+osmo_trx_ms_uhd_LDADD = \
+	$(builddir)/device/bladerf/libdevice.la \
 	$(COMMON_LDADD) \
 	$(UHD_LIBS) \
 	$(TRXCON_LA)
-osmo_trx_uhd_CPPFLAGS  = $(AM_CPPFLAGS) $(UHD_CFLAGS)
-endif
+osmo_trx_ms_uhd_CPPFLAGS  = $(AM_CPPFLAGS) $(UHD_CFLAGS) -DBUILDUHD
 
-if DEVICE_USRP1
-bin_PROGRAMS += osmo-trx-usrp1
-osmo_trx_usrp1_SOURCES = osmo-trx.cpp
-osmo_trx_usrp1_LDADD = \
-	$(builddir)/device/usrp1/libdevice.la \
-	$(COMMON_LDADD) \
-	$(USRP_LIBS)
-osmo_trx_usrp1_CPPFLAGS  = $(AM_CPPFLAGS) $(USRP_CFLAGS)
-endif
+bin_PROGRAMS += osmo-trx-syncthing-uhd
+osmo_trx_syncthing_uhd_SOURCES = $(osmo_trx_ms_uhd_SOURCES) ms/ms_rx_burst.cpp
+osmo_trx_syncthing_uhd_LDADD =  $(osmo_trx_ms_uhd_LDADD)
+osmo_trx_syncthing_uhd_CPPFLAGS  = $(AM_CPPFLAGS) $(UHD_CFLAGS) -DSYNCTHINGONLY -DBUILDUHD
+#osmo_trx_syncthing_LDFLAGS  = -fsanitize=address,undefined -shared-libsan
 
-if DEVICE_LMS
-bin_PROGRAMS += osmo-trx-lms
-osmo_trx_lms_SOURCES = osmo-trx.cpp
-osmo_trx_lms_LDADD = \
-	$(builddir)/device/lms/libdevice.la \
-	$(COMMON_LDADD) \
-	$(LMS_LIBS)
-osmo_trx_lms_CPPFLAGS  = $(AM_CPPFLAGS) $(LMS_CFLAGS)
-endif
-
-if DEVICE_IPC
-bin_PROGRAMS += osmo-trx-ipc
-osmo_trx_ipc_SOURCES = osmo-trx.cpp
-osmo_trx_ipc_LDADD = \
-	$(builddir)/device/ipc/libdevice.la \
-	$(COMMON_LDADD)
-osmo_trx_ipc_CPPFLAGS  = $(AM_CPPFLAGS)
 endif
 
 if DEVICE_BLADE
-bin_PROGRAMS += osmo-trx-blade
-osmo_trx_blade_SOURCES = osmo-trx.cpp
-osmo_trx_blade_LDADD = \
+bin_PROGRAMS += osmo-trx-ms-blade
+osmo_trx_ms_blade_SOURCES = ms/syncthing.cpp ms/ms_rx_lower.cpp ms/ms_rx_upper.cpp ms/ms_commandhandler.cpp
+osmo_trx_ms_blade_LDADD = \
 	$(builddir)/device/bladerf/libdevice.la \
 	$(COMMON_LDADD) \
-	$(BLADE_LIBS)
-osmo_trx_blade_CPPFLAGS  = $(AM_CPPFLAGS) $(BLADE_CFLAGS)
+	$(BLADE_LIBS) \
+	$(TRXCON_LA)
+osmo_trx_ms_blade_CPPFLAGS  = $(AM_CPPFLAGS) $(BLADE_CFLAGS) -DBUILDBLADE
+
+bin_PROGRAMS += osmo-trx-syncthing-blade
+osmo_trx_syncthing_blade_SOURCES = $(osmo_trx_ms_blade_SOURCES) ms/ms_rx_burst.cpp
+osmo_trx_syncthing_blade_LDADD =  $(osmo_trx_ms_blade_LDADD)
+osmo_trx_syncthing_blade_CPPFLAGS  = $(AM_CPPFLAGS) $(BLADE_CFLAGS) -DSYNCTHINGONLY -DBUILDBLADE -mcpu=cortex-a72 -mfloat-abi=hard -mfpu=neon-fp-armv8
+#osmo_trx_syncthing_LDFLAGS  = -fsanitize=address,undefined -shared-libsan
 endif
 
+
+noinst_HEADERS += \
+	ms/syncthing.h \
+	ms/bladerf_specific.h \
+	ms/uhd_specific.h \
+	ms/ms_rx_upper.h \
+	ms/ms_state.h \
+	itrq.h
+# -fsanitize=address,undefined -shared-libsan -O0
+#
diff --git a/Transceiver52M/Resampler.cpp b/Transceiver52M/Resampler.cpp
index 910c7ff..00d19ec 100644
--- a/Transceiver52M/Resampler.cpp
+++ b/Transceiver52M/Resampler.cpp
@@ -32,7 +32,7 @@
 #define M_PI			3.14159265358979323846264338327f
 #endif
 
-#define MAX_OUTPUT_LEN		4096
+#define MAX_OUTPUT_LEN 4096 * 4
 
 using namespace std;
 
diff --git a/Transceiver52M/Transceiver2.cpp b/Transceiver52M/Transceiver2.cpp
index 4430b83..983643b 100644
--- a/Transceiver52M/Transceiver2.cpp
+++ b/Transceiver52M/Transceiver2.cpp
@@ -26,6 +26,7 @@
 #include <stdio.h>
 #include <Logger.h>
 #include "Transceiver2.h"
+#include <grgsm_vitac/grgsm_vitac.h>
 
 extern "C" {
 #include "sch.h"
@@ -635,35 +636,108 @@
   if (rc < 0)
     goto release;
 
-  if(type == TSC){
-    unsigned char outbin[148];
+#if 0
+  if(type == SCH){
+    unsigned char outbin[148] = {0};
+
+    bits = demodAnyBurst(*burst, type, rx_sps, &ebp);
+
 
     auto start = reinterpret_cast<float*>(burst->begin());
     for(int i=0; i < 625*2; i++)
-      start[i] *= 1./32767.;
+      start[i] *= 1./2047.;
 
-    int ret = process_vita_burst(reinterpret_cast<std::complex<float>*>(burst->begin()), mTSC, outbin);
-    bits = new SoftVector();
-    bits->resize(148);
-    for(int i=0; i < 148; i++)
-      (*bits)[i] = outbin[i] < 1 ? -1 : 1;
+    auto ss = reinterpret_cast<std::complex<float>*>(burst->begin());
 
-    // printme = ret >= 0 ? true : false;
+ //   int ret = process_vita_sc_burst(ss, mTSC, outbin, 0);
 
-    // if(printme) {
-    //   std::cerr << std::endl << "vita:" << std::endl;
-    //   for(auto i : outbin)
-    //     std::cerr << (int) i;
-    //   std::cerr << std::endl << "org:" << std::endl;
-    // }
-  } else {
-    /* Ignore noise threshold on MS mode for now */
-    //if ((type == SCH) || (avg - state->mNoiseLev > 0.0))
-    bits = demodAnyBurst(*burst, type, rx_sps, &ebp);
+    {
+      std::vector<gr_complex> channel_imp_resp(CHAN_IMP_RESP_LENGTH* d_OSR);
+          /* Get channel impulse response */
+      auto d_c0_burst_start = get_sch_chan_imp_resp(ss, &channel_imp_resp[0], 0, (SYNC_POS + SYNC_SEARCH_RANGE) * d_OSR + SYNC_POS * d_OSR );
+
+      if(d_c0_burst_start < 0) {
+        std::cerr << " fck! offset <0! " << ebp.toa << std::endl;
+        d_c0_burst_start = 0;
+      } else {
+        std::cerr << " ## offset " << d_c0_burst_start << " vs " << ebp.toa << std::endl;
+      }
+      /* Perform MLSE detection */
+      detect_burst(ss, &channel_imp_resp[0],
+      d_c0_burst_start, outbin);
+    }
+
+    // auto bits2 = new SoftVector();
+    // bits2->resize(156);
+    // for(int i=0; i < 148; i++)
+    //   (*bits2)[i] = outbin[i] < 1 ? -1 : 1;
+
     
-    // if(printme)
-    //   for(int i=0; i < 148; i++)
-    //     std::cerr << (int) (bits->operator[](i) > 0 ? 1 : 0);
+
+    printme =  true;
+
+    if(printme) {
+      std::cerr << std::endl << "vita:" << std::endl;
+      for(auto i : outbin)
+        std::cerr << (int) i;
+      std::cerr << std::endl << "org:" << std::endl;
+      for(int i=0; i < 148; i++)
+        std::cerr << (int) (bits->operator[](i) > 0 ? 1 : 0);
+    }
+
+  } else
+#endif
+  if (type == TSC) {
+	  unsigned char outbin[148];
+
+	  // bits = demodAnyBurst(*burst, type, rx_sps, &ebp);
+
+	  {
+		  auto start = reinterpret_cast<float *>(burst->begin());
+		  for (int i = 0; i < 625 * 2; i++)
+			  start[i] *= 1. / 2047.;
+
+		  auto ss = reinterpret_cast<std::complex<float> *>(burst->begin());
+		  // int ret = process_vita_burst(reinterpret_cast<std::complex<float>*>(burst->begin()), mTSC, outbin);
+
+		  float ncmax, dcmax;
+		  std::vector<gr_complex> channel_imp_resp(CHAN_IMP_RESP_LENGTH * d_OSR),
+			  channel_imp_resp2(CHAN_IMP_RESP_LENGTH * d_OSR);
+		  auto normal_burst_start = get_norm_chan_imp_resp(ss, &channel_imp_resp[0], &ncmax, mTSC);
+		  auto dummy_burst_start = get_norm_chan_imp_resp(ss, &channel_imp_resp2[0], &dcmax, TS_DUMMY);
+		  auto is_nb = ncmax > dcmax;
+
+		  std::cerr << " ## " << is_nb << " o nb " << normal_burst_start << " db " << dummy_burst_start
+			    << " vs " << ebp.toa << std::endl;
+
+		  if (is_nb)
+			  detect_burst(ss, &channel_imp_resp[0], normal_burst_start, outbin);
+		  else
+			  detect_burst(ss, &channel_imp_resp2[0], dummy_burst_start, outbin);
+		  ;
+	  }
+
+	  bits = new SoftVector();
+	  bits->resize(148);
+	  for (int i = 0; i < 148; i++)
+		  (*bits)[i] = outbin[i] < 1 ? -1 : 1;
+
+	  // printme = ret >= 0 ? true : false;
+
+	  // if(printme) {
+	  //   std::cerr << std::endl << "vita:" << std::endl;
+	  //   for(auto i : outbin)
+	  //     std::cerr << (int) i;
+	  //   std::cerr << std::endl << "org:" << std::endl;
+	  // }
+  } else {
+	  /* Ignore noise threshold on MS mode for now */
+	  //if ((type == SCH) || (avg - state->mNoiseLev > 0.0))
+	  bits = demodAnyBurst(*burst, type, rx_sps, &ebp);
+
+	  // if(printme)
+	  //   for(int i=0; i < 148; i++)
+	  //     std::cerr << (int) (bits->operator[](i) > 0 ? 1 : 0);
   }
 
   /* MS: Decode SCH and adjust GSM clock */
diff --git a/Transceiver52M/grgsm_vitac/constants.h b/Transceiver52M/grgsm_vitac/constants.h
index f678cd4..e57ef43 100644
--- a/Transceiver52M/grgsm_vitac/constants.h
+++ b/Transceiver52M/grgsm_vitac/constants.h
@@ -28,7 +28,7 @@
 #define TS_PER_FRAME      8

 #define FRAME_BITS        (TS_PER_FRAME * TS_BITS + 2) // 156.25 * 8

 #define FCCH_POS          TAIL_BITS

-#define SYNC_POS          39

+#define SYNC_POS          (TAIL_BITS + 39)

 #define TRAIN_POS         ( TAIL_BITS + (DATA_BITS+STEALING_BIT) + 5) //first 5 bits of a training sequence

 													   //aren't used for channel impulse response estimation

 #define TRAIN_BEGINNING   5

diff --git a/Transceiver52M/grgsm_vitac/grgsm_vitac.cpp b/Transceiver52M/grgsm_vitac/grgsm_vitac.cpp
index a31c8bd..b4b6134 100644
--- a/Transceiver52M/grgsm_vitac/grgsm_vitac.cpp
+++ b/Transceiver52M/grgsm_vitac/grgsm_vitac.cpp
@@ -20,8 +20,7 @@
  * Boston, MA 02110-1301, USA.

  */

 

-#include "grgsm_vitac/constants.h"

-#define _CRT_SECURE_NO_WARNINGS

+#include "constants.h"

 

 #ifdef HAVE_CONFIG_H

 #include "config.h"

@@ -37,61 +36,12 @@
 #include <fstream>

 

 #include "viterbi_detector.h"

-

-

+#include "grgsm_vitac.h"

 

 //signalVector mChanResp;

 gr_complex d_sch_training_seq[N_SYNC_BITS]; ///<encoded training sequence of a SCH burst

 gr_complex d_norm_training_seq[TRAIN_SEQ_NUM][N_TRAIN_BITS]; ///<encoded training sequences of a normal and dummy burst

-int get_norm_chan_imp_resp(const gr_complex* input, gr_complex* chan_imp_resp, float* corr_max, int* corr_max_index);

-

-#define SYNC_SEARCH_RANGE 30

-

-const int d_OSR(4);

-const int d_chan_imp_length(CHAN_IMP_RESP_LENGTH);

-std::vector<gr_complex> channel_imp_resp(CHAN_IMP_RESP_LENGTH* d_OSR);

-

-void initv();

-void process();

-int

-get_sch_chan_imp_resp(const gr_complex* input,

-	gr_complex* chan_imp_resp);

-void

-detect_burst(const gr_complex* input,

-	gr_complex* chan_imp_resp, int burst_start,

-	unsigned char* output_binary);

-void

-gmsk_mapper(const unsigned char* input,

-	int nitems, gr_complex* gmsk_output, gr_complex start_point)

-	;

-gr_complex

-correlate_sequence(const gr_complex* sequence,

-	int length, const gr_complex* input)

-	;

-inline void

-autocorrelation(const gr_complex* input,

-	gr_complex* out, int nitems)

-	;

-inline void

-mafi(const gr_complex* input, int nitems,

-	gr_complex* filter, int filter_length, gr_complex* output)

-	;

-int

-get_norm_chan_imp_resp(const gr_complex* input,

-	gr_complex* chan_imp_resp, float* corr_max, int bcc)

-	;

-

-

-struct fdata {

-	unsigned int fn;

-	int tn;

-	int bcc;

-	std::string fpath;

-	std::vector<gr_complex> data;

-};

-

-

-std::vector<fdata> files_to_process;

+const int d_chan_imp_length = CHAN_IMP_RESP_LENGTH;

 

 void initvita() {

 

@@ -104,6 +54,8 @@
 	 */

 	gmsk_mapper(SYNC_BITS, N_SYNC_BITS,

 		d_sch_training_seq, gr_complex(0.0, -1.0));

+	for (auto &i : d_sch_training_seq)

+		i = conj(i);

 

 	/* Prepare bits of training sequences */

 	for (int i = 0; i < TRAIN_SEQ_NUM; i++) {

@@ -115,92 +67,12 @@
 			gr_complex(1.0, 0.0) : gr_complex(-1.0, 0.0);

 		gmsk_mapper(train_seq[i], N_TRAIN_BITS,

 			d_norm_training_seq[i], startpoint);

+		for (auto &i : d_norm_training_seq[i])

+			i = conj(i);

 	}

 

 }

 

-int

-get_sch_chan_imp_resp(const gr_complex* input,

-	gr_complex* chan_imp_resp)

-{

-	std::vector<gr_complex> correlation_buffer;

-	std::vector<float> window_energy_buffer;

-	std::vector<float> power_buffer;

-

-	int chan_imp_resp_center = 0;

-	int strongest_window_nr;

-	int burst_start;

-	float energy = 0;

-

-	int len = (SYNC_POS + SYNC_SEARCH_RANGE) * d_OSR;

-	for (int ii = SYNC_POS * d_OSR; ii < len; ii++) {

-		gr_complex correlation = correlate_sequence(&d_sch_training_seq[5],

-			N_SYNC_BITS - 10, &input[ii]);

-		correlation_buffer.push_back(correlation);

-		power_buffer.push_back(std::pow(abs(correlation), 2));

-	}

-

-	/* Compute window energies */

-	std::vector<float>::iterator iter = power_buffer.begin();

-	while (iter != power_buffer.end()) {

-		std::vector<float>::iterator iter_ii = iter;

-		bool loop_end = false;

-		energy = 0;

-

-		for (int ii = 0; ii < (d_chan_imp_length)*d_OSR; ii++, iter_ii++) {

-			if (iter_ii == power_buffer.end()) {

-				loop_end = true;

-				break;

-			}

-

-			energy += (*iter_ii);

-		}

-

-		if (loop_end)

-			break;

-

-		window_energy_buffer.push_back(energy);

-		iter++;

-	}

-

-	strongest_window_nr = max_element(window_energy_buffer.begin(),

-		window_energy_buffer.end()) - window_energy_buffer.begin();

-

-#if 0

-	d_channel_imp_resp.clear();

-#endif

-

-	float max_correlation = 0;

-	for (int ii = 0; ii < (d_chan_imp_length)*d_OSR; ii++) {

-		gr_complex correlation = correlation_buffer[strongest_window_nr + ii];

-		if (abs(correlation) > max_correlation) {

-			chan_imp_resp_center = ii;

-			max_correlation = abs(correlation);

-		}

-

-#if 0

-		d_channel_imp_resp.push_back(correlation);

-#endif

-

-		chan_imp_resp[ii] = correlation;

-	}

-

-	burst_start = strongest_window_nr + chan_imp_resp_center

-		- 48 * d_OSR - 2 * d_OSR + 2 + SYNC_POS * d_OSR;

-	return burst_start;

-}

-

-

-#if defined(__has_attribute)

-  #if __has_attribute(target_clones)

-    #if defined(__x86_64)

-    #define MULTI_VER_TARGET_ATTR __attribute__((target_clones("avx","sse4.2","sse3","sse2","sse","default")))

-    #endif

-  #else

-  #define MULTI_VER_TARGET_ATTR

-  #endif

-#endif

-

 MULTI_VER_TARGET_ATTR

 void

 detect_burst(const gr_complex* input,

@@ -228,12 +100,9 @@
 		output_binary[i] = output[i] > 0;

 }

 

-

-

-int d_c0_burst_start;

-

 int process_vita_burst(gr_complex* input, int tsc, unsigned char* output_binary) {

-	unsigned int normal_burst_start, dummy_burst_start;

+	gr_complex channel_imp_resp[CHAN_IMP_RESP_LENGTH * d_OSR];

+	int normal_burst_start, dummy_burst_start;

 	float dummy_corr_max, normal_corr_max;

 

 	dummy_burst_start = get_norm_chan_imp_resp(input,

@@ -242,45 +111,30 @@
 		&channel_imp_resp[0], &normal_corr_max, tsc);

 

 	if (normal_corr_max > dummy_corr_max) {

-		d_c0_burst_start = normal_burst_start;

-

 		/* Perform MLSE detection */

 		detect_burst(input, &channel_imp_resp[0],

 			normal_burst_start, output_binary);

 		

 		return 0;

 

-	}

-	else {

-		d_c0_burst_start = dummy_burst_start;

+	} else {

 		memcpy(output_binary, dummy_burst, 148);

 		//std::cerr << std::endl << "#NOPE#" << dd.fpath << std::endl << std::endl;

 		return -1;

 	}

-

 }

 

 int process_vita_sc_burst(gr_complex* input, int tsc, unsigned char* output_binary, int* offset) {

-

-	int ncc, bcc;

-	int t1, t2, t3;

-	int rc;

+	gr_complex channel_imp_resp[CHAN_IMP_RESP_LENGTH * d_OSR];

 

 	/* Get channel impulse response */

-	d_c0_burst_start = get_sch_chan_imp_resp(input,

-	&channel_imp_resp[0]);

-	

+	int d_c0_burst_start = get_sch_chan_imp_resp(input, &channel_imp_resp[0]);

+	//	*offset = d_c0_burst_start;

 	/* Perform MLSE detection */

 	detect_burst(input, &channel_imp_resp[0],

 	d_c0_burst_start, output_binary);

 

-	/**

-	* Decoding was successful, now

-	* compute offset from burst_start,

-	* burst should start after a guard period.

-	*/

-	*offset = d_c0_burst_start - floor((GUARD_PERIOD) * d_OSR);

-

+	return 0;

 }

 

 void

@@ -316,9 +170,9 @@
 	gr_complex result(0.0, 0.0);

 

 	for (int ii = 0; ii < length; ii++)

-		result += sequence[ii] * conj(input[ii * d_OSR]);

+		result += sequence[ii] * input[ii * d_OSR];

 

-	return result / gr_complex(length, 0);

+	return conj(result) / gr_complex(length, 0);

 }

 

 /* Computes autocorrelation for positive arguments */

@@ -350,35 +204,27 @@
 	}

 }

 

-/* Especially computations of strongest_window_nr */

-int

-get_norm_chan_imp_resp(const gr_complex* input,

-	gr_complex* chan_imp_resp, float* corr_max, int bcc)

+int get_chan_imp_resp(const gr_complex *input, gr_complex *chan_imp_resp, int search_center, int search_start_pos,

+		      int search_stop_pos, gr_complex *tseq, int tseqlen, float *corr_max)

 {

 	std::vector<gr_complex> correlation_buffer;

 	std::vector<float> window_energy_buffer;

 	std::vector<float> power_buffer;

 

-	int search_center = (int)(TRAIN_POS + 0) * d_OSR;

-	int search_start_pos = search_center + 1 - 5 * d_OSR;

-	int search_stop_pos = search_center

-		+ d_chan_imp_length * d_OSR + 5 * d_OSR;

-

 	for (int ii = search_start_pos; ii < search_stop_pos; ii++) {

-		gr_complex correlation = correlate_sequence(

-			&d_norm_training_seq[bcc][TRAIN_BEGINNING],

-			N_TRAIN_BITS - 10, &input[ii]);

+		gr_complex correlation = correlate_sequence(tseq, tseqlen, &input[ii]);

 		correlation_buffer.push_back(correlation);

 		power_buffer.push_back(std::pow(abs(correlation), 2));

 	}

 

-#if 0

-	plot(power_buffer);

-#endif

+	int strongest_corr_nr = max_element(power_buffer.begin(), power_buffer.end()) - power_buffer.begin();

 

 	/* Compute window energies */

-	std::vector<float>::iterator iter = power_buffer.begin();

-	while (iter != power_buffer.end()) {

+	auto window_energy_start_offset = strongest_corr_nr - 6 * d_OSR;

+	auto window_energy_end_offset = strongest_corr_nr + 6 * d_OSR + d_chan_imp_length * d_OSR;

+	auto iter = power_buffer.begin() + window_energy_start_offset;

+	auto iter_end = power_buffer.begin() + window_energy_end_offset;

+	while (iter != iter_end) {

 		std::vector<float>::iterator iter_ii = iter;

 		bool loop_end = false;

 		float energy = 0;

@@ -401,23 +247,25 @@
 	}

 

 	/* Calculate the strongest window number */

-	int strongest_window_nr = max_element(window_energy_buffer.begin(),

-		window_energy_buffer.end() - d_chan_imp_length * d_OSR)

-		- window_energy_buffer.begin();

+	int strongest_window_nr = window_energy_start_offset +

+				  max_element(window_energy_buffer.begin(), window_energy_buffer.end()) -

+				  window_energy_buffer.begin();

 

-	if (strongest_window_nr < 0)

-		strongest_window_nr = 0;

+	// auto window_search_start = window_energy_buffer.begin() + strongest_corr_nr - 5* d_OSR;

+	// auto window_search_end = window_energy_buffer.begin() + strongest_corr_nr + 10* d_OSR;

+	// window_search_end = window_search_end >= window_energy_buffer.end() ? window_energy_buffer.end() : window_search_end;

+

+	// /* Calculate the strongest window number */

+	// int strongest_window_nr = max_element(window_search_start, window_search_end /* - d_chan_imp_length * d_OSR*/) - window_energy_buffer.begin();

+

+	// if (strongest_window_nr < 0)

+	// 	strongest_window_nr = 0;

 

 	float max_correlation = 0;

 	for (int ii = 0; ii < d_chan_imp_length * d_OSR; ii++) {

 		gr_complex correlation = correlation_buffer[strongest_window_nr + ii];

 		if (abs(correlation) > max_correlation)

 			max_correlation = abs(correlation);

-

-#if 0

-		d_channel_imp_resp.push_back(correlation);

-#endif

-

 		chan_imp_resp[ii] = correlation;

 	}

 

@@ -427,8 +275,57 @@
 	 * Compute first sample position, which corresponds

 	 * to the first sample of the impulse response

 	 */

-	return search_start_pos + strongest_window_nr - TRAIN_POS * d_OSR;

+	return search_start_pos + strongest_window_nr - search_center * d_OSR;

 }

 

+/*

 

+3 + 57 + 1 + 26 + 1 + 57 + 3 + 8.25

 

+search center = 3 + 57 + 1 + 5 (due to tsc 5+16+5 split)

+this is +-5 samples around (+5 beginning) of truncated t16 tsc

+

+*/

+int get_norm_chan_imp_resp(const gr_complex *input, gr_complex *chan_imp_resp, float *corr_max, int bcc)

+{

+	const int search_center = TRAIN_POS;

+	const int search_start_pos = (search_center - 5) * d_OSR + 1;

+	const int search_stop_pos = (search_center + 5 + d_chan_imp_length) * d_OSR;

+	const auto tseq = &d_norm_training_seq[bcc][TRAIN_BEGINNING];

+	const auto tseqlen = N_TRAIN_BITS - (2 * TRAIN_BEGINNING);

+	return get_chan_imp_resp(input, chan_imp_resp, search_center, search_start_pos, search_stop_pos, tseq, tseqlen,

+				 corr_max);

+}

+

+/*

+

+3 tail | 39 data | 64 tsc | 39 data | 3 tail | 8.25 guard

+start 3+39 - 10

+end 3+39 + SYNC_SEARCH_RANGE

+

+*/

+int get_sch_chan_imp_resp(const gr_complex *input, gr_complex *chan_imp_resp)

+{

+	const int search_center = SYNC_POS + TRAIN_BEGINNING;

+	const int search_start_pos = (search_center - 10) * d_OSR;

+	const int search_stop_pos = (search_center + SYNC_SEARCH_RANGE) * d_OSR;

+	const auto tseq = &d_sch_training_seq[TRAIN_BEGINNING];

+	const auto tseqlen = N_SYNC_BITS - (2 * TRAIN_BEGINNING);

+

+	// strongest_window_nr + chan_imp_resp_center + SYNC_POS *d_OSR - 48 * d_OSR - 2 * d_OSR + 2 ;

+	float corr_max;

+	return get_chan_imp_resp(input, chan_imp_resp, search_center, search_start_pos, search_stop_pos, tseq, tseqlen,

+				 &corr_max);

+}

+

+int get_sch_buffer_chan_imp_resp(const gr_complex *input, gr_complex *chan_imp_resp, unsigned int len, float *corr_max)

+{

+	const auto tseqlen = N_SYNC_BITS - (2 * TRAIN_BEGINNING);

+	const int search_center = SYNC_POS + TRAIN_BEGINNING;

+	const int search_start_pos = 0;

+	const int search_stop_pos = len - tseqlen;

+	auto tseq = &d_sch_training_seq[TRAIN_BEGINNING];

+

+	return get_chan_imp_resp(input, chan_imp_resp, search_center, search_start_pos, search_stop_pos, tseq, tseqlen,

+				 corr_max);

+}
\ No newline at end of file
diff --git a/Transceiver52M/grgsm_vitac/grgsm_vitac.h b/Transceiver52M/grgsm_vitac/grgsm_vitac.h
new file mode 100644
index 0000000..3c8e2b8
--- /dev/null
+++ b/Transceiver52M/grgsm_vitac/grgsm_vitac.h
@@ -0,0 +1,42 @@
+
+#pragma once
+
+#include <vector>
+#include "constants.h"
+
+#if defined(__has_attribute)
+#if __has_attribute(target_clones) && defined(__x86_64) && false
+#define MULTI_VER_TARGET_ATTR __attribute__((target_clones("avx", "sse4.2", "sse3", "sse2", "sse", "default")))
+#else
+#define MULTI_VER_TARGET_ATTR
+#endif
+#endif
+
+#define SYNC_SEARCH_RANGE 30
+const int d_OSR(4);
+
+void initvita();
+
+int process_vita_burst(gr_complex *input, int tsc, unsigned char *output_binary);
+int process_vita_sc_burst(gr_complex *input, int tsc, unsigned char *output_binary, int *offset);
+
+MULTI_VER_TARGET_ATTR
+void detect_burst(const gr_complex *input, gr_complex *chan_imp_resp, int burst_start, unsigned char *output_binary);
+void gmsk_mapper(const unsigned char *input, int nitems, gr_complex *gmsk_output, gr_complex start_point);
+gr_complex correlate_sequence(const gr_complex *sequence, int length, const gr_complex *input);
+inline void autocorrelation(const gr_complex *input, gr_complex *out, int nitems);
+inline void mafi(const gr_complex *input, int nitems, gr_complex *filter, int filter_length, gr_complex *output);
+int get_sch_chan_imp_resp(const gr_complex *input, gr_complex *chan_imp_resp);
+int get_norm_chan_imp_resp(const gr_complex *input, gr_complex *chan_imp_resp, float *corr_max, int bcc);
+int get_sch_buffer_chan_imp_resp(const gr_complex *input, gr_complex *chan_imp_resp, unsigned int len, float *corr_max);
+
+enum class btype { NB, SCH };
+struct fdata {
+	btype t;
+	unsigned int fn;
+	int tn;
+	int bcc;
+	std::string fpath;
+	std::vector<gr_complex> data;
+	unsigned int data_start_offset;
+};
\ No newline at end of file
diff --git a/Transceiver52M/itrq.h b/Transceiver52M/itrq.h
new file mode 100644
index 0000000..f086af3
--- /dev/null
+++ b/Transceiver52M/itrq.h
@@ -0,0 +1,204 @@
+#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 l;
+
+    public:
+	explicit spsc_cond_detail()
+	{
+	}
+
+	~spsc_cond_detail()
+	{
+	}
+
+	ssize_t spsc_check_r()
+	{
+		std::unique_lock<std::mutex> lk(l);
+		cond_r.wait(lk);
+		return 1;
+	}
+	ssize_t spsc_check_w()
+	{
+		std::unique_lock<std::mutex> lk(l);
+		cond_w.wait(lk);
+		return 1;
+	}
+	void spsc_notify_r()
+	{
+		cond_r.notify_one();
+	}
+	void spsc_notify_w()
+	{
+		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(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/l1if.cpp b/Transceiver52M/l1if.cpp
index 440f6e7..385395a 100644
--- a/Transceiver52M/l1if.cpp
+++ b/Transceiver52M/l1if.cpp
@@ -84,7 +84,7 @@
 	uint64_t one = 1;
 	int rc;
 	trxif.c_from_trx.push(i);
-	std::clog << trxif.c_from_trx.sz() << std::endl;
+	// std::clog << trxif.c_from_trx.sz() << std::endl;
 	rc = ::write(trxif.g_event_ofd_C.fd, &one, sizeof(one));
 	return;
 };
diff --git a/Transceiver52M/l1if.h b/Transceiver52M/l1if.h
index d477e47..ab6ccce 100644
--- a/Transceiver52M/l1if.h
+++ b/Transceiver52M/l1if.h
@@ -29,20 +29,20 @@
 /* 148 bytes output symbol values, 0 & 1                                    */
 /* ------------------------------------------------------------------------ */
 
-struct trxd_to_trx {
-   uint8_t ts;
-   uint32_t fn;
-   uint8_t txlev;
-   uint8_t symbols[148];
+struct __attribute__((packed)) trxd_to_trx {
+	uint8_t ts;
+	uint32_t fn;
+	uint8_t txlev;
+	uint8_t symbols[148];
 };
 
-struct trxd_from_trx {
-   uint8_t ts;
-   uint32_t fn;
-   uint8_t rssi;
-   uint16_t toa;
-   uint8_t symbols[148];
-   uint8_t pad[2];
+struct __attribute__((packed)) trxd_from_trx {
+	uint8_t ts;
+	uint32_t fn;
+	uint8_t rssi;
+	uint16_t toa;
+	uint8_t symbols[148];
+	uint8_t pad[2];
 };
 
 #define TRXC_BUF_SIZE	1024
diff --git a/Transceiver52M/ms/bladerf_specific.h b/Transceiver52M/ms/bladerf_specific.h
new file mode 100644
index 0000000..fcfcc9c
--- /dev/null
+++ b/Transceiver52M/ms/bladerf_specific.h
@@ -0,0 +1,482 @@
+#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 doPrint(std::ostream &out, Arg &&arg, Args &&...args)
+{
+	out << '(' << std::forward<Arg>(arg);
+	using expander = int[];
+	(void)expander{ 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;
+		doPrint(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>;
+	unsigned int rxFullScale, txFullScale;
+	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;
+			return -1;
+		}
+		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;
+		}
+
+		// bladerf_sample_rate r = (1625e3 * 4)/6, act;
+		// blade_check(bladerf_set_sample_rate,dev, BLADERF_CHANNEL_RX(0), r, &act);
+		// blade_check(bladerf_set_sample_rate,dev, BLADERF_CHANNEL_TX(0), r, &act);
+
+		// auto ratrate = (1625e3 * 4) / 6;
+		// rate.integer = (uint32_t)ratrate;
+		// rate.den = 10000;
+		// rate.num = (ratrate - rate.integer) * rate.den;
+
+		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)50);
+		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;
+			set_name_aff_sched("rxrun", 2, SCHED_FIFO, sched_get_priority_max(SCHED_FIFO) - 2);
+			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;
+			set_name_aff_sched("txrun", 2, SCHED_FIFO, sched_get_priority_max(SCHED_FIFO) - 1);
+			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, 100U);
+		blade_check(bladerf_submit_stream_buffer_nb, tx_stream, (void *)rcd);
+	}
+
+	void set_name_aff_sched(const char *name, int cpunum, int schedtype, int prio)
+	{
+		pthread_setname_np(pthread_self(), name);
+
+		cpu_set_t cpuset;
+
+		CPU_ZERO(&cpuset);
+		CPU_SET(cpunum, &cpuset);
+
+		auto rv = pthread_setaffinity_np(pthread_self(), 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(pthread_self(), schedtype, &sch_params);
+		if (rv < 0) {
+			std::cerr << name << " sched: errreur! " << std::strerror(errno);
+			return exit(0);
+		}
+	}
+};
diff --git a/Transceiver52M/ms/ms_commandhandler.cpp b/Transceiver52M/ms/ms_commandhandler.cpp
new file mode 100644
index 0000000..13b95f9
--- /dev/null
+++ b/Transceiver52M/ms/ms_commandhandler.cpp
@@ -0,0 +1,229 @@
+/*
+ * (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 <radioInterface.h>
+#include "l1if.h"
+#include "ms_rx_upper.h"
+#include "syncthing.h"
+#include "ms_state.h"
+
+void upper_trx::driveControl()
+{
+#ifdef IPCIF
+	auto m = pop_c();
+	if (!m)
+		return;
+#else
+	TRX_C cmd;
+
+	socklen_t addr_len = sizeof(ctrlsrc);
+	int rdln = recvfrom(mCtrlSockets, (void *)cmd.cmd, sizeof(cmd) - 1, 0, &ctrlsrc, &addr_len);
+	if (rdln < 0 && errno == EAGAIN) {
+		std::cerr << "fuck, send ctrl?" << std::endl;
+		exit(0);
+	}
+
+	TRX_C *m = &cmd;
+#endif
+
+	auto response = (TRX_C *)malloc(sizeof(TRX_C));
+	response->cmd[0] = '\0';
+	commandhandler(m->cmd, response->cmd);
+#ifdef IPCIF
+	free(m);
+#endif
+	std::clog << "response is " << response->cmd << std::endl;
+#ifdef IPCIF
+	push_c(response);
+#else
+
+	int rv = sendto(mCtrlSockets, response, strlen(response->cmd) + 1, 0, &ctrlsrc, sizeof(struct sockaddr_in));
+	if (rv < 0) {
+		std::cerr << "fuck, rcv ctrl?" << std::endl;
+		exit(0);
+	}
+	free(response);
+
+#endif
+}
+
+void upper_trx::commandhandler(char *buffer, char *response)
+{
+	int MAX_PACKET_LENGTH = TRXC_BUF_SIZE;
+
+	char cmdcheck[4];
+	char command[MAX_PACKET_LENGTH];
+
+	sscanf(buffer, "%3s %s", cmdcheck, command);
+
+	if (strcmp(cmdcheck, "CMD") != 0) {
+		LOG(WARNING) << "bogus message on control interface";
+		return;
+	}
+	std::clog << "command is " << buffer << std::endl << std::flush;
+
+	if (strcmp(command, "MEASURE") == 0) {
+		msleep(100);
+		int freq;
+		sscanf(buffer, "%3s %s %d", cmdcheck, command, &freq);
+		sprintf(response, "RSP MEASURE 0 %d -80", freq);
+	} else if (strcmp(command, "ECHO") == 0) {
+		msleep(100);
+		sprintf(response, "RSP ECHO 0");
+	} else if (strcmp(command, "POWEROFF") == 0) {
+		set_ta(0);
+		// turn off transmitter/demod
+		sprintf(response, "RSP POWEROFF 0");
+	} else if (strcmp(command, "POWERON") == 0) {
+		// turn on transmitter/demod
+		if (!mTxFreq || !mRxFreq)
+			sprintf(response, "RSP POWERON 1");
+		else {
+			sprintf(response, "RSP POWERON 0");
+			if (!mOn) {
+				// Prepare for thread start
+				mPower = -20;
+				start_ms();
+
+				writeClockInterface();
+				mOn = true;
+			}
+		}
+	} else if (strcmp(command, "SETMAXDLY") == 0) {
+		//set expected maximum time-of-arrival
+		int maxDelay;
+		sscanf(buffer, "%3s %s %d", cmdcheck, command, &maxDelay);
+		mMaxExpectedDelay = maxDelay; // 1 GSM symbol is approx. 1 km
+		sprintf(response, "RSP SETMAXDLY 0 %d", maxDelay);
+	} else if (strcmp(command, "SETRXGAIN") == 0) {
+		//set expected maximum time-of-arrival
+		int newGain;
+		sscanf(buffer, "%3s %s %d", cmdcheck, command, &newGain);
+		newGain = setRxGain(newGain);
+		sprintf(response, "RSP SETRXGAIN 0 %d", newGain);
+	} else if (strcmp(command, "NOISELEV") == 0) {
+		if (mOn) {
+			float lev = 0; //mStates[chan].mNoiseLev;
+			sprintf(response, "RSP NOISELEV 0 %d", (int)round(20.0 * log10(rxFullScale / lev)));
+		} else {
+			sprintf(response, "RSP NOISELEV 1  0");
+		}
+	} else if (!strcmp(command, "SETPOWER")) {
+		// set output power in dB
+		int dbPwr;
+		sscanf(buffer, "%3s %s %d", cmdcheck, command, &dbPwr);
+		if (!mOn)
+			sprintf(response, "RSP SETPOWER 1 %d", dbPwr);
+		else {
+			mPower = dbPwr;
+			setPowerAttenuation(mPower);
+			sprintf(response, "RSP SETPOWER 0 %d", dbPwr);
+		}
+	} else if (!strcmp(command, "ADJPOWER")) {
+		// adjust power in dB steps
+		int dbStep;
+		sscanf(buffer, "%3s %s %d", cmdcheck, command, &dbStep);
+		if (!mOn)
+			sprintf(response, "RSP ADJPOWER 1 %d", mPower);
+		else {
+			mPower += dbStep;
+			setPowerAttenuation(mPower);
+			sprintf(response, "RSP ADJPOWER 0 %d", mPower);
+		}
+	} else if (strcmp(command, "RXTUNE") == 0) {
+		// tune receiver
+		int freqKhz;
+		sscanf(buffer, "%3s %s %d", cmdcheck, command, &freqKhz);
+		mRxFreq = freqKhz * 1e3;
+		if (!tuneRx(mRxFreq)) {
+			LOG(ALERT) << "RX failed to tune";
+			sprintf(response, "RSP RXTUNE 1 %d", freqKhz);
+		} else
+			sprintf(response, "RSP RXTUNE 0 %d", freqKhz);
+	} else if (strcmp(command, "TXTUNE") == 0) {
+		// tune txmtr
+		int freqKhz;
+		sscanf(buffer, "%3s %s %d", cmdcheck, command, &freqKhz);
+		mTxFreq = freqKhz * 1e3;
+		if (!tuneTx(mTxFreq)) {
+			LOG(ALERT) << "TX failed to tune";
+			sprintf(response, "RSP TXTUNE 1 %d", freqKhz);
+		} else
+			sprintf(response, "RSP TXTUNE 0 %d", freqKhz);
+	} else if (!strcmp(command, "SETTSC")) {
+		// set TSC
+		unsigned TSC;
+		sscanf(buffer, "%3s %s %d", cmdcheck, command, &TSC);
+		if (mOn)
+			sprintf(response, "RSP SETTSC 1 %d", TSC);
+		// else if (chan && (TSC != mTSC))
+		// 	sprintf(response, "RSP SETTSC 1 %d", TSC);
+		else {
+			mTSC = TSC;
+			//generateMidamble(rx_sps, TSC);
+			sprintf(response, "RSP SETTSC 0 %d", TSC);
+		}
+	} else if (!strcmp(command, "GETBSIC")) {
+		if (mBSIC < 0)
+			sprintf(response, "RSP GETBSIC 1");
+		else
+			sprintf(response, "RSP GETBSIC 0 %d", mBSIC);
+	} else if (strcmp(command, "SETSLOT") == 0) {
+		// set TSC
+		int corrCode;
+		int timeslot;
+		sscanf(buffer, "%3s %s %d %d", cmdcheck, command, &timeslot, &corrCode);
+		if ((timeslot < 0) || (timeslot > 7)) {
+			LOG(WARNING) << "bogus message on control interface";
+			sprintf(response, "RSP SETSLOT 1 %d %d", timeslot, corrCode);
+			return;
+		}
+		mStates.chanType[timeslot] = (ChannelCombination)corrCode;
+		mStates.setModulus(timeslot);
+		sprintf(response, "RSP SETSLOT 0 %d %d", timeslot, corrCode);
+	} else if (!strcmp(command, "SETRXMASK")) {
+		int slot;
+		unsigned long long mask;
+		sscanf(buffer, "%3s %s %d 0x%llx", cmdcheck, command, &slot, &mask);
+		if ((slot < 0) || (slot > 7)) {
+			sprintf(response, "RSP SETRXMASK 1");
+		} else {
+			mRxSlotMask[slot] = mask;
+			sprintf(response, "RSP SETRXMASK 0 %d 0x%llx", slot, mask);
+		}
+	} else if (!strcmp(command, "SYNC")) {
+		// msleep(10);
+		mStates.mode = trx_mode::TRX_MODE_MS_TRACK;
+		sprintf(response, "RSP SYNC 0");
+		mMaxExpectedDelay = 48;
+		// setRxGain(30);
+		// msleep(10);
+	} else if (!strcmp(command, "SETTA")) {
+		int ta;
+		sscanf(buffer, "%3s %s %d", cmdcheck, command, &ta);
+		set_ta(ta);
+		sprintf(response, "RSP SETTA 0 %d", ta);
+	} else {
+		LOG(WARNING) << "bogus command " << command << " on control interface.";
+	}
+
+	//mCtrlSockets[chan]->write(response, strlen(response) + 1);
+}
diff --git a/Transceiver52M/ms/ms_rx_burst.cpp b/Transceiver52M/ms/ms_rx_burst.cpp
new file mode 100644
index 0000000..999584d
--- /dev/null
+++ b/Transceiver52M/ms/ms_rx_burst.cpp
@@ -0,0 +1,211 @@
+/*
+ * (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 "syncthing.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
+
+__attribute__((xray_always_instrument)) __attribute__((noinline)) 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();
+}
+
+__attribute__((xray_always_instrument)) __attribute__((noinline)) static void
+handle_it(one_burst &e, signalVector &burst, unsigned int tsc)
+{
+	memset(burst.begin(), 0, burst.size() * sizeof(std::complex<float>));
+	auto is_sch = gsm_sch_check_fn(e.gsmts.FN()) && e.gsmts.TN() == 0;
+	auto is_fcch = gsm_fcch_check_fn(e.gsmts.FN()) && e.gsmts.TN() == 0;
+
+	// if (is_sch)
+	// 	return;
+
+	if (is_fcch)
+		return;
+
+	if (is_sch) {
+		unsigned 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 / 32767.f);
+
+			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 / 2047.f);
+	// std::cerr << "@" << tsc << " " << e.gsmts.FN() << ":" << e.gsmts.TN() << " " << ebp.toa << " "
+	// 	  << std::endl;
+
+	unsigned 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);
+	;
+
+	auto bits = SoftVector(148);
+	for (int i = 0; i < 148; i++)
+		(bits)[i] = outbin[i] < 1 ? -1 : 1;
+
+#endif
+}
+
+__attribute__((xray_always_instrument)) __attribute__((noinline)) void rcv_bursts_test(rx_queue_t *q, unsigned int *tsc)
+{
+	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);
+
+		// 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();
+	}
+}
\ No newline at end of file
diff --git a/Transceiver52M/ms/ms_rx_burst.h b/Transceiver52M/ms/ms_rx_burst.h
new file mode 100644
index 0000000..301ebcf
--- /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 "syncthing.h"
+
+void rcv_bursts_test(rx_queue_t *q, unsigned int *tsc);
\ 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..15e562c
--- /dev/null
+++ b/Transceiver52M/ms/ms_rx_lower.cpp
@@ -0,0 +1,330 @@
+/*
+ * (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 "syncthing.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
+__attribute__((xray_always_instrument)) __attribute__((noinline)) 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], 62, 39);
+	float_to_sbit(&bits[106], &data[39], 62, 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
+		// auto sch11 = gsm_sch_check_fn(fn + 11);
+		// DBGLG() << "next sch: "<< (sch11 ? "11":"10")<<" first ts " << first_sch_buf_rcv_ts << std::endl;
+		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;
+}
+
+bool ms_trx::handle_sch_or_nb(bool get_first_sch)
+{
+	one_burst brst;
+	auto current_gsm_time = timekeeper.gsmtime();
+	brst.gsmts = current_gsm_time;
+	memcpy(brst.burst, burst_copy_buffer, sizeof(blade_sample_type) * ONE_TS_BURST_LEN);
+	auto pushok = rxqueue.spsc_push(&brst);
+#ifdef PRINT_Q_OVERFLOW
+	if (!pushok)
+		std::cout << "F" << std::endl;
+#endif
+	if (do_auto_gain)
+		maybe_update_gain(brst);
+
+	// only continue for SCH, don't touch FCCH
+	auto is_sch = gsm_sch_check_fn(current_gsm_time.FN()) && current_gsm_time.TN() == 0;
+	auto is_fcch = gsm_fcch_check_fn(current_gsm_time.FN()) && current_gsm_time.TN() == 0;
+#pragma unused(is_fcch)
+
+	if (!is_sch) {
+		// sched_yield();
+		return false;
+	}
+	auto rv = handle_sch(false);
+	// sched_yield();
+	return rv;
+}
+
+float bernd[SCH_LEN_SPS * 2];
+
+bool ms_trx::handle_sch(bool is_first_sch_acq)
+{
+	struct estim_burst_params ebp;
+	auto current_gsm_time = timekeeper.gsmtime();
+	const auto buf_len = is_first_sch_acq ? SCH_LEN_SPS : ONE_TS_BURST_LEN;
+	const auto which_buffer = is_first_sch_acq ? first_sch_buf : burst_copy_buffer;
+
+	std::complex<float> channel_imp_resp[CHAN_IMP_RESP_LENGTH * d_OSR];
+	unsigned char outbin[148];
+	float max_corr = 0;
+	const auto ss = reinterpret_cast<std::complex<float> *>(&bernd[0]);
+	convert_and_scale<float, int16_t>(bernd, which_buffer, buf_len * 2, 1.f / 2047.f);
+
+	auto start = is_first_sch_acq ? get_sch_buffer_chan_imp_resp(ss, &channel_imp_resp[0], buf_len, &max_corr) :
+					get_sch_chan_imp_resp(ss, channel_imp_resp);
+	detect_burst(&ss[start], &channel_imp_resp[0], 0, outbin);
+
+	SoftVector bitss(148);
+	for (int i = 0; i < 148; i++) {
+		bitss[i] = (!outbin[i]) < 1 ? -1 : 1;
+	}
+
+	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);
+		} 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;
+}
+__attribute__((optnone)) 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;
+		// memcpy(burst_copy_buffer, partial_buf, partial_rdofs * sizeof(blade_sample_type));
+		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_rx_upper.cpp b/Transceiver52M/ms/ms_rx_upper.cpp
new file mode 100644
index 0000000..b87986e
--- /dev/null
+++ b/Transceiver52M/ms/ms_rx_upper.cpp
@@ -0,0 +1,364 @@
+/*
+ * (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 "syncthing.h"
+#include "l1if.h"
+#include <signalVector.h>
+#include <radioVector.h>
+#include <radioInterface.h>
+#include "grgsm_vitac/grgsm_vitac.h"
+#include "ms_state.h"
+#include "ms_rx_upper.h"
+
+extern "C" {
+#include "sch.h"
+#include "convolve.h"
+#include "convert.h"
+#include "proto_trxd.h"
+}
+
+#ifdef LOG
+#undef LOG
+#define LOG(...) upper_trx::dummy_log()
+#endif
+
+void upper_trx::start_threads()
+{
+	thr_rx = std::thread([this] {
+		set_name_aff_sched("upper_rx", 1, SCHED_FIFO, sched_get_priority_max(SCHED_FIFO) - 5);
+		while (1) {
+			driveReceiveFIFO();
+			pthread_testcancel();
+		}
+	});
+	msleep(1);
+
+	thr_control = std::thread([this] {
+		set_name_aff_sched("upper_ctrl", 1, SCHED_RR, sched_get_priority_max(SCHED_RR));
+		while (1) {
+			driveControl();
+			pthread_testcancel();
+		}
+	});
+	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();
+			pthread_testcancel();
+		}
+	});
+}
+
+void upper_trx::start_ms()
+{
+	ms_trx::start();
+}
+
+/* Detect SCH synchronization sequence within a burst */
+bool upper_trx::detectSCH(ms_TransceiverState *state, signalVector &burst, struct estim_burst_params *ebp)
+{
+	int shift;
+	sch_detect_type full;
+	float mag, threshold = 4.0;
+
+	full = (state->mode == trx_mode::TRX_MODE_MS_TRACK) ? sch_detect_type::SCH_DETECT_NARROW :
+							      sch_detect_type::SCH_DETECT_FULL;
+
+	if (!detectSCHBurst(burst, threshold, rx_sps, full, ebp))
+		return false;
+
+	std::clog << "SCH : Timing offset     " << ebp->toa << " symbols" << std::endl;
+
+	mag = fabsf(ebp->toa);
+	if (mag < 1.0f)
+		return true;
+
+	shift = (int)(mag / 2.0f);
+	if (!shift)
+		shift++;
+
+	shift = ebp->toa > 0 ? shift : -shift;
+	std::clog << "SCH : shift ->     " << shift << " symbols" << std::endl;
+	// mRadioInterface->applyOffset(shift);
+	return false;
+}
+
+SoftVector *upper_trx::pullRadioVector(GSM::Time &wTime, int &RSSI, int &timingOffset) __attribute__((optnone))
+{
+	float pow, avg = 1.0;
+	signalVector *burst;
+	SoftVector *bits = new SoftVector(148);
+	GSM::Time burst_time;
+
+	one_burst e;
+	unsigned char outbin[148];
+	std::complex<float> chan_imp_resp[CHAN_IMP_RESP_LENGTH * d_OSR];
+	std::stringstream dbgout;
+
+	while (!rxqueue.spsc_pop(&e)) {
+		rxqueue.spsc_prep_pop();
+	}
+
+	auto sv = signalVector(625, 40);
+	burst = &sv;
+	auto ss = reinterpret_cast<std::complex<float> *>(burst->begin());
+
+	convert_and_scale<float, int16_t>(burst->begin(), e.burst, ONE_TS_BURST_LEN * 2, 1.f / 2047.f);
+
+	/* Set time and determine correlation type */
+	burst_time = e.gsmts;
+
+	wTime = burst_time;
+
+	CorrType type = mStates.expectedCorrType(burst_time, mRxSlotMask);
+
+	switch (mStates.mode) {
+	case trx_mode::TRX_MODE_MS_TRACK:
+		if (gsm_sch_check_fn(burst_time.FN()) && burst_time.TN() == 0)
+			type = SCH;
+		else if (burst_time.TN() == 0 && !gsm_fcch_check_fn(burst_time.FN())) // all ts0, but not fcch or sch..
+			type = TSC;
+		else if (type == OFF)
+			goto release;
+		break;
+
+	case trx_mode::TRX_MODE_OFF:
+	default:
+		goto release;
+	}
+
+	pow = energyDetect(*burst, 20 * rx_sps);
+	if (pow < -1) {
+		LOG(ALERT) << "Received empty burst";
+		goto release;
+	}
+
+	avg = sqrt(pow);
+
+	if (type == SCH) {
+		int d_c0_burst_start = get_sch_chan_imp_resp(ss, &chan_imp_resp[0]);
+		detect_burst(ss, &chan_imp_resp[0], d_c0_burst_start, outbin);
+
+		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;
+
+		// std::cerr << dbgout.str();
+	} else {
+		float ncmax, dcmax;
+		std::complex<float> chan_imp_resp2[CHAN_IMP_RESP_LENGTH * d_OSR];
+		auto normal_burst_start = get_norm_chan_imp_resp(ss, &chan_imp_resp[0], &ncmax, mTSC);
+		auto dummy_burst_start = get_norm_chan_imp_resp(ss, &chan_imp_resp2[0], &dcmax, TS_DUMMY);
+		auto is_nb = ncmax > dcmax;
+
+		// std::cerr << " 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);
+
+		for (int i = 0; i < 148; i++)
+			(*bits)[i] = (outbin[i]) < 1 ? -1 : 1;
+	}
+
+	RSSI = (int)floor(20.0 * log10(rxFullScale / avg));
+	timingOffset = (int)round(0);
+
+	return bits;
+
+release:
+
+	delete bits;
+	return NULL;
+}
+
+void upper_trx::driveReceiveFIFO()
+{
+	SoftVector *rxBurst = NULL;
+	int RSSI;
+	int TOA; // in 1/256 of a symbol
+	GSM::Time burstTime;
+
+	rxBurst = pullRadioVector(burstTime, RSSI, TOA);
+	if (!mOn)
+		return;
+
+	// _only_ return useless fcch trash to tickle trxcons tx path
+	// auto is_fcch = [&burstTime]{ return burstTime.TN() == 0 && gsm_fcch_check_fn(burstTime.FN());};
+	// if(!rxBurst && !is_fcch())
+	// 	return;
+
+	auto response = (trxd_from_trx *)calloc(1, sizeof(trxd_from_trx));
+
+	response->ts = burstTime.TN();
+	response->fn = htonl(burstTime.FN());
+	response->rssi = RSSI;
+	response->toa = htons(TOA);
+	if (rxBurst) {
+		SoftVector::const_iterator burstItr = rxBurst->begin();
+		if (gsm_sch_check_fn(burstTime.FN())) {
+			clamp_array(rxBurst->begin(), 148, 1.5f);
+			for (unsigned int i = 0; i < gSlotLen; i++) {
+				auto val = *burstItr++;
+				auto vval = isnan(val) ? 0 : val;
+				((int8_t *)response->symbols)[i] = round((vval - 0.5) * 64.0);
+			}
+		} else {
+			// invert and fix to +-127 sbits
+			for (int i = 0; i < 148; i++)
+				((int8_t *)response->symbols)[i] = *burstItr++ > 0.0f ? -127 : 127;
+		}
+
+		delete rxBurst;
+	}
+
+#ifdef IPCIF
+	push_d(response);
+#else
+	int rv = sendto(mDataSockets, response, sizeof(trxd_from_trx), 0, (struct sockaddr *)&datadest,
+			sizeof(struct sockaddr_in));
+	if (rv < 0) {
+		std::cerr << "fuck, send?" << std::endl;
+		exit(0);
+	}
+	free(response);
+
+#endif
+}
+
+void upper_trx::driveTx()
+{
+#ifdef IPCIF
+	auto burst = pop_d();
+	if (!burst) {
+		// std::cerr << "wtf no tx burst?" << std::endl;
+		// exit(0);
+		continue;
+	}
+#else
+	trxd_to_trx buffer;
+
+	socklen_t addr_len = sizeof(datasrc);
+	int rdln = recvfrom(mDataSockets, (void *)&buffer, sizeof(trxd_to_trx), 0, &datasrc, &addr_len);
+	if (rdln < 0 && errno == EAGAIN) {
+		std::cerr << "fuck, rcv?" << std::endl;
+		exit(0);
+	}
+
+	trxd_to_trx *burst = &buffer;
+#endif
+	auto proper_fn = ntohl(burst->fn);
+	// std::cerr << "got burst!" << proper_fn << ":" << burst->ts
+	// 	  << " current: " << timekeeper.gsmtime().FN()
+	// 	  << " dff: " << (int64_t)((int64_t)timekeeper.gsmtime().FN() - (int64_t)proper_fn)
+	// 	  << std::endl;
+
+	auto currTime = GSM::Time(proper_fn, burst->ts);
+	int RSSI = (int)burst->txlev;
+
+	static BitVector newBurst(gSlotLen);
+	BitVector::iterator itr = newBurst.begin();
+	auto *bufferItr = burst->symbols;
+	while (itr < newBurst.end())
+		*itr++ = *bufferItr++;
+
+	auto txburst = modulateBurst(newBurst, 8 + (currTime.TN() % 4 == 0), 4);
+	scaleVector(*txburst, txFullScale * 0.7 /* * 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);
+
+	// 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)
+	// 	std::cerr << "RACH D! " << ebp.toa << std::endl;
+	// else
+	// 	std::cerr << "RACH NOOOOOOOOOO D! " << ebp.toa << std::endl;
+
+	// memory read --binary --outfile /tmp/mem.bin &burst_buf[0] --count 2500 --force
+
+	submit_burst(burst_buf, txburst->size(), currTime);
+
+#ifdef IPCIF
+	free(burst);
+#endif
+}
+
+// __attribute__((xray_always_instrument)) static void *rx_stream_callback(struct bladerf *dev,
+// 									struct bladerf_stream *stream,
+// 									struct bladerf_metadata *meta, void *samples,
+// 									size_t num_samples, void *user_data)
+// {
+// 	struct ms_trx *trx = (struct ms_trx *)user_data;
+// 	return trx->rx_cb(dev, stream, meta, samples, num_samples, user_data);
+// }
+
+// __attribute__((xray_always_instrument)) static void *tx_stream_callback(struct bladerf *dev,
+// 									struct bladerf_stream *stream,
+// 									struct bladerf_metadata *meta, void *samples,
+// 									size_t num_samples, void *user_data)
+// {
+// 	struct ms_trx *trx = (struct ms_trx *)user_data;
+// 	return BLADERF_STREAM_NO_DATA;
+// }
+
+int trxc_main(int argc, char *argv[])
+{
+	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(0, 0);
+	trx->start_threads();
+
+	return status;
+}
+
+extern "C" volatile bool gshutdown = false;
+extern "C" void init_external_transceiver(int argc, char **argv)
+{
+	std::cout << "init?" << std::endl;
+	trxc_main(argc, argv);
+}
+
+extern "C" void stop_trx()
+{
+	std::cout << "Shutting down transceiver..." << std::endl;
+}
diff --git a/Transceiver52M/ms/ms_rx_upper.h b/Transceiver52M/ms/ms_rx_upper.h
new file mode 100644
index 0000000..09154bf
--- /dev/null
+++ b/Transceiver52M/ms/ms_rx_upper.h
@@ -0,0 +1,121 @@
+
+#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 "syncthing.h"
+#include "ms_state.h"
+
+class upper_trx : public ms_trx {
+	int rx_sps, tx_sps;
+
+	ms_TransceiverState mStates;
+
+	bool mOn; ///< flag to indicate that transceiver is powered on
+	double mTxFreq; ///< the transmit frequency
+	double mRxFreq; ///< the receive frequency
+	int mPower; ///< the transmit power in dB
+	unsigned mMaxExpectedDelay; ///< maximum TOA offset in GSM symbols
+	unsigned long long mRxSlotMask[8]; ///< MS - enabled multiframe slot mask
+
+	int mDataSockets;
+	sockaddr_in datadest;
+	sockaddr datasrc;
+	int mCtrlSockets;
+	sockaddr_in ctrldest;
+	sockaddr ctrlsrc;
+
+	void openudp(int *mSocketFD, unsigned short localPort, const char *wlocalIP)
+	{
+		*mSocketFD = socket(AF_INET, SOCK_DGRAM, 0);
+		int on = 1;
+		setsockopt(*mSocketFD, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
+
+		struct sockaddr_in address;
+		size_t length = sizeof(address);
+		bzero(&address, length);
+		address.sin_family = AF_INET;
+		address.sin_addr.s_addr = inet_addr(wlocalIP);
+		address.sin_port = htons(localPort);
+		if (bind(*mSocketFD, (struct sockaddr *)&address, length) < 0) {
+			std::cerr << "bind fail!" << std::endl;
+			exit(0);
+		}
+	}
+
+	bool resolveAddress(struct sockaddr_in *address, const char *host, unsigned short port)
+	{
+		struct hostent *hp;
+		int h_errno_local;
+
+		struct hostent hostData;
+		char tmpBuffer[2048];
+
+		auto rc = gethostbyname2_r(host, AF_INET, &hostData, tmpBuffer, sizeof(tmpBuffer), &hp, &h_errno_local);
+		if (hp == NULL || hp->h_addrtype != AF_INET || rc != 0) {
+			std::cerr << "WARNING -- gethostbyname() failed for " << host << ", "
+				  << hstrerror(h_errno_local);
+			exit(0);
+			return false;
+		}
+
+		address->sin_family = hp->h_addrtype;
+		assert(sizeof(address->sin_addr) == hp->h_length);
+		memcpy(&(address->sin_addr), hp->h_addr_list[0], hp->h_length);
+		address->sin_port = htons(port);
+		return true;
+	}
+
+	void driveControl();
+	void driveReceiveFIFO();
+	void driveTx();
+	void commandhandler(char *buffer, char *response);
+	void writeClockInterface(){};
+
+	SoftVector *pullRadioVector(GSM::Time &wTime, int &RSSI, int &timingOffset);
+
+	bool detectSCH(ms_TransceiverState *state, signalVector &burst, struct estim_burst_params *ebp);
+
+	std::thread thr_control, thr_rx, thr_tx;
+
+    public:
+	void start_threads();
+	void start_ms();
+
+	upper_trx() : rx_sps(4), tx_sps(4)
+	{
+		auto c_srcport = 6700 + 2 * 0 + 1;
+		auto c_dstport = 6700 + 2 * 0 + 101;
+		auto d_srcport = 6700 + 2 * 0 + 2;
+		auto d_dstport = 6700 + 2 * 0 + 102;
+
+		openudp(&mCtrlSockets, c_srcport, "127.0.0.1");
+		openudp(&mDataSockets, d_srcport, "127.0.0.1");
+		resolveAddress(&ctrldest, "127.0.0.1", c_dstport);
+		resolveAddress(&datadest, "127.0.0.1", d_dstport);
+	};
+};
diff --git a/Transceiver52M/ms/ms_state.h b/Transceiver52M/ms/ms_state.h
new file mode 100644
index 0000000..c00d072
--- /dev/null
+++ b/Transceiver52M/ms/ms_state.h
@@ -0,0 +1,175 @@
+#pragma once
+
+#include <radioVector.h>
+#include <signalVector.h>
+
+enum class trx_mode {
+	TRX_MODE_OFF,
+	TRX_MODE_BTS,
+	TRX_MODE_MS_ACQUIRE,
+	TRX_MODE_MS_TRACK,
+};
+
+enum class ChannelCombination {
+	FILL, ///< Channel is transmitted, but unused
+	I, ///< TCH/FS
+	II, ///< TCH/HS, idle every other slot
+	III, ///< TCH/HS
+	IV, ///< FCCH+SCH+CCCH+BCCH, uplink RACH
+	V, ///< FCCH+SCH+CCCH+BCCH+SDCCH/4+SACCH/4, uplink RACH+SDCCH/4
+	VI, ///< CCCH+BCCH, uplink RACH
+	VII, ///< SDCCH/8 + SACCH/8
+	VIII, ///< TCH/F + FACCH/F + SACCH/M
+	IX, ///< TCH/F + SACCH/M
+	X, ///< TCH/FD + SACCH/MD
+	XI, ///< PBCCH+PCCCH+PDTCH+PACCH+PTCCH
+	XII, ///< PCCCH+PDTCH+PACCH+PTCCH
+	XIII, ///< PDTCH+PACCH+PTCCH
+	NONE_INACTIVE, ///< Channel is inactive, default
+	LOOPBACK ///< similar go VII, used in loopback testing
+};
+
+struct ms_TransceiverState {
+	ms_TransceiverState() : mFreqOffsets(10), mode(trx_mode::TRX_MODE_OFF)
+	{
+		for (int i = 0; i < 8; i++) {
+			chanType[i] = ChannelCombination::NONE_INACTIVE;
+			fillerModulus[i] = 26;
+
+			for (int n = 0; n < 102; n++)
+				fillerTable[n][i] = nullptr;
+		}
+	}
+
+	~ms_TransceiverState()
+	{
+		for (int i = 0; i < 8; i++) {
+			for (int n = 0; n < 102; n++)
+				delete fillerTable[n][i];
+		}
+	}
+
+	void setModulus(size_t timeslot)
+	{
+		switch (chanType[timeslot]) {
+		case ChannelCombination::NONE_INACTIVE:
+		case ChannelCombination::I:
+		case ChannelCombination::II:
+		case ChannelCombination::III:
+		case ChannelCombination::FILL:
+			fillerModulus[timeslot] = 26;
+			break;
+		case ChannelCombination::IV:
+		case ChannelCombination::VI:
+		case ChannelCombination::V:
+			fillerModulus[timeslot] = 51;
+			break;
+			//case V:
+		case ChannelCombination::VII:
+			fillerModulus[timeslot] = 102;
+			break;
+		case ChannelCombination::XIII:
+			fillerModulus[timeslot] = 52;
+			break;
+		default:
+			break;
+		}
+	}
+
+	CorrType expectedCorrType(GSM::Time currTime, unsigned long long *mRxSlotMask)
+	{
+		unsigned burstTN = currTime.TN();
+		unsigned burstFN = currTime.FN();
+
+		if (mode == trx_mode::TRX_MODE_MS_TRACK) {
+			/* 102 modulus case currently unhandled */
+			if (fillerModulus[burstTN] > 52)
+				return OFF;
+
+			int modFN = burstFN % fillerModulus[burstTN];
+			unsigned long long reg = (unsigned long long)1 << modFN;
+			if (reg & mRxSlotMask[burstTN])
+				return TSC;
+			else
+				return OFF;
+		}
+
+		switch (chanType[burstTN]) {
+		case ChannelCombination::NONE_INACTIVE:
+			return OFF;
+			break;
+		case ChannelCombination::FILL:
+			return IDLE;
+			break;
+		case ChannelCombination::I:
+			return TSC;
+			/*if (burstFN % 26 == 25) 
+      return IDLE;
+    else
+      return TSC;*/
+			break;
+		case ChannelCombination::II:
+			return TSC;
+			break;
+		case ChannelCombination::III:
+			return TSC;
+			break;
+		case ChannelCombination::IV:
+		case ChannelCombination::VI:
+			return RACH;
+			break;
+		case ChannelCombination::V: {
+			int mod51 = burstFN % 51;
+			if ((mod51 <= 36) && (mod51 >= 14))
+				return RACH;
+			else if ((mod51 == 4) || (mod51 == 5))
+				return RACH;
+			else if ((mod51 == 45) || (mod51 == 46))
+				return RACH;
+			else
+				return TSC;
+			break;
+		}
+		case ChannelCombination::VII:
+			if ((burstFN % 51 <= 14) && (burstFN % 51 >= 12))
+				return IDLE;
+			else
+				return TSC;
+			break;
+		case ChannelCombination::XIII: {
+			int mod52 = burstFN % 52;
+			if ((mod52 == 12) || (mod52 == 38))
+				return RACH;
+			else if ((mod52 == 25) || (mod52 == 51))
+				return IDLE;
+			else
+				return TSC;
+			break;
+		}
+		case ChannelCombination::LOOPBACK:
+			if ((burstFN % 51 <= 50) && (burstFN % 51 >= 48))
+				return IDLE;
+			else
+				return TSC;
+			break;
+		default:
+			return OFF;
+			break;
+		}
+	}
+
+	/* Initialize a multiframe slot in the filler table */
+	void init(size_t slot, signalVector *burst, bool fill);
+
+	ChannelCombination chanType[8];
+
+	/* The filler table */
+	signalVector *fillerTable[102][8];
+	int fillerModulus[8];
+
+	/* Received noise energy levels */
+	avgVector mFreqOffsets;
+
+	/* Transceiver mode */
+	trx_mode mode;
+};
\ No newline at end of file
diff --git a/Transceiver52M/ms/syncthing.cpp b/Transceiver52M/ms/syncthing.cpp
new file mode 100644
index 0000000..4f58c39
--- /dev/null
+++ b/Transceiver52M/ms/syncthing.cpp
@@ -0,0 +1,332 @@
+
+/*
+ * (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 "syncthing.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;
+
+const int offset_start = -15;
+int offsetrange = 200;
+static int offset_ctr = 0;
+
+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;
+
+		// 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;
+
+		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);
+
+		// auto this_offset = offset_start + (offset_ctr++ % offsetrange);
+		// std::cerr << "-- O " << this_offset << std::endl;
+		// send_ts = now_ts + ((target.FN() * 8 + (int)target.TN()) - (now_time.FN() * 8 + (int)now_time.TN())) * ONE_TS_BURST_LEN - timing_advance;
+
+		t->submit_burst_ts(buf2, burst->size() + pad, send_ts - pad);
+
+		// signalVector test(burst->size() + pad);
+		// convert_and_scale<float, int16_t>(test.begin(), buf2, burst->size() * 2 + pad, 1.f / 2047.f);
+		// 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;
+	}
+}
+#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(0, 0);
+	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).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_streams(void *rx_cb, void *tx_cb)
+{
+	return 0;
+}
+
+int ms_trx::init_dev_and_streams(void *rx_cb, void *tx_cb)
+{
+	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);
+
+	usleep(1000);
+	auto fn2 = get_tx_burst_handler_fn(tx_bh());
+	tx_task = std::thread(fn2);
+}
+
+void ms_trx::stop_threads()
+{
+	std::cerr << "killing threads...\r\n" << std::endl;
+	rx_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;
+
+	// 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;
+
+#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/syncthing.h b/Transceiver52M/ms/syncthing.h
new file mode 100644
index 0000000..8fb4aeb
--- /dev/null
+++ b/Transceiver52M/ms/syncthing.h
@@ -0,0 +1,224 @@
+#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>
+#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 = 12 * 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 {
+	GSM::Time gsmts;
+	blade_sample_type burst[ONE_TS_BURST_LEN];
+};
+
+using rx_queue_t = spsc_cond<8 * NUM_RXQ_FRAMES, one_burst, true, false>;
+
+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();
+
+	bool handle_sch_or_nb(bool first = false);
+	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);
+
+	int init_device();
+	int init_streams(void *rx_cb, void *tx_cb);
+	int init_dev_and_streams(void *rx_cb, void *tx_cb);
+	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;
+	}
+};
diff --git a/Transceiver52M/ms/uhd_specific.h b/Transceiver52M/ms/uhd_specific.h
new file mode 100644
index 0000000..88b9a1b
--- /dev/null
+++ b/Transceiver52M/ms/uhd_specific.h
@@ -0,0 +1,274 @@
+#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;
+	unsigned int rxFullScale, txFullScale;
+	int rxtxdelay;
+	float rxgain, txgain;
+
+	virtual ~uhd_hw()
+	{
+		delete[] one_pkt_buf;
+	}
+	uhd_hw() : rxFullScale(32767), txFullScale(32767), rxtxdelay(-67)
+	{
+	}
+
+	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, 3.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. Please consider the following:\n"
+						   "  Your write medium must sustain a rate of %fMB/s.\n"
+						   "  Dropped samples will not be written to the file.\n"
+						   "  Please modify this example for your purposes.\n"
+						   "  This message will not appear again.\n") %
+					     1.f;
+			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 (1) {
+				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);
+
+		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;
+	}
+
+	void set_name_aff_sched(const char *name, int cpunum, int schedtype, int prio)
+	{
+		pthread_setname_np(pthread_self(), name);
+
+		cpu_set_t cpuset;
+
+		CPU_ZERO(&cpuset);
+		CPU_SET(cpunum, &cpuset);
+
+		auto rv = pthread_setaffinity_np(pthread_self(), 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(pthread_self(), schedtype, &sch_params);
+		if (rv < 0) {
+			std::cerr << name << " sched: errreur! " << std::strerror(errno);
+			return exit(0);
+		}
+	}
+};
\ No newline at end of file
diff --git a/Transceiver52M/sch.c b/Transceiver52M/sch.c
index 5d2b5c6..9d81558 100644
--- a/Transceiver52M/sch.c
+++ b/Transceiver52M/sch.c
@@ -7,6 +7,8 @@
 #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"
 
@@ -52,13 +54,6 @@
 	.next_state  = sch_next_state,
 };
 
-const struct osmo_crc16gen_code gsm0503_sch_crc10 = {
-	.bits = 10,
-	.poly = 0x175,
-	.init = 0x000,
-	.remainder = 0x3ff,
-};
-
 #define GSM_MAX_BURST_LEN	157 * 4
 #define GSM_SYM_RATE		(1625e3 / 6) * 4
 
@@ -157,7 +152,7 @@
 }
 
 /* From osmo-bts */
-int gsm_sch_decode(uint8_t *info, sbit_t *data)
+__attribute__((xray_always_instrument)) __attribute__((noinline)) int gsm_sch_decode(uint8_t *info, sbit_t *data)
 {
 	int rc;
 	ubit_t uncoded[GSM_SCH_UNCODED_LEN];
diff --git a/Transceiver52M/sigProcLib.cpp b/Transceiver52M/sigProcLib.cpp
index c2cf2c9..51f7599 100644
--- a/Transceiver52M/sigProcLib.cpp
+++ b/Transceiver52M/sigProcLib.cpp
@@ -131,6 +131,7 @@
 static CorrelationSequence *gEdgeMidambles[] = {NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL};
 static CorrelationSequence *gRACHSequences[] = {NULL,NULL,NULL};
 static CorrelationSequence *gSCHSequence = NULL;
+static CorrelationSequence *gDummySequence = NULL;
 static PulseSequence *GSMPulse1 = NULL;
 static PulseSequence *GSMPulse4 = NULL;
 
@@ -156,6 +157,9 @@
   delete gSCHSequence;
   gSCHSequence = NULL;
 
+  delete gDummySequence;
+  gDummySequence = NULL;
+
   delete GMSKRotation1;
   delete GMSKReverseRotation1;
   delete GMSKRotation4;
@@ -1297,6 +1301,77 @@
   return status;
 }
 
+static bool generateDummyMidamble(int sps)
+{
+  bool status = true;
+  float toa;
+  complex *data = NULL;
+  signalVector *autocorr = NULL, *midamble = NULL;
+  signalVector *midMidamble = NULL, *_midMidamble = NULL;
+
+  delete gDummySequence;
+
+  /* Use middle 16 bits of each TSC. Correlation sequence is not pulse shaped */
+  midMidamble = modulateBurst(gDummyBurstTSC.segment(5,16), 0, sps, true);
+  if (!midMidamble)
+    return false;
+
+  /* Simulated receive sequence is pulse shaped */
+  midamble = modulateBurst(gDummyBurstTSC, 0, sps, false);
+  if (!midamble) {
+    status = false;
+    goto release;
+  }
+
+  // NOTE: Because ideal TSC 16-bit midamble is 66 symbols into burst,
+  //       the ideal TSC has an + 180 degree phase shift,
+  //       due to the pi/2 frequency shift, that
+  //       needs to be accounted for.
+  //       26-midamble is 61 symbols into burst, has +90 degree phase shift.
+  scaleVector(*midMidamble, complex(-1.0, 0.0));
+  scaleVector(*midamble, complex(0.0, 1.0));
+
+  conjugateVector(*midMidamble);
+
+  /* For SSE alignment, reallocate the midamble sequence on 16-byte boundary */
+  data = (complex *) convolve_h_alloc(midMidamble->size());
+  _midMidamble = new signalVector(data, 0, midMidamble->size(), convolve_h_alloc, free);
+  _midMidamble->setAligned(true);
+  midMidamble->copyTo(*_midMidamble);
+
+  autocorr = convolve(midamble, _midMidamble, NULL, NO_DELAY);
+  if (!autocorr) {
+    status = false;
+    goto release;
+  }
+
+  gDummySequence = new CorrelationSequence;
+  gDummySequence->sequence = _midMidamble;
+  gDummySequence->gain = peakDetect(*autocorr, &toa, NULL);
+
+  /* For 1 sps only
+   *     (Half of correlation length - 1) + midpoint of pulse shape + remainder
+   *     13.5 = (16 / 2 - 1) + 1.5 + (26 - 10) / 2
+   */
+  if (sps == 1)
+    gDummySequence->toa = toa - 13.5;
+  else
+    gDummySequence->toa = 0;
+
+release:
+  delete autocorr;
+  delete midamble;
+  delete midMidamble;
+
+  if (!status) {
+    delete _midMidamble;
+    free(data);
+    gDummySequence = NULL;
+  }
+
+  return status;
+}
+
 static CorrelationSequence *generateEdgeMidamble(int tsc)
 {
   complex *data = NULL;
@@ -1513,17 +1588,18 @@
   return energy/windowLength;
 }
 
-static signalVector *downsampleBurst(const signalVector &burst)
+static signalVector *downsampleBurst(const signalVector &burst, int in_len = DOWNSAMPLE_IN_LEN,
+				     int out_len = DOWNSAMPLE_OUT_LEN)
 {
-  signalVector in(DOWNSAMPLE_IN_LEN, dnsampler->len());
-  signalVector *out = new signalVector(DOWNSAMPLE_OUT_LEN);
-  burst.copyToSegment(in, 0, DOWNSAMPLE_IN_LEN);
+	signalVector in(in_len, dnsampler->len());
+	// gSCHSequence->sequence->size(), ensure next conv has no realloc
+	signalVector *out = new signalVector(out_len, 64);
+	burst.copyToSegment(in, 0, in_len);
 
-  if (dnsampler->rotate((float *) in.begin(), DOWNSAMPLE_IN_LEN,
-                        (float *) out->begin(), DOWNSAMPLE_OUT_LEN) < 0) {
-    delete out;
-    out = NULL;
-  }
+	if (dnsampler->rotate((float *)in.begin(), in_len, (float *)out->begin(), out_len) < 0) {
+		delete out;
+		out = NULL;
+	}
 
   return out;
 };
@@ -1544,6 +1620,9 @@
   if(ps < 0) // might be -22 for toa 40 with N=64, if off by a lot during sch ms sync
     return 0;
 
+  if (ps + N > burst->size())
+	  return 0;
+
   /* Estimate Signal power */
   S = 0.0f;
   for (int i=0, j=ps; i<(int)N; i++,j++)
@@ -1586,11 +1665,11 @@
     corr_in = &burst;
     break;
   case 4:
-    dec = downsampleBurst(burst);
-     /* Running at the downsampled rate at this point: */
-     corr_in = dec;
-     sps = 1;
-     break;
+	  dec = downsampleBurst(burst, len * 4, len);
+	  /* Running at the downsampled rate at this point: */
+	  corr_in = dec;
+	  sps = 1;
+	  break;
   default:
      osmo_panic("%s:%d SPS %d not supported! Only 1 or 4 supported", __FILE__, __LINE__, sps);
   }
@@ -1668,11 +1747,11 @@
   // Detect potential clipping
   // We still may be able to demod the burst, so we'll give it a try
   // and only report clipping if we can't demod.
-  float maxAmpl = maxAmplitude(rxBurst);
-  if (maxAmpl > CLIP_THRESH) {
-    LOG(INFO) << "max burst amplitude: " << maxAmpl << " is above the clipping threshold: " << CLIP_THRESH << std::endl;
-    clipping = true;
-  }
+  // float maxAmpl = maxAmplitude(rxBurst);
+  // if (maxAmpl > CLIP_THRESH) {
+  //   LOG(INFO) << "max burst amplitude: " << maxAmpl << " is above the clipping threshold: " << CLIP_THRESH << std::endl;
+  //   clipping = true;
+  // }
 
   start = target - head - 1;
   len = head + tail;
@@ -1735,7 +1814,6 @@
   int rc, start, target, head, tail, len;
   float _toa;
   complex _amp;
-  signalVector *corr, *_burst;
   CorrelationSequence *sync;
 
   if ((sps != 1) && (sps != 4))
@@ -1748,6 +1826,11 @@
     head = 4;
     tail = 4;
     break;
+  case sch_detect_type::SCH_DETECT_BUFFER:
+	  target = 1;
+	  head = 0;
+	  tail = (12 * 8 * 625) / 4; // 12 frames, downsampled /4 to 1 sps
+	  break;
   case sch_detect_type::SCH_DETECT_FULL:
   default:
     head = target - 1;
@@ -1758,19 +1841,16 @@
   start = (target - head) * 1 - 1;
   len = (head + tail) * 1;
   sync = gSCHSequence;
-  corr = new signalVector(len);
+  signalVector corr(len);
 
-  _burst = new signalVector(burst, sync->sequence->size(), 5);
+  signalVector _burst(burst, sync->sequence->size(), 5);
 
-  memcpy(_burst->begin() - sync->sequence->size(), sync->history,
-         sync->sequence->size() * sizeof(complex));
+  memcpy(_burst.begin() - sync->sequence->size(), sync->history, sync->sequence->size() * sizeof(complex));
 
   memcpy(sync->history, &burst.begin()[burst.size() - sync->sequence->size()],
          sync->sequence->size() * sizeof(complex));
 
-  rc = detectBurst(*_burst, *corr, sync,
-                   thresh, sps, start, len, ebp);
-  delete corr;
+  rc = detectBurst(_burst, corr, sync, thresh, sps, start, len, ebp);
 
   if (rc < 0) {
     return -1;
@@ -1780,12 +1860,31 @@
     return 0;
   }
 
-  /* Subtract forward search bits from delay */
-    ebp->toa = ebp->toa - head;
+  if (state == sch_detect_type::SCH_DETECT_BUFFER)
+	  ebp->toa = ebp->toa - (3 + 39 + 64);
+  else {
+	  /* Subtract forward search bits from delay */
+	  ebp->toa = ebp->toa - head;
+  }
 
   return rc;
 }
 
+static int detectDummyBurst(const signalVector &burst, float threshold,
+                               int sps, unsigned max_toa, struct estim_burst_params *ebp)
+{
+  int rc, target, head, tail;
+  CorrelationSequence *sync;
+
+  target = 3 + 58 + 16 + 5;
+  head = 10;
+  tail = 6 + max_toa;
+  sync = gDummySequence;
+
+  ebp->tsc = 0;
+  rc = detectGeneralBurst(burst, threshold, sps, target, head, tail, sync, ebp);
+  return rc;
+}
 
 /*
  * Normal burst detection
@@ -1854,6 +1953,9 @@
   case RACH:
     rc = detectRACHBurst(burst, threshold, sps, max_toa, type == EXT_RACH, ebp);
     break;
+  case IDLE:
+    rc = detectDummyBurst(burst, threshold, sps, max_toa, ebp);
+    break;
   default:
     LOG(ERR) << "Invalid correlation type";
   }
@@ -2057,6 +2159,7 @@
   generateRACHSequence(&gRACHSequences[2], gRACHSynchSequenceTS2, 1);
 
   generateSCHSequence(1);
+  generateDummyMidamble(1);
 
   for (int tsc = 0; tsc < 8; tsc++) {
     generateMidamble(1, tsc);
diff --git a/Transceiver52M/sigProcLib.h b/Transceiver52M/sigProcLib.h
index 73d4704..39c8ddd 100644
--- a/Transceiver52M/sigProcLib.h
+++ b/Transceiver52M/sigProcLib.h
@@ -94,6 +94,8 @@
 void scaleVector(signalVector &x,
                  complex scale);
 
+signalVector *delayVector(const signalVector *in, signalVector *out, float delay);
+
 /**
         Rough energy estimator.
         @param rxBurst A GSM burst.
@@ -137,6 +139,7 @@
 enum class sch_detect_type {
 	SCH_DETECT_FULL,
 	SCH_DETECT_NARROW,
+	SCH_DETECT_BUFFER,
 };
 
 int detectSCHBurst(signalVector &rxBurst,
diff --git a/trxcon/l1ctl.c b/trxcon/l1ctl.c
index f39bf03..8853c63 100644
--- a/trxcon/l1ctl.c
+++ b/trxcon/l1ctl.c
@@ -353,14 +353,14 @@
 	l1l->fbsb_conf_sent = false;
 
 	/* Only if current ARFCN differs */
-//	if (l1l->trx->band_arfcn != band_arfcn) {
+	if (l1l->trx->band_arfcn != band_arfcn) {
 		/* Update current ARFCN */
 		l1l->trx->band_arfcn = band_arfcn;
 
 		/* Tune transceiver to required ARFCN */
 		trx_if_cmd_rxtune(l1l->trx, band_arfcn);
 		trx_if_cmd_txtune(l1l->trx, band_arfcn);
-//	}
+	}
 
 	/* Transceiver might have been powered on before, e.g.
 	 * in case of sending L1CTL_FBSB_REQ due to signal loss. */
@@ -373,8 +373,7 @@
 	l1l->fbsb_timer.data = l1l;
 	l1l->fbsb_timer.cb = fbsb_timer_cb;
 	LOGP(DL1C, LOGL_INFO, "Starting FBSB timer %u ms\n", timeout * GSM_TDMA_FN_DURATION_uS / 1000);
-	osmo_timer_schedule(&l1l->fbsb_timer, 35,
-		timeout * GSM_TDMA_FN_DURATION_uS);
+	osmo_timer_schedule(&l1l->fbsb_timer, 2, timeout * GSM_TDMA_FN_DURATION_uS);
 
 exit:
 	msgb_free(msg);
diff --git a/trxcon/logging.c b/trxcon/logging.c
index 78915f2..0a4c08a 100644
--- a/trxcon/logging.c
+++ b/trxcon/logging.c
@@ -62,7 +62,7 @@
 		.name = "DSCH",
 		.description = "Scheduler management",
 		.color = "\033[1;36m",
-		.enabled = 1, .loglevel = LOGL_NOTICE,
+		.enabled = 0, .loglevel = LOGL_NOTICE,
 	},
 	[DSCHD] = {
 		.name = "DSCHD",
diff --git a/trxcon/trx_if.c b/trxcon/trx_if.c
index 84694c8..ffad6c0 100644
--- a/trxcon/trx_if.c
+++ b/trxcon/trx_if.c
@@ -47,7 +47,9 @@
 #include "logging.h"
 #include "scheduler.h"
 
+#ifdef IPCIF
 #include "../Transceiver52M/l1if.h"
+#endif
 
 static struct value_string trx_evt_names[] = {
 	{ 0, NULL } /* no events? */
@@ -146,13 +148,19 @@
 		return;
 	tcm = llist_entry(trx->trx_ctrl_list.next, struct trx_ctrl_msg, list);
 
+#ifdef IPCIF
 	char* cmd = malloc(TRXC_BUF_SIZE);
 	memcpy(cmd, tcm->cmd, TRXC_BUF_SIZE);
 
 	/* Send command */
 	LOGP(DTRX, LOGL_DEBUG, "Sending control '%s'\n", tcm->cmd);
 	trxif_to_trx_c(cmd);
-//	send(trx->trx_ofd_ctrl.fd, tcm->cmd, strlen(tcm->cmd) + 1, 0);
+
+#else
+	/* Send command */
+	LOGP(DTRX, LOGL_DEBUG, "Sending control '%s'\n", tcm->cmd);
+	send(trx->trx_ofd_ctrl.fd, tcm->cmd, strlen(tcm->cmd) + 1, 0);
+#endif
 
 	/* Trigger state machine */
 	if (trx->fsm->state != TRX_STATE_RSP_WAIT) {
@@ -476,7 +484,9 @@
 	struct trx_ctrl_msg *tcm;
 	int resp, rsp_len;
 	char buf[TRXC_BUF_SIZE], *p;
+	ssize_t read_len;
 
+#ifdef IPCIF
 	char* response = trxif_from_trx_c();
 	if (!response) {
 		LOGP(DTRX, LOGL_ERROR, "read() failed with rc=%zd\n", response);
@@ -484,6 +494,14 @@
 	}
 	memcpy(buf, response, TRXC_BUF_SIZE);
 	free(response);
+#else
+	read_len = read(ofd->fd, buf, sizeof(buf) - 1);
+	if (read_len <= 0) {
+		LOGP(DTRX, LOGL_ERROR, "read() failed with rc=%zd\n", read_len);
+		return read_len;
+	}
+	buf[read_len] = '\0';
+#endif
 
 	if (!!strncmp(buf, "RSP ", 4)) {
 		LOGP(DTRX, LOGL_NOTICE, "Unknown message on CTRL port: %s\n", buf);
@@ -592,7 +610,7 @@
 	uint32_t fn;
 	ssize_t read_len;
 
-
+#ifdef IPCIF
 	struct trxd_from_trx* rcvd = trxif_from_trx_d();
 	if (!rcvd) {
 		LOGP(DTRX, LOGL_ERROR, "read() failed with rc=%zd\n", rcvd);
@@ -609,6 +627,29 @@
 	memcpy(bits, rcvd->symbols, 148);
 
 	free(rcvd);
+#else
+	read_len = read(ofd->fd, buf, sizeof(buf));
+	if (read_len <= 0) {
+		LOGP(DTRXD, LOGL_ERROR, "read() failed with rc=%zd\n", read_len);
+		return read_len;
+	}
+
+	if (read_len != 158) {
+		LOGP(DTRXD, LOGL_ERROR,
+		     "Got data message with invalid "
+		     "length '%zd'\n",
+		     read_len);
+		return -EINVAL;
+	}
+#endif
+	tn = buf[0];
+	fn = osmo_load32be(buf + 1);
+	rssi = -(int8_t)buf[5];
+	toa256 = ((int16_t)(buf[6] << 8) | buf[7]);
+
+	/* Copy and convert bits {254..0} to sbits {-127..127} */
+	//osmo_ubit2sbit(bits, buf + 8, 148);
+	memcpy(bits, buf + 8, 148);
 
 	if (tn >= 8) {
 		LOGP(DTRXD, LOGL_ERROR, "Illegal TS %d\n", tn);
@@ -643,12 +684,44 @@
 int trx_if_tx_burst(struct trx_instance *trx, uint8_t tn, uint32_t fn,
 	uint8_t pwr, const ubit_t *bits)
 {
+#ifdef IPCIF
 	struct trxd_to_trx* t = malloc(sizeof(struct trxd_to_trx));
 	t->ts = tn;
 	t->fn = fn;
 	t->txlev = pwr;
 	memcpy(t->symbols, bits, 148);
 	trxif_to_trx_d(t);
+#else
+	uint8_t buf[TRXD_BUF_SIZE];
+
+	/**
+	 * We must be sure that we have clock,
+	 * and we have sent all control data
+	 *
+	 * TODO: introduce proper state machines for both
+	 *       transceiver and its TRXC interface.
+	 */
+#if 0
+	if (trx->fsm->state != TRX_STATE_ACTIVE) {
+		LOGP(DTRXD, LOGL_ERROR, "Ignoring TX data, "
+			"transceiver isn't ready\n");
+		return -EAGAIN;
+	}
+#endif
+
+	LOGP(DTRXD, LOGL_DEBUG, "TX burst tn=%u fn=%u pwr=%u\n", tn, fn, pwr);
+
+	buf[0] = tn;
+	osmo_store32be(fn, buf + 1);
+	buf[5] = pwr;
+
+	/* Copy ubits {0,1} */
+	memcpy(buf + 6, bits, 148);
+
+	/* Send data to transceiver */
+	send(trx->trx_ofd_data.fd, buf, 154, 0);
+
+#endif
 	return 0;
 }
 
@@ -683,6 +756,7 @@
 	/* Initialize CTRL queue */
 	INIT_LLIST_HEAD(&trx->trx_ctrl_list);
 
+#ifdef IPCIF
 	rc = eventfd(0, 0);
 	osmo_fd_setup(get_c_fd(), rc, OSMO_FD_READ, trx_ctrl_read_cb, trx, 0);
 	osmo_fd_register(get_c_fd());
@@ -690,8 +764,25 @@
 	rc = eventfd(0, 0);
 	osmo_fd_setup(get_d_fd(), rc, OSMO_FD_READ, trx_data_rx_cb, trx, 0);
 	osmo_fd_register(get_d_fd());
+#else
+	/* Open sockets */
+	rc = trx_udp_open(trx, &trx->trx_ofd_ctrl, local_host, base_port + 101, remote_host, base_port + 1,
+			  trx_ctrl_read_cb);
+	if (rc < 0)
+		goto udp_error;
 
+	rc = trx_udp_open(trx, &trx->trx_ofd_data, local_host, base_port + 102, remote_host, base_port + 2,
+			  trx_data_rx_cb);
+	if (rc < 0)
+		goto udp_error;
+#endif
 	return trx;
+
+udp_error:
+	LOGP(DTRX, LOGL_ERROR, "Couldn't establish UDP connection\n");
+	osmo_fsm_inst_free(trx->fsm);
+	talloc_free(trx);
+	return NULL;
 }
 
 /* Flush pending control messages */
@@ -723,8 +814,13 @@
 	trx_if_flush_ctrl(trx);
 
 	/* Close sockets */
+#ifdef IPCIF
 	close(get_c_fd()->fd);
 	close(get_d_fd()->fd);
+#else
+	trx_udp_close(&trx->trx_ofd_ctrl);
+	trx_udp_close(&trx->trx_ofd_data);
+#endif
 
 	/* Free memory */
 	osmo_fsm_inst_free(trx->fsm);
diff --git a/trxcon/trx_if.h b/trxcon/trx_if.h
index 58f76ed..abbde97 100644
--- a/trxcon/trx_if.h
+++ b/trxcon/trx_if.h
@@ -22,8 +22,10 @@
 };
 
 struct trx_instance {
-//	struct osmo_fd trx_ofd_ctrl;
-//	struct osmo_fd trx_ofd_data;
+#ifndef IPCIF
+	struct osmo_fd trx_ofd_ctrl;
+	struct osmo_fd trx_ofd_data;
+#endif
 
 	struct osmo_timer_list trx_ctrl_timer;
 	struct llist_head trx_ctrl_list;
diff --git a/trxcon/trxcon.c b/trxcon/trxcon.c
index 3005a93..d5fd6e1 100644
--- a/trxcon/trxcon.c
+++ b/trxcon/trxcon.c
@@ -21,6 +21,7 @@
  *
  */
 
+#define _GNU_SOURCE
 #include <stdio.h>
 #include <stdlib.h>
 #include <stdint.h>
@@ -29,6 +30,7 @@
 #include <unistd.h>
 #include <signal.h>
 #include <time.h>
+#include <pthread.h>
 
 #include <arpa/inet.h>
 
@@ -277,6 +279,21 @@
 {
 	int rc = 0;
 
+	cpu_set_t cpuset;
+
+	CPU_ZERO(&cpuset);
+	CPU_SET(3, &cpuset);
+	pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);
+
+	int prio = sched_get_priority_max(SCHED_RR) - 5;
+	struct sched_param param;
+	param.sched_priority = prio;
+	int rv = sched_setscheduler(0, SCHED_RR, &param);
+	if (rv < 0) {
+		LOGP(DAPP, LOGL_ERROR, "Failed to set sched!\n");
+		exit(0);
+	}
+
 	printf("%s", COPYRIGHT);
 	init_defaults();
 	handle_options(argc, argv);