diff --git a/ci-scripts/Jenkinsfile-physim-deploy b/ci-scripts/Jenkinsfile-physim-deploy
index 2d2cf054b5e589f26bbcaba1a61641747338ef72..eac572e6cdcb12bc06af68e7905d8b97218445af 100644
--- a/ci-scripts/Jenkinsfile-physim-deploy
+++ b/ci-scripts/Jenkinsfile-physim-deploy
@@ -221,9 +221,9 @@ pipeline {
               archiveArtifacts artifacts: "physim_deploytest_logs_${env.BUILD_ID}.zip"
             }
             if (fileExists("test_results.html")) {
-              sh "mv test_results.html test_results_${env.JOB_NAME}.html"
-              sh "sed -i -e 's#TEMPLATE_JOB_NAME#${JOB_NAME}#' -e 's@build #TEMPLATE_BUILD_ID@build #${BUILD_ID}@' -e 's#Build-ID: TEMPLATE_BUILD_ID#Build-ID: <a href=\"${BUILD_URL}\">${BUILD_ID}</a>#' -e 's#TEMPLATE_STAGE_NAME#${testStageName}#' test_results_${JOB_NAME}.html"
-              archiveArtifacts artifacts: "test_results_${env.JOB_NAME}.html"
+              sh "mv test_results.html test_results-${env.JOB_NAME}.html"
+              sh "sed -i -e 's#TEMPLATE_JOB_NAME#${JOB_NAME}#' -e 's@build #TEMPLATE_BUILD_ID@build #${BUILD_ID}@' -e 's#Build-ID: TEMPLATE_BUILD_ID#Build-ID: <a href=\"${BUILD_URL}\">${BUILD_ID}</a>#' -e 's#TEMPLATE_STAGE_NAME#${testStageName}#' test_results-${JOB_NAME}.html"
+              archiveArtifacts artifacts: "test_results-${env.JOB_NAME}.html"
             }
           }
         }
