diff --git a/ci-scripts/attenuatorctl.py b/ci-scripts/attenuatorctl.py
new file mode 100644
index 0000000000..e6f8c01837
--- /dev/null
+++ b/ci-scripts/attenuatorctl.py
@@ -0,0 +1,175 @@
+#!/usr/bin/env python3
+
+import argparse
+import logging
+import itertools
+import usb.core
+import usb.util
+import sys
+import time
+
+logging.basicConfig(
+ level=logging.DEBUG,
+ stream=sys.stdout,
+ format="%(message)s"
+)
+
+# helper class parsing command line options for channels and attenuators
+class ValidateChAtt(argparse.Action):
+ def _parse_channels(s):
+ if len(s.split("-")) == 2:
+ s, e = [int(x) for x in s.split("-")]
+ chs = [x for x in range(s, e+1)]
+ if len(chs) == 0:
+ raise Exception(f"empty range")
+ elif len(s.split(",")) > 1:
+ chs = [int(x) for x in s.split(",")]
+ elif s.isdigit():
+ chs = [int(s)]
+ else:
+ raise Exception(f"could not parse channel expression")
+ for c in chs:
+ if not 1 <= c <= 4:
+ raise Exception(f"channel number must be within [1,4], but have {c}")
+ return chs
+
+ def __call__(self, parse, args, values, option_string=None):
+ ch, att = values
+ try:
+ chs = ValidateChAtt._parse_channels(ch)
+ except Exception as e:
+ parse.exit(1, f"while parsing channel expression {ch}: {e}\n")
+ if not att.isdigit():
+ parse.exit(1, f'expected number for attenuation, but got {att}\n')
+ if not 0 <= int(att) <= 63:
+ parse.exit(1, f'attenuation must be within [0,63], but is {att}\n')
+ opts = getattr(args, self.dest) or {}
+ for c in chs:
+ opts[c] = int(att)
+ setattr(args, self.dest, opts)
+
+def _parse_args() -> argparse.Namespace:
+ epilog = '''
+[CHs] can have the following forms:
+ (1) single channel number w (within [1-4])
+ (2) a range of channels in the form x-y (x <= y)
+ (3) a list of channels in the form a,b,c
+[ATT] must be within [0,63] (dB)
+
+--set and --reach are mutually exclusive
+
+Examples:
+- reset all channels to 0: -s 1-4 0
+- set channels 3 and 4 to attenuation 60: -s 3,4 60
+- perform attenuation sweep during 10s, reaching ch. 1+2 to 60: -r 1,2 60 -d 10
+'''
+ parser = argparse.ArgumentParser(description='Mini-Circuit RC*DAT attenuator controller',
+ epilog=epilog,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+
+ parser.add_argument('--info', '-i', action='store_true', default=False,
+ help='Get Infos for all connected Mini-Circuits RC*DAT',)
+ parser.add_argument('--set', '-s', nargs=2, metavar=("[CHs]","[ATT]"),
+ action=ValidateChAtt, help="Set a fixed attenuation ATT on channels CHs")
+ parser.add_argument('--reach', '-r', nargs=2, metavar=("[CHs]","[ATT]"),
+ action=ValidateChAtt, help="Perform an \"attenuation sweep\": reach attenuation ATT on channels CHs")
+ parser.add_argument('--duration', '-d', action='store', type=float, default=5.0,
+ help='DURATION for attenuation sweep to --reach given attenuation (Default: 5)',)
+ parser.add_argument('--progress', '-p', action='store_true', default=False,
+ help='If provided, will show progress during attenuation sweep',)
+ return parser.parse_args()
+
+def _exec(dev, cmd):
+ dev.write(1, cmd)
+ ans = dev.read(0x81,64)
+ # take from ans(wer) while valid bytes
+ sn = itertools.takewhile(lambda x: x < 255 and x > 0, ans)
+ # turn bytes into characters and accumulate into string
+ return "".join(map(chr, sn))
+
+def devId(dev):
+ return f"{dev.idVendor:04x}:{dev.idProduct:04x}"
+
+def _info(dev):
+ ident = devId(dev)
+ SerialN = _exec(dev, "*:SN?")
+ ModelN = _exec(dev, "*:MN?")
+ Fw = _exec(dev, "*:FIRMWARE?")
+
+ logging.info(f'{ident}: model is {ModelN}')
+ logging.info(f'{ident}: serial number is {SerialN}')
+ logging.info(f'{ident}: FW version is {Fw}')
+
+def _get_attenuation(dev):
+ ident = devId(dev)
+ att = _exec(dev, "*:ATT?")
+ # remove leading "*" and split by channels
+ return [float(n) for n in att[1:].split(" ")]
+
+def _set_attenuation(dev, ch_att):
+ ident = devId(dev)
+ for ch, att in ch_att:
+ #logging.debug(f"{ident}: Set attenuation channel {ch}: {att} dB")
+ att = round(4 * att) / 4 # round to closest 0.25dB
+ resp = _exec(dev, f"*:CHAN:{ch}:SETATT:{att};" )
+
+def _continuous_set_attenuation(dev, ch_att_end, duration, show_progress=False, step_duration=0.25):
+ ident = devId(dev)
+
+ current = _get_attenuation(dev)
+ steps = duration / step_duration
+ ch_att_step = [(ch, (att_end - current[ch - 1]) / steps) for ch, att_end in ch_att_end]
+ logging.info(f"{ident}: sweep attenuation during {duration} s in {steps} steps: increments {ch_att_step}")
+ for i in range(1, int(steps)+1):
+ next_att = [(ch, current[ch - 1] + i * att_step) for ch, att_step in ch_att_step]
+ _set_attenuation(dev, next_att)
+ if show_progress:
+ logging.debug(f"{ident}: attenuation for channel(s): {next_att}")
+ time.sleep(step_duration)
+ _set_attenuation(dev, ch_att_end)
+
+def _get_usb_devs(idVendor, idProduct):
+ devs = []
+ for dev in usb.core.find(idVendor=idVendor, idProduct=idProduct, find_all=True):
+ for configuration in dev:
+ for interface in configuration:
+ ifnum = interface.bInterfaceNumber
+ if not dev.is_kernel_driver_active(ifnum):
+ continue
+ try:
+ dev.detach_kernel_driver(ifnum)
+ except e:
+ pass
+ devs.append(dev)
+ return devs
+
+if __name__ == '__main__':
+ # Parse the arguments
+ args = _parse_args()
+ if args.set and args.reach:
+ logging.error("--set and --reach are mutually exclusive")
+ sys.exit(1)
+
+ # 20ce:0023 is Mini-Circuit RC*DAT device
+ devs = _get_usb_devs(idVendor=0x20ce, idProduct=0x0023)
+ if len(devs) == 0:
+ logging.error('no Mini-Circuits RC*DAT device detected')
+ sys.exit(-1)
+ if len(devs) > 1:
+ logging.error('more than one Mini-Circuits RC*DAT device detected')
+ sys.exit(-1)
+ dev = devs[0]
+
+ if args.info:
+ _info(dev)
+
+ if args.set:
+ _set_attenuation(dev, [(k,v) for k,v in args.set.items()])
+
+ if args.reach:
+ _continuous_set_attenuation(dev, [(k,v) for k,v in args.reach.items()], args.duration, args.progress)
+
+ ident = devId(dev)
+ for i, a in enumerate(_get_attenuation(dev)):
+ logging.info(f"{ident}: Attenuation channel {i+1}: {a} dB")
+ sys.exit(0)
diff --git a/ci-scripts/ci_infra.yaml b/ci-scripts/ci_infra.yaml
index 85dbd10180..837f1edb82 100644
--- a/ci-scripts/ci_infra.yaml
+++ b/ci-scripts/ci_infra.yaml
@@ -85,7 +85,7 @@ oc-cn5g-20897:
Undeploy: "! scripts/oc-cn5g-undeploy.sh /opt/oai-cn5g-fed-develop-2025-jan oaicicd-core-for-fhi72"
LogCollect: "! scripts/oc-cn5g-logcollect.sh /opt/oai-cn5g-fed-develop-2025-jan oaicicd-core-for-fhi72 %%log_dir%%"
-oc-cn5g-00102-ho:
+oc-cn5g-00103-ho:
Host: groot
NetworkScript: echo "inet 172.21.6.111"
RunIperf3Server: False
diff --git a/ci-scripts/conf_files/gnb-cu.sa.f1.ho.conf b/ci-scripts/conf_files/gnb-cu.sa.f1.ho.conf
index 8159eff571..f16a94b472 100644
--- a/ci-scripts/conf_files/gnb-cu.sa.f1.ho.conf
+++ b/ci-scripts/conf_files/gnb-cu.sa.f1.ho.conf
@@ -1,4 +1,4 @@
-Active_gNBs = ( "oai-cu");
+Active_gNBs = ( "CU-OAI");
# Asn1_verbosity, choice in: none, info, annoying
Asn1_verbosity = "none";
@@ -10,7 +10,7 @@ gNBs =
# cell_type = "CELL_MACRO_GNB";
- gNB_name = "oai-cu";
+ gNB_name = "CU-OAI";
// Tracking area code, 0x0000 and 0xfffe are reserved values
tracking_area_code = 1;
diff --git a/ci-scripts/conf_files/gnb-du.sa.band78.51prb.usrpb210.ho-pci0.conf b/ci-scripts/conf_files/gnb-du.sa.band78.51prb.usrpb210.ho-pci0.conf
index eb52e68566..df225e84c2 100644
--- a/ci-scripts/conf_files/gnb-du.sa.band78.51prb.usrpb210.ho-pci0.conf
+++ b/ci-scripts/conf_files/gnb-du.sa.band78.51prb.usrpb210.ho-pci0.conf
@@ -1,4 +1,4 @@
-Active_gNBs = ( "oai-du0");
+Active_gNBs = ( "DU0-OAI");
# Asn1_verbosity, choice in: none, info, annoying
Asn1_verbosity = "none";
@@ -8,7 +8,7 @@ gNBs =
////////// Identification parameters:
gNB_ID = 0xe88;
gNB_DU_ID = 0xe00;
- gNB_name = "oai-du0";
+ gNB_name = "DU0-OAI";
// Tracking area code, 0x0000 and 0xfffe are reserved values
tracking_area_code = 1 ;
@@ -107,7 +107,7 @@ gNBs =
pucchGroupHopping = 0;
hoppingId = 40;
p0_nominal = -90;
-# ssb_PositionsInBurs_BitmapPR
+
ssb_PositionsInBurst_Bitmap = 1;
# ssb_periodicityServingCell
@@ -159,9 +159,7 @@ MACRLCs = (
tr_n_preference = "f1";
local_n_address = "172.21.19.98";
remote_n_address = "172.21.19.98";
- local_n_portc = 500;
local_n_portd = 2154;
- remote_n_portc = 501;
remote_n_portd = 2153;
pusch_TargetSNRx10 = 200;
pucch_TargetSNRx10 = 230;
@@ -176,7 +174,6 @@ L1s = (
prach_dtx_threshold = 100;
pucch0_dtx_threshold = 10;
max_ldpc_iterations = 20;
- sl_ahead = 3;
ofdm_offset_divisor = 8; #set this to UINT_MAX for offset 0
}
);
@@ -188,6 +185,7 @@ RUs = (
nb_rx = 1
att_tx = 0;
att_rx = 0;
+ sl_ahead = 3;
bands = [77];
max_pdschReferenceSignalPower = -27;
max_rxgain = 114;
diff --git a/ci-scripts/conf_files/gnb-du.sa.band78.51prb.usrpb210.ho-pci1.conf b/ci-scripts/conf_files/gnb-du.sa.band78.51prb.usrpb210.ho-pci1.conf
index 3ba7e17b46..14ec8b1e82 100644
--- a/ci-scripts/conf_files/gnb-du.sa.band78.51prb.usrpb210.ho-pci1.conf
+++ b/ci-scripts/conf_files/gnb-du.sa.band78.51prb.usrpb210.ho-pci1.conf
@@ -1,4 +1,4 @@
-Active_gNBs = ( "oai-du1");
+Active_gNBs = ( "DU1-OAI");
# Asn1_verbosity, choice in: none, info, annoying
Asn1_verbosity = "none";
@@ -8,7 +8,7 @@ gNBs =
////////// Identification parameters:
gNB_ID = 0xe88;
gNB_DU_ID = 0xe01;
- gNB_name = "oai-du1";
+ gNB_name = "DU1-OAI";
// Tracking area code, 0x0000 and 0xfffe are reserved values
tracking_area_code = 1 ;
@@ -107,8 +107,8 @@ gNBs =
pucchGroupHopping = 0;
hoppingId = 40;
p0_nominal = -90;
-# ssb_PositionsInBurs_BitmapPR
- ssb_PositionsInBurst_Bitmap = 1;
+
+ ssb_PositionsInBurst_Bitmap = 2;
# ssb_periodicityServingCell
# 0 = ms5, 1=ms10, 2=ms20, 3=ms40, 4=ms80, 5=ms160, 6=spare2, 7=spare1
@@ -159,9 +159,7 @@ MACRLCs = (
tr_n_preference = "f1";
local_n_address = "172.21.19.99";
remote_n_address = "172.21.19.98";
- local_n_portc = 500;
local_n_portd = 2154;
- remote_n_portc = 501;
remote_n_portd = 2153;
pusch_TargetSNRx10 = 200;
pucch_TargetSNRx10 = 230;
@@ -176,7 +174,6 @@ L1s = (
prach_dtx_threshold = 100;
pucch0_dtx_threshold = 10;
max_ldpc_iterations = 20;
- sl_ahead = 3;
ofdm_offset_divisor = 8; #set this to UINT_MAX for offset 0
}
);
@@ -188,6 +185,7 @@ RUs = (
nb_rx = 1
att_tx = 0;
att_rx = 0;
+ sl_ahead = 3;
bands = [77];
max_pdschReferenceSignalPower = -27;
max_rxgain = 114;
diff --git a/ci-scripts/main.py b/ci-scripts/main.py
index 3e7be396cc..ac4ac2c3be 100644
--- a/ci-scripts/main.py
+++ b/ci-scripts/main.py
@@ -321,11 +321,15 @@ def ExecuteActionWithParam(action, ctx):
# Change all execution targets to localhost
node = 'localhost'
command = test.findtext('command')
+ # Allow referencing repository workspace path in XML via %%workspace%%
+ command = command.replace("%%workspace%%", CONTAINERS.eNBSourceCodePath)
success = cls_oaicitest.Custom_Command(HTML, node, command)
elif action == 'Custom_Script':
node = test.findtext('node')
script = test.findtext('script')
+ # Allow referencing repository workspace path in XML via %%workspace%%
+ script = script.replace("%%workspace%%", CONTAINERS.eNBSourceCodePath)
success = cls_oaicitest.Custom_Script(HTML, node, script)
elif action == 'Pull_Cluster_Image':
diff --git a/ci-scripts/xml_files/container_sa_f1_ho_b210_quectel.xml b/ci-scripts/xml_files/container_sa_f1_ho_b210_quectel.xml
index 814346b5f3..77b2312409 100644
--- a/ci-scripts/xml_files/container_sa_f1_ho_b210_quectel.xml
+++ b/ci-scripts/xml_files/container_sa_f1_ho_b210_quectel.xml
@@ -51,6 +51,8 @@
000024
000025
000026
+ 000027
+ 000028
@@ -58,7 +60,7 @@
DeployCoreNetwork
Initialize 5G Core
- oc-cn5g-00102-ho
+ oc-cn5g-00103-ho
@@ -88,127 +90,134 @@
- Deploy_Object
- Deploy gNB-CU in a container
- ci-scripts/yaml_files/5g_sa_f1_b210_ho
- groot
- oai_cu
+ Custom_Command
+ Set attenuator to 0 for DU0 and 17 for DU1
+ rocket
+ python3 %%workspace%%/ci-scripts/attenuatorctl.py -r 1,2 0 -r 3,4 17 -d 5
Deploy_Object
- Deploy gNB-DU0 (TDD/Band77/20MHz/B210) in a container
+ Deploy gNB-CU in a container
ci-scripts/yaml_files/5g_sa_f1_b210_ho
- oai_du0
groot
+ oai-cu
+ Deploy_Object
+ Deploy gNB-DU0 (TDD/Band77/20MHz/B210) in a container
+ ci-scripts/yaml_files/5g_sa_f1_b210_ho
+ oai-du0
+ groot
+
+
+
+ Deploy_Object
+ Deploy gNB-DU1 (TDD/Band77/20MHz/B210) in a container
+ ci-scripts/yaml_files/5g_sa_f1_b210_ho
+ rocket
+ oai-du1
+
+
+
Attach_UE
Attach Quectel
raspix
-
+
Ping
Ping: 100pings in 20sec
raspix
- oc-cn5g-00102-ho
+ oc-cn5g-00103-ho
-c 100 -i 0.2
5
-
- Deploy_Object
- Deploy gNB-DU1 (TDD/Band77/20MHz/B210) in a container
- ci-scripts/yaml_files/5g_sa_f1_b210_ho
- rocket
- oai_du1
-
-
-
+
IdleSleep
Sleep
5
-
+
Custom_Command
- Trigger Handover
- groot
- echo ci trigger_f1_ho | ncat 127.0.0.1 9090
+ Attenuate DU0 by 15 and remove the attenuation on DU1 to trigger Handover
+ rocket
+ python3 %%workspace%%/ci-scripts/attenuatorctl.py -r 1,2 15 -r 3,4 0 -d 5
-
+
Ping
Ping: 50pings in 10sec
raspix
- oc-cn5g-00102-ho
+ oc-cn5g-00103-ho
-c 50 -i 0.2
5
-
+
Custom_Command
UE connected to DU1?
groot
echo ci fetch_du_by_ue_id 1 | ncat 127.0.0.1 9090 | grep "3585"
-
- Custom_Command
- Trigger Handover
- groot
- echo ci trigger_f1_ho | ncat 127.0.0.1 9090
+
+ Custom_Command
+ Attenuate DU1 by 17dB and remove attenuation on DU0 to trigger Handover
+ rocket
+ python3 %%workspace%%/ci-scripts/attenuatorctl.py -r 1,2 0 -r 3,4 17 -d 5
-
+
Ping
Ping: 50pings in 10sec
raspix
- oc-cn5g-00102-ho
+ oc-cn5g-00103-ho
-c 50 -i 0.2
5
-
+
Custom_Command
UE connected to DU0?
groot
echo ci fetch_du_by_ue_id 1 | ncat 127.0.0.1 9090 | grep "3584"
-
- Custom_Command
- Trigger Handover
- groot
- echo ci trigger_f1_ho | ncat 127.0.0.1 9090
+
+ Custom_Command
+ Attenuate DU0 by 15 and remove the attenuation on DU1 to trigger Handover
+ rocket
+ python3 %%workspace%%/ci-scripts/attenuatorctl.py -r 1,2 15 -r 3,4 0 -d 5
-
+
Ping
Ping: 50pings in 10sec
raspix
- oc-cn5g-00102-ho
+ oc-cn5g-00103-ho
-c 50 -i 0.2
5
-
+
Custom_Command
UE connected to DU1?
groot
echo ci fetch_du_by_ue_id 1 | ncat 127.0.0.1 9090 | grep "3585"
-
+
Detach_UE
true
Detach Quectel
raspix
-
+
Undeploy_Object
true
Undeploy CU-DU0
@@ -216,7 +225,7 @@
ci-scripts/yaml_files/5g_sa_f1_b210_ho
-
+
Undeploy_Object
true
Undeploy DU1
@@ -224,7 +233,15 @@
ci-scripts/yaml_files/5g_sa_f1_b210_ho
-
+
+ Custom_Command
+ Reset all channels on the attenuator to 0
+ rocket
+ python3 %%workspace%%/ci-scripts/attenuatorctl.py -s 1-4 0
+ true
+
+
+
Clean_Test_Server_Images
true
Clean Test Images on Test Server
@@ -232,7 +249,7 @@
oai-gnb
-
+
Clean_Test_Server_Images
true
Clean Test Images on Test Server
@@ -240,11 +257,11 @@
oai-gnb
-
+
UndeployCoreNetwork
true
Terminate 5G Core
- oc-cn5g-00102-ho
+ oc-cn5g-00103-ho
diff --git a/ci-scripts/yaml_files/5g_sa_f1_b210_ho/docker-compose.yml b/ci-scripts/yaml_files/5g_sa_f1_b210_ho/docker-compose.yml
index 24a3055701..c1c3db1607 100644
--- a/ci-scripts/yaml_files/5g_sa_f1_b210_ho/docker-compose.yml
+++ b/ci-scripts/yaml_files/5g_sa_f1_b210_ho/docker-compose.yml
@@ -1,5 +1,5 @@
services:
- oai_cu:
+ oai-cu:
image: ${REGISTRY:-oaisoftwarealliance}/${GNB_IMG:-oai-gnb}:${TAG:-develop}
container_name: oai-cu
cap_drop:
@@ -25,7 +25,7 @@ services:
timeout: 5s
retries: 5
- oai_du0:
+ oai-du0:
image: ${REGISTRY:-oaisoftwarealliance}/${GNB_IMG:-oai-gnb}:${TAG:-develop}
container_name: oai-du0
cap_drop:
@@ -57,7 +57,7 @@ services:
timeout: 5s
retries: 5
- oai_du1:
+ oai-du1:
image: ${REGISTRY:-oaisoftwarealliance}/${GNB_IMG:-oai-gnb}:${TAG:-develop}
container_name: oai-du1
cap_drop:
diff --git a/doc/TESTBenches.md b/doc/TESTBenches.md
index e7564b8678..8674103a1a 100644
--- a/doc/TESTBenches.md
+++ b/doc/TESTBenches.md
@@ -24,6 +24,9 @@
| gracehopper1-oai | Gracehopper1 | gNB (n78, PNF/Nvidia CUBB + VNF) | Foxconn RU, _Nvidia Aerial integration_ |
| gracehopper3-oai | Gracehopper3 | build | -- |
| acamas | Acamas | 4G and 5G rfsimulator| -- |
+| groot | Groot | gNB (n77) | B210 (30AD30F) |
+| rocket | Rocket | gNB (n77) | B210 (31F8010) |
+| raspix | Raspix | COTS UE | Quectel RM520N |
Note: The available resources, and their current usage, is indicated here:
- [Lockable resources of jenkins-oai](https://jenkins-oai.eurecom.fr/lockable-resources/):
@@ -226,6 +229,11 @@ information on how the images are built.
- cacofonix + FHI72 + Metanoia (gNB), up2 (Quectel RM520N UE), OAI CN5G
- OpenShift cluster for CN deployment
- FHI 7.2 testing with 100 MHz bandwidth, 2 layers in DL
+- [RAN-SA-Handover-CN5G](https://jenkins-oai.eurecom.fr/job/RAN-SA-Handover-CN5G/)
+ ~5G-NR
+ - groot (CU+DU0) + B210, rocket (DU1) + B210, raspix (Quectel RM520N UE), OAI CN5G
+ - OpenShift cluster for CN deployment
+ - Attenuator (mini circuits RC4DAT-6G-60) - controlled from rocket
### RAN-CI-NSA-Trigger
diff --git a/openair2/RRC/NR/rrc_gNB_du.c b/openair2/RRC/NR/rrc_gNB_du.c
index edf6f19164..246df60626 100644
--- a/openair2/RRC/NR/rrc_gNB_du.c
+++ b/openair2/RRC/NR/rrc_gNB_du.c
@@ -257,6 +257,11 @@ static bool valid_du_in_neighbour_configs(const seq_arr_t *neighbour_cell_config
nc->absoluteFrequencySSB);
return false;
}
+ // Check neighbour band
+ if (get_dl_band(cell) != nc->band) {
+ LOG_W(NR_RRC, "Cell %ld in neighbour config: Band mismatch (%d vs %d)\n", cell->nr_cellid, get_dl_band(cell), nc->band);
+ return false;
+ }
if (get_ssb_scs(cell) != nc->subcarrierSpacing) {
LOG_W(NR_RRC,
"Cell %ld in neighbour config: SCS mismatch (%d vs %d)\n",