diff --git a/ci-scripts/Jenkinsfile-GitLab-Container b/ci-scripts/Jenkinsfile-GitLab-Container
index da85a8a282f0fd850392223d63b6e65dbd7f5c2e..654ad23230bfaf745d3b0d6953a972c44493888e 100644
--- a/ci-scripts/Jenkinsfile-GitLab-Container
+++ b/ci-scripts/Jenkinsfile-GitLab-Container
@@ -246,17 +246,12 @@ pipeline {
         script {
           triggerSlaveJob ('RAN-DockerHub-Push', 'Push-to-Docker-Hub')
         }
-        post {
-          always {
-            script {
-              echo "Push to Docker-Hub OK"
-            }
-          }
-          failure {
-            script {
-              echo "Push to Docker-Hub KO"
-              currentBuild.result = 'FAILURE'
-            }
+      }
+      post {
+        failure {
+          script {
+            echo "Push to Docker-Hub KO"
+            currentBuild.result = 'FAILURE'
           }
         }
       }
diff --git a/ci-scripts/Jenkinsfile-git-dashboard b/ci-scripts/Jenkinsfile-git-dashboard
index 6ebd46bc762410647c059ad02af2272ac679c0e9..87e781fe8a67f44c4d75a379a4af426bc04314cd 100644
--- a/ci-scripts/Jenkinsfile-git-dashboard
+++ b/ci-scripts/Jenkinsfile-git-dashboard
@@ -33,9 +33,12 @@ pipeline {
     stages {
         stage ("gDashboard") {
             steps {
-                script {     
-                    //retrieve MR data from gitlab and export to gSheet            
-                    sh returnStdout: true, script: 'python3 ci-scripts/ran_dashboard.py'
+                script { 
+                    dir ("ci-scripts/ran_dashboard") {    
+                              //retrieve MR data from gitlab / mySQL db, build HTML pages and load them to AWS S3 bucket (configured as static web page hosting)            
+                              //deprecated method : sh returnStdout: true, script: 'python3 ran_dashboard.py'
+                              sh returnStdout: true, script: 'python3 Hdashboard.py'
+                             }
                 }                                  
             }   
         }
diff --git a/ci-scripts/cls_containerize.py b/ci-scripts/cls_containerize.py
index dac24080627394564bf19b0f44bd2ff8e74b8c6c..8ee611362864a1a10ca4a860717148f4f7fd5efa 100644
--- a/ci-scripts/cls_containerize.py
+++ b/ci-scripts/cls_containerize.py
@@ -227,6 +227,8 @@ class Containerize():
 				if result is not None:
 					forceSharedImageBuild = True
 					sharedTag = 'ci-temp'
+		else:
+			forceSharedImageBuild = True
 
 		# Let's remove any previous run artifacts if still there
 		mySSH.command(self.cli + ' image prune --force', '\$', 30)
@@ -578,6 +580,8 @@ class Containerize():
 		# Currently support only one
 		mySSH.command('docker-compose --file ci-docker-compose.yml config', '\$', 5)
 		result = re.search('container_name: (?P<container_name>[a-zA-Z0-9\-\_]+)', mySSH.getBefore())
+		if self.eNB_logFile[self.eNB_instance] == '':
+			self.eNB_logFile[self.eNB_instance] = 'enb_' + HTML.testCase_id + '.log'
 		if result is not None:
 			containerName = result.group('container_name')
 			mySSH.command('docker kill --signal INT ' + containerName, '\$', 30)
@@ -718,7 +722,7 @@ class Containerize():
 		cmd = 'mkdir -p ../cmake_targets/log'
 		deployStatus = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, universal_newlines=True, timeout=10)
 
-		cmd = 'docker exec ' + self.pingContName + ' /bin/bash -c "ping ' + self.pingOptions + '" 2>&1 | tee ../cmake_targets/log/ping_' + HTML.testCase_id + '.log'
+		cmd = 'docker exec ' + self.pingContName + ' /bin/bash -c "ping ' + self.pingOptions + '" 2>&1 | tee ../cmake_targets/log/ping_' + HTML.testCase_id + '.log || true'
 		logging.debug(cmd)
 		deployStatus = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, universal_newlines=True, timeout=100)
 
@@ -784,18 +788,18 @@ class Containerize():
 		logStatus = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, universal_newlines=True, timeout=10)
 
 		# Start the server process
-		cmd = 'docker exec -d ' + self.svrContName + ' /bin/bash -c "nohup iperf ' + self.svrOptions + ' > /tmp/iperf_server.log 2>&1"'
+		cmd = 'docker exec -d ' + self.svrContName + ' /bin/bash -c "nohup iperf ' + self.svrOptions + ' > /tmp/iperf_server.log 2>&1" || true'
 		logging.debug(cmd)
 		serverStatus = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, universal_newlines=True, timeout=10)
 		time.sleep(5)
 
 		# Start the client process
-		cmd = 'docker exec ' + self.cliContName + ' /bin/bash -c "iperf ' + self.cliOptions + '" 2>&1 | tee ../cmake_targets/log/iperf_client_' + HTML.testCase_id + '.log'
+		cmd = 'docker exec ' + self.cliContName + ' /bin/bash -c "iperf ' + self.cliOptions + '" 2>&1 | tee ../cmake_targets/log/iperf_client_' + HTML.testCase_id + '.log || true'
 		logging.debug(cmd)
 		clientStatus = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, universal_newlines=True, timeout=100)
 
 		# Stop the server process
-		cmd = 'docker exec ' + self.svrContName + ' /bin/bash -c "pkill iperf"'
+		cmd = 'docker exec ' + self.svrContName + ' /bin/bash -c "pkill iperf" || true'
 		logging.debug(cmd)
 		serverStatus = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, universal_newlines=True, timeout=10)
 		time.sleep(5)
@@ -812,6 +816,7 @@ class Containerize():
 			else:
 				message = 'Server Report and Connection refused Not Found!'
 			self.IperfExit(HTML, False, message)
+			logging.error('\u001B[1;37;41m Iperf Test FAIL\u001B[0m')
 			return
 
 		# Computing the requested bandwidth in float
@@ -884,6 +889,8 @@ class Containerize():
 			self.IperfExit(HTML, iperfStatus, 'problem?')
 		if iperfStatus:
 			logging.info('\u001B[1m Iperf Test PASS\u001B[0m')
+		else:
+			logging.error('\u001B[1;37;41m Iperf Test FAIL\u001B[0m')
 
 	def IperfExit(self, HTML, status, message):
 		html_queue = SimpleQueue()
@@ -921,7 +928,7 @@ class Containerize():
 			if result is None:
 				mySSH.command('echo ' + password + ' | sudo -S sysctl net.ipv4.conf.all.forwarding=1', '\$', 10)
 			# Check if iptables forwarding is accepted
-			mySSH.command('echo ' + password + ' | sudo -S iptables -L', '\$', 10)
+			mySSH.command('echo ' + password + ' | sudo -S iptables -L FORWARD', '\$', 10)
 			result = re.search('Chain FORWARD .*policy ACCEPT', mySSH.getBefore())
 			if result is None:
 				mySSH.command('echo ' + password + ' | sudo -S iptables -P FORWARD ACCEPT', '\$', 10)
@@ -949,7 +956,7 @@ class Containerize():
 			if result is None:
 				mySSH.command('echo ' + password + ' | sudo -S sysctl net.ipv4.conf.all.forwarding=1', '\$', 10)
 			# Check if iptables forwarding is accepted
-			mySSH.command('echo ' + password + ' | sudo -S iptables -L', '\$', 10)
+			mySSH.command('echo ' + password + ' | sudo -S iptables -L FORWARD', '\$', 10)
 			result = re.search('Chain FORWARD .*policy ACCEPT', mySSH.getBefore())
 			if result is None:
 				mySSH.command('echo ' + password + ' | sudo -S iptables -P FORWARD ACCEPT', '\$', 10)
@@ -977,7 +984,7 @@ class Containerize():
 			if result is None:
 				mySSH.command('echo ' + password + ' | sudo -S sysctl net.ipv4.conf.all.forwarding=1', '\$', 10)
 			# Check if iptables forwarding is accepted
-			mySSH.command('echo ' + password + ' | sudo -S iptables -L', '\$', 10)
+			mySSH.command('echo ' + password + ' | sudo -S iptables -L FORWARD', '\$', 10)
 			result = re.search('Chain FORWARD .*policy ACCEPT', mySSH.getBefore())
 			if result is None:
 				mySSH.command('echo ' + password + ' | sudo -S iptables -P FORWARD ACCEPT', '\$', 10)
@@ -1000,7 +1007,7 @@ class Containerize():
 			if result is None:
 				mySSH.command('echo ' + password + ' | sudo -S sysctl net.ipv4.conf.all.forwarding=1', '\$', 10)
 			# Check if iptables forwarding is accepted
-			mySSH.command('echo ' + password + ' | sudo -S iptables -L', '\$', 10)
+			mySSH.command('echo ' + password + ' | sudo -S iptables -L FORWARD', '\$', 10)
 			result = re.search('Chain FORWARD .*policy ACCEPT', mySSH.getBefore())
 			if result is None:
 				mySSH.command('echo ' + password + ' | sudo -S iptables -P FORWARD ACCEPT', '\$', 10)
diff --git a/ci-scripts/cls_oaicitest.py b/ci-scripts/cls_oaicitest.py
index ab7d249f7e05dee1b120d657a38c800e95d6f48f..cc6e235ab3c3d8da93f389d25ad9701df66722f0 100644
--- a/ci-scripts/cls_oaicitest.py
+++ b/ci-scripts/cls_oaicitest.py
@@ -368,7 +368,7 @@ class OaiCiTest():
 		except:
 			os.kill(os.getppid(),signal.SIGUSR1)
 
-	def InitializeUE(self,HTML,RAN,EPC, COTS_UE, InfraUE,ue_trace):
+	def InitializeUE(self,HTML,RAN,EPC, COTS_UE, InfraUE,ue_trace,CONTAINERS):
 		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)
@@ -424,12 +424,12 @@ class OaiCiTest():
 					Module_UE.CheckModuleMTU()
 				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,InfraUE)
+					self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 					return
 			
 
 
-	def InitializeOAIUE(self,HTML,RAN,EPC,COTS_UE,InfraUE):
+	def InitializeOAIUE(self,HTML,RAN,EPC,COTS_UE,InfraUE,CONTAINERS):
 		if self.UEIPAddress == '' or self.UEUserName == '' or self.UEPassword == '' or self.UESourceCodePath == '':
 			HELP.GenericHelp(CONST.Version)
 			sys.exit('Insufficient Parameter')
@@ -653,7 +653,7 @@ class OaiCiTest():
 				HTML.htmlUEFailureMsg='nr-uesoftmodem did NOT synced'
 				HTML.CreateHtmlTestRow(self.air_interface + ' ' +  self.Initialize_OAI_UE_args, 'KO', CONST.OAI_UE_PROCESS_COULD_NOT_SYNC, 'OAI UE')
 			logging.error('\033[91mInitialize OAI UE Failed! \033[0m')
-			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 
 	def checkDevTTYisUnlocked(self):
 		SSH = sshconnection.SSHConnection()
@@ -731,7 +731,7 @@ class OaiCiTest():
 		HTML.CreateHtmlTestRow('N/A', 'OK', CONST.ALL_PROCESSES_OK)
 		self.checkDevTTYisUnlocked()
 
-	def AttachCatM(self,HTML,RAN,COTS_UE,EPC,InfraUE):
+	def AttachCatM(self,HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS):
 		if self.ADBIPAddress == '' or self.ADBUserName == '' or self.ADBPassword == '':
 			HELP.GenericHelp(CONST.Version)
 			sys.exit('Insufficient Parameter')
@@ -804,9 +804,9 @@ class OaiCiTest():
 			html_cell = '<pre style="background-color:white">CAT-M module Attachment Failed</pre>'
 			html_queue.put(html_cell)
 			HTML.CreateHtmlTestRowQueue('N/A', 'KO', 1, html_queue)
-			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 
-	def PingCatM(self,HTML,RAN,EPC,COTS_UE,InfraUE):
+	def PingCatM(self,HTML,RAN,EPC,COTS_UE,InfraUE,CONTAINERS):
 		if EPC.IPAddress == '' or EPC.UserName == '' or EPC.Password == '' or EPC.SourceCodePath == '':
 			HELP.GenericHelp(CONST.Version)
 			sys.exit('Insufficient Parameter')
@@ -815,7 +815,7 @@ class OaiCiTest():
 		pStatus = self.CheckProcessExist(check_eNB, check_OAI_UE,RAN,EPC)
 		if (pStatus < 0):
 			HTML.CreateHtmlTestRow(self.ping_args, 'KO', pStatus)
-			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 			return
 		try:
 			statusQueue = SimpleQueue()
@@ -836,7 +836,7 @@ class OaiCiTest():
 					moduleIPAddr = result.group('ipaddr')
 				else:
 					HTML.CreateHtmlTestRow(self.ping_args, 'KO', pStatus)
-					self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+					self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 					return
 			ping_time = re.findall("-c (\d+)",str(self.ping_args))
 			device_id = 'catm'
@@ -900,7 +900,7 @@ class OaiCiTest():
 				HTML.CreateHtmlTestRowQueue(self.ping_args, 'OK', 1, statusQueue)
 			else:
 				HTML.CreateHtmlTestRowQueue(self.ping_args, 'KO', 1, statusQueue)
-				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 		except:
 			os.kill(os.getppid(),signal.SIGUSR1)
 
@@ -992,7 +992,7 @@ class OaiCiTest():
 		except:
 			os.kill(os.getppid(),signal.SIGUSR1)
 
-	def AttachUE(self,HTML,RAN,EPC,COTS_UE,InfraUE):
+	def AttachUE(self,HTML,RAN,EPC,COTS_UE,InfraUE,CONTAINERS):
 		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)
@@ -1002,7 +1002,7 @@ class OaiCiTest():
 			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,InfraUE)
+				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 				return
 			multi_jobs = []
 			status_queue = SimpleQueue()
@@ -1021,7 +1021,7 @@ class OaiCiTest():
 
 			if (status_queue.empty()):
 				HTML.CreateHtmlTestRow('N/A', 'KO', CONST.ALL_PROCESSES_OK)
-				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 				return
 			else:
 				attach_status = True
@@ -1050,7 +1050,7 @@ class OaiCiTest():
 						time.sleep(5)
 				else:
 					HTML.CreateHtmlTestRowQueue('N/A', 'KO', len(self.UEDevices), html_queue)
-					self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+					self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 
 		else: #if an ID is specified, it is a module from the yaml infrastructure file
 			#Attention, as opposed to InitializeUE, the connect manager process is not checked as it is supposed to be active already
@@ -1086,7 +1086,7 @@ class OaiCiTest():
 				Module_UE.CheckModuleMTU()
 			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,InfraUE)
+				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 				return					
 
 	def DetachUE_common(self, device_id, idx,COTS_UE):
@@ -1111,7 +1111,7 @@ class OaiCiTest():
 		except:
 			os.kill(os.getppid(),signal.SIGUSR1)
 
-	def DetachUE(self,HTML,RAN,EPC,COTS_UE,InfraUE):
+	def DetachUE(self,HTML,RAN,EPC,COTS_UE,InfraUE,CONTAINERS):
 		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)
@@ -1121,7 +1121,7 @@ class OaiCiTest():
 			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,InfraUE)
+				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 				return
 			multi_jobs = []
 			cnt = 0
@@ -1396,7 +1396,7 @@ class OaiCiTest():
 		except:
 			os.kill(os.getppid(),signal.SIGUSR1)
 
-	def CheckStatusUE(self,HTML,RAN,EPC,COTS_UE,InfraUE):
+	def CheckStatusUE(self,HTML,RAN,EPC,COTS_UE,InfraUE,CONTAINERS):
 		if self.ADBIPAddress == '' or self.ADBUserName == '' or self.ADBPassword == '':
 			HELP.GenericHelp(CONST.Version)
 			sys.exit('Insufficient Parameter')
@@ -1443,7 +1443,7 @@ class OaiCiTest():
 
 		if (status_queue.empty()):
 			HTML.CreateHtmlTestRow(htmlOptions, 'KO', CONST.ALL_PROCESSES_OK)
-			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 		else:
 			check_status = True
 			html_queue = SimpleQueue()
@@ -1459,7 +1459,7 @@ class OaiCiTest():
 				HTML.CreateHtmlTestRowQueue(htmlOptions, 'OK', len(self.UEDevices), html_queue)
 			else:
 				HTML.CreateHtmlTestRowQueue(htmlOptions, 'KO', len(self.UEDevices), html_queue)
-				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 
 	def GetAllUEIPAddresses(self):
 		SSH = sshconnection.SSHConnection()
@@ -1686,14 +1686,14 @@ class OaiCiTest():
 		html_queue.put(html_cell)
 		HTML.CreateHtmlTestRowQueue(self.ping_args, 'KO', len(self.UEDevices), html_queue)
 
