Implement transceiver interface

This change introduces a new block 'TRX Interface', which is aimed
to provide an interface for external applications, such as Osmocom
MS side stack implementation - OsmocomBB. Currently one allows to
exchange raw GSM bursts between GR-GSM and other applications.

Moreover, there is a new 'trx.py' application, which implements a
simple follow graph, where all demodulated bursts are being sent
to external application via UDP link provided by 'TRX Interface'.
OsmoTRX (Osmocom's fork of OpenBTS transceiver) like control
interface is used to initialize, configure, start and stop the
application. Messages on this interface are human readable ASCII
strings, which contain a command and some related parameters.
diff --git a/apps/trx/ctrl_if.py b/apps/trx/ctrl_if.py
new file mode 100644
index 0000000..43fc185
--- /dev/null
+++ b/apps/trx/ctrl_if.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+
+# GR-GSM based transceiver
+# Transceiver UDP interface
+#
+# (C) 2016-2017 by Vadim Yanitskiy <axilirator@gmail.com>
+#
+# All Rights Reserved
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import socket
+import select
+
+class UDPServer:
+	def __init__(self, remote_addr, remote_port, bind_port):
+		self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+		self.sock.bind(('0.0.0.0', bind_port))
+		self.sock.setblocking(0)
+
+		# Save remote info
+		self.remote_addr = remote_addr
+		self.remote_port = remote_port
+
+	def loop(self):
+		r_event, w_event, x_event = select.select([self.sock], [], [])
+
+		# Check for incoming data
+		if self.sock in r_event:
+			data, addr = self.sock.recvfrom(128)
+			self.handle_rx(data)
+
+	def shutdown(self):
+		self.sock.close();
+
+	def send(self, data):
+		self.sock.sendto(data, (self.remote_addr, self.remote_port))
+
+	def handle_rx(self, data):
+		raise NotImplementedError
+
+class CTRLInterface(UDPServer):
+	def __init__(self, remote_addr, remote_port, bind_port, radio_if):
+		print("[i] Init TRX CTRL interface")
+		UDPServer.__init__(self, remote_addr, remote_port, bind_port)
+		self.tb = radio_if
+
+	def shutdown(self):
+		print("[i] Shutdown TRX CTRL interface")
+		UDPServer.shutdown(self)
+
+	def handle_rx(self, data):
+		if self.verify_req(data):
+			request = self.prepare_req(data)
+			self.parse_cmd(request)
+		else:
+			print("[!] Wrong data on CTRL interface")
+
+	def verify_req(self, data):
+		# Verify command signature
+		return data.startswith("CMD")
+
+	def prepare_req(self, data):
+		# Strip signature, paddings and \0
+		request = data[4:].strip().strip("\0")
+		# Split into a command and arguments
+		request = request.split(" ")
+		# Now we have something like ["TXTUNE", "941600"]
+		return request
+
+	def verify_cmd(self, request, cmd, argc):
+		# Check if requested command matches
+		if request[0] != cmd:
+			return False
+
+		# And has enough arguments
+		if len(request) - 1 != argc:
+			return False
+
+		# Check if all arguments are numeric
+		for v in request[1:]:
+			if not v.isdigit():
+				return False
+
+		return True
+
+	def parse_cmd(self, request):
+		response_code = "0"
+
+		# Power control
+		if self.verify_cmd(request, "POWERON", 0):
+			print("[i] Recv POWERON cmd")
+			if not self.tb.trx_started:
+				if self.tb.check_available():
+					print("[i] Starting transceiver...")
+					self.tb.trx_started = True
+					self.tb.start()
+				else:
+					print("[!] Transceiver isn't ready to start")
+					response_code = "-1"
+			else:
+				print("[!] Transceiver already started!")
+				response_code = "-1"
+		elif self.verify_cmd(request, "POWEROFF", 0):
+			print("[i] Recv POWEROFF cmd")
+			print("[i] Stopping transceiver...")
+			self.tb.trx_started = False
+			# TODO: flush all buffers between blocks
+			self.tb.stop()
+		elif self.verify_cmd(request, "SETRXGAIN", 1):
+			print("[i] Recv SETRXGAIN cmd")
+			# TODO: check gain value
+			gain = int(request[1])
+			self.tb.set_gain(gain)
+
+		# Tuning Control
+		elif self.verify_cmd(request, "RXTUNE", 1):
+			print("[i] Recv RXTUNE cmd")
+			# TODO: check freq range
+			freq = int(request[1]) * 1000
+			self.tb.set_fc(freq)
+		elif self.verify_cmd(request, "TXTUNE", 1):
+			print("[i] Recv TXTUNE cmd")
+			# TODO: is not implemented yet
+
+		# Misc
+		elif self.verify_cmd(request, "ECHO", 0):
+			print("[i] Recv ECHO cmd")
+
+		# Wrong / unknown command
+		else:
+			print("[!] Wrong request on CTRL interface")
+			response_code = "-1"
+
+		# Anyway, we need to respond
+		self.send_response(request, response_code)
+
+	def send_response(self, request, response_code):
+		# Include status code, for example ["TXTUNE", "0", "941600"]
+		request.insert(1, response_code)
+		# Add the response signature, and join back to string
+		response = "RSP " + " ".join(request) + "\0"
+		# Now we have something like "RSP TXTUNE 0 941600"
+		self.send(response)
diff --git a/apps/trx/radio_if.py b/apps/trx/radio_if.py
new file mode 100644
index 0000000..41eb3e8
--- /dev/null
+++ b/apps/trx/radio_if.py
@@ -0,0 +1,184 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+
+# GR-GSM based transceiver
+# Follow graph implementation
+#
+# (C) 2016-2017 by Vadim Yanitskiy <axilirator@gmail.com>
+#
+# All Rights Reserved
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import pmt
+import time
+import grgsm
+import osmosdr
+
+from math import pi
+
+from gnuradio.eng_option import eng_option
+from gnuradio import eng_notation
+from gnuradio.filter import firdes
+from gnuradio import blocks
+from gnuradio import gr
+
+class RadioInterface(gr.top_block):
+	# PHY specific variables
+	samp_rate = 2000000
+	shiftoff = 400e3
+	subdev_spec = "" # TODO: use it
+	device_args = ""
+	fc = 941.6e6 # TODO: set ARFCN to 0?
+	gain = 30
+	ppm = 0
+
+	# Application state flags
+	trx_started = False
+	fc_set = False
+
+	def __init__(self, phy_args, phy_subdev_spec,
+				phy_sample_rate, phy_gain, phy_ppm,
+				trx_remote_addr, trx_base_port):
+		print("[i] Init Radio interface")
+
+		# TRX block specific variables
+		self.trx_remote_addr = trx_remote_addr
+		self.trx_base_port = trx_base_port
+
+		# PHY specific variables
+		self.subdev_spec = phy_subdev_spec
+		self.samp_rate = phy_sample_rate
+		self.device_args = phy_args
+		self.gain = phy_gain
+		self.ppm = phy_ppm
+
+		gr.top_block.__init__(self, "GR-GSM TRX")
+		shift_fc = self.fc - self.shiftoff
+
+		##################################################
+		# PHY Definition
+		##################################################
+		self.phy = osmosdr.source(
+			args = "numchan=%d %s" % (1, self.device_args))
+
+		self.phy.set_bandwidth(250e3 + abs(self.shiftoff), 0)
+		self.phy.set_center_freq(shift_fc, 0)
+		self.phy.set_sample_rate(self.samp_rate)
+		self.phy.set_freq_corr(self.ppm, 0)
+		self.phy.set_iq_balance_mode(2, 0)
+		self.phy.set_dc_offset_mode(2, 0)
+		self.phy.set_gain_mode(False, 0)
+		self.phy.set_gain(self.gain, 0)
+		self.phy.set_if_gain(20, 0)
+		self.phy.set_bb_gain(20, 0)
+		self.phy.set_antenna("", 0)
+
+		##################################################
+		# GR-GSM Magic
+		##################################################
+		self.blocks_rotator = blocks.rotator_cc(
+			-2 * pi * self.shiftoff / self.samp_rate)
+
+		self.gsm_input = grgsm.gsm_input(
+			ppm = self.ppm, osr = 4, fc = self.fc,
+			samp_rate_in = self.samp_rate)
+
+		self.gsm_receiver = grgsm.receiver(4, ([0]), ([]))
+
+		self.gsm_clck_ctrl = grgsm.clock_offset_control(
+			shift_fc, self.samp_rate, osr = 4)
+
+		# TODO: implement configurable TS filter
+		self.gsm_ts_filter = grgsm.burst_timeslot_filter(0)
+
+		self.gsm_trx_if = grgsm.trx(self.trx_remote_addr,
+			str(self.trx_base_port))
+
+		##################################################
+		# Connections
+		##################################################
+		self.connect((self.phy, 0), (self.blocks_rotator, 0))
+		self.connect((self.blocks_rotator, 0), (self.gsm_input, 0))
+		self.connect((self.gsm_input, 0), (self.gsm_receiver, 0))
+
+		self.msg_connect((self.gsm_receiver, 'measurements'),
+			(self.gsm_clck_ctrl, 'measurements'))
+
+		self.msg_connect((self.gsm_clck_ctrl, 'ctrl'),
+			(self.gsm_input, 'ctrl_in'))
+
+		self.msg_connect((self.gsm_receiver, 'C0'),
+			(self.gsm_ts_filter, 'in'))
+
+		self.msg_connect((self.gsm_ts_filter, 'out'),
+			(self.gsm_trx_if, 'bursts'))
+
+	def check_available(self):
+		return self.fc_set
+
+	def shutdown(self):
+		print("[i] Shutdown Radio interface")
+		self.stop()
+		self.wait()
+
+	def get_args(self):
+		return self.args
+
+	def set_args(self, args):
+		self.args = args
+
+	def get_fc(self):
+		return self.fc
+
+	def set_fc(self, fc):
+		self.phy.set_center_freq(fc - self.shiftoff, 0)
+		self.gsm_input.set_fc(fc)
+		self.fc_set = True
+		self.fc = fc
+
+	def get_gain(self):
+		return self.gain
+
+	def set_gain(self, gain):
+		self.phy.set_gain(gain, 0)
+		self.gain = gain
+
+	def get_ppm(self):
+		return self.ppm
+
+	def set_ppm(self, ppm):
+		self.rtlsdr_source_0.set_freq_corr(ppm, 0)
+		self.ppm = ppm
+
+	def get_samp_rate(self):
+		return self.samp_rate
+
+	def set_samp_rate(self, samp_rate):
+		self.blocks_rotator.set_phase_inc(
+			-2 * pi * self.shiftoff / samp_rate)
+		self.gsm_input.set_samp_rate_in(samp_rate)
+		self.phy.set_sample_rate(samp_rate)
+		self.samp_rate = samp_rate
+
+	def get_shiftoff(self):
+		return self.shiftoff
+
+	def set_shiftoff(self, shiftoff):
+		self.blocks_rotator.set_phase_inc(
+			-2 * pi * shiftoff / self.samp_rate)
+		self.phy.set_bandwidth(250e3 + abs(shiftoff), 0)
+		self.phy.set_center_freq(self.fc - shiftoff, 0)
+		self.shiftoff = shiftoff
diff --git a/apps/trx/trx.py b/apps/trx/trx.py
new file mode 100755
index 0000000..25f76cf
--- /dev/null
+++ b/apps/trx/trx.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+
+# GR-GSM based transceiver
+#
+# (C) 2016-2017 by Vadim Yanitskiy <axilirator@gmail.com>
+#
+# All Rights Reserved
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import signal
+import getopt
+import sys
+
+from ctrl_if import CTRLInterface
+from radio_if import RadioInterface
+
+COPYRIGHT = \
+	"Copyright (C) 2016-2017 by Vadim Yanitskiy <axilirator@gmail.com>\n" \
+	"License GPLv2+: GNU GPL version 2 or later " \
+	"<http://gnu.org/licenses/gpl.html>\n" \
+	"This is free software: you are free to change and redistribute it.\n" \
+	"There is NO WARRANTY, to the extent permitted by law.\n"
+
+class Application:
+	# Application variables
+	remote_addr = "127.0.0.1"
+	base_port = 5700
+
+	# PHY specific
+	phy_sample_rate = 2000000
+	phy_subdev_spec = False
+	phy_gain = 30
+	phy_args = ""
+	phy_ppm = 0
+
+	def __init__(self):
+		self.print_copyright()
+		self.parse_argv()
+
+		# Set up signal handlers
+		signal.signal(signal.SIGINT, self.sig_handler)
+
+	def run(self):
+		# Init Radio interface
+		self.radio = RadioInterface(self.phy_args, self.phy_subdev_spec,
+			self.phy_sample_rate, self.phy_gain, self.phy_ppm,
+			self.remote_addr, self.base_port)
+
+		# Init TRX CTRL interface
+		self.server = CTRLInterface(self.remote_addr,
+			self.base_port + 101, self.base_port + 1, self.radio)
+
+		print("[i] Init complete")
+
+		# Enter main loop
+		while True:
+			self.server.loop()
+
+	def shutdown(self):
+		print("[i] Shutting down...")
+		self.server.shutdown()
+		self.radio.shutdown()
+
+	def print_copyright(self):
+		print(COPYRIGHT)
+
+	def print_help(self):
+		s  = " Usage: " + sys.argv[0] + " [options]\n\n" \
+			 " Some help...\n" \
+			 "  -h --help         this text\n\n"
+
+		# TRX specific
+		s += " TRX interface specific\n" \
+			 "  -s --remote-addr  Set remote address (default 127.0.0.1)\n" \
+			 "  -p --base-port    Set base port number (default 5700)\n\n"
+
+		# PHY specific
+		s += " Radio interface specific\n" \
+			 "  -a --device-args  Set device arguments\n" \
+			 "  -s --sample-rate  Set PHY sample rate (default 2000000)\n" \
+			 "  -S --subdev-spec  Set PHY sub-device specification\n" \
+			 "  -g --gain         Set PHY gain (default 30)\n" \
+			 "     --ppm          Set PHY frequency correction (default 0)\n"
+
+		print(s)
+
+	def parse_argv(self):
+		try:
+			opts, args = getopt.getopt(sys.argv[1:],
+				"a:p:i:s:S:g:h",
+				["help", "remote-addr=", "base-port=", "device-args=",
+				"gain=", "subdev-spec=", "sample-rate=", "ppm="])
+		except getopt.GetoptError as err:
+			# Print(help and exit)
+			self.print_help()
+			print("[!] " + str(err))
+			sys.exit(2)
+
+		for o, v in opts:
+			if o in ("-h", "--help"):
+				self.print_help()
+				sys.exit(2)
+
+			# TRX specific
+			elif o in ("-i", "--remote-addr"):
+				self.remote_addr = v
+			elif o in ("-p", "--base-port"):
+				if int(v) >= 0 and int(v) <= 65535:
+					self.base_port = int(v)
+				else:
+					print("[!] The port number should be in range [0-65536]")
+					sys.exit(2)
+
+			# PHY specific
+			elif o in ("-a", "--device-args"):
+				self.phy_args = v
+			elif o in ("-g", "--gain"):
+				self.phy_gain = int(v)
+			elif o in ("-S", "--subdev-spec"):
+				self.phy_subdev_spec = v
+			elif o in ("-s", "--sample-rate"):
+				self.phy_sample_rate = int(v)
+			elif o in ("--ppm"):
+				self.phy_ppm = int(v)
+
+	def sig_handler(self, signum, frame):
+		print("Signal %d received" % signum)
+		if signum is signal.SIGINT:
+			self.shutdown()
+			sys.exit(0)
+
+def main():
+	Application().run()
+
+if __name__ == '__main__':
+	main()