diff --git a/ci-scripts/Jenkinsfile-trig-nsa b/ci-scripts/Jenkinsfile-trig-nsa
index 3dd5ce3b50ccd74719a9e8ab4c968b53263360e3..5ace29d86673dc44db3789fb76c8c6781ff43a85 100644
--- a/ci-scripts/Jenkinsfile-trig-nsa
+++ b/ci-scripts/Jenkinsfile-trig-nsa
@@ -37,7 +37,7 @@ pipeline {
             steps {
                 script {     
                     //retrieve MR that are opened nd with tag READY_TO_BE_MERGED             
-                    MR_LIST= sh returnStdout: true, script: 'curl --silent "https://gitlab.eurecom.fr/api/v4/projects/oai%2Fopenairinterface5g/merge_requests?state=opened&per_page=100&labels=READY_TO_BE_MERGED" | jq ".[].iid" || true '
+                    MR_LIST= sh returnStdout: true, script: 'curl --silent "https://gitlab.eurecom.fr/api/v4/projects/oai%2Fopenairinterface5g/merge_requests?state=opened&per_page=100&milestone=REVIEW_COMPLETED_AND_APPROVED" | jq ".[].iid" || true '
                     echo "List of selected MR:\n${MR_LIST}" 
                     def MR_ARRAY = MR_LIST.split('\n') 
                     //for every selected MR, retrieve the branch name and the latest commit              
@@ -48,7 +48,7 @@ pipeline {
                         COMMIT_ID=COMMIT_ID.trim()
                         echo "Testing NSA on : ${MR} ${SRC_BRANCH} ${COMMIT_ID}"
                         //calling NSA sub job
-                        build job: "RAN-CI-NSA-B210", wait : false, propagate : false, parameters: [
+                        build job: "RAN-CI-NSA-B210-N310-ModuleUE", wait : false, propagate : false, parameters: [
                             string(name: 'eNB_MR', value: String.valueOf(MR)),                          
                             string(name: 'eNB_Branch', value: String.valueOf(SRC_BRANCH)),
                             string(name: 'eNB_CommitID', value: String.valueOf(COMMIT_ID)),
diff --git a/ci-scripts/build_fr1_template.yaml b/ci-scripts/build_fr1_template.yaml
index 2a8f9cc6a02a3e7a798f90c6d0a9f02c292fbf7f..02e49bc4b59b303ee3a1cd36136a34d3796ce0ad 100755
--- a/ci-scripts/build_fr1_template.yaml
+++ b/ci-scripts/build_fr1_template.yaml
@@ -1,15 +1,15 @@
 
 ranRepository : https://gitlab.eurecom.fr/oai/openairinterface5g.git
-ranBranch : BRANCH_NAME 
-ranCommitID : COMMIT_ID 
-ranAllowMerge : 'true' 
+ranBranch : integration_2021_wk13_a  
+ranCommitID : 104aa7eed5d6702c1b9da663414079ef698da206     
+ranAllowMerge : 'yes' 
 ranTargetBranch : develop
 
 steps:
   - InitiateHtml,none
   - TesteNB,xml_files/fr1_multi_node_build.xml
   - TesteNB,xml_files/fr1_epc_start.xml
-  - TesteNB,xml_files/fr1_ran_ue_base.xml #ue toggle, nodes initialize, ue toggle, ping, nodes terminate
+  - TesteNB,xml_files/fr1_nsa_base_next.xml #ue toggle, nodes initialize, ue toggle, ping, nodes terminate
   - TesteNB,xml_files/fr1_epc_closure.xml
 
 
diff --git a/ci-scripts/ci_ctl_qtel.py b/ci-scripts/ci_ctl_qtel.py
new file mode 100644
index 0000000000000000000000000000000000000000..3785d7ee89a6b66fe884d957087680d4b26c6730
--- /dev/null
+++ b/ci-scripts/ci_ctl_qtel.py
@@ -0,0 +1,86 @@
+# * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
+# * contributor license agreements.  See the NOTICE file distributed with
+# * this work for additional information regarding copyright ownership.
+# * The OpenAirInterface Software Alliance licenses this file to You under
+# * the OAI Public License, Version 1.1  (the "License"); you may not use this file
+# * except in compliance with the License.
+# * You may obtain a copy of the License at
+# *
+# *      http://www.openairinterface.org/?page_id=698
+# *
+# * Unless required by applicable law or agreed to in writing, software
+# * distributed under the License is distributed on an "AS IS" BASIS,
+# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# * See the License for the specific language governing permissions and
+# * limitations under the License.
+# *-------------------------------------------------------------------------------
+# * For more information about the OpenAirInterface (OAI) Software Alliance:
+# *      contact@openairinterface.org
+# */
+#---------------------------------------------------------------------
+#
+#   Required Python Version
+#     Python 3.x
+#
+#---------------------------------------------------------------------
+
+#usage example:
+#sudo python3 ci_ctl_qtel.py /dev/ttyUSB2 wup
+#sudo python3 ci_ctl_qtel.py /dev/ttyUSB2 detach
+
+
+import sys
+import time
+import serial
+
+
+class qtel_ctl:
+	#---------------
+	#private methods
+	#---------------
+    def __init__(self, usb_port_at):
+        self.QUECTEL_USB_PORT_AT = usb_port_at #ex : '/dev/ttyUSB2'
+        self.modem = serial.Serial(self.QUECTEL_USB_PORT_AT, timeout=1)
+        self.cmd_dict= {"wup": self.wup,"detach":self.detach}#dictionary of function pointers
+
+    def __set_modem_state(self,ser,state):
+	    self.__send_command(ser,"AT+CFUN={}\r".format(state))
+
+    def __send_command(self,ser,com):
+        ser.write(com.encode())
+        time.sleep(0.1)
+        ret=[]
+        while ser.inWaiting()>0:
+            print("waiting")
+            msg=ser.readline()
+            msg=msg.decode("utf-8")
+            msg=msg.replace("\r","")
+            msg=msg.replace("\n","")
+            print(msg)
+            if msg!="":
+                ret.append(msg)
+            else:
+                print("msg empty")
+        return ret
+
+	#--------------
+	#public methods
+	#--------------
+    def wup(self):#sending AT+CFUN=0, then AT+CFUN=1
+        self.__set_modem_state(self.modem,'0')
+        time.sleep(3)
+        self.__set_modem_state(self.modem,'1')
+
+    def detach(self):#sending AT+CFUN=0
+        self.__set_modem_state(self.modem,'0')
+
+
+
+
+if __name__ == "__main__":
+    #argv[1] : usb port
+    #argv[2] : qtel command (see function pointers dict "wup", "detach" etc ...)
+    command = sys.argv[2]
+    Module=qtel_ctl(sys.argv[1])
+    #calling the function to be applied
+    Module.cmd_dict[command]()
diff --git a/ci-scripts/ci_ueinfra.yaml b/ci-scripts/ci_ueinfra.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..1b3989138267b0f5e2e6d72fd27f7acdeb0bdc0d
--- /dev/null
+++ b/ci-scripts/ci_ueinfra.yaml
@@ -0,0 +1,32 @@
+idefix:
+  ID: idefix
+  State : enabled
+  Kind : quectel
+  Process : 
+    Name : quectel-CM
+    Cmd : /home/oaicicd/quectel-CM/quectel-CM -s oai.ipv4 -4
+  WakeupScript : ci_ctl_qtel.py /dev/ttyUSB2 wup
+  DetachScript : ci_ctl_qtel.py /dev/ttyUSB2 detach
+  PLMN : 22201
+  UENetwork : wwan0
+  HostIPAddress : 192.168.18.188
+  HostUsername : oaicicd
+  HostPassword : oaicicd
+  HostSourceCodePath : none
+dummy:
+  ID: ''
+  State : ''
+  Kind : ''
+  Process : 
+    Name : ''
+    Cmd : ''
+  WakeupScript : ''
+  DetachScript : ''
+  PLMN : 22201
+  UENetwork : wwan0
+  HostIPAddress : 192.168.18.188
+  HostUsername : oaicicd
+  HostPassword : oaicicd
+  HostSourceCodePath : none
+
+
diff --git a/ci-scripts/cls_ci_ueinfra.py b/ci-scripts/cls_ci_ueinfra.py
new file mode 100644
index 0000000000000000000000000000000000000000..0326a92a4f4ca9434093b87b633352d4477dd857
--- /dev/null
+++ b/ci-scripts/cls_ci_ueinfra.py
@@ -0,0 +1,57 @@
+# * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
+# * contributor license agreements.  See the NOTICE file distributed with
+# * this work for additional information regarding copyright ownership.
+# * The OpenAirInterface Software Alliance licenses this file to You under
+# * the OAI Public License, Version 1.1  (the "License"); you may not use this file
+# * except in compliance with the License.
+# * You may obtain a copy of the License at
+# *
+# *      http://www.openairinterface.org/?page_id=698
+# *
+# * Unless required by applicable law or agreed to in writing, software
+# * distributed under the License is distributed on an "AS IS" BASIS,
+# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# * See the License for the specific language governing permissions and
+# * limitations under the License.
+# *-------------------------------------------------------------------------------
+# * For more information about the OpenAirInterface (OAI) Software Alliance:
+# *      contact@openairinterface.org
+# */
+#---------------------------------------------------------------------
+#
+#   Required Python Version
+#     Python 3.x
+#
+#---------------------------------------------------------------------
+
+#to use isfile
+import os
+import sys
+import logging
+#to create a SSH object locally in the methods
+import sshconnection
+#time.sleep
+import time
+#to load ue infrastructure dictionary
+import yaml
+
+class InfraUE:
+	def __init__(self):
+		self.ci_ue_infra ={}
+
+
+#-----------------$
+#PUBLIC Methods$
+#-----------------$
+
+	#This method reads the yaml file describing the multi-UE infrastructure
+	#and stores the infra permanently in the related class attribute self.ci_ue_infra
+	def Get_UE_Infra(self,ue_infra_filename):
+		f_yaml=ue_infra_filename
+		with open(f_yaml,'r') as file:
+			logging.debug('Loading UE infrastructure from file '+f_yaml)
+			#load it permanently in the class attribute
+			self.ci_ue_infra = yaml.load(file,Loader=yaml.FullLoader)
+
+
+
diff --git a/ci-scripts/cls_module_ue.py b/ci-scripts/cls_module_ue.py
new file mode 100644
index 0000000000000000000000000000000000000000..a94af5bcda7f45b025fdaf3670937682c642a9fa
--- /dev/null
+++ b/ci-scripts/cls_module_ue.py
@@ -0,0 +1,132 @@
+# * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
+# * contributor license agreements.  See the NOTICE file distributed with
+# * this work for additional information regarding copyright ownership.
+# * The OpenAirInterface Software Alliance licenses this file to You under
+# * the OAI Public License, Version 1.1  (the "License"); you may not use this file
+# * except in compliance with the License.
+# * You may obtain a copy of the License at
+# *
+# *      http://www.openairinterface.org/?page_id=698
+# *
+# * Unless required by applicable law or agreed to in writing, software
+# * distributed under the License is distributed on an "AS IS" BASIS,
+# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# * See the License for the specific language governing permissions and
+# * limitations under the License.
+# *-------------------------------------------------------------------------------
+# * For more information about the OpenAirInterface (OAI) Software Alliance:
+# *      contact@openairinterface.org
+# */
+#---------------------------------------------------------------------
+#
+#   Required Python Version
+#     Python 3.x
+#
+#---------------------------------------------------------------------
+
+#to use isfile
+import os
+import sys
+import logging
+#to create a SSH object locally in the methods
+import sshconnection
+#time.sleep
+import time
+
+
+import re
+import subprocess
+
+
+
+class Module_UE:
+
+	def __init__(self,Module):
+		#create attributes as in the Module dictionary
+		for k, v in Module.items():
+			setattr(self, k, v)
+		self.UEIPAddress = ""
+		#dictionary linking command names and related module scripts
+		self.cmd_dict= {"wup": self.WakeupScript,"detach":self.DetachScript}#dictionary of function scripts		
+
+
+
+#-----------------$
+#PUBLIC Methods$
+#-----------------$
+
+	#this method checks if the specified Process is running on the server hosting the module
+	#if not it will be started
+	def CheckCMProcess(self):
+		HOST=self.HostIPAddress
+		COMMAND="ps aux | grep " + self.Process['Name'] + " | grep -v grep "
+		logging.debug(COMMAND)
+		ssh = subprocess.Popen(["ssh", "%s" % HOST, COMMAND],shell=False,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
+		result = ssh.stdout.readlines()
+		if len(result)!=0:
+			logging.debug(self.Process['Name'] + " process found")
+			return True 
+		else:#start process and check again  
+			logging.debug(self.Process['Name'] + " process NOT found")
+			#starting the process
+			logging.debug('Starting ' + self.Process['Name'])
+			mySSH = sshconnection.SSHConnection()
+			mySSH.open(self.HostIPAddress, self.HostUsername, self.HostPassword)
+			mySSH.command('echo ' + self.HostPassword + ' | sudo -S ' + self.Process['Cmd'] + ' &','\$',5)
+			mySSH.close()
+			#checking the process
+			HOST=self.HostIPAddress
+			COMMAND="ps aux | grep " + self.Process['Name'] + " | grep -v grep "
+			logging.debug(COMMAND)
+			ssh = subprocess.Popen(["ssh", "%s" % HOST, COMMAND],shell=False,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
+			result = ssh.stdout.readlines()
+			if len(result)!=0:
+				logging.debug(self.Process['Name'] + " process found")
+				return True
+			else:
+				logging.debug(self.Process['Name'] + " process NOT found")
+				return False 
+
+	#Generic command function, using function pointers dictionary
+	def Command(self,cmd):
+		mySSH = sshconnection.SSHConnection()
+		mySSH.open(self.HostIPAddress, self.HostUsername, self.HostPassword)
+		mySSH.command('echo ' + self.HostPassword + ' | sudo -S python3 ' + self.cmd_dict[cmd],'\$',5)
+		time.sleep(5)
+		logging.debug("Module "+ cmd)
+		mySSH.close()
+
+
+	#this method retrieves the Module IP address (not the Host IP address) 
+	def GetModuleIPAddress(self):
+		HOST=self.HostIPAddress
+		response= []
+		tentative = 10
+		while (len(response)==0) and (tentative>0):
+			COMMAND="ip a show dev " + self.UENetwork + " | grep inet | grep " + self.UENetwork
+			logging.debug(COMMAND)
+			ssh = subprocess.Popen(["ssh", "%s" % HOST, COMMAND],shell=False,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
+			response = ssh.stdout.readlines()
+			tentative-=1
+			time.sleep(10)
+		if (tentative==0) and (len(response)==0):
+			logging.debug('\u001B[1;37;41m Module IP Address Not Found! Time expired \u001B[0m')
+			return -1
+		else: #check response
+			result = re.search('inet (?P<moduleipaddress>[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)', response[0].decode("utf-8") )
+			if result is not None: 
+				if result.group('moduleipaddress') is not None: 
+					self.UEIPAddress = result.group('moduleipaddress')
+					logging.debug('\u001B[1mUE Module IP Address is ' + self.UEIPAddress + '\u001B[0m')
+					return 0
+				else:
+					logging.debug('\u001B[1;37;41m Module IP Address Not Found! \u001B[0m')
+					return -1
+			else:
+				logging.debug('\u001B[1;37;41m Module IP Address Not Found! \u001B[0m')
+				return -1
+
+
+
+
+
diff --git a/ci-scripts/cls_oaicitest.py b/ci-scripts/cls_oaicitest.py
index 7da829b7f7dc401fb423875b1cfd6f0af08fced0..7b75cf82c58c3d6b82a9a156d3e6bfb25f04d5fc 100644
--- a/ci-scripts/cls_oaicitest.py
+++ b/ci-scripts/cls_oaicitest.py
@@ -54,7 +54,8 @@ import helpreadme as HELP
 import constants as CONST
 import sshconnection
 
-
+import cls_module_ue
+import cls_ci_ueinfra		#class defining the multi Ue infrastrucure
 
 
 #-----------------------------------------------------------
@@ -129,6 +130,7 @@ class OaiCiTest():
 		self.iperf_packetloss_threshold = ''
 		self.iperf_profile = ''
 		self.iperf_options = ''
+		self.iperf_direction = ''
 		self.nbMaxUEtoAttach = -1
 		self.UEDevices = []
 		self.UEDevicesStatus = []
@@ -157,6 +159,7 @@ class OaiCiTest():
 		self.clean_repository = True
 		self.air_interface=''
 		self.expectedNbOfConnectedUEs = 0
+		self.ue_id = '' #used for module identification
 
 
 	def BuildOAIUE(self,HTML):
@@ -364,29 +367,45 @@ class OaiCiTest():
 		except:
 			os.kill(os.getppid(),signal.SIGUSR1)
 
-	def InitializeUE(self,HTML,COTS_UE):
-		if self.ADBIPAddress == '' or self.ADBUserName == '' or self.ADBPassword == '':
-			HELP.GenericHelp(CONST.Version)
-			sys.exit('Insufficient Parameter')
-		multi_jobs = []
-		i = 0
-		for device_id in self.UEDevices:
-			p = Process(target = self.InitializeUE_common, args = (device_id,i,COTS_UE,))
-			p.daemon = True
-			p.start()
-			multi_jobs.append(p)
-			i += 1
-		for job in multi_jobs:
-			job.join()
-		HTML.CreateHtmlTestRow('N/A', 'OK', CONST.ALL_PROCESSES_OK)
-
-
+	def InitializeUE(self,HTML,RAN,EPC, COTS_UE, InfraUE):
+		if self.ue_id=='':#no ID specified, then it is a COTS controlled by ADB
+			if self.ADBIPAddress == '' or self.ADBUserName == '' or self.ADBPassword == '':
+				HELP.GenericHelp(CONST.Version)
+				sys.exit('Insufficient Parameter')
+			multi_jobs = []
+			i = 0
+			for device_id in self.UEDevices:
+				p = Process(target = self.InitializeUE_common, args = (device_id,i,COTS_UE,))
+				p.daemon = True
+				p.start()
+				multi_jobs.append(p)
+				i += 1
+			for job in multi_jobs:
+				job.join()
+			HTML.CreateHtmlTestRow('N/A', 'OK', CONST.ALL_PROCESSES_OK)
+		else: #if an ID is specified, it is a module from the yaml infrastructure file
+			#RH
+			Module_UE = cls_module_ue.Module_UE(InfraUE.ci_ue_infra[self.ue_id])
+			is_module=Module_UE.CheckCMProcess()
+			if is_module:
+				Module_UE.Command("wup")
+				status=Module_UE.GetModuleIPAddress()
+				if status==0:
+					HTML.CreateHtmlTestRow(Module_UE.UEIPAddress, 'OK', CONST.ALL_PROCESSES_OK)	
+					self.UEIPAddresses.append(Module_UE.UEIPAddress)
+					logging.debug('UE IP addresss : '+ Module_UE.UEIPAddress)
+				else: #status==-1 failed to retrieve IP address
+					HTML.CreateHtmlTestRow('N/A', 'KO', CONST.UE_IP_ADDRESS_ISSUE)
+					self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC)
+					return
 
 
 	def InitializeOAIUE(self,HTML,RAN,EPC,COTS_UE):
 		if self.UEIPAddress == '' or self.UEUserName == '' or self.UEPassword == '' or self.UESourceCodePath == '':
 			HELP.GenericHelp(CONST.Version)
 			sys.exit('Insufficient Parameter')
+
+			
 		if self.air_interface == 'lte-uesoftmodem':
 			result = re.search('--no-L2-connect', str(self.Initialize_OAI_UE_args))
 			if result is None:
@@ -945,30 +964,31 @@ class OaiCiTest():
 			os.kill(os.getppid(),signal.SIGUSR1)
 
 	def AttachUE(self,HTML,RAN,EPC,COTS_UE):
-		if self.ADBIPAddress == '' or self.ADBUserName == '' or self.ADBPassword == '':
-			HELP.GenericHelp(CONST.Version)
-			sys.exit('Insufficient Parameter')
-		check_eNB = True
-		check_OAI_UE = False
-		pStatus = self.CheckProcessExist(check_eNB, check_OAI_UE,RAN,EPC)
-		if (pStatus < 0):
-			HTML.CreateHtmlTestRow('N/A', 'KO', pStatus)
-			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC)
-			return
-		multi_jobs = []
-		status_queue = SimpleQueue()
-		lock = Lock()
-		nb_ue_to_connect = 0
-		for device_id in self.UEDevices:
-			if (self.nbMaxUEtoAttach == -1) or (nb_ue_to_connect < self.nbMaxUEtoAttach):
-				self.UEDevicesStatus[nb_ue_to_connect] = CONST.UE_STATUS_ATTACHING
-				p = Process(target = self.AttachUE_common, args = (device_id, status_queue, lock,nb_ue_to_connect,COTS_UE,))
-				p.daemon = True
-				p.start()
-				multi_jobs.append(p)
-			nb_ue_to_connect = nb_ue_to_connect + 1
-		for job in multi_jobs:
-			job.join()
+		if self.ue_id=='':#no ID specified, then it is a COTS controlled by ADB
+			if self.ADBIPAddress == '' or self.ADBUserName == '' or self.ADBPassword == '':
+				HELP.GenericHelp(CONST.Version)
+				sys.exit('Insufficient Parameter')
+			check_eNB = True
+			check_OAI_UE = False
+			pStatus = self.CheckProcessExist(check_eNB, check_OAI_UE,RAN,EPC)
+			if (pStatus < 0):
+				HTML.CreateHtmlTestRow('N/A', 'KO', pStatus)
+				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC)
+				return
+			multi_jobs = []
+			status_queue = SimpleQueue()
+			lock = Lock()
+			nb_ue_to_connect = 0
+			for device_id in self.UEDevices:
+				if (self.nbMaxUEtoAttach == -1) or (nb_ue_to_connect < self.nbMaxUEtoAttach):
+					self.UEDevicesStatus[nb_ue_to_connect] = CONST.UE_STATUS_ATTACHING
+					p = Process(target = self.AttachUE_common, args = (device_id, status_queue, lock,nb_ue_to_connect,COTS_UE,))
+					p.daemon = True
+					p.start()
+					multi_jobs.append(p)
+				nb_ue_to_connect = nb_ue_to_connect + 1
+			for job in multi_jobs:
+				job.join()
 
 		if (status_queue.empty()):
 			HTML.CreateHtmlTestRow('N/A', 'KO', CONST.ALL_PROCESSES_OK)
@@ -1025,37 +1045,44 @@ class OaiCiTest():
 		except:
 			os.kill(os.getppid(),signal.SIGUSR1)
 
-	def DetachUE(self,HTML,RAN,EPC,COTS_UE):
-		if self.ADBIPAddress == '' or self.ADBUserName == '' or self.ADBPassword == '':
-			HELP.GenericHelp(CONST.Version)
-			sys.exit('Insufficient Parameter')
-		check_eNB = True
-		check_OAI_UE = False
-		pStatus = self.CheckProcessExist(check_eNB, check_OAI_UE,RAN,EPC)
-		if (pStatus < 0):
-			HTML.CreateHtmlTestRow('N/A', 'KO', pStatus)
-			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC)
-			return
-		multi_jobs = []
-		cnt = 0
-		for device_id in self.UEDevices:
-			self.UEDevicesStatus[cnt] = CONST.UE_STATUS_DETACHING
-			p = Process(target = self.DetachUE_common, args = (device_id,cnt,COTS_UE,))
-			p.daemon = True
-			p.start()
-			multi_jobs.append(p)
-			cnt += 1
-		for job in multi_jobs:
-			job.join()
-		HTML.CreateHtmlTestRow('N/A', 'OK', CONST.ALL_PROCESSES_OK)
-		result = re.search('T_stdout', str(RAN.Initialize_eNB_args))
-		if result is not None:
-			logging.debug('Waiting 5 seconds to fill up record file')
-			time.sleep(5)
-		cnt = 0
-		while cnt < len(self.UEDevices):
-			self.UEDevicesStatus[cnt] = CONST.UE_STATUS_DETACHED
-			cnt += 1
+	def DetachUE(self,HTML,RAN,EPC,COTS_UE,InfraUE):
+		if self.ue_id=='':#no ID specified, then it is a COTS controlled by ADB
+			if self.ADBIPAddress == '' or self.ADBUserName == '' or self.ADBPassword == '':
+				HELP.GenericHelp(CONST.Version)
+				sys.exit('Insufficient Parameter')
+			check_eNB = True
+			check_OAI_UE = False
+			pStatus = self.CheckProcessExist(check_eNB, check_OAI_UE,RAN,EPC)
+			if (pStatus < 0):
+				HTML.CreateHtmlTestRow('N/A', 'KO', pStatus)
+				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC)
+				return
+			multi_jobs = []
+			cnt = 0
+			for device_id in self.UEDevices:
+				self.UEDevicesStatus[cnt] = CONST.UE_STATUS_DETACHING
+				p = Process(target = self.DetachUE_common, args = (device_id,cnt,COTS_UE,))
+				p.daemon = True
+				p.start()
+				multi_jobs.append(p)
+				cnt += 1
+			for job in multi_jobs:
+				job.join()
+			HTML.CreateHtmlTestRow('N/A', 'OK', CONST.ALL_PROCESSES_OK)
+			result = re.search('T_stdout', str(RAN.Initialize_eNB_args))
+			if result is not None:
+				logging.debug('Waiting 5 seconds to fill up record file')
+				time.sleep(5)
+			cnt = 0
+			while cnt < len(self.UEDevices):
+				self.UEDevicesStatus[cnt] = CONST.UE_STATUS_DETACHED
+				cnt += 1
+		else:#if an ID is specified, it is a module from the yaml infrastructure file
+			Module_UE = cls_module_ue.Module_UE(InfraUE.ci_ue_infra[self.ue_id])
+			Module_UE.Command("detach")
+			HTML.CreateHtmlTestRow('N/A', 'OK', CONST.ALL_PROCESSES_OK)	
+				
+							
 
 	def RebootUE_common(self, device_id):
 		try:
@@ -1437,14 +1464,20 @@ class OaiCiTest():
 		statusQueue.put(message)
 		lock.release()
 
-	def Ping_common(self, lock, UE_IPAddress, device_id, statusQueue,EPC):
+	def Ping_common(self, lock, UE_IPAddress, device_id, statusQueue,EPC, Module_UE):
 		try:
 			SSH = sshconnection.SSHConnection()
 			# Launch ping on the EPC side (true for ltebox and old open-air-cn)
 			# But for OAI-Rel14-CUPS, we launch from python executor
 			launchFromEpc = True
+			launchFromModule = False
 			if re.match('OAI-Rel14-CUPS', EPC.Type, re.IGNORECASE):
 				launchFromEpc = False
+			#if module, ping from module to EPC
+			if self.ue_id!='':
+				launchFromEpc = False
+				launchfromModule = True
+
 			ping_time = re.findall("-c (\d+)",str(self.ping_args))
 
 			if launchFromEpc:
@@ -1462,21 +1495,33 @@ class OaiCiTest():
 				#copy the ping log file to have it locally for analysis (ping stats)
 				SSH.copyin(EPC.IPAddress, EPC.UserName, EPC.Password, EPC.SourceCodePath + '/scripts/ping_' + self.testCase_id + '_' + device_id + '.log', '.')				
 			else:
-				#ping log file is on the python executor
-				cmd = 'ping ' + self.ping_args + ' ' + UE_IPAddress + ' 2>&1 > ping_' + self.testCase_id + '_' + device_id + '.log' 
-				message = cmd + '\n'
-				logging.debug(cmd)
-				ret = subprocess.run(cmd, shell=True)
-				ping_status = ret.returncode
-				#copy the ping log file to an other folder for log collection (source and destination are EPC)
-				SSH.copyout(EPC.IPAddress, EPC.UserName, EPC.Password, 'ping_' + self.testCase_id + '_' + device_id + '.log', EPC.SourceCodePath + '/scripts')
-                #copy the ping log file to have it locally for analysis (ping stats)
-				logging.debug(EPC.SourceCodePath + 'ping_' + self.testCase_id + '_' + device_id + '.log')
-				SSH.copyin(EPC.IPAddress, EPC.UserName, EPC.Password, EPC.SourceCodePath  +'/scripts/ping_' + self.testCase_id + '_' + device_id + '.log', '.')
+				if launchfromModule == False:
+					#ping log file is on the python executor
+					cmd = 'ping ' + self.ping_args + ' ' + UE_IPAddress + ' 2>&1 > ping_' + self.testCase_id + '_' + device_id + '.log' 
+					message = cmd + '\n'
+					logging.debug(cmd)
+					ret = subprocess.run(cmd, shell=True)
+					ping_status = ret.returncode
+					#copy the ping log file to an other folder for log collection (source and destination are EPC)
+					SSH.copyout(EPC.IPAddress, EPC.UserName, EPC.Password, 'ping_' + self.testCase_id + '_' + device_id + '.log', EPC.SourceCodePath + '/scripts')
+                	#copy the ping log file to have it locally for analysis (ping stats)
+					logging.debug(EPC.SourceCodePath + 'ping_' + self.testCase_id + '_' + device_id + '.log')
+					SSH.copyin(EPC.IPAddress, EPC.UserName, EPC.Password, EPC.SourceCodePath  +'/scripts/ping_' + self.testCase_id + '_' + device_id + '.log', '.')
+
+					SSH.open(EPC.IPAddress, EPC.UserName, EPC.Password)
+					#cat is executed on EPC
+					SSH.command('cat ' + EPC.SourceCodePath + '/scripts/ping_' + self.testCase_id + '_' + device_id + '.log', '\$', 5)
+				else: #launch from Module
+					SSH.open(Module_UE.HostIPAddress, Module_UE.HostUsername, Module_UE.HostPassword)
+					cmd = 'ping -I ' + UE_IPAddress + ' ' + self.ping_args + ' ' +  EPC.IPAddress  + ' 2>&1 > ping_' + self.testCase_id + '_' + self.ue_id + '.log' 
+					SSH.command(cmd,'\$',int(ping_time[0])*1.5)
+					#copy the ping log file to have it locally for analysis (ping stats)
+					SSH.copyin(Module_UE.HostIPAddress, Module_UE.HostUsername, Module_UE.HostPassword, 'ping_' + self.testCase_id + '_' + self.ue_id + '.log', '.')
+
+					#cat is executed locally 
+					SSH.command('cat ping_' + self.testCase_id + '_' + self.ue_id + '.log', '\$', 5)
+					ping_status=0
 
-				SSH.open(EPC.IPAddress, EPC.UserName, EPC.Password)
-				#cat is executed on EPC
-				SSH.command('cat ' + EPC.SourceCodePath + '/scripts/ping_' + self.testCase_id + '_' + device_id + '.log', '\$', 5)
 			# TIMEOUT CASE
 			if ping_status < 0:
 				message = 'Ping with UE (' + str(UE_IPAddress) + ') crashed due to TIMEOUT!'
@@ -1660,7 +1705,7 @@ class OaiCiTest():
 		except:
 			os.kill(os.getppid(),signal.SIGUSR1)
 
-	def Ping(self,HTML,RAN,EPC,COTS_UE):
+	def Ping(self,HTML,RAN,EPC,COTS_UE, InfraUE):
 		result = re.search('noS1', str(RAN.Initialize_eNB_args))
 		if result is not None:
 			self.PingNoS1(HTML,RAN,EPC,COTS_UE)
@@ -1678,18 +1723,30 @@ class OaiCiTest():
 			HTML.CreateHtmlTestRow(self.ping_args, 'KO', pStatus)
 			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC)
 			return
-		ueIpStatus = self.GetAllUEIPAddresses()
-		if (ueIpStatus < 0):
-			HTML.CreateHtmlTestRow(self.ping_args, 'KO', CONST.UE_IP_ADDRESS_ISSUE)
-			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC)
-			return
+
+		if self.ue_id=="":
+			Module_UE = cls_module_ue.Module_UE(InfraUE.ci_ue_infra['dummy']) #RH, temporary, we need a dummy Module_UE object to pass to Ping_common
+			ueIpStatus = self.GetAllUEIPAddresses()
+			if (ueIpStatus < 0):
+				HTML.CreateHtmlTestRow(self.ping_args, 'KO', CONST.UE_IP_ADDRESS_ISSUE)
+				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC)
+				return
+		else:
+			Module_UE = cls_module_ue.Module_UE(InfraUE.ci_ue_infra[self.ue_id])
+			Module_UE.GetModuleIPAddress()
+			if Module_UE.UEIPAddress not in self.UEIPAddresses:
+				self.UEIPAddresses.append(Module_UE.UEIPAddress)
+		logging.debug(self.UEIPAddresses)
 		multi_jobs = []
 		i = 0
 		lock = Lock()
 		status_queue = SimpleQueue()
 		for UE_IPAddress in self.UEIPAddresses:
-			device_id = self.UEDevices[i]
-			p = Process(target = self.Ping_common, args = (lock,UE_IPAddress,device_id,status_queue,EPC,))
+			if self.ue_id=="":
+				device_id = self.UEDevices[i]
+			else:
+				device_id = Module_UE.ID + "-" + Module_UE.Kind 
+			p = Process(target = self.Ping_common, args = (lock,UE_IPAddress,device_id,status_queue,EPC,Module_UE,))
 			p.daemon = True
 			p.start()
 			multi_jobs.append(p)
@@ -1864,8 +1921,8 @@ class OaiCiTest():
 		else:
 			return -2
 
-	def Iperf_analyzeV2Server(self, lock, UE_IPAddress, device_id, statusQueue, iperf_real_options):
-		if (not os.path.isfile('iperf_server_' + self.testCase_id + '_' + device_id + '.log')):
+	def Iperf_analyzeV2Server(self, lock, UE_IPAddress, device_id, statusQueue, iperf_real_options, filename,type):
+		if (not os.path.isfile(filename)):
 			self.ping_iperf_wrong_exit(lock, UE_IPAddress, device_id, statusQueue, 'Could not analyze from server log')
 			return
 		# Computing the requested bandwidth in float
@@ -1890,14 +1947,18 @@ class OaiCiTest():
 				req_bandwidth = '%.1f Gbits/sec' % req_bw
 				req_bw = req_bw * 1000000000
 
-		server_file = open('iperf_server_' + self.testCase_id + '_' + device_id + '.log', 'r')
+		server_file = open(filename, 'r')
 		br_sum = 0.0
 		ji_sum = 0.0
 		pl_sum = 0
 		ps_sum = 0
 		row_idx = 0
 		for line in server_file.readlines():
-			result = re.search('(?P<bitrate>[0-9\.]+ [KMG]bits\/sec) +(?P<jitter>[0-9\.]+ ms) +(?P<lostPack>[0-9]+)/ +(?P<sentPack>[0-9]+)', str(line))
+			if type==0:
+				result = re.search('(?P<bitrate>[0-9\.]+ [KMG]bits\/sec) +(?P<jitter>[0-9\.]+ ms) +(?P<lostPack>[0-9]+)/ +(?P<sentPack>[0-9]+)', str(line))
+			else:
+				result = re.search('^\[  3\].+ +(?P<bitrate>[0-9\.]+ [KMG]bits\/sec) +(?P<jitter>[0-9\.]+ ms) +(?P<lostPack>[0-9]+)\/(?P<sentPack>[0-9]+)', str(line))
+
 			if result is not None:
 				bitrate = result.group('bitrate')
 				jitter = result.group('jitter')
@@ -2127,12 +2188,58 @@ class OaiCiTest():
 					SSH.command('docker cp prod-trf-gen:/iperf-2.0.5/iperf_server_' + self.testCase_id + '_' + device_id + '.log ' + EPC.SourceCodePath + '/scripts', '\$', 5)
 					SSH.close()
 				SSH.copyin(EPC.IPAddress, EPC.UserName, EPC.Password, EPC.SourceCodePath+ '/scripts/iperf_server_' + self.testCase_id + '_' + device_id + '.log', '.')
-			self.Iperf_analyzeV2Server(lock, UE_IPAddress, device_id, statusQueue, modified_options)
+			filename='iperf_server_' + self.testCase_id + '_' + device_id + '.log'
+			self.Iperf_analyzeV2Server(lock, UE_IPAddress, device_id, statusQueue, modified_options,filename,0)
 		# in case of OAI-UE 
 		if (device_id == 'OAI-UE'):
 			SSH.copyin(self.UEIPAddress, self.UEUserName, self.UEPassword, self.UESourceCodePath + '/cmake_targets/iperf_' + self.testCase_id + '_' + device_id + '.log', '.')
 			SSH.copyout(EPC.IPAddress, EPC.UserName, EPC.Password, 'iperf_' + self.testCase_id + '_' + device_id + '.log', EPC.SourceCodePath + '/scripts')
 
+
+	def Iperf_Module(self, lock, UE_IPAddress, device_id, idx, ue_num, statusQueue,EPC, Module_UE):
+		SSH = sshconnection.SSHConnection()
+		iperf_time = self.Iperf_ComputeTime()	
+		if self.iperf_direction=="DL":
+			logging.debug("Iperf for Module in DL mode detected")
+			#server side UE
+			SSH.open(Module_UE.HostIPAddress, Module_UE.HostUsername, Module_UE.HostPassword)
+			cmd = 'echo $USER; nohup iperf -s -B ' + UE_IPAddress + ' -u  2>&1 > iperf_server_' + self.testCase_id + '_' + self.ue_id + '.log' 
+			SSH.command(cmd,'\$',5)
+			#client side EPC
+			SSH.open(EPC.IPAddress, EPC.UserName, EPC.Password)
+			cmd = 'iperf -c ' + UE_IPAddress + ' ' + self.iperf_args + ' 2>&1 > iperf_client_' + self.testCase_id + '_' + self.ue_id + '.log' 
+			SSH.command(cmd,'\$',int(iperf_time)*5.0)
+
+			#copy the 2 resulting files locally
+			SSH.copyin(Module_UE.HostIPAddress, Module_UE.HostUsername, Module_UE.HostPassword, 'iperf_server_' + self.testCase_id + '_' + self.ue_id + '.log', '.')
+			SSH.copyin(EPC.IPAddress, EPC.UserName, EPC.Password, 'iperf_client_' + self.testCase_id + '_' + self.ue_id + '.log', '.')
+			#send for analysis
+			filename='iperf_server_' + self.testCase_id + '_' + self.ue_id + '.log'
+			self.Iperf_analyzeV2Server(lock, UE_IPAddress, device_id, statusQueue, self.iperf_args,filename,1)	
+
+		elif self.iperf_direction=="UL":
+			logging.debug("Iperf for Module in UL mode detected")
+			#server side EPC
+			SSH.open(EPC.IPAddress, EPC.UserName, EPC.Password)
+			cmd = 'echo $USER; nohup iperf -s -u 2>&1 > iperf_server_' + self.testCase_id + '_' + self.ue_id + '.log' 
+			SSH.command(cmd,'\$',5)
+			#client side UE
+			SSH.open(Module_UE.HostIPAddress, Module_UE.HostUsername, Module_UE.HostPassword)
+			cmd = 'iperf -B ' + UE_IPAddress + ' ' + '-c ' + EPC.IPAddress + ' ' + self.iperf_args + ' > iperf_client_' + self.testCase_id + '_' + self.ue_id + '.log' 
+			SSH.command(cmd,'\$',int(iperf_time)*5.0)
+
+			#copy the 2 resulting files locally
+			SSH.copyin(Module_UE.HostIPAddress, Module_UE.HostUsername, Module_UE.HostPassword, 'iperf_server_' + self.testCase_id + '_' + self.ue_id + '.log', '.')
+			SSH.copyin(EPC.IPAddress, EPC.UserName, EPC.Password, 'iperf_client_' + self.testCase_id + '_' + self.ue_id + '.log', '.')
+			#send for analysis
+			filename='iperf_server_' + self.testCase_id + '_' + self.ue_id + '.log'
+			self.Iperf_analyzeV2Server(lock, UE_IPAddress, device_id, statusQueue, self.iperf_args,filename,1)				
+		else :
+			logging.debug("Incorrect or missing IPERF direction in XML")
+
+		SSH.close()
+		return
+
 	def Iperf_common(self, lock, UE_IPAddress, device_id, idx, ue_num, statusQueue,EPC):
 		try:
 			SSH = sshconnection.SSHConnection()
@@ -2229,8 +2336,13 @@ class OaiCiTest():
 			# Launch the IPERF client on the EPC side for DL (true for ltebox and old open-air-cn
 			# But for OAI-Rel14-CUPS, we launch from python executor
 			launchFromEpc = True
+			launchFromModule = False
 			if re.match('OAI-Rel14-CUPS', EPC.Type, re.IGNORECASE):
 				launchFromEpc = False
+			#if module
+			if self.ue_id!='' and self.iperf :
+				launchFromEpc = False
+				launchfromModule = True
 			# When using a docker-based deployment, IPERF client shall be launched from trf container
 			launchFromTrfContainer = False
 			if re.match('OAI-Rel14-Docker', EPC.Type, re.IGNORECASE):
@@ -2334,7 +2446,8 @@ class OaiCiTest():
 					subprocess.run(cmd, shell=True)
 				except:
 					pass
-				self.Iperf_analyzeV2Server(lock, UE_IPAddress, device_id, statusQueue, modified_options)
+				filename='iperf_server_' + self.testCase_id + '_' + device_id + '.log'
+				self.Iperf_analyzeV2Server(lock, UE_IPAddress, device_id, statusQueue, modified_options,filename,0)
 
 			# in case of OAI UE: 
 			if (device_id == 'OAI-UE'):
@@ -2422,7 +2535,8 @@ class OaiCiTest():
 			if (os.path.isfile('iperf_server_' + self.testCase_id + '.log')):
 				os.remove('iperf_server_' + self.testCase_id + '.log')
 			SSH.copyin(iServerIPAddr, iServerUser, iServerPasswd, '/tmp/tmp_iperf_server_' + self.testCase_id + '.log', 'iperf_server_' + self.testCase_id + '_OAI-UE.log')
-			self.Iperf_analyzeV2Server(lock, '10.0.1.2', 'OAI-UE', status_queue, modified_options)
+			filename='iperf_server_' + self.testCase_id + '_' + device_id + '.log'
+			self.Iperf_analyzeV2Server(lock, '10.0.1.2', 'OAI-UE', status_queue, modified_options,filename,0)
 
 		# copying on the EPC server for logCollection
 		if (clientStatus == -1):
@@ -2457,7 +2571,7 @@ class OaiCiTest():
 			HTML.CreateHtmlTestRowQueue(self.iperf_args, 'KO', len(self.UEDevices), html_queue)
 			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC)
 
