| # osmo_gsm_tester: base classes to share code among eNodeB subclasses. |
| # |
| # 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/>. |
| |
| from abc import ABCMeta, abstractmethod |
| from ..core import log, config |
| from ..core import schema |
| from . import run_node |
| from .gnuradio_zmq_broker import GrBroker |
| |
| def on_register_schemas(): |
| resource_schema = { |
| 'label': schema.STR, |
| 'type': schema.STR, |
| 'gtp_bind_addr': schema.IPV4, |
| 'id': schema.UINT, |
| 'num_prb': schema.UINT, |
| 'duplex': schema.STR, |
| 'tdd_uldl_config': schema.UINT, |
| 'tdd_special_subframe_pattern': schema.UINT, |
| 'transmission_mode': schema.LTE_TRANSMISSION_MODE, |
| 'tx_gain': schema.UINT, |
| 'rx_gain': schema.UINT, |
| 'rf_dev_type': schema.STR, |
| 'rf_dev_args': schema.STR, |
| 'rf_dev_sync': schema.STR, |
| 'additional_args[]': schema.STR, |
| 'inactivity_timer': schema.INT, |
| 'enable_measurements': schema.BOOL_STR, |
| 'enable_dl_awgn': schema.BOOL_STR, |
| 'dl_awgn_snr': schema.INT, |
| 'a1_report_type': schema.STR, |
| 'a1_report_value': schema.INT, |
| 'a1_hysteresis': schema.INT, |
| 'a1_time_to_trigger': schema.INT, |
| 'a2_report_type': schema.STR, |
| 'a2_report_value': schema.INT, |
| 'a2_hysteresis': schema.INT, |
| 'a2_time_to_trigger': schema.INT, |
| 'a3_report_type': schema.STR, |
| 'a3_report_value': schema.INT, |
| 'a3_hysteresis': schema.INT, |
| 'a3_time_to_trigger': schema.INT, |
| 'num_cells': schema.UINT, |
| 'cell_list[].cell_id': schema.UINT, |
| 'cell_list[].rf_port': schema.UINT, |
| 'cell_list[].pci': schema.UINT, |
| 'cell_list[].ncell_list[].enb_id': schema.UINT, |
| 'cell_list[].ncell_list[].cell_id': schema.UINT, |
| 'cell_list[].ncell_list[].pci': schema.UINT, |
| 'cell_list[].ncell_list[].dl_earfcn': schema.UINT, |
| 'cell_list[].scell_list[]': schema.UINT, |
| 'cell_list[].dl_earfcn': schema.UINT, |
| 'cell_list[].root_seq_idx': schema.UINT, |
| 'cell_list[].tac': schema.UINT, |
| 'cell_list[].dl_rfemu.type': schema.STR, |
| 'cell_list[].dl_rfemu.addr': schema.IPV4, |
| 'cell_list[].dl_rfemu.ports[]': schema.UINT, |
| } |
| for key, val in run_node.RunNode.schema().items(): |
| resource_schema['run_node.%s' % key] = val |
| schema.register_resource_schema('enb', resource_schema) |
| |
| class eNodeB(log.Origin, metaclass=ABCMeta): |
| |
| ############## |
| # PROTECTED |
| ############## |
| def __init__(self, testenv, conf, name): |
| super().__init__(log.C_RUN, '%s' % name) |
| self._conf = conf |
| self._run_node = run_node.RunNode.from_conf(conf.get('run_node', {})) |
| self._gtp_bind_addr = conf.get('gtp_bind_addr', None) |
| if self._gtp_bind_addr is None: |
| self._gtp_bind_addr = self._run_node.run_addr() |
| label = conf.get('label', None) |
| self.set_name('%s_%s' % (name, label if label is not None else self._run_node.run_addr())) |
| self._txmode = 0 |
| self._id = None |
| self._duplex = None |
| self._num_prb = 0 |
| self._num_cells = None |
| self._epc = None |
| self.gen_conf = None |
| self.gr_broker = GrBroker.ref() |
| self.gr_broker.register_enb(self) |
| self._use_gr_broker = False |
| |
| def using_grbroker(self, cfg_values): |
| # whether we are to use Grbroker in between ENB and UE. |
| # Initial checks: |
| if cfg_values['enb'].get('rf_dev_type') != 'zmq': |
| return False |
| cell_list = cfg_values['enb']['cell_list'] |
| use_match = False |
| notuse_match = False |
| for cell in cell_list: |
| if cell.get('dl_rfemu', False) and cell['dl_rfemu'].get('type', None) == 'gnuradio_zmq': |
| use_match = True |
| else: |
| notuse_match = True |
| if use_match and notuse_match: |
| raise log.Error('Some Cells are configured to use gnuradio_zmq and some are not, unsupported') |
| return use_match |
| |
| def calc_required_zmq_ports(self, cfg_values): |
| cell_list = cfg_values['enb']['cell_list'] |
| return len(cell_list) * self.num_ports() # *2 if MIMO |
| |
| def calc_required_zmq_ports_joined_earfcn(self, cfg_values): |
| #gr_broker will join the earfcns, so we need to count uniqe earfcns: |
| cell_list = cfg_values['enb']['cell_list'] |
| earfcn_li = [] |
| [earfcn_li.append(int(cell['dl_earfcn'])) for cell in cell_list if int(cell['dl_earfcn']) not in earfcn_li] |
| return len(earfcn_li) * self.num_ports() # *2 if MIMO |
| |
| |
| def assign_enb_zmq_ports(self, cfg_values, port_name, base_port): |
| port_offset = 0 |
| cell_list = cfg_values['enb']['cell_list'] |
| for cell in cell_list: |
| cell[port_name] = base_port + port_offset |
| port_offset += self.num_ports() |
| # TODO: do we need to assign cell_list back? |
| |
| def assign_enb_zmq_ports_joined_earfcn(self, cfg_values, port_name, base_port): |
| # TODO: Set in cell one bind port per unique earfcn, this is where UE will connect to when we use grbroker. |
| cell_list = cfg_values['enb']['cell_list'] |
| earfcn_li = [] |
| [earfcn_li.append(int(cell['dl_earfcn'])) for cell in cell_list if int(cell['dl_earfcn']) not in earfcn_li] |
| for cell in cell_list: |
| cell[port_name] = base_port + earfcn_li.index(int(cell['dl_earfcn'])) * self.num_ports() |
| |
| def configure(self, config_specifics_li): |
| values = dict(enb=config.get_defaults('enb')) |
| for config_specifics in config_specifics_li: |
| config.overlay(values, dict(enb=config.get_defaults(config_specifics))) |
| config.overlay(values, dict(enb=self.testenv.suite().config().get('enb', {}))) |
| for config_specifics in config_specifics_li: |
| config.overlay(values, dict(enb=self.testenv.suite().config().get(config_specifics, {}))) |
| config.overlay(values, dict(enb=self._conf)) |
| self._id = int(values['enb'].get('id', None)) |
| assert self._id is not None |
| self._duplex = values['enb'].get('duplex', None) |
| assert self._duplex |
| self._num_prb = int(values['enb'].get('num_prb', None)) |
| assert self._num_prb |
| self._txmode = int(values['enb'].get('transmission_mode', None)) |
| assert self._txmode |
| config.overlay(values, dict(enb={ 'num_ports': self.num_ports() })) |
| self._inactivity_timer = int(values['enb'].get('inactivity_timer', None)) |
| assert self._inactivity_timer |
| assert self._epc is not None |
| config.overlay(values, dict(enb={ 'addr': self.addr() })) |
| config.overlay(values, dict(enb={ 'mme_addr': self._epc.addr() })) |
| config.overlay(values, dict(enb={ 'gtp_bind_addr': self._gtp_bind_addr })) |
| self._num_cells = int(values['enb'].get('num_cells', None)) |
| assert self._num_cells |
| |
| # adjust cell_list to num_cells length: |
| len_cell_list = len(values['enb']['cell_list']) |
| if len_cell_list >= self._num_cells: |
| values['enb']['cell_list'] = values['enb']['cell_list'][:self._num_cells] |
| else: |
| raise log.Error('enb.cell_list items (%d) < enb.num_cells (%d) attribute!' % (len_cell_list, self._num_cells)) |
| # adjust scell list (to only contain values available in cell_list): |
| cell_id_list = [c['cell_id'] for c in values['enb']['cell_list']] |
| for i in range(len(values['enb']['cell_list'])): |
| scell_list_old = values['enb']['cell_list'][i]['scell_list'] |
| scell_list_new = [] |
| for scell_id in scell_list_old: |
| if scell_id in cell_id_list: |
| scell_list_new.append(scell_id) |
| values['enb']['cell_list'][i]['scell_list'] = scell_list_new |
| |
| # Assign ZMQ ports to each Cell/EARFCN. |
| if values['enb'].get('rf_dev_type') == 'zmq': |
| resourcep = self.testenv.suite().resource_pool() |
| num_ports = self.calc_required_zmq_ports(values) |
| num_ports_joined_earfcn = self.calc_required_zmq_ports_joined_earfcn(values) |
| ue_bind_port = self.ue.zmq_base_bind_port() |
| enb_bind_port = resourcep.next_zmq_port_range(self, num_ports) |
| self.assign_enb_zmq_ports(values, 'zmq_enb_bind_port', enb_bind_port) |
| # If we are to use a GrBroker, then initialize here to have remote zmq ports available: |
| self._use_gr_broker = self.using_grbroker(values) |
| if self._use_gr_broker: |
| zmq_enb_peer_port = resourcep.next_zmq_port_range(self, num_ports) |
| self.assign_enb_zmq_ports(values, 'zmq_enb_peer_port', zmq_enb_peer_port) # These are actually bound to GrBroker |
| self.assign_enb_zmq_ports_joined_earfcn(values, 'zmq_ue_bind_port', ue_bind_port) # This is were GrBroker binds on the UE side |
| zmq_ue_peer_port = resourcep.next_zmq_port_range(self, num_ports_joined_earfcn) |
| self.assign_enb_zmq_ports_joined_earfcn(values, 'zmq_ue_peer_port', zmq_ue_peer_port) # This is were GrBroker binds on the UE side |
| # Already set gen_conf here in advance since gr_broker needs the cell list |
| self.gen_conf = values |
| self.gr_broker.start() |
| else: |
| self.assign_enb_zmq_ports(values, 'zmq_enb_peer_port', ue_bind_port) |
| self.assign_enb_zmq_ports(values, 'zmq_ue_bind_port', ue_bind_port) #If no broker we need to match amount of ports |
| self.assign_enb_zmq_ports(values, 'zmq_ue_peer_port', enb_bind_port) |
| |
| return values |
| |
| def id(self): |
| return self._id |
| |
| def num_ports(self): |
| if self._txmode == 1: |
| return 1 |
| return 2 |
| |
| def num_cells(self): |
| return self._num_cells |
| |
| ######################## |
| # PUBLIC - INTERNAL API |
| ######################## |
| def cleanup(self): |
| 'Nothing to do by default. Subclass can override if required.' |
| if self.gr_broker: |
| self.gr_broker.unregister_enb(self) |
| GrBroker.unref() |
| self.gr_broker = None |
| |
| def num_prb(self): |
| return self._num_prb |
| |
| #reference: srsLTE.git srslte_symbol_sz() |
| def num_prb2symbol_sz(self, num_prb): |
| if num_prb == 6: |
| return 128 |
| if num_prb == 50: |
| return 768 |
| if num_prb == 75: |
| return 1024 |
| return 1536 |
| |
| raise log.Error('invalid num_prb %r', num_prb) |
| |
| def num_prb2base_srate(self, num_prb): |
| return self.num_prb2symbol_sz(num_prb) * 15 * 1000 |
| |
| def get_zmq_rf_dev_args(self, cfg_values): |
| base_srate = self.num_prb2base_srate(self.num_prb()) |
| |
| if self._use_gr_broker: |
| ul_rem_addr = self.gr_broker.addr() |
| else: |
| ul_rem_addr = self.ue.addr() |
| |
| rf_dev_args = 'fail_on_disconnect=true' |
| idx = 0 |
| cell_list = cfg_values['enb']['cell_list'] |
| # Define all 8 possible RF ports (2x CA with 2x2 MIMO) |
| for cell in cell_list: |
| rf_dev_args += ',tx_port%u=tcp://%s:%u' %(idx, self.addr(), cell['zmq_enb_bind_port'] + 0) |
| if self.num_ports() > 1: |
| rf_dev_args += ',tx_port%u=tcp://%s:%u' %(idx + 1, self.addr(), cell['zmq_enb_bind_port'] + 1) |
| rf_dev_args += ',rx_port%u=tcp://%s:%u' %(idx, ul_rem_addr, cell['zmq_enb_peer_port'] + 0) |
| if self.num_ports() > 1: |
| rf_dev_args += ',rx_port%u=tcp://%s:%u' %(idx + 1, ul_rem_addr, cell['zmq_enb_peer_port'] + 1) |
| idx += self.num_ports() |
| |
| rf_dev_args += ',id=enb,base_srate=' + str(base_srate) |
| return rf_dev_args |
| |
| def get_zmq_rf_dev_args_for_ue(self, ue): |
| cell_list = self.gen_conf['enb']['cell_list'] |
| rf_dev_args = '' |
| idx = 0 |
| earfcns_done = [] |
| for cell in cell_list: |
| if self._use_gr_broker: |
| if cell['dl_earfcn'] in earfcns_done: |
| continue |
| earfcns_done.append(cell['dl_earfcn']) |
| rf_dev_args += ',tx_port%u=tcp://%s:%u' %(idx, ue.addr(), cell['zmq_ue_bind_port'] + 0) |
| if self.num_ports() > 1: |
| rf_dev_args += ',tx_port%u=tcp://%s:%u' %(idx + 1, ue.addr(), cell['zmq_ue_bind_port'] + 1) |
| rf_dev_args += ',rx_port%u=tcp://%s:%u' %(idx, self.addr(), cell['zmq_ue_peer_port'] + 0) |
| if self.num_ports() > 1: |
| rf_dev_args += ',rx_port%u=tcp://%s:%u' %(idx + 1, self.addr(), cell['zmq_ue_peer_port'] + 1) |
| idx += self.num_ports() |
| # remove trailing comma: |
| if rf_dev_args[0] == ',': |
| return rf_dev_args[1:] |
| return rf_dev_args |
| |
| def get_instance_by_type(testenv, conf): |
| """Allocate a ENB child class based on type. Opts are passed to the newly created object.""" |
| enb_type = conf.get('type') |
| if enb_type is None: |
| raise RuntimeError('ENB type is not defined!') |
| |
| if enb_type == 'amarisoftenb': |
| from .enb_amarisoft import AmarisoftENB |
| enb_class = AmarisoftENB |
| elif enb_type == 'srsenb': |
| from .enb_srs import srsENB |
| enb_class = srsENB |
| else: |
| raise log.Error('ENB type not supported:', enb_type) |
| return enb_class(testenv, conf) |
| |
| ################### |
| # PUBLIC (test API included) |
| ################### |
| @abstractmethod |
| def start(self, epc): |
| 'Starts ENB, it will connect to "epc"' |
| pass |
| |
| @abstractmethod |
| def ue_add(self, ue): |
| pass |
| |
| @abstractmethod |
| def running(self): |
| pass |
| |
| @abstractmethod |
| def ue_max_rate(self, downlink=True, num_carriers=1): |
| pass |
| |
| @abstractmethod |
| def get_rfemu(self, cell=0, dl=True): |
| 'Get rfemu.RFemulation subclass implementation object for given cell index and direction.' |
| pass |
| |
| def addr(self): |
| return self._run_node.run_addr() |
| |
| # vim: expandtab tabstop=4 shiftwidth=4 |