203 lines
5.3 KiB
Python
203 lines
5.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
'''
|
|
TRXD PDU definitions based on declarative codec.
|
|
'''
|
|
|
|
# TRX Toolkit
|
|
#
|
|
# (C) 2021 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
|
|
# Author: Vadim Yanitskiy <vyanitskiy@sysmocom.de>
|
|
#
|
|
# All Rights Reserved
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# 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 codec
|
|
|
|
class Header(codec.BitFieldSet):
|
|
''' Header constructor for TRXD PDUs. '''
|
|
|
|
def __init__(self, ver: int, batched: bool = False):
|
|
f = [ # Dynamically generated field list
|
|
codec.BitField('ver', bl=4, val=ver) if not batched
|
|
else codec.BitField.Spare(bl=4), # RFU
|
|
codec.BitField.Spare(bl=1),
|
|
codec.BitField('tn', bl=3),
|
|
]
|
|
|
|
if ver >= 2: # TRXDv2 and higher
|
|
f.append(codec.BitField('batch', bl=1))
|
|
f.append(codec.BitField.Spare(bl=1))
|
|
f.append(codec.BitField('trxn', bl=6))
|
|
|
|
codec.BitFieldSet.__init__(self, set=tuple(f))
|
|
|
|
class MTS(codec.BitFieldSet):
|
|
''' Modulation and Training Sequence. '''
|
|
|
|
DEF_LEN = 1
|
|
STRUCT = (
|
|
codec.BitField('nope', bl=1),
|
|
codec.BitField('mod', bl=4),
|
|
codec.BitField('tsc', bl=3),
|
|
)
|
|
|
|
@staticmethod
|
|
def get_burst_len(mod: int) -> int:
|
|
''' Get burst length by modulation type. '''
|
|
|
|
GMSK_BURST_LEN = 148
|
|
|
|
if (mod >> 2) == 0b00: # GMSK
|
|
return 1 * GMSK_BURST_LEN
|
|
elif (mod >> 2) == 0b11: # AQPSK
|
|
return 2 * GMSK_BURST_LEN
|
|
elif (mod >> 1) == 0b010: # 8-PSK
|
|
return 3 * GMSK_BURST_LEN
|
|
elif (mod >> 1) == 0b100: # 16QAM
|
|
return 4 * GMSK_BURST_LEN
|
|
elif (mod >> 1) == 0b101: # 32QAM
|
|
return 5 * GMSK_BURST_LEN
|
|
elif mod == 0b0110: # GMSK (Access Burst)
|
|
return 1 * GMSK_BURST_LEN
|
|
raise ValueError('Unknown modulation type')
|
|
|
|
class Power(codec.Codec):
|
|
''' SCPIR and Tx power reduction (TRXDv2 and higher).
|
|
|
|
+-----------------+---------------------------------+
|
|
| 7 6 5 4 3 2 1 0 | Description |
|
|
+-----------------+---------------------------------+
|
|
| . . . . x x x x | Power REDuction (in 2 dB steps) |
|
|
+-----------------+---------------------------------+
|
|
| . x x x . . . . | SCPIR value (in 2 dB steps) |
|
|
+-----------------+---------------------------------+
|
|
| x . . . . . . . | SCPIR sign indicator |
|
|
+-----------------+---------------------------------+
|
|
|
|
'''
|
|
|
|
def from_bytes(self, vals: dict, data: bytes) -> int:
|
|
blob = ord(data) # Convert a byte to an int
|
|
vals['red'] = (blob & 0b1111) * 2
|
|
vals['scpir'] = ((blob >> 4) & 0b111) * 2
|
|
if blob & (1 << 7): # negative sign
|
|
vals['scpir'] *= -1
|
|
return 1
|
|
|
|
def to_bytes(self, vals: dict) -> bytes:
|
|
blob = (vals['red'] & 0b1111) \
|
|
| (abs(vals['scpir']) << 4) // 2 \
|
|
| (0x80 if (vals['scpir'] < 0) else 0x00)
|
|
return bytes((blob,))
|
|
|
|
class BurstBits(codec.Buf):
|
|
''' Soft-/hard-bits with variable length. '''
|
|
|
|
def __init__(self, name: str, *args, **kw):
|
|
codec.Buf.__init__(self, name, *args, **kw)
|
|
|
|
# This field has a variable length that depends on moduletion type
|
|
self.get_len = lambda v, _: MTS.get_burst_len(v['mod'])
|
|
# This field is not present in NOPE / IDLE indications
|
|
self.get_pres = lambda v: not v['nope']
|
|
|
|
|
|
class PDUv0Rx(codec.Envelope):
|
|
STRUCT = (
|
|
Header(ver=0),
|
|
codec.Uint32BE('fn'),
|
|
codec.Uint('rssi', mult=-1),
|
|
codec.Int16BE('toa256'),
|
|
codec.Buf('soft-bits'),
|
|
codec.Buf('pad'), # Optional
|
|
)
|
|
|
|
def __init__(self, *args, **kw):
|
|
codec.Envelope.__init__(self, *args, **kw)
|
|
|
|
# Field 'soft-bits' is either 148 (GMSK) or 444 (8-PSK) octets long
|
|
self.STRUCT[-2].get_len = lambda _, data: 444 if len(data) > 148 else 148
|
|
|
|
class PDUv0Tx(codec.Envelope):
|
|
STRUCT = (
|
|
Header(ver=0),
|
|
codec.Uint32BE('fn'),
|
|
codec.Uint('pwr'),
|
|
codec.Buf('hard-bits'),
|
|
)
|
|
|
|
|
|
class PDUv1Rx(codec.Envelope):
|
|
STRUCT = (
|
|
Header(ver=1),
|
|
codec.Uint32BE('fn'),
|
|
codec.Uint('rssi', mult=-1),
|
|
codec.Int16BE('toa256'),
|
|
MTS(),
|
|
codec.Int16BE('cir'),
|
|
BurstBits('soft-bits'),
|
|
)
|
|
|
|
class PDUv1Tx(PDUv0Tx):
|
|
# Same structure as PDUv0Tx, only the version is different
|
|
def __init__(self, *args, **kw):
|
|
PDUv0Tx.__init__(self, *args, **kw)
|
|
self.STRUCT[0]._fields[0].val = 1
|
|
|
|
|
|
class PDUv2Rx(codec.Envelope):
|
|
class BPDU(codec.Envelope):
|
|
''' Batched PDU part. '''
|
|
STRUCT = (
|
|
Header(ver=2, batched=True),
|
|
MTS(),
|
|
codec.Uint('rssi', mult=-1),
|
|
codec.Int16BE('toa256'),
|
|
codec.Int16BE('cir'),
|
|
BurstBits('soft-bits'),
|
|
)
|
|
|
|
STRUCT = (
|
|
Header(ver=2),
|
|
MTS(),
|
|
codec.Uint('rssi', mult=-1),
|
|
codec.Int16BE('toa256'),
|
|
codec.Int16BE('cir'),
|
|
codec.Uint32BE('fn'),
|
|
BurstBits('soft-bits'),
|
|
codec.Sequence(item=BPDU()).f('bpdu'),
|
|
)
|
|
|
|
class PDUv2Tx(codec.Envelope):
|
|
class BPDU(codec.Envelope):
|
|
''' Batched PDU part. '''
|
|
STRUCT = (
|
|
Header(ver=2, batched=True),
|
|
MTS(),
|
|
Power(),
|
|
BurstBits('hard-bits'),
|
|
)
|
|
|
|
STRUCT = (
|
|
Header(ver=2),
|
|
MTS(),
|
|
Power(),
|
|
codec.Uint32BE('fn'),
|
|
BurstBits('hard-bits'),
|
|
codec.Sequence(item=BPDU()).f('bpdu'),
|
|
)
|