-	def Iperf(self,HTML,RAN,EPC,COTS_UE):
+	def Iperf(self,HTML,RAN,EPC,COTS_UE, InfraUE):
 		result = re.search('noS1', str(RAN.Initialize_eNB_args))
 		if result is not None:
 			self.IperfNoS1(HTML,RAN,EPC,COTS_UE)
@@ -2475,12 +2589,21 @@ class OaiCiTest():
 			HTML.CreateHtmlTestRow(self.iperf_args, 'KO', pStatus)
 			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC)
 			return
-		ueIpStatus = self.GetAllUEIPAddresses()
-		if (ueIpStatus < 0):
-			logging.debug('going here')
-			HTML.CreateHtmlTestRow(self.iperf_args, 'KO', CONST.UE_IP_ADDRESS_ISSUE)
-			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC)
-			return
+
+		if self.ue_id=="":#is not a module, follow legacy code
+			ueIpStatus = self.GetAllUEIPAddresses()
+			if (ueIpStatus < 0):
+				HTML.CreateHtmlTestRow(self.iperf_args, 'KO', CONST.UE_IP_ADDRESS_ISSUE)
+				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC)
+				return
+		else: #is a module
+			Module_UE = cls_module_ue.Module_UE(InfraUE.ci_ue_infra[self.ue_id])
+			Module_UE.GetModuleIPAddress()
+			if Module_UE.UEIPAddress not in self.UEIPAddresses:
+				self.UEIPAddresses.append(Module_UE.UEIPAddress)
+
+
+
 
 		self.dummyIperfVersion = '2.0.10'
 		#cmd = 'iperf --version'