-	def PingNoS1(self,HTML,RAN,EPC,COTS_UE,InfraUE):
+	def PingNoS1(self,HTML,RAN,EPC,COTS_UE,InfraUE,CONTAINERS):
 		SSH=sshconnection.SSHConnection()
 		check_eNB = True
 		check_OAI_UE = True
 		pStatus = self.CheckProcessExist(check_eNB, check_OAI_UE,RAN,EPC)
 		if (pStatus < 0):
 			HTML.CreateHtmlTestRow(self.ping_args, 'KO', pStatus)
-			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 			return
 		ping_from_eNB = re.search('oaitun_enb1', str(self.ping_args))
 		if ping_from_eNB is not None:
@@ -1779,10 +1779,10 @@ class OaiCiTest():
 		except:
 			os.kill(os.getppid(),signal.SIGUSR1)
 
-	def Ping(self,HTML,RAN,EPC,COTS_UE, InfraUE):
+	def Ping(self,HTML,RAN,EPC,COTS_UE, InfraUE, CONTAINERS):
 		result = re.search('noS1', str(RAN.Initialize_eNB_args))
 		if result is not None:
-			self.PingNoS1(HTML,RAN,EPC,COTS_UE,InfraUE)
+			self.PingNoS1(HTML,RAN,EPC,COTS_UE,InfraUE,CONTAINERS)
 			return
 		if EPC.IPAddress == '' or EPC.UserName == '' or EPC.Password == '' or EPC.SourceCodePath == '':
 			HELP.GenericHelp(CONST.Version)
@@ -1795,7 +1795,7 @@ class OaiCiTest():
 		pStatus = self.CheckProcessExist(check_eNB, check_OAI_UE,RAN,EPC)
 		if (pStatus < 0):
 			HTML.CreateHtmlTestRow(self.ping_args, 'KO', pStatus)
-			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 			return
 
 		if self.ue_id=="":
@@ -1803,7 +1803,7 @@ class OaiCiTest():
 			ueIpStatus = self.GetAllUEIPAddresses()
 			if (ueIpStatus < 0):
 				HTML.CreateHtmlTestRow(self.ping_args, 'KO', CONST.UE_IP_ADDRESS_ISSUE)
-				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 				return
 		else:
 			self.UEIPAddresses=[]
@@ -1830,7 +1830,7 @@ class OaiCiTest():
 
 		if (status_queue.empty()):
 			HTML.CreateHtmlTestRow(self.ping_args, 'KO', CONST.ALL_PROCESSES_OK)
-			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 		else:
 			ping_status = True
 			html_queue = SimpleQueue()
@@ -1847,7 +1847,7 @@ class OaiCiTest():
 				HTML.CreateHtmlTestRowQueue(self.ping_args, 'OK', len(self.UEDevices), html_queue)
 			else:
 				HTML.CreateHtmlTestRowQueue(self.ping_args, 'KO', len(self.UEDevices), html_queue)
-				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 
 	def Iperf_ComputeTime(self):
 		result = re.search('-t (?P<iperf_time>\d+)', str(self.iperf_args))
@@ -2647,7 +2647,7 @@ class OaiCiTest():
 		except:
 			os.kill(os.getppid(),signal.SIGUSR1)
 
-	def IperfNoS1(self,HTML,RAN,EPC,COTS_UE,InfraUE):
+	def IperfNoS1(self,HTML,RAN,EPC,COTS_UE,InfraUE,CONTAINERS):
 		SSH = sshconnection.SSHConnection()
 		if RAN.eNBIPAddress == '' or RAN.eNBUserName == '' or RAN.eNBPassword == '' or self.UEIPAddress == '' or self.UEUserName == '' or self.UEPassword == '':
 			HELP.GenericHelp(CONST.Version)
@@ -2657,7 +2657,7 @@ class OaiCiTest():
 		pStatus = self.CheckProcessExist(check_eNB, check_OAI_UE,RAN,EPC)
 		if (pStatus < 0):
 			HTML.CreateHtmlTestRow(self.iperf_args, 'KO', pStatus)
-			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 			return
 		server_on_enb = re.search('-R', str(self.iperf_args))
 		if server_on_enb is not None:
@@ -2756,12 +2756,12 @@ class OaiCiTest():
 			HTML.CreateHtmlTestRowQueue(self.iperf_args, 'OK', len(self.UEDevices), html_queue)
 		else:
 			HTML.CreateHtmlTestRowQueue(self.iperf_args, 'KO', len(self.UEDevices), html_queue)
-			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 
-	def Iperf(self,HTML,RAN,EPC,COTS_UE, InfraUE):
+	def Iperf(self,HTML,RAN,EPC,COTS_UE, InfraUE,CONTAINERS):
 		result = re.search('noS1', str(RAN.Initialize_eNB_args))
 		if result is not None:
-			self.IperfNoS1(HTML,RAN,EPC,COTS_UE,InfraUE)
+			self.IperfNoS1(HTML,RAN,EPC,COTS_UE,InfraUE,CONTAINERS)
 			return
 		if EPC.IPAddress == '' or EPC.UserName == '' or EPC.Password == '' or EPC.SourceCodePath == '' or self.ADBIPAddress == '' or self.ADBUserName == '' or self.ADBPassword == '':
 			HELP.GenericHelp(CONST.Version)
@@ -2774,14 +2774,14 @@ class OaiCiTest():
 		pStatus = self.CheckProcessExist(check_eNB, check_OAI_UE,RAN,EPC)
 		if (pStatus < 0):
 			HTML.CreateHtmlTestRow(self.iperf_args, 'KO', pStatus)
-			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 			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,InfraUE)
+				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 				return
 		else: #is a module
 			self.UEIPAddresses=[]
@@ -2826,7 +2826,7 @@ class OaiCiTest():
 
 		if (status_queue.empty()):
 			HTML.CreateHtmlTestRow(self.iperf_args, 'KO', CONST.ALL_PROCESSES_OK)
-			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+			self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 		else:
 			iperf_status = True
 			iperf_noperf = False
@@ -2848,7 +2848,7 @@ class OaiCiTest():
 				HTML.CreateHtmlTestRowQueue(self.iperf_args, 'OK', len(self.UEDevices), html_queue)
 			else:
 				HTML.CreateHtmlTestRowQueue(self.iperf_args, 'KO', len(self.UEDevices), html_queue)
-				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+				self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 
 	def CheckProcessExist(self, check_eNB, check_OAI_UE,RAN,EPC):
 		multi_jobs = []
@@ -3280,7 +3280,7 @@ class OaiCiTest():
 			else:
 				HTML.CreateHtmlTestRow('QLog trace is disabled', 'OK', CONST.ALL_PROCESSES_OK)			
 
-	def TerminateOAIUE(self,HTML,RAN,COTS_UE,EPC, InfraUE):
+	def TerminateOAIUE(self,HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS):
 		SSH = sshconnection.SSHConnection()
 		SSH.open(self.UEIPAddress, self.UEUserName, self.UEPassword)
 		SSH.command('cd ' + self.UESourceCodePath + '/cmake_targets', '\$', 5)
@@ -3321,11 +3321,11 @@ class OaiCiTest():
 					# Not an error then
 					if (logStatus != CONST.OAI_UE_PROCESS_COULD_NOT_SYNC) or (ueAction != 'Sniffing'):
 						self.Initialize_OAI_UE_args = ''
-						self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+						self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 				else:
 					if (logStatus == CONST.OAI_UE_PROCESS_COULD_NOT_SYNC):
 						self.Initialize_OAI_UE_args = ''
-						self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE)
+						self.AutoTerminateUEandeNB(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 			else:
 				logging.debug('\u001B[1m' + ueAction + ' Completed \u001B[0m')
 				HTML.htmlUEFailureMsg='<b>' + ueAction + ' Completed</b>\n' + HTML.htmlUEFailureMsg
@@ -3334,26 +3334,26 @@ class OaiCiTest():
 		else:
 			HTML.CreateHtmlTestRow('N/A', 'OK', CONST.ALL_PROCESSES_OK)
 
-	def AutoTerminateUEandeNB(self,HTML,RAN,COTS_UE,EPC,InfraUE):
+	def AutoTerminateUEandeNB(self,HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS):
 		if (self.ADBIPAddress != 'none'):
 			self.testCase_id = 'AUTO-KILL-UE'
-			HTML.testCase_id=self.testCase_id
+			HTML.testCase_id = self.testCase_id
 			self.desc = 'Automatic Termination of UE'
-			HTML.desc='Automatic Termination of UE'
+			HTML.desc = self.desc
 			self.ShowTestID()
 			self.TerminateUE(HTML,COTS_UE,InfraUE,self.ue_trace)
 		if (self.Initialize_OAI_UE_args != ''):
 			self.testCase_id = 'AUTO-KILL-OAI-UE'
-			HTML.testCase_id=self.testCase_id
+			HTML.testCase_id = self.testCase_id
 			self.desc = 'Automatic Termination of OAI-UE'
-			HTML.desc='Automatic Termination of OAI-UE'
+			HTML.desc = self.desc
 			self.ShowTestID()
 			self.TerminateOAIUE(HTML,RAN,COTS_UE,EPC,InfraUE)
 		if (RAN.Initialize_eNB_args != ''):
 			self.testCase_id = 'AUTO-KILL-RAN'
-			HTML.testCase_id=self.testCase_id
+			HTML.testCase_id = self.testCase_id
 			self.desc = 'Automatic Termination of all RAN nodes'
-			HTML.desc='Automatic Termination of RAN nodes'
+			HTML.desc = self.desc
 			self.ShowTestID()
 			#terminate all RAN nodes eNB/gNB/OCP
 			for instance in range(0, len(RAN.air_interface)):
@@ -3363,11 +3363,21 @@ class OaiCiTest():
 					RAN.TerminateeNB(HTML,EPC)
 		if RAN.flexranCtrlInstalled and RAN.flexranCtrlStarted:
 			self.testCase_id = 'AUTO-KILL-flexran-ctl'
-			HTML.testCase_id=self.testCase_id
+			HTML.testCase_id = self.testCase_id
 			self.desc = 'Automatic Termination of FlexRan CTL'
-			HTML.desc='Automatic Termination of FlexRan CTL'
+			HTML.desc = self.desc
 			self.ShowTestID()
 			self.TerminateFlexranCtrl(HTML,RAN,EPC)
+		if CONTAINERS.yamlPath[0] != '':
+			self.testCase_id = 'AUTO-KILL-CONTAINERS'
+			HTML.testCase_id = self.testCase_id
+			self.desc = 'Automatic Termination of all RAN containers'
+			HTML.desc = self.desc
+			self.ShowTestID()
+			for instance in range(0, len(CONTAINERS.yamlPath)):
+				if CONTAINERS.yamlPath[instance]!='':
+					CONTAINERS.eNB_instance=instance
+					CONTAINERS.UndeployObject(HTML,RAN)
 		RAN.prematureExit=True
 
 	def IdleSleep(self,HTML):
diff --git a/ci-scripts/conf_files/enb.band7.tm1.fr1.25PRB.usrpb210.conf b/ci-scripts/conf_files/enb.band7.tm1.fr1.25PRB.usrpb210.conf
index 454649f20d4b858c8abcf280882603e0d7cfa901..64bcb4b35f0fc5789c5c9754ca75cc7955fd592a 100644
--- a/ci-scripts/conf_files/enb.band7.tm1.fr1.25PRB.usrpb210.conf
+++ b/ci-scripts/conf_files/enb.band7.tm1.fr1.25PRB.usrpb210.conf
@@ -245,7 +245,7 @@ THREAD_STRUCT =
 (
   {
     #three config for level of parallelism "PARALLEL_SINGLE_THREAD", "PARALLEL_RU_L1_SPLIT", or "PARALLEL_RU_L1_TRX_SPLIT"
-    parallel_config    = "PARALLEL_RU_L1_TRX_SPLIT";
+    parallel_config    = "PARALLEL_SINGLE_THREAD";
     #two option for worker "WORKER_DISABLE" or "WORKER_ENABLE"
     worker_config      = "WORKER_ENABLE";
   }
diff --git a/ci-scripts/conf_files/gnb.band78.nsa_2x2.106PRB.usrpn310.conf b/ci-scripts/conf_files/gnb.band78.nsa_2x2.106PRB.usrpn310.conf
index a4452b202d76a6973cf77637ec5fb9d3f2cf0aaa..c8dc1477b080a89fee03dc2eaa1f586843cbc106 100644
--- a/ci-scripts/conf_files/gnb.band78.nsa_2x2.106PRB.usrpn310.conf
+++ b/ci-scripts/conf_files/gnb.band78.nsa_2x2.106PRB.usrpn310.conf
@@ -226,6 +226,7 @@ MACRLCs = (
 	num_cc = 1;
 	tr_s_preference = "local_L1";
 	tr_n_preference = "local_RRC";
+	ulsch_max_frame_inactivity = 1;
         }  
 );
 
diff --git a/ci-scripts/conf_files/gnb.band78.tm1.fr1.106PRB.usrpb210.conf b/ci-scripts/conf_files/gnb.band78.tm1.fr1.106PRB.usrpb210.conf
index 7bda7f7eb52ca4feafff62cf2589960e2acf7342..a72147ee99663b553aae96aee9c476366a45927a 100644
--- a/ci-scripts/conf_files/gnb.band78.tm1.fr1.106PRB.usrpb210.conf
+++ b/ci-scripts/conf_files/gnb.band78.tm1.fr1.106PRB.usrpb210.conf
@@ -226,6 +226,7 @@ MACRLCs = (
     tr_n_preference     = "local_RRC";
     pusch_TargetSNRx10  = 200;
     pucch_TargetSNRx10  = 200;
+    ulsch_max_frame_inactivity = 1;
   }
 );
 
diff --git a/ci-scripts/main.py b/ci-scripts/main.py
index 409647e216b7253d405522fe78111ebdf5c08c0b..e9acdc9152fc9892ce7ddf4c7df1fe2a89aef62a 100644
--- a/ci-scripts/main.py
+++ b/ci-scripts/main.py
@@ -544,7 +544,7 @@ elif re.match('^TerminateOAIUE$', mode, re.IGNORECASE):
 		HELP.GenericHelp(CONST.Version)
 		sys.exit('Insufficient Parameter')
 	signal.signal(signal.SIGUSR1, receive_signal)
-	CiTestObj.TerminateOAIUE(HTML,RAN,COTS_UE,EPC,InfraUE)
+	CiTestObj.TerminateOAIUE(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 elif re.match('^TerminateHSS$', mode, re.IGNORECASE):
 	if EPC.IPAddress == '' or EPC.UserName == '' or EPC.Password == '' or EPC.Type == '' or EPC.SourceCodePath == '':
 		HELP.GenericHelp(CONST.Version)
@@ -809,39 +809,39 @@ 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,RAN, EPC, COTS_UE, InfraUE, CiTestObj.ue_trace)
+					CiTestObj.InitializeUE(HTML,RAN, EPC, COTS_UE, InfraUE, CiTestObj.ue_trace, CONTAINERS)
 				elif action == 'Terminate_UE':
 					CiTestObj.TerminateUE(HTML,COTS_UE, InfraUE, CiTestObj.ue_trace)
 				elif action == 'Attach_UE':
-					CiTestObj.AttachUE(HTML,RAN,EPC,COTS_UE,InfraUE)
+					CiTestObj.AttachUE(HTML,RAN,EPC,COTS_UE,InfraUE,CONTAINERS)
 				elif action == 'Detach_UE':
-					CiTestObj.DetachUE(HTML,RAN,EPC,COTS_UE,InfraUE)
+					CiTestObj.DetachUE(HTML,RAN,EPC,COTS_UE,InfraUE,CONTAINERS)
 				elif action == 'DataDisable_UE':
 					CiTestObj.DataDisableUE(HTML)
 				elif action == 'DataEnable_UE':
 					CiTestObj.DataEnableUE(HTML)
 				elif action == 'CheckStatusUE':
-					CiTestObj.CheckStatusUE(HTML,RAN,EPC,COTS_UE,InfraUE)
+					CiTestObj.CheckStatusUE(HTML,RAN,EPC,COTS_UE,InfraUE,CONTAINERS)
 				elif action == 'Build_OAI_UE':
 					CiTestObj.BuildOAIUE(HTML)
 				elif action == 'Initialize_OAI_UE':
-					CiTestObj.InitializeOAIUE(HTML,RAN,EPC,COTS_UE,InfraUE)
+					CiTestObj.InitializeOAIUE(HTML,RAN,EPC,COTS_UE,InfraUE,CONTAINERS)
 				elif action == 'Terminate_OAI_UE':
