amarisoft_enb: add NR support

this patch adds the ability to configure NR cells with
Amarisoft eNB. It adds the new DRB-NR template and updates
the normal enb.cfg template to allow using it as LTE only
or with NR cells (5G NSA).

Change-Id: Ia27bbc6db5920ce14bacabe8043601aa2adaa5fe
diff --git a/src/osmo_gsm_tester/obj/enb.py b/src/osmo_gsm_tester/obj/enb.py
index 252fa14..e976753 100644
--- a/src/osmo_gsm_tester/obj/enb.py
+++ b/src/osmo_gsm_tester/obj/enb.py
@@ -68,12 +68,18 @@
         'cell_list[].ncell_list[].pci': schema.UINT,
         'cell_list[].ncell_list[].dl_earfcn': schema.UINT,
         'cell_list[].scell_list[]': schema.UINT,
+        'cell_list[].nr_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,
+        'num_nr_cells': schema.UINT,
+        'nr_cell_list[].rf_port': schema.UINT,
+        'nr_cell_list[].cell_id': schema.UINT,
+        'nr_cell_list[].band': schema.UINT,
+        'nr_cell_list[].dl_nr_arfcn': schema.UINT,
         }
     for key, val in run_node.RunNode.schema().items():
         resource_schema['run_node.%s' % key] = val
@@ -98,9 +104,11 @@
             self.set_name('%s_%s' % (name, self._run_node.run_addr()))
         self._txmode = 0
         self._id = None
+        self._ran_config = "lte" # Used to determine whether we are in NSA
         self._duplex = None
         self._num_prb = 0
         self._num_cells = None
+        self._num_nr_cells = None
         self._epc = None
         self.gen_conf = None
         self.gr_broker = GrBroker.ref()
@@ -126,10 +134,11 @@
 
     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
+        nr_cell_list = cfg_values['enb']['nr_cell_list']
+        return len(cell_list) * self.num_ports() + len(nr_cell_list) # *2 if LTE MIMO
 
     def calc_required_zmq_ports_joined_earfcn(self, cfg_values):
-        #gr_broker will join the earfcns, so we need to count uniqe earfcns:
+        #gr_broker will join the earfcns, so we need to count unique earfcns (only implemented for LTE):
         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]
@@ -142,6 +151,10 @@
         for cell in cell_list:
             cell[port_name] = base_port + port_offset
             port_offset += self.num_ports()
+        nr_cell_list = cfg_values['enb']['nr_cell_list']
+        for nr_cell in nr_cell_list:
+            nr_cell[port_name] = base_port + port_offset
+            port_offset += 1
         # TODO: do we need to assign cell_list back?
 
     def assign_enb_zmq_ports_joined_earfcn(self, cfg_values, port_name, base_port):
@@ -176,7 +189,9 @@
         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
+        self._num_nr_cells = int(values['enb'].get('num_nr_cells', None))
+        assert self._num_cells is not None
+        assert self._num_nr_cells is not None
 
         # adjust cell_list to num_cells length:
         len_cell_list = len(values['enb']['cell_list'])
@@ -231,6 +246,9 @@
     def num_cells(self):
         return self._num_cells
 
+    def num_nr_cells(self):
+        return self._num_nr_cells
+
 ########################
 # PUBLIC - INTERNAL API
 ########################
@@ -280,6 +298,13 @@
                 rf_dev_args += ',rx_port%u=tcp://%s:%u' %(idx + 1, ul_rem_addr, cell['zmq_enb_peer_port'] + 1)
             idx += self.num_ports()
 
+        # Only single antenna supported for NR cells
+        nr_cell_list = cfg_values['enb']['nr_cell_list']
+        for nr_cell in nr_cell_list:
+            rf_dev_args += ',tx_port%u=tcp://%s:%u' % (idx, self.addr(), nr_cell['zmq_enb_bind_port'] + 0)
+            rf_dev_args += ',rx_port%u=tcp://%s:%u' % (idx, ul_rem_addr, nr_cell['zmq_enb_peer_port'] + 0)
+            idx += 1
+
         rf_dev_args += ',id=enb,base_srate=' + str(base_srate)
         return rf_dev_args
 