@@ -2498,9 +2621,15 @@ class OaiCiTest():
 		ue_num = len(self.UEIPAddresses)
 		lock = Lock()
 		status_queue = SimpleQueue()
+		logging.debug(self.UEIPAddresses)
 		for UE_IPAddress in self.UEIPAddresses:
 			device_id = self.UEDevices[i]
-			p = Process(target = self.Iperf_common, args = (lock,UE_IPAddress,device_id,i,ue_num,status_queue,EPC,))
+        	#special quick and dirty treatment for modules, iperf to be restructured
+			if self.ue_id!="": #is module
+				device_id = Module_UE.ID + "-" + Module_UE.Kind
+				p = Process(target = self.Iperf_Module ,args = (lock, UE_IPAddress, device_id, i, ue_num, status_queue, EPC, Module_UE,))
+			else: #legacy code
+				p = Process(target = self.Iperf_common, args = (lock, UE_IPAddress, device_id, i, ue_num, status_queue, EPC, ))
 			p.daemon = True
 			p.start()
 			multi_jobs.append(p)
diff --git a/ci-scripts/cls_physim1.py b/ci-scripts/cls_physim1.py
index 41beb1e0966b4b27d03eeb30d228c1fd7f6673de..86150a14704a7580927c7eef9b229edf4fb677bb 100644
--- a/ci-scripts/cls_physim1.py
+++ b/ci-scripts/cls_physim1.py
@@ -154,6 +154,7 @@ class PhySim:
 			sys.exit(-1)
 		else:
 			logging.debug('\u001B[1m Podman Login to OC Cluster Registry Successfully\u001B[0m')