-					CiTestObj.TerminateOAIUE(HTML,RAN,COTS_UE,EPC,InfraUE)
+					CiTestObj.TerminateOAIUE(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 				elif action == 'Initialize_CatM_module':
 					CiTestObj.InitializeCatM(HTML)
 				elif action == 'Terminate_CatM_module':
 					CiTestObj.TerminateCatM(HTML)
 				elif action == 'Attach_CatM_module':
-					CiTestObj.AttachCatM(HTML,RAN,COTS_UE,EPC,InfraUE)
+					CiTestObj.AttachCatM(HTML,RAN,COTS_UE,EPC,InfraUE,CONTAINERS)
 				elif action == 'Detach_CatM_module':
 					CiTestObj.TerminateCatM(HTML)
 				elif action == 'Ping_CatM_module':
-					CiTestObj.PingCatM(HTML,RAN,EPC,COTS_UE,EPC,InfraUE)
+					CiTestObj.PingCatM(HTML,RAN,EPC,COTS_UE,EPC,InfraUE,CONTAINERS)
 				elif action == 'Ping':
-					CiTestObj.Ping(HTML,RAN,EPC,COTS_UE, InfraUE)
+					CiTestObj.Ping(HTML,RAN,EPC,COTS_UE, InfraUE, CONTAINERS)
 				elif action == 'Iperf':
-					CiTestObj.Iperf(HTML,RAN,EPC,COTS_UE, InfraUE)
+					CiTestObj.Iperf(HTML,RAN,EPC,COTS_UE, InfraUE, CONTAINERS)
 				elif action == 'Reboot_UE':
 					CiTestObj.RebootUE(HTML,RAN,EPC)
 				elif action == 'Initialize_HSS':
diff --git a/ci-scripts/mysql4testresults/docker-compose.yml b/ci-scripts/mysql4testresults/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1c60057ab2f31f7c932977830881dcca0447b185
--- /dev/null
+++ b/ci-scripts/mysql4testresults/docker-compose.yml
@@ -0,0 +1,12 @@
+version: '2'
+services:
+  mysql:
+    container_name: oaicicd_mysql
+    restart: always
+    image: mysql:latest
+    environment:
+      MYSQL_ROOT_PASSWORD: 'ucZBc2XRYdvEm59F' 
+    ports: 
+      - "3307:3306"
+    volumes:
+      - /home/oaicicd/mysql/data:/var/lib/mysql
diff --git a/ci-scripts/mysql4testresults/sql_connect.py b/ci-scripts/mysql4testresults/sql_connect.py
new file mode 100644
index 0000000000000000000000000000000000000000..333e573e786a184e8f9650c1a383d24a1629cbd0
--- /dev/null
+++ b/ci-scripts/mysql4testresults/sql_connect.py
@@ -0,0 +1,33 @@
+import pymysql
+import sys
+from datetime import datetime
+
+#This is the script used to write the test results to the mysql DB
+#Called by Jenkins pipeline (jenkinsfile)
+#Must be located in /home/oaicicid/mysql on the database host
+#Usage from Jenkinsfile : 
+#python3 /home/oaicicd/mysql/sql_connect.py ${JOB_NAME} ${params.eNB_MR} ${params.eNB_Branch} ${env.BUILD_ID} ${env.BUILD_URL} ${StatusForDb} ''
+
+class SQLConnect:
+    def __init__(self):
+        self.connection = pymysql.connect(
+                host='172.22.0.2',
+                user='root', 
+                password = 'ucZBc2XRYdvEm59F',
+                db='oaicicd_tests',
+                port=3306
+                )
+
+    def put(self,TEST,MR,BRANCH,BUILD,BUILD_LINK,STATUS):
+        now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+        cur=self.connection.cursor()
+        cur.execute ('INSERT INTO test_results (TEST,MR,BRANCH,BUILD,BUILD_LINK,STATUS,DATE) VALUES (%s,%s,%s,%s,%s,%s,%s);' , (TEST, MR, BRANCH, BUILD, BUILD_LINK, STATUS, now))
+        self.connection.commit()
+        self.connection.close()
+
+
+if __name__ == "__main__":
+    mydb=SQLConnect()
+    mydb.put(sys.argv[1],sys.argv[2],sys.argv[3],sys.argv[4],sys.argv[5],sys.argv[6])
+
+
diff --git a/ci-scripts/ran.py b/ci-scripts/ran.py
index 74f13d031b8d57c3107c1eaccac444359b87e214..27fc2726871b5cd483af8e8df2d9e160478e114c 100644
--- a/ci-scripts/ran.py
+++ b/ci-scripts/ran.py
@@ -738,8 +738,8 @@ class RANManagement():
 		mySSH.command('echo ' + self.eNBPassword + ' | sudo -S mv /tmp/enb_*.pcap .','\$',20)
 		mySSH.command('echo ' + self.eNBPassword + ' | sudo -S mv /tmp/gnb_*.pcap .','\$',20)
 		mySSH.command('echo ' + self.eNBPassword + ' | sudo -S rm -f enb.log.zip', '\$', 5)
-		mySSH.command('echo ' + self.eNBPassword + ' | sudo -S zip enb.log.zip enb*.log core* enb_*record.raw enb_*.pcap gnb_*.pcap enb_*txt physim_*.log *stats.log *monitor.pickle *monitor*.png', '\$', 60)
-		mySSH.command('echo ' + self.eNBPassword + ' | sudo -S rm enb*.log core* enb_*record.raw enb_*.pcap gnb_*.pcap enb_*txt physim_*.log *stats.log *monitor.pickle *monitor*.png', '\$', 5)
+		mySSH.command('echo ' + self.eNBPassword + ' | sudo -S zip enb.log.zip enb*.log core* enb_*record.raw enb_*.pcap gnb_*.pcap enb_*txt physim_*.log *stats.log *monitor.pickle *monitor*.png log/*/*.log log/*/*.pcap', '\$', 60)
+		mySSH.command('echo ' + self.eNBPassword + ' | sudo -S rm enb*.log core* enb_*record.raw enb_*.pcap gnb_*.pcap enb_*txt physim_*.log *stats.log *monitor.pickle *monitor*.png log/*/*.log log/*/*.pcap', '\$', 15)
 		mySSH.close()
 
 	def AnalyzeLogFile_eNB(self, eNBlogFile, HTML):
@@ -800,10 +800,21 @@ class RANManagement():
 		x2ap_pdu = 0
 		#NSA specific log markers
 		nsa_markers ={'SgNBReleaseRequestAcknowledge': [],'FAILURE': [], 'scgFailureInformationNR-r15': [], 'SgNBReleaseRequest': []}
+		nodeB_prefix_found = False
 	
 		line_cnt=0 #log file line counter
 		for line in enb_log_file.readlines():
 			line_cnt+=1
+			# Detection of eNB/gNB from a container log
+			result = re.search('Starting eNB soft modem', str(line))
+			if result is not None:
+				nodeB_prefix_found = True
+				nodeB_prefix = 'e'
+			result = re.search('Starting gNB soft modem', str(line))
+			if result is not None:
+				nodeB_prefix_found = True
+				nodeB_prefix = 'g'
+			result = re.search('Run time:' ,str(line))
 			# Runtime statistics
 			result = re.search('Run time:' ,str(line))
 			if result is not None:
@@ -1037,10 +1048,11 @@ class RANManagement():
 		logging.debug('   File analysis (stdout, stats) completed')
 
 		#post processing depending on the node type
-		if (self.air_interface[self.eNB_instance] == 'lte-softmodem') or (self.air_interface[self.eNB_instance] == 'ocp-enb'):
-			nodeB_prefix = 'e'
-		else:
-			nodeB_prefix = 'g'
+		if not nodeB_prefix_found:
+			if (self.air_interface[self.eNB_instance] == 'lte-softmodem') or (self.air_interface[self.eNB_instance] == 'ocp-enb'):
+				nodeB_prefix = 'e'
+			else:
+				nodeB_prefix = 'g'
 
 		if nodeB_prefix == 'g':
 			if ulschReceiveOK > 0:
diff --git a/ci-scripts/ran_dashboard/Hdashboard.py b/ci-scripts/ran_dashboard/Hdashboard.py
new file mode 100644
index 0000000000000000000000000000000000000000..5e34ae50bf3777f1c7032b2608facd0ef16f6666
--- /dev/null
+++ b/ci-scripts/ran_dashboard/Hdashboard.py
@@ -0,0 +1,401 @@
+#/*
+# * 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
+# */
+#---------------------------------------------------------------------
+# Merge Requests Dashboard for RAN on googleSheet 
+#
+#   Required Python Version
+#     Python 3.x
+#
+#---------------------------------------------------------------------
+
+#-----------------------------------------------------------
+# Import
+#-----------------------------------------------------------
+
+
+#Author Remi
+import boto3
+import shlex
+import subprocess
+import json       #json structures
+import datetime   #now() and date formating
+from datetime import datetime
+import re
+import gitlab
+import yaml
+import os
+import time
+
+
+from sqlconnect import SQLConnect
+
+#-----------------------------------------------------------
+# Class Declaration
+#-----------------------------------------------------------
+
+class Dashboard:
+    def __init__(self): 
+
+
+        #init with data sources : git, yaml config file, test results databases
+        print("Collecting Data")
+        cmd="""curl --silent "https://gitlab.eurecom.fr/api/v4/projects/oai%2Fopenairinterface5g/merge_requests?state=opened&per_page=100" """       
+        self.git = self.__getGitData(cmd) #git data from Gitlab
+        self.tests = self.__loadCfg('ran_dashboard_cfg.yaml') #tests table setup from yaml
+        self.db = self.__loadFromDB() #test results from database
+
+
+    def __loadCfg(self,yaml_file):
+        with open(yaml_file,'r') as f:
+            tests = yaml.load(f)
+        return tests
+
+    def __getGitData(self,cmd):
+        #cmd="""curl --silent "https://gitlab.eurecom.fr/api/v4/projects/oai%2Fopenairinterface5g/merge_requests?state=opened&per_page=100" """
+        process = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE)
+        output = process.stdout.readline()
+        tmp=output.decode("utf-8") 
+        d = json.loads(tmp)
+        return d
+
+    def __loadFromDB(self):
+        mr_list=[] 
+        for x in range(len(self.git)):
+            mr_list.append(str(self.git[x]['iid']))
+        mydb=SQLConnect()
+        for MR in mr_list: 
+            mydb.get(MR)
+        mydb.close_connection()
+        return mydb.data
+
+    def Test_initHTML(self, date):
+        self.f_html.write('<!DOCTYPE html>\n')
+        self.f_html.write('<head>\n')
+        self.f_html.write('<link rel="stylesheet" href="test_styles.css">\n')
+        self.f_html.write('<title>Test Dashboard</title>\n')
+        self.f_html.write('</head>\n')
+        self.f_html.write('<br>\n')
+        self.f_html.write('<br>\n')
+        self.f_html.write('<table>\n')
+        self.f_html.write('<tr>\n')
+        self.f_html.write('<td class="Main">OAI RAN TEST Status Dashboard</td>\n')
+        self.f_html.write('</td>\n')
+        self.f_html.write('<tr>\n')
+        self.f_html.write('<td class="DashLink"> <a href="https://oairandashboard.s3.eu-west-1.amazonaws.com/index.html">Merge Requests Dashboard</a></td>\n')
+        self.f_html.write('</td>\n')
+        self.f_html.write('<tr></tr>\n')
+        self.f_html.write('<tr>\n')
+        self.f_html.write('<td class="Date">Update : '+date+'</td>\n')
+        self.f_html.write('</td>\n')
+        self.f_html.write('</tr>\n')
+        self.f_html.write('</table>\n')
+        self.f_html.write('<br>\n')
+        self.f_html.write('<br>\n')
+
+    def Test_terminateHTML(self):
+        self.f_html.write('</body>\n')
+        self.f_html.write('</html>\n')
+        self.f_html.close()
+
+
+    def MR_initHTML(self,date):
+        self.f_html.write('<!DOCTYPE html>\n')
+        self.f_html.write('<head>\n')
+        self.f_html.write('<link rel="stylesheet" href="mr_styles.css">\n')
+        self.f_html.write('<title>MR Dashboard</title>\n')
+        self.f_html.write('</head>\n')
+        self.f_html.write('<br>\n')
+        self.f_html.write('<br>\n')
+        self.f_html.write('<table>\n')
+        self.f_html.write('<tr>\n')
+        self.f_html.write('<td class="Main">OAI RAN MR Status Dashboard</td>\n')
+        self.f_html.write('</td>\n')
+        self.f_html.write('<tr>\n')
+        self.f_html.write('<td class="DashLink"> <a href="https://oaitestdashboard.s3.eu-west-1.amazonaws.com/index.html">Tests Dashboard</a></td>\n')
+        self.f_html.write('</td>\n')
+        self.f_html.write('<tr></tr>\n')
+        self.f_html.write('<tr>\n')
+        self.f_html.write('<td class="Date">Update : '+date+'</td>\n')
+        self.f_html.write('</td>\n')
+        self.f_html.write('</tr>\n')
+        self.f_html.write('</table>\n')
+        self.f_html.write('<br>\n')
+        self.f_html.write('<br>\n')
+        self.f_html.write('<table class="MR_Table">\n')
+        self.f_html.write('<tr>\n')
+        self.f_html.write('<th class="MR">MR</th>\n')
+        self.f_html.write('<th class="CREATED_AT">Created_At</th>\n')
+        self.f_html.write('<th class="AUTHOR">Author</th>\n')
+        self.f_html.write('<th class="TITLE">Title</th>\n')
+        self.f_html.write('<th class="ASSIGNEE">Assignee</th>\n')
+        self.f_html.write('<th class="REVIEWER">Reviewer</th>\n')
+        self.f_html.write('<th class="CAN_START">CAN START</th>\n')
+        self.f_html.write('<th class="IN_PROGRESS">IN PROGRESS</th>\n')
+        self.f_html.write('<th class="COMPLETED">COMPLETED</th>\n')
+        self.f_html.write('<th class="REVIEW_FORM">Review Form</th>\n')
+        self.f_html.write('<th class="OK_MERGE">OK Merge</th>\n')
+        self.f_html.write('<th class="MERGE_CONFLICTS">Merge Conflicts</th>\n')
+        self.f_html.write('</tr>\n')
+
+    def MR_terminateHTML(self):
+        self.f_html.write('</table> \n')
+        self.f_html.write('</body>\n')
+        self.f_html.write('</html>\n')
+        self.f_html.close()
+
+
+    def MR_rowHTML(self,row):
+        self.f_html.write('<tr>\n')
+        self.f_html.write('<td><a href=\"'+row[0]+'\">'+row[1]+'</a></td>\n')
+        self.f_html.write('<td>'+row[2]+'</td>\n')
+        self.f_html.write('<td>'+row[3]+'</td>\n')
+        self.f_html.write('<td class="title_cell">'+row[4]+'</td>\n')
+        self.f_html.write('<td>'+row[5]+'</td>\n')
+        self.f_html.write('<td>'+row[6]+'</td>\n')
+        if row[7]=='X':
+            self.f_html.write('<td style="background-color: orange;">'+row[7]+'</td>\n')
+        else:
+            self.f_html.write('<td></td>\n')
+        if row[8]=='X':
+            self.f_html.write('<td style="background-color: yellow;">'+row[8]+'</td>\n')
+        else:
+            self.f_html.write('<td></td>\n')
+        if row[9]=='X':
+            self.f_html.write('<td style="background-color: rgb(144, 221, 231);">'+row[9]+'</td>\n')
+        else:
+            self.f_html.write('<td></td>\n')
+        if row[10]=='X':
+            self.f_html.write('<td style="background-color: rgb(58, 236, 58);">'+row[10]+'</td>\n')
+        else:
+            self.f_html.write('<td></td>\n')
+        if row[11]=='X':
+            self.f_html.write('<td style="background-color: rgb(58, 236, 58);">'+row[11]+'</td>\n')
+        else:
+            self.f_html.write('<td></td>\n')
+        if row[12]=='YES':
+            self.f_html.write('<td style="background-color: red;">'+row[12]+'</td>\n')
+        else:
+            self.f_html.write('<td></td>\n')
+        self.f_html.write('</tr>\n')
+
+
+    def Build(self, type, htmlfilename):
+        if type=='MR':
+            self.Build_MR_Table(htmlfilename)
+        elif type=='Tests':
+            self.Build_Test_Table(htmlfilename)
+        else :
+            print("Undefined Dashboard Type, options : MR or Tests")
+
+
+    def Build_Test_Table(self,htmlfilename):
+        print("Building Tests Dashboard...")
+
+        self.f_html=open(htmlfilename,'w')
+
+        ###update date/time, format dd/mm/YY H:M:S
+        now = datetime.now()
+        dt_string = now.strftime("%d/%m/%Y %H:%M")	  
+        #HTML table header
+        self.Test_initHTML(dt_string)
+
+
+        #1 table per MR if test results exist
+        for x in range(len(self.git)):
+            mr=str(self.git[x]['iid'])
+            if 'PASS' not in self.db[mr]:
+                self.f_html.write('<h3><a href="https://gitlab.eurecom.fr/oai/openairinterface5g/-/merge_requests/'+mr+'">'+mr+'</a>'+'   '+self.git[x]['title'] + '</h3>\n')
+                self.f_html.write('<table class="Test_Table">\n')
+                self.f_html.write('<tr>\n')
+                self.f_html.write('<th class="Test_Name">Test Name</th>\n')
+                self.f_html.write('<th class="Test_Descr">Bench</th> \n')  
+                self.f_html.write('<th class="Test_Descr">Test</th> \n')
+                self.f_html.write('<th class="Pass"># Pass</th>\n')
+                self.f_html.write('<th class="Fail"># Fail</th>\n')
+                self.f_html.write('<th class="Last_Pass">Last Pass</th>\n')
+                self.f_html.write('<th class="Last_Fail">Last Fail</th>\n')
+                self.f_html.write('</tr>\n')
+
+                #parsing the tests
+                for t in self.tests:
+
+                    row=[]
+                    short_name= t
+                    hyperlink= self.tests[t]['link']
+                    job=self.tests[t]['job']
+
+
+                    if job in self.db[mr]:
+                        if 'PASS' in self.db[mr][job]:
+                            row.append(self.db[mr][job]['PASS'])
+                        else:
+                            row.append('')
+                        if 'FAIL' in self.db[mr][job]:
+                            row.append(self.db[mr][job]['FAIL'])
+                        else:
+                            row.append('')
+                        #2 columns for last_pass and last_fail links
+                        if 'last_pass' in self.db[mr][job]:
+                            lastpasshyperlink=  self.db[mr][job]['last_pass'][1]
+                            lastpasstext= self.db[mr][job]['last_pass'][0]
+                        else:
+                            lastpasshyperlink=''
+                            lastpasstext=''
+
+                        if 'last_fail' in self.db[mr][job]:
+                            lastfailhyperlink=  self.db[mr][job]['last_fail'][1] 
+                            lastfailtext= self.db[mr][job]['last_fail'][0]
+                        else:
+                            lastfailhyperlink=''
+                            lastfailtext=''
+
+
+
+                        self.f_html.write('<tr>\n')
+                        self.f_html.write('<td><a href='+hyperlink+'>'+short_name+'</a></td>\n')
+                        self.f_html.write('<td>'+self.tests[t]['bench']+'</td>\n')
+                        self.f_html.write('<td>'+self.tests[t]['test']+'</td>\n')
+                        if row[0]!='':
+                            self.f_html.write('<td style="background-color: rgb(58, 236, 58);">'+str(row[0])+'</td>\n')
+                        else:
+                            self.f_html.write('<td></td>\n')
+                        if row[1]!='':
+                            self.f_html.write('<td style="background-color: red;">'+str(row[1])+'</td>\n')
+                        else:
+                            self.f_html.write('<td></td>\n')
+                        self.f_html.write('<td><a href='+lastpasshyperlink+'>'+lastpasstext+'</a></td>\n')
+                        self.f_html.write('<td><a href='+lastfailhyperlink+'>'+lastfailtext+'</a></td>\n')
+                        self.f_html.write('</tr>\n')
+
+                self.f_html.write('</table>\n')
+
+        #terminate HTML table and close file
+        self.Test_terminateHTML()
+
+
+    def Build_MR_Table(self,htmlfilename):
+
+        print("Building Merge Requests Dashboard...")
+
+        self.f_html=open(htmlfilename,'w')
+
+        ###update date/time, format dd/mm/YY H:M:S
+        now = datetime.now()
+        dt_string = now.strftime("%d/%m/%Y %H:%M")	  
+
+        #HTML table header
+        self.MR_initHTML(dt_string)
+
+
+        ###MR data lines
+        for x in range(len(self.git)):
+
+
+            hyperlink= 'https://gitlab.eurecom.fr/oai/openairinterface5g/-/merge_requests/'+ str(self.git[x]['iid'])
+            text= str(self.git[x]['iid'])
+
+                      
+            date_time_str = self.git[x]['created_at']
+            date_time_obj = datetime.strptime(date_time_str, '%Y-%m-%dT%H:%M:%S.%fZ')
+    
+            milestone1=milestone2=milestone3=milestone4=""
+            if self.git[x]['milestone']!=None:
+                if self.git[x]['milestone']['title']=="REVIEW_CAN_START":
+                    milestone1="X"
+                elif self.git[x]['milestone']['title']=="REVIEW_IN_PROGRESS":
+                    milestone2="X"
+                elif self.git[x]['milestone']['title']=="REVIEW_COMPLETED_AND_APPROVED":
+                    milestone3="X"
+                elif self.git[x]['milestone']['title']=="OK_TO_BE_MERGED": 
+                    milestone4="X" 
+                else:
+                    pass
+            else:
+                pass
+
+            #check if empty or not
+            if self.git[x]['assignee']!=None:
+                assignee = str(self.git[x]['assignee']['name'])
+            else:
+                assignee = ""
+ 
+            #check if empty or not       
+            if len(self.git[x]['reviewers'])!=0:
+                reviewer = str(self.git[x]['reviewers'][0]['name'])
+            else:
+                reviewer = ""
+
+            if self.git[x]['has_conflicts']==True:
+                conflicts = "YES"
+            else:
+                conflicts = ""
+
+
+            #add a column flagging that the review form is present
+            #we use gitlab API to parse the MR notes
+            gl = gitlab.Gitlab.from_config('OAI')
+            project_id = 223
+            project = gl.projects.get(project_id)
+            #get the opened MR in the project
+            mrs = project.mergerequests.list(state='opened',per_page=100)
+            review_form=''
+            for m in range (0,len(mrs)):
+                if mrs[m].iid==self.git[x]['iid']:#check the iid is the one we are on
+                    mr_notes = mrs[m].notes.list(all=True)
+                    n=0
+                    found=False
+                    while found==False and n<len(mr_notes):
+                        res=re.search('Code Review by',mr_notes[n].body)#this is the marker we are looking for in all notes
+                        if res!=None:
+                            review_form = "X"
+                            found=True
+                        n+=1
+
+            #build final row to be inserted
+            row =[hyperlink, text, str(date_time_obj.date()),str(self.git[x]['author']['name']), str(self.git[x]['title']),\
+            assignee, reviewer,\
+            milestone1,milestone2,milestone3,review_form,milestone4,conflicts]
+
+            self.MR_rowHTML(row)
+
+        #terminate HTML table and close file
+        self.MR_terminateHTML()
+
+    def CopyToS3(self,htmlfilename,bucket,key):
+        print("Uploading to S3 bucket")
+        #Creating Session With Boto3.
+        s3 = boto3.client('s3')
+
+        #Creating S3 Resource From the Session.
+        result = s3.upload_file(htmlfilename, bucket,key, ExtraArgs={'ACL':'public-read','ContentType': 'text/html'})
+
+def main():
+
+    htmlDash=Dashboard()
+    htmlDash.Build('MR','/tmp/MR_index.html') 
+    htmlDash.CopyToS3('/tmp/MR_index.html','oairandashboard','index.html')  
+    htmlDash.Build('Tests','/tmp/Tests_index.html') 
+    htmlDash.CopyToS3('/tmp/Tests_index.html','oaitestdashboard','index.html') 
+
+        
+if __name__ == "__main__":
+    # execute only if run as a script
+    main()      
diff --git a/ci-scripts/ran_dashboard/mr_styles.css b/ci-scripts/ran_dashboard/mr_styles.css
new file mode 100755
index 0000000000000000000000000000000000000000..5d938669cb2fba9078b3ac9026b4826df8ccb9e2
--- /dev/null
+++ b/ci-scripts/ran_dashboard/mr_styles.css
@@ -0,0 +1,136 @@
+body {
+    font-family: 'lato', sans-serif;
+  }
+
+
+.Main {
+    text-align:left;
+    font-size: 30px;
+    font-weight: bold;   
+}
+
+.DashLink {
+    text-align:left;
+    font-size: 16px;    
+}
+
+.DashLink:hover {
+    font-weight: bold;      
+}
+
+.Date {
+    text-align:left;
+    font-size: 16px;
+    font-weight: bold;      
+}
+
+
+a { text-decoration: none; }
+a:visited { text-decoration: none; color:blue}
+a:hover { text-decoration: none; font-weight: bold; color:blue}
+
+
+.MR_Table {
+    border-collapse: collapse;
+}
+
+.MR_Table tr {
+    font-size: 12px;
+    text-align:center;
+}
+
+
+.MR_Table tr:hover {      
+    background-color:lightgray;
+}
+
+.MR_Table td {
+    border: 1px solid black;
+    padding : 5px;
+}
+.MR_Table td:hover {
+font-weight : bold; 
+}
+
+.MR_Table th {
+    font-size: 14px;
+    text-align:center;
+    padding : 5px;
+}
+
+
+.title_cell {
+    text-align:left;   
+}
+
+
+.MR {
+    table-layout: fixed;
+    width: 50px;
+    background-color: rgb(143, 154, 216);
+}
+
+.CREATED_AT {
+    table-layout: fixed;
+    width: 100px;
+    background-color: rgb(143, 154, 216);
+}
+
+.AUTHOR {
+    table-layout: fixed;
+    width: 150px;
+    background-color: rgb(143, 154, 216);
+}
+
+.TITLE {
+    table-layout: fixed;
+    width: 500px;
+    background-color: rgb(143, 154, 216);
+}
+
+.ASSIGNEE {
+    table-layout: fixed;
+    width: 150px;
+    background-color: rgb(143, 154, 216);
+}
+
+.REVIEWER {
+    table-layout: fixed;
+    width: 150px;
+    background-color: rgb(143, 154, 216);
+}
+
+.CAN_START {
+    background-color: orange;
+    table-layout: fixed;
+    width: 150px;
+}
+.IN_PROGRESS {
+    background-color: yellow;
+    table-layout: fixed;
+    width: 150px;
+}
+.COMPLETED {
+    background-color: rgb(144, 221, 231);
+    table-layout: fixed;
+    width: 150px;
+}
+
+.REVIEW_FORM {
+    table-layout: fixed;
+    width: 100px;
+    background-color: rgb(143, 154, 216);
+}
+
+.OK_MERGE {
+    background-color: rgb(58, 236, 58);
+    table-layout: fixed;
+    width: 150px;
+}
+
+.MERGE_CONFLICTS {
+    table-layout: fixed;
+    width: 100px;
+    background-color: rgb(143, 154, 216);
+}
+
diff --git a/ci-scripts/ran_dashboard.py b/ci-scripts/ran_dashboard/ran_dashboard.py
similarity index 61%
rename from ci-scripts/ran_dashboard.py
rename to ci-scripts/ran_dashboard/ran_dashboard.py
index d9ce62b3b667ceb7257516aa6eb01b964f7b75da..54a98ec878d167b52ddb9b278607df420dc7b6bb 100644
--- a/ci-scripts/ran_dashboard.py
+++ b/ci-scripts/ran_dashboard/ran_dashboard.py
@@ -30,6 +30,8 @@
 # Import
 #-----------------------------------------------------------
 