@@ -300,6 +325,14 @@
             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()
+
+        # NR cells again only with single antenna support
+        nr_cell_list = self.gen_conf['enb']['nr_cell_list']
+        for nr_cell in nr_cell_list:
+            rf_dev_args += ',tx_port%u=tcp://%s:%u' %(idx, ue.addr(), nr_cell['zmq_ue_bind_port'] + 0)
+            rf_dev_args += ',rx_port%u=tcp://%s:%u' %(idx, self.addr(), nr_cell['zmq_ue_peer_port'] + 0)
+            idx += 1
+
         # remove trailing comma:
         if rf_dev_args[0] == ',':
             return rf_dev_args[1:]
diff --git a/src/osmo_gsm_tester/obj/enb_amarisoft.py b/src/osmo_gsm_tester/obj/enb_amarisoft.py
index 405ed68..34ab5c1 100644
--- a/src/osmo_gsm_tester/obj/enb_amarisoft.py
+++ b/src/osmo_gsm_tester/obj/enb_amarisoft.py
@@ -22,6 +22,7 @@
 
 from ..core import log, util, config, template, process, remote
 from ..core import schema
+from ..core.event_loop import MainLoop
 from . import enb
 from . import rfemu
 
@@ -33,12 +34,16 @@
 
     config_schema = {
         'log_options': schema.STR,
+        'nr_bandwidth': schema.INT,
         }
     schema.register_config_schema('amarisoftenb', config_schema)
 
 def rf_type_valid(rf_type_str):
     return rf_type_str in ('uhd', 'zmq', 'sdr')
 
+def ran_type_valid(ran_type_str):
+    return ran_type_str in ('lte', '5g_nsa')
+
 class AmarisoftENB(enb.eNodeB):
 
     REMOTE_DIR = '/osmo-gsm-tester-amarisoftenb'
@@ -48,6 +53,7 @@
     CFGFILE_SIB23 = 'amarisoft_sib23.asn'
     CFGFILE_RF = 'amarisoft_rf_driver.cfg'
     CFGFILE_DRB = 'amarisoft_drb.cfg'
+    CFGFILE_DRB_NR = 'amarisoft_drb_nr.cfg'
     LOGFILE = 'lteenb.log'
     PHY_SIGNAL_FILE = 'lteenb.log.bin'
 
@@ -63,6 +69,7 @@
         self.config_sib23_file = None
         self.config_rf_file = None
         self.config_drb_file = None
+        self.config_drb_nr_file = None
         self.log_file = None
         self.process = None
         self.rem_host = None
@@ -72,8 +79,11 @@
         self.remote_config_sib23_file = None
         self.remote_config_rf_file = None
         self.remote_config_drb_file = None
+        self.remote_config_drb_nr_file = None
         self.remote_log_file = None
         self.enable_measurements = False
+        self.nr_bandwidth = None
+        self.ran_type = None
         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))
@@ -131,8 +141,8 @@
         self.process.launch()
 
     def stop(self):
-        # Not implemented
-        pass
+        # Allow for some time to flush logs
+        MainLoop.sleep(5)
 
     def gen_conf_file(self, path, filename, values):
         self.dbg('AmarisoftENB ' + filename + ':\n' + pprint.pformat(values))
@@ -151,6 +161,7 @@
         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.config_drb_nr_file = self.run_dir.child(AmarisoftENB.CFGFILE_DRB_NR)
         self.log_file = self.run_dir.child(AmarisoftENB.LOGFILE)
         self.phy_signal_file = self.run_dir.child(AmarisoftENB.PHY_SIGNAL_FILE)
 
@@ -165,6 +176,7 @@
             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_config_drb_nr_file = remote_run_dir.child(AmarisoftENB.CFGFILE_DRB_NR)
             self.remote_log_file = remote_run_dir.child(AmarisoftENB.LOGFILE)
             self.remote_phy_signal_file = remote_run_dir.child(AmarisoftENB.PHY_SIGNAL_FILE)
 
@@ -176,6 +188,17 @@
 
         config.overlay(values, dict(enb={'enable_dl_awgn': util.str2bool(values['enb'].get('enable_dl_awgn', 'false'))}))
 