+		time.sleep(2)
 		mySSH.command('oc create -f openshift/oai-physim-image-stream.yml', '\$', 6)
 		if mySSH.getBefore().count('(AlreadyExists):') == 0 and mySSH.getBefore().count('created') == 0:
 			logging.error(f'\u001B[1m Image Stream "oai-physim" Creation Failed on OC Cluster {ocProjectName}\u001B[0m')
@@ -161,7 +162,9 @@ class PhySim:
 			sys.exit(-1)
 		else:
 			logging.debug(f'\u001B[1m   Image Stream "oai-physim" created on OC project {ocProjectName}\u001B[0m')
+		time.sleep(2)
 		mySSH.command(f'sudo podman tag oai-physim:{imageTag} default-route-openshift-image-registry.apps.5glab.nsa.eurecom.fr/{self.OCProjectName}/oai-physim:{imageTag}', '\$', 6)
+		time.sleep(2)
 		mySSH.command(f'sudo podman push default-route-openshift-image-registry.apps.5glab.nsa.eurecom.fr/{self.OCProjectName}/oai-physim:{imageTag} --tls-verify=false', '\$', 30)
 		if mySSH.getBefore().count('Storing signatures') == 0:
 			logging.error('\u001B[1m Image "oai-physim" push to OC Cluster Registry Failed\u001B[0m')