+#author Remi
+
 #import google spreadsheet API
 import gspread
 from oauth2client.service_account import ServiceAccountCredentials
@@ -42,6 +44,13 @@ import datetime   #now() and date formating
 from datetime import datetime
 import re
 import gitlab
+import yaml
+import os
+import pickle
+import time
+
+
+from sqlconnect import SQLConnect
 
 #-----------------------------------------------------------
 # Class Declaration
@@ -57,48 +66,78 @@ class gDashboard:
         #worksheet
         self.sheet = self.ss.worksheet(worksheet)
         self.ss.del_worksheet(self.sheet) #start by deleting the old sheet
-        self.sheet = self.ss.add_worksheet(title=worksheet, rows="100", cols="20") #create a new one
-        
-        self.d = {} #data dictionary
+        self.sheet = self.ss.add_worksheet(title=worksheet, rows="100", cols="30") #create a new one
 
+        #init with data sources : git, yaml config file, test results databases
+        cmd="""curl --silent "https://gitlab.eurecom.fr/api/v4/projects/oai%2Fopenairinterface5g/merge_requests?state=opened&per_page=100" """       
+        self.git = self.__getGitData(cmd) #git data from Gitlab
+        self.tests = self.__loadCfg('ran_dashboard_cfg.yaml') #tests table setup from yaml
+        self.db = self.__loadFromDB() #test results from database
 
-    def fetchData(self,cmd):
+
+    def __loadCfg(self,yaml_file):
+        with open(yaml_file,'r') as f:
+            tests = yaml.load(f)
+        return tests
+
+    def __getGitData(self,cmd):
         #cmd="""curl --silent "https://gitlab.eurecom.fr/api/v4/projects/oai%2Fopenairinterface5g/merge_requests?state=opened&per_page=100" """
         process = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE)
         output = process.stdout.readline()
         tmp=output.decode("utf-8") 
-        self.d = json.loads(tmp)
+        d = json.loads(tmp)
+        return d
+
+    def __loadFromDB(self):
+        mr_list=[] 
+        for x in range(len(self.git)):
+            mr_list.append(str(self.git[x]['iid']))
+        mydb=SQLConnect()
+        for MR in mr_list: 
+            mydb.get(MR)
+        mydb.close_connection()
+        return mydb.data
+
 
 
     def gBuild(self, destinationSheetName):
 
-        #line 1 : update date/time, format dd/mm/YY H:M:S
+        ###line 1 : update date/time, format dd/mm/YY H:M:S
         now = datetime.now()
         dt_string = "Update : " + now.strftime("%d/%m/%Y %H:%M")	
         row =[dt_string]
         self.sheet.insert_row(row, index=1, value_input_option='RAW')
 
-        #line 2 empty
-        #line 3 is for the column names
+        ###line 2 is for the test short names (links to jenkins pipeline), updated at the end 
+
+        ###line 3 is for the column names
         i=3
-        row =["MR","Created_at","Author","Title","Assignee", "Reviewer", "CAN START","IN PROGRESS","COMPLETED","Review Form","OK MERGE","Merge conflicts"]
+        row =["MR","Created_at","Author","Title","Assignee", "Reviewer", "CAN START","IN PROGRESS","COMPLETED","Review Form","OK MERGE","Merge conflicts",""]
+
+        #tests
+        for t in range(0,len(self.tests)):
+            row.append("# PASS")
+            row.append("# FAIL")
+            row.append("Last Pass")
+            row.append("Last Fail")
+
         self.sheet.insert_row(row, index=i, value_input_option='RAW')
 
-        #line 4 onward, MR data lines
-        for x in range(len(self.d)):
+        ###line 4 onward, MR data lines
+        for x in range(len(self.git)):
             i=i+1                        
-            date_time_str = self.d[x]['created_at']
+            date_time_str = self.git[x]['created_at']
             date_time_obj = datetime.strptime(date_time_str, '%Y-%m-%dT%H:%M:%S.%fZ')
     
             milestone1=milestone2=milestone3=milestone4=""
-            if self.d[x]['milestone']!=None:
-                if self.d[x]['milestone']['title']=="REVIEW_CAN_START":
+            if self.git[x]['milestone']!=None:
+                if self.git[x]['milestone']['title']=="REVIEW_CAN_START":
                     milestone1="X"
-                elif self.d[x]['milestone']['title']=="REVIEW_IN_PROGRESS":
+                elif self.git[x]['milestone']['title']=="REVIEW_IN_PROGRESS":
                     milestone2="X"
-                elif self.d[x]['milestone']['title']=="REVIEW_COMPLETED_AND_APPROVED":
+                elif self.git[x]['milestone']['title']=="REVIEW_COMPLETED_AND_APPROVED":
                     milestone3="X"
-                elif self.d[x]['milestone']['title']=="OK_TO_BE_MERGED": 
+                elif self.git[x]['milestone']['title']=="OK_TO_BE_MERGED": 
                     milestone4="X" 
                 else:
                     pass
@@ -106,18 +145,18 @@ class gDashboard:
                 pass
 
             #check if empty or not
-            if self.d[x]['assignee']!=None:
-                assignee = str(self.d[x]['assignee']['name'])
+            if self.git[x]['assignee']!=None:
+                assignee = str(self.git[x]['assignee']['name'])
             else:
                 assignee = ""
  
             #check if empty or not       
-            if len(self.d[x]['reviewers'])!=0:
-                reviewer = str(self.d[x]['reviewers'][0]['name'])
+            if len(self.git[x]['reviewers'])!=0:
+                reviewer = str(self.git[x]['reviewers'][0]['name'])
             else:
                 reviewer = ""
 
-            if self.d[x]['has_conflicts']==True:
+            if self.git[x]['has_conflicts']==True:
                 conflicts = "YES"
             else:
                 conflicts = ""
@@ -129,13 +168,13 @@ class gDashboard:
             project_id = 223
             project = gl.projects.get(project_id)
             #get the opened MR in the project
-            mrs = project.mergerequests.list(state='opened')
+            mrs = project.mergerequests.list(state='opened',per_page=100)
+            review_form=''
             for m in range (0,len(mrs)):