+        self.nr_bandwidth = int(values['enb'].get('nr_bandwidth', 10))
+        config.overlay(values, dict(enb={'nr_bandwidth': self.nr_bandwidth}))
+
+        if (self._num_cells > 0):
+            if (self._num_nr_cells <= 0):
+                self.ran_type = "lte"
+            else:
+                self.ran_type = "nsa"
+        else:
+            raise log.Error('5G SA not supported yet')
+
         # Remove EEA0 from cipher list, if specified, as it's always assumed as default
         cipher_list = values['enb'].get('cipher_list', None)
         if "eea0" in cipher_list: cipher_list.remove("eea0")
@@ -237,6 +260,7 @@
         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)
+        self.gen_conf_file(self.config_drb_nr_file, AmarisoftENB.CFGFILE_DRB_NR, values)
 
         if not self._run_node.is_local():
             self.rem_host.recreate_remote_dir(self.remote_inst)
@@ -247,6 +271,7 @@
             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)
+            self.rem_host.scp('scp-cfg-drb-nr-to-remote', self.config_drb_nr_file, self.remote_config_drb_nr_file)
 
     def ue_add(self, ue):
         if self.ue is not None:
@@ -279,11 +304,17 @@
         rfemu_obj = rfemu.get_instance_by_type(rfemu_cfg['type'], rfemu_cfg)
         return rfemu_obj
 
+    def get_nr_bandwidth(self):
+        return self.nr_bandwidth
+
     def ue_max_rate(self, downlink=True, num_carriers=1):
-        if self._duplex == 'fdd':
-            return self.ue_max_rate_fdd(downlink, num_carriers)
+        if self.ran_type == 'lte':
+            if self._duplex == 'fdd':
+                return self.ue_max_rate_fdd(downlink, num_carriers)
+            else:
+                return self.ue_max_rate_tdd(downlink, num_carriers)
         else:
-            return self.ue_max_rate_tdd(downlink, num_carriers)
+            return self.ue_max_rate_nsa_tdd(downlink)
 
     def ue_max_rate_fdd(self, downlink, num_carriers):
         # The max rate for a single UE per PRB configuration in TM1 with MCS 28 QAM64
@@ -323,7 +354,7 @@
         return max_rate
 
     def ue_max_rate_tdd(self, downlink, num_carriers):
-        # Max rate calculation for TDD depends on the acutal TDD configuration
+        # Max rate calculation for TDD depends on the actual TDD configuration
         # See: https://www.sharetechnote.com/html/Handbook_LTE_ThroughputCalculationExample_TDD.html
         # and https://i0.wp.com/www.techtrained.com/wp-content/uploads/2017/09/Blog_Post_1_TDD_Max_Throughput_Theoretical.jpg
         max_phy_rate_tdd_uldl_config0_sp0 = { 6 : 1.5e6,
@@ -333,8 +364,21 @@
                                75 : 18.4e6,
                                100 : 54.5e6 }
         if downlink:
-            max_rate = max_phy_rate_tdd_uldl_config0_sp0[self.num_prb()]
+            return max_phy_rate_tdd_uldl_config0_sp0[self.num_prb()]
         else:
             return 1e6 # dummy value, we need to replace that later
 
+    def ue_max_rate_nsa_tdd(self, downlink):
+        # Max rate calculation based on https://5g-tools.com/5g-nr-throughput-calculator/
+        # Only FR1 15kHz SCS, QAM64, 6 DL slots, 3 UL slots
+        max_phy_rate_nsa_dl_fr1_15khz = { 10: 18.4e6,
+                                          20: 38.0e6 }
+        max_phy_rate_nsa_ul_fr1_15khz = { 10: 10.7e6,
+                                          20: 23.0e6 }
+
+        if downlink:
+            return max_phy_rate_nsa_dl_fr1_15khz[self.get_nr_bandwidth()]
+        else:
+            return max_phy_rate_nsa_ul_fr1_15khz[self.get_nr_bandwidth()]
+
 # vim: expandtab tabstop=4 shiftwidth=4