@@ -171,6 +174,7 @@ class PhySim:
 			logging.debug('\u001B[1m Image "oai-physim" push to OC Cluster Registry Successfully\u001B[0m')
 
 		# Using helm charts deployment
+		time.sleep(5)
 		mySSH.command(f'sed -i -e "s#TAG#{imageTag}#g" ./charts/physims/values.yaml', '\$', 6)
 		mySSH.command('helm install physim ./charts/physims/ | tee -a cmake_targets/log/physim_helm_summary.txt 2>&1', '\$', 6)
 		if mySSH.getBefore().count('STATUS: deployed') == 0:
@@ -203,11 +207,10 @@ class PhySim:
 		# doing a deep copy!
 		tmpPodNames = podNames.copy()
 		while(count < 28 and isFinished == False):
-			time.sleep(58)
+			time.sleep(60)
 			for podName in tmpPodNames:
-				mySSH.command(f'oc logs --tail=1 {podName} 2>&1', '\$', 6, silent=True, resync=True)
-				time.sleep(2)
-				if mySSH.getBefore().count('FINISHED') != 0:
+				mySSH.command2(f'oc logs --tail=1 {podName} 2>&1', 6, silent=True)
+				if mySSH.cmd2Results.count('FINISHED') != 0:
 					logging.debug(podName + ' is finished')
 					tmpPodNames.remove(podName)
 			if not tmpPodNames:
diff --git a/ci-scripts/conf_files/gnb.band78.tm1.fr1.106PRB.usrpn310.conf b/ci-scripts/conf_files/gnb.band78.tm1.fr1.106PRB.usrpn310.conf
index 5e480bce54a8092ca44922773f81ff21a1a94c55..a078ff127cb6999bd1debd5e1fa2e57405267265 100644
--- a/ci-scripts/conf_files/gnb.band78.tm1.fr1.106PRB.usrpn310.conf
+++ b/ci-scripts/conf_files/gnb.band78.tm1.fr1.106PRB.usrpn310.conf
@@ -179,7 +179,7 @@ gNBs =
       nrofUplinkSlots                                               = 2;
       nrofUplinkSymbols                                             = 4; //0; //4;
 
-  ssPBCH_BlockPower                                             = -25;
+  ssPBCH_BlockPower                                             = -15;
   }
 
   );
diff --git a/ci-scripts/main.py b/ci-scripts/main.py
index 961bb16975f7503b2ce18d59b22e30caa535f5cd..389e7f8614f4db7165901bdc20350c50e18fc801 100644
--- a/ci-scripts/main.py
+++ b/ci-scripts/main.py
@@ -43,6 +43,7 @@ import cls_physim               #class PhySim for physical simulators build and
 import cls_cots_ue              #class CotsUe for Airplane mode control
 import cls_containerize         #class Containerize for all container-based operations on RAN/UE objects
 import cls_static_code_analysis #class for static code analysis
+import cls_ci_ueinfra			#class defining the multi Ue infrastrucure
 import cls_physim1          #class PhySim for physical simulators deploy and run
 
 import sshconnection 
@@ -193,6 +194,20 @@ def GetParametersFromXML(action):
 		else :
 			RAN.air_interface[RAN.eNB_instance] = 'ocp-enb'
 
+	elif action == 'Initialize_UE':
+		ue_id = test.findtext('id')
+		if (ue_id is None):
+			CiTestObj.ue_id = ""
+		else:
+			CiTestObj.ue_id = ue_id
+
+	elif action == 'Detach_UE':
+		ue_id = test.findtext('id')
+		if (ue_id is None):
+			CiTestObj.ue_id = ""
+		else:
+			CiTestObj.ue_id = ue_id
+
 	elif action == 'Attach_UE':
 		nbMaxUEtoAttach = test.findtext('nbMaxUEtoAttach')
 		if (nbMaxUEtoAttach is None):
@@ -253,9 +268,20 @@ def GetParametersFromXML(action):
 	elif (action == 'Ping') or (action == 'Ping_CatM_module'):
 		CiTestObj.ping_args = test.findtext('ping_args')
 		CiTestObj.ping_packetloss_threshold = test.findtext('ping_packetloss_threshold')
+		ue_id = test.findtext('id')
+		if (ue_id is None):
+			CiTestObj.ue_id = ""
+		else:
+			CiTestObj.ue_id = ue_id
 
 	elif action == 'Iperf':
 		CiTestObj.iperf_args = test.findtext('iperf_args')
+		ue_id = test.findtext('id')
+		if (ue_id is None):
+			CiTestObj.ue_id = ""
+		else:
+			CiTestObj.ue_id = ue_id
+		CiTestObj.iperf_direction = test.findtext('direction')#used for modules only	
 		CiTestObj.iperf_packetloss_threshold = test.findtext('iperf_packetloss_threshold')
 		CiTestObj.iperf_profile = test.findtext('iperf_profile')
 		if (CiTestObj.iperf_profile is None):
@@ -370,6 +396,20 @@ with open(yaml_file,'r') as f:
 
 
 