-                if mrs[m].iid==self.d[x]['iid']:#check the iid is the one we are on
+                if mrs[m].iid==self.git[x]['iid']:#check the iid is the one we are on
                     mr_notes = mrs[m].notes.list(all=True)
                     n=0
                     found=False
-                    review_form=""
                     while found==False and n<len(mr_notes):
                         res=re.search('Code Review by',mr_notes[n].body)#this is the marker we are looking for in all notes
                         if res!=None:
@@ -143,29 +182,81 @@ class gDashboard:
                             found=True
                         n+=1
 
-
             #build final row to be inserted, the first column is left empty for now, will be filled afterward with hyperlinks to gitlab MR
-            row =["", str(date_time_obj.date()),str(self.d[x]['author']['name']),str(self.d[x]['title']),\
+            row =["", str(date_time_obj.date()),str(self.git[x]['author']['name']),	str(self.git[x]['title']),\
             assignee, reviewer,\
-            milestone1,milestone2,milestone3,review_form,milestone4,conflicts]
-            
-            #insert the row to worksheet
+            milestone1,milestone2,milestone3,review_form,milestone4,conflicts,""]
+            #and append the test results coming from self.db 
+            mr=str(self.git[x]['iid'])
+            for t in self.tests:
+                if mr in self.db:
+                    job=self.tests[t]['job']
+                    if job in self.db[mr]:
+                        if 'PASS' in self.db[mr][job]:
+                            row.append(self.db[mr][job]['PASS'])
+                        else:
+                            row.append('')
+                        if 'FAIL' in self.db[mr][job]:
+                            row.append(self.db[mr][job]['FAIL'])
+                        else:
+                            row.append('')
+                        #leave 2 columns for last_pass and last_fail links
+                        row.append('')
+                        row.append('')
+                    else:
+                        #4 columns are empty
+                        row.append('') 
+                        row.append('')
+                        row.append('')
+                        row.append('')
+
+            #insert the final row to worksheet
             self.sheet.insert_row(row, index=i, value_input_option='RAW')
-        
-        
+            time.sleep(10)
+
+
         #add MR hyperlinks in a list of requests to be sent as one update batch; this to save API calls (quotas) 
         i=3
         requests=[]
-        for x in range(len(self.d)):              
+        for x in range(len(self.git)):              
             rowIndex=i
             colIndex=0
-            hyperlink= '\"'+"https://gitlab.eurecom.fr/oai/openairinterface5g/-/merge_requests/"+ str(self.d[x]['iid']) +'\"'
-            text= '\"'+str(self.d[x]['iid'])+'"'
+            hyperlink= '\"'+"https://gitlab.eurecom.fr/oai/openairinterface5g/-/merge_requests/"+ str(self.git[x]['iid']) +'\"'
+            text= '\"'+str(self.git[x]['iid'])+'"'
             requests.append(self.addHyperlink(hyperlink, text, destinationSheetName, rowIndex, colIndex))
-            i=i+1
+
+            mr=str(self.git[x]['iid'])
+            colIndex=15
+            for t in self.tests:
+                job=self.tests[t]['job']
+                if job in self.db[mr]:
+                    if 'last_pass' in self.db[mr][job]:
+                        hyperlink= '\"'+ self.db[mr][job]['last_pass'][1] +'\"'
+                        text= '\"'+self.db[mr][job]['last_pass'][0]+'"'
+                        requests.append(self.addHyperlink(hyperlink, text, destinationSheetName, rowIndex, colIndex))
+                    if 'last_fail' in self.db[mr][job]:
+                        hyperlink= '\"'+ self.db[mr][job]['last_fail'][1] +'\"'
+                        text= '\"'+self.db[mr][job]['last_fail'][0]+'"'
+                        requests.append(self.addHyperlink(hyperlink, text, destinationSheetName, rowIndex, colIndex+1))
+                colIndex+=4 #move to next test
+            i=i+1 #increment row index for next MR
+
         body = {"requests": requests}    
         self.ss.batch_update(body)        
             
+        ###line 2 is for the test names 
+        #add MR hyperlinks in a list of requests to be sent as one update batch; this to save API calls (quotas) 
+        requests=[]
+        rowIndex=1
+        colIndex=13
+        for t in self.tests :
+            hyperlink= '\"'+self.tests[t]['link']+'\"'
+            short_name= '\"'+ t +'\"'
+            requests.append(self.addHyperlink(hyperlink, short_name, destinationSheetName, rowIndex, colIndex))
+            colIndex+=4
+
+        body = {"requests": requests}
+        self.ss.batch_update(body)
 
     
     def addHyperlink(self, hyperlink, text, destinationSheetName, rowIndex, colIndex):
@@ -207,17 +298,17 @@ class gDashboard:
                     "copyPaste": {
                         "source": {
                             "sheetId": sourceSheetId,
-                            "startRowIndex": 0,
+                            "startRowIndex": 1,
                             "endRowIndex": 40,
                             "startColumnIndex": 0,
-                            "endColumnIndex": 12
+                            "endColumnIndex": 30 
                         },
                         "destination": {
                             "sheetId": destinationSheetId,
-                            "startRowIndex": 0,
+                            "startRowIndex": 1,
                             "endRowIndex": 40,
                             "startColumnIndex": 0,
-                            "endColumnIndex": 12
+                            "endColumnIndex": 30
                         },
                         "pasteType": "PASTE_FORMAT"
                     }
@@ -280,15 +371,30 @@ class gDashboard:
                 }
         )
     
-        body = {"requests": requests}    
-        self.ss.batch_update(body)
   
+        #group MR related columns
+#        sheetId = self.ss.worksheet(destinationSheetName)._properties['sheetId']
+#        requests.append(
+#                {
+#                    "addDimensionGroup": {
+#                        "range": {
+#                            "dimension": "COLUMNS",
+#                            "sheetId": sheetId,
+#                            "startIndex": 3,
+#                            "endIndex": 12
+#                        },
+#                    }
+#                }
+#        )
+#
+        body = {"requests": requests}
+        self.ss.batch_update(body)
+
+
 
 
 def main():
     my_gDashboard=gDashboard("/opt/dashboard/g_creds.json", 'OAI RAN Dashboard', 'MR Status')
-    cmd="""curl --silent "https://gitlab.eurecom.fr/api/v4/projects/oai%2Fopenairinterface5g/merge_requests?state=opened&per_page=100" """ 
-    my_gDashboard.fetchData(cmd)
     my_gDashboard.gBuild("MR Status")
     my_gDashboard.gFormat("Formating" , "MR Status")
 
diff --git a/ci-scripts/ran_dashboard/ran_dashboard_cfg.yaml b/ci-scripts/ran_dashboard/ran_dashboard_cfg.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..071b9e2e6747fca041d361623a57c3cea08c3063
--- /dev/null
+++ b/ci-scripts/ran_dashboard/ran_dashboard_cfg.yaml
@@ -0,0 +1,21 @@
+LTE-2x2 : #short name used in the dashboard
+  job : 'RAN-LTE-2x2-Module-OAIEPC' #job name from Jenkins, used in the database
+  link : 'https://jenkins-oai.eurecom.fr/view/RAN/job/RAN-LTE-2x2-Module-OAIEPC'
+  bench : 'Obelix-N310-OAIEPC-Quectel(nrmodule2)'
+  test : 'TDD, 40MHz, MCS9, 26Mb DL, 7Mb UL'
+NSA-B200 :
+  job : 'RAN-NSA-B200-Module-LTEBOX'
+  link : 'https://jenkins-oai.eurecom.fr/view/RAN/job/RAN-NSA-B200-Module-LTEBOX'
+  bench : 'Nepes-B200-Obelix-B200-LTEBOX-Quectel(idefix)'
+  test : '20MHz, 60Mb DL, 3Mb UL'
+NSA-2x2 :
+  job : 'RAN-NSA-2x2-Module-OAIEPC'
+  link : 'https://jenkins-oai.eurecom.fr/view/RAN/job/RAN-NSA-2x2-Module-OAIEPC'
+  bench : 'Asterix-N310-Obelix-N310-OAIEPC-Quectel(nrmodule2)'
+  test : 'TDD, 40MHz, 60Mb DL, 3Mb UL'
+SA-N310 :
+  job : 'RAN-SA-Module-CN5G'
+  link : 'https://jenkins-oai.eurecom.fr/view/RAN/job/RAN-SA-Module-CN5G'
+  bench : 'Asterix-N310-OAICN5G-Quectel(nrmodule2)'
+  test : 'TDD, 40MHz, 60Mb DL, 3Mb UL'
+
diff --git a/ci-scripts/ran_dashboard/readme.txt b/ci-scripts/ran_dashboard/readme.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e72ef78a405878b3df5632f10b6aacc90d635627
--- /dev/null
+++ b/ci-scripts/ran_dashboard/readme.txt
@@ -0,0 +1,3 @@
+The code ran_dashboard.py was initially developped to bring MR status and test resuts to a google sheet, using google sheets API.
+This method is now deprecated, and replaced by Hdashboard.py that builds the results as HTML page and loads it to AWS S3.
+ran_dashboard_cfg.yaml is still used by Hdashboard.py as tests config file.
diff --git a/ci-scripts/ran_dashboard/sqlconnect.py b/ci-scripts/ran_dashboard/sqlconnect.py
new file mode 100644
index 0000000000000000000000000000000000000000..9045c25ca9ac7fd960d5661107fd8661d5e032b7
--- /dev/null
+++ b/ci-scripts/ran_dashboard/sqlconnect.py
@@ -0,0 +1,107 @@
+#/*
+# * 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
+# */
+#---------------------------------------------------------------------
+# Merge Requests Dashboard for RAN on googleSheet 
+#
+#   Required Python Version
+#     Python 3.x
+#
+#---------------------------------------------------------------------
+
+#author Remi
+
+
+import pymysql
+import sys
+from datetime import datetime
+import pickle
+
+#This is the script/package used by the dashboard to retrieve the MR test results from the database
+
+class SQLConnect:
+    def __init__(self):
+        self.connection = pymysql.connect(
+                host='172.22.0.2',
+                user='root', 
+                password = 'ucZBc2XRYdvEm59F',
+                db='oaicicd_tests',
+                port=3306
+                )
+        self.data={} 
+
+
+    #retrieve data from mysql database and organize it in a dictionary (per MR passed as argument)
+    def get(self,MR):
+        self.data[MR]={}
+        cur=self.connection.cursor()
+
+        #get counters per test
+        sql = "select TEST,STATUS, count(*) AS COUNT from test_results where MR=(%s)  group by TEST, STATUS;"
+        cur.execute(sql,MR)
+        response=cur.fetchall()
+        if len(response)==0:#no test results yet
+            self.data[MR]['PASS']=''
+            self.data[MR]['FAIL']=''
+        else:
+            for i in range(0,len(response)):
+                test=response[i][0]
+                status=response[i][1]
+                count=response[i][2]
+                if test in self.data[MR]:
+                    self.data[MR][test][status]=count
+                else:
+                    self.data[MR][test]={}
+                    self.data[MR][test][status]=count
+      
+        #get last failing build and link
+        sql = "select TEST,BUILD, BUILD_LINK from test_results where MR=(%s) and STATUS='FAIL' order by DATE DESC;"
+        cur.execute(sql,MR)
+        response=cur.fetchall()
+        if len(response)!=0:
+            for i in range(0,len(response)):
+                test=response[i][0]
+                build=response[i][1]
+                link=response[i][2]
+                if 'last_fail' not in self.data[MR][test]:
+                    self.data[MR][test]['last_fail']=[]
+                    self.data[MR][test]['last_fail'].append(build)
+                    self.data[MR][test]['last_fail'].append(link)
+
+        #get last passing build and link
+        sql = "select TEST,BUILD, BUILD_LINK from test_results where MR=(%s) and STATUS='PASS' order by DATE DESC;"
+        cur.execute(sql,MR)
+        response=cur.fetchall()
+        if len(response)!=0:
+            for i in range(0,len(response)):
+                test=response[i][0]
+                build=response[i][1]
+                link=response[i][2]
+                if 'last_pass' not in self.data[MR][test]:
+                    self.data[MR][test]['last_pass']=[]
+                    self.data[MR][test]['last_pass'].append(build)
+                    self.data[MR][test]['last_pass'].append(link)
+
+
+    #close database connection
+    def close_connection(self):
+        self.connection.close()
+
+
diff --git a/ci-scripts/ran_dashboard/test_styles.css b/ci-scripts/ran_dashboard/test_styles.css
new file mode 100755
index 0000000000000000000000000000000000000000..a2876e8fa116ea86fcb13927595317f03ca72e85
--- /dev/null
+++ b/ci-scripts/ran_dashboard/test_styles.css
@@ -0,0 +1,103 @@
+body {
+    font-family: 'lato', sans-serif;
+  }
+
+.Main {
+    text-align: left;
+    font-size: 30px;
+    font-weight: bold;
+}
+
+
+.DashLink {
+    text-align:left;
+    font-size: 16px;    
+}
+
+.DashLink:hover {
+    font-weight: bold;      
+}
+
+
+.Date {
+    text-align: left;
+    font-size: 16px;
+    font-weight: bold;
+}
+
+h3 {
+    font-size: 16px;    
+}
+
+a { text-decoration: none; }
+a:visited { text-decoration: none; color:blue}
+a:hover { text-decoration: none; font-weight: bold; color:blue}
+
+
+.Test_Table {
+    border-collapse: collapse;
+}
+
+.Test_Table tr {
+    font-size: 12px;
+    text-align:center;
+}
+
+
+.Test_Table tr:hover {
+    background-color:lightgray;
+}
+
+.Test_Table td {
+    border: 1px solid black;
+    padding : 5px;
+}
+.Test_Table td:hover {
+font-weight : bold;
+}
+
+.Test_Table th {
+    font-size: 14px;
+    text-align:center;
+    padding : 5px;
+}
+
+
+
+
+.Test_Name {
+    table-layout: fixed;
+    width: 100px;
+    background-color: rgb(143, 154, 216);
+}
+
+.Test_Descr {
+    table-layout: fixed;
+    width: 400px;
+    background-color: rgb(143, 154, 216);
+}
+
+.Pass {
+    table-layout: fixed;
+    width: 100px;
+    background-color: rgb(143, 154, 216);
+}
+
+.Fail {
+    table-layout: fixed;
+    width: 100px;
+    background-color: rgb(143, 154, 216);
+}
+
+.Last_Pass {
+    table-layout: fixed;
+    width: 100px;
+    background-color: rgb(143, 154, 216);
+}
+
+.Last_Fail {
+    table-layout: fixed;
+    width: 100px;
+    background-color: rgb(143, 154, 216);
+}
+
diff --git a/ci-scripts/xml_files/container_4g_rfsim.xml b/ci-scripts/xml_files/container_4g_rfsim.xml
index 5430b2b0567ea92703cbf25f967d38cc8a9e3af4..7f13d599b8945efae275790a161b55b4f2595ed7 100644
--- a/ci-scripts/xml_files/container_4g_rfsim.xml
+++ b/ci-scripts/xml_files/container_4g_rfsim.xml
@@ -24,6 +24,7 @@
         <htmlTabRef>rfsim-4glte</htmlTabRef>
         <htmlTabName>Testing 4G LTE RF sim in containers</htmlTabName>
         <htmlTabIcon>wrench</htmlTabIcon>
+        <repeatCount>2</repeatCount>
         <TestCaseRequestedList>
  100011
  000011
diff --git a/ci-scripts/xml_files/container_5g_rfsim.xml b/ci-scripts/xml_files/container_5g_rfsim.xml
index 660cd7cde86090c0739d6c1fa94ba7393c90703e..11f85688ecea2b2e26211169be214413190c49e0 100644
--- a/ci-scripts/xml_files/container_5g_rfsim.xml
+++ b/ci-scripts/xml_files/container_5g_rfsim.xml
@@ -24,6 +24,7 @@
         <htmlTabRef>rfsim-5gnr</htmlTabRef>
         <htmlTabName>Testing 5G NR RF sim in containers</htmlTabName>
         <htmlTabIcon>wrench</htmlTabIcon>
+        <repeatCount>4</repeatCount>
         <TestCaseRequestedList>
  100001
  000001
diff --git a/ci-scripts/xml_files/container_nsa_b200_quectel.xml b/ci-scripts/xml_files/container_nsa_b200_quectel.xml
index 76610c2656200688b1d2b84cec23ccea525e9e5d..cc8312d7a39751e8dd39c21ab034cfc57edcc2b2 100644
--- a/ci-scripts/xml_files/container_nsa_b200_quectel.xml
+++ b/ci-scripts/xml_files/container_nsa_b200_quectel.xml
@@ -115,8 +115,8 @@
 
 	<testCase id="070000">
 		<class>Iperf</class>
-		<desc>iperf (DL/20Mbps/UDP)(60 sec)(single-ue profile)</desc>
-		<iperf_args>-u -b 20M -t 60</iperf_args>
+		<desc>iperf (DL/40Mbps/UDP)(60 sec)(single-ue profile)</desc>
+		<iperf_args>-u -b 40M -t 60</iperf_args>
 		<direction>DL</direction>
 		<id>idefix</id>
 		<iperf_packetloss_threshold>3</iperf_packetloss_threshold>
diff --git a/ci-scripts/xml_files/container_nsa_b200_terminate.xml b/ci-scripts/xml_files/container_nsa_b200_terminate.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4b446988cdde3b0a30f0e990ff8a649bea6c3eb9
--- /dev/null
+++ b/ci-scripts/xml_files/container_nsa_b200_terminate.xml
@@ -0,0 +1,51 @@
+<!--
+
+ 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-B200-terminate</htmlTabRef>
+	<htmlTabName>NSA tear-down in case of problem</htmlTabName>
+	<htmlTabIcon>tasks</htmlTabIcon>
+	<repeatCount>1</repeatCount>
+	<TestCaseRequestedList>
+ 030202
+ 030201
+	</TestCaseRequestedList>
+	<TestCaseExclusionList></TestCaseExclusionList>
+
+    <testCase id="030201">
+        <class>Undeploy_Object</class>
+        <desc>Undeploy eNB</desc>
+		<yaml_path>ci-scripts/yaml_files/nsa_b200_enb</yaml_path>
+        <eNB_instance>0</eNB_instance>
+        <eNB_serverId>0</eNB_serverId>
+    </testCase>
+
+    <testCase id="030202">
+        <class>Undeploy_Object</class>
+        <desc>Undeploy gNB</desc>
+		<yaml_path>ci-scripts/yaml_files/nsa_b200_gnb</yaml_path>
+        <eNB_instance>1</eNB_instance>
+        <eNB_serverId>1</eNB_serverId>
+    </testCase>
+
+</testCaseList>
+
diff --git a/ci-scripts/xml_files/fr1_sa_quectel.xml b/ci-scripts/xml_files/fr1_sa_quectel.xml
index f4473710cfbc8fe2f963e045fb4415580403d7e2..ff877287aa64bac1891cb0bd359250f5c2393ea8 100644
--- a/ci-scripts/xml_files/fr1_sa_quectel.xml
+++ b/ci-scripts/xml_files/fr1_sa_quectel.xml
@@ -21,7 +21,7 @@
 
 -->
 <testCaseList>
-	<htmlTabRef>TEST-SA-FR1-TM1</htmlTabRef>
+	<htmlTabRef>TEST-SA-FR1-Tab1</htmlTabRef>
 	<htmlTabName>SA Ping DL UL with QUECTEL</htmlTabName>
 	<htmlTabIcon>tasks</htmlTabIcon>
 	<repeatCount>1</repeatCount>
diff --git a/ci-scripts/xml_files/fr1_sa_quectel_stages.xml b/ci-scripts/xml_files/fr1_sa_quectel_stages.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b2236dd5276251358939a14be7674880ff81a3ac
--- /dev/null
+++ b/ci-scripts/xml_files/fr1_sa_quectel_stages.xml
@@ -0,0 +1,156 @@
+<!--
+
+ 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-SA-FR1-Tab2</htmlTabRef>
+	<htmlTabName>SA Staged DL with QUECTEL</htmlTabName>
+	<htmlTabIcon>tasks</htmlTabIcon>
+	<repeatCount>1</repeatCount>
+	<TestCaseRequestedList>
+ 040000
+ 000002
+ 010000
+ 000001
+ 050000
+ 000001
+ 070000
+ 000001
+ 070001
+ 000001
+ 070002
+ 000001
+ 070003
+ 000001
+ 070004
+ 000001
+ 010002
+ 080000
+	</TestCaseRequestedList>
+	<TestCaseExclusionList></TestCaseExclusionList>
+
+	<testCase id="010000">
+		<class>Initialize_UE</class>
+		<desc>Initialize Quectel</desc>
+		<id>nrmodule2_quectel</id>
+		<UE_Trace>yes</UE_Trace>
+	</testCase>
+
+
+	<testCase id="010002">
+		<class>Terminate_UE</class>
+		<desc>Terminate Quectel</desc>
+		<id>nrmodule2_quectel</id>
+	</testCase>
+
+
+	<testCase id="040000">
+		<class>Initialize_eNB</class>
+		<desc>Initialize gNB</desc>
+		<Initialize_eNB_args>-O ci-scripts/conf_files/gnb.band78.sa.fr1.106PRB.2x2.usrpn310.conf --sa -q --usrp-tx-thread-config 1 --thread-pool 0,2,4,6</Initialize_eNB_args>
+		<eNB_instance>0</eNB_instance>
+		<eNB_serverId>0</eNB_serverId>
+		<air_interface>nr</air_interface>
+		<eNB_Trace>yes</eNB_Trace>
+		<eNB_Stats>yes</eNB_Stats>
+		<USRP_IPAddress>192.168.18.240</USRP_IPAddress>
+	</testCase>
+
+	<testCase id="000001">
+		<class>IdleSleep</class>
+		<desc>Sleep</desc>
+		<idle_sleep_time_in_sec>5</idle_sleep_time_in_sec>
+	</testCase>
+
+	<testCase id="000002">
+		<class>IdleSleep</class>
+		<desc>Sleep</desc>
+		<idle_sleep_time_in_sec>20</idle_sleep_time_in_sec>
+	</testCase>
+
+
+	<testCase id="050000">
+		<class>Ping</class>
+		<desc>Ping: 20pings in 20sec</desc>
+		<id>nrmodule2_quectel</id>
+		<ping_args>-c 20</ping_args>
+		<ping_packetloss_threshold>5</ping_packetloss_threshold>
+	</testCase>
+
+
+	<testCase id="070000">
+		<class>Iperf</class>
+		<desc>iperf (DL/10Mbps/UDP)(30 sec)(single-ue profile)</desc>
+		<iperf_args>-u -b 10M -t 30</iperf_args>
+		<direction>DL</direction>
+		<id>nrmodule2_quectel</id>
+		<iperf_packetloss_threshold>5</iperf_packetloss_threshold>
+		<iperf_profile>single-ue</iperf_profile>
+	</testCase>
+	<testCase id="070001">
+		<class>Iperf</class>
+		<desc>iperf (DL/20Mbps/UDP)(30 sec)(single-ue profile)</desc>
+		<iperf_args>-u -b 20M -t 30</iperf_args>
+		<direction>DL</direction>
+		<id>nrmodule2_quectel</id>
+		<iperf_packetloss_threshold>5</iperf_packetloss_threshold>
+		<iperf_profile>single-ue</iperf_profile>
+	</testCase>
+	<testCase id="070002">
+		<class>Iperf</class>
+		<desc>iperf (DL/40Mbps/UDP)(30 sec)(single-ue profile)</desc>
+		<iperf_args>-u -b 40M -t 30</iperf_args>
+		<direction>DL</direction>
+		<id>nrmodule2_quectel</id>
+		<iperf_packetloss_threshold>5</iperf_packetloss_threshold>
+		<iperf_profile>single-ue</iperf_profile>
+	</testCase>
+	<testCase id="070003">
+		<class>Iperf</class>
+		<desc>iperf (DL/60Mbps/UDP)(30 sec)(single-ue profile)</desc>
+		<iperf_args>-u -b 60M -t 30</iperf_args>
+		<direction>DL</direction>
+		<id>nrmodule2_quectel</id>
+		<iperf_packetloss_threshold>5</iperf_packetloss_threshold>
+		<iperf_profile>single-ue</iperf_profile>
+	</testCase>
+	<testCase id="070004">
+		<class>Iperf</class>
+		<desc>iperf (DL/90Mbps/UDP)(30 sec)(single-ue profile)</desc>
+		<iperf_args>-u -b 90M -t 30</iperf_args>
+		<direction>DL</direction>
+		<id>nrmodule2_quectel</id>
+		<iperf_packetloss_threshold>5</iperf_packetloss_threshold>
+		<iperf_profile>single-ue</iperf_profile>
+	</testCase>
+
+
+
+	<testCase id="080000">
+		<class>Terminate_eNB</class>
+		<desc>Terminate gNB</desc>
+		<eNB_instance>0</eNB_instance>
+		<eNB_serverId>0</eNB_serverId>
+		<air_interface>nr</air_interface>
+	</testCase>
+
+</testCaseList>
+
diff --git a/ci-scripts/yaml_files/nsa_b200_enb/docker-compose.yml b/ci-scripts/yaml_files/nsa_b200_enb/docker-compose.yml
index 60e82de50a3016f853cbb7e3e755bafa57a461e3..9bd08aeabca231990a90be98401ed3b619435da1 100644
--- a/ci-scripts/yaml_files/nsa_b200_enb/docker-compose.yml
+++ b/ci-scripts/yaml_files/nsa_b200_enb/docker-compose.yml
@@ -30,7 +30,7 @@ services:
             FLEXRAN_ENABLED: 'no'
             FLEXRAN_INTERFACE_NAME: eth0
             FLEXRAN_IPV4_ADDRESS: 192.168.18.210
-            THREAD_PARALLEL_CONFIG: PARALLEL_RU_L1_TRX_SPLIT
+            THREAD_PARALLEL_CONFIG: PARALLEL_SINGLE_THREAD
         volumes:
             - /dev:/dev
         networks:
diff --git a/docker/scripts/enb_entrypoint.sh b/docker/scripts/enb_entrypoint.sh
index ff92e3c86f817932cb55f0a1e25843871ab8119e..c71a2cf12f5c8cea28d9865ad1cc8cca94434329 100755
--- a/docker/scripts/enb_entrypoint.sh
+++ b/docker/scripts/enb_entrypoint.sh
@@ -9,15 +9,15 @@ ENABLE_X2=${ENABLE_X2:-no}
 THREAD_PARALLEL_CONFIG=${THREAD_PARALLEL_CONFIG:-PARALLEL_SINGLE_THREAD}
 
 # Based another env var, pick one template to use
-if [[ -v USE_FDD_CU ]]; then ln -s $PREFIX/etc/cu.fdd.conf $PREFIX/etc/enb.conf; fi
-if [[ -v USE_FDD_DU ]]; then ln -s $PREFIX/etc/du.fdd.conf $PREFIX/etc/enb.conf; fi
-if [[ -v USE_FDD_MONO ]]; then ln -s $PREFIX/etc/enb.fdd.conf $PREFIX/etc/enb.conf; fi
-if [[ -v USE_TDD_MONO ]]; then ln -s $PREFIX/etc/enb.tdd.conf $PREFIX/etc/enb.conf; fi
-if [[ -v USE_FDD_FAPI_RCC ]]; then ln -s $PREFIX/etc/rcc.nfapi.fdd.conf $PREFIX/etc/enb.conf; fi
-if [[ -v USE_FDD_IF4P5_RCC ]]; then ln -s $PREFIX/etc/rcc.if4p5.fdd.conf $PREFIX/etc/enb.conf; fi
-if [[ -v USE_TDD_IF4P5_RCC ]]; then ln -s $PREFIX/etc/rcc.if4p5.tdd.conf $PREFIX/etc/enb.conf; fi
-if [[ -v USE_FDD_RRU ]]; then ln -s $PREFIX/etc/rru.fdd.conf $PREFIX/etc/enb.conf; fi
-if [[ -v USE_TDD_RRU ]]; then ln -s $PREFIX/etc/rru.tdd.conf $PREFIX/etc/enb.conf; fi
+if [[ -v USE_FDD_CU ]]; then cp $PREFIX/etc/cu.fdd.conf $PREFIX/etc/enb.conf; fi
+if [[ -v USE_FDD_DU ]]; then cp $PREFIX/etc/du.fdd.conf $PREFIX/etc/enb.conf; fi
+if [[ -v USE_FDD_MONO ]]; then cp $PREFIX/etc/enb.fdd.conf $PREFIX/etc/enb.conf; fi
+if [[ -v USE_TDD_MONO ]]; then cp $PREFIX/etc/enb.tdd.conf $PREFIX/etc/enb.conf; fi
+if [[ -v USE_FDD_FAPI_RCC ]]; then cp $PREFIX/etc/rcc.nfapi.fdd.conf $PREFIX/etc/enb.conf; fi
+if [[ -v USE_FDD_IF4P5_RCC ]]; then cp $PREFIX/etc/rcc.if4p5.fdd.conf $PREFIX/etc/enb.conf; fi
+if [[ -v USE_TDD_IF4P5_RCC ]]; then cp $PREFIX/etc/rcc.if4p5.tdd.conf $PREFIX/etc/enb.conf; fi
+if [[ -v USE_FDD_RRU ]]; then cp $PREFIX/etc/rru.fdd.conf $PREFIX/etc/enb.conf; fi
+if [[ -v USE_TDD_RRU ]]; then cp $PREFIX/etc/rru.tdd.conf $PREFIX/etc/enb.conf; fi
 
 # Only this template will be manipulated
 CONFIG_FILES=`ls $PREFIX/etc/enb.conf || true`
diff --git a/docker/scripts/gnb_entrypoint.sh b/docker/scripts/gnb_entrypoint.sh
index 05b42d76bbbe0bcbcb056152e5563eed72b6f4b4..d2bdc636c95972e1a17ab93a8636459b83ad03d3 100755
--- a/docker/scripts/gnb_entrypoint.sh
+++ b/docker/scripts/gnb_entrypoint.sh
@@ -7,8 +7,8 @@ ENABLE_X2=${ENABLE_X2:-yes}
 THREAD_PARALLEL_CONFIG=${THREAD_PARALLEL_CONFIG:-PARALLEL_SINGLE_THREAD}
 
 # Based another env var, pick one template to use
-if [[ -v USE_NSA_TDD_MONO ]]; then ln -s $PREFIX/etc/gnb.nsa.tdd.conf $PREFIX/etc/gnb.conf; fi
-if [[ -v USE_SA_TDD_MONO ]]; then ln -s $PREFIX/etc/gnb.sa.tdd.conf $PREFIX/etc/gnb.conf; fi
+if [[ -v USE_NSA_TDD_MONO ]]; then cp $PREFIX/etc/gnb.nsa.tdd.conf $PREFIX/etc/gnb.conf; fi
+if [[ -v USE_SA_TDD_MONO ]]; then cp $PREFIX/etc/gnb.sa.tdd.conf $PREFIX/etc/gnb.conf; fi
 
 # Only this template will be manipulated
 CONFIG_FILES=`ls $PREFIX/etc/gnb.conf || true`
diff --git a/docker/scripts/lte_ru_entrypoint.sh b/docker/scripts/lte_ru_entrypoint.sh
index 93122b0d029ca94ff824fc52daa563042785ef11..297e437ac08a7acabd03c25864b2fb7f5b885055 100755
--- a/docker/scripts/lte_ru_entrypoint.sh
+++ b/docker/scripts/lte_ru_entrypoint.sh
@@ -5,8 +5,8 @@ set -euo pipefail
 PREFIX=/opt/oai-lte-ru
 
 # Based another env var, pick one template to use
-if [[ -v USE_FDD_RRU ]]; then ln -s $PREFIX/etc/rru.fdd.conf $PREFIX/etc/rru.conf; fi
-if [[ -v USE_TDD_RRU ]]; then ln -s $PREFIX/etc/rru.tdd.conf $PREFIX/etc/rru.conf; fi
+if [[ -v USE_FDD_RRU ]]; then cp $PREFIX/etc/rru.fdd.conf $PREFIX/etc/rru.conf; fi
+if [[ -v USE_TDD_RRU ]]; then cp $PREFIX/etc/rru.tdd.conf $PREFIX/etc/rru.conf; fi
 
 # Only this template will be manipulated
 CONFIG_FILES=`ls $PREFIX/etc/rru.conf || true`
diff --git a/docker/scripts/lte_ue_entrypoint.sh b/docker/scripts/lte_ue_entrypoint.sh
index de7ae8b496e70b24597f4e1ceb9d48e677d7f35b..82e96dd07ff14e6909903b0f6ee64a1ed36bf68f 100755
--- a/docker/scripts/lte_ue_entrypoint.sh
+++ b/docker/scripts/lte_ue_entrypoint.sh
@@ -5,7 +5,7 @@ set -euo pipefail
 PREFIX=/opt/oai-lte-ue
 
 # Based another env var, pick one template to use
-if [[ -v USE_NFAPI ]]; then ln -s $PREFIX/etc/ue.nfapi.conf $PREFIX/etc/ue.conf; fi
+if [[ -v USE_NFAPI ]]; then cp $PREFIX/etc/ue.nfapi.conf $PREFIX/etc/ue.conf; fi
 
 # Only this template will be manipulated and the USIM one!
 CONFIG_FILES=`ls $PREFIX/etc/ue.conf $PREFIX/etc/ue_usim.conf || true`
diff --git a/docker/scripts/nr_ue_entrypoint.sh b/docker/scripts/nr_ue_entrypoint.sh
index 91d25c9e1eec3cc71db9424a87d1218c3acc7176..af2b8a83333d140aefdbaae0527322d7af2de269 100755
--- a/docker/scripts/nr_ue_entrypoint.sh
+++ b/docker/scripts/nr_ue_entrypoint.sh
@@ -5,7 +5,7 @@ set -euo pipefail
 PREFIX=/opt/oai-nr-ue
 
 # Based another env var, pick one template to use
-#if [[ -v USE_NFAPI ]]; then ln -s $PREFIX/etc/ue.nfapi.conf $PREFIX/etc/ue.conf; fi
+#if [[ -v USE_NFAPI ]]; then cp $PREFIX/etc/ue.nfapi.conf $PREFIX/etc/ue.conf; fi
 
 # Only this template will be manipulated and the USIM one!
 CONFIG_FILES=`ls $PREFIX/etc/ue.conf $PREFIX/etc/nr-ue-sim.conf || true`
diff --git a/nfapi/open-nFAPI/nfapi/public_inc/fapi_nr_ue_interface.h b/nfapi/open-nFAPI/nfapi/public_inc/fapi_nr_ue_interface.h
index e1bb2579eb056cfb686307bea53ddf7a893890ba..0e923931ad4163fd8e4d3f26f31a1ff9d248da4d 100644
--- a/nfapi/open-nFAPI/nfapi/public_inc/fapi_nr_ue_interface.h
+++ b/nfapi/open-nFAPI/nfapi/public_inc/fapi_nr_ue_interface.h
@@ -16,6 +16,7 @@
 
 #ifndef _FAPI_NR_UE_INTERFACE_H_
 #define _FAPI_NR_UE_INTERFACE_H_
+#include <pthread.h>
 
 #include "stddef.h"
 #include "platform_types.h"
@@ -349,6 +350,7 @@ typedef struct {
   uint16_t slot;
   uint8_t number_pdus;
   fapi_nr_ul_config_request_pdu_t ul_config_list[FAPI_NR_UL_CONFIG_LIST_NUM];
+  pthread_mutex_t mutex_ul_config;
 } fapi_nr_ul_config_request_t;
 
 
