Change in ...osmocom-bb[master]: trx_toolkit/data_msg.py: introduce header coding version 0x01

fixeria gerrit-no-reply at lists.osmocom.org
Wed Jul 3 14:11:47 UTC 2019


fixeria has submitted this change and it was merged. ( https://gerrit.osmocom.org/c/osmocom-bb/+/14579 )

Change subject: trx_toolkit/data_msg.py: introduce header coding version 0x01
......................................................................

trx_toolkit/data_msg.py: introduce header coding version 0x01

The new version adds the following fields to the TRX2L1 message,
keeping the L12TRX message unchanged:

  +------+-----+-----+-----+--------------------+
  | RSSI | ToA | MTS | C/I | soft-bits (254..0) |
  +------+-----+-----+-----+--------------------+

  - MTS (1 octet)  - Modulation and Training Sequence info, and
  - C/I (2 octets) - Carrier-to-Interference ratio (big endian).

== Coding of MTS: Modulation and Training Sequence info

3GPP TS 45.002 version 15.1.0 defines several modulation types,
and a few sets of training sequences for each type. The most
common are GMSK and 8-PSK (which is used in EDGE).

  +-----------------+---------------------------------------+
  | 7 6 5 4 3 2 1 0 | bit numbers (value range)             |
  +-----------------+---------------------------------------+
  | . . . . . X X X | Training Sequence Code (0..7)         |
  +-----------------+---------------------------------------+
  | . X X X X . . . | Modulation, TS set number (see below) |
  +-----------------+---------------------------------------+
  | X . . . . . . . | IDLE / nope frame indication (0 or 1) |
  +-----------------+---------------------------------------+

The bit number 7 (MSB) is set to high when either nothing has been
detected, or during IDLE frames, so we can deliver noise levels,
and avoid clock gaps on the L1 side. Other bits are ignored,
and should be set to low (0) in this case.

== Coding of modulation and TS set number

GMSK has 4 sets of training sequences (see tables 5.2.3a-d),
while 8-PSK (see tables 5.2.3f-g) and the others have 2 sets.
Access and Synchronization bursts also have several synch.
sequences.

  +-----------------+---------------------------------------+
  | 7 6 5 4 3 2 1 0 | bit numbers (value range)             |
  +-----------------+---------------------------------------+
  | . 0 0 X X . . . | GMSK, 4 TS sets (0..3)                |
  +-----------------+---------------------------------------+
  | . 0 1 0 X . . . | 8-PSK, 2 TS sets (0..1)               |
  +-----------------+---------------------------------------+
  | . 0 1 1 X . . . | AQPSK, 2 TS sets (0..1)               |
  +-----------------+---------------------------------------+
  | . 1 0 0 X . . . | 16QAM, 2 TS sets (0..1)               |
  +-----------------+---------------------------------------+
  | . 1 0 1 X . . . | 32QAM, 2 TS sets (0..1)               |
  +-----------------+---------------------------------------+
  | . 1 1 1 X . . . | RESERVED (0)                          |
  +-----------------+---------------------------------------+

== C/I: Carrier-to-Interference ratio

The C/I value is computed from the training sequence of each burst,
where we can compare the "ideal" training sequence with the actual
training sequence, and then express that difference in centiBels.

Change-Id: Ie810c5a482d1c908994e8cdd32a2ea641ae7cedd
Related: OS#4006, OS#1855
---
M src/target/trx_toolkit/data_msg.py
1 file changed, 362 insertions(+), 33 deletions(-)

Approvals:
  Jenkins Builder: Verified
  pespin: Looks good to me, but someone else must approve
  fixeria: Looks good to me, approved
  Hoernchen: Looks good to me, but someone else must approve



diff --git a/src/target/trx_toolkit/data_msg.py b/src/target/trx_toolkit/data_msg.py
index 5bd04ca..a2996ab 100644
--- a/src/target/trx_toolkit/data_msg.py
+++ b/src/target/trx_toolkit/data_msg.py
@@ -25,8 +25,37 @@
 import random
 import struct
 
+from enum import Enum
 from gsm_shared import *
 
+class Modulation(Enum):
+	""" Modulation types defined in 3GPP TS 45.002 """
+	ModGMSK  = (0b0000, 148)
+	Mod8PSK  = (0b0100, 444)
+	ModAQPSK = (0b0110, 296)
+	Mod16QAM = (0b1000, 592)
+	Mod32QAM = (0b1010, 740)
+
+	def __init__(self, coding, bl):
+		# Coding in TRXD header
+		self.coding = coding
+		# Burst length
+		self.bl = bl
+
+	@classmethod
+	def pick(self, coding):
+		for mod in list(self):
+			if mod.coding == coding:
+				return mod
+		return None
+
+	@classmethod
+	def pick_by_bl(self, bl):
+		for mod in list(self):
+			if mod.bl == bl:
+				return mod
+		return None
+
 class DATAMSG:
 	""" TRXD (DATA) message codec (common part).
 
@@ -92,7 +121,7 @@
 
 	# NOTE: up to 16 versions can be encoded
 	CHDR_VERSION_MAX = 0b1111
-	known_versions = [0x00]
+	known_versions = [0x00, 0x01]
 
 	# Common constructor
 	def __init__(self, fn = None, tn = None, burst = None, ver = 0):
@@ -101,6 +130,12 @@
 		self.fn = fn
 		self.tn = tn
 
+	# The common header length
+	@property
+	def CHDR_LEN(self):
+		# (VER + TN) + FN
+		return 1 + 4
+
 	# Generates message specific header
 	def gen_hdr(self):
 		raise NotImplementedError
@@ -197,12 +232,6 @@
 		if not self.ver in self.known_versions:
 			return False
 
-		if self.burst is None:
-			return False
-
-		if len(self.burst) not in (GSM_BURST_LEN, EDGE_BURST_LEN):
-			return False
-
 		if self.fn is None:
 			return False
 
@@ -237,7 +266,8 @@
 		buf += hdr
 
 		# Generate burst
-		buf += self.gen_burst()
+		if self.burst is not None:
+			buf += self.gen_burst()
 
 		# This is a rudiment from (legacy) OpenBTS transceiver,
 		# some L1 implementations still expect two dummy bytes.
@@ -248,11 +278,8 @@
 
 	# Parses a TRX DATA message
 	def parse_msg(self, msg):
-		# Calculate message length
-		length = len(msg)
-
-		# Check length
-		if length < (self.HDR_LEN + GSM_BURST_LEN):
+		# Make sure we have at least header
+		if len(msg) < self.HDR_LEN:
 			raise ValueError("Message is to short")
 
 		# Parse version and TDMA TN
@@ -267,7 +294,10 @@
 
 		# Copy burst, skipping header
 		msg_burst = msg[self.HDR_LEN:]
-		self.parse_burst(msg_burst)
+		if len(msg_burst) > 0:
+			self.parse_burst(msg_burst)
+		else:
+			self.burst = None
 
 class DATAMSG_L12TRX(DATAMSG):
 	""" L12TRX (L1 -> TRX) message codec.
@@ -276,6 +306,8 @@
 	or an Uplink burst on the MS side, and has the following
 	message specific fixed-size header preceding the burst bits:
 
+	== Versions 0x00, 0x01
+
 	  +-----+--------------------+
 	  | PWR | hard-bits (1 or 0) |
 	  +-----+--------------------+
@@ -290,13 +322,26 @@
 	"""
 
 	# Constants
-	HDR_LEN = 6
 	PWR_MIN = 0x00
 	PWR_MAX = 0xff
 
 	# Specific message fields
 	pwr = None
 
+	# Calculates header length depending on its version
+	@property
+	def HDR_LEN(self):
+		# Common header length
+		length = self.CHDR_LEN
+
+		# Message specific header length
+		if self.ver in (0x00, 0x01):
+			length += 1 # PWR
+		else:
+			raise IndexError("Unhandled version %u" % self.ver)
+
+		return length
+
 	# Validates the message fields
 	def validate(self):
 		# Validate common fields
@@ -309,6 +354,14 @@
 		if self.pwr < self.PWR_MIN or self.pwr > self.PWR_MAX:
 			return False
 
+		# FIXME: properly handle IDLE / NOPE indications
+		if self.burst is None:
+			return False
+
+		# FIXME: properly handle IDLE / NOPE indications
+		if len(self.burst) not in (GSM_BURST_LEN, EDGE_BURST_LEN):
+			return False
+
 		return True
 
 	# Generates a random power level
@@ -394,16 +447,78 @@
 	or a Downlink burst on the MS side, and has the following
 	message specific fixed-size header preceding the burst bits:
 
+	== Version 0x00
+
 	  +------+-----+--------------------+
 	  | RSSI | ToA | soft-bits (254..0) |
 	  +------+-----+--------------------+
 
+	== Version 0x01
+
+	  +------+-----+-----+-----+--------------------+
+	  | RSSI | ToA | MTS | C/I | soft-bits (254..0) |
+	  +------+-----+-----+-----+--------------------+
+
 	where:
 
 	  - RSSI (1 octet) - Received Signal Strength Indication
 			     encoded without the negative sign.
 	  - ToA (2 octets) - Timing of Arrival in units of 1/256
 			     of symbol (big endian).
+	  - MTS (1 octet)  - Modulation and Training Sequence info.
+	  - C/I (2 octets) - Carrier-to-Interference ratio (big endian).
+
+	== Coding of MTS: Modulation and Training Sequence info
+
+	3GPP TS 45.002 version 15.1.0 defines several modulation types,
+	and a few sets of training sequences for each type. The most
+	common are GMSK and 8-PSK (which is used in EDGE).
+
+	  +-----------------+---------------------------------------+
+	  | 7 6 5 4 3 2 1 0 | bit numbers (value range)             |
+	  +-----------------+---------------------------------------+
+	  | . . . . . X X X | Training Sequence Code (0..7)         |
+	  +-----------------+---------------------------------------+
+	  | . X X X X . . . | Modulation, TS set number (see below) |
+	  +-----------------+---------------------------------------+
+	  | X . . . . . . . | IDLE / nope frame indication (0 or 1) |
+	  +-----------------+---------------------------------------+
+
+	The bit number 7 (MSB) is set to high when either nothing has been
+	detected, or during IDLE frames, so we can deliver noise levels,
+	and avoid clock gaps on the L1 side. Other bits are ignored,
+	and should be set to low (0) in this case. L16 shall be set to 0x00.
+
+	== Coding of modulation and TS set number
+
+	GMSK has 4 sets of training sequences (see tables 5.2.3a-d),
+	while 8-PSK (see tables 5.2.3f-g) and the others have 2 sets.
+	Access and Synchronization bursts also have several synch.
+	sequences.
+
+	  +-----------------+---------------------------------------+
+	  | 7 6 5 4 3 2 1 0 | bit numbers (value range)             |
+	  +-----------------+---------------------------------------+
+	  | . 0 0 X X . . . | GMSK, 4 TS sets (0..3)                |
+	  +-----------------+---------------------------------------+
+	  | . 0 1 0 X . . . | 8-PSK, 2 TS sets (0..1)               |
+	  +-----------------+---------------------------------------+
+	  | . 0 1 1 X . . . | AQPSK, 2 TS sets (0..1)               |
+	  +-----------------+---------------------------------------+
+	  | . 1 0 0 X . . . | 16QAM, 2 TS sets (0..1)               |
+	  +-----------------+---------------------------------------+
+	  | . 1 0 1 X . . . | 32QAM, 2 TS sets (0..1)               |
+	  +-----------------+---------------------------------------+
+	  | . 1 1 1 X . . . | RESERVED (0)                          |
+	  +-----------------+---------------------------------------+
+
+	== C/I: Carrier-to-Interference ratio
+
+	The C/I value can be computed from the training sequence of each
+	burst, where we can compare the "ideal" training sequence with
+	the actual training sequence and then express that in centiBels.
+
+	== Coding of the burst bits
 
 	Unlike to be transmitted bursts, the received bursts are designated
 	using the soft-bits notation, so the receiver can indicate its
@@ -416,9 +531,6 @@
 
 	"""
 
-	# Constants
-	HDR_LEN = 8
-
 	# rxlev2dbm(0..63) gives us [-110..-47], plus -10 dbm for noise
 	RSSI_MIN = -120
 	RSSI_MAX = -47
@@ -427,11 +539,81 @@
 	TOA256_MIN = -32768
 	TOA256_MAX = 32767
 
+	# TSC (Training Sequence Code) range
+	TSC_RANGE = range(0, 8)
+
+	# C/I range (in centiBels)
+	CI_MIN = -1280
+	CI_MAX = 1280
+
+	# IDLE frame / nope detection indicator
+	NOPE_IND = (1 << 7)
+
 	# Specific message fields
 	rssi = None
 	toa256 = None
 
-	# Validates the message fields
+	# Version 0x01 specific (default values)
+	mod_type = Modulation.ModGMSK
+	nope_ind = False
+
+	tsc_set = None
+	tsc = None
+	ci = None
+
+	# Calculates header length depending on its version
+	@property
+	def HDR_LEN(self):
+		# Common header length
+		length = self.CHDR_LEN
+
+		# Message specific header length
+		if self.ver == 0x00:
+			# RSSI + ToA
+			length += 1 + 2
+		elif self.ver == 0x01:
+			# RSSI + ToA + TS + C/I
+			length += 1 + 2 + 1 + 2
+		else:
+			raise IndexError("Unhandled version %u" % self.ver)
+
+		return length
+
+	def _validate_burst_v0(self):
+		# Burst is mandatory
+		if self.burst is None:
+			return False
+
+		# ... and can be either of GSM (GMSK) or EDGE (8-PSK)
+		if len(self.burst) not in (GSM_BURST_LEN, EDGE_BURST_LEN):
+			return False
+
+		return True
+
+	def _validate_burst_v1(self):
+		# Burst is omitted in case of an IDLE / NOPE indication
+		if self.nope_ind and self.burst is None:
+			return True
+		if self.nope_ind and self.burst is not None:
+			return False
+
+		if self.burst is None:
+			return False
+
+		# Burst length depends on modulation type
+		if len(self.burst) != self.mod_type.bl:
+			return False
+
+		return True
+
+	# Validates the burst
+	def validate_burst(self):
+		if self.ver == 0x00:
+			return self._validate_burst_v0()
+		elif self.ver >= 0x01:
+			return self._validate_burst_v1()
+
+	# Validates the message header fields
 	def validate(self):
 		# Validate common fields
 		if not DATAMSG.validate(self):
@@ -449,6 +631,35 @@
 		if self.toa256 < self.TOA256_MIN or self.toa256 > self.TOA256_MAX:
 			return False
 
+		if self.ver >= 0x01:
+			if type(self.mod_type) is not Modulation:
+				return False
+
+			if self.tsc_set is None:
+				return False
+
+			if self.mod_type is Modulation.ModGMSK:
+				if self.tsc_set not in range(0, 4):
+					return False
+			else:
+				if self.tsc_set not in range(0, 2):
+					return False
+
+			if self.tsc is None:
+				return False
+
+			if self.tsc not in self.TSC_RANGE:
+				return False
+
+			if self.ci is None:
+				return False
+
+			if self.ci < self.CI_MIN or self.ci > self.CI_MAX:
+				return False
+
+		if not self.validate_burst():
+			return False
+
 		return True
 
 	# Generates a random RSSI value
@@ -477,6 +688,17 @@
 		self.rssi = self.rand_rssi()
 		self.toa256 = self.rand_toa256()
 
+		if self.ver >= 0x01:
+			self.mod_type = random.choice(list(Modulation))
+			if self.mod_type is Modulation.ModGMSK:
+				self.tsc_set = random.randint(0, 3)
+			else:
+				self.tsc_set = random.randint(0, 1)
+			self.tsc = random.choice(self.TSC_RANGE)
+
+			# C/I: Carrier-to-Interference ratio
+			self.ci = random.randint(self.CI_MIN, self.CI_MAX)
+
 	# Generates human-readable header description
 	def desc_hdr(self):
 		# Describe the common part
@@ -488,9 +710,61 @@
 		if self.toa256 is not None:
 			result += ("toa256=%d " % self.toa256)
 
+		if self.ver >= 0x01:
+			if not self.nope_ind:
+				if self.mod_type is not None:
+					result += ("%s " % self.mod_type)
+				if self.tsc_set is not None:
+					result += ("set=%u " % self.tsc_set)
+				if self.tsc is not None:
+					result += ("tsc=%u " % self.tsc)
+				if self.ci is not None:
+					result += ("C/I=%d cB " % self.ci)
+			else:
+				result += "(IDLE / NOPE IND) "
+
 		# Strip useless whitespace and return
 		return result.strip()
 
+	# Encodes Modulation and Training Sequence info
+	def gen_mts(self):
+		# IDLE / nope indication has no MTS info
+		if self.nope_ind:
+			return self.NOPE_IND
+
+		# TSC: . . . . . X X X
+		mts = self.tsc & 0b111
+
+		# MTS: . X X X X . . .
+		mts |= self.mod_type.coding << 3
+		mts |= self.tsc_set << 3
+
+		return mts
+
+	# Parses Modulation and Training Sequence info
+	def parse_mts(self, mts):
+		# IDLE / nope indication has no MTS info
+		self.nope_ind = (mts & self.NOPE_IND) > 0
+		if self.nope_ind:
+			self.mod_type = None
+			self.tsc_set = None
+			self.tsc = None
+			return
+
+		# TSC: . . . . . X X X
+		self.tsc = mts & 0b111
+
+		# MTS: . X X X X . . .
+		mts = (mts >> 3) & 0b1111
+		if (mts & 0b1100) > 0:
+			# Mask: . . . . M M M S
+			self.mod_type = Modulation.pick(mts & 0b1110)
+			self.tsc_set = mts & 0b1
+		else:
+			# GMSK: . . . . 0 0 S S
+			self.mod_type = Modulation.ModGMSK
+			self.tsc_set = mts & 0b11
+
 	# Generates message specific header part
 	def gen_hdr(self):
 		# Allocate an empty byte-array
@@ -503,6 +777,17 @@
 		# Big endian, 2 bytes (int32_t)
 		buf += struct.pack(">h", self.toa256)
 
+		if self.ver >= 0x01:
+			# Modulation and Training Sequence info
+			mts = self.gen_mts()
+			buf.append(mts)
+
+			# C/I: Carrier-to-Interference ratio (in centiBels)
+			if not self.nope_ind:
+				buf += struct.pack(">h", self.ci)
+			else:
+				buf += bytearray(2)
+
 		return buf
 
 	# Parses message specific header part
@@ -513,6 +798,16 @@
 		# Parse ToA (Time of Arrival)
 		self.toa256 = struct.unpack(">h", hdr[6:8])[0]
 
+		if self.ver >= 0x01:
+			# Modulation and Training Sequence info
+			self.parse_mts(hdr[8])
+
+			# C/I: Carrier-to-Interference ratio (in centiBels)
+			if not self.nope_ind:
+				self.ci = struct.unpack(">h", hdr[9:11])[0]
+			else:
+				self.ci = None
+
 	# Generates message specific burst
 	def gen_burst(self):
 		# Convert soft-bits to unsigned soft-bits
@@ -521,26 +816,38 @@
 		# Encode to bytes
 		return bytearray(burst_usbits)
 
+	# Parses message specific burst for header version 0
+	def _parse_burst_v0(self, burst):
+		bl = len(burst)
+
+		# We need to guess modulation by the length of burst
+		self.mod_type = Modulation.pick_by_bl(bl)
+		if self.mod_type is None:
+			# Some old transceivers append two dummy bytes
+			self.mod_type = Modulation.pick_by_bl(bl - 2)
+
+		if self.mod_type is None:
+			raise ValueError("Odd burst length")
+
+		return burst[:self.mod_type.bl]
+
 	# Parses message specific burst
 	def parse_burst(self, burst):
-		length = len(burst)
+		burst = list(burst)
 
-		# Distinguish between GSM and EDGE
-		if length >= EDGE_BURST_LEN:
-			burst_usbits = list(burst[:EDGE_BURST_LEN])
-		else:
-			burst_usbits = list(burst[:GSM_BURST_LEN])
+		if self.ver == 0x00:
+			burst = self._parse_burst_v0(burst)
 
 		# Convert unsigned soft-bits to soft-bits
-		burst_sbits = self.usbit2sbit(burst_usbits)
-
-		# Save
-		self.burst = burst_sbits
+		self.burst = self.usbit2sbit(burst)
 
 	# Generate a random message specific burst
-	def rand_burst(self, length = GSM_BURST_LEN):
+	def rand_burst(self, length = None):
 		self.burst = []
 
+		if length is None:
+			length = self.mod_type.bl
+
 		for i in range(length):
 			sbit = random.randint(-127, 127)
 			self.burst.append(sbit)
@@ -697,12 +1004,20 @@
 		assert(msg_l12trx_dec.tn == msg_l12trx.tn)
 		assert(msg_trx2l1_dec.fn == msg_trx2l1.fn)
 
+		# Match version specific fields
+		if msg_trx2l1.ver >= 0x01:
+			assert(msg_trx2l1_dec.nope_ind == msg_trx2l1.nope_ind)
+			assert(msg_trx2l1_dec.mod_type == msg_trx2l1.mod_type)
+			assert(msg_trx2l1_dec.tsc_set == msg_trx2l1.tsc_set)
+			assert(msg_trx2l1_dec.tsc == msg_trx2l1.tsc)
+			assert(msg_trx2l1_dec.ci == msg_trx2l1.ci)
+
+		log.info("Check header version %u coding: OK" % ver)
+
 		# Compare bursts
 		assert(msg_l12trx_dec.burst == msg_l12trx.burst)
 		assert(msg_trx2l1_dec.burst == msg_trx2l1.burst)
 
-		log.info("Check header version %u coding: OK" % ver)
-
 		msg_trx2l1_gen = msg_l12trx.gen_trx2l1()
 		msg_l12trx_gen = msg_trx2l1.gen_l12trx()
 
@@ -717,4 +1032,18 @@
 		assert(msg_trx2l1_gen.tn == msg_l12trx.tn)
 		assert(msg_l12trx_gen.fn == msg_trx2l1.fn)
 
-		log.info("Verify direct transformation: OK")
+		log.info("Verify version %u direct transformation: OK" % ver)
+
+		# Verify NOPE indication coding
+		if msg_trx2l1.ver >= 0x01:
+			msg_trx2l1 = DATAMSG_TRX2L1(ver = ver)
+			msg_trx2l1.nope_ind = True
+			msg_trx2l1.rand_hdr()
+
+			msg_trx2l1_dec = DATAMSG_TRX2L1()
+			msg_trx2l1_dec.parse_msg(msg_trx2l1.gen_msg())
+
+			assert(msg_trx2l1.nope_ind == msg_trx2l1_dec.nope_ind)
+			assert(msg_trx2l1.burst == msg_trx2l1_dec.burst)
+
+			log.info("Verify version %u NOPE indication coding: OK" % ver)

-- 
To view, visit https://gerrit.osmocom.org/c/osmocom-bb/+/14579
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings

Gerrit-Project: osmocom-bb
Gerrit-Branch: master
Gerrit-Change-Id: Ie810c5a482d1c908994e8cdd32a2ea641ae7cedd
Gerrit-Change-Number: 14579
Gerrit-PatchSet: 5
Gerrit-Owner: fixeria <axilirator at gmail.com>
Gerrit-Reviewer: Hoernchen <ewild at sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: fixeria <axilirator at gmail.com>
Gerrit-Reviewer: laforge <laforge at gnumonks.org>
Gerrit-Reviewer: pespin <pespin at sysmocom.de>
Gerrit-MessageType: merged
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20190703/64fb1d5a/attachment.html>


More information about the gerrit-log mailing list