+#loading UE infrastructure from yaml
+ue_infra_file='ci_ueinfra.yaml'
+if (os.path.isfile(ue_infra_file)):
+	yaml_file=ue_infra_file
+elif (os.path.isfile('ci-scripts/'+ue_infra_file)):
+	yaml_file='ci-scripts/'+ue_infra_file
+else:
+	logging.error("UE infrastructure yaml file cannot be found")
+	sys.exit("UE infrastructure file cannot be found")
+InfraUE=cls_ci_ueinfra.InfraUE() #initialize UE infrastructure class
+InfraUE.Get_UE_Infra(yaml_file) #read the UE infra, filename is hardcoded and unique for the moment but should be passed as parameter from the test suite
+
+
+
 mode = ''
 
 CiTestObj = cls_oaicitest.OaiCiTest()
@@ -385,7 +425,6 @@ PHYSIM = cls_physim1.PhySim()
 ldpc=cls_physim.PhySim()    #create an instance for LDPC test using GPU or CPU build
 
 
-
 #-----------------------------------------------------------
 # Parsing Command Line Arguments
 #-----------------------------------------------------------
@@ -674,13 +713,13 @@ elif re.match('^TesteNB$', mode, re.IGNORECASE) or re.match('^TestUE$', mode, re
 				elif action == 'Terminate_eNB':
 					RAN.TerminateeNB(HTML, EPC)
 				elif action == 'Initialize_UE':
-					CiTestObj.InitializeUE(HTML,COTS_UE)
+					CiTestObj.InitializeUE(HTML,RAN, EPC, COTS_UE, InfraUE)
 				elif action == 'Terminate_UE':
 					CiTestObj.TerminateUE(HTML,COTS_UE)
 				elif action == 'Attach_UE':
 					CiTestObj.AttachUE(HTML,RAN,EPC,COTS_UE)
 				elif action == 'Detach_UE':
-					CiTestObj.DetachUE(HTML,RAN,EPC,COTS_UE)
+					CiTestObj.DetachUE(HTML,RAN,EPC,COTS_UE,InfraUE)
 				elif action == 'DataDisable_UE':
 					CiTestObj.DataDisableUE(HTML)
 				elif action == 'DataEnable_UE':
@@ -704,9 +743,9 @@ elif re.match('^TesteNB$', mode, re.IGNORECASE) or re.match('^TestUE$', mode, re
 				elif action == 'Ping_CatM_module':
 					CiTestObj.PingCatM(HTML,RAN,EPC,COTS_UE,EPC)
 				elif action == 'Ping':
-					CiTestObj.Ping(HTML,RAN,EPC,COTS_UE)
+					CiTestObj.Ping(HTML,RAN,EPC,COTS_UE, InfraUE)
 				elif action == 'Iperf':
-					CiTestObj.Iperf(HTML,RAN,EPC,COTS_UE)
+					CiTestObj.Iperf(HTML,RAN,EPC,COTS_UE, InfraUE)
 				elif action == 'Reboot_UE':
 					CiTestObj.RebootUE(HTML,RAN,EPC)
 				elif action == 'Initialize_HSS':
diff --git a/ci-scripts/ran.py b/ci-scripts/ran.py
index f5f53c20b7e9ecd501e704c84affe583e3fb2502..7079773a344d681c0ec2442a21c7d22164126896 100644
--- a/ci-scripts/ran.py
+++ b/ci-scripts/ran.py
@@ -242,7 +242,6 @@ class RANManagement():
 		while (count > 0) and buildOAIprocess:
 			mySSH.command('ps aux | grep --color=never build_ | grep -v grep', '\$', 6)
 			result = re.search('build_oai', mySSH.getBefore())
-			print(result)
 			if result is None:
 				buildOAIprocess = False
 			else:
@@ -636,7 +635,8 @@ class RANManagement():
 					HTML.CreateHtmlTestRow(self.runtime_stats, 'OK', CONST.ALL_PROCESSES_OK)
 			else:
 				HTML.CreateHtmlTestRow(self.runtime_stats, 'OK', CONST.ALL_PROCESSES_OK)
-		if len(self.datalog_rt_stats)!=0:
+		#display rt stats for gNB only
+		if len(self.datalog_rt_stats)!=0 and nodeB_prefix == 'g':
 			HTML.CreateHtmlDataLogTable(self.datalog_rt_stats)
 		self.eNBmbmsEnables[int(self.eNB_instance)] = False
 		self.eNBstatuses[int(self.eNB_instance)] = -1
diff --git a/ci-scripts/sshconnection.py b/ci-scripts/sshconnection.py
index 4a0992436b2844b40500877ebb93d9833cdb3bfb..b4087c9695900422b107227c3bc60b0fae23c1f4 100644
--- a/ci-scripts/sshconnection.py
+++ b/ci-scripts/sshconnection.py
@@ -35,6 +35,7 @@ import pexpect          # pexpect
 import logging
 import time             # sleep
 import re
+import subprocess
 import sys
 
 #-----------------------------------------------------------
@@ -44,6 +45,9 @@ class SSHConnection():
 	def __init__(self):
 		self.ssh = ''
 		self.picocom_closure = False
+		self.ipaddress = ''
+		self.username = ''
+		self.cmd2Results = ''
 
 	def disablePicocomClosure(self):
 		self.picocom_closure = False
@@ -98,6 +102,8 @@ class SSHConnection():
 			pass
 		else:
 			sys.exit('SSH Connection Failed')
+		self.ipaddress = ipaddress
+		self.username = username
 
 
 
@@ -146,10 +152,23 @@ class SSHConnection():
 			logging.debug('Expected Line : ' + expectedline)
 			sys.exit(self.sshresponse)
 
+	def command2(self, commandline, timeout, silent=False):
+		if not silent:
+			logging.debug(commandline)
+		self.cmd2Results = ''
+		myHost = self.username + '@' + self.ipaddress
+		# CAUTION: THIS METHOD IMPLIES THAT THERE ARE VALID SSH KEYS
+		# BETWEEN THE PYTHON EXECUTOR NODE AND THE REMOTE HOST
+		# OTHERWISE IT WON'T WORK
+		lSsh = subprocess.Popen(["ssh", "%s" % myHost, commandline],shell=False,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
+		self.cmd2Results = str(lSsh.stdout.readlines())
+
 	def close(self):
 		self.ssh.timeout = 5
 		self.ssh.sendline('exit')
 		self.sshresponse = self.ssh.expect([pexpect.EOF, pexpect.TIMEOUT])
+		self.ipaddress = ''
+		self.username = ''
 		if self.sshresponse == 0:
 			pass
 		elif self.sshresponse == 1:
diff --git a/ci-scripts/xml_files/fr1_nsa_base_ref.xml b/ci-scripts/xml_files/benetel_nsa_quectel.xml
similarity index 74%
rename from ci-scripts/xml_files/fr1_nsa_base_ref.xml
rename to ci-scripts/xml_files/benetel_nsa_quectel.xml
index fd47db10b2b1ecdc8fb084c4656451c35e54ca91..71301807535fb1c510b0a0a72adfc0187d69cf14 100644
--- a/ci-scripts/xml_files/fr1_nsa_base_ref.xml
+++ b/ci-scripts/xml_files/benetel_nsa_quectel.xml
@@ -22,64 +22,58 @@
 -->
 <testCaseList>
 	<htmlTabRef>TEST-NSA-FR1-TM1</htmlTabRef>
-	<htmlTabName>NSA FULL</htmlTabName>
+	<htmlTabName>NSA Ping DL UL with QUECTEL</htmlTabName>
 	<htmlTabIcon>tasks</htmlTabIcon>
+	<repeatCount>3</repeatCount>
 	<TestCaseRequestedList>
- 010000
  030000
  040000
- 010001
+ 000002
+ 010000
  000001
  050000
  050001
  000001
- 060000
- 060001
- 000001
  070000
+ 000001
+ 070001
+ 000001
  010002
  000001
  080001
  080000
- 010003
+  
 	</TestCaseRequestedList>
 	<TestCaseExclusionList></TestCaseExclusionList>
 
 	<testCase id="010000">
 		<class>Initialize_UE</class>
-		<desc>Initialize UE</desc>
-	</testCase>
-
-	<testCase id="010003">
-		<class>Terminate_UE</class>
-		<desc>Terminate UE</desc>
+		<desc>Initialize Quectel</desc>
+		<id>idefix</id>
 	</testCase>
 
-	<testCase id="010001">
-		<class>Attach_UE</class>
-		<desc>Attach UE</desc>
-	</testCase>
 
 	<testCase id="010002">
 		<class>Detach_UE</class>
 		<desc>Detach UE</desc>
+		<id>idefix</id>
 	</testCase>
 
 
 	<testCase id="030000">
 		<class>Initialize_eNB</class>
 		<desc>Initialize eNB</desc>
-		<Initialize_eNB_args>-O ci-scripts/conf_files/enb.band7.tm1.fr1.25PRB.usrpb210.conf</Initialize_eNB_args>
+		<Initialize_eNB_args>-O ci-scripts/conf_files/benetel-4g.conf</Initialize_eNB_args>
 		<eNB_instance>0</eNB_instance>
 		<eNB_serverId>0</eNB_serverId>
 		<air_interface>lte</air_interface>
-   </testCase>
+	</testCase>
 
 
 	<testCase id="040000">
 		<class>Initialize_eNB</class>
 		<desc>Initialize gNB</desc>
-		<Initialize_eNB_args>-O ci-scripts/conf_files/gnb.band78.tm1.fr1.106PRB.usrpb210.conf -E</Initialize_eNB_args>
+		<Initialize_eNB_args>-O ci-scripts/conf_files/benetel-5g.conf -q</Initialize_eNB_args>
 		<eNB_instance>1</eNB_instance>
 		<eNB_serverId>1</eNB_serverId>
 		<air_interface>nr</air_interface>
@@ -88,12 +82,20 @@
 	<testCase id="000001">
 		<class>IdleSleep</class>
 		<desc>Sleep</desc>
-		<idle_sleep_time_in_sec>20</idle_sleep_time_in_sec>
+		<idle_sleep_time_in_sec>10</idle_sleep_time_in_sec>
 	</testCase>
 
+	<testCase id="000002">
+		<class>IdleSleep</class>
+		<desc>Sleep</desc>
+		<idle_sleep_time_in_sec>30</idle_sleep_time_in_sec>
+	</testCase>
+
+
 	<testCase id="050000">
 		<class>Ping</class>
 		<desc>Ping: 20pings in 20sec</desc>
+		<id>idefix</id>
 		<ping_args>-c 20</ping_args>
 		<ping_packetloss_threshold>50</ping_packetloss_threshold>
 	</testCase>
@@ -101,32 +103,17 @@
 	<testCase id="050001">
 		<class>Ping</class>
 		<desc>Ping: 100pings in 20sec</desc>
+		<id>idefix</id>
 		<ping_args>-c 100 -i 0.2</ping_args>
 		<ping_packetloss_threshold>50</ping_packetloss_threshold>
 	</testCase>
 
-
-	<testCase id="060000">
-		<class>Iperf</class>
-		<desc>iperf (DL/3Mbps/UDP)(20 sec)(single-ue profile)</desc>
-		<iperf_args>-u -b 3M -t 20 -i 1</iperf_args>
-		<iperf_packetloss_threshold>50</iperf_packetloss_threshold>
-		<iperf_profile>single-ue</iperf_profile>
-	</testCase>
-
-	<testCase id="060001">
-		<class>Iperf</class>
-		<desc>iperf (UL/1Mbps/UDP)(20 sec)(single-ue profile)</desc>
-		<iperf_args>-u -b 1M -t 20 -i 1 -R</iperf_args>
-		<iperf_packetloss_threshold>50</iperf_packetloss_threshold>
-		<iperf_profile>single-ue</iperf_profile>
-	</testCase>
-
-
 	<testCase id="070000">
 		<class>Iperf</class>
 		<desc>iperf (DL/20Mbps/UDP)(20 sec)(single-ue profile)</desc>
 		<iperf_args>-u -b 20M -t 20 -i 1</iperf_args>
+		<direction>DL</direction>
+		<id>idefix</id>
 		<iperf_packetloss_threshold>50</iperf_packetloss_threshold>
 		<iperf_profile>single-ue</iperf_profile>
 	</testCase>
@@ -134,13 +121,14 @@
 	<testCase id="070001">
 		<class>Iperf</class>
 		<desc>iperf (UL/3Mbps/UDP)(20 sec)(single-ue profile)</desc>
-		<iperf_args>-u -b 3M -t 20 -i 1 -R</iperf_args>
+		<iperf_args>-u -b 3M -t 20 -i 1</iperf_args>
+		<direction>UL</direction>
+		<id>idefix</id>
 		<iperf_packetloss_threshold>50</iperf_packetloss_threshold>
 		<iperf_profile>single-ue</iperf_profile>
 	</testCase>
 
 
-
 	<testCase id="080000">
 		<class>Terminate_eNB</class>
 		<desc>Terminate eNB</desc>
diff --git a/ci-scripts/xml_files/fr1_nsa_quectel.xml b/ci-scripts/xml_files/fr1_nsa_quectel.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c48aa707396084cf0fd1cf98e29d4d6e2f8e59f2
--- /dev/null
+++ b/ci-scripts/xml_files/fr1_nsa_quectel.xml
@@ -0,0 +1,149 @@
+<!--
+
+ Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The OpenAirInterface Software Alliance licenses this file to You under
+ the OAI Public License, Version 1.1  (the "License"); you may not use this file
+ except in compliance with the License.
+ You may obtain a copy of the License at
+
+      http://www.openairinterface.org/?page_id=698
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ For more information about the OpenAirInterface (OAI) Software Alliance:
+      contact@openairinterface.org
+
+-->
+<testCaseList>
+	<htmlTabRef>TEST-NSA-FR1-TM1</htmlTabRef>
+	<htmlTabName>NSA Ping DL UL with QUECTEL</htmlTabName>
+	<htmlTabIcon>tasks</htmlTabIcon>
+	<repeatCount>3</repeatCount>
+	<TestCaseRequestedList>
+ 030000
+ 040000
+ 000002
+ 010000
+ 000001
+ 050000
+ 050001
+ 000001
+ 070000
+ 000001
+ 070001
+ 000001
+ 010002
+ 000001
+ 080001
+ 080000
+  
+	</TestCaseRequestedList>
+	<TestCaseExclusionList></TestCaseExclusionList>
+
+	<testCase id="010000">
+		<class>Initialize_UE</class>
+		<desc>Initialize Quectel</desc>
+		<id>idefix</id>
+	</testCase>
+
+
+	<testCase id="010002">
+		<class>Detach_UE</class>
+		<desc>Detach UE</desc>
+		<id>idefix</id>
+	</testCase>
+
+
+	<testCase id="030000">
+		<class>Initialize_eNB</class>
+		<desc>Initialize eNB</desc>
+		<Initialize_eNB_args>-O ci-scripts/conf_files/enb.band7.tm1.fr1.25PRB.usrpb210.conf</Initialize_eNB_args>
+		<eNB_instance>0</eNB_instance>
+		<eNB_serverId>0</eNB_serverId>
+		<air_interface>lte</air_interface>
+	</testCase>
+
+
+	<testCase id="040000">
+		<class>Initialize_eNB</class>
+		<desc>Initialize gNB</desc>
+		<Initialize_eNB_args>-O ci-scripts/conf_files/gnb.band78.tm1.fr1.106PRB.usrpn310.conf -q</Initialize_eNB_args>
+		<eNB_instance>1</eNB_instance>
+		<eNB_serverId>1</eNB_serverId>
+		<air_interface>nr</air_interface>
+	</testCase>
+
+	<testCase id="000001">
+		<class>IdleSleep</class>
+		<desc>Sleep</desc>
+		<idle_sleep_time_in_sec>10</idle_sleep_time_in_sec>
+	</testCase>
+
+	<testCase id="000002">
+		<class>IdleSleep</class>
+		<desc>Sleep</desc>
+		<idle_sleep_time_in_sec>30</idle_sleep_time_in_sec>
+	</testCase>
+
+
+	<testCase id="050000">
+		<class>Ping</class>
+		<desc>Ping: 20pings in 20sec</desc>
+		<id>idefix</id>
+		<ping_args>-c 20</ping_args>
+		<ping_packetloss_threshold>50</ping_packetloss_threshold>
+	</testCase>
+
+	<testCase id="050001">
+		<class>Ping</class>
+		<desc>Ping: 100pings in 20sec</desc>
+		<id>idefix</id>
+		<ping_args>-c 100 -i 0.2</ping_args>
+		<ping_packetloss_threshold>50</ping_packetloss_threshold>
+	</testCase>
+
+	<testCase id="070000">
+		<class>Iperf</class>
+		<desc>iperf (DL/20Mbps/UDP)(60 sec)(single-ue profile)</desc>
+		<iperf_args>-u -b 20M -t 60 -i 1</iperf_args>
+		<direction>DL</direction>
+		<id>idefix</id>
+		<iperf_packetloss_threshold>50</iperf_packetloss_threshold>
+		<iperf_profile>single-ue</iperf_profile>
+	</testCase>
+
+	<testCase id="070001">
+		<class>Iperf</class>
+		<desc>iperf (UL/3Mbps/UDP)(60 sec)(single-ue profile)</desc>
+		<iperf_args>-u -b 3M -t 60 -i 1</iperf_args>
+		<direction>UL</direction>
+		<id>idefix</id>
+		<iperf_packetloss_threshold>50</iperf_packetloss_threshold>
+		<iperf_profile>single-ue</iperf_profile>
+	</testCase>
+
+
+	<testCase id="080000">
+		<class>Terminate_eNB</class>
+		<desc>Terminate eNB</desc>
+		<eNB_instance>0</eNB_instance>
+		<eNB_serverId>0</eNB_serverId>
+		<air_interface>lte</air_interface>
+	</testCase>
+
+	<testCase id="080001">
+		<class>Terminate_eNB</class>
+		<desc>Terminate gNB</desc>
+		<eNB_instance>1</eNB_instance>
+		<eNB_serverId>1</eNB_serverId>
+		<air_interface>nr</air_interface>
+	</testCase>
+
+</testCaseList>
+