diff --git a/openair1/SCHED_NR_UE/fapi_nr_ue_l1.c b/openair1/SCHED_NR_UE/fapi_nr_ue_l1.c
index 697f5b19d10f3b5e8effa4958a8ba5dfa854f80d..f6e4d90beed5925749ae8b1fc32a5eece712e296 100644
--- a/openair1/SCHED_NR_UE/fapi_nr_ue_l1.c
+++ b/openair1/SCHED_NR_UE/fapi_nr_ue_l1.c
@@ -153,6 +153,7 @@ int8_t nr_ue_scheduled_response(nr_scheduled_response_t *scheduled_response){
 
       fapi_nr_ul_config_request_t *ul_config = scheduled_response->ul_config;
 
+      pthread_mutex_lock(&ul_config->mutex_ul_config);
       for (i = 0; i < ul_config->number_pdus; ++i){
 
         AssertFatal(ul_config->ul_config_list[i].pdu_type <= FAPI_NR_UL_CONFIG_TYPES,"pdu_type %d out of bounds\n",ul_config->ul_config_list[i].pdu_type);
@@ -174,6 +175,7 @@ int8_t nr_ue_scheduled_response(nr_scheduled_response_t *scheduled_response){
           pusch_config_pdu = &ul_config->ul_config_list[i].pusch_config_pdu;
           current_harq_pid = pusch_config_pdu->pusch_data.harq_process_id;
           NR_UL_UE_HARQ_t *harq_process_ul_ue = ulsch0->harq_processes[current_harq_pid];
+          harq_process_ul_ue->status = 0;
 
           if (harq_process_ul_ue){
 
@@ -183,14 +185,16 @@ int8_t nr_ue_scheduled_response(nr_scheduled_response_t *scheduled_response){
 
             ulsch0->f_pusch = pusch_config_pdu->absolute_delta_PUSCH;
 
-            if (scheduled_response->tx_request){ 
-              fapi_nr_tx_request_body_t *tx_req_body = &scheduled_response->tx_request->tx_request_body[i];
-              LOG_D(PHY,"%d.%d Copying %d bytes to harq_process_ul_ue->a (harq_pid %d)\n",scheduled_response->frame,slot,tx_req_body->pdu_length,current_harq_pid);
-              memcpy(harq_process_ul_ue->a, tx_req_body->pdu, tx_req_body->pdu_length);
-
-              harq_process_ul_ue->status = ACTIVE;
-
-              scheduled_response->tx_request->number_of_pdus = 0;
+            if (scheduled_response->tx_request) {
+              for (int j=0; j<scheduled_response->tx_request->number_of_pdus; j++) {
+                fapi_nr_tx_request_body_t *tx_req_body = &scheduled_response->tx_request->tx_request_body[j];
+                if (tx_req_body->pdu_index == i) {
+                  LOG_D(PHY,"%d.%d Copying %d bytes to harq_process_ul_ue->a (harq_pid %d)\n",scheduled_response->frame,slot,tx_req_body->pdu_length,current_harq_pid);
+                  memcpy(harq_process_ul_ue->a, tx_req_body->pdu, tx_req_body->pdu_length);
+                  harq_process_ul_ue->status = ACTIVE;
+                  break;
+                }
+              }
             }
 
           } else {
@@ -228,7 +232,13 @@ int8_t nr_ue_scheduled_response(nr_scheduled_response_t *scheduled_response){
         break;
         }
       }
-      memset(ul_config, 0, sizeof(fapi_nr_ul_config_request_t));
+      if (scheduled_response->tx_request)
+        scheduled_response->tx_request->number_of_pdus = 0;
+      ul_config->sfn = 0;
+      ul_config->slot = 0;
+      ul_config->number_pdus = 0;
+      memset(ul_config->ul_config_list, 0, sizeof(ul_config->ul_config_list));
+      pthread_mutex_unlock(&ul_config->mutex_ul_config);
     }
   }
   return 0;
diff --git a/openair2/LAYER2/NR_MAC_UE/config_ue.c b/openair2/LAYER2/NR_MAC_UE/config_ue.c
index 4408a96eef33586018df1fa0b5610bf8bf1ff87c..f67a3e1fa52424c96c97f1e007febebb953d1d6c 100644
--- a/openair2/LAYER2/NR_MAC_UE/config_ue.c
+++ b/openair2/LAYER2/NR_MAC_UE/config_ue.c
@@ -660,6 +660,8 @@ int nr_rrc_mac_config_req_ue(
       if (mac->scc_SIB->tdd_UL_DL_ConfigurationCommon->pattern1.nrofUplinkSymbols>0) num_slots_ul++;
       LOG_I(MAC, "Initializing ul_config_request. num_slots_ul = %d\n", num_slots_ul);
       mac->ul_config_request = (fapi_nr_ul_config_request_t *)calloc(num_slots_ul, sizeof(fapi_nr_ul_config_request_t));
+      for (int i=0; i<num_slots_ul; i++)
+        pthread_mutex_init(&(mac->ul_config_request[i].mutex_ul_config), NULL);
       // Setup the SSB to Rach Occasions mapping according to the config
       build_ssb_to_ro_map(mac);//->scc, mac->phy_config.config_req.cell_config.frame_duplex_type);
       mac->if_module->phy_config_request(&mac->phy_config);
diff --git a/openair2/LAYER2/NR_MAC_UE/main_ue_nr.c b/openair2/LAYER2/NR_MAC_UE/main_ue_nr.c
index af0c84dc4b67056e1fb087966f8bf28526aaa380..73b5a35b623ffa45fa88477b15edd3ab5ad2810c 100644
--- a/openair2/LAYER2/NR_MAC_UE/main_ue_nr.c
+++ b/openair2/LAYER2/NR_MAC_UE/main_ue_nr.c
@@ -40,6 +40,7 @@
 #include "openair2/LAYER2/nr_pdcp/nr_pdcp_entity.h"
 #include "executables/softmodem-common.h"
 #include "openair2/LAYER2/nr_pdcp/nr_pdcp.h"
+#include <pthread.h>
 
 static NR_UE_MAC_INST_t *nr_ue_mac_inst; 
 
@@ -74,6 +75,8 @@ NR_UE_MAC_INST_t * nr_l2_init_ue(NR_UE_RRC_INST_t* rrc_inst) {
           num_slots_ul++;
         LOG_D(MAC, "Initializing ul_config_request. num_slots_ul = %d\n", num_slots_ul);
         nr_ue_mac_inst->ul_config_request = (fapi_nr_ul_config_request_t *)calloc(num_slots_ul, sizeof(fapi_nr_ul_config_request_t));
+        for (int i=0; i<num_slots_ul; i++)
+          pthread_mutex_init(&(nr_ue_mac_inst->ul_config_request[i].mutex_ul_config), NULL);
       }
     }
     else {
diff --git a/openair2/LAYER2/NR_MAC_UE/nr_ue_procedures.c b/openair2/LAYER2/NR_MAC_UE/nr_ue_procedures.c
index d555014bc8fa38a763d840a1e305007f0d38020e..0f0a9a6a4b132b64e94e9fd9ac75869e11617199 100644
--- a/openair2/LAYER2/NR_MAC_UE/nr_ue_procedures.c
+++ b/openair2/LAYER2/NR_MAC_UE/nr_ue_procedures.c
@@ -668,15 +668,15 @@ int8_t nr_ue_process_dci(module_id_t module_id, int cc_id, uint8_t gNB_index, fr
         LOG_W(MAC, "In %s: ul_config request is NULL. Probably due to unexpected UL DCI in frame.slot %d.%d. Ignoring DCI!\n", __FUNCTION__, frame, slot);
         return -1;
       }
-
+      pthread_mutex_lock(&ul_config->mutex_ul_config);
       AssertFatal(ul_config->number_pdus<FAPI_NR_UL_CONFIG_LIST_NUM, "ul_config->number_pdus %d out of bounds\n",ul_config->number_pdus);
       nfapi_nr_ue_pusch_pdu_t *pusch_config_pdu = &ul_config->ul_config_list[ul_config->number_pdus].pusch_config_pdu;
 
       fill_ul_config(ul_config, frame_tx, slot_tx, FAPI_NR_UL_CONFIG_TYPE_PUSCH);
+      pthread_mutex_unlock(&ul_config->mutex_ul_config);
 
       // Config PUSCH PDU
       ret = nr_config_pusch_pdu(mac, pusch_config_pdu, dci, NULL, rnti, &dci_format);
-
     }
     
     break;
@@ -729,13 +729,15 @@ int8_t nr_ue_process_dci(module_id_t module_id, int cc_id, uint8_t gNB_index, fr
         return -1;
       }
 
+      pthread_mutex_lock(&ul_config->mutex_ul_config);
+      AssertFatal(ul_config->number_pdus<FAPI_NR_UL_CONFIG_LIST_NUM, "ul_config->number_pdus %d out of bounds\n",ul_config->number_pdus);
       nfapi_nr_ue_pusch_pdu_t *pusch_config_pdu = &ul_config->ul_config_list[ul_config->number_pdus].pusch_config_pdu;
 
       fill_ul_config(ul_config, frame_tx, slot_tx, FAPI_NR_UL_CONFIG_TYPE_PUSCH);
+      pthread_mutex_unlock(&ul_config->mutex_ul_config);
 
       // Config PUSCH PDU
       ret = nr_config_pusch_pdu(mac, pusch_config_pdu, dci, NULL, rnti, &dci_format);
-
     } else AssertFatal(1==0,"Cannot schedule PUSCH\n");
     break;
   }
@@ -3921,13 +3923,15 @@ int nr_ue_process_rar(nr_downlink_indication_t *dl_info, NR_UL_TIME_ALIGNMENT_t
         rnti = ra->t_crnti;
       }
 
+      pthread_mutex_lock(&ul_config->mutex_ul_config);
+      AssertFatal(ul_config->number_pdus<FAPI_NR_UL_CONFIG_LIST_NUM, "ul_config->number_pdus %d out of bounds\n",ul_config->number_pdus);
       nfapi_nr_ue_pusch_pdu_t *pusch_config_pdu = &ul_config->ul_config_list[ul_config->number_pdus].pusch_config_pdu;
 
       fill_ul_config(ul_config, frame_tx, slot_tx, FAPI_NR_UL_CONFIG_TYPE_PUSCH);
+      pthread_mutex_unlock(&ul_config->mutex_ul_config);
 
       // Config Msg3 PDU
       nr_config_pusch_pdu(mac, pusch_config_pdu, NULL, &rar_grant, rnti, NULL);
-
     }
 
   } else {
diff --git a/openair2/LAYER2/NR_MAC_UE/nr_ue_scheduler.c b/openair2/LAYER2/NR_MAC_UE/nr_ue_scheduler.c
index cfa8b88f321fe45d49e076993f5260934d77e250..a782dd8fe40aa2ffa7a4fbcbb97b788bc3a18eab 100644
--- a/openair2/LAYER2/NR_MAC_UE/nr_ue_scheduler.c
+++ b/openair2/LAYER2/NR_MAC_UE/nr_ue_scheduler.c
@@ -30,6 +30,7 @@
 
 #include <stdio.h>
 #include <math.h>
+#include <pthread.h>
 
 /* exe */
 #include <common/utils/nr/nr_common.h>
@@ -59,7 +60,6 @@ static prach_association_pattern_t prach_assoc_pattern;
 static ssb_list_info_t ssb_list;
 
 void fill_ul_config(fapi_nr_ul_config_request_t *ul_config, frame_t frame_tx, int slot_tx, uint8_t pdu_type){
-
   ul_config->ul_config_list[ul_config->number_pdus].pdu_type = pdu_type;
   ul_config->slot = slot_tx;
   ul_config->sfn = frame_tx;
@@ -945,77 +945,88 @@ NR_UE_L2_STATE_t nr_ue_scheduler(nr_downlink_indication_t *dl_info, nr_uplink_in
 
     // Schedule ULSCH only if the current frame and slot match those in ul_config_req
     // AND if a UL grant (UL DCI or Msg3) has been received (as indicated by num_pdus)
-    if (ul_config && (ul_info->slot_tx == ul_config->slot && ul_info->frame_tx == ul_config->sfn) && ul_config->number_pdus > 0){
-
-      LOG_D(NR_MAC, "In %s:[%d.%d]: number of UL PDUs: %d with UL transmission in [%d.%d]\n", __FUNCTION__, frame_tx, slot_tx, ul_config->number_pdus, ul_config->sfn, ul_config->slot);
-
-      uint8_t ulsch_input_buffer[MAX_ULSCH_PAYLOAD_BYTES];
-      nr_scheduled_response_t scheduled_response;
-      fapi_nr_tx_request_t tx_req;
-
-      for (int j = 0; j < ul_config->number_pdus; j++) {
+    if (ul_config){
+      pthread_mutex_lock(&ul_config->mutex_ul_config);
+      if ((ul_info->slot_tx == ul_config->slot && ul_info->frame_tx == ul_config->sfn) && ul_config->number_pdus > 0){
+
+        LOG_D(NR_MAC, "In %s:[%d.%d]: number of UL PDUs: %d with UL transmission in [%d.%d]\n", __FUNCTION__, frame_tx, slot_tx, ul_config->number_pdus, ul_config->sfn, ul_config->slot);
+
+        uint8_t ulsch_input_buffer_array[NFAPI_MAX_NUM_UL_PDU][MAX_ULSCH_PAYLOAD_BYTES];
+        nr_scheduled_response_t scheduled_response;
+        fapi_nr_tx_request_t tx_req;
+        tx_req.slot = slot_tx;
+        tx_req.sfn = frame_tx;
+        tx_req.number_of_pdus = 0;
+
+        for (int j = 0; j < ul_config->number_pdus; j++) {
+          uint8_t *ulsch_input_buffer = &(ulsch_input_buffer_array[tx_req.number_of_pdus][MAX_ULSCH_PAYLOAD_BYTES]);
+
+          fapi_nr_ul_config_request_pdu_t *ulcfg_pdu = &ul_config->ul_config_list[j];
+
+          if (ulcfg_pdu->pdu_type == FAPI_NR_UL_CONFIG_TYPE_PUSCH) {
+            int mac_pdu_exist = 0;
+            uint16_t TBS_bytes = ulcfg_pdu->pusch_config_pdu.pusch_data.tb_size;
+            LOG_D(NR_MAC,"harq_id %d, NDI %d NDI_DCI %d, TBS_bytes %d (ra_state %d)\n",
+                  ulcfg_pdu->pusch_config_pdu.pusch_data.harq_process_id,
+                  mac->UL_ndi[ulcfg_pdu->pusch_config_pdu.pusch_data.harq_process_id],
+                  ulcfg_pdu->pusch_config_pdu.pusch_data.new_data_indicator,
+                  TBS_bytes,ra->ra_state);
+            if (ra->ra_state == WAIT_RAR && !ra->cfra){
+              memcpy(ulsch_input_buffer, mac->ulsch_pdu.payload, TBS_bytes);
+              LOG_D(NR_MAC,"[RAPROC] Msg3 to be transmitted:\n");
+              for (int k = 0; k < TBS_bytes; k++) {
+                LOG_D(NR_MAC,"(%i): 0x%x\n",k,mac->ulsch_pdu.payload[k]);
+              }
+              LOG_D(NR_MAC,"Flipping NDI for harq_id %d (Msg3)\n",ulcfg_pdu->pusch_config_pdu.pusch_data.new_data_indicator);
+              mac->UL_ndi[ulcfg_pdu->pusch_config_pdu.pusch_data.harq_process_id] = ulcfg_pdu->pusch_config_pdu.pusch_data.new_data_indicator;
+              mac->first_ul_tx[ulcfg_pdu->pusch_config_pdu.pusch_data.harq_process_id] = 0;
+              mac_pdu_exist = 1;
+            } else {
+
+              if ((mac->UL_ndi[ulcfg_pdu->pusch_config_pdu.pusch_data.harq_process_id] != ulcfg_pdu->pusch_config_pdu.pusch_data.new_data_indicator ||
+                  mac->first_ul_tx[ulcfg_pdu->pusch_config_pdu.pusch_data.harq_process_id]==1) &&
+                  ((get_softmodem_params()->phy_test == 1) ||
+                  (ra->ra_state == RA_SUCCEEDED) ||
+                  (ra->ra_state == WAIT_RAR && ra->cfra))){
+
+                // Getting IP traffic to be transmitted
+                nr_ue_get_sdu(mod_id, cc_id,frame_tx, slot_tx, gNB_index, ulsch_input_buffer, TBS_bytes);
+                mac_pdu_exist = 1;
+              }
 
-        fapi_nr_ul_config_request_pdu_t *ulcfg_pdu = &ul_config->ul_config_list[j];
+              LOG_D(NR_MAC,"Flipping NDI for harq_id %d\n",ulcfg_pdu->pusch_config_pdu.pusch_data.new_data_indicator);
+              mac->UL_ndi[ulcfg_pdu->pusch_config_pdu.pusch_data.harq_process_id] = ulcfg_pdu->pusch_config_pdu.pusch_data.new_data_indicator;
+              mac->first_ul_tx[ulcfg_pdu->pusch_config_pdu.pusch_data.harq_process_id] = 0;
 
-        if (ulcfg_pdu->pdu_type == FAPI_NR_UL_CONFIG_TYPE_PUSCH) {
+            }
 
-          uint16_t TBS_bytes = ulcfg_pdu->pusch_config_pdu.pusch_data.tb_size;
-          LOG_D(NR_MAC,"harq_id %d, NDI %d NDI_DCI %d, TBS_bytes %d (ra_state %d)\n",
-                ulcfg_pdu->pusch_config_pdu.pusch_data.harq_process_id,
-                mac->UL_ndi[ulcfg_pdu->pusch_config_pdu.pusch_data.harq_process_id],
-                ulcfg_pdu->pusch_config_pdu.pusch_data.new_data_indicator,
-                TBS_bytes,ra->ra_state);
-          if (ra->ra_state == WAIT_RAR && !ra->cfra){
-            memcpy(ulsch_input_buffer, mac->ulsch_pdu.payload, TBS_bytes);
-            LOG_D(NR_MAC,"[RAPROC] Msg3 to be transmitted:\n");
-            for (int k = 0; k < TBS_bytes; k++) {
-              LOG_D(NR_MAC,"(%i): 0x%x\n",k,mac->ulsch_pdu.payload[k]);
+            // Config UL TX PDU
+            if (mac_pdu_exist) {
+              tx_req.tx_request_body[tx_req.number_of_pdus].pdu_length = TBS_bytes;
+              tx_req.tx_request_body[tx_req.number_of_pdus].pdu_index = j;
+              tx_req.tx_request_body[tx_req.number_of_pdus].pdu = ulsch_input_buffer;
+              tx_req.number_of_pdus++;
             }
-            LOG_D(NR_MAC,"Flipping NDI for harq_id %d (Msg3)\n",ulcfg_pdu->pusch_config_pdu.pusch_data.new_data_indicator);
-            mac->UL_ndi[ulcfg_pdu->pusch_config_pdu.pusch_data.harq_process_id] = ulcfg_pdu->pusch_config_pdu.pusch_data.new_data_indicator;
-            mac->first_ul_tx[ulcfg_pdu->pusch_config_pdu.pusch_data.harq_process_id] = 0;
-          } else {
-
-            if ((mac->UL_ndi[ulcfg_pdu->pusch_config_pdu.pusch_data.harq_process_id] != ulcfg_pdu->pusch_config_pdu.pusch_data.new_data_indicator ||
-                mac->first_ul_tx[ulcfg_pdu->pusch_config_pdu.pusch_data.harq_process_id]==1) &&
-                ((get_softmodem_params()->phy_test == 1) ||
-                (ra->ra_state == RA_SUCCEEDED) ||
-                (ra->ra_state == WAIT_RAR && ra->cfra))){
-
-              // Getting IP traffic to be transmitted
-              nr_ue_get_sdu(mod_id, cc_id,frame_tx, slot_tx, gNB_index, ulsch_input_buffer, TBS_bytes);
+            if (ra->ra_state == WAIT_CONTENTION_RESOLUTION && !ra->cfra){
+              LOG_I(NR_MAC,"[RAPROC][%d.%d] RA-Msg3 retransmitted\n", frame_tx, slot_tx);
+              // 38.321 restart the ra-ContentionResolutionTimer at each HARQ retransmission in the first symbol after the end of the Msg3 transmission
+              nr_Msg3_transmitted(ul_info->module_id, ul_info->cc_id, ul_info->frame_tx, ul_info->slot_tx, ul_info->gNB_index);
+            }
+            if (ra->ra_state == WAIT_RAR && !ra->cfra){
+              LOG_I(NR_MAC,"[RAPROC][%d.%d] RA-Msg3 transmitted\n", frame_tx, slot_tx);
+              nr_Msg3_transmitted(ul_info->module_id, ul_info->cc_id, ul_info->frame_tx, ul_info->slot_tx, ul_info->gNB_index);
             }
-
-            LOG_D(NR_MAC,"Flipping NDI for harq_id %d\n",ulcfg_pdu->pusch_config_pdu.pusch_data.new_data_indicator);
-            mac->UL_ndi[ulcfg_pdu->pusch_config_pdu.pusch_data.harq_process_id] = ulcfg_pdu->pusch_config_pdu.pusch_data.new_data_indicator;
-            mac->first_ul_tx[ulcfg_pdu->pusch_config_pdu.pusch_data.harq_process_id] = 0;
-
-          }
-
-          // Config UL TX PDU
-          tx_req.slot = slot_tx;
-          tx_req.sfn = frame_tx;
-          tx_req.number_of_pdus++;
-          tx_req.tx_request_body[0].pdu_length = TBS_bytes;
-          tx_req.tx_request_body[0].pdu_index = j;
-          tx_req.tx_request_body[0].pdu = ulsch_input_buffer;
-
-          if (ra->ra_state == WAIT_CONTENTION_RESOLUTION && !ra->cfra){
-            LOG_I(NR_MAC,"[RAPROC] RA-Msg3 retransmitted\n");
-            // 38.321 restart the ra-ContentionResolutionTimer at each HARQ retransmission in the first symbol after the end of the Msg3 transmission
-            nr_Msg3_transmitted(ul_info->module_id, ul_info->cc_id, ul_info->frame_tx, ul_info->slot_tx, ul_info->gNB_index);
-          }
-          if (ra->ra_state == WAIT_RAR && !ra->cfra){
-            LOG_I(NR_MAC,"[RAPROC] RA-Msg3 transmitted\n");
-            nr_Msg3_transmitted(ul_info->module_id, ul_info->cc_id, ul_info->frame_tx, ul_info->slot_tx, ul_info->gNB_index);
           }
         }
-      }
+        pthread_mutex_unlock(&ul_config->mutex_ul_config); // avoid double lock
 
-      fill_scheduled_response(&scheduled_response, NULL, ul_config, &tx_req, mod_id, cc_id, rx_frame, rx_slot, ul_info->thread_id);
-      if(mac->if_module != NULL && mac->if_module->scheduled_response != NULL){
-        mac->if_module->scheduled_response(&scheduled_response);
+        fill_scheduled_response(&scheduled_response, NULL, ul_config, &tx_req, mod_id, cc_id, rx_frame, rx_slot, ul_info->thread_id);
+        if(mac->if_module != NULL && mac->if_module->scheduled_response != NULL){
+          mac->if_module->scheduled_response(&scheduled_response);
+        }
+        pthread_mutex_lock(&ul_config->mutex_ul_config);
       }
+      pthread_mutex_unlock(&ul_config->mutex_ul_config);
     }
   }
 
@@ -2144,7 +2155,13 @@ void nr_ue_pucch_scheduler(module_id_t module_idP, frame_t frameP, int slotP, in
     pucch->resource_set_id = find_pucch_resource_set(mac, N_UCI);
     select_pucch_resource(mac, pucch);
     fapi_nr_ul_config_request_t *ul_config = get_ul_config_request(mac, slotP);
+    pthread_mutex_lock(&ul_config->mutex_ul_config);
+    AssertFatal(ul_config->number_pdus<FAPI_NR_UL_CONFIG_LIST_NUM, "ul_config->number_pdus %d out of bounds\n",ul_config->number_pdus);
     fapi_nr_ul_config_pucch_pdu *pucch_pdu = &ul_config->ul_config_list[ul_config->number_pdus].pucch_config_pdu;
+
+    fill_ul_config(ul_config, frameP, slotP, FAPI_NR_UL_CONFIG_TYPE_PUCCH);
+    pthread_mutex_unlock(&ul_config->mutex_ul_config);
+
     nr_ue_configure_pucch(mac,
                           slotP,
                           rnti,
@@ -2152,7 +2169,6 @@ void nr_ue_pucch_scheduler(module_id_t module_idP, frame_t frameP, int slotP, in
                           pucch_pdu,
                           O_SR, O_ACK, O_CSI);
     LOG_D(NR_MAC,"Configuring pucch, is_common = %d\n",pucch->is_common);
-    fill_ul_config(ul_config, frameP, slotP, FAPI_NR_UL_CONFIG_TYPE_PUCCH);
     nr_scheduled_response_t scheduled_response;
     fill_scheduled_response(&scheduled_response, NULL, ul_config, NULL, module_idP, 0 /*TBR fix*/, frameP, slotP, thread_id);
     if(mac->if_module != NULL && mac->if_module->scheduled_response != NULL)
@@ -2213,13 +2229,16 @@ void nr_ue_prach_scheduler(module_id_t module_idP, frame_t frameP, sub_frame_t s
       format0 = format & 0xff;        // single PRACH format
       format1 = (format >> 8) & 0xff; // dual PRACH format
 
+      pthread_mutex_lock(&ul_config->mutex_ul_config);
+      AssertFatal(ul_config->number_pdus<FAPI_NR_UL_CONFIG_LIST_NUM, "ul_config->number_pdus %d out of bounds\n",ul_config->number_pdus);
       prach_config_pdu = &ul_config->ul_config_list[ul_config->number_pdus].prach_config_pdu;
-      memset(prach_config_pdu, 0, sizeof(fapi_nr_ul_config_prach_pdu));
 
       fill_ul_config(ul_config, frameP, slotP, FAPI_NR_UL_CONFIG_TYPE_PRACH);
-
+      pthread_mutex_unlock(&ul_config->mutex_ul_config);
       LOG_D(PHY, "In %s: (%p) %d UL PDUs:\n", __FUNCTION__, ul_config, ul_config->number_pdus);
 
+      memset(prach_config_pdu, 0, sizeof(fapi_nr_ul_config_prach_pdu));
+
       ncs = get_NCS(rach_ConfigGeneric->zeroCorrelationZoneConfig, format0, setup->restrictedSetConfig);
 
       prach_config_pdu->phys_cell_id = mac->physCellId;
@@ -2295,6 +2314,7 @@ void nr_ue_prach_scheduler(module_id_t module_idP, frame_t frameP, sub_frame_t s
             AssertFatal(1 == 0, "Invalid PRACH format");
         }
       } // if format1
+
       fill_scheduled_response(&scheduled_response, NULL, ul_config, NULL, module_idP, 0 /*TBR fix*/, frameP, slotP, thread_id);
       if(mac->if_module != NULL && mac->if_module->scheduled_response != NULL)
         mac->if_module->scheduled_response(&scheduled_response);
@@ -2723,7 +2743,11 @@ uint8_t nr_ue_get_sdu(module_id_t module_idP,
 
       if (sdu_length > 0) {
 
-        LOG_D(NR_MAC, "In %s: Generating UL MAC sub-PDU for SDU %d, length %d bytes, RB with LCID 0x%02x (buflen (TBS) %d bytes)\n", __FUNCTION__,
+        LOG_D(NR_MAC, "In %s: [UE %d] [%d.%d] UL-DXCH -> ULSCH, Generating UL MAC sub-PDU for SDU %d, length %d bytes, RB with LCID 0x%02x (buflen (TBS) %d bytes)\n",
+          __FUNCTION__,
+          module_idP,
+          frameP,
+          subframe,
           num_sdus + 1,
           sdu_length,
           lcid,
diff --git a/openair2/RRC/NR/MESSAGES/asn1_msg.c b/openair2/RRC/NR/MESSAGES/asn1_msg.c
index 0d95fce930a0e618f27621cded5c6c13668c9da4..ff6923429ef5c4d55c4080dc892a59fd496b379c 100755
--- a/openair2/RRC/NR/MESSAGES/asn1_msg.c
+++ b/openair2/RRC/NR/MESSAGES/asn1_msg.c
@@ -72,6 +72,8 @@
 #include "NR_RRCReestablishmentRequest.h"
 #include "NR_UE-CapabilityRequestFilterNR.h"
 #include "PHY/defs_nr_common.h"
+#include "common/utils/nr/nr_common.h"
+#include "openair2/LAYER2/NR_MAC_COMMON/nr_mac.h"
 #if defined(NR_Rel16)
   #include "NR_SCS-SpecificCarrier.h"
   #include "NR_TDD-UL-DL-ConfigCommon.h"
@@ -989,7 +991,7 @@ void fill_initial_SpCellConfig(rnti_t rnti,
                                NR_SpCellConfig_t *SpCellConfig,
                                NR_ServingCellConfigCommon_t *scc,
                                rrc_gNB_carrier_data_t *carrier) {
-
+  int curr_bwp = NRRIV2BW(scc->downlinkConfigCommon->initialDownlinkBWP->genericParameters.locationAndBandwidth,MAX_BWP_SIZE);
   SpCellConfig->servCellIndex = NULL;
   SpCellConfig->reconfigurationWithSync = NULL;
   SpCellConfig->rlmInSyncOutOfSyncThreshold = NULL;
@@ -1018,7 +1020,9 @@ void fill_initial_SpCellConfig(rnti_t rnti,
   // one symbol (13)
   NR_PUCCH_Resource_t *pucchres0=calloc(1,sizeof(*pucchres0));
   pucchres0->pucch_ResourceId=0;
-  pucchres0->startingPRB=0;
+  //pucchres0->startingPRB=0;
+  pucchres0->startingPRB=(8+rnti) % curr_bwp;
+  LOG_D(NR_RRC, "pucchres0->startPRB %ld rnti %d curr_bwp %d\n", pucchres0->startingPRB, rnti, curr_bwp);
   pucchres0->intraSlotFrequencyHopping=NULL;
   pucchres0->secondHopPRB=NULL;
   pucchres0->format.present= NR_PUCCH_Resource__format_PR_format0;
@@ -1170,9 +1174,9 @@ void fill_initial_SpCellConfig(rnti_t rnti,
   AssertFatal(scc->downlinkConfigCommon->initialDownlinkBWP->genericParameters.subcarrierSpacing==NR_SubcarrierSpacing_kHz30,
               "SCS != 30kHz\n");
   AssertFatal(scc->tdd_UL_DL_ConfigurationCommon->pattern1.dl_UL_TransmissionPeriodicity==NR_TDD_UL_DL_Pattern__dl_UL_TransmissionPeriodicity_ms5,
-	      "TDD period != 5ms : %ld\n",scc->tdd_UL_DL_ConfigurationCommon->pattern1.dl_UL_TransmissionPeriodicity);
-  
-  schedulingRequestResourceConfig->periodicityAndOffset->choice.sl40 = 8 + 10*((rnti>>1)&3) + (rnti&1);
+              "TDD period != 5ms : %ld\n",scc->tdd_UL_DL_ConfigurationCommon->pattern1.dl_UL_TransmissionPeriodicity);
+
+  schedulingRequestResourceConfig->periodicityAndOffset->choice.sl40 = 8;
   schedulingRequestResourceConfig->resource = calloc(1,sizeof(*schedulingRequestResourceConfig->resource));
   *schedulingRequestResourceConfig->resource = 0;
   ASN_SEQUENCE_ADD(&pucch_Config->schedulingRequestResourceToAddModList->list,schedulingRequestResourceConfig);