osmo-gsm-tester/src/osmo_gsm_tester/obj/enb_amarisoft.py

303 lines
14 KiB
Python
Raw Normal View History

# osmo_gsm_tester: specifics for running an SRS eNodeB process
#
# Copyright (C) 2020 by sysmocom - s.f.m.c. GmbH
#
# Author: Pau Espin Pedrol <pespin@sysmocom.de>
#
# 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 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import pprint
from ..core import log, util, config, template, process, remote
from ..core import schema
from . import enb
from . import rfemu
def on_register_schemas():
config_schema = {
'license_server_addr': schema.IPV4,
}
schema.register_config_schema('amarisoft', config_schema)
config_schema = {
'log_options': schema.STR,
}
schema.register_config_schema('amarisoftenb', config_schema)
def rf_type_valid(rf_type_str):
return rf_type_str in ('uhd', 'zmq')
class AmarisoftENB(enb.eNodeB):
REMOTE_DIR = '/osmo-gsm-tester-amarisoftenb'
BINFILE = 'lteenb'
CFGFILE = 'amarisoft_enb.cfg'
CFGFILE_SIB1 = 'amarisoft_sib1.asn'
CFGFILE_SIB23 = 'amarisoft_sib23.asn'
CFGFILE_RF = 'amarisoft_rf_driver.cfg'
CFGFILE_DRB = 'amarisoft_drb.cfg'
LOGFILE = 'lteenb.log'
PHY_SIGNAL_FILE = 'lteenb.log.bin'
def __init__(self, testenv, conf):
super().__init__(testenv, conf, 'amarisoftenb')
self.ue = None
self.run_dir = None
self.inst = None
self._bin_prefix = None
self.gen_conf = None
self.config_file = None
self.config_sib1_file = None
self.config_sib23_file = None
self.config_rf_file = None
self.config_drb_file = None
self.log_file = None
self.process = None
self.rem_host = None
self.remote_inst = None
self.remote_config_file = None
self.remote_config_sib1_file = None
self.remote_config_sib23_file = None
self.remote_config_rf_file = None
self.remote_config_drb_file = None
self.remote_log_file = None
self.enable_measurements = False
self.testenv = testenv
if not rf_type_valid(conf.get('rf_dev_type', None)):
raise log.Error('Invalid rf_dev_type=%s' % conf.get('rf_dev_type', None))
def bin_prefix(self):
if self._bin_prefix is None:
self._bin_prefix = os.getenv('AMARISOFT_PATH_ENB', None)
if self._bin_prefix == None:
self._bin_prefix = self.testenv.suite().trial().get_inst('amarisoftenb', self._run_node.run_label())
return self._bin_prefix
def cleanup(self):
if self.process is None:
return
if self._run_node.is_local():
return
# copy back files (may not exist, for instance if there was an early error of process):
try:
self.rem_host.scpfrom('scp-back-log', self.remote_log_file, self.log_file)
except Exception as e:
self.log(repr(e))
try:
self.rem_host.scpfrom('scp-back-phy-signal-log', self.remote_phy_signal_file, self.phy_signal_file)
except Exception as e:
self.log(repr(e))
4g: Introduce ZMQ GnuRadio stream broker srsENB currently creates 1 zmq stream (1 tx, 1 rx) for each cell (2 if MIMO is enabled). Each cell transceives on a given EARFCN (and several cells can transmit on same EARFCN). However, for handover test purposes, we want to join all cells operating on the same EARFCN to transceive on the same ZMQ conn, so that an srsUE can interact with them at the same time (same as if the medium was shared). Furthermore, we want to set different gains on each of those paths before merging them in order to emulate RF conditions like handover. In order to do so, a new element called the Broker is introduced, which is placed in between ENBs and UEs ZMQ conenctions, multiplexing the connections on the ENB side towards the UE side. A separate process for the broker is run remotely (ENB run host) which listens on a ctrl socket for commands. An internal Broker class is used in osmo-gsm-tester to interact with the remote script, for instance to configure the ports, start and stop the remote process, send commands to it, etc. On each ENB, when the rfemu "gnuradio_zmq" rfemu implementation is selected in configuration, it will configure its zmq connections and the UE ones to go over the Broker. As a result, that means the UE zmq port configuration is expected to be different than when no broker is in used, since there's the multiplexing per EARFCN in between. In this commit, only 1 ENB is supported, but multi-enb support is planned in the future. The handover test passes in the docker setup with this config: """ OSMO_GSM_TESTER_OPTS="-T -l dbg -s 4g:srsue-rftype@zmq+srsenb-rftype@zmq+" \ "mod-enb-nprb@6+mod-enb-ncells@2+mod-enb-cells-2ca+suite-4g@10,2+" \ "mod-enb-meas-enable -t =handover.py" """ and in resources.conf (or scenario), added: """ enb: ... cell_list: - dl_rfemu: type: gnuradio_zmq - dl_rfemu: type: gnuradio_zmq """ Note that since the broker is used, there's not need for mod-srsue-ncarriers@2 since the broker is joining the 2 enb cells into 1 stream on the UE side. Change-Id: I6282cda400558dcb356276786d91e6388524c5b1
2020-10-05 17:23:38 +00:00
# Clean up for parent class:
super().cleanup()
def start(self, epc):
self.log('Starting AmarisoftENB')
self._epc = epc
self.run_dir = util.Dir(self.testenv.test().get_run_dir().new_dir(self.name()))
self.configure()
self._start()
# send t+Enter to enable console trace
self.dbg('Enabling console trace')
self.process.stdin_write('t\n')
def _start(self):
if self._run_node.is_local():
env = { 'LD_LIBRARY_PATH': util.prepend_library_path(self.inst) }
binary = self.inst.child('.', AmarisoftENB.BINFILE)
self.dbg(run_dir=self.run_dir, binary=binary, env=env)
args = (binary, os.path.abspath(self.config_file))
self.process = process.Process(self.name(), self.run_dir, args, env=env)
else:
remote_env = { 'LD_LIBRARY_PATH': self.remote_inst }
remote_binary = self.remote_inst.child('', AmarisoftENB.BINFILE)
args = (remote_binary, self.remote_config_file)
self.process = self.rem_host.RemoteProcess(AmarisoftENB.BINFILE, args, remote_env=remote_env)
self.testenv.remember_to_stop(self.process)
self.process.launch()
def gen_conf_file(self, path, filename, values):
self.dbg('AmarisoftENB ' + filename + ':\n' + pprint.pformat(values))
with open(path, 'w') as f:
r = template.render(filename, values)
self.dbg(r)
f.write(r)
def configure(self):
self.inst = util.Dir(os.path.abspath(self.bin_prefix()))
if not self.inst.isfile('', AmarisoftENB.BINFILE):
raise log.Error('No %s binary in' % AmarisoftENB.BINFILE, self.inst)
self.config_file = self.run_dir.child(AmarisoftENB.CFGFILE)
self.config_sib1_file = self.run_dir.child(AmarisoftENB.CFGFILE_SIB1)
self.config_sib23_file = self.run_dir.child(AmarisoftENB.CFGFILE_SIB23)
self.config_rf_file = self.run_dir.child(AmarisoftENB.CFGFILE_RF)
self.config_drb_file = self.run_dir.child(AmarisoftENB.CFGFILE_DRB)
self.log_file = self.run_dir.child(AmarisoftENB.LOGFILE)
self.phy_signal_file = self.run_dir.child(AmarisoftENB.PHY_SIGNAL_FILE)
if not self._run_node.is_local():
self.rem_host = remote.RemoteHost(self.run_dir, self._run_node.ssh_user(), self._run_node.ssh_addr())
remote_prefix_dir = util.Dir(AmarisoftENB.REMOTE_DIR)
self.remote_inst = util.Dir(remote_prefix_dir.child(os.path.basename(str(self.inst))))
remote_run_dir = util.Dir(remote_prefix_dir.child(AmarisoftENB.BINFILE))
self.remote_config_file = remote_run_dir.child(AmarisoftENB.CFGFILE)
self.remote_config_sib1_file = remote_run_dir.child(AmarisoftENB.CFGFILE_SIB1)
self.remote_config_sib23_file = remote_run_dir.child(AmarisoftENB.CFGFILE_SIB23)
self.remote_config_rf_file = remote_run_dir.child(AmarisoftENB.CFGFILE_RF)
self.remote_config_drb_file = remote_run_dir.child(AmarisoftENB.CFGFILE_DRB)
self.remote_log_file = remote_run_dir.child(AmarisoftENB.LOGFILE)
self.remote_phy_signal_file = remote_run_dir.child(AmarisoftENB.PHY_SIGNAL_FILE)
values = super().configure(['amarisoft', 'amarisoftenb'])
# Convert parsed boolean string to Python boolean:
self.enable_measurements = util.str2bool(values['enb'].get('enable_measurements', 'false'))
config.overlay(values, dict(enb={'enable_measurements': self.enable_measurements}))
config.overlay(values, dict(enb={'enable_dl_awgn': util.str2bool(values['enb'].get('enable_dl_awgn', 'false'))}))
# We need to set some specific variables programatically here to match IP addresses:
if self._conf.get('rf_dev_type') == 'zmq':
base_srate = self.num_prb2base_srate(self.num_prb())
4g: Introduce ZMQ GnuRadio stream broker srsENB currently creates 1 zmq stream (1 tx, 1 rx) for each cell (2 if MIMO is enabled). Each cell transceives on a given EARFCN (and several cells can transmit on same EARFCN). However, for handover test purposes, we want to join all cells operating on the same EARFCN to transceive on the same ZMQ conn, so that an srsUE can interact with them at the same time (same as if the medium was shared). Furthermore, we want to set different gains on each of those paths before merging them in order to emulate RF conditions like handover. In order to do so, a new element called the Broker is introduced, which is placed in between ENBs and UEs ZMQ conenctions, multiplexing the connections on the ENB side towards the UE side. A separate process for the broker is run remotely (ENB run host) which listens on a ctrl socket for commands. An internal Broker class is used in osmo-gsm-tester to interact with the remote script, for instance to configure the ports, start and stop the remote process, send commands to it, etc. On each ENB, when the rfemu "gnuradio_zmq" rfemu implementation is selected in configuration, it will configure its zmq connections and the UE ones to go over the Broker. As a result, that means the UE zmq port configuration is expected to be different than when no broker is in used, since there's the multiplexing per EARFCN in between. In this commit, only 1 ENB is supported, but multi-enb support is planned in the future. The handover test passes in the docker setup with this config: """ OSMO_GSM_TESTER_OPTS="-T -l dbg -s 4g:srsue-rftype@zmq+srsenb-rftype@zmq+" \ "mod-enb-nprb@6+mod-enb-ncells@2+mod-enb-cells-2ca+suite-4g@10,2+" \ "mod-enb-meas-enable -t =handover.py" """ and in resources.conf (or scenario), added: """ enb: ... cell_list: - dl_rfemu: type: gnuradio_zmq - dl_rfemu: type: gnuradio_zmq """ Note that since the broker is used, there's not need for mod-srsue-ncarriers@2 since the broker is joining the 2 enb cells into 1 stream on the UE side. Change-Id: I6282cda400558dcb356276786d91e6388524c5b1
2020-10-05 17:23:38 +00:00
rf_dev_args = self.get_zmq_rf_dev_args(values)
config.overlay(values, dict(enb=dict(sample_rate = base_srate / (1000*1000),
rf_dev_args = rf_dev_args)))
# Set UHD frame size as a function of the cell bandwidth on B2XX
if self._conf.get('rf_dev_type') == 'uhd' and values['enb'].get('rf_dev_args', None) is not None:
if 'b200' in values['enb'].get('rf_dev_args'):
rf_dev_args = values['enb'].get('rf_dev_args', '')
rf_dev_args += ',' if rf_dev_args != '' and not rf_dev_args.endswith(',') else ''
if self._txmode == 1:
# SISO config
if self._num_prb < 25:
rf_dev_args += 'send_frame_size=512,recv_frame_size=512'
elif self._num_prb == 25:
rf_dev_args += 'send_frame_size=1024,recv_frame_size=1024'
else:
rf_dev_args += ''
else:
# MIMO config
if self._num_prb == 6:
rf_dev_args += 'send_frame_size=512,recv_frame_size=512'
else:
rf_dev_args += 'num_recv_frames=64,num_send_frames=64'
if self._num_prb > 50:
# Reduce over the wire format to sc12
rf_dev_args += ',otw_format=sc12'
config.overlay(values, dict(enb=dict(rf_dev_args=rf_dev_args)))
logfile = self.log_file if self._run_node.is_local() else self.remote_log_file
config.overlay(values, dict(enb=dict(log_filename=logfile)))
phy_signal_file = self.phy_signal_file if self._run_node.is_local() else self.remote_phy_signal_file
config.overlay(values, dict(enb=dict(phy_signal_file=phy_signal_file)))
# rf driver is shared between amarisoft enb and ue, so it has a
# different cfg namespace 'trx'. Copy needed values over there:
config.overlay(values, dict(trx=dict(rf_dev_type=values['enb'].get('rf_dev_type', None),
rf_dev_args=values['enb'].get('rf_dev_args', None),
rf_dev_sync=values['enb'].get('rf_dev_sync', None),
rx_gain=values['enb'].get('rx_gain', None),
tx_gain=values['enb'].get('tx_gain', None),
)))
self.gen_conf = values
self.gen_conf_file(self.config_file, AmarisoftENB.CFGFILE, values)
self.gen_conf_file(self.config_sib1_file, AmarisoftENB.CFGFILE_SIB1, values)
self.gen_conf_file(self.config_sib23_file, AmarisoftENB.CFGFILE_SIB23, values)
self.gen_conf_file(self.config_rf_file, AmarisoftENB.CFGFILE_RF, values)
self.gen_conf_file(self.config_drb_file, AmarisoftENB.CFGFILE_DRB, values)
if not self._run_node.is_local():
self.rem_host.recreate_remote_dir(self.remote_inst)
self.rem_host.scp('scp-inst-to-remote', str(self.inst), remote_prefix_dir)
self.rem_host.recreate_remote_dir(remote_run_dir)
self.rem_host.scp('scp-cfg-to-remote', self.config_file, self.remote_config_file)
self.rem_host.scp('scp-cfg-sib1-to-remote', self.config_sib1_file, self.remote_config_sib1_file)
self.rem_host.scp('scp-cfg-sib23-to-remote', self.config_sib23_file, self.remote_config_sib23_file)
self.rem_host.scp('scp-cfg-rr-to-remote', self.config_rf_file, self.remote_config_rf_file)
self.rem_host.scp('scp-cfg-drb-to-remote', self.config_drb_file, self.remote_config_drb_file)
def ue_add(self, ue):
if self.ue is not None:
raise log.Error("More than one UE per ENB not yet supported (ZeroMQ)")
self.ue = ue
def running(self):
return not self.process.terminated()
def get_rfemu(self, cell=0, dl=True):
cell_list = self.gen_conf['enb'].get('cell_list', None)
if cell_list is None or len(cell_list) < cell + 1:
raise log.Error('cell_list attribute or subitem not found!')
rfemu_cfg = cell_list[cell].get('dl_rfemu', None)
if rfemu_cfg is None: # craft amarisoft by default:
rfemu_cfg = {'type': 'amarisoftctl',
'addr': self.addr(),
'ports': [9001]
}
if rfemu_cfg['type'] == 'amarisoftctl': # this one requires extra config:
config.overlay(rfemu_cfg, dict(cell_id=cell_list[cell]['cell_id']))
rfemu_obj = rfemu.get_instance_by_type(rfemu_cfg['type'], rfemu_cfg)
return rfemu_obj
def ue_max_rate(self, downlink=True, num_carriers=1):
# The max rate for a single UE per PRB configuration in TM1 with MCS 28 QAM64
max_phy_rate_tm1_dl = { 6 : 3.2e6,
15 : 9.2e6,
25 : 18e6,
50 : 36e6,
75 : 55e6,
100 : 75e6 }
max_phy_rate_tm1_ul = { 6 : 2.0e6,
15 : 5.1e6,
25 : 10e6,
50 : 21e6,
75 : 32e6,
100 : 47e6 }
if downlink:
max_rate = max_phy_rate_tm1_dl[self.num_prb()]
else:
max_rate = max_phy_rate_tm1_ul[self.num_prb()]
# MIMO only supported for Downlink
if downlink:
if self._txmode > 2:
max_rate *= 2
# Lower max MCS for TM2 and above results in lower max rate
if self._txmode >= 2 and self.num_prb() <= 25:
max_rate *= 0.85
# Assume we schedule all carriers
max_rate *= num_carriers
# Reduce expected UL rate due to bug in UCI scheduling in Amarisoft eNB
if downlink == False and num_carriers == 2:
# 2nd carrier @ 25%
max_rate = max_rate / 2 + (.25 * max_rate / 2)
return max_rate
# vim: expandtab tabstop=4 shiftwidth=4