From d73a9148de07b03f368c1c000690b2be2a7d4fb3 Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Wed, 12 Nov 2025 17:32:04 +0100 Subject: [PATCH 01/17] Refactor: add common helper functions to construct 5G-S-TMSI * 5G-S-TMSI construction is defined in 3GPP TS 23.003 and relevant at both gNB and UE side * Add helper functions to be later used in NG Paging * Adopt in NAS UE --- common/utils/nr/nr_common.c | 32 ++++++++++++++++++++++++++++++++ common/utils/nr/nr_common.h | 6 ++++++ openair3/NAS/NR_UE/nr_nas_msg.c | 3 ++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/common/utils/nr/nr_common.c b/common/utils/nr/nr_common.c index 96b8681cff..cfbb09e939 100644 --- a/common/utils/nr/nr_common.c +++ b/common/utils/nr/nr_common.c @@ -1487,3 +1487,35 @@ float get_beta_dmrs(int num_cdm_groups_no_data, bool is_type2) } return beta_dmrs_pusch; } + +/** @brief Construct full 5G-S-TMSI from 5G-S-TMSI components + * @param amf_set_id AMF Set ID (10 bits) + * @param amf_pointer AMF Pointer (6 bits) + * @param m_tmsi 5G-TMSI (32 bits) + * @return Full 5G-S-TMSI (48 bits) + * @note The 5G-S-TMSI is constructed as a 48-bit value: + * - Bits 38-47: AMF Set ID (10 bits) + * - Bits 32-37: AMF Pointer (6 bits) + * - Bits 0-31: 5G-TMSI (32 bits) + * @ref 3GPP TS 23.003 */ +uint64_t nr_construct_5g_s_tmsi(uint16_t amf_set_id, uint8_t amf_pointer, uint32_t m_tmsi) +{ + // Construct full 5G-S-TMSI: <5G-TMSI (32 bits)> + return ((uint64_t)amf_set_id << 38) | ((uint64_t)amf_pointer << 32) | m_tmsi; +} + +/** @brief Construct 5G-S-TMSI-Part1 from 5G-S-TMSI components + * @param amf_set_id AMF Set ID (10 bits) + * @param amf_pointer AMF Pointer (6 bits) + * @param m_tmsi 5G-TMSI (32 bits) + * @return 5G-S-TMSI-Part1 (rightmost 39 bits of the full 5G-S-TMSI) + * @note 5G-S-TMSI-Part1 is the rightmost 39 bits of the full 5G-S-TMSI: + * - Bits 32-37: AMF Pointer (6 bits) + * - Bits 0-31: 5G-TMSI (32 bits) + * @ref 3GPP TS 23.003 */ +uint64_t nr_construct_5g_s_tmsi_part1(uint16_t amf_set_id, uint8_t amf_pointer, uint32_t m_tmsi) +{ + // Construct full 5G-S-TMSI and extract Part1: rightmost 39 bits + uint64_t full_s_tmsi = nr_construct_5g_s_tmsi(amf_set_id, amf_pointer, m_tmsi); + return full_s_tmsi & ((1ULL << 39) - 1); +} diff --git a/common/utils/nr/nr_common.h b/common/utils/nr/nr_common.h index e62d66658c..42ea232140 100644 --- a/common/utils/nr/nr_common.h +++ b/common/utils/nr/nr_common.h @@ -357,6 +357,12 @@ frequency_range_t get_freq_range_from_band(uint16_t band); */ float get_beta_dmrs(int num_cdm_groups_no_data, bool is_type2); +/** @brief Construct full 5G-S-TMSI from 5G-S-TMSI components */ +uint64_t nr_construct_5g_s_tmsi(uint16_t amf_set_id, uint8_t amf_pointer, uint32_t m_tmsi); + +/** @brief Construct 5G-S-TMSI-Part1 from 5G-S-TMSI components */ +uint64_t nr_construct_5g_s_tmsi_part1(uint16_t amf_set_id, uint8_t amf_pointer, uint32_t m_tmsi); + #define CEILIDIV(a,b) ((a+b-1)/b) #define ROUNDIDIV(a,b) (((a<<1)+b)/(b<<1)) #define BOUNDED_EVAL(a, b, c) (min(c, max(a, b))) diff --git a/openair3/NAS/NR_UE/nr_nas_msg.c b/openair3/NAS/NR_UE/nr_nas_msg.c index 8f4f9bd280..64f9feef54 100644 --- a/openair3/NAS/NR_UE/nr_nas_msg.c +++ b/openair3/NAS/NR_UE/nr_nas_msg.c @@ -38,6 +38,7 @@ #include #include #include "common/utils/ds/byte_array.h" +#include "common/utils/nr/nr_common.h" #include "AuthenticationResponseParameter.h" #include "FGCNasMessageContainer.h" #include "FGSDeregistrationRequestUEOriginating.h" @@ -1787,7 +1788,7 @@ static void send_nas_5gmm_ind(instance_t instance, const Guti5GSMobileIdentity_t MessageDef *msg = itti_alloc_new_message(TASK_NAS_NRUE, 0, NAS_5GMM_IND); nas_5gmm_ind_t *ind = &NAS_5GMM_IND(msg); LOG_I(NR_RRC, "5G-GUTI: AMF pointer %u, AMF Set ID %u, 5G-TMSI %u \n", guti->amfpointer, guti->amfsetid, guti->tmsi); - ind->fiveG_STMSI = ((uint64_t)guti->amfsetid << 38) | ((uint64_t)guti->amfpointer << 32) | guti->tmsi; + ind->fiveG_STMSI = nr_construct_5g_s_tmsi(guti->amfsetid, guti->amfpointer, guti->tmsi); itti_send_msg_to_task(TASK_RRC_NRUE, instance, msg); } From 5d657c801c87b700d1a711fe3c77760d110236ba Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Mon, 20 Oct 2025 12:31:52 +0200 Subject: [PATCH 02/17] Refactor (F1AP): align cell lists definitions with TS 38.473 * Commit applies to Setup Request, CU/DU Configuration Update * Set standard compliant max number of cells per DU * Allocate large lists on the heap to prevent stack overflow due to the large size * Update relevant free functions * Also, fix bug in eq_f1ap_cu_configuration_update Also, improve comments in struct definition. --- openair2/COMMON/f1ap_messages_types.h | 66 ++++---- openair2/F1AP/lib/f1ap_interface_management.c | 155 ++++++++++++------ openair2/F1AP/lib/f1ap_interface_management.h | 1 + openair2/F1AP/tests/f1ap_lib_test.c | 127 ++++++++------ openair2/GNB_APP/gnb_config.c | 1 + openair2/LAYER2/NR_MAC_gNB/config.c | 15 +- openair2/RRC/NR/rrc_gNB_du.c | 3 +- 7 files changed, 219 insertions(+), 149 deletions(-) diff --git a/openair2/COMMON/f1ap_messages_types.h b/openair2/COMMON/f1ap_messages_types.h index 0cd090ebc7..c891a44adc 100644 --- a/openair2/COMMON/f1ap_messages_types.h +++ b/openair2/COMMON/f1ap_messages_types.h @@ -74,12 +74,11 @@ */ #define F1AP_TRANSPORT_LAYER_ADDRESS_SIZE (160 / 8) - -// Note this should be 512 from maxval in 38.473 -#define F1AP_MAX_NB_CELLS 2 +/* F1 Setup Request (9.3.1.4 of 3GPP TS 38.473) */ +#define F1AP_MAX_NB_CELLS 512 #define F1AP_MAX_NO_OF_TNL_ASSOCIATIONS 32 -#define F1AP_MAX_NO_UE_ID 1024 +#define F1AP_MAX_NO_UE_ID 65536 /* 9.3.1.42 of 3GPP TS 38.473 - gNB-CU System Information */ #define F1AP_MAX_NO_SIB_TYPES 32 @@ -122,54 +121,53 @@ typedef struct f1ap_tdd_info_t { } f1ap_tdd_info_t; typedef struct f1ap_served_cell_info_t { - // NR CGI + // NR CGI (Mandatory) plmn_id_t plmn; uint64_t nr_cellid; // NR Global Cell Id - - // NR Physical Cell Ids + // NR PCI (Mandatory) uint16_t nr_pci; - - /* Tracking area code */ + // 5GS TAC (Optional) uint32_t *tac; - - // Number of slice support items (max 16, could be increased to as much as 1024) + // TAI Slice Support List (Optional) uint16_t num_ssi; nssai_t nssai[MAX_NUM_SLICES]; - + // NR-Mode-Info (Mandatory) f1ap_mode_t mode; union { f1ap_fdd_info_t fdd; f1ap_tdd_info_t tdd; }; - + // Measurement Timing Configuration (Mandatory) uint8_t *measurement_timing_config; int measurement_timing_config_len; } f1ap_served_cell_info_t; typedef struct f1ap_gnb_du_system_info_t { + // MIB message (Mandatory) uint8_t *mib; int mib_length; + // SIB1 message (Mandatory) uint8_t *sib1; int sib1_length; } f1ap_gnb_du_system_info_t; typedef struct f1ap_setup_req_s { - /// ulong transaction id + // Transaction ID (Mandatory) uint64_t transaction_id; - - // F1_Setup_Req payload + // gNB-DU ID (Mandatory) uint64_t gNB_DU_id; + // gNB-DU Name (Optional) char *gNB_DU_name; - - /// rrc version + // gNB-DU RRC version (Mandatory) uint8_t rrc_ver[3]; - - /// number of DU cells available - uint16_t num_cells_available; //0< num_cells_available <= 512; + // gNB-DU Served Cells List (0..1) + uint16_t num_cells_available; // (1..512) struct { + // Served Cell Information (Mandatory) f1ap_served_cell_info_t info; + // gNB-DU System Information (Optional) f1ap_gnb_du_system_info_t *sys_info; - } cell[F1AP_MAX_NB_CELLS]; + } *cell; } f1ap_setup_req_t; typedef struct f1ap_du_register_req_t { @@ -203,8 +201,8 @@ typedef struct f1ap_setup_resp_s { /// string holding gNB_CU_name char *gNB_CU_name; /// number of DU cells to activate - uint16_t num_cells_to_activate; //0< num_cells_to_activate <= 512; - served_cells_to_activate_t cells_to_activate[F1AP_MAX_NB_CELLS]; + uint16_t num_cells_to_activate; + served_cells_to_activate_t *cells_to_activate; /// rrc version uint8_t rrc_ver[3]; @@ -215,8 +213,8 @@ typedef struct f1ap_gnb_cu_configuration_update_s { /// Transaction ID uint64_t transaction_id; /// number of DU cells to activate - uint16_t num_cells_to_activate; //0< num_cells_to_activate/mod <= 512; - served_cells_to_activate_t cells_to_activate[F1AP_MAX_NB_CELLS]; + uint16_t num_cells_to_activate; + served_cells_to_activate_t *cells_to_activate; } f1ap_gnb_cu_configuration_update_t; typedef struct f1ap_setup_failure_s { @@ -236,7 +234,7 @@ typedef struct f1ap_gnb_cu_configuration_update_acknowledge_s { plmn_id_t plmn; uint64_t nr_cellid; uint16_t cause; - } cells_failed_to_be_activated[F1AP_MAX_NB_CELLS]; + } * cells_failed_to_be_activated; int have_criticality; uint16_t criticality_diagnostics; // gNB-CU TNL Association Setup List @@ -245,14 +243,14 @@ typedef struct f1ap_gnb_cu_configuration_update_acknowledge_s { // gNB-CU TNL Association Failed to Setup List uint16_t noofTNLAssociations_failed; f1ap_cp_tnl_t tnlAssociations_failed[F1AP_MAX_NO_OF_TNL_ASSOCIATIONS]; - // Dedicated SI Delivery Needed UE List + // Dedicated SI Delivery Needed UE List (max 65536 UE IDs) uint16_t noofDedicatedSIDeliveryNeededUEs; struct { uint32_t gNB_CU_ue_id; // NR CGI plmn_id_t ue_plmn; uint64_t ue_nr_cellid; - } dedicatedSIDeliveryNeededUEs[F1AP_MAX_NO_UE_ID]; + } * dedicatedSIDeliveryNeededUEs; } f1ap_gnb_cu_configuration_update_acknowledge_t; typedef struct f1ap_gnb_cu_configuration_update_failure_s { @@ -279,7 +277,7 @@ typedef struct f1ap_gnb_du_configuration_update_s { struct { f1ap_served_cell_info_t info; f1ap_gnb_du_system_info_t *sys_info; - } cell_to_add[F1AP_MAX_NB_CELLS]; + } * cell_to_add; /// int cells_to_modify uint16_t num_cells_to_modify; @@ -288,7 +286,7 @@ typedef struct f1ap_gnb_du_configuration_update_s { uint64_t old_nr_cellid; // NR Global Cell Id f1ap_served_cell_info_t info; f1ap_gnb_du_system_info_t *sys_info; - } cell_to_modify[F1AP_MAX_NB_CELLS]; + } * cell_to_modify; /// int cells_to_delete uint16_t num_cells_to_delete; @@ -296,9 +294,9 @@ typedef struct f1ap_gnb_du_configuration_update_s { // NR CGI plmn_id_t plmn; uint64_t nr_cellid; // NR Global Cell Id - } cell_to_delete[F1AP_MAX_NB_CELLS]; + } * cell_to_delete; - f1ap_cell_status_t status[F1AP_MAX_NB_CELLS]; + f1ap_cell_status_t *status; int num_status; /// gNB-DU unique ID, at least within a gNB-CU (0 .. 2^36 - 1) @@ -310,7 +308,7 @@ typedef struct f1ap_gnb_du_configuration_update_acknowledge_s { uint64_t transaction_id; /// number of DU cells to activate uint16_t num_cells_to_activate; // 0< num_cells_to_activate <= 512; - served_cells_to_activate_t cells_to_activate[F1AP_MAX_NB_CELLS]; + served_cells_to_activate_t *cells_to_activate; } f1ap_gnb_du_configuration_update_acknowledge_t; typedef struct f1ap_gnb_du_configuration_update_failure_s { diff --git a/openair2/F1AP/lib/f1ap_interface_management.c b/openair2/F1AP/lib/f1ap_interface_management.c index 2253493832..38de5e4dcc 100644 --- a/openair2/F1AP/lib/f1ap_interface_management.c +++ b/openair2/F1AP/lib/f1ap_interface_management.c @@ -842,8 +842,9 @@ bool decode_f1ap_setup_request(const F1AP_F1AP_PDU_t *pdu, f1ap_setup_req_t *out /* GNB_DU_Served_Cells_List */ out->num_cells_available = ie->value.choice.GNB_DU_Served_Cells_List.list.count; AssertError(out->num_cells_available > 0, return false, "at least 1 cell must be present"); + out->cell = calloc_or_fail(out->num_cells_available, sizeof(*out->cell)); /* Loop over gNB-DU Served Cells Items */ - for (int i = 0; i < out->num_cells_available; i++) { + for (int i = 0; i < out->num_cells_available && i < F1AP_MAX_NB_CELLS; i++) { F1AP_GNB_DU_Served_Cells_Item_t *served_cells_item = &(((F1AP_GNB_DU_Served_Cells_ItemIEs_t *)ie->value.choice.GNB_DU_Served_Cells_List.list.array[i]) ->value.choice.GNB_DU_Served_Cells_Item); @@ -952,7 +953,9 @@ f1ap_setup_req_t cp_f1ap_setup_request(const f1ap_setup_req_t *msg) cp.transaction_id = msg->transaction_id; /* num_cells_available */ cp.num_cells_available = msg->num_cells_available; - for (int n = 0; n < msg->num_cells_available; n++) { + if (cp.num_cells_available > 0) + cp.cell = calloc_or_fail(cp.num_cells_available, sizeof(*cp.cell)); + for (int n = 0; n < msg->num_cells_available && n < F1AP_MAX_NB_CELLS; n++) { /* cell.info */ cp.cell[n].info = copy_f1ap_served_cell_info(&msg->cell[n].info); /* cell.sys_info */ @@ -972,7 +975,7 @@ bool eq_f1ap_setup_request(const f1ap_setup_req_t *a, const f1ap_setup_req_t *b) _F1_EQ_CHECK_STR(a->gNB_DU_name, b->gNB_DU_name); _F1_EQ_CHECK_LONG(a->transaction_id, b->transaction_id); _F1_EQ_CHECK_INT(a->num_cells_available, b->num_cells_available); - for (int i = 0; i < a->num_cells_available; i++) { + for (int i = 0; i < a->num_cells_available && i < F1AP_MAX_NB_CELLS; i++) { if (!eq_f1ap_cell_info(&a->cell[i].info, &b->cell[i].info)) return false; if (!eq_f1ap_sys_info(a->cell[i].sys_info, b->cell[i].sys_info)) @@ -992,8 +995,11 @@ void free_f1ap_setup_request(const f1ap_setup_req_t *msg) { DevAssert(msg != NULL); free(msg->gNB_DU_name); - for (int i = 0; i < msg->num_cells_available; i++) { - free_f1ap_cell(&msg->cell[i].info, msg->cell[i].sys_info); + if (msg->cell != NULL) { + for (int i = 0; i < msg->num_cells_available; i++) { + free_f1ap_cell(&msg->cell[i].info, msg->cell[i].sys_info); + } + free(msg->cell); } } @@ -1032,15 +1038,16 @@ F1AP_F1AP_PDU_t *encode_f1ap_setup_response(const f1ap_setup_resp_t *msg) OCTET_STRING_fromBuf(&ie2->value.choice.GNB_CU_Name, msg->gNB_CU_name, strlen(msg->gNB_CU_name)); } // Cells to be Activated List (O) - for (int i = 0; i < msg->num_cells_to_activate; i++) { + if (msg->num_cells_to_activate > 0) { + DevAssert(msg->num_cells_to_activate <= F1AP_MAX_NB_CELLS); asn1cSequenceAdd(out->protocolIEs.list, F1AP_F1SetupResponseIEs_t, ie3); ie3->id = F1AP_ProtocolIE_ID_id_Cells_to_be_Activated_List; ie3->criticality = F1AP_Criticality_reject; ie3->value.present = F1AP_F1SetupResponseIEs__value_PR_Cells_to_be_Activated_List; - asn1cSequenceAdd(ie3->value.choice.Cells_to_be_Activated_List.list, - F1AP_Cells_to_be_Activated_List_ItemIEs_t, - cells_to_be_activated_ies); - encode_cells_to_activate(&msg->cells_to_activate[i], cells_to_be_activated_ies); + for (int i = 0; i < msg->num_cells_to_activate; i++) { + asn1cSequenceAdd(ie3->value.choice.Cells_to_be_Activated_List.list, F1AP_Cells_to_be_Activated_List_ItemIEs_t, cells); + encode_cells_to_activate(&msg->cells_to_activate[i], cells); + } } // gNB-CU RRC version (M) asn1cSequenceAdd(out->protocolIEs.list, F1AP_F1SetupResponseIEs_t, ie4); @@ -1131,7 +1138,8 @@ bool decode_f1ap_setup_response(const F1AP_F1AP_PDU_t *pdu, f1ap_setup_resp_t *o out->num_cells_to_activate = Cells_to_be_Activated_List->list.count; // Loop Cells to be Activated List Items (count >= 1) AssertError(out->num_cells_to_activate > 0, return false, "At least 1 cell must be present"); - for (int i = 0; i < out->num_cells_to_activate; i++) { + out->cells_to_activate = calloc_or_fail(out->num_cells_to_activate, sizeof(*out->cells_to_activate)); + for (int i = 0; i < out->num_cells_to_activate && i < F1AP_MAX_NB_CELLS; i++) { const F1AP_Cells_to_be_Activated_List_ItemIEs_t *itemIEs = (F1AP_Cells_to_be_Activated_List_ItemIEs_t *)Cells_to_be_Activated_List->list.array[i]; if (!decode_cells_to_activate(&out->cells_to_activate[i], itemIEs)) @@ -1157,7 +1165,7 @@ bool eq_f1ap_setup_response(const f1ap_setup_resp_t *a, const f1ap_setup_resp_t _F1_EQ_CHECK_INT(a->num_cells_to_activate, b->num_cells_to_activate); _F1_EQ_CHECK_LONG(a->transaction_id, b->transaction_id); if (a->num_cells_to_activate) { - for (int i = 0; i < a->num_cells_to_activate; i++) { + for (int i = 0; i < a->num_cells_to_activate && i < F1AP_MAX_NB_CELLS; i++) { const served_cells_to_activate_t *a_cell = &a->cells_to_activate[i]; const served_cells_to_activate_t *b_cell = &b->cells_to_activate[i]; _F1_EQ_CHECK_LONG(a_cell->nr_cellid, b_cell->nr_cellid); @@ -1197,7 +1205,8 @@ f1ap_setup_resp_t cp_f1ap_setup_response(const f1ap_setup_resp_t *msg) cp.transaction_id = msg->transaction_id; /* num_cells_available */ cp.num_cells_to_activate = msg->num_cells_to_activate; - for (int n = 0; n < msg->num_cells_to_activate; n++) { + cp.cells_to_activate = calloc_or_fail(cp.num_cells_to_activate, sizeof(*cp.cells_to_activate)); + for (int n = 0; n < msg->num_cells_to_activate && n < F1AP_MAX_NB_CELLS; n++) { /* cell.info */ served_cells_to_activate_t *cp_cell = &cp.cells_to_activate[n]; const served_cells_to_activate_t *msg_cell = &msg->cells_to_activate[n]; @@ -1230,6 +1239,7 @@ void free_f1ap_setup_response(const f1ap_setup_resp_t *msg) for (int j = 0; j < msg->cells_to_activate[i].num_SI; j++) if (msg->cells_to_activate[i].SI_msg[j].SI_container_length > 0) free(msg->cells_to_activate[i].SI_msg[j].SI_container); + free(msg->cells_to_activate); } /* ==================================== @@ -1373,6 +1383,7 @@ F1AP_F1AP_PDU_t *encode_f1ap_du_configuration_update(const f1ap_gnb_du_configura /* mandatory */ /* c2. Served_Cells_To_Add */ if (msg->num_cells_to_add > 0) { + DevAssert(msg->num_cells_to_add <= F1AP_MAX_NB_CELLS); asn1cSequenceAdd(out->protocolIEs.list, F1AP_GNBDUConfigurationUpdateIEs_t, ie2); ie2->id = F1AP_ProtocolIE_ID_id_Served_Cells_To_Add_List; ie2->criticality = F1AP_Criticality_reject; @@ -1397,6 +1408,7 @@ F1AP_F1AP_PDU_t *encode_f1ap_du_configuration_update(const f1ap_gnb_du_configura /* mandatory */ /* c3. Served_Cells_To_Modify */ if (msg->num_cells_to_modify > 0) { + DevAssert(msg->num_cells_to_modify <= F1AP_MAX_NB_CELLS); asn1cSequenceAdd(out->protocolIEs.list, F1AP_GNBDUConfigurationUpdateIEs_t, ie3); ie3->id = F1AP_ProtocolIE_ID_id_Served_Cells_To_Modify_List; ie3->criticality = F1AP_Criticality_reject; @@ -1426,6 +1438,7 @@ F1AP_F1AP_PDU_t *encode_f1ap_du_configuration_update(const f1ap_gnb_du_configura /* mandatory */ /* c4. Served_Cells_To_Delete */ if (msg->num_cells_to_delete > 0) { + DevAssert(msg->num_cells_to_delete <= F1AP_MAX_NB_CELLS); asn1cSequenceAdd(out->protocolIEs.list, F1AP_GNBDUConfigurationUpdateIEs_t, ie4); ie4->id = F1AP_ProtocolIE_ID_id_Served_Cells_To_Delete_List; ie4->criticality = F1AP_Criticality_reject; @@ -1447,6 +1460,7 @@ F1AP_F1AP_PDU_t *encode_f1ap_du_configuration_update(const f1ap_gnb_du_configura } if (msg->num_status > 0) { + DevAssert(msg->num_status <= F1AP_MAX_NB_CELLS); asn1cSequenceAdd(out->protocolIEs.list, F1AP_GNBDUConfigurationUpdateIEs_t, ie4); ie4->id = F1AP_ProtocolIE_ID_id_Cells_Status_List; ie4->criticality = F1AP_Criticality_reject; @@ -1507,7 +1521,8 @@ bool decode_f1ap_du_configuration_update(const F1AP_F1AP_PDU_t *pdu, f1ap_gnb_du /* Served Cells To Add List */ out->num_cells_to_add = ie->value.choice.Served_Cells_To_Add_List.list.count; AssertError(out->num_cells_to_add > 0, return false, "at least 1 cell to add shall to be present"); - for (int i = 0; i < out->num_cells_to_add; i++) { + out->cell_to_add = calloc_or_fail(out->num_cells_to_add, sizeof(*out->cell_to_add)); + for (int i = 0; i < out->num_cells_to_add && i < F1AP_MAX_NB_CELLS; i++) { F1AP_Served_Cells_To_Add_Item_t *served_cells_item = &((F1AP_Served_Cells_To_Add_ItemIEs_t *)ie->value.choice.Served_Cells_To_Add_List.list.array[i]) ->value.choice.Served_Cells_To_Add_Item; @@ -1525,7 +1540,8 @@ bool decode_f1ap_du_configuration_update(const F1AP_F1AP_PDU_t *pdu, f1ap_gnb_du /* Served Cells To Modify List (O) */ out->num_cells_to_modify = ie->value.choice.Served_Cells_To_Modify_List.list.count; AssertError(out->num_cells_to_modify > 0, return false, "at least 1 cell to modify shall to be present"); - for (int i = 0; i < out->num_cells_to_modify; i++) { + out->cell_to_modify = calloc_or_fail(out->num_cells_to_modify, sizeof(*out->cell_to_modify)); + for (int i = 0; i < out->num_cells_to_modify && i < F1AP_MAX_NB_CELLS; i++) { /* Served Cells To Modify List item (count >= 1) */ F1AP_Served_Cells_To_Modify_Item_t *served_cells_item = &((F1AP_Served_Cells_To_Modify_ItemIEs_t *)ie->value.choice.Served_Cells_To_Modify_List.list.array[i]) @@ -1550,7 +1566,8 @@ bool decode_f1ap_du_configuration_update(const F1AP_F1AP_PDU_t *pdu, f1ap_gnb_du /* Served Cells To Delete List */ out->num_cells_to_delete = ie->value.choice.Served_Cells_To_Delete_List.list.count; AssertError(out->num_cells_to_delete > 0, return false, "at least 1 cell to delete shall to be present"); - for (int i = 0; i < out->num_cells_to_delete; i++) { + out->cell_to_delete = calloc_or_fail(out->num_cells_to_delete, sizeof(*out->cell_to_delete)); + for (int i = 0; i < out->num_cells_to_delete && i < F1AP_MAX_NB_CELLS; i++) { F1AP_Served_Cells_To_Delete_Item_t *served_cells_item = &((F1AP_Served_Cells_To_Delete_ItemIEs_t *)ie->value.choice.Served_Cells_To_Delete_List.list.array[i]) ->value.choice.Served_Cells_To_Delete_Item; @@ -1565,7 +1582,8 @@ bool decode_f1ap_du_configuration_update(const F1AP_F1AP_PDU_t *pdu, f1ap_gnb_du case F1AP_ProtocolIE_ID_id_Cells_Status_List: /* Cells Status List (O) */ out->num_status = ie->value.choice.Cells_Status_List.list.count; - for (int i = 0; i < out->num_status; ++i) { + out->status = calloc_or_fail(out->num_status, sizeof(*out->status)); + for (int i = 0; i < out->num_status && i < F1AP_MAX_NB_CELLS; ++i) { const F1AP_Cells_Status_ItemIEs_t *csi_ie = (F1AP_Cells_Status_ItemIEs_t *)ie->value.choice.Cells_Status_List.list.array[i]; AssertError(csi_ie->value.present == F1AP_Cells_Status_ItemIEs__value_PR_Cells_Status_Item, @@ -1604,11 +1622,14 @@ void free_f1ap_du_configuration_update(const f1ap_gnb_du_configuration_update_t for (int i = 0; i < msg->num_cells_to_add; i++) { free_f1ap_cell(&msg->cell_to_add[i].info, msg->cell_to_add[i].sys_info); } + free(msg->cell_to_add); for (int i = 0; i < msg->num_cells_to_modify; i++) { free_f1ap_cell(&msg->cell_to_modify[i].info, msg->cell_to_modify[i].sys_info); } + free(msg->cell_to_modify); + free(msg->cell_to_delete); + free(msg->status); } - /** * @brief F1 gNB-DU Configuration Update check */ @@ -1621,7 +1642,7 @@ bool eq_f1ap_du_configuration_update(const f1ap_gnb_du_configuration_update_t *a _F1_EQ_CHECK_LONG(a->transaction_id, b->transaction_id); /* to add */ _F1_EQ_CHECK_INT(a->num_cells_to_add, b->num_cells_to_add); - for (int i = 0; i < a->num_cells_to_add; i++) { + for (int i = 0; i < a->num_cells_to_add && i < F1AP_MAX_NB_CELLS; i++) { if (!eq_f1ap_cell_info(&a->cell_to_add[i].info, &b->cell_to_add[i].info)) return false; if (!eq_f1ap_sys_info(a->cell_to_add[i].sys_info, b->cell_to_add[i].sys_info)) @@ -1629,14 +1650,14 @@ bool eq_f1ap_du_configuration_update(const f1ap_gnb_du_configuration_update_t *a } /* to delete */ _F1_EQ_CHECK_INT(a->num_cells_to_delete, b->num_cells_to_delete); - for (int i = 0; i < a->num_cells_to_delete; i++) { + for (int i = 0; i < a->num_cells_to_delete && i < F1AP_MAX_NB_CELLS; i++) { _F1_EQ_CHECK_LONG(a->cell_to_delete[i].nr_cellid, b->cell_to_delete[i].nr_cellid); if (!eq_f1ap_plmn(&a->cell_to_delete[i].plmn, &b->cell_to_delete[i].plmn)) return false; } /* to modify */ _F1_EQ_CHECK_INT(a->num_cells_to_modify, b->num_cells_to_modify); - for (int i = 0; i < a->num_cells_to_modify; i++) { + for (int i = 0; i < a->num_cells_to_modify && i < F1AP_MAX_NB_CELLS; i++) { if (!eq_f1ap_plmn(&a->cell_to_modify[i].old_plmn, &b->cell_to_modify[i].old_plmn)) return false; _F1_EQ_CHECK_LONG(a->cell_to_modify[i].old_nr_cellid, b->cell_to_modify[i].old_nr_cellid); @@ -1647,7 +1668,7 @@ bool eq_f1ap_du_configuration_update(const f1ap_gnb_du_configuration_update_t *a } /* cell status */ _F1_EQ_CHECK_INT(a->num_status, b->num_status); - for (int i = 0; i < a->num_status; ++i) { + for (int i = 0; i < a->num_status && i < F1AP_MAX_NB_CELLS; ++i) { const f1ap_cell_status_t *astatus = &a->status[i]; const f1ap_cell_status_t *bstatus = &b->status[i]; if (!eq_f1ap_plmn(&astatus->plmn, &bstatus->plmn)) @@ -1673,19 +1694,22 @@ f1ap_gnb_du_configuration_update_t cp_f1ap_du_configuration_update(const f1ap_gn cp.transaction_id = msg->transaction_id; /* to add */ cp.num_cells_to_add = msg->num_cells_to_add; - for (int i = 0; i < cp.num_cells_to_add; ++i) { + cp.cell_to_add = calloc_or_fail(cp.num_cells_to_add, sizeof(*cp.cell_to_add)); + for (int i = 0; i < cp.num_cells_to_add && i < F1AP_MAX_NB_CELLS; ++i) { cp.cell_to_add[i].info = copy_f1ap_served_cell_info(&msg->cell_to_add[i].info); cp.cell_to_add[i].sys_info = copy_f1ap_gnb_du_system_info(msg->cell_to_add[i].sys_info); } /* to delete */ cp.num_cells_to_delete = msg->num_cells_to_delete; - for (int i = 0; i < cp.num_cells_to_delete; i++) { + cp.cell_to_delete = calloc_or_fail(cp.num_cells_to_delete, sizeof(*cp.cell_to_delete)); + for (int i = 0; i < cp.num_cells_to_delete && i < F1AP_MAX_NB_CELLS; i++) { cp.cell_to_delete[i].nr_cellid = msg->cell_to_delete[i].nr_cellid; cp.cell_to_delete[i].plmn = msg->cell_to_delete[i].plmn; } /* to modify */ cp.num_cells_to_modify = msg->num_cells_to_modify; - for (int i = 0; i < cp.num_cells_to_modify; i++) { + cp.cell_to_modify = calloc_or_fail(cp.num_cells_to_modify, sizeof(*cp.cell_to_modify)); + for (int i = 0; i < cp.num_cells_to_modify && i < F1AP_MAX_NB_CELLS; i++) { cp.cell_to_modify[i].old_plmn = msg->cell_to_modify[i].old_plmn; cp.cell_to_modify[i].old_nr_cellid = msg->cell_to_modify[i].old_nr_cellid; cp.cell_to_modify[i].info = copy_f1ap_served_cell_info(&msg->cell_to_modify[i].info); @@ -1693,7 +1717,8 @@ f1ap_gnb_du_configuration_update_t cp_f1ap_du_configuration_update(const f1ap_gn } /* cell status */ cp.num_status = msg->num_status; - for (int i = 0; i < cp.num_status; ++i) + cp.status = calloc_or_fail(cp.num_status, sizeof(*cp.status)); + for (int i = 0; i < cp.num_status && i < F1AP_MAX_NB_CELLS; ++i) cp.status[i] = msg->status[i]; return cp; } @@ -1725,15 +1750,16 @@ F1AP_F1AP_PDU_t *encode_f1ap_cu_configuration_update(const f1ap_gnb_cu_configura ieC1->value.choice.TransactionID = msg->transaction_id; /* optional c2. Cells_to_be_Activated_List (O) */ - for (int i = 0; i < msg->num_cells_to_activate; i++) { + if (msg->num_cells_to_activate > 0) { asn1cSequenceAdd(cfgUpdate->protocolIEs.list, F1AP_GNBCUConfigurationUpdateIEs_t, ieC3); ieC3->id = F1AP_ProtocolIE_ID_id_Cells_to_be_Activated_List; ieC3->criticality = F1AP_Criticality_reject; ieC3->value.present = F1AP_GNBCUConfigurationUpdateIEs__value_PR_Cells_to_be_Activated_List; - asn1cSequenceAdd(ieC3->value.choice.Cells_to_be_Activated_List.list, - F1AP_Cells_to_be_Activated_List_ItemIEs_t, - cells_to_be_activated_ies); - encode_cells_to_activate(&msg->cells_to_activate[i], cells_to_be_activated_ies); + DevAssert(msg->num_cells_to_activate <= F1AP_MAX_NB_CELLS); + for (int i = 0; i < msg->num_cells_to_activate; i++) { + asn1cSequenceAdd(ieC3->value.choice.Cells_to_be_Activated_List.list, F1AP_Cells_to_be_Activated_List_ItemIEs_t, cells); + encode_cells_to_activate(&msg->cells_to_activate[i], cells); + } } return pdu; } @@ -1768,7 +1794,8 @@ bool decode_f1ap_cu_configuration_update(const F1AP_F1AP_PDU_t *pdu, f1ap_gnb_cu F1AP_Cells_to_be_Activated_List_t *Cells_to_be_Activated_List = &ie->value.choice.Cells_to_be_Activated_List; out->num_cells_to_activate = Cells_to_be_Activated_List->list.count; AssertError(out->num_cells_to_activate > 0, return false, "At least 1 cell to activate must be present"); - for (int i = 0; i < out->num_cells_to_activate; i++) { + out->cells_to_activate = calloc_or_fail(out->num_cells_to_activate, sizeof(*out->cells_to_activate)); + for (int i = 0; i < out->num_cells_to_activate && i < F1AP_MAX_NB_CELLS; i++) { if (!decode_cells_to_activate(&out->cells_to_activate[i], (F1AP_Cells_to_be_Activated_List_ItemIEs_t *)Cells_to_be_Activated_List->list.array[i])) return false; @@ -1786,8 +1813,9 @@ bool decode_f1ap_cu_configuration_update(const F1AP_F1AP_PDU_t *pdu, f1ap_gnb_cu void free_f1ap_cu_configuration_update(const f1ap_gnb_cu_configuration_update_t *msg) { for (int i = 0; i < msg->num_cells_to_activate; i++) - for (int j = 0; j < msg->cells_to_activate[j].num_SI; j++) + for (int j = 0; j < msg->cells_to_activate[i].num_SI; j++) free(msg->cells_to_activate[i].SI_msg[j].SI_container); + free(msg->cells_to_activate); } /** @@ -1798,16 +1826,18 @@ bool eq_f1ap_cu_configuration_update(const f1ap_gnb_cu_configuration_update_t *a _F1_EQ_CHECK_LONG(a->transaction_id, b->transaction_id); /* to activate */ _F1_EQ_CHECK_INT(a->num_cells_to_activate, b->num_cells_to_activate); - for (int i = 0; i < a->num_cells_to_activate; i++) { + for (int i = 0; i < a->num_cells_to_activate && i < F1AP_MAX_NB_CELLS; i++) { _F1_EQ_CHECK_LONG(a->cells_to_activate[i].nr_cellid, b->cells_to_activate[i].nr_cellid); _F1_EQ_CHECK_INT(a->cells_to_activate[i].nrpci, b->cells_to_activate[i].nrpci); if (!eq_f1ap_plmn(&a->cells_to_activate[i].plmn, &b->cells_to_activate[i].plmn)) return false; _F1_EQ_CHECK_INT(a->cells_to_activate[i].num_SI, b->cells_to_activate[i].num_SI); for (int s = 0; s < a->cells_to_activate[i].num_SI; s++) { - _F1_EQ_CHECK_INT(*a->cells_to_activate[i].SI_msg[s].SI_container, *a->cells_to_activate[i].SI_msg[s].SI_container); - _F1_EQ_CHECK_INT(a->cells_to_activate[i].SI_msg[s].SI_container_length, a->cells_to_activate[i].SI_msg[s].SI_container_length); - _F1_EQ_CHECK_INT(a->cells_to_activate[i].SI_msg[s].SI_type, a->cells_to_activate[i].SI_msg[s].SI_type); + f1ap_sib_msg_t *a_sib_msg = &a->cells_to_activate[i].SI_msg[s]; + f1ap_sib_msg_t *b_sib_msg = &b->cells_to_activate[i].SI_msg[s]; + _F1_EQ_CHECK_INT(*a_sib_msg->SI_container, *b_sib_msg->SI_container); + _F1_EQ_CHECK_INT(a_sib_msg->SI_container_length, b_sib_msg->SI_container_length); + _F1_EQ_CHECK_INT(a_sib_msg->SI_type, b_sib_msg->SI_type); } } return true; @@ -1822,7 +1852,8 @@ f1ap_gnb_cu_configuration_update_t cp_f1ap_cu_configuration_update(const f1ap_gn /* transaction_id */ cp.transaction_id = msg->transaction_id; cp.num_cells_to_activate = msg->num_cells_to_activate; - for (int i = 0; i < cp.num_cells_to_activate; i++) { + cp.cells_to_activate = calloc_or_fail(cp.num_cells_to_activate, sizeof(*cp.cells_to_activate)); + for (int i = 0; i < cp.num_cells_to_activate && i < F1AP_MAX_NB_CELLS; i++) { cp.cells_to_activate[i] = msg->cells_to_activate[i]; for (int s = 0; s < cp.cells_to_activate[i].num_SI; s++) { cp.cells_to_activate[i].SI_msg[s] = msg->cells_to_activate[i].SI_msg[s]; @@ -1862,6 +1893,7 @@ F1AP_F1AP_PDU_t *encode_f1ap_cu_configuration_update_acknowledge(const f1ap_gnb_ ie1->value.choice.TransactionID = msg->transaction_id; // Cells Failed to be Activated List (0..1) if (msg->num_cells_failed_to_be_activated > 0) { + DevAssert(msg->num_cells_failed_to_be_activated <= F1AP_MAX_NB_CELLS); asn1cSequenceAdd(out->protocolIEs.list, F1AP_GNBCUConfigurationUpdateAcknowledgeIEs_t, ie2); ie2->id = F1AP_ProtocolIE_ID_id_Cells_Failed_to_be_Activated_List; ie2->criticality = F1AP_Criticality_reject; @@ -1878,9 +1910,8 @@ F1AP_F1AP_PDU_t *encode_f1ap_cu_configuration_update_acknowledge(const f1ap_gnb_ p1->cause.choice.radioNetwork = msg->cells_failed_to_be_activated[i].cause; // NR CGI (M) const plmn_id_t *plmn = &msg->cells_failed_to_be_activated[i].plmn; - MCC_MNC_TO_PLMNID(plmn->mcc, plmn->mnc, plmn->mnc_digit_length, &(p1->nRCGI.pLMN_Identity)); - printf("plmn->mcc %d %d %d %ld \n", - p1->nRCGI.pLMN_Identity.buf[0], p1->nRCGI.pLMN_Identity.buf[1], p1->nRCGI.pLMN_Identity.buf[2], p1->nRCGI.pLMN_Identity.size); + F1AP_PLMN_Identity_t *p2 = &p1->nRCGI.pLMN_Identity; + MCC_MNC_TO_PLMNID(plmn->mcc, plmn->mnc, plmn->mnc_digit_length, p2); NR_CELL_ID_TO_BIT_STRING(msg->cells_failed_to_be_activated[i].nr_cellid, &(p1->nRCGI.nRCellIdentity)); } } @@ -1919,7 +1950,9 @@ bool decode_f1ap_cu_configuration_update_acknowledge(const F1AP_F1AP_PDU_t *pdu, F1AP_GNBCUConfigurationUpdateAcknowledgeIEs__value_PR_Cells_Failed_to_be_Activated_List); F1AP_Cells_Failed_to_be_Activated_List_t *cell_fail_list = &ie->value.choice.Cells_Failed_to_be_Activated_List; out->num_cells_failed_to_be_activated = cell_fail_list->list.count; - for (int j = 0; j < out->num_cells_failed_to_be_activated; j++) { + out->cells_failed_to_be_activated = + calloc_or_fail(out->num_cells_failed_to_be_activated, sizeof(*out->cells_failed_to_be_activated)); + for (int j = 0; j < out->num_cells_failed_to_be_activated && j < F1AP_MAX_NB_CELLS; j++) { const F1AP_Cells_Failed_to_be_Activated_List_ItemIEs_t *itemIE = (F1AP_Cells_Failed_to_be_Activated_List_ItemIEs_t *)cell_fail_list->list.array[j]; const F1AP_Cells_Failed_to_be_Activated_List_Item_t *item = &itemIE->value.choice.Cells_Failed_to_be_Activated_List_Item; @@ -1966,7 +1999,7 @@ bool eq_f1ap_cu_configuration_update_acknowledge(const f1ap_gnb_cu_configuration _F1_EQ_CHECK_LONG(a->transaction_id, b->transaction_id); // number of cells failed to be activated _F1_EQ_CHECK_INT(a->num_cells_failed_to_be_activated, b->num_cells_failed_to_be_activated); - for (int i = 0; i < a->num_cells_failed_to_be_activated; i++) { + for (int i = 0; i < a->num_cells_failed_to_be_activated && i < F1AP_MAX_NB_CELLS; i++) { if (!eq_f1ap_plmn(&a->cells_failed_to_be_activated[i].plmn, &b->cells_failed_to_be_activated[i].plmn)) return false; _F1_EQ_CHECK_LONG(a->cells_failed_to_be_activated[i].nr_cellid, b->cells_failed_to_be_activated[i].nr_cellid); @@ -1974,21 +2007,21 @@ bool eq_f1ap_cu_configuration_update_acknowledge(const f1ap_gnb_cu_configuration } // TNL Associations to setup _F1_EQ_CHECK_INT(a->noofTNLAssociations_to_setup, b->noofTNLAssociations_to_setup); - for (int i = 0; i < a->noofTNLAssociations_to_setup; i++) { + for (int i = 0; i < a->noofTNLAssociations_to_setup && i < F1AP_MAX_NO_OF_TNL_ASSOCIATIONS; i++) { // Explicit comparison of TNL Association fields _F1_EQ_CHECK_INT(a->tnlAssociations_to_setup[i].tl_address, b->tnlAssociations_to_setup[i].tl_address); _F1_EQ_CHECK_INT(a->tnlAssociations_to_setup[i].port, b->tnlAssociations_to_setup[i].port); } // TNL Associations failed to setup _F1_EQ_CHECK_INT(a->noofTNLAssociations_failed, b->noofTNLAssociations_failed); - for (int i = 0; i < a->noofTNLAssociations_failed; i++) { + for (int i = 0; i < a->noofTNLAssociations_failed && i < F1AP_MAX_NO_OF_TNL_ASSOCIATIONS; i++) { // Explicit comparison of TNL Association fields _F1_EQ_CHECK_INT(a->tnlAssociations_failed[i].tl_address, b->tnlAssociations_failed[i].tl_address); _F1_EQ_CHECK_INT(a->tnlAssociations_failed[i].port, b->tnlAssociations_failed[i].port); } // Dedicated SI Delivery Needed UE List _F1_EQ_CHECK_INT(a->noofDedicatedSIDeliveryNeededUEs, b->noofDedicatedSIDeliveryNeededUEs); - for (int i = 0; i < a->noofDedicatedSIDeliveryNeededUEs; i++) { + for (int i = 0; i < a->noofDedicatedSIDeliveryNeededUEs && i < F1AP_MAX_NO_UE_ID; i++) { _F1_EQ_CHECK_INT(a->dedicatedSIDeliveryNeededUEs[i].gNB_CU_ue_id, b->dedicatedSIDeliveryNeededUEs[i].gNB_CU_ue_id); if (!eq_f1ap_plmn(&a->dedicatedSIDeliveryNeededUEs[i].ue_plmn, &b->dedicatedSIDeliveryNeededUEs[i].ue_plmn)) return false; @@ -2008,23 +2041,34 @@ f1ap_gnb_cu_configuration_update_acknowledge_t cp_f1ap_cu_configuration_update_a cp.transaction_id = msg->transaction_id; // number of cells failed to be activated cp.num_cells_failed_to_be_activated = msg->num_cells_failed_to_be_activated; - for (int i = 0; i < cp.num_cells_failed_to_be_activated; i++) + cp.cells_failed_to_be_activated = calloc_or_fail(cp.num_cells_failed_to_be_activated, sizeof(*cp.cells_failed_to_be_activated)); + for (int i = 0; i < cp.num_cells_failed_to_be_activated && i < F1AP_MAX_NB_CELLS; i++) cp.cells_failed_to_be_activated[i] = msg->cells_failed_to_be_activated[i]; // TNL Associations to setup cp.noofTNLAssociations_to_setup = msg->noofTNLAssociations_to_setup; - for (int i = 0; i < cp.noofTNLAssociations_to_setup; i++) + for (int i = 0; i < cp.noofTNLAssociations_to_setup && i < F1AP_MAX_NO_OF_TNL_ASSOCIATIONS; i++) cp.tnlAssociations_to_setup[i] = msg->tnlAssociations_to_setup[i]; // TNL Associations failed to setup cp.noofTNLAssociations_failed = msg->noofTNLAssociations_failed; - for (int i = 0; i < cp.noofTNLAssociations_failed; i++) + for (int i = 0; i < cp.noofTNLAssociations_failed && i < F1AP_MAX_NO_OF_TNL_ASSOCIATIONS; i++) cp.tnlAssociations_failed[i] = msg->tnlAssociations_failed[i]; // Dedicated SI Delivery Needed UE List cp.noofDedicatedSIDeliveryNeededUEs = msg->noofDedicatedSIDeliveryNeededUEs; - for (int i = 0; i < cp.noofDedicatedSIDeliveryNeededUEs; i++) + cp.dedicatedSIDeliveryNeededUEs = calloc_or_fail(cp.noofDedicatedSIDeliveryNeededUEs, sizeof(*cp.dedicatedSIDeliveryNeededUEs)); + for (int i = 0; i < cp.noofDedicatedSIDeliveryNeededUEs && i < F1AP_MAX_NO_UE_ID; i++) cp.dedicatedSIDeliveryNeededUEs[i] = msg->dedicatedSIDeliveryNeededUEs[i]; return cp; } +/** + * @brief F1 gNB-CU Configuration Update Acknowledge memory management + */ +void free_f1ap_cu_configuration_update_acknowledge(const f1ap_gnb_cu_configuration_update_acknowledge_t *msg) +{ + free(msg->cells_failed_to_be_activated); + free(msg->dedicatedSIDeliveryNeededUEs); +} + /* ================================== * F1AP gNB-DU Configuration Ack * ================================== */ @@ -2094,10 +2138,11 @@ bool decode_f1ap_du_configuration_update_acknowledge(const F1AP_F1AP_PDU_t *pdu, _F1_EQ_CHECK_INT(ie->value.present, F1AP_GNBDUConfigurationUpdateAcknowledgeIEs__value_PR_Cells_to_be_Activated_List); F1AP_Cells_to_be_Activated_List_t *list = &ie->value.choice.Cells_to_be_Activated_List; out->num_cells_to_activate = list->list.count; - for (int j = 0; j < out->num_cells_to_activate; j++) { + out->cells_to_activate = calloc_or_fail(out->num_cells_to_activate, sizeof(*out->cells_to_activate)); + for (int j = 0; j < out->num_cells_to_activate && j < F1AP_MAX_NB_CELLS; j++) { const F1AP_Cells_to_be_Activated_List_ItemIEs_t *itemIE = (F1AP_Cells_to_be_Activated_List_ItemIEs_t *)list->list.array[j]; - decode_cells_to_activate(&out->cells_to_activate[0], itemIE); + decode_cells_to_activate(&out->cells_to_activate[j], itemIE); } break; } @@ -2120,7 +2165,7 @@ bool eq_f1ap_du_configuration_update_acknowledge(const f1ap_gnb_du_configuration // number of cells to activate _F1_EQ_CHECK_INT(a->num_cells_to_activate, b->num_cells_to_activate); // loop over cells to activate - for (int i = 0; i < a->num_cells_to_activate; i++) { + for (int i = 0; i < a->num_cells_to_activate && i < F1AP_MAX_NB_CELLS; i++) { _F1_EQ_CHECK_LONG(a->cells_to_activate[i].nr_cellid, b->cells_to_activate[i].nr_cellid); _F1_EQ_CHECK_INT(a->cells_to_activate[i].nrpci, b->cells_to_activate[i].nrpci); if (!eq_f1ap_plmn(&a->cells_to_activate[i].plmn, &b->cells_to_activate[i].plmn)) @@ -2148,7 +2193,8 @@ f1ap_gnb_du_configuration_update_acknowledge_t cp_f1ap_du_configuration_update_a // number of cells to activate cp.num_cells_to_activate = msg->num_cells_to_activate; // Loop through cells to activate - for (int i = 0; i < cp.num_cells_to_activate; i++) { + cp.cells_to_activate = calloc_or_fail(cp.num_cells_to_activate, sizeof(*cp.cells_to_activate)); + for (int i = 0; i < cp.num_cells_to_activate && i < F1AP_MAX_NB_CELLS; i++) { cp.cells_to_activate[i] = msg->cells_to_activate[i]; for (int s = 0; s < cp.cells_to_activate[i].num_SI; s++) { f1ap_sib_msg_t *cp_sib = &cp.cells_to_activate[i].SI_msg[s]; @@ -2175,4 +2221,5 @@ void free_f1ap_du_configuration_update_acknowledge(const f1ap_gnb_du_configurati } } } + free(msg->cells_to_activate); } diff --git a/openair2/F1AP/lib/f1ap_interface_management.h b/openair2/F1AP/lib/f1ap_interface_management.h index 1b484f3476..56f6579f2f 100644 --- a/openair2/F1AP/lib/f1ap_interface_management.h +++ b/openair2/F1AP/lib/f1ap_interface_management.h @@ -84,6 +84,7 @@ bool eq_f1ap_cu_configuration_update_acknowledge(const f1ap_gnb_cu_configuration const f1ap_gnb_cu_configuration_update_acknowledge_t *b); f1ap_gnb_cu_configuration_update_acknowledge_t cp_f1ap_cu_configuration_update_acknowledge( const f1ap_gnb_cu_configuration_update_acknowledge_t *msg); +void free_f1ap_cu_configuration_update_acknowledge(const f1ap_gnb_cu_configuration_update_acknowledge_t *msg); /* F1 gNB-DU Configuration Update Acknowledge */ struct F1AP_F1AP_PDU *encode_f1ap_du_configuration_update_acknowledge(const f1ap_gnb_du_configuration_update_acknowledge_t *msg); diff --git a/openair2/F1AP/tests/f1ap_lib_test.c b/openair2/F1AP/tests/f1ap_lib_test.c index cba787f4dc..e60d768b94 100644 --- a/openair2/F1AP/tests/f1ap_lib_test.c +++ b/openair2/F1AP/tests/f1ap_lib_test.c @@ -244,8 +244,9 @@ static void test_f1ap_setup_request(void) .rrc_ver[0] = 12, .rrc_ver[1] = 34, .rrc_ver[2] = 56, - .cell[0].info = info, }; + orig.cell = calloc_or_fail(orig.num_cells_available, sizeof(*orig.cell)); + orig.cell[0].info = info; orig.cell[0].sys_info = calloc_or_fail(1, sizeof(*orig.cell[0].sys_info)); *orig.cell[0].sys_info = sys_info; // encode @@ -322,12 +323,14 @@ static void test_f1ap_setup_response(void) .rrc_ver[1] = 34, .rrc_ver[2] = 56, .num_cells_to_activate = 1, - .cells_to_activate[0].nr_cellid = 123456, - .cells_to_activate[0].nrpci = 1, - .cells_to_activate[0].plmn.mcc = 12, - .cells_to_activate[0].plmn.mnc = 123, - .cells_to_activate[0].plmn.mnc_digit_length = 3, }; + /* Allocate memory for cells_to_activate array */ + orig.cells_to_activate = calloc_or_fail(orig.num_cells_to_activate, sizeof(*orig.cells_to_activate)); + orig.cells_to_activate[0].nr_cellid = 123456; + orig.cells_to_activate[0].nrpci = 1; + orig.cells_to_activate[0].plmn.mcc = 12; + orig.cells_to_activate[0].plmn.mnc = 123; + orig.cells_to_activate[0].plmn.mnc_digit_length = 3; /* Cells to activate */ if (orig.num_cells_to_activate) { /* SI_container */ @@ -556,30 +559,37 @@ static void test_f1ap_du_configuration_update(void) f1ap_gnb_du_configuration_update_t orig = { .transaction_id = 2, .num_cells_to_add = 1, - .cell_to_add[0].info = info2, .num_cells_to_modify = 1, - .cell_to_modify[0].info = info, - .cell_to_modify[0].old_nr_cellid = 1235UL, - .cell_to_modify[0].old_plmn.mcc = 208, - .cell_to_modify[0].old_plmn.mnc = 88, - .cell_to_modify[0].old_plmn.mnc_digit_length = 2, .num_cells_to_delete = 1, - .cell_to_delete[0].nr_cellid = 1234UL, - .cell_to_delete[0].plmn.mcc = 1, - .cell_to_delete[0].plmn.mnc = 1, - .cell_to_delete[0].plmn.mnc_digit_length = 3, .num_status = 2, - .status[0].nr_cellid = 542UL, - .status[0].plmn.mcc = 1, - .status[0].plmn.mnc = 2, - .status[0].plmn.mnc_digit_length = 3, - .status[0].service_state = F1AP_STATE_IN_SERVICE, - .status[1].nr_cellid = 33UL, - .status[1].plmn.mcc = 5, - .status[1].plmn.mnc = 13, - .status[1].plmn.mnc_digit_length = 2, - .status[1].service_state = F1AP_STATE_OUT_OF_SERVICE, }; + /* Allocate memory for arrays */ + orig.cell_to_add = calloc_or_fail(orig.num_cells_to_add, sizeof(*orig.cell_to_add)); + orig.cell_to_modify = calloc_or_fail(orig.num_cells_to_modify, sizeof(*orig.cell_to_modify)); + orig.cell_to_delete = calloc_or_fail(orig.num_cells_to_delete, sizeof(*orig.cell_to_delete)); + orig.status = calloc_or_fail(orig.num_status, sizeof(*orig.status)); + + /* Fill in the data */ + orig.cell_to_add[0].info = info2; + orig.cell_to_modify[0].info = info; + orig.cell_to_modify[0].old_nr_cellid = 1235UL; + orig.cell_to_modify[0].old_plmn.mcc = 208; + orig.cell_to_modify[0].old_plmn.mnc = 88; + orig.cell_to_modify[0].old_plmn.mnc_digit_length = 2; + orig.cell_to_delete[0].nr_cellid = 1234UL; + orig.cell_to_delete[0].plmn.mcc = 1; + orig.cell_to_delete[0].plmn.mnc = 1; + orig.cell_to_delete[0].plmn.mnc_digit_length = 3; + orig.status[0].nr_cellid = 542UL; + orig.status[0].plmn.mcc = 1; + orig.status[0].plmn.mnc = 2; + orig.status[0].plmn.mnc_digit_length = 3; + orig.status[0].service_state = F1AP_STATE_IN_SERVICE; + orig.status[1].nr_cellid = 33UL; + orig.status[1].plmn.mcc = 5; + orig.status[1].plmn.mnc = 13; + orig.status[1].plmn.mnc_digit_length = 2; + orig.status[1].service_state = F1AP_STATE_OUT_OF_SERVICE; orig.cell_to_modify[0].sys_info = calloc_or_fail(1, sizeof(*orig.cell_to_modify[0].sys_info)); *orig.cell_to_modify[0].sys_info = sys_info; orig.gNB_DU_ID = malloc_or_fail(sizeof(*orig.gNB_DU_ID)); @@ -614,18 +624,18 @@ static void test_f1ap_du_configuration_update_acknowledge(void) f1ap_gnb_du_configuration_update_acknowledge_t orig = { .transaction_id = 5, .num_cells_to_activate = 1, - .cells_to_activate = {{ - .nr_cellid = 987654321, - .nrpci = 50, - .plmn = {.mcc = 001, .mnc = 01, .mnc_digit_length = 2}, - .num_SI = 1, - .SI_msg = {{ - .SI_type = 7, - .SI_container_length = 10, - .SI_container = malloc(sizeof(uint8_t) * 10), - }}, - }}, }; + // Allocate memory for cells_to_activate array + orig.cells_to_activate = calloc_or_fail(orig.num_cells_to_activate, sizeof(*orig.cells_to_activate)); + orig.cells_to_activate[0].nr_cellid = 987654321; + orig.cells_to_activate[0].nrpci = 50; + orig.cells_to_activate[0].plmn.mcc = 001; + orig.cells_to_activate[0].plmn.mnc = 01; + orig.cells_to_activate[0].plmn.mnc_digit_length = 2; + orig.cells_to_activate[0].num_SI = 1; + orig.cells_to_activate[0].SI_msg[0].SI_type = 7; + orig.cells_to_activate[0].SI_msg[0].SI_container_length = 10; + orig.cells_to_activate[0].SI_msg[0].SI_container = malloc(sizeof(uint8_t) * 10); for (int i = 0; i < orig.cells_to_activate[0].SI_msg[0].SI_container_length; i++) { orig.cells_to_activate[0].SI_msg[0].SI_container[i] = i; } @@ -657,17 +667,21 @@ static void test_f1ap_du_configuration_update_acknowledge(void) static void test_f1ap_cu_configuration_update(void) { /* create message */ - f1ap_gnb_cu_configuration_update_t orig = {.transaction_id = 2, - .num_cells_to_activate = 1, - .cells_to_activate = {{.nr_cellid = 123456789, - .nrpci = 100, - .plmn = {.mcc = 001, .mnc = 01, .mnc_digit_length = 2}, - .num_SI = 1, - .SI_msg = {{ - .SI_type = 7, - .SI_container_length = 10, - .SI_container = malloc(sizeof(uint8_t) * 10), - }}}}}; + f1ap_gnb_cu_configuration_update_t orig = { + .transaction_id = 2, + .num_cells_to_activate = 1, + }; + // Allocate memory for cells_to_activate array + orig.cells_to_activate = calloc_or_fail(orig.num_cells_to_activate, sizeof(*orig.cells_to_activate)); + orig.cells_to_activate[0].nr_cellid = 123456789; + orig.cells_to_activate[0].nrpci = 100; + orig.cells_to_activate[0].plmn.mcc = 001; + orig.cells_to_activate[0].plmn.mnc = 01; + orig.cells_to_activate[0].plmn.mnc_digit_length = 2; + orig.cells_to_activate[0].num_SI = 1; + orig.cells_to_activate[0].SI_msg[0].SI_type = 7; + orig.cells_to_activate[0].SI_msg[0].SI_container_length = 10; + orig.cells_to_activate[0].SI_msg[0].SI_container = malloc(sizeof(uint8_t) * 10); F1AP_F1AP_PDU_t *f1enc = encode_f1ap_cu_configuration_update(&orig); F1AP_F1AP_PDU_t *f1dec = f1ap_encode_decode(f1enc); f1ap_msg_free(f1enc); @@ -697,10 +711,14 @@ static void test_f1ap_cu_configuration_update_acknowledge(void) f1ap_gnb_cu_configuration_update_acknowledge_t orig = { .transaction_id = 2, .num_cells_failed_to_be_activated = 1, - .cells_failed_to_be_activated = {{.nr_cellid = 123456789, - .plmn = {.mcc = 001, .mnc = 01, .mnc_digit_length = 2}, - .cause = F1AP_CAUSE_RADIO_NETWORK}} - }; + }; + // Allocate memory for cells_failed_to_be_activated array + orig.cells_failed_to_be_activated = calloc_or_fail(orig.num_cells_failed_to_be_activated, sizeof(*orig.cells_failed_to_be_activated)); + orig.cells_failed_to_be_activated[0].nr_cellid = 123456789; + orig.cells_failed_to_be_activated[0].plmn.mcc = 001; + orig.cells_failed_to_be_activated[0].plmn.mnc = 01; + orig.cells_failed_to_be_activated[0].plmn.mnc_digit_length = 2; + orig.cells_failed_to_be_activated[0].cause = F1AP_CAUSE_RADIO_NETWORK; // F1AP Enc/dec F1AP_F1AP_PDU_t *f1enc = encode_f1ap_cu_configuration_update_acknowledge(&orig); F1AP_F1AP_PDU_t *f1dec = f1ap_encode_decode(f1enc); @@ -713,13 +731,14 @@ static void test_f1ap_cu_configuration_update_acknowledge(void) // Equality check ret = eq_f1ap_cu_configuration_update_acknowledge(&orig, &decoded); AssertFatal(ret, "eq_f1ap_cu_configuration_update_acknowledge(): decoded message doesn't match\n"); - // No free needed + free_f1ap_cu_configuration_update_acknowledge(&decoded); // Deep copy f1ap_gnb_cu_configuration_update_acknowledge_t cp = cp_f1ap_cu_configuration_update_acknowledge(&orig); // Equality check ret = eq_f1ap_cu_configuration_update_acknowledge(&orig, &cp); AssertFatal(ret, "eq_f1ap_cu_configuration_update_acknowledge(): copied message doesn't match\n"); - // No free needed + free_f1ap_cu_configuration_update_acknowledge(&cp); + free_f1ap_cu_configuration_update_acknowledge(&orig); } static byte_array_t get_test_ba(const char *s) diff --git a/openair2/GNB_APP/gnb_config.c b/openair2/GNB_APP/gnb_config.c index 0b3845f5b9..fadb402a71 100644 --- a/openair2/GNB_APP/gnb_config.c +++ b/openair2/GNB_APP/gnb_config.c @@ -1161,6 +1161,7 @@ static f1ap_setup_req_t *RC_read_F1Setup(uint64_t id, req->gNB_DU_id = id; req->gNB_DU_name = strdup(name); req->num_cells_available = 1; + req->cell = calloc_or_fail(req->num_cells_available, sizeof(*req->cell)); req->cell[0].info = *info; LOG_I(GNB_APP, "F1AP: gNB idx %d gNB_DU_id %ld, gNB_DU_name %s, TAC %d MCC/MNC/length %d/%d/%d cellID %ld\n", diff --git a/openair2/LAYER2/NR_MAC_gNB/config.c b/openair2/LAYER2/NR_MAC_gNB/config.c index dfa68773a4..ca1a516c0f 100644 --- a/openair2/LAYER2/NR_MAC_gNB/config.c +++ b/openair2/LAYER2/NR_MAC_gNB/config.c @@ -1093,14 +1093,17 @@ void prepare_du_configuration_update(gNB_MAC_INST *mac, { /* send gNB-DU configuration update to RRC */ f1ap_gnb_du_configuration_update_t update = { - .transaction_id = 1, - .num_status = 1, - .status[0].plmn = info->plmn, - .status[0].nr_cellid = info->nr_cellid, - .status[0].service_state = F1AP_STATE_IN_SERVICE, + .transaction_id = 1, + .num_status = 1, + .status = calloc_or_fail(1, sizeof(f1ap_cell_status_t)), }; + update.status[0].plmn = info->plmn; + update.status[0].nr_cellid = info->nr_cellid; + update.status[0].service_state = F1AP_STATE_IN_SERVICE; + if (mib && sib1) { - update.num_cells_to_modify = 1, + update.num_cells_to_modify = 1; + update.cell_to_modify = calloc_or_fail(1, sizeof(*update.cell_to_modify)); update.cell_to_modify[0].old_nr_cellid = info->nr_cellid; update.cell_to_modify[0].info = *info; update.cell_to_modify[0].sys_info = get_sys_info(mib, sib1, NULL); diff --git a/openair2/RRC/NR/rrc_gNB_du.c b/openair2/RRC/NR/rrc_gNB_du.c index 246df60626..302b45cac0 100644 --- a/openair2/RRC/NR/rrc_gNB_du.c +++ b/openair2/RRC/NR/rrc_gNB_du.c @@ -436,7 +436,8 @@ void rrc_gNB_process_f1_setup_req(f1ap_setup_req_t *req, sctp_assoc_t assoc_id) f1ap_setup_resp_t resp = {.transaction_id = req->transaction_id, .num_cells_to_activate = 1, - .cells_to_activate[0] = cell}; + .cells_to_activate = calloc_or_fail(1, sizeof(*resp.cells_to_activate))}; + resp.cells_to_activate[0] = cell; int num = read_version(TO_STRING(NR_RRC_VERSION), &resp.rrc_ver[0], &resp.rrc_ver[1], &resp.rrc_ver[2]); AssertFatal(num == 3, "could not read RRC version string %s\n", TO_STRING(NR_RRC_VERSION)); if (rrc->node_name != NULL) From 4c56c6b2de196229d7043076938fb5a2d66c0bf7 Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Thu, 5 Feb 2026 11:33:11 +0100 Subject: [PATCH 03/17] Refactor (RRC): improve UE context removal logging Replace generic UE event log with explicit INFO log including UE ID and RNTI when removing a UE context and centralize access to UE fields via a local pointer. rrc_delete_ue_data removes the all the list in the UE context, including the serving cells, and therefore the cell id should not be logged in the function responsible for the removal of the UE context, which at this point is already emptied and while the cells themselves are still there (in the DU/global cell structures), the lists in the UE context are empty. --- openair2/RRC/NR/rrc_gNB_UE_context.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openair2/RRC/NR/rrc_gNB_UE_context.c b/openair2/RRC/NR/rrc_gNB_UE_context.c index 86838927ab..01163e319f 100644 --- a/openair2/RRC/NR/rrc_gNB_UE_context.c +++ b/openair2/RRC/NR/rrc_gNB_UE_context.c @@ -155,10 +155,11 @@ void rrc_gNB_remove_ue_context(gNB_RRC_INST *rrc_instance_pP, rrc_gNB_ue_context return; } - LOG_UE_EVENT(&ue_context_pP->ue_context, "Remove UE context\n"); + gNB_RRC_UE_t *ue = &ue_context_pP->ue_context; + LOG_I(NR_RRC, "[--] (UE ID %d RNTI %04x) Remove UE context\n", ue->rrc_ue_id, ue->rnti); RB_REMOVE(rrc_nr_ue_tree_s, &rrc_instance_pP->rrc_ue_head, ue_context_pP); - uid_linear_allocator_free(&rrc_instance_pP->uid_allocator, ue_context_pP->ue_context.rrc_ue_id - 1); - cu_remove_f1_ue_data(ue_context_pP->ue_context.rrc_ue_id); + uid_linear_allocator_free(&rrc_instance_pP->uid_allocator, ue->rrc_ue_id - 1); + cu_remove_f1_ue_data(ue->rrc_ue_id); rrc_gNB_free_mem_ue_context(ue_context_pP); } From c6229a9c7a11cd83b6657589a7f809cf72cdfc6b Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Thu, 29 Jan 2026 14:39:03 +0100 Subject: [PATCH 04/17] RRC multi-cell: enforce unique PCI in per-cell neighbour lists Neighbour lists are built per serving cell in fill_neighbour_cell_configuration. Each neighbour in a list is identified by PCI; having two entries with the same PCI in one cell's list would be ambiguous and indicate a configuration error. This commit adds validation so that duplicate PCIs within a cell's neighbour list are detected at configuration load time and cause a fatal assertion with a clear error message. Changes: - Add eq_neighbour_pci() and get_neighbour_by_pci() using common/utils/alg/find.h to look up a neighbour by PCI in a seq_arr. - In fill_neighbour_cell_configuration(), before appending each cell's neighbour list, iterate over neighbours and assert that no PCI appears more than once --- openair2/GNB_APP/gnb_config.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/openair2/GNB_APP/gnb_config.c b/openair2/GNB_APP/gnb_config.c index fadb402a71..8edec8e84f 100644 --- a/openair2/GNB_APP/gnb_config.c +++ b/openair2/GNB_APP/gnb_config.c @@ -75,6 +75,7 @@ #include "s1ap_messages_types.h" #include "sctp_default_values.h" #include "seq_arr.h" +#include "common/utils/alg/find.h" #include "uper_encoder.h" #include "utils.h" #include "x2ap_messages_types.h" @@ -1851,6 +1852,22 @@ static seq_arr_t *fill_cu_sibs(paramdef_t *GNBparamarray) return SIBs; } +static bool eq_neighbour_pci(const void *vval, const void *vit) +{ + const int *pci = (const int *)vval; + const nr_neighbour_cell_t *neighbour = (const nr_neighbour_cell_t *)vit; + return neighbour->physicalCellId == *pci; +} + +/** @brief Find neighbour by PCI within a cell's neighbour list (first match) */ +static nr_neighbour_cell_t *get_neighbour_by_pci(seq_arr_t *neighbour_cells, int pci) +{ + elm_arr_t elm = find_if(neighbour_cells, &pci, eq_neighbour_pci); + if (elm.found) + return (nr_neighbour_cell_t *)elm.it; + return NULL; +} + static void fill_neighbour_cell_configuration(uint8_t gnb_idx, gNB_RRC_INST *rrc) { char gnbpath[MAX_OPTNAME_SIZE + 8]; @@ -1908,6 +1925,14 @@ static void fill_neighbour_cell_configuration(uint8_t gnb_idx, gNB_RRC_INST *rrc n.plmn.mcc = *NeighbourPlmn[GNB_MOBILE_COUNTRY_CODE_IDX].uptr; n.plmn.mnc = *NeighbourPlmn[GNB_MOBILE_NETWORK_CODE_IDX].uptr; n.plmn.mnc_digit_length = *NeighbourPlmn[GNB_MNC_DIGIT_LENGTH].uptr; + if (get_neighbour_by_pci(cell.neighbour_cells, n.physicalCellId) != NULL) { + LOG_E(GNB_APP, + "Cell %ld: duplicate PCI %d in neighbour list (nrcell_id %ld)\n", + cell.nr_cell_id, + n.physicalCellId, + n.nrcell_id); + AssertFatal(false, "PCI must be unique within a cell's neighbour list\n"); + } seq_arr_push_back(cell.neighbour_cells, &n, sizeof(n)); LOG_I(GNB_APP, " [%d] neighbor ID %d cellId %ld PCI %d SCS %d SSB ARFCN %u TAC %u PLMN %03u.%0*u\n", From b699884ae401f79e5e588254605751268f0b7d47 Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Mon, 5 Jan 2026 19:21:05 +0100 Subject: [PATCH 05/17] RRC multi-cell: add data structure definitions for multi-cell architecture Add foundational data structures in nr_rrc_defs.h for multi-cell handling in RRC, separating DU management from cell management. Design uses a global cell tree for O(log N) lookups while keeping cell ownership in DUs for locality and maintenance. The implementation maintains backward compatibility and follows the same patterns as existing DU and CUUP management in the codebase, i.e. RB-trees with logarithmic complexity for scalable multi-cell deployments. - Cell container structures: * nr_rrc_cell_container_t: cell-specific data (cell_id, pci, plmn, tac, frequency info, mib, sib1, mtc) * nr_rrc_cell_info_t: TDD/FDD modes; frequency structs nr_rrc_freq_info_t, nr_rrc_tdd_info_t, nr_rrc_fdd_info_t - UE serving cell tracking: * ue_serving_cell_t: nci, serving_cell_id (ServCellIndex TS 38.331, 0=PCell, 1-31=SCell), assoc_id for fast DU lookup * gNB_RRC_UE_t: seq_arr_t serving_cells, with PCell always being the first in the sequence - DU container refactoring: * nr_rrc_du_container_t: DU-only fields (gNB_DU_id, gNB_DU_name, rrc_ver); seq_arr_t cells for per-DU iteration; setup_req/mib/sib1/mtc kept for backward compatibility - Global cell tree: * gNB_RRC_INST_s: RB_HEAD(rrc_cell_tree, nr_rrc_cell_container_t) cells, num_cells (data only; lookup helpers get_cell_by_cell_id/get_cell_by_pci to be added separately) --- openair2/RRC/NR/nr_rrc_defs.h | 78 ++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/openair2/RRC/NR/nr_rrc_defs.h b/openair2/RRC/NR/nr_rrc_defs.h index 46e1ef3384..58f9ffd1fb 100644 --- a/openair2/RRC/NR/nr_rrc_defs.h +++ b/openair2/RRC/NR/nr_rrc_defs.h @@ -166,6 +166,21 @@ typedef struct { pdusession_level_qos_parameter_t qos; } nr_rrc_qos_t; +/** @brief UE serving cell information + * @note ServCellIndex is a short identity used to uniquely identify a serving cell + * (PCell, PSCell, or SCell) across cell groups (TS 38.331). + * Value 0 applies for the PCell, while the SCellIndex that has previously + * been assigned applies for SCells. + * @note Range: 0..maxNrofServingCells-1 where maxNrofServingCells = 32 */ +typedef struct { + /* NR Cell Identity (cell_id) */ + uint64_t nci; + /* ServCellIndex (TS 38.331): 0 = PCell, 1-31 = SCell */ + uint8_t serving_cell_id; + /* SCTP association ID of the DU that owns this cell (for fast lookup) */ + sctp_assoc_t assoc_id; +} ue_serving_cell_t; + /* forward declaration */ typedef struct nr_handover_context_s nr_handover_context_t; @@ -208,6 +223,9 @@ typedef struct gNB_RRC_UE_s { NR_EstablishmentCause_t establishment_cause; uint64_t nr_cellid; + /* Dynamic array of UE serving cells */ + seq_arr_t serving_cells; /* ue_serving_cell_t */ + uint32_t rrc_ue_id; uint64_t amf_ue_ngap_id; // Globally Unique AMF Identifier @@ -335,15 +353,69 @@ typedef struct cucp_cuup_if_s { cucp_cuup_bearer_context_release_func_t bearer_context_release; } cucp_cuup_if_t; +typedef struct { + int band; + uint32_t arfcn; + uint8_t scs; + uint16_t nrb; +} nr_rrc_freq_info_t; + +typedef struct { + nr_rrc_freq_info_t dlul; +} nr_rrc_tdd_info_t; + +typedef struct { + nr_rrc_freq_info_t dl; + nr_rrc_freq_info_t ul; +} nr_rrc_fdd_info_t; + +typedef struct { + /* operating mode: TDD or FDD */ + enum { NR_MODE_TDD = 0, NR_MODE_FDD = 1 } mode; + uint64_t cell_id; + uint16_t pci; + union { + nr_rrc_tdd_info_t tdd; + nr_rrc_fdd_info_t fdd; + }; + plmn_id_t plmn; + uint16_t tac; +} nr_rrc_cell_info_t; + +typedef struct nr_rrc_cell_container_t { + /* Tree-related data */ + RB_ENTRY(nr_rrc_cell_container_t) entries; + /* transport association */ + sctp_assoc_t assoc_id; + /* Cell-only RRC-local info */ + nr_rrc_cell_info_t info; + /* MIB message (6.2.2 TS 38.331) */ + NR_MIB_t *mib; + /* SIB1 message (6.2.2 TS 38.331) */ + NR_SIB1_t *sib1; + /* MeasurementTimingConfiguration inter-node (TS 38.331) */ + NR_MeasurementTimingConfiguration_t *mtc; +} nr_rrc_cell_container_t; + typedef struct nr_rrc_du_container_t { /* Tree-related data */ RB_ENTRY(nr_rrc_du_container_t) entries; - + /* DU-only information */ + /* Transport association identifier for this DU */ sctp_assoc_t assoc_id; + /* DU identity */ + uint64_t gNB_DU_id; + /* DU name */ + char *gNB_DU_name; + /* RRC version */ + uint8_t rrc_ver[3]; + // Cell-specific information (F1AP Setup Request, MIB, SIB1, MeasurementTimingConfiguration) f1ap_setup_req_t *setup_req; NR_MIB_t *mib; NR_SIB1_t *sib1; NR_MeasurementTimingConfiguration_t *mtc; + /* Cells, indexed by cell_id */ + seq_arr_t cells; /* nr_rrc_cell_container_t* */ } nr_rrc_du_container_t; typedef struct nr_rrc_cuup_container_t { @@ -387,6 +459,10 @@ typedef struct gNB_RRC_INST_s { RB_HEAD(rrc_du_tree, nr_rrc_du_container_t) dus; // DUs, indexed by assoc_id size_t num_dus; + /* Global cell tree, indexed by cell_id */ + RB_HEAD(rrc_cell_tree, nr_rrc_cell_container_t) cells; + size_t num_cells; + RB_HEAD(rrc_cuup_tree, nr_rrc_cuup_container_t) cuups; // CU-UPs, indexed by assoc_id size_t num_cuups; From 3dfdc66c41320a4a4d4c3f984c6998dafffa3fb5 Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Mon, 5 Jan 2026 19:39:10 +0100 Subject: [PATCH 06/17] RRC multi-cell: add cell management library with lookup and cleanup This commit introduces a new library module (rrc_cell_management.c/h) to centralise functions that provides cell lookup, DU-specific cell management, and cell lifecycle managements. The design uses a dual-tree architecture: - Global cell tree: RB-tree at RRC level (rrc->cells) keyed by cell_id, used for O(log N_CELL) lookups and for maintaining num_cells. - DU-specific storage: seq_arr_t per DU (du->cells) for iteration and management of cells belonging to each DU. API added: - Global tree: get_cell_by_cell_id()(finds cell using global RB-tree lookup (O(log N_CELL) complexity); rrc_add_cell(); rrc_rm_cell() (rrc_rm_cell removes from tree, decrements num_cells, and frees the cell via rrc_free_cell_container). - DU-local seq_arr: rrc_get_cell_for_du(), rrc_get_cell_by_pci_for_du(), rrc_add_cell_to_du() (with duplicate cell_id/PCI checks). - Cleanup: rrc_free_cell_container() (frees ASN.1 MIB/SIB1/MTC and container Build: add rrc_cell_management static library in openair2/RRC/NR; link it into nr_rrc from top-level CMakeLists.txt. --- CMakeLists.txt | 2 +- openair2/RRC/NR/CMakeLists.txt | 5 + openair2/RRC/NR/rrc_cell_management.c | 182 ++++++++++++++++++++++++++ openair2/RRC/NR/rrc_cell_management.h | 47 +++++++ 4 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 openair2/RRC/NR/rrc_cell_management.c create mode 100644 openair2/RRC/NR/rrc_cell_management.h diff --git a/CMakeLists.txt b/CMakeLists.txt index bae1c590bc..5b0dae95ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -342,7 +342,7 @@ target_link_libraries(lte_rrc PRIVATE nr_rrc) #NR RRC ####### add_library(nr_rrc ${OPENAIR2_DIR}/RRC/NR/MESSAGES/asn1_msg.c) -target_link_libraries(nr_rrc PUBLIC asn1_nr_rrc asn1_lte_rrc) +target_link_libraries(nr_rrc PUBLIC asn1_nr_rrc asn1_lte_rrc rrc_cell_management) # S1AP ############## diff --git a/openair2/RRC/NR/CMakeLists.txt b/openair2/RRC/NR/CMakeLists.txt index 658bfeee5a..9ca17a42bd 100644 --- a/openair2/RRC/NR/CMakeLists.txt +++ b/openair2/RRC/NR/CMakeLists.txt @@ -1,5 +1,10 @@ add_subdirectory(MESSAGES) +# Library for RRC cell management functions +add_library(rrc_cell_management STATIC rrc_cell_management.c) +target_link_libraries(rrc_cell_management PUBLIC asn1_nr_rrc asn1_lte_rrc) +target_link_libraries(rrc_cell_management PRIVATE utils LOG asn1_nr_rrc_hdrs asn1_lte_rrc_hdrs) + if(ENABLE_TESTS) add_subdirectory(tests) endif() diff --git a/openair2/RRC/NR/rrc_cell_management.c b/openair2/RRC/NR/rrc_cell_management.c new file mode 100644 index 0000000000..ebe8f78961 --- /dev/null +++ b/openair2/RRC/NR/rrc_cell_management.c @@ -0,0 +1,182 @@ +/* + * 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 + */ + +#include +#include +#include "rrc_cell_management.h" +#include "common/utils/assertions.h" // for DevAssert +#include "common/utils/collection/tree.h" // for RB_FOREACH +#include "common/utils/alg/find.h" // for find_if +#include "common/utils/ds/seq_arr.h" // for seq_arr_t, seq_arr_free +#include "common/utils/LOG/log.h" // for LOG_I, LOG_W, LOG_E +#include "asn_application.h" // for ASN_STRUCT_FREE +#include "NR_MIB.h" // for asn_DEF_NR_MIB +#include "NR_SIB1.h" // for asn_DEF_NR_SIB1 +#include "NR_MeasurementTimingConfiguration.h" // for asn_DEF_NR_MeasurementTimingConfiguration + +/** @brief Comparison function for cell tree + * @param[in] a Pointer to first cell container + * @param[in] b Pointer to second cell container + * @return Comparison result */ +int rrc_cell_cmp(const struct nr_rrc_cell_container_t *a, const struct nr_rrc_cell_container_t *b) +{ + if (a->info.cell_id < b->info.cell_id) + return -1; + if (a->info.cell_id > b->info.cell_id) + return 1; + return 0; +} + +/** @brief Generate RB tree functions for cell tree */ +RB_GENERATE(rrc_cell_tree, nr_rrc_cell_container_t, entries, rrc_cell_cmp); + +/** @brief Find cell container by cell ID (lookup in global cell tree) + * @param[in] cells Pointer to global cell tree + * @param[in] cell_id Cell ID + * @return Pointer to the cell container, or NULL if not found + * @note O(log N_CELL) using RB-tree lookup */ +nr_rrc_cell_container_t *get_cell_by_cell_id(struct rrc_cell_tree *cells, const uint64_t cell_id) +{ + DevAssert(cells != NULL); + nr_rrc_cell_container_t search_key = {.info = {.cell_id = cell_id}}; + return RB_FIND(rrc_cell_tree, cells, &search_key); +} + +/** @brief Add cell to global cell tree and increment rrc->num_cells on success + * @param[in] rrc Pointer to RRC instance + * @param[in] cell Pointer to cell container to add + * @return NULL if inserted successfully; pointer to existing cell (collision) if duplicate cell_id */ +nr_rrc_cell_container_t *rrc_add_cell(gNB_RRC_INST *rrc, nr_rrc_cell_container_t *cell) +{ + DevAssert(rrc != NULL); + DevAssert(cell != NULL); + nr_rrc_cell_container_t *collision = RB_INSERT(rrc_cell_tree, &rrc->cells, cell); + if (collision == NULL) + rrc->num_cells++; + return collision; +} + +/** @brief Remove cell from global cell tree, decrement rrc->num_cells, and free the cell + * @param[in] rrc Pointer to RRC instance + * @param[in] cell Pointer to cell container to remove */ +void rrc_rm_cell(gNB_RRC_INST *rrc, nr_rrc_cell_container_t *cell) +{ + DevAssert(rrc != NULL); + DevAssert(cell != NULL); + nr_rrc_cell_container_t *removed = RB_REMOVE(rrc_cell_tree, &rrc->cells, cell); + AssertFatal(removed == cell, "Failed to remove cell %ld from tree\n", cell->info.cell_id); + rrc->num_cells--; + rrc_free_cell_container(cell); +} + +static bool eq_cell_id(const void *vval, const void *vit) +{ + const uint64_t *id = (const uint64_t *)vval; + const nr_rrc_cell_container_t **elem_ptr = (const nr_rrc_cell_container_t **)vit; + return (*elem_ptr) != NULL && (*elem_ptr)->info.cell_id == *id; +} + +/** @brief Get cell by cell_id from DU's cells array + * @param[in] cells Pointer to DU's cells sequence array + * @param[in] cell_id Cell ID to search for + * @return Pointer to the cell container, or NULL if not found */ +nr_rrc_cell_container_t *rrc_get_cell_for_du(seq_arr_t *cells, uint64_t cell_id) +{ + DevAssert(cells); + elm_arr_t elm = find_if(cells, &cell_id, eq_cell_id); + if (elm.found) { + // elm.it points to an element in seq_arr, which stores nr_rrc_cell_container_t* + nr_rrc_cell_container_t **cell_ptr = (nr_rrc_cell_container_t **)elm.it; + return *cell_ptr; + } + return NULL; +} + +static bool eq_pci_in_du(const void *vval, const void *vit) +{ + const uint16_t *pci = (const uint16_t *)vval; + const nr_rrc_cell_container_t **elem_ptr = (const nr_rrc_cell_container_t **)vit; + return (*elem_ptr) != NULL && (*elem_ptr)->info.pci == *pci; +} + +/** @brief Get cell by PCI from DU's cells array + * @param[in] cells Pointer to DU's cells sequence array + * @param[in] pci PCI to search for + * @return Pointer to the cell container, or NULL if not found + * @note PCI must be unique within a DU; returns the first match */ +nr_rrc_cell_container_t *rrc_get_cell_by_pci_for_du(const seq_arr_t *cells, uint16_t pci) +{ + DevAssert(cells); + elm_arr_t elm = find_if((seq_arr_t *)cells, &pci, eq_pci_in_du); + if (elm.found) { + // elm.it points to an element in seq_arr, which stores nr_rrc_cell_container_t* + nr_rrc_cell_container_t **cell_ptr = (nr_rrc_cell_container_t **)elm.it; + return *cell_ptr; + } + return NULL; +} + +/** @brief Add cell to DU's cells array + * @param[in] cells Pointer to DU's cells sequence array + * @param[in] cell Pointer to cell container to add + * @return Pointer to the added cell, or NULL on duplicate cell_id/PCI or error + * @note Rejects duplicate cell_id or PCI within the DU */ +nr_rrc_cell_container_t *rrc_add_cell_to_du(seq_arr_t *cells, nr_rrc_cell_container_t *cell) +{ + DevAssert(cells); + DevAssert(cell); + + // Check for duplicate cell_id within this DU + nr_rrc_cell_container_t *exists = rrc_get_cell_for_du(cells, cell->info.cell_id); + if (exists) { + LOG_E(NR_RRC, "Trying to add an already existing cell with ID=%lu\n", cell->info.cell_id); + return NULL; + } + + // Check for duplicate PCI within this DU (PCI must be unique within a DU) + nr_rrc_cell_container_t *existing_pci = rrc_get_cell_by_pci_for_du(cells, cell->info.pci); + if (existing_pci != NULL) { + LOG_E(NR_RRC, + "Trying to add cell with ID=%lu, but PCI %d already exists in this DU (cell ID %lu)\n", + cell->info.cell_id, + cell->info.pci, + existing_pci->info.cell_id); + return NULL; + } + + seq_arr_push_back(cells, &cell, sizeof(nr_rrc_cell_container_t *)); + LOG_I(NR_RRC, "Added cell %lu to DU cells array (total cells = %ld)\n", cell->info.cell_id, seq_arr_size(cells)); + + return cell; +} + +/** @brief Free a cell container and all its associated resources + * @param[in] cell Pointer to the cell container to free + * @note Frees ASN.1 structures (mib, sib1, mtc) and the cell container itself + * @note Does NOT remove the cell from the tree - caller must do RB_REMOVE first */ +void rrc_free_cell_container(nr_rrc_cell_container_t *cell) +{ + DevAssert(cell); + ASN_STRUCT_FREE(asn_DEF_NR_MIB, cell->mib); + ASN_STRUCT_FREE(asn_DEF_NR_SIB1, cell->sib1); + ASN_STRUCT_FREE(asn_DEF_NR_MeasurementTimingConfiguration, cell->mtc); + free(cell); +} diff --git a/openair2/RRC/NR/rrc_cell_management.h b/openair2/RRC/NR/rrc_cell_management.h new file mode 100644 index 0000000000..d083b3d174 --- /dev/null +++ b/openair2/RRC/NR/rrc_cell_management.h @@ -0,0 +1,47 @@ +/* + * 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 + */ + +#ifndef RRC_CELL_MANAGEMENT_H_ +#define RRC_CELL_MANAGEMENT_H_ + +#include "nr_rrc_defs.h" // for nr_rrc_du_container_t, nr_rrc_cell_container_t, RB_HEAD +#include "common/utils/collection/tree.h" +#include +#include + +// Generate RB tree functions (prototypes) +int rrc_cell_cmp(const struct nr_rrc_cell_container_t *a, const struct nr_rrc_cell_container_t *b); +RB_PROTOTYPE(rrc_cell_tree, nr_rrc_cell_container_t, entries, rrc_cell_cmp); + +// Cell management (global cell tree) +nr_rrc_cell_container_t *get_cell_by_cell_id(struct rrc_cell_tree *cells, const uint64_t cell_id); +nr_rrc_cell_container_t *rrc_add_cell(gNB_RRC_INST *rrc, nr_rrc_cell_container_t *cell); +void rrc_rm_cell(gNB_RRC_INST *rrc, nr_rrc_cell_container_t *cell); + +// Cell management (DU-specific cell tree) +nr_rrc_cell_container_t *rrc_get_cell_for_du(seq_arr_t *cells, uint64_t cell_id); +nr_rrc_cell_container_t *rrc_get_cell_by_pci_for_du(const seq_arr_t *cells, uint16_t pci); +nr_rrc_cell_container_t *rrc_add_cell_to_du(seq_arr_t *cells, nr_rrc_cell_container_t *cell); + +// Cell cleanup +void rrc_free_cell_container(nr_rrc_cell_container_t *cell); + +#endif /* RRC_CELL_MANAGEMENT_H_ */ From 6d0c7945c2b3891c1bde3801cb5e82a6ab740c4e Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Tue, 6 Jan 2026 15:30:58 +0100 Subject: [PATCH 07/17] RRC multi-cell: extract DU management to cell management library Move DU tree management from rrc_gNB_du.c into rrc_cell_management so that DU and cell lookup logic live in one place, improving maintainability and testability (there is no DU lib). - rrc_cell_management: add du_compare(), RB_GENERATE(rrc_du_tree), get_du_by_assoc_id(), rrc_add_du(), rrc_rm_du(), rrc_cleanup_du(), rrc_free_du_container(); declare them in rrc_cell_management.h. - rrc_gNB_du: move du_compare(), RB_GENERATE, get_du_by_assoc_id() to rrc_cell_management --- openair2/RRC/NR/rrc_cell_management.c | 83 +++++++++++++++++++++++++++ openair2/RRC/NR/rrc_cell_management.h | 9 +++ openair2/RRC/NR/rrc_gNB.c | 1 + openair2/RRC/NR/rrc_gNB_du.c | 19 +----- openair2/RRC/NR/rrc_gNB_du.h | 1 - 5 files changed, 94 insertions(+), 19 deletions(-) diff --git a/openair2/RRC/NR/rrc_cell_management.c b/openair2/RRC/NR/rrc_cell_management.c index ebe8f78961..eac0f38260 100644 --- a/openair2/RRC/NR/rrc_cell_management.c +++ b/openair2/RRC/NR/rrc_cell_management.c @@ -32,6 +32,89 @@ #include "NR_SIB1.h" // for asn_DEF_NR_SIB1 #include "NR_MeasurementTimingConfiguration.h" // for asn_DEF_NR_MeasurementTimingConfiguration +/** @brief Comparison function for DU tree + * @param[in] a Pointer to first DU container + * @param[in] b Pointer to second DU container + * @return Comparison result */ +int du_compare(const nr_rrc_du_container_t *a, const nr_rrc_du_container_t *b) +{ + if (a->assoc_id > b->assoc_id) + return 1; + if (a->assoc_id == b->assoc_id) + return 0; + return -1; /* a->assoc_id < b->assoc_id */ +} + +// Generate RB tree functions for DU tree +RB_GENERATE(rrc_du_tree, nr_rrc_du_container_t, entries, du_compare); + +/** @brief Find DU by SCTP association ID (uses DU RB-tree lookup) + * @param[in] rrc Pointer to RRC instance + * @param[in] assoc_id SCTP association ID + * @return Pointer to the DU container, or NULL if not found + * @note O(log N_DU) complexity using DU RB-tree lookup */ +nr_rrc_du_container_t *get_du_by_assoc_id(gNB_RRC_INST *rrc, const sctp_assoc_t assoc_id) +{ + DevAssert(rrc != NULL); + nr_rrc_du_container_t e = {.assoc_id = assoc_id}; + return RB_FIND(rrc_du_tree, &rrc->dus, &e); +} + +/** @brief Add DU to RRC DU tree and increment rrc->num_dus on success + * @param[in] rrc Pointer to RRC instance + * @param[in] du Pointer to DU container to add + * @return NULL if inserted successfully; pointer to existing DU (collision) if duplicate assoc_id */ +nr_rrc_du_container_t *rrc_add_du(gNB_RRC_INST *rrc, nr_rrc_du_container_t *du) +{ + DevAssert(rrc != NULL); + DevAssert(du != NULL); + nr_rrc_du_container_t *collision = RB_INSERT(rrc_du_tree, &rrc->dus, du); + if (collision == NULL) + rrc->num_dus++; + return collision; +} + +/** @brief Free a DU container and its allocated fields (gNB_DU_name); free the DU itself + * @param[in] du Pointer to DU container to free + * @note Does NOT remove the DU from the tree or free du->cells; caller must do RB_REMOVE and seq_arr_free first */ +void rrc_free_du_container(nr_rrc_du_container_t *du) +{ + DevAssert(du != NULL); + free(du->gNB_DU_name); + free(du); +} + +/** @brief Remove DU from RRC DU tree, decrement rrc->num_dus, and free the DU and its resources + * @param[in] rrc Pointer to RRC instance + * @param[in] du Pointer to DU container to remove (caller must have removed and freed all cells first) */ +void rrc_rm_du(gNB_RRC_INST *rrc, nr_rrc_du_container_t *du) +{ + DevAssert(rrc != NULL); + DevAssert(du != NULL); + nr_rrc_du_container_t *removed = RB_REMOVE(rrc_du_tree, &rrc->dus, du); + AssertFatal(removed == du, "Failed to remove DU (assoc_id %d) from tree\n", du->assoc_id); + rrc->num_dus--; + seq_arr_free(&du->cells, NULL); + rrc_free_du_container(du); +} + +/** @brief Remove all cells from the DU (and from global cell tree), remove the DU from the tree, and free the DU + * @param[in] rrc Pointer to RRC instance + * @param[in] du Pointer to DU container to cleanup (removed from tree and freed) */ +void rrc_cleanup_du(gNB_RRC_INST *rrc, nr_rrc_du_container_t *du) +{ + DevAssert(rrc != NULL); + DevAssert(du != NULL); + for (size_t i = seq_arr_size(&du->cells); i > 0; i--) { + nr_rrc_cell_container_t **cell_ptr = seq_arr_at(&du->cells, i - 1); + nr_rrc_cell_container_t *cell = *cell_ptr; + LOG_I(NR_RRC, "Removing cell %lu from DU cells array\n", cell->info.cell_id); + seq_arr_erase(&du->cells, cell_ptr); + rrc_rm_cell(rrc, cell); + } + rrc_rm_du(rrc, du); +} + /** @brief Comparison function for cell tree * @param[in] a Pointer to first cell container * @param[in] b Pointer to second cell container diff --git a/openair2/RRC/NR/rrc_cell_management.h b/openair2/RRC/NR/rrc_cell_management.h index d083b3d174..76790fda4f 100644 --- a/openair2/RRC/NR/rrc_cell_management.h +++ b/openair2/RRC/NR/rrc_cell_management.h @@ -30,6 +30,15 @@ // Generate RB tree functions (prototypes) int rrc_cell_cmp(const struct nr_rrc_cell_container_t *a, const struct nr_rrc_cell_container_t *b); RB_PROTOTYPE(rrc_cell_tree, nr_rrc_cell_container_t, entries, rrc_cell_cmp); +int du_compare(const struct nr_rrc_du_container_t *a, const struct nr_rrc_du_container_t *b); +RB_PROTOTYPE(rrc_du_tree, nr_rrc_du_container_t, entries, du_compare); + +// DU management (global DU tree) +nr_rrc_du_container_t *rrc_add_du(gNB_RRC_INST *rrc, nr_rrc_du_container_t *du); +void rrc_rm_du(gNB_RRC_INST *rrc, nr_rrc_du_container_t *du); +void rrc_cleanup_du(gNB_RRC_INST *rrc, nr_rrc_du_container_t *du); +void rrc_free_du_container(nr_rrc_du_container_t *du); +nr_rrc_du_container_t *get_du_by_assoc_id(gNB_RRC_INST *rrc, const sctp_assoc_t assoc_id); // Cell management (global cell tree) nr_rrc_cell_container_t *get_cell_by_cell_id(struct rrc_cell_tree *cells, const uint64_t cell_id); diff --git a/openair2/RRC/NR/rrc_gNB.c b/openair2/RRC/NR/rrc_gNB.c index 6b914c27e9..fd92bca995 100644 --- a/openair2/RRC/NR/rrc_gNB.c +++ b/openair2/RRC/NR/rrc_gNB.c @@ -84,6 +84,7 @@ #include "rrc_gNB_du.h" #include "rrc_gNB_mobility.h" #include "rrc_gNB_radio_bearers.h" +#include "rrc_cell_management.h" #include "rrc_messages_types.h" #include "rrc_gNB_asn1.h" #include "seq_arr.h" diff --git a/openair2/RRC/NR/rrc_gNB_du.c b/openair2/RRC/NR/rrc_gNB_du.c index 302b45cac0..83e158546d 100644 --- a/openair2/RRC/NR/rrc_gNB_du.c +++ b/openair2/RRC/NR/rrc_gNB_du.c @@ -20,6 +20,7 @@ */ #include "rrc_gNB_du.h" +#include "rrc_cell_management.h" #include #include #include @@ -84,18 +85,6 @@ int get_ssb_arfcn(const struct nr_rrc_du_container_t *du) return ssb_arfcn_mtc(du->mtc); } -static int du_compare(const nr_rrc_du_container_t *a, const nr_rrc_du_container_t *b) -{ - if (a->assoc_id > b->assoc_id) - return 1; - if (a->assoc_id == b->assoc_id) - return 0; - return -1; /* a->assoc_id < b->assoc_id */ -} - -/* Tree management functions */ -RB_GENERATE/*_STATIC*/(rrc_du_tree, nr_rrc_du_container_t, entries, du_compare); - static bool rrc_gNB_plmn_matches(const gNB_RRC_INST *rrc, const f1ap_served_cell_info_t *info) { const gNB_RrcConfigurationReq *conf = &rrc->configuration; @@ -629,12 +618,6 @@ nr_rrc_du_container_t *get_du_for_ue(gNB_RRC_INST *rrc, uint32_t ue_id) return get_du_by_assoc_id(rrc, ue_data.du_assoc_id); } -nr_rrc_du_container_t *get_du_by_assoc_id(gNB_RRC_INST *rrc, sctp_assoc_t assoc_id) -{ - nr_rrc_du_container_t e = {.assoc_id = assoc_id}; - return RB_FIND(rrc_du_tree, &rrc->dus, &e); -} - /* \brief find DU by cell ID. Note: currently the CU is limited to one cell per * DU, hence here, DU == cell. Modify this to look up a specific cell. */ nr_rrc_du_container_t *get_du_by_cell_id(gNB_RRC_INST *rrc, uint64_t cell_id) diff --git a/openair2/RRC/NR/rrc_gNB_du.h b/openair2/RRC/NR/rrc_gNB_du.h index 514416211f..4206096aa2 100644 --- a/openair2/RRC/NR/rrc_gNB_du.h +++ b/openair2/RRC/NR/rrc_gNB_du.h @@ -38,7 +38,6 @@ void rrc_CU_process_f1_lost_connection(struct gNB_RRC_INST_s *rrc, struct f1ap_l void rrc_gNB_process_f1_du_configuration_update(struct f1ap_gnb_du_configuration_update_s *conf_up, sctp_assoc_t assoc_id); struct nr_rrc_du_container_t *get_du_for_ue(struct gNB_RRC_INST_s *rrc, uint32_t ue_id); -struct nr_rrc_du_container_t *get_du_by_assoc_id(struct gNB_RRC_INST_s *rrc, sctp_assoc_t assoc_id); const struct f1ap_served_cell_info_t *get_cell_information_by_phycellId(int phyCellId); struct nr_rrc_du_container_t *get_du_by_cell_id(struct gNB_RRC_INST_s *rrc, uint64_t cell_id); From 2ff071a52e47d0541a8ba8756821a2a6070f2ccb Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Tue, 6 Jan 2026 00:18:09 +0100 Subject: [PATCH 08/17] RRC multi-cell: add UE serving cell management functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add functions to manage UE serving cells so the RRC can track which cells a UE is using (PCell and SCells) for multi-cell UE context and future SCell management. - rrc_cell_management.c / rrc_cell_management.h: * rrc_get_ue_serving_cell_by_id(): return serving cell by servCellIndex (O(k) linear search, k ≤ 32). * rrc_add_ue_serving_cell(ue, cell, serving_cell_id): add entry to UE serving_cells with validation: reject duplicate servCellIndex with different nci, enforce max 32 serving cells (TS 38.331), keep PCell at index 0 for O(1) access; return existing entry if same servCellIndex and nci. * rrc_remove_ue_scells_from_du(ue, assoc_id): remove all serving cells for the given DU (handover/DU disconnect); use stored assoc_id for O(k) comparison. * ue_get_pcell_entry(): return PCell entry (first element, O(1)). * rrc_get_pcell_for_ue(): return PCell container from cell tree using UE's PCell entry. * Header: RRC_PCELL_INDEX (0), RRC_MAX_NUM_SERVING_CELLS (32), and declarations for the above. - UE context lifecycle: * rrc_gNB_create_ue_context(): seq_arr_init(serving_cells). * rrc_delete_ue_data(): seq_arr_free(serving_cells). --- openair2/RRC/NR/rrc_cell_management.c | 146 ++++++++++++++++++++++++++ openair2/RRC/NR/rrc_cell_management.h | 17 +++ openair2/RRC/NR/rrc_gNB.c | 1 + openair2/RRC/NR/rrc_gNB_UE_context.c | 3 + 4 files changed, 167 insertions(+) diff --git a/openair2/RRC/NR/rrc_cell_management.c b/openair2/RRC/NR/rrc_cell_management.c index eac0f38260..0df2665c04 100644 --- a/openair2/RRC/NR/rrc_cell_management.c +++ b/openair2/RRC/NR/rrc_cell_management.c @@ -27,6 +27,7 @@ #include "common/utils/alg/find.h" // for find_if #include "common/utils/ds/seq_arr.h" // for seq_arr_t, seq_arr_free #include "common/utils/LOG/log.h" // for LOG_I, LOG_W, LOG_E +#include "openair2/F1AP/f1ap_ids.h" // for cu_exists_f1_ue_data, cu_get_f1_ue_data #include "asn_application.h" // for ASN_STRUCT_FREE #include "NR_MIB.h" // for asn_DEF_NR_MIB #include "NR_SIB1.h" // for asn_DEF_NR_SIB1 @@ -263,3 +264,148 @@ void rrc_free_cell_container(nr_rrc_cell_container_t *cell) ASN_STRUCT_FREE(asn_DEF_NR_MeasurementTimingConfiguration, cell->mtc); free(cell); } + +static bool eq_serving_cell_id(const void *vval, const void *vit) +{ + const uint8_t *id = (const uint8_t *)vval; + const ue_serving_cell_t *elem = (const ue_serving_cell_t *)vit; + return elem->serving_cell_id == *id; +} + +/** @brief Get serving cell from UE's serving_cells array by serving_cell_id + * @param[in] ue UE context + * @param[in] serving_cell_id ServCellIndex (0 = PCell, 1-31 = SCell) + * @return Pointer to serving cell entry, or NULL if not found */ +ue_serving_cell_t *rrc_get_ue_serving_cell_by_id(const gNB_RRC_UE_t *ue, uint8_t serving_cell_id) +{ + DevAssert(ue); + elm_arr_t elm = find_if((seq_arr_t *)&ue->serving_cells, &serving_cell_id, eq_serving_cell_id); + if (elm.found) + return (ue_serving_cell_t *)elm.it; + return NULL; +} + +/** @brief Add a serving cell to UE's serving_cells array + * @param[in] ue UE context + * @param[in] cell Cell container (nci and assoc_id taken from here) + * @param[in] serving_cell_id ServCellIndex (0 = PCell, 1-31 = SCell) + * @return Pointer to the added or existing entry, or NULL on error + * @note Uniqueness: (serving_cell_id, nci) must be unique. Same pair returns existing; + * same serving_cell_id, different nci returns NULL. */ +ue_serving_cell_t *rrc_add_ue_serving_cell(gNB_RRC_UE_t *ue, const nr_rrc_cell_container_t *cell, uint8_t serving_cell_id) +{ + DevAssert(ue); + DevAssert(serving_cell_id < RRC_MAX_NUM_SERVING_CELLS); + + // Check maximum number of items in list + size_t current_count = seq_arr_size(&ue->serving_cells); + if (current_count >= RRC_MAX_NUM_SERVING_CELLS) { + LOG_E(NR_RRC, "Maximum number of serving cells (%d) reached\n", RRC_MAX_NUM_SERVING_CELLS); + return NULL; + } + + // Check if serving cell with this serving_cell_id already exists + ue_serving_cell_t *existing = rrc_get_ue_serving_cell_by_id(ue, serving_cell_id); + if (existing != NULL) { + // If it's the same nci, return existing + if (existing->nci == cell->info.cell_id) { + LOG_D(NR_RRC, "Serving cell %d already has cell %ld\n", serving_cell_id, cell->info.cell_id); + return existing; + } + // Reject if already exists with different nci + LOG_E(NR_RRC, "Serving cell with serving_cell_id %d already exists with different nci\n", serving_cell_id); + return NULL; + } + + LOG_I(NR_RRC, + "Adding new %s (cell %ld, assoc_id %d)\n", + serving_cell_id == RRC_PCELL_INDEX ? "PCell" : "SCell", + cell->info.cell_id, + cell->assoc_id); + ue_serving_cell_t new_cell = {.nci = cell->info.cell_id, .serving_cell_id = serving_cell_id, .assoc_id = cell->assoc_id}; + + size_t elt_size = sizeof(ue_serving_cell_t); + if (serving_cell_id == RRC_PCELL_INDEX) { + /* PCell must match F1 UE data (serving_cells and F1 UE data stay in sync). */ + DevAssert(cu_exists_f1_ue_data(ue->rrc_ue_id)); + DevAssert(cu_get_f1_ue_data(ue->rrc_ue_id).du_assoc_id == cell->assoc_id); + /* Ensure PCell is always at index 0 in serving_cells */ + size_t n = seq_arr_size(&ue->serving_cells); + seq_arr_push_back(&ue->serving_cells, &new_cell, elt_size); + if (n > 0) { + /* If there are existing cells, shift them right to make room for the new PCell */ + void *first = seq_arr_front(&ue->serving_cells); + memmove((uint8_t *)first + elt_size, first, n * elt_size); + memcpy(first, &new_cell, elt_size); + } + } else { + /* SCells are appended after PCell (if any) */ + seq_arr_push_back(&ue->serving_cells, &new_cell, elt_size); + } + return rrc_get_ue_serving_cell_by_id(ue, serving_cell_id); +} + +/** @brief Remove serving cells that belong to a specific DU + * @param[in] ue UE context + * @param[in] assoc_id Association ID of the DU + * @note Removes all serving cells (including PCell) that belong to the specified DU. + * Uses stored assoc_id for O(k) comparison without cell lookup. */ +void rrc_remove_ue_scells_from_du(gNB_RRC_UE_t *ue, sctp_assoc_t assoc_id) +{ + DevAssert(ue); + LOG_I(NR_RRC, "Removing SCells from DU (assoc_id=%d) for UE %d\n", assoc_id, ue->rrc_ue_id); + + // Collect indices of SCells to delete (don't modify array while iterating) + uint32_t indices_to_delete[RRC_MAX_NUM_SERVING_CELLS]; + size_t num_to_delete = 0; + uint32_t index = 0; + + FOR_EACH_SEQ_ARR (ue_serving_cell_t *, scell, &ue->serving_cells) { + // Use stored assoc_id for direct comparison + if (scell->assoc_id == assoc_id) { + indices_to_delete[num_to_delete++] = index; + } + index++; + } + + // Now delete the collected serving cells from end to beginning for safety + for (size_t i = num_to_delete; i > 0; i--) { + uint32_t idx = indices_to_delete[i - 1]; + void *it = seq_arr_at(&ue->serving_cells, idx); + seq_arr_erase(&ue->serving_cells, it); + } + +} + +/** @brief Get PCell serving cell entry for a UE based on serving_cells + * @param[in] ue UE context + * @return Pointer to serving cell entry for PCell, or NULL if not found + * @note PCell is always at index 0. */ +const ue_serving_cell_t *ue_get_pcell_entry(const gNB_RRC_UE_t *ue) +{ + DevAssert(ue); + size_t count = seq_arr_size(&ue->serving_cells); + if (count == 0) + return NULL; + + /* PCell is always the first serving cell */ + const ue_serving_cell_t *first = seq_arr_front(&ue->serving_cells); + DevAssert(first != NULL); + DevAssert(first->serving_cell_id == RRC_PCELL_INDEX); + return first; +} + +/** @brief Get UE's PCell container from the global cell tree + * @param[in] rrc RRC instance (global cell tree) + * @param[in] ue UE context + * @return Pointer to PCell container, or NULL if UE has no PCell or cell not in tree */ +nr_rrc_cell_container_t *rrc_get_pcell_for_ue(gNB_RRC_INST *rrc, const gNB_RRC_UE_t *ue) +{ + DevAssert(rrc != NULL); + DevAssert(ue != NULL); + const ue_serving_cell_t *pcell_entry = ue_get_pcell_entry(ue); + if (pcell_entry == NULL) + return NULL; + return get_cell_by_cell_id(&rrc->cells, pcell_entry->nci); +} + diff --git a/openair2/RRC/NR/rrc_cell_management.h b/openair2/RRC/NR/rrc_cell_management.h index 76790fda4f..99071cceb2 100644 --- a/openair2/RRC/NR/rrc_cell_management.h +++ b/openair2/RRC/NR/rrc_cell_management.h @@ -27,6 +27,16 @@ #include #include +/** @brief ServCellIndex value for Primary Cell (TS 38.331) + * @note ServCellIndex is a short identity used to uniquely identify a serving cell + * (PCell, PSCell, or SCell) across cell groups. Value 0 applies for the PCell. */ +#define RRC_PCELL_INDEX 0 + +/** @brief Maximum number of serving cells (TS 38.331) + * @note maxNrofServingCells = 32 (1 PCell + up to 31 SCells) + * @note ServCellIndex range: INTEGER (0..maxNrofServingCells-1) = 0..31 */ +#define RRC_MAX_NUM_SERVING_CELLS 32 + // Generate RB tree functions (prototypes) int rrc_cell_cmp(const struct nr_rrc_cell_container_t *a, const struct nr_rrc_cell_container_t *b); RB_PROTOTYPE(rrc_cell_tree, nr_rrc_cell_container_t, entries, rrc_cell_cmp); @@ -53,4 +63,11 @@ nr_rrc_cell_container_t *rrc_add_cell_to_du(seq_arr_t *cells, nr_rrc_cell_contai // Cell cleanup void rrc_free_cell_container(nr_rrc_cell_container_t *cell); +// UE serving cell functions +ue_serving_cell_t *rrc_add_ue_serving_cell(gNB_RRC_UE_t *ue, const nr_rrc_cell_container_t *cell, uint8_t serving_cell_id); +void rrc_remove_ue_scells_from_du(gNB_RRC_UE_t *ue, sctp_assoc_t assoc_id); +ue_serving_cell_t *rrc_get_ue_serving_cell_by_id(const gNB_RRC_UE_t *ue, uint8_t serving_cell_id); +const ue_serving_cell_t *ue_get_pcell_entry(const gNB_RRC_UE_t *ue); +nr_rrc_cell_container_t *rrc_get_pcell_for_ue(gNB_RRC_INST *rrc, const gNB_RRC_UE_t *ue); + #endif /* RRC_CELL_MANAGEMENT_H_ */ diff --git a/openair2/RRC/NR/rrc_gNB.c b/openair2/RRC/NR/rrc_gNB.c index fd92bca995..6b40d37b36 100644 --- a/openair2/RRC/NR/rrc_gNB.c +++ b/openair2/RRC/NR/rrc_gNB.c @@ -2664,6 +2664,7 @@ static void rrc_delete_ue_data(gNB_RRC_UE_t *UE) UE->redcap_cap = NULL; seq_arr_free(&UE->pduSessions, free_pdusession); seq_arr_free(&UE->drbs, free_drb); + seq_arr_free(&UE->serving_cells, NULL); } void rrc_remove_ue(gNB_RRC_INST *rrc, rrc_gNB_ue_context_t *ue_context_p) diff --git a/openair2/RRC/NR/rrc_gNB_UE_context.c b/openair2/RRC/NR/rrc_gNB_UE_context.c index 01163e319f..6c47a7478e 100644 --- a/openair2/RRC/NR/rrc_gNB_UE_context.c +++ b/openair2/RRC/NR/rrc_gNB_UE_context.c @@ -41,6 +41,7 @@ #include "openair2/F1AP/f1ap_ids.h" #include "tree.h" #include "rrc_gNB_radio_bearers.h" +#include "rrc_cell_management.h" static void rrc_gNB_ue_context_update_time(rrc_gNB_ue_context_t *ctxt) { @@ -224,6 +225,8 @@ rrc_gNB_ue_context_t *rrc_gNB_create_ue_context(sctp_assoc_t assoc_id, seq_arr_init(&ue->pduSessions, sizeof(rrc_pdu_session_param_t)); // Initialise setup DRBs list seq_arr_init(&ue->drbs, sizeof(drb_t)); + // Initialise serving cells list + seq_arr_init(&ue->serving_cells, sizeof(ue_serving_cell_t)); RB_INSERT(rrc_nr_ue_tree_s, &rrc_instance_pP->rrc_ue_head, ue_context_p); LOG_UE_EVENT(ue, From c453c342ed0d2b87b0599fe57998d275ba1582b2 Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Wed, 12 Nov 2025 14:03:41 +0100 Subject: [PATCH 09/17] RRC multi-cell: add integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add integration tests for RRC cell and DU management and UE serving-cell logic so that multi-cell behaviour and regressions can be validated without running the full stack. * test_du_lookup() – DU container lookup by assoc_id: - Multiple DU creation and insertion into RRC DU tree - Lookup of existing DUs by assoc_id - Non-existing and invalid assoc_ids - Cleanup of DU containers and cells * test_cell_lookup() – Cell management and UE association: - Cell lookup by cell_id and PCI (global and per-DU) - DU-specific lookup: rrc_get_cell_for_du(), rrc_get_cell_by_pci_for_du() - Duplicate cell and PCI collision handling (including PCI reuse across DUs and rejection of duplicate PCI within same DU) - Cell–DU association (add/remove cells from DU containers) - UE serving cell: add PCell/SCells, get by ID, get PCell, max serving cells limit, single PCell enforcement - validate_ue_cell_association(), rrc_remove_ue_scells_from_du() - Edge cases and error handling Add openair2/RRC/NR/tests/rrc_cell_management_test.c and extend openair2/RRC/NR/tests/CMakeLists.txt to build and register the test. --- openair2/RRC/NR/tests/CMakeLists.txt | 16 + .../RRC/NR/tests/rrc_cell_management_test.c | 460 ++++++++++++++++++ 2 files changed, 476 insertions(+) create mode 100644 openair2/RRC/NR/tests/rrc_cell_management_test.c diff --git a/openair2/RRC/NR/tests/CMakeLists.txt b/openair2/RRC/NR/tests/CMakeLists.txt index f7a4a95b1b..f68501c99e 100644 --- a/openair2/RRC/NR/tests/CMakeLists.txt +++ b/openair2/RRC/NR/tests/CMakeLists.txt @@ -5,3 +5,19 @@ add_test(NAME rrc_bearers_test COMMAND rrc_bearers_test) target_include_directories(rrc_bearers_test PRIVATE ./common/utils/ds common/utils/LOG) target_link_libraries(rrc_bearers_test PRIVATE ds alg asn1_e1ap asn1_lte_rrc_hdrs asn1_nr_rrc_hdrs LOG T) target_compile_definitions(rrc_bearers_test PRIVATE ENABLE_TESTS) + +add_executable(rrc_cell_management_test rrc_cell_management_test.c) +add_dependencies(tests rrc_cell_management_test) +add_test(NAME rrc_cell_management_test COMMAND rrc_cell_management_test) +target_include_directories(rrc_cell_management_test PRIVATE ./common/utils/ds common/utils/LOG) +target_link_libraries(rrc_cell_management_test PRIVATE + rrc_cell_management + f1ap + ds + alg + LOG + utils + asn1_nr_rrc_hdrs + asn1_lte_rrc_hdrs +) +target_compile_definitions(rrc_cell_management_test PRIVATE ENABLE_TESTS) diff --git a/openair2/RRC/NR/tests/rrc_cell_management_test.c b/openair2/RRC/NR/tests/rrc_cell_management_test.c new file mode 100644 index 0000000000..b3aa5e6881 --- /dev/null +++ b/openair2/RRC/NR/tests/rrc_cell_management_test.c @@ -0,0 +1,460 @@ +/* + * 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 + */ + +#include +#include +#include +#include "common/utils/utils.h" // for calloc_or_fail +#include "common/utils/assertions.h" // for AssertFatal, DevAssert +#include "common/utils/collection/tree.h" // for RB tree macros +#include "openair2/RRC/NR/rrc_cell_management.h" // for cell management functions +#include "openair2/RRC/NR/rrc_gNB_du.h" // for get_du_by_assoc_id +#include "common/utils/LOG/log.h" // for LOG_I, LOG_A, etc. +#include "common/utils/ds/seq_arr.h" // for seq_arr_free +#include "openair2/F1AP/f1ap_ids.h" // for cu_init_f1_ue_data, cu_add_f1_ue_data + +// Test constants +static const sctp_assoc_t ASSOC_ID_DU1 = 8; +static const sctp_assoc_t ASSOC_ID_DU2 = 9; +static const sctp_assoc_t ASSOC_ID_DU3 = 10; +static const uint64_t DU_ID_1 = 1001; +static const uint64_t DU_ID_2 = 1002; +static const uint64_t DU_ID_3 = 1003; +static const uint64_t CELL_ID_1 = 12345678; +static const uint64_t CELL_ID_2 = 87654321; +static const uint64_t CELL_ID_3 = 11111111; +static const uint64_t CELL_ID_4 = 22222222; +static const uint64_t CELL_ID_5 = 33333333; +static const uint64_t CELL_ID_6 = 44444444; +static const uint64_t CELL_ID_7 = 55555555; +static const uint64_t CELL_ID_NONEXISTENT = 99999999; +static const uint16_t PCI_1 = 1; +static const uint16_t PCI_2 = 4; +static const uint16_t PCI_3 = 5; +static const uint16_t PCI_10 = 10; +static const uint16_t PCI_20 = 20; +static const uint16_t PCI_30 = 30; +static const uint16_t PCI_40 = 40; +static const uint16_t PCI_NONEXISTENT = 99; +static const uint8_t SERVING_CELL_ID_SCELL1 = 1; +static const uint8_t SERVING_CELL_ID_SCELL2 = 2; +static const uint32_t UE_ID = 1; + +// Mock functions and variables +configmodule_interface_t *uniqCfg = NULL; +softmodem_params_t *get_softmodem_params(void) +{ + return NULL; +} + +void exit_function(const char *file, const char *function, const int line, const char *s, const int assert) +{ + printf("detected error at %s:%d:%s: %s\n", file, line, function, s); + abort(); +} + +// Helper function to create a test cell container +static nr_rrc_cell_container_t *create_test_cell(uint64_t cell_id, uint16_t pci, sctp_assoc_t assoc_id) +{ + nr_rrc_cell_container_t *cell = calloc_or_fail(1, sizeof(*cell)); + cell->info.cell_id = cell_id; + cell->info.pci = pci; + cell->assoc_id = assoc_id; + cell->info.mode = NR_MODE_TDD; + cell->info.plmn.mcc = 222; + cell->info.plmn.mnc = 1; + cell->info.tac = 1; + return cell; +} + +// Helper function to create a test DU container +static nr_rrc_du_container_t *create_test_du(sctp_assoc_t assoc_id, uint64_t du_id, const char *name) +{ + nr_rrc_du_container_t *du = calloc_or_fail(1, sizeof(*du)); + du->assoc_id = assoc_id; + du->gNB_DU_id = du_id; + du->gNB_DU_name = strdup(name); + AssertFatal(du->gNB_DU_name != NULL, "strdup failed for DU name"); + seq_arr_init(&du->cells, sizeof(nr_rrc_cell_container_t *)); + return du; +} + +// Helper function to add DU to RRC instance +static void add_du_to_rrc(gNB_RRC_INST *rrc, nr_rrc_du_container_t *du) +{ + nr_rrc_du_container_t *collision = rrc_add_du(rrc, du); + AssertFatal(collision == NULL, "rrc_add_du should succeed for new DU (assoc_id %d)", du->assoc_id); +} + +// Helper function to find cell by PCI in global tree (for testing PCI reuse across network) +// Returns first match; PCI is not globally unique +static nr_rrc_cell_container_t *get_cell_by_pci(struct rrc_cell_tree *cells, const uint16_t pci) +{ + nr_rrc_cell_container_t *cell = NULL; + RB_FOREACH (cell, rrc_cell_tree, cells) { + if (cell->info.pci == pci) + return cell; + } + return NULL; +} + +/** @brief Test DU lookup by assoc_id + * @note Tests the functions: + * - get_du_by_assoc_id() */ +static void test_du_lookup(void) +{ + LOG_I(NR_RRC, "Testing DU lookup by assoc_id\n"); + + gNB_RRC_INST rrc = {0}; + RB_INIT(&rrc.dus); + RB_INIT(&rrc.cells); + + /* Sub-test 1: Create multiple DUs and insert into RRC DU tree */ + struct { + nr_rrc_du_container_t *du; + sctp_assoc_t assoc_id; + uint64_t du_id; + } dus[] = { + {create_test_du(ASSOC_ID_DU1, DU_ID_1, "DU-1"), ASSOC_ID_DU1, DU_ID_1}, + {create_test_du(ASSOC_ID_DU2, DU_ID_2, "DU-2"), ASSOC_ID_DU2, DU_ID_2}, + {create_test_du(ASSOC_ID_DU3, DU_ID_3, "DU-3"), ASSOC_ID_DU3, DU_ID_3}, + }; + + for (size_t i = 0; i < sizeof(dus) / sizeof(dus[0]); i++) { + add_du_to_rrc(&rrc, dus[i].du); + } + + /* Sub-test 2: Test lookup existing DUs */ + for (size_t i = 0; i < sizeof(dus) / sizeof(dus[0]); i++) { + nr_rrc_du_container_t *found = get_du_by_assoc_id(&rrc, dus[i].assoc_id); + AssertFatal(found == dus[i].du, "DU with assoc_id %d should be found", dus[i].assoc_id); + AssertFatal(found->gNB_DU_id == dus[i].du_id, "DU ID should match (expected %lu, got %lu)", dus[i].du_id, found->gNB_DU_id); + } + + /* Sub-test 3: Test lookup non-existing assoc_id */ + static const sctp_assoc_t ASSOC_ID_NONEXISTENT = 99; + nr_rrc_du_container_t *found = get_du_by_assoc_id(&rrc, ASSOC_ID_NONEXISTENT); + AssertFatal(found == NULL, "Non-existing assoc_id %d should return NULL", ASSOC_ID_NONEXISTENT); + + /* Sub-test 4: Test invalid assoc_id (0 is considered invalid) */ + static const sctp_assoc_t ASSOC_ID_INVALID = 0; + found = get_du_by_assoc_id(&rrc, ASSOC_ID_INVALID); + AssertFatal(found == NULL, "Invalid assoc_id %d should return NULL", ASSOC_ID_INVALID); + + LOG_A(NR_RRC, "DU lookup test passed\n"); + + /* Sub-test 5: Cleanup with free of DUs and their cell containers */ + for (size_t i = 0; i < sizeof(dus) / sizeof(dus[0]); i++) { + rrc_cleanup_du(&rrc, dus[i].du); + } +} + +/** @brief Test cell lookup, cell-DU association, and UE cell association + * @note Tests the functions: + * - get_cell_by_cell_id() + * - rrc_get_cell_for_du() + * - rrc_get_cell_by_pci_for_du() + * - rrc_add_cell_to_du() + * - rrc_add_ue_serving_cell() + * - rrc_get_ue_serving_cell_by_id() + * - rrc_get_pcell_for_ue() + * - rrc_remove_ue_scells_from_du() + * - rrc_free_cell_container() */ +static void test_cell_lookup(void) +{ + LOG_I(NR_RRC, "Testing cell lookup functions\n"); + + cu_init_f1_ue_data(); + + gNB_RRC_INST rrc = {0}; + RB_INIT(&rrc.dus); + RB_INIT(&rrc.cells); + + nr_rrc_du_container_t *du1 = create_test_du(ASSOC_ID_DU1, DU_ID_1, "DU-1"); + nr_rrc_du_container_t *du2 = create_test_du(ASSOC_ID_DU2, DU_ID_2, "DU-2"); + add_du_to_rrc(&rrc, du1); + add_du_to_rrc(&rrc, du2); + + /* Sub-test 1: Create multiple cells per DU and insert into global cell tree and DU's seq_arr */ + struct { + nr_rrc_cell_container_t *cell; + uint64_t cell_id; + uint16_t pci; + nr_rrc_du_container_t *du; + } cells[] = { + {create_test_cell(CELL_ID_1, PCI_10, ASSOC_ID_DU1), CELL_ID_1, PCI_10, du1}, + {create_test_cell(CELL_ID_2, PCI_20, ASSOC_ID_DU1), CELL_ID_2, PCI_20, du1}, + {create_test_cell(CELL_ID_3, PCI_1, ASSOC_ID_DU1), CELL_ID_3, PCI_1, du1}, + {create_test_cell(CELL_ID_4, PCI_2, ASSOC_ID_DU2), CELL_ID_4, PCI_2, du2}, + {create_test_cell(CELL_ID_5, PCI_3, ASSOC_ID_DU2), CELL_ID_5, PCI_3, du2}, + }; + + // Insert cells: check for duplicate cell_id first, then insert into global tree and DU's seq_arr + for (size_t i = 0; i < sizeofArray(cells); i++) { + nr_rrc_cell_container_t *existing = get_cell_by_cell_id(&rrc.cells, cells[i].cell_id); + AssertFatal(existing == NULL, "Cell ID %lu should not exist before insertion", cells[i].cell_id); + // Insert into global cell tree + nr_rrc_cell_container_t *collision = rrc_add_cell(&rrc, cells[i].cell); + AssertFatal(collision == NULL, "rrc_add_cell should succeed for new cell (ID %lu)", cells[i].cell_id); + // Insert into DU's seq_arr + nr_rrc_cell_container_t *added = rrc_add_cell_to_du(&cells[i].du->cells, cells[i].cell); + AssertFatal(added != NULL, "rrc_add_cell_to_du should succeed for new cell (ID %lu)", cells[i].cell_id); + } + + nr_rrc_cell_container_t *found; + /* Sub-test 2: Test lookup by cell_id */ + found = get_cell_by_cell_id(&rrc.cells, CELL_ID_1); + AssertFatal(found != NULL && found == cells[0].cell, "Cell ID %lu should be found", CELL_ID_1); + found = get_cell_by_cell_id(&rrc.cells, CELL_ID_4); + AssertFatal(found != NULL && found == cells[3].cell, "Cell ID %lu should be found", CELL_ID_4); + + /* Sub-test 3: Test lookup by PCI (returns first match) */ + found = get_cell_by_pci(&rrc.cells, PCI_10); + AssertFatal(found != NULL && found->info.cell_id == CELL_ID_1, + "Cell with PCI %d should be found (expected cell_id %lu)", + PCI_10, + CELL_ID_1); + + /* Sub-test 4: Test lookup non-existing cell */ + found = get_cell_by_cell_id(&rrc.cells, CELL_ID_NONEXISTENT); + AssertFatal(found == NULL, "Non-existing cell_id should return NULL"); + found = get_cell_by_pci(&rrc.cells, PCI_NONEXISTENT); + AssertFatal(found == NULL, "Non-existing PCI should return NULL"); + + /* Sub-test 5: Test rrc_get_cell_for_du() - DU-specific cell lookup */ + found = rrc_get_cell_for_du(&du1->cells, CELL_ID_1); + AssertFatal(found != NULL && found->info.cell_id == CELL_ID_1, "rrc_get_cell_for_du should find cell in DU1"); + found = rrc_get_cell_for_du(&du2->cells, CELL_ID_4); + AssertFatal(found != NULL && found->info.cell_id == CELL_ID_4, "rrc_get_cell_for_du should find cell in DU2"); + // Test non-existing cell in DU + nr_rrc_cell_container_t *not_found = rrc_get_cell_for_du(&du1->cells, CELL_ID_NONEXISTENT); + AssertFatal(not_found == NULL, "rrc_get_cell_for_du should return NULL for non-existing cell"); + // Test cell from different DU + not_found = rrc_get_cell_for_du(&du1->cells, CELL_ID_4); + AssertFatal(not_found == NULL, "rrc_get_cell_for_du should return NULL for cell from different DU"); + + /* Sub-test 6: Test rrc_get_cell_by_pci_for_du() - DU-specific PCI lookup */ + nr_rrc_cell_container_t *cell_in_du = rrc_get_cell_by_pci_for_du(&du1->cells, PCI_10); + AssertFatal(cell_in_du != NULL, "rrc_get_cell_by_pci_for_du should find cell with PCI %d in DU1", PCI_10); + AssertFatal(cell_in_du->info.cell_id == CELL_ID_1, + "Found cell should have correct cell_id (expected %lu, got %lu)", + CELL_ID_1, + cell_in_du->info.cell_id); + cell_in_du = rrc_get_cell_by_pci_for_du(&du2->cells, PCI_2); + AssertFatal(cell_in_du != NULL, "rrc_get_cell_by_pci_for_du should find cell with PCI %d in DU2", PCI_2); + AssertFatal(cell_in_du->info.cell_id == CELL_ID_4, + "Found cell should have correct cell_id (expected %lu, got %lu)", + CELL_ID_4, + cell_in_du->info.cell_id); + // Test non-existing PCI in DU + cell_in_du = rrc_get_cell_by_pci_for_du(&du1->cells, PCI_NONEXISTENT); + AssertFatal(cell_in_du == NULL, "rrc_get_cell_by_pci_for_du should return NULL for non-existing PCI"); + // Test PCI from different DU (should not find it in this DU) + cell_in_du = rrc_get_cell_by_pci_for_du(&du1->cells, PCI_2); + AssertFatal(cell_in_du == NULL, "rrc_get_cell_by_pci_for_du should return NULL for PCI from different DU"); + + /* Sub-test 8: Test PCI reuse across different DUs (allowed globally) */ + // Add a cell to DU2 with the same PCI as a cell in DU1 (PCI reuse is allowed globally) + nr_rrc_cell_container_t *cell_on_du2_same_pci = create_test_cell(CELL_ID_6, PCI_10, ASSOC_ID_DU2); + nr_rrc_cell_container_t *already_in_tree = get_cell_by_cell_id(&rrc.cells, CELL_ID_6); + AssertFatal(already_in_tree == NULL, "Cell ID %lu should not exist before insertion", CELL_ID_6); + // Insert into global cell tree (PCI reuse is allowed) + nr_rrc_cell_container_t *tree_collision = rrc_add_cell(&rrc, cell_on_du2_same_pci); + AssertFatal(tree_collision == NULL, "rrc_add_cell should succeed for new cell (ID %lu)", CELL_ID_6); + // Add to DU2 (should succeed - PCI reuse across DUs is allowed) + nr_rrc_cell_container_t *added_to_du2 = rrc_add_cell_to_du(&du2->cells, cell_on_du2_same_pci); + AssertFatal(added_to_du2 != NULL, "rrc_add_cell_to_du should succeed for cell with PCI reused from different DU"); + // Verify both cells with same PCI exist in global tree (fetch by cell_id) + nr_rrc_cell_container_t *cell1 = get_cell_by_cell_id(&rrc.cells, CELL_ID_1); + nr_rrc_cell_container_t *cell6 = get_cell_by_cell_id(&rrc.cells, CELL_ID_6); + AssertFatal(cell1 != NULL && cell6 != NULL, "Both cells with same PCI should exist in global tree"); + AssertFatal(cell1->info.pci == PCI_10 && cell6->info.pci == PCI_10, "Both cells should have PCI %d", PCI_10); + // get_cell_by_pci returns first match only; verify it returns one of them + nr_rrc_cell_container_t *first_match_global = get_cell_by_pci(&rrc.cells, PCI_10); + AssertFatal(first_match_global != NULL, "get_cell_by_pci should find a cell with PCI %d", PCI_10); + AssertFatal(first_match_global == cell1 || first_match_global == cell6, + "get_cell_by_pci should return one of the two cells with PCI %d", + PCI_10); + // Verify each DU can find its own cell by PCI + nr_rrc_cell_container_t *cell_in_du1 = rrc_get_cell_by_pci_for_du(&du1->cells, PCI_10); + AssertFatal(cell_in_du1 != NULL && cell_in_du1->info.cell_id == CELL_ID_1, "DU1 should find its own cell with PCI %d", PCI_10); + nr_rrc_cell_container_t *cell_in_du2 = rrc_get_cell_by_pci_for_du(&du2->cells, PCI_10); + AssertFatal(cell_in_du2 != NULL && cell_in_du2->info.cell_id == CELL_ID_6, "DU2 should find its own cell with PCI %d", PCI_10); + + /* Sub-test 9: Test PCI reuse within same DU (should be rejected) */ + nr_rrc_cell_container_t *cell_with_duplicate_pci = create_test_cell(CELL_ID_NONEXISTENT, PCI_10, ASSOC_ID_DU1); + // Try to add to DU1 - should fail because PCI_10 already exists in DU1 + nr_rrc_cell_container_t *add_result = rrc_add_cell_to_du(&du1->cells, cell_with_duplicate_pci); + AssertFatal(add_result == NULL, "rrc_add_cell_to_du should reject cell with duplicate PCI within same DU"); + rrc_free_cell_container(cell_with_duplicate_pci); + + /* Sub-test 10: Verify cell counts per DU */ + static const int expected_du1_cells = 3; + static const int expected_du2_cells = 3; // includes cell_on_du2_same_pci + size_t count_du1 = seq_arr_size(&du1->cells); + size_t count_du2 = seq_arr_size(&du2->cells); + AssertFatal(count_du1 == expected_du1_cells, "DU 1 should have %d cells (got %zu)", expected_du1_cells, count_du1); + AssertFatal(count_du2 == expected_du2_cells, "DU 2 should have %d cells (got %zu)", expected_du2_cells, count_du2); + + /* Sub-test 11: Test UE cell association - create test UE context */ + gNB_RRC_UE_t ue = {0}; + ue.rrc_ue_id = UE_ID; + seq_arr_init(&ue.serving_cells, sizeof(ue_serving_cell_t)); + + /* Register F1 UE data for this UE at the CU */ + f1_ue_data_t f1_data = { + .secondary_ue = 0, + .e1_assoc_id = 0, + .du_assoc_id = ASSOC_ID_DU1, + }; + bool f1_added = cu_add_f1_ue_data(UE_ID, &f1_data); + AssertFatal(f1_added, "cu_add_f1_ue_data should succeed for test UE"); + + /* Sub-test 12: Test rrc_add_ue_serving_cell() - Add PCell, ue_get_pcell_entry(), rrc_get_pcell_for_ue() */ + ue_serving_cell_t *added_pcell = rrc_add_ue_serving_cell(&ue, cells[0].cell, RRC_PCELL_INDEX); + AssertFatal(added_pcell != NULL && added_pcell->nci == CELL_ID_1 && added_pcell->serving_cell_id == RRC_PCELL_INDEX, + "rrc_add_ue_serving_cell should succeed"); + const ue_serving_cell_t *pcell_entry = ue_get_pcell_entry(&ue); + AssertFatal(pcell_entry != NULL && pcell_entry->nci == CELL_ID_1 && pcell_entry->serving_cell_id == RRC_PCELL_INDEX, + "ue_get_pcell_entry should return correct PCell entry"); + nr_rrc_cell_container_t *pcell = rrc_get_pcell_for_ue(&rrc, &ue); + AssertFatal(pcell == cells[0].cell, "rrc_get_pcell_for_ue should return same cell as PCell"); + + /* Sub-test 13: Test adding SCells and rrc_get_ue_serving_cell_by_id() */ + ue_serving_cell_t *added_scell = rrc_add_ue_serving_cell(&ue, cells[1].cell, SERVING_CELL_ID_SCELL1); + AssertFatal(added_scell != NULL && added_scell->serving_cell_id == SERVING_CELL_ID_SCELL1, + "rrc_add_ue_serving_cell should succeed for SCell"); + AssertFatal(added_scell->assoc_id == ASSOC_ID_DU1, + "SCell should have correct assoc_id (expected %d, got %d)", + ASSOC_ID_DU1, + added_scell->assoc_id); + // Test rrc_get_ue_serving_cell_by_id() for PCell and SCell + ue_serving_cell_t *found_serving_cell = rrc_get_ue_serving_cell_by_id(&ue, RRC_PCELL_INDEX); + AssertFatal(found_serving_cell != NULL && found_serving_cell == added_pcell, "rrc_get_ue_serving_cell_by_id should find PCell"); + found_serving_cell = rrc_get_ue_serving_cell_by_id(&ue, SERVING_CELL_ID_SCELL1); + AssertFatal(found_serving_cell != NULL && found_serving_cell == added_scell, "rrc_get_ue_serving_cell_by_id should find SCell"); + // Test non-existing serving cell ID + ue_serving_cell_t *not_found_serving = rrc_get_ue_serving_cell_by_id(&ue, SERVING_CELL_ID_SCELL2); + AssertFatal(not_found_serving == NULL, "rrc_get_ue_serving_cell_by_id should return NULL for non-existing serving cell ID"); + static const size_t expected_serving_cells_2 = 2; + AssertFatal(seq_arr_size(&ue.serving_cells) == expected_serving_cells_2, + "UE should have %zu serving cells (got %zu)", + expected_serving_cells_2, + seq_arr_size(&ue.serving_cells)); + + // Add second SCell + nr_rrc_cell_container_t *scell2 = create_test_cell(CELL_ID_7, PCI_30, ASSOC_ID_DU1); + nr_rrc_cell_container_t *already_present = get_cell_by_cell_id(&rrc.cells, CELL_ID_7); + AssertFatal(already_present == NULL, "Cell ID %lu should not exist before insertion", CELL_ID_7); + // Insert into global cell tree + nr_rrc_cell_container_t *collision = rrc_add_cell(&rrc, scell2); + AssertFatal(collision == NULL, "rrc_add_cell should succeed for new cell (ID %lu)", CELL_ID_7); + // Insert into DU's seq_arr + nr_rrc_cell_container_t *added_scell2_du = rrc_add_cell_to_du(&du1->cells, scell2); + AssertFatal(added_scell2_du != NULL, "rrc_add_cell_to_du should succeed for new cell (ID %lu)", CELL_ID_7); + ue_serving_cell_t *added_scell2 = rrc_add_ue_serving_cell(&ue, scell2, SERVING_CELL_ID_SCELL2); + AssertFatal(added_scell2 != NULL, "rrc_add_ue_serving_cell should add new SCell"); + AssertFatal(added_scell2->serving_cell_id == SERVING_CELL_ID_SCELL2, "New SCell should have correct serving_cell_id (expected %d, got %d)", SERVING_CELL_ID_SCELL2, added_scell2->serving_cell_id); + AssertFatal(added_scell2->assoc_id == ASSOC_ID_DU1, + "SCell should have correct assoc_id (expected %d, got %d)", + ASSOC_ID_DU1, + added_scell2->assoc_id); + + /* Sub-test 14: Test multiple PCell enforcement - adding another PCell should fail if one already exists */ + nr_rrc_cell_container_t *test_cell = create_test_cell(CELL_ID_NONEXISTENT, PCI_NONEXISTENT, ASSOC_ID_DU1); + AssertFatal(rrc_add_ue_serving_cell(&ue, test_cell, RRC_PCELL_INDEX) == NULL, + "Adding another PCell when one already exists should fail"); + AssertFatal(rrc_get_pcell_for_ue(&rrc, &ue) == cells[0].cell, + "PCell should still be the original one when adding duplicate PCell fails"); + rrc_free_cell_container(test_cell); + + /* Sub-test 15: Test rrc_add_ue_serving_cell() with existing PCell (same nci, should return existing) */ + ue_serving_cell_t *existing_pcell = rrc_add_ue_serving_cell(&ue, cells[0].cell, RRC_PCELL_INDEX); + AssertFatal(existing_pcell != NULL && existing_pcell->nci == CELL_ID_1, + "rrc_add_ue_serving_cell should succeed for existing PCell with same nci"); + AssertFatal(existing_pcell == added_pcell, "Should return existing PCell entry, not create new one"); + + /* Sub-test 16: Test maximum serving cells limit */ + static const uint64_t test_cell_id_base = 10000000; + static const uint8_t starting_serving_cell_id = 3; + nr_rrc_cell_container_t *test_cells[RRC_MAX_NUM_SERVING_CELLS] = {0}; + size_t test_cell_count = 0; + for (uint8_t i = starting_serving_cell_id; i < RRC_MAX_NUM_SERVING_CELLS; i++) { + uint64_t test_cell_id = test_cell_id_base + i; + uint16_t test_pci = PCI_40 + i; + test_cells[test_cell_count] = create_test_cell(test_cell_id, test_pci, ASSOC_ID_DU1); + // Insert into global cell tree + nr_rrc_cell_container_t *existing = rrc_add_cell(&rrc, test_cells[test_cell_count]); + if (existing != NULL) { + LOG_E(NR_RRC, "Cell %lu already exists in tree, skipping insertion\n", test_cell_id); + rrc_free_cell_container(test_cells[test_cell_count]); + test_cells[test_cell_count] = NULL; + break; + } + // Insert into DU's seq_arr + rrc_add_cell_to_du(&du1->cells, test_cells[test_cell_count]); + if (rrc_add_ue_serving_cell(&ue, test_cells[test_cell_count], i) == NULL) + break; + test_cell_count++; + } + size_t final_count = seq_arr_size(&ue.serving_cells); + AssertFatal(final_count <= RRC_MAX_NUM_SERVING_CELLS, + "Should not exceed max serving cells (got %zu, max %d)", + final_count, + RRC_MAX_NUM_SERVING_CELLS); + + /* Sub-test 17: Test rrc_remove_ue_scells_from_du() */ + size_t before_remove = seq_arr_size(&ue.serving_cells); + rrc_remove_ue_scells_from_du(&ue, ASSOC_ID_DU1); + AssertFatal(seq_arr_size(&ue.serving_cells) < before_remove, "Should remove some serving cells"); + AssertFatal(ue_get_pcell_entry(&ue) == NULL, "PCell should be removed when it has matching assoc_id"); + + /* Sub-test 18: Test rrc_get_pcell_for_ue() edge case (UE with no serving cells) */ + gNB_RRC_UE_t ue_empty = {0}; + seq_arr_init(&ue_empty.serving_cells, sizeof(ue_serving_cell_t)); + AssertFatal(rrc_get_pcell_for_ue(&rrc, &ue_empty) == NULL, + "rrc_get_pcell_for_ue should return NULL for UE with no serving cells"); + seq_arr_free(&ue_empty.serving_cells, NULL); + + LOG_A(NR_RRC, "Cell lookup test passed\n"); + + /* Sub-test 19: Cleanup */ + seq_arr_free(&ue.serving_cells, NULL); + rrc_cleanup_du(&rrc, du1); + rrc_cleanup_du(&rrc, du2); +} + +int main() +{ + // Initialize logging system + logInit(); + set_log(NR_RRC, OAILOG_INFO); + + LOG_I(NR_RRC, "Starting RRC Cell Management Tests\n"); + + // DU lookup tests + test_du_lookup(); + // Cell lookup, cell-DU association, and UE cell association tests + test_cell_lookup(); + + LOG_A(NR_RRC, "All RRC Cell Management tests passed successfully!\n"); + return 0; +} From 95d35b9b7d5fae76e2e10cce95ba1e8254ce3608 Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Tue, 6 Jan 2026 16:45:52 +0100 Subject: [PATCH 10/17] RRC multi-cell: integrate cell containers into F1 setup flow Refactor F1 setup processing to create and manage cell containers during DU initialization, establishing the foundation for multi-cell architecture where cells are managed independently from DUs. Why: Cell information is currently tied to F1AP structures (f1ap_served_cell_info_t), which complicates multi-cell support. Introducing cell containers at F1 setup decouples RRC cell management from F1AP and allows cells to be managed independently while keeping existing code paths working. Key changes: 1. Cell container creation (cp_f1_served_cell_info_to_cell): New helper copying F1AP served cell info into nr_rrc_cell_container_t (cell_id, pci, plmn, tac, TDD/FDD frequency info). Extracts and stores MTC in the cell. 2. Function signature updates: get_cell_neighbour_list() and label_intra_frequency_neighbours() now take nr_rrc_cell_container_t* and use cell->info; improved logging (cell_id, PCI, SSB ARFCN). 3. valid_du_in_neighbour_configs: Removed ssb_arfcn parameter; MTC is extracted and freed inside the function. 4. F1 Setup (rrc_gNB_process_f1_setup_req): Initialize DU cell tree (RB_INIT(&du->cells)). Populate DU-only fields (gNB_DU_id, gNB_DU_name, rrc_ver). Flow: - create cell with cp_f1_served_cell_info_to_cell - add to global tree via rrc_add_cell() - add DU via rrc_add_du() - attach cell to DU with rrc_add_cell_to_du(). Store MIB/SIB1/MTC in cell. Use cell container for neighbour labeling and SIB encoding. 5. Duplicate cell detection: Use rrc_add_cell() for global cell tree insert; collision return indicates duplicate cell_id. Clearer error messages for Cell ID vs PCI conflicts. Use F1AP_CauseRadioNetwork_cell_not_available for cell conflicts. 6. DU configuration update: Use get_cell_by_cell_id() to find cell, check assoc_id, pass cell container to label_intra_frequency_neighbours(). 7. DU disconnection: Use rrc_cleanup_du() to remove cells from global tree and free resources; logging uses DU fields (gNB_DU_id, gNB_DU_name). 8. Global cell tree: Initialize rrc->cells in openair_rrc_gNB_configuration(). Backward compatibility: setup_req still built via cp_f1ap_setup_request(); du->mib, du->sib1, du->mtc remain set (shared with cell container). --- openair2/RRC/NR/rrc_gNB.c | 1 + openair2/RRC/NR/rrc_gNB_du.c | 161 +++++++++++++++++++++++------------ 2 files changed, 107 insertions(+), 55 deletions(-) diff --git a/openair2/RRC/NR/rrc_gNB.c b/openair2/RRC/NR/rrc_gNB.c index 6b40d37b36..0dab76eae2 100644 --- a/openair2/RRC/NR/rrc_gNB.c +++ b/openair2/RRC/NR/rrc_gNB.c @@ -370,6 +370,7 @@ void openair_rrc_gNB_configuration(gNB_RRC_INST *rrc, gNB_RrcConfigurationReq *c RB_INIT(&rrc->rrc_ue_head); RB_INIT(&rrc->cuups); RB_INIT(&rrc->dus); + RB_INIT(&rrc->cells); rrc->configuration = *configuration; } diff --git a/openair2/RRC/NR/rrc_gNB_du.c b/openair2/RRC/NR/rrc_gNB_du.c index 83e158546d..79ebdd89d9 100644 --- a/openair2/RRC/NR/rrc_gNB_du.c +++ b/openair2/RRC/NR/rrc_gNB_du.c @@ -161,13 +161,13 @@ static int nr_cell_id_match(const void *key, const void *element) return 1; } -static neighbour_cell_configuration_t *get_cell_neighbour_list(const gNB_RRC_INST *rrc, const f1ap_served_cell_info_t *cell_info) +static neighbour_cell_configuration_t *get_cell_neighbour_list(const gNB_RRC_INST *rrc, const nr_rrc_cell_container_t *cell) { void *base = seq_arr_front(rrc->neighbour_cell_configuration); size_t nmemb = seq_arr_size(rrc->neighbour_cell_configuration); size_t size = sizeof(neighbour_cell_configuration_t); - void *it = bsearch((void *)&cell_info->nr_cellid, base, nmemb, size, nr_cell_id_match); + void *it = bsearch((void *)&cell->info.cell_id, base, nmemb, size, nr_cell_id_match); return (neighbour_cell_configuration_t *)it; } @@ -201,28 +201,39 @@ static void is_intra_frequency_neighbour(void *ssb_arfcn, void *neighbour_cell) /** * @brief Labels neighbour cells if they are intra frequency to prepare meas config only for intra frequency ho * @param[in] rrc Pointer to RRC instance - * @param[in] cell_info Pointer to cell information + * @param[in] du Pointer to DU container + * @param[in] cell Pointer to cell container */ static void label_intra_frequency_neighbours(gNB_RRC_INST *rrc, const nr_rrc_du_container_t *du, - const f1ap_served_cell_info_t *cell_info) + const nr_rrc_cell_container_t *cell) { if (!rrc->neighbour_cell_configuration) return; - neighbour_cell_configuration_t *neighbour_cell_config = get_cell_neighbour_list(rrc, cell_info); + neighbour_cell_configuration_t *neighbour_cell_config = get_cell_neighbour_list(rrc, cell); if (!neighbour_cell_config) return; - LOG_D(NR_RRC, "HO LOG: Cell: %lu has neighbour cell configuration!\n", cell_info->nr_cellid); uint32_t ssb_arfcn = get_ssb_arfcn(du); + LOG_D(NR_RRC, + "Cell %lu (PCI %d, SSB ARFCN %u) has neighbour cell configuration, labeling intra-frequency neighbours\n", + cell->info.cell_id, + cell->info.pci, + ssb_arfcn); seq_arr_t *cell_neighbour_list = neighbour_cell_config->neighbour_cells; for_each(cell_neighbour_list, (void *)&ssb_arfcn, is_intra_frequency_neighbour); } -static bool valid_du_in_neighbour_configs(const seq_arr_t *neighbour_cell_configuration, const f1ap_served_cell_info_t *cell, int ssb_arfcn) +static bool valid_du_in_neighbour_configs(const seq_arr_t *neighbour_cell_configuration, const f1ap_served_cell_info_t *cell) { + // MTC is mandatory, but some DUs don't send it in the F1 Setup Request, so + // "tolerate" this behavior, despite it being mandatory + NR_MeasurementTimingConfiguration_t *mtc = extract_mtc(cell->measurement_timing_config, cell->measurement_timing_config_len); + int ssb_arfcn = ssb_arfcn_mtc(mtc); + ASN_STRUCT_FREE(asn_DEF_NR_MeasurementTimingConfiguration, mtc); + for (int c = 0; c < neighbour_cell_configuration->size; c++) { const neighbour_cell_configuration_t *neighbour_config = seq_arr_at(neighbour_cell_configuration, c); for (int ni = 0; ni < neighbour_config->neighbour_cells->size; ni++) { @@ -278,6 +289,44 @@ static bool valid_du_in_neighbour_configs(const seq_arr_t *neighbour_cell_config return true; } +static void cp_f1_served_cell_info_to_cell(nr_rrc_cell_container_t *dst, const f1ap_served_cell_info_t *src) +{ + DevAssert(dst != NULL); + DevAssert(src != NULL); + + // Copy basic cell information + dst->info.cell_id = src->nr_cellid; + dst->info.pci = src->nr_pci; + dst->info.plmn = src->plmn; + dst->info.tac = src->tac ? *src->tac : 0; + + // Copy frequency information based on mode + if (src->mode == F1AP_MODE_TDD) { + dst->info.mode = NR_MODE_TDD; + dst->info.tdd.dlul.band = src->tdd.freqinfo.band; + dst->info.tdd.dlul.arfcn = src->tdd.freqinfo.arfcn; + dst->info.tdd.dlul.scs = src->tdd.tbw.scs; + dst->info.tdd.dlul.nrb = src->tdd.tbw.nrb; + } else { + dst->info.mode = NR_MODE_FDD; + /* DL */ + dst->info.fdd.dl.band = src->fdd.dl_freqinfo.band; + dst->info.fdd.dl.arfcn = src->fdd.dl_freqinfo.arfcn; + dst->info.fdd.dl.scs = src->fdd.dl_tbw.scs; + dst->info.fdd.dl.nrb = src->fdd.dl_tbw.nrb; + /* UL */ + dst->info.fdd.ul.band = src->fdd.ul_freqinfo.band; + dst->info.fdd.ul.arfcn = src->fdd.ul_freqinfo.arfcn; + dst->info.fdd.ul.scs = src->fdd.ul_tbw.scs; + dst->info.fdd.ul.nrb = src->fdd.ul_tbw.nrb; + } + + // Extract and store MTC + if (src->measurement_timing_config_len > 0) { + dst->mtc = extract_mtc(src->measurement_timing_config, src->measurement_timing_config_len); + } +} + void rrc_gNB_process_f1_setup_req(f1ap_setup_req_t *req, sctp_assoc_t assoc_id) { AssertFatal(assoc_id != 0, "illegal assoc_id == 0: should be -1 (monolithic) or >0 (split)\n"); @@ -315,46 +364,22 @@ void rrc_gNB_process_f1_setup_req(f1ap_setup_req_t *req, sctp_assoc_t assoc_id) } nr_rrc_du_container_t *it = NULL; RB_FOREACH(it, rrc_du_tree, &rrc->dus) { - if (it->setup_req->gNB_DU_id == req->gNB_DU_id) { + if (it->gNB_DU_id == req->gNB_DU_id) { LOG_E(NR_RRC, "gNB-DU ID: existing DU %s on assoc_id %d already has ID %ld, rejecting requesting gNB-DU\n", - it->setup_req->gNB_DU_name, + it->gNB_DU_name, it->assoc_id, - it->setup_req->gNB_DU_id); - fail.cause = F1AP_CauseMisc_unspecified; - rrc->mac_rrc.f1_setup_failure(assoc_id, &fail); - return; - } - // note: we assume that each DU contains only one cell; otherwise, we would - // need to check every cell in the requesting DU to any existing cell. - const f1ap_served_cell_info_t *exist_info = &it->setup_req->cell[0].info; - const f1ap_served_cell_info_t *new_info = &req->cell[0].info; - if (exist_info->nr_cellid == new_info->nr_cellid || exist_info->nr_pci == new_info->nr_pci) { - LOG_E(NR_RRC, - "existing DU %s on assoc_id %d already has cellID %ld/physCellId %d, rejecting requesting gNB-DU with cellID %ld/physCellId %d\n", - it->setup_req->gNB_DU_name, - it->assoc_id, - exist_info->nr_cellid, - exist_info->nr_pci, - new_info->nr_cellid, - new_info->nr_pci); + it->gNB_DU_id); fail.cause = F1AP_CauseMisc_unspecified; rrc->mac_rrc.f1_setup_failure(assoc_id, &fail); return; } } - // MTC is mandatory, but some DUs don't send it in the F1 Setup Request, so - // "tolerate" this behavior, despite it being mandatory - NR_MeasurementTimingConfiguration_t *mtc = - extract_mtc(cell_info->measurement_timing_config, cell_info->measurement_timing_config_len); - - if (rrc->neighbour_cell_configuration - && !valid_du_in_neighbour_configs(rrc->neighbour_cell_configuration, cell_info, ssb_arfcn_mtc(mtc))) { + if (rrc->neighbour_cell_configuration && !valid_du_in_neighbour_configs(rrc->neighbour_cell_configuration, cell_info)) { LOG_E(NR_RRC, "problem with DU %ld in neighbor configuration, rejecting DU\n", req->gNB_DU_id); f1ap_setup_failure_t fail = {.cause = F1AP_CauseMisc_unspecified}; rrc->mac_rrc.f1_setup_failure(assoc_id, &fail); - ASN_STRUCT_FREE(asn_DEF_NR_MeasurementTimingConfiguration, mtc); return; } @@ -367,21 +392,41 @@ void rrc_gNB_process_f1_setup_req(f1ap_setup_req_t *req, sctp_assoc_t assoc_id) LOG_W(NR_RRC, "rejecting DU ID %ld\n", req->gNB_DU_id); fail.cause = F1AP_CauseProtocol_semantic_error; rrc->mac_rrc.f1_setup_failure(assoc_id, &fail); - ASN_STRUCT_FREE(asn_DEF_NR_MeasurementTimingConfiguration, mtc); return; } } LOG_I(NR_RRC, "Accepting DU %ld (%s), sending F1 Setup Response\n", req->gNB_DU_id, req->gNB_DU_name); LOG_I(NR_RRC, "DU uses RRC version %u.%u.%u\n", req->rrc_ver[0], req->rrc_ver[1], req->rrc_ver[2]); - // we accept the DU - nr_rrc_du_container_t *du = calloc(1, sizeof(*du)); - AssertFatal(du, "out of memory\n"); - du->assoc_id = assoc_id; + /* Create cell and add to global tree */ + nr_rrc_cell_container_t *new = calloc_or_fail(1, sizeof(*new)); + new->assoc_id = assoc_id; + cp_f1_served_cell_info_to_cell(new, cell_info); + new->mib = mib; + new->sib1 = sib1; + // Add cell to DU's cell array + nr_rrc_cell_container_t *collision = rrc_add_cell(rrc, new); + if (collision != NULL) { + nr_rrc_du_container_t *existing_du = get_du_by_assoc_id(rrc, collision->assoc_id); + const char *du_name = existing_du ? existing_du->gNB_DU_name : "unknown"; + LOG_E(NR_RRC, + "Cell ID %lu already exists in DU %s (assoc_id %d), rejecting requesting gNB-DU\n", + new->info.cell_id, + du_name, + collision->assoc_id); + rrc_free_cell_container(new); + fail.cause = F1AP_CauseRadioNetwork_cell_not_available; + rrc->mac_rrc.f1_setup_failure(assoc_id, &fail); + return; + } - /* ITTI will free the setup request message via free(). So the memory - * "inside" of the message will remain, but the "outside" container no, so - * allocate memory and copy it in */ + // DU is accepted, add it to tree and add cell to DU's cell array + nr_rrc_du_container_t *du = calloc_or_fail(1, sizeof(*du)); + du->mtc = new->mtc; // TODO: remove in upcoming commit (kept for backward compatibility) + du->assoc_id = assoc_id; + du->gNB_DU_id = req->gNB_DU_id; + du->gNB_DU_name = strdup(req->gNB_DU_name ? req->gNB_DU_name : "N/A"); + memcpy(du->rrc_ver, req->rrc_ver, sizeof(du->rrc_ver)); du->setup_req = calloc(1,sizeof(*du->setup_req)); AssertFatal(du->setup_req, "out of memory\n"); // Copy F1AP message @@ -389,9 +434,14 @@ void rrc_gNB_process_f1_setup_req(f1ap_setup_req_t *req, sctp_assoc_t assoc_id) // MIB can be null and configured later via DU Configuration Update du->mib = mib; du->sib1 = sib1; - du->mtc = mtc; - RB_INSERT(rrc_du_tree, &rrc->dus, du); - rrc->num_dus++; + // Initialize cell array for this DU + seq_arr_init(&du->cells, sizeof(nr_rrc_cell_container_t *)); + nr_rrc_du_container_t *du_collision = rrc_add_du(rrc, du); + AssertFatal(du_collision == NULL, "rrc_add_du should succeed for new DU (assoc_id %d)", assoc_id); + + nr_rrc_cell_container_t *added = rrc_add_cell_to_du(&du->cells, new); + AssertFatal(added != NULL, "Failed to add cell %ld to DU %ld\n", new->info.cell_id, du->gNB_DU_id); + LOG_I(NR_RRC, "DU %ld: Added cell %ld\n", du->gNB_DU_id, new->info.cell_id); served_cells_to_activate_t cell = { .plmn = cell_info->plmn, @@ -407,7 +457,7 @@ void rrc_gNB_process_f1_setup_req(f1ap_setup_req_t *req, sctp_assoc_t assoc_id) nr_SIBs_t *sib = (nr_SIBs_t *)seq_arr_at(sibs, i); switch (sib->SIB_type) { case 2: { - NR_SSB_MTC_t *ssbmtc = get_ssb_mtc(mtc); + NR_SSB_MTC_t *ssbmtc = get_ssb_mtc(new->mtc); sib->SIB_size = do_SIB2_NR(&sib->SIB_buffer, ssbmtc); cell.SI_msg[cell.num_SI].SI_container = sib->SIB_buffer; cell.SI_msg[cell.num_SI].SI_container_length = sib->SIB_size; @@ -420,8 +470,8 @@ void rrc_gNB_process_f1_setup_req(f1ap_setup_req_t *req, sctp_assoc_t assoc_id) } } - if (du->mib != NULL && du->sib1 != NULL) - label_intra_frequency_neighbours(rrc, du, cell_info); + if (new->mib != NULL &&new->sib1 != NULL) + label_intra_frequency_neighbours(rrc, du, new); f1ap_setup_resp_t resp = {.transaction_id = req->transaction_id, .num_cells_to_activate = 1, @@ -557,8 +607,10 @@ void rrc_gNB_process_f1_du_configuration_update(f1ap_gnb_du_configuration_update } } if (du->mib != NULL && du->sib1 != NULL) { - const f1ap_served_cell_info_t *cell_info = &du->setup_req->cell[0].info; - label_intra_frequency_neighbours(rrc, du, cell_info); + nr_rrc_cell_container_t *cell = get_cell_by_cell_id(&rrc->cells, conf_up->cell_to_modify[0].old_nr_cellid); + if (cell != NULL && cell->assoc_id == du->assoc_id) { + label_intra_frequency_neighbours(rrc, du, cell); + } } } @@ -599,10 +651,9 @@ void rrc_CU_process_f1_lost_connection(gNB_RRC_INST *rrc, f1ap_lost_connection_t if (du->setup_req) free_f1ap_setup_request(du->setup_req); free(du->setup_req); - nr_rrc_du_container_t *removed = RB_REMOVE(rrc_du_tree, &rrc->dus, du); - DevAssert(removed != NULL); - free(du); - rrc->num_dus--; + + // Clean up DU and associated cell resources + rrc_cleanup_du(rrc, du); int num = invalidate_du_connections(rrc, assoc_id); if (num > 0) { From 15c79b0288696f08b110dcd4314422572fdbde1f Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Tue, 6 Jan 2026 12:15:54 +0100 Subject: [PATCH 11/17] RRC multi-cell: adopt cell management in F1 DU configuration update procedures Migrate F1 DU configuration update procedures in openair2/RRC/NR/rrc_gNB_du.c to use the cell management API instead of accessing cell data through setup_req, completing the transition to the new multi-cell architecture. 1. update_cell_info: - Refactored to use cell management API: takes RRC instance, old cell_id, and new cell info; finds cell via get_cell_by_cell_id() internally. - Returns cell container pointer (or NULL on failure) instead of void. - When cell_id changes: removes cell from RB tree (old key), updates via cp_f1_served_cell_info_to_cell(), re-inserts (new key); validates no duplicate cell_id/PCI before allowing change. - Frees old MTC when new measurement timing config is present. - Encapsulates cell lookup and update logic. 2. rrc_gNB_process_f1_du_configuration_update: - Uses update_cell_info() for cell modify; validates cell belongs to DU (assoc_id) after update. - Duplicate checking via get_cell_by_cell_id() and rrc_get_cell_by_pci_for_du() - For cells to add: validates duplicate cell_id globally and duplicate PCI within DU; rejects with logging. - MIB/SIB1 updated in cell container (cell->mib, cell->sib1); uses du->gNB_DU_id; passes cell to label_intra_frequency_neighbours(); removes direct setup_req->cell[0] access. Backward compatibility: setup_req remains until migration is completed; cell-specific data is managed in the cell container and accessed via the cell management API. Improves separation of concerns and enables proper multi-cell support. --- openair2/RRC/NR/rrc_gNB_du.c | 160 ++++++++++++++++++++++++++--------- 1 file changed, 120 insertions(+), 40 deletions(-) diff --git a/openair2/RRC/NR/rrc_gNB_du.c b/openair2/RRC/NR/rrc_gNB_du.c index 79ebdd89d9..6024079936 100644 --- a/openair2/RRC/NR/rrc_gNB_du.c +++ b/openair2/RRC/NR/rrc_gNB_du.c @@ -500,6 +500,7 @@ static int invalidate_du_connections(gNB_RRC_INST *rrc, sctp_assoc_t assoc_id) uint32_t ue_id = UE->rrc_ue_id; if (UE->ho_context != NULL) LOG_W(NR_RRC, "DU disconnected while handover for UE %d active\n", ue_id); + f1_ue_data_t ue_data = cu_get_f1_ue_data(ue_id); if (ue_data.du_assoc_id != assoc_id) continue; /* this UE is on another DU */ @@ -526,26 +527,77 @@ static int invalidate_du_connections(gNB_RRC_INST *rrc, sctp_assoc_t assoc_id) return count; } -static void update_cell_info(nr_rrc_du_container_t *du, const f1ap_served_cell_info_t *new_ci) +/** @brief Update cell information from F1AP DU configuration update + * @param[in] rrc RRC instance (needed to get DU and check PCI duplicates) + * @param[in] old_nci The cell_id used to find the cell in the tree + * @param[in] new_ci Pointer to the new cell information from F1AP message + * @return Pointer to the updated cell container, or NULL on failure + * @note RB tree manipulation: The cell tree is keyed by cell_id. If the cell_id changes + * during update, the node must be removed from the tree before updating (using the + * old cell_id) and re-inserted after updating (using the new cell_id). This is + * necessary because RB trees maintain ordering based on the key value. If cell_id + * doesn't change, no tree manipulation is needed. */ +static nr_rrc_cell_container_t *update_cell_info(gNB_RRC_INST *rrc, const uint64_t old_nci, const f1ap_served_cell_info_t *new_ci) { - DevAssert(du != NULL); + DevAssert(rrc != NULL); DevAssert(new_ci != NULL); - AssertFatal(du->setup_req->num_cells_available == 1, "expected 1 cell for DU, but has %d\n", du->setup_req->num_cells_available); - f1ap_served_cell_info_t *ci = &du->setup_req->cell[0].info; - // make sure no memory is allocated - free_f1ap_cell(ci, NULL); - *ci = copy_f1ap_served_cell_info(new_ci); - - NR_MeasurementTimingConfiguration_t *new_mtc = - extract_mtc(new_ci->measurement_timing_config, new_ci->measurement_timing_config_len); - if (new_mtc != NULL) { - ASN_STRUCT_FREE(asn_DEF_NR_MeasurementTimingConfiguration, du->mtc); - du->mtc = new_mtc; - } else { - LOG_E(NR_RRC, "error decoding MeasurementTimingConfiguration during cell update, ignoring new config\n"); - ASN_STRUCT_FREE(asn_DEF_NR_MeasurementTimingConfiguration, new_mtc); + struct rrc_cell_tree *cells = &rrc->cells; + // Find the cell by old cell_id from the update message + nr_rrc_cell_container_t *cell = get_cell_by_cell_id(cells, old_nci); + if (cell == NULL) { + LOG_W(NR_RRC, "no cell with ID %ld found, ignoring gNB-DU configuration update\n", old_nci); + return NULL; } + + // Check if PCI will change - if so, check for duplicate PCI within the same DU + bool pci_changed = (cell->info.pci != new_ci->nr_pci); + if (pci_changed) { + nr_rrc_du_container_t *du = get_du_by_assoc_id(rrc, cell->assoc_id); + if (du != NULL) { + // Check if new PCI would conflict with another cell in this DU (excluding current cell) + nr_rrc_cell_container_t *existing_pci = rrc_get_cell_by_pci_for_du(&du->cells, new_ci->nr_pci); + if (existing_pci != NULL && existing_pci != cell) { + LOG_E(NR_RRC, + "Cannot change PCI from %d to %d: PCI %d already exists in this DU (cell ID %lu)\n", + cell->info.pci, + new_ci->nr_pci, + new_ci->nr_pci, + existing_pci->info.cell_id); + return NULL; + } + } + } + + // Check if cell_id will change - if so, need to update tree structure + bool cell_id_changed = (old_nci != new_ci->nr_cellid); + if (cell_id_changed) { + // Check if new cell_id would create a duplicate (excluding current cell) + nr_rrc_cell_container_t *existing = get_cell_by_cell_id(cells, new_ci->nr_cellid); + if (existing != NULL && existing != cell) { + LOG_E(NR_RRC, "Cannot change cell_id from %ld to %ld: new cell_id already exists\n", old_nci, new_ci->nr_cellid); + return NULL; + } + // Remove from tree before updating cell_id + nr_rrc_cell_container_t *removed = RB_REMOVE(rrc_cell_tree, cells, cell); + AssertFatal(removed == cell, "Failed to remove cell %ld from tree\n", old_nci); + } + + // Free old MTC before updating + if (new_ci->measurement_timing_config_len > 0) + ASN_STRUCT_FREE(asn_DEF_NR_MeasurementTimingConfiguration, cell->mtc); + + // Update cell information from the new F1AP served cell info + cp_f1_served_cell_info_to_cell(cell, new_ci); + + // If cell_id changed, re-insert into tree with new cell_id + if (cell_id_changed) { + nr_rrc_cell_container_t *existing = RB_INSERT(rrc_cell_tree, cells, cell); + AssertFatal(existing == NULL, "Failed to insert cell %ld into tree\n", cell->info.cell_id); + LOG_I(NR_RRC, "Updated cell_id from %ld to %ld\n", old_nci, cell->info.cell_id); + } + + return cell; } void rrc_gNB_process_f1_du_configuration_update(f1ap_gnb_du_configuration_update_t *conf_up, sctp_assoc_t assoc_id) @@ -562,55 +614,83 @@ void rrc_gNB_process_f1_du_configuration_update(f1ap_gnb_du_configuration_update nr_rrc_du_container_t *du = get_du_by_assoc_id(rrc, assoc_id); AssertError(du != NULL, return, "no DU found for assoc_id %d\n", assoc_id); - - const f1ap_served_cell_info_t *info = &du->setup_req->cell[0].info; - if (conf_up->num_cells_to_add > 0) { - // Here we check if the number of cell limit is respectet, otherwise send failure - LOG_W(NR_RRC, "du_configuration_update->cells_to_add_list is not supported yet"); + // Validate each cell to add for uniqueness (cell_id globally; PCI within this DU) + for (uint16_t i = 0; i < conf_up->num_cells_to_add; i++) { + f1ap_served_cell_info_t *cell_info = &conf_up->cell_to_add[i].info; + // Check for duplicate cell_id globally + nr_rrc_cell_container_t *existing = get_cell_by_cell_id(&rrc->cells, cell_info->nr_cellid); + if (existing != NULL) { + nr_rrc_du_container_t *existing_du = get_du_by_assoc_id(rrc, existing->assoc_id); + const char *du_name = existing_du ? existing_du->gNB_DU_name : "unknown"; + LOG_E(NR_RRC, + "Cell ID %lu already exists in DU %s (assoc_id %d), rejecting cell addition\n", + cell_info->nr_cellid, + du_name, + existing->assoc_id); + /** @todo send failure message */ + return; + } + // Check for duplicate PCI within this DU + nr_rrc_cell_container_t *existing_pci = rrc_get_cell_by_pci_for_du(&du->cells, cell_info->nr_pci); + if (existing_pci != NULL) { + LOG_E(NR_RRC, + "PCI %d already exists in this DU (cell ID %lu), rejecting cell addition (cell ID %lu)\n", + cell_info->nr_pci, + existing_pci->info.cell_id, + cell_info->nr_cellid); + /** @todo send failure message */ + return; + } } if (conf_up->num_cells_to_modify > 0) { // here the old nrcgi is used to find the cell information, if it exist then we modify consequently otherwise we fail AssertFatal(conf_up->num_cells_to_modify == 1, "cannot handle more than one cell!\n"); - - if (info->nr_cellid != conf_up->cell_to_modify[0].old_nr_cellid) { - LOG_W(NR_RRC, "no cell with ID %ld found, ignoring gNB-DU configuration update\n", conf_up->cell_to_modify[0].old_nr_cellid); - return; - } + const uint64_t old_nci = conf_up->cell_to_modify[0].old_nr_cellid; + const f1ap_served_cell_info_t *new_ci = &conf_up->cell_to_modify[0].info; // verify the new plmn of the cell - if (!rrc_gNB_plmn_matches(rrc, &conf_up->cell_to_modify[0].info)) { + if (!rrc_gNB_plmn_matches(rrc, new_ci)) { LOG_W(NR_RRC, "PLMN does not match, ignoring gNB-DU configuration update\n"); return; } - update_cell_info(du, &conf_up->cell_to_modify[0].info); + nr_rrc_cell_container_t *cell = update_cell_info(rrc, old_nci, new_ci); + if (cell == NULL) { + LOG_W(NR_RRC, "Failed to update cell %ld, ignoring gNB-DU configuration update\n", old_nci); + return; + } + if (cell->assoc_id != du->assoc_id) { + LOG_W(NR_RRC, + "cell %ld belongs to different DU (assoc_id %d vs %d), ignoring gNB-DU configuration update\n", + old_nci, + cell->assoc_id, + du->assoc_id); + return; + } const f1ap_gnb_du_system_info_t *sys_info = conf_up->cell_to_modify[0].sys_info; if (sys_info != NULL && sys_info->mib != NULL && !(sys_info->sib1 == NULL && IS_SA_MODE(get_softmodem_params()))) { // MIB is mandatory, so will be overwritten. SIB1 is optional, so will // only be overwritten if present in sys_info - ASN_STRUCT_FREE(asn_DEF_NR_MIB, du->mib); + ASN_STRUCT_FREE(asn_DEF_NR_MIB, cell->mib); if (sys_info->sib1 != NULL) { - ASN_STRUCT_FREE(asn_DEF_NR_SIB1, du->sib1); - du->sib1 = NULL; + ASN_STRUCT_FREE(asn_DEF_NR_SIB1, cell->sib1); + cell->sib1 = NULL; } NR_MIB_t *mib = NULL; - if (!extract_sys_info(sys_info, &mib, &du->sib1)) { - LOG_W(NR_RRC, "cannot update sys_info for DU %ld\n", du->setup_req->gNB_DU_id); + if (!extract_sys_info(sys_info, &mib, &cell->sib1)) { + LOG_W(NR_RRC, "cannot update sys_info for DU %ld\n", du->gNB_DU_id); } else { DevAssert(mib != NULL); - du->mib = mib; - LOG_I(NR_RRC, "update system information of DU %ld\n", du->setup_req->gNB_DU_id); + cell->mib = mib; + LOG_I(NR_RRC, "update system information of DU %ld\n", du->gNB_DU_id); } } - if (du->mib != NULL && du->sib1 != NULL) { - nr_rrc_cell_container_t *cell = get_cell_by_cell_id(&rrc->cells, conf_up->cell_to_modify[0].old_nr_cellid); - if (cell != NULL && cell->assoc_id == du->assoc_id) { - label_intra_frequency_neighbours(rrc, du, cell); - } + if (du->mib != NULL && du->sib1 != NULL && cell != NULL) { + label_intra_frequency_neighbours(rrc, du, cell); } } From 5980dbcb408ca1c8a9bbcb0c0420be5d984c7500 Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Tue, 6 Jan 2026 21:39:25 +0100 Subject: [PATCH 12/17] RRC multi-cell: use cell lookups in NGAP Handover Request handling In rrc_gNB_NGAP.c, rrc_gNB_process_Handover_Request() is updated to resolve the target by cell instead of by DU, then resolve the DU from the cell's assoc_id. This aligns Handover Request with the new cell-centric model and cell management APIs. - Resolve target with get_cell_by_cell_id(&rrc->cells, msg->nr_cell_id) instead of get_du_by_cell_id(); on failure, log "no cell found" and send NGAP handover failure. - Resolve DU with get_du_by_assoc_id(rrc, cell->assoc_id); add a failure path (log, NGAP handover failure, return -1) when no DU exists for the cell's assoc_id. - Use cell->info (nr_rrc_cell_info_t) for logging and for key derivation (nr_derive_key_ng_ran_star); use du->assoc_id for UE context creation instead of du->setup_req->cell[0].info. get_ssb_arfcn(du) is unchanged and still takes the DU; other DU-based lookups are kept where still required. --- openair2/RRC/NR/rrc_gNB_NGAP.c | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/openair2/RRC/NR/rrc_gNB_NGAP.c b/openair2/RRC/NR/rrc_gNB_NGAP.c index bf8a797a49..acb556d8f5 100644 --- a/openair2/RRC/NR/rrc_gNB_NGAP.c +++ b/openair2/RRC/NR/rrc_gNB_NGAP.c @@ -90,6 +90,7 @@ #include "uper_encoder.h" #include "rrc_gNB_mobility.h" #include "rrc_gNB_du.h" +#include "rrc_cell_management.h" #include "common/utils/alg/find.h" #ifdef E2_AGENT @@ -1102,10 +1103,11 @@ int rrc_gNB_process_Handover_Request(gNB_RRC_INST *rrc, instance_t instance, nga return -1; } - struct nr_rrc_du_container_t *du = get_du_by_cell_id(rrc, msg->nr_cell_id); - if (du == NULL) { + // Get cell by cell_id + nr_rrc_cell_container_t *cell = get_cell_by_cell_id(&rrc->cells, msg->nr_cell_id); + if (cell == NULL) { /* Cell Not Found! Return HO Request Failure*/ - LOG_E(RRC, "Failed to process Handover Request: no DU found with NR Cell ID=%lu \n", msg->nr_cell_id); + LOG_E(RRC, "Failed to process Handover Request: no cell found with NR Cell ID=%lu \n", msg->nr_cell_id); ngap_handover_failure_t fail = { .amf_ue_ngap_id = msg->amf_ue_ngap_id, .cause.type = NGAP_CAUSE_RADIO_NETWORK, @@ -1115,6 +1117,18 @@ int rrc_gNB_process_Handover_Request(gNB_RRC_INST *rrc, instance_t instance, nga return -1; } + struct nr_rrc_du_container_t *du = get_du_by_assoc_id(rrc, cell->assoc_id); + if(du == NULL) { + LOG_E(NR_RRC, "Failed to process Handover Request: no DU found with assoc_id=%d\n", cell->assoc_id); + ngap_handover_failure_t fail = { + .amf_ue_ngap_id = msg->amf_ue_ngap_id, + .cause.type = NGAP_CAUSE_RADIO_NETWORK, + .cause.value = NGAP_CAUSE_RADIO_NETWORK_HO_FAILURE_IN_TARGET_5GC_NGRAN_NODE_OR_TARGET_SYSTEM, + }; + rrc_gNB_send_NGAP_HANDOVER_FAILURE(rrc, &fail); + return -1; + } + // Validate PLMN from GUAMI against allowed PLMN list const plmn_id_t *serving_plmn = get_serving_plmn(rrc, &msg->guami.plmn); if (!serving_plmn) { @@ -1128,12 +1142,11 @@ int rrc_gNB_process_Handover_Request(gNB_RRC_INST *rrc, instance_t instance, nga return -1; } - uint16_t pci = du->setup_req->cell[0].info.nr_pci; + uint16_t pci = cell->info.pci; LOG_I(NR_RRC, "Received Handover Request (on NR Cell ID=%lu, PCI=%u) \n", msg->nr_cell_id, pci); // Create UE context - sctp_assoc_t curr_assoc_id = du->assoc_id; - rrc_gNB_ue_context_t *ue_context_p = rrc_gNB_create_ue_context(curr_assoc_id, UINT16_MAX, rrc, UINT64_MAX, UINT32_MAX); + rrc_gNB_ue_context_t *ue_context_p = rrc_gNB_create_ue_context(du->assoc_id, UINT16_MAX, rrc, UINT64_MAX, UINT32_MAX); gNB_RRC_UE_t *UE = &ue_context_p->ue_context; // allocate context for target @@ -1159,9 +1172,9 @@ int rrc_gNB_process_Handover_Request(gNB_RRC_INST *rrc, instance_t instance, nga // Reset KgNB memset(UE->kgnb, 0, SECURITY_KEY_LENGTH); // Derive KgNB* - const f1ap_served_cell_info_t *cell_info = &du->setup_req->cell[0].info; + const nr_rrc_cell_info_t *cell_info = &cell->info; uint32_t ssb_arfcn = get_ssb_arfcn(du); - nr_derive_key_ng_ran_star(cell_info->nr_pci, ssb_arfcn, UE->nh, UE->kgnb); + nr_derive_key_ng_ran_star(cell_info->pci, ssb_arfcn, UE->nh, UE->kgnb); UE->as_security_active = true; // Activate SRBs activate_srb(UE, SRB1); From d4710466230af3876c69298160478d9400afcd45 Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Tue, 6 Jan 2026 12:30:07 +0100 Subject: [PATCH 13/17] RRC multi-cell: complete migration to cell container architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finalize the migration from single-cell-per-DU to the multi-cell architecture: remove legacy DU/cell fields and refactor RRC procedures to use cell containers and UE serving cells instead of DU-based lookups. Data structure changes: - Remove nr_cellid from gNB_RRC_UE_t; cell identity comes from the UE's serving_cells (PCell entry via ue_get_pcell_entry(ue)). UE_LOG_ARGS and LOG_UE_* macros take a PCell entry pointer for null-safe cell_id in logs; all call sites pass ue_get_pcell_entry(ue). - Remove setup_req, mib, sib1, mtc from nr_rrc_du_container_t (held in cell containers); remove nr_cellid from gNB_RRC_INST and its assignment in gnb_config.c. Function and signature updates: - get_ssb_arfcn(): take cell container instead of DU. - label_intra_frequency_neighbours(): drop du parameter, use cell only. - Handover: Handover context (source and target) now stores cell pointers (source->cell, target->cell) instead of DU pointers. When the context is created (N2: on Handover Request; F1: in nr_rrc_trigger_f1_ho), the target cell is set in ho_context->target->cell and, for F1, the source cell in ho_context->source->cell. nr_initiate_handover() takes only source_cell and reads the target from ho_ctx->target->cell, so it no longer takes a target parameter. The target is known at handover creation time, while the UE’s serving_cells are only updated after F1 UE Context Setup Response. - nr_rrc_trigger_f1_ho() and the N2 handover path pass cell pointers and set ho_context->target->cell (and for F1, source->cell) when the handover is triggered. Procedure refactoring: - Lookups: replace get_du_by_cell_id() with get_cell_by_cell_id()/ get_cell_by_pci(); remove get_du_by_cell_id() and get_cell_information_by_phycellId(). Rationale: cell-specific data (MIB, SIB1, mtc, info) now lives in cell containers, and a DU can host multiple cells. Callers need the cell (to use cell->info, cell->mtc, etc.), not the DU. Looking up by cell_id therefore returns the cell container (get_cell_by_cell_id()); when the DU is needed, use cell->assoc_id and get_du_by_assoc_id(). Looking up by PCI is scoped (e.g. get_cell_by_pci_for_du() for a given DU, or via the UE’s serving cells) because PCI is not globally unique. - Setup/Reestablishment: resolve cell, register as PCell via rrc_add_ue_serving_cell(); reestablishment resolves cell by PCI (rrc_get_cell_by_pci_for_du), then rrc_remove_ue_scells_from_du + rrc_add_ue_serving_cell; validate_ue_cell_association() after update. - Measurement config: use cell->info (SCS/band) and cell->mtc. - Handover: use cell containers end-to-end; after UE context modification response call nr_rrc_update_cell_assoc_after_ho(). - NGAP: cell_id and TAC from UE PCell via rrc_get_pcell_for_ue(); Handover Notify: get cell for message content, use get_du_for_ue() for DU existence check; paging resolves UE by 5G-S-TMSI-Part1 and uses that UE's PCell (rrc_get_pcell_for_ue) for F1AP_PAGING_IND nr_cellid. Performance and robustness: - Use rrc_get_pcell_for_ue() when cell data is needed; use get_du_for_ue() when only the DU is needed (F1/N2 HO trigger, Handover Notify DU check) for efficiency. - Reestablishment: remove old PCell before adding new; error handling for PCell add and validation. Code cleanup: - F1 setup: stop copying/storing setup request in DU container; F1 disconnect only frees cell containers via rrc_free_cell_container(). - dump_du_info(): iterate du->cells and show all cells per DU. - telnetsrv_ci.c: use du->gNB_DU_id directly and fix format (%ld). - UeContextSetupRequest servCellIndex set to RRC_PCELL_INDEX; remove servCellIndex from F1AP UE context modification request. --- common/utils/telnetsrv/telnetsrv_ci.c | 2 +- openair2/GNB_APP/gnb_config.c | 2 - openair2/RRC/NR/nr_rrc_defs.h | 14 +- openair2/RRC/NR/nr_rrc_proto.h | 4 +- openair2/RRC/NR/rrc_gNB.c | 233 +++++++++++++++++--------- openair2/RRC/NR/rrc_gNB_NGAP.c | 49 ++++-- openair2/RRC/NR/rrc_gNB_cuup.c | 1 + openair2/RRC/NR/rrc_gNB_du.c | 120 ++++++------- openair2/RRC/NR/rrc_gNB_du.h | 5 +- openair2/RRC/NR/rrc_gNB_mobility.c | 201 +++++++++++++++++----- openair2/RRC/NR/rrc_gNB_mobility.h | 17 +- openair2/RRC/NR/rrc_gNB_nsa.c | 26 ++- 12 files changed, 444 insertions(+), 230 deletions(-) diff --git a/common/utils/telnetsrv/telnetsrv_ci.c b/common/utils/telnetsrv/telnetsrv_ci.c index a2bcccd150..084e3b93f3 100644 --- a/common/utils/telnetsrv/telnetsrv_ci.c +++ b/common/utils/telnetsrv/telnetsrv_ci.c @@ -168,7 +168,7 @@ int fetch_du_by_ue_id(char *buf, int debug, telnet_printfunc_t prnt) nr_rrc_du_container_t *du = get_du_for_ue(RC.nrrrc[0], ue_id); if (du) { - prnt("gNB_DU_id %d is connected to ue_id %ld\n", du->setup_req->gNB_DU_id, ue_id); + prnt("gNB_DU_id %ld is connected to ue_id %ld\n", du->gNB_DU_id, ue_id); return 0; } else { ERROR_MSG_RET("No DU connected\n"); diff --git a/openair2/GNB_APP/gnb_config.c b/openair2/GNB_APP/gnb_config.c index 8edec8e84f..338f9ad8a5 100644 --- a/openair2/GNB_APP/gnb_config.c +++ b/openair2/GNB_APP/gnb_config.c @@ -2077,8 +2077,6 @@ gNB_RRC_INST *RCconfig_NRRRC() rrc->eth_params_s.transp_preference = ETH_UDP_MODE; } - rrc->nr_cellid = (uint64_t)*(GNBParamList.paramarray[i][GNB_NRCELLID_IDX].u64ptr); - if (strcmp(*(GNBParamList.paramarray[i][GNB_TRANSPORT_S_PREFERENCE_IDX].strptr), "local_mac") == 0) { } else if (strcmp(*(GNBParamList.paramarray[i][GNB_TRANSPORT_S_PREFERENCE_IDX].strptr), "cudu") == 0) { diff --git a/openair2/RRC/NR/nr_rrc_defs.h b/openair2/RRC/NR/nr_rrc_defs.h index 58f9ffd1fb..cf834d2c67 100644 --- a/openair2/RRC/NR/nr_rrc_defs.h +++ b/openair2/RRC/NR/nr_rrc_defs.h @@ -222,7 +222,6 @@ typedef struct gNB_RRC_UE_s { uint64_t ng_5G_S_TMSI_Part1; NR_EstablishmentCause_t establishment_cause; - uint64_t nr_cellid; /* Dynamic array of UE serving cells */ seq_arr_t serving_cells; /* ue_serving_cell_t */ @@ -409,11 +408,6 @@ typedef struct nr_rrc_du_container_t { char *gNB_DU_name; /* RRC version */ uint8_t rrc_ver[3]; - // Cell-specific information (F1AP Setup Request, MIB, SIB1, MeasurementTimingConfiguration) - f1ap_setup_req_t *setup_req; - NR_MIB_t *mib; - NR_SIB1_t *sib1; - NR_MeasurementTimingConfiguration_t *mtc; /* Cells, indexed by cell_id */ seq_arr_t cells; /* nr_rrc_cell_container_t* */ } nr_rrc_du_container_t; @@ -436,8 +430,6 @@ typedef struct gNB_RRC_INST_s { eth_params_t eth_params_s; uid_allocator_t uid_allocator; RB_HEAD(rrc_nr_ue_tree_s, rrc_gNB_ue_context_s) rrc_ue_head; // ue_context tree key search by rnti - /// NR cell id - uint64_t nr_cellid; // RRC configuration gNB_RrcConfigurationReq configuration; @@ -471,8 +463,12 @@ typedef struct gNB_RRC_INST_s { nr_rlc_configuration_t rlc_config; } gNB_RRC_INST; +/** Forward declaration for UE log macros */ +const ue_serving_cell_t *ue_get_pcell_entry(const gNB_RRC_UE_t *ue); + #define UE_LOG_FMT "(cellID %lx, UE ID %d RNTI %04x)" -#define UE_LOG_ARGS(ue_context) (ue_context)->nr_cellid, (ue_context)->rrc_ue_id, (ue_context)->rnti +#define UE_LOG_ARGS(ue_context) \ + (ue_get_pcell_entry(ue_context) ? ue_get_pcell_entry(ue_context)->nci : 0), (ue_context)->rrc_ue_id, (ue_context)->rnti #define LOG_UE_DL_EVENT(ue_context, fmt, ...) LOG_A(NR_RRC, "[DL] " UE_LOG_FMT " " fmt, UE_LOG_ARGS(ue_context) __VA_OPT__(,) __VA_ARGS__) #define LOG_UE_EVENT(ue_context, fmt, ...) LOG_A(NR_RRC, "[--] " UE_LOG_FMT " " fmt, UE_LOG_ARGS(ue_context) __VA_OPT__(,) __VA_ARGS__) diff --git a/openair2/RRC/NR/nr_rrc_proto.h b/openair2/RRC/NR/nr_rrc_proto.h index 1db6a4d236..5838e01b47 100644 --- a/openair2/RRC/NR/nr_rrc_proto.h +++ b/openair2/RRC/NR/nr_rrc_proto.h @@ -127,11 +127,11 @@ void init_delayed_action(delayed_action_state_t *delayed_action); /** @brief Prepare and send F1AP UE Context Setup Request for a target DU * @param rrc RRC instance * @param ue UE context - * @param du Target DU container containing cell and setup information + * @param cell Target cell container containing cell information * @param ho_prep_info Optional handover preparation information (NULL if not provided) */ void rrc_f1_ue_context_setup_for_target_du(const gNB_RRC_INST *rrc, gNB_RRC_UE_t *ue, - const nr_rrc_du_container_t *du, + const nr_rrc_cell_container_t *cell, const byte_array_t *ho_prep_info); #endif diff --git a/openair2/RRC/NR/rrc_gNB.c b/openair2/RRC/NR/rrc_gNB.c index 0dab76eae2..7a85c11c3c 100644 --- a/openair2/RRC/NR/rrc_gNB.c +++ b/openair2/RRC/NR/rrc_gNB.c @@ -82,6 +82,7 @@ #include "openair2/F1AP/lib/f1ap_ue_context.h" #include "rrc_gNB_NGAP.h" #include "rrc_gNB_du.h" +#include "rrc_cell_management.h" #include "rrc_gNB_mobility.h" #include "rrc_gNB_radio_bearers.h" #include "rrc_cell_management.h" @@ -533,16 +534,18 @@ void activate_srb(gNB_RRC_UE_t *UE, int srb_id) * This helper function initializes common fields that are shared between * different UE context setup scenarios (serving DU, target DU). * @param ue UE context - * @param du DU container for measurement timing configuration + * @param cell (primary serving) cell container * @param gNB_DU_ue_id Optional gNB-DU UE ID to include in the request (NULL if not provided) * @return F1AP UE Context Setup Request */ static f1ap_ue_context_setup_req_t rrc_fill_f1_ue_context_setup(gNB_RRC_UE_t *ue, - const nr_rrc_du_container_t *du, + const nr_rrc_cell_container_t *cell, const uint32_t *gNB_DU_ue_id) { f1ap_ue_context_setup_req_t req = { .gNB_CU_ue_id = ue->rrc_ue_id, - .servCellIndex = 0, + .servCellIndex = RRC_PCELL_INDEX, + .plmn = cell->info.plmn, + .nr_cellid = cell->info.cell_id, }; /* UE Aggregate Maximum Bit Rate Uplink is C-ifDRBSetup: 1 Gbps */ @@ -550,7 +553,7 @@ static f1ap_ue_context_setup_req_t rrc_fill_f1_ue_context_setup(gNB_RRC_UE_t *ue *req.gnb_du_ue_agg_mbr_ul = 1000000000; /* bps */ /* Measurement timing configuration */ - req.cu_to_du_rrc_info.meas_timing_config = get_meas_timing_config(du->mtc, ue->measConfig); + req.cu_to_du_rrc_info.meas_timing_config = get_meas_timing_config(cell->mtc, ue->measConfig); /* UE capabilities */ if (ue->ue_cap_buffer.len > 0) { @@ -564,13 +567,6 @@ static f1ap_ue_context_setup_req_t rrc_fill_f1_ue_context_setup(gNB_RRC_UE_t *ue *req.gNB_DU_ue_id = *gNB_DU_ue_id; } - /* PLMN and cell ID from DU setup request */ - const f1ap_served_cell_info_t *cell_info = &du->setup_req->cell[0].info; - req.plmn.mcc = cell_info->plmn.mcc; - req.plmn.mnc = cell_info->plmn.mnc; - req.plmn.mnc_digit_length = cell_info->plmn.mnc_digit_length; - req.nr_cellid = cell_info->nr_cellid; - return req; } @@ -772,22 +768,24 @@ void free_RRCReconfiguration_params(nr_rrc_reconfig_param_t params) NR_MeasConfig_t *nr_rrc_get_measconfig(const gNB_RRC_INST *rrc, uint64_t nr_cellid) { - nr_rrc_du_container_t *du = get_du_by_cell_id((gNB_RRC_INST *)rrc, nr_cellid); - DevAssert(du != NULL); - f1ap_served_cell_info_t *cell_info = &du->setup_req->cell[0].info; + nr_rrc_cell_container_t *cell = get_cell_by_cell_id(&((gNB_RRC_INST *)rrc)->cells, nr_cellid); + DevAssert(cell != NULL); - if (du->mtc != NULL) { + if (cell->mtc != NULL) { NR_ReportConfigToAddMod_t *rc_PER = NULL; NR_ReportConfigToAddMod_t *rc_A2 = NULL; seq_arr_t rc_A3_seq = {0}; seq_arr_t neigh_seq = {0}; seq_arr_init(&rc_A3_seq, sizeof(NR_ReportConfigToAddMod_t)); seq_arr_init(&neigh_seq, sizeof(nr_neighbour_cell_t)); - int scs = get_ssb_scs(cell_info); - int band = get_dl_band(cell_info); - const NR_MeasTimingList_t *mtlist = du->mtc->criticalExtensions.choice.c1->choice.measTimingConf->measTiming; + + // Get cell info from the cell container + int scs = cell->info.mode == NR_MODE_TDD ? cell->info.tdd.dlul.scs : cell->info.fdd.dl.scs; + int band = cell->info.mode == NR_MODE_TDD ? cell->info.tdd.dlul.band : cell->info.fdd.dl.band; + + const NR_MeasTimingList_t *mtlist = cell->mtc->criticalExtensions.choice.c1->choice.measTimingConf->measTiming; const NR_MeasTiming_t *mt = mtlist->list.array[0]; - const neighbour_cell_configuration_t *neighbour_config = get_neighbour_cell_config(rrc, cell_info->nr_cellid); + const neighbour_cell_configuration_t *neighbour_config = get_neighbour_cell_config(rrc, cell->info.cell_id); seq_arr_t *neighbour_cells = NULL; if (neighbour_config) neighbour_cells = neighbour_config->neighbour_cells; @@ -837,7 +835,7 @@ NR_MeasConfig_t *nr_rrc_get_measconfig(const gNB_RRC_INST *rrc, uint64_t nr_cell if (rrc->measurementConfiguration.a2_event) rc_A2 = prepare_a2_event_report(rrc->measurementConfiguration.a2_event); - NR_MeasConfig_t *result = get_MeasConfig(mt, band, scs, cell_info->nr_pci, rc_PER, rc_A2, &rc_A3_seq, &neigh_seq, neigh_a3_id); + NR_MeasConfig_t *result = get_MeasConfig(mt, band, scs, cell->info.pci, rc_PER, rc_A2, &rc_A3_seq, &neigh_seq, neigh_a3_id); // Clean up sequence arrays seq_arr_free(&rc_A3_seq, NULL); @@ -1060,12 +1058,14 @@ static void rrc_gNB_send_f1_drb_release_request(gNB_RRC_INST *rrc, gNB_RRC_UE_t f1ap_ue_context_mod_req_t req = { .gNB_CU_ue_id = ue_p->rrc_ue_id, .gNB_DU_ue_id = ue_data.secondary_ue, - .servCellIndex = 0, + .servCellIndex = RRC_PCELL_INDEX, }; req.plmn = malloc_or_fail(sizeof(*req.plmn)); *req.plmn = rrc->configuration.plmn[0]; + nr_rrc_cell_container_t *cell = rrc_get_pcell_for_ue(rrc, ue_p); + DevAssert(cell != NULL); req.nr_cellid = malloc_or_fail(sizeof(*req.nr_cellid)); - *req.nr_cellid = rrc->nr_cellid; + *req.nr_cellid = cell->info.cell_id; req.drbs_rel = malloc_or_fail(sizeof(*req.drbs_rel)); req.drbs_rel_len = n_drb_to_release; memcpy(req.drbs_rel, drb_to_release, n_drb_to_release * sizeof(int)); @@ -1192,7 +1192,7 @@ static void cuup_notify_reestablishment(gNB_RRC_INST *rrc, gNB_RRC_UE_t *ue_p) * @param du DU container containing cell information for key derivation */ static void rrc_gNB_generate_RRCReestablishment(rrc_gNB_ue_context_t *ue_context_pP, const rnti_t old_rnti, - const nr_rrc_du_container_t *du) + const nr_rrc_cell_container_t *cell) { module_id_t module_id = 0; gNB_RRC_INST *rrc = RC.nrrrc[module_id]; @@ -1200,16 +1200,19 @@ static void rrc_gNB_generate_RRCReestablishment(rrc_gNB_ue_context_t *ue_context uint8_t buffer[NR_RRC_BUF_SIZE] = {0}; uint8_t xid = rrc_gNB_get_next_transaction_identifier(module_id); ue_p->xids[xid] = RRC_REESTABLISH; - const f1ap_served_cell_info_t *cell_info = &du->setup_req->cell[0].info; - uint32_t ssb_arfcn = get_ssb_arfcn(du); - LOG_I(NR_RRC, "Reestablishment update key pci=%d, earfcn_dl=%u\n", cell_info->nr_pci, ssb_arfcn); + + nr_rrc_cell_container_t *pcell = rrc_get_pcell_for_ue(rrc, ue_p); + DevAssert(pcell != NULL); + DevAssert(pcell->info.cell_id == cell->info.cell_id); + uint32_t ssb_arfcn = get_ssb_arfcn(pcell); + LOG_I(NR_RRC, "Reestablishment update key pci=%d, earfcn_dl=%u\n", pcell->info.pci, ssb_arfcn); /* Per TS 33.501 6.9.2.3.4: When sending RRCReestablishment with nextHopChainingCount, * we're staying at the same NCC level (not advancing), so use horizontal derivation * from the currently active KgNB. Vertical derivation (from NH) is only used when * advancing to a new NCC level, which happens during handover or masterKeyUpdate. */ const uint8_t *base_key = ue_p->kgnb; - nr_derive_key_ng_ran_star(cell_info->nr_pci, ssb_arfcn, base_key, ue_p->kgnb); + nr_derive_key_ng_ran_star(pcell->info.pci, ssb_arfcn, base_key, ue_p->kgnb); int size = do_RRCReestablishment(ue_context_pP->ue_context.nh_ncc, buffer, NR_RRC_BUF_SIZE, xid); LOG_A(NR_RRC, "Send RRCReestablishment [%d bytes] to RNTI %04x\n", size, ue_p->rnti); @@ -1284,13 +1287,13 @@ static void rrc_gNB_process_RRCReestablishmentComplete(gNB_RRC_INST *rrc, /** Handle UE access on a different DU than the original one * Per 38.401 8.7: "If the UE accessed from a gNB-DU other than the original * one, the gNB-CU should trigger the UE Context Setup procedure". */ - /* UE is accessing a different DU - trigger UE Context Setup per TS 38.401 §8.7 */ - nr_rrc_du_container_t *du = get_du_by_assoc_id(rrc, assoc_id); - if (du == NULL) { - LOG_E(NR_RRC, "UE %d: cannot find DU for assoc_id %d\n", ue_p->rrc_ue_id, assoc_id); + nr_rrc_cell_container_t *cell = rrc_get_pcell_for_ue(rrc, ue_p); + // UE should have a PCell set at this point (set when RRCReestablishmentRequest was handled) + if (cell == NULL) { + LOG_E(NR_RRC, "UE %d: no PCell for reestablishment on different DU\n", ue_p->rrc_ue_id); return; } - rrc_f1_ue_context_setup_for_target_du(rrc, ue_p, du, NULL); + rrc_f1_ue_context_setup_for_target_du(rrc, ue_p, cell, NULL); } else { // Request DU to provide CellGroupConfig with reestablishRLC flags for re-establishment // According to TS 38.473 transparency requirements, CU should not construct or re-encode CellGroupConfig @@ -1406,9 +1409,17 @@ static void rrc_handle_RRCSetupRequest(gNB_RRC_INST *rrc, byte_array_t cgc = {.buf = msg->du2cu_rrc_container, .len = msg->du2cu_rrc_container_length}; store_cgc(UE, &cgc); UE->establishment_cause = rrcSetupRequest->establishmentCause; - UE->nr_cellid = msg->nr_cellid; + // Add PCell to serving_cells array + nr_rrc_cell_container_t *cell = get_cell_by_cell_id(&rrc->cells, msg->nr_cellid); + if (!cell) { + LOG_E(NR_RRC, "Cell %ld not found, rejecting UE\n", msg->nr_cellid); + rrc_gNB_generate_RRCReject(rrc, ue_context_p); + return; + } + /* Add PCell to serving_cells array */ + rrc_add_ue_serving_cell(UE, cell, RRC_PCELL_INDEX); UE->ongoing_reconfiguration = false; - UE->measConfig = nr_rrc_get_measconfig(rrc, UE->nr_cellid); + UE->measConfig = nr_rrc_get_measconfig(rrc, msg->nr_cellid); activate_srb(UE, 1); rrc_gNB_generate_RRCSetup(0, msg->crnti, ue_context_p, msg->du2cu_rrc_container, msg->du2cu_rrc_container_length); } @@ -1436,7 +1447,8 @@ static rrc_gNB_ue_context_t *rrc_gNB_get_ue_context_source_cell(gNB_RRC_INST *rr if (!ue->ho_context || !ue->ho_context->source) continue; nr_ho_source_cu_t *source_ctx = ue->ho_context->source; - if (source_ctx->old_rnti == rntiP && source_ctx->du->setup_req->cell[0].info.nr_pci == pci) + const nr_rrc_cell_container_t *source_cell = source_ctx->cell; + if (source_cell && source_ctx->old_rnti == rntiP && source_cell->info.pci == pci) return ue_context_p; } return NULL; @@ -1456,6 +1468,28 @@ static void f1u_dl_gtp_rollback(gNB_RRC_UE_t *UE) } } +/** @brief Resolve previous/old cell (UE identity physCellId): same cell, else neighbor + * of current cell, else any cell with that PCI in this DU. */ +static const nr_rrc_cell_container_t *get_previous_cell_by_pci_in_du(gNB_RRC_INST *rrc, + const nr_rrc_cell_container_t *current_cell, + const nr_rrc_du_container_t *du, + uint16_t pci) +{ + if (current_cell->info.pci == pci) + return current_cell; + + const neighbour_cell_configuration_t *neigh_config = get_neighbour_cell_config(rrc, current_cell->info.cell_id); + if (neigh_config != NULL) { + const nr_neighbour_cell_t *neighbour = get_neighbour_cell_by_pci(neigh_config, pci); + if (neighbour != NULL) { + nr_rrc_cell_container_t *c = get_cell_by_cell_id(&rrc->cells, neighbour->nrcell_id); + if (c != NULL && c->assoc_id == du->assoc_id) + return c; + } + } + return rrc_get_cell_by_pci_for_du(&du->cells, pci); +} + static void rrc_handle_RRCReestablishmentRequest(gNB_RRC_INST *rrc, sctp_assoc_t assoc_id, const NR_RRCReestablishmentRequest_IEs_t *req, @@ -1486,15 +1520,17 @@ static void rrc_handle_RRCReestablishmentRequest(gNB_RRC_INST *rrc, goto fallback_rrc_setup; } - if (du->mib == NULL || du->sib1 == NULL) { - /* we don't have MIB/SIB1 of the DU, and therefore cannot generate the - * Reestablishment (as we would need the SSB's ARFCN, which we cannot - * compute). So generate RRC Setup instead */ - LOG_E(NR_RRC, "Reestablishment request: no MIB/SIB1 of DU present, cannot do reestablishment, force setup request\n"); - goto fallback_rrc_setup; + // Fetch current cell: where the reestablishment request was received + nr_rrc_cell_container_t *current_cell = get_cell_by_cell_id(&rrc->cells, msg->nr_cellid); + if (current_cell == NULL || current_cell->assoc_id != du->assoc_id) { + LOG_E(NR_RRC, + "received CCCH message, but no corresponding cell found for nr_cellid %ld in DU (assoc_id %d)\n", + msg->nr_cellid, + assoc_id); + return; } - if (du->mtc == NULL) { + if (current_cell->mtc == NULL) { // some UEs don't send MeasurementTimingConfiguration, so we don't know the // SSB ARFCN and can't do reestablishment. handle it gracefully by doing // RRC setup procedure instead @@ -1503,7 +1539,12 @@ static void rrc_handle_RRCReestablishmentRequest(gNB_RRC_INST *rrc, goto fallback_rrc_setup; } - // Try to find UE context by RNTI and assoc_id (normal case: same DU) + const nr_rrc_cell_container_t *cell = get_previous_cell_by_pci_in_du(rrc, current_cell, du, (uint16_t)physCellId); + if (cell == NULL) { + LOG_E(NR_RRC, "received CCCH message, but no corresponding cell found for PCI %ld in DU (assoc_id %d)\n", physCellId, assoc_id); + return; + } + ue_context_p = rrc_gNB_get_ue_context_by_rnti(rrc, assoc_id, old_rnti); if (ue_context_p == NULL) { // Fallback 1: Try to find UE by RNTI only (re-establishment on different DU scenario) @@ -1526,14 +1567,12 @@ static void rrc_handle_RRCReestablishmentRequest(gNB_RRC_INST *rrc, goto fallback_rrc_setup; } - /* should check phys cell ID to identify the correct cell */ - const f1ap_served_cell_info_t *cell_info = &du->setup_req->cell[0].info; - const f1ap_served_cell_info_t *previous_cell_info = get_cell_information_by_phycellId(physCellId); f1_ue_data_t ue_data = cu_get_f1_ue_data(UE->rrc_ue_id); nr_ho_source_cu_t *source_ctx = UE->ho_context ? UE->ho_context->source : NULL; - DevAssert(!source_ctx || source_ctx->du->setup_req->num_cells_available == 1); - bool ho_reestab_on_source = source_ctx && previous_cell_info->nr_cellid == source_ctx->du->setup_req->cell[0].info.nr_cellid; + DevAssert(!source_ctx || source_ctx->cell != NULL); + const nr_rrc_cell_container_t *source_cell = source_ctx ? source_ctx->cell : NULL; + bool ho_reestab_on_source = source_cell ? cell->info.cell_id == source_cell->info.cell_id : false; if (ho_reestab_on_source) { /* the UE came back on the source DU while doing handover, release at @@ -1553,11 +1592,11 @@ static void rrc_handle_RRCReestablishmentRequest(gNB_RRC_INST *rrc, store_cgc(UE, &source_ctx->old_cgc); /* update to old DU assoc id -- RNTI + secondary DU UE ID further below */ - ue_data.du_assoc_id = source_ctx->du->assoc_id; + ue_data.du_assoc_id = source_cell->assoc_id; bool success = cu_update_f1_ue_data(UE->rrc_ue_id, &ue_data); DevAssert(success); nr_rrc_finalize_ho(UE); - } else if (physCellId != cell_info->nr_pci) { + } else if (physCellId != cell->info.pci) { /* Check if this is a different DU scenario or "too fast movement" scenario */ if (assoc_id != ue_data.du_assoc_id) { /* Different DU scenario - physCellId differs because UE is re-establishing on a different DU. @@ -1569,7 +1608,7 @@ static void rrc_handle_RRCReestablishmentRequest(gNB_RRC_INST *rrc, "UE %d: Re-establishment on different DU (physCellId %ld from old cell != %d from new DU, du_assoc_id %d -> %d)\n", UE->rrc_ue_id, physCellId, - cell_info->nr_pci, + cell->info.pci, ue_data.du_assoc_id, assoc_id); @@ -1608,7 +1647,7 @@ static void rrc_handle_RRCReestablishmentRequest(gNB_RRC_INST *rrc, LOG_I(NR_RRC, "RRC Reestablishment Request from different physCellId (%ld) than current physCellId (%d), fallback to RRC setup\n", physCellId, - cell_info->nr_pci); + cell->info.pci); ngap_cause = NGAP_CAUSE_RADIO_NETWORK_RELEASE_DUE_TO_NGRAN_GENERATED_REASON; goto fallback_rrc_setup; } @@ -1618,12 +1657,24 @@ static void rrc_handle_RRCReestablishmentRequest(gNB_RRC_INST *rrc, // update with new RNTI, and update secondary UE association UE->rnti = msg->crnti; - UE->nr_cellid = msg->nr_cellid; + + /* Update PCell in serving_cells array */ + DevAssert(cell->info.cell_id == msg->nr_cellid); + // Reset the serving set to the new PCell, remove all serving cells from the current PCell's DU. + const ue_serving_cell_t *existing_pcell = ue_get_pcell_entry(UE); + DevAssert(existing_pcell != NULL); + rrc_remove_ue_scells_from_du(UE, existing_pcell->assoc_id); + ue_serving_cell_t *added = rrc_add_ue_serving_cell(UE, cell, RRC_PCELL_INDEX); + if (added == NULL) { + LOG_E(NR_RRC, "Reestablishment: failed to add PCell (cell %ld), rejecting reestablishment\n", cell->info.cell_id); + goto fallback_rrc_setup; + } + ue_data.secondary_ue = msg->gNB_DU_ue_id; bool success = cu_update_f1_ue_data(UE->rrc_ue_id, &ue_data); DevAssert(success); - rrc_gNB_generate_RRCReestablishment(ue_context_p, old_rnti, du); + rrc_gNB_generate_RRCReestablishment(ue_context_p, old_rnti, cell); return; fallback_rrc_setup: @@ -1715,13 +1766,34 @@ static void process_Event_Based_Measurement_Report(gNB_RRC_INST *rrc, neighbour_pci, neighbourCellRSRP); - const f1ap_served_cell_info_t *neigh_cell = get_cell_information_by_phycellId(neighbour_pci); - const f1ap_served_cell_info_t *serving_cell = get_cell_information_by_phycellId(scell_pci); - const neighbour_cell_configuration_t *cell = get_neighbour_cell_config(rrc, serving_cell->nr_cellid); + // Resolve source cell by PCI from UE's serving cells + const nr_rrc_cell_container_t *source_cell = NULL; + FOR_EACH_SEQ_ARR (ue_serving_cell_t *, scell, &ue->serving_cells) { + nr_rrc_cell_container_t *cell_by_id = get_cell_by_cell_id(&rrc->cells, scell->nci); + if (cell_by_id != NULL && cell_by_id->info.pci == scell_pci) { + source_cell = cell_by_id; + break; + } + } + if (source_cell == NULL) { + LOG_W(NR_RRC, + "UE %d: received measurement report but source cell with PCI %d not found in UE's serving cells, skipping\n", + ue->rrc_ue_id, + scell_pci); + continue; + } + + // Resolve target cell via neighbour configuration + const neighbour_cell_configuration_t *cell = get_neighbour_cell_config(rrc, source_cell->info.cell_id); const nr_neighbour_cell_t *neighbour = get_neighbour_cell_by_pci(cell, neighbour_pci); + const nr_rrc_cell_container_t *target_cell = NULL; + if (neighbour != NULL) { + // Resolve target cell by cell_id from neighbour config (may be NULL if no F1 connection) + target_cell = get_cell_by_cell_id(&rrc->cells, neighbour->nrcell_id); + } // CU does not have f1 connection with neighbour cell context. So check does serving cell has this phyCellId as a // neighbour. - if (!neigh_cell && neighbour) { + if (!target_cell && neighbour) { // No F1 connection but static neighbour configuration is available const nr_a3_event_t *a3_event_configuration = get_a3_configuration(rrc, neighbour->physicalCellId); /* if no A3 event configured for this physical cell ID, try to get the default one */ @@ -1740,12 +1812,9 @@ static void process_Event_Based_Measurement_Report(gNB_RRC_INST *rrc, } LOG_D(NR_RRC, "HO LOG: Trigger N2 HO for the neighbour gnb: %u cell: %lu\n", neighbour->gNB_ID, neighbour->nrcell_id); } - } else if (neigh_cell && neighbour) { + } else if (target_cell && neighbour) { /* we know the cell and are connected to the DU! */ - nr_rrc_du_container_t *source_du = get_du_by_cell_id(rrc, serving_cell->nr_cellid); - DevAssert(source_du); - nr_rrc_du_container_t *target_du = get_du_by_cell_id(rrc, neigh_cell->nr_cellid); - nr_rrc_trigger_f1_ho(rrc, ue, source_du, target_du); + nr_rrc_trigger_f1_ho(rrc, ue, source_cell, target_cell); } else { LOG_W(NR_RRC, "UE %d: received A3 event for stronger neighbor PCI %d, but no such neighbour in configuration\n", ue->rrc_ue_id, neighbour_pci); } @@ -2614,12 +2683,15 @@ static void rrc_CU_process_ue_context_release_request(MessageDef *msg_p, sctp_as gNB_RRC_UE_t *UE = &ue_context_p->ue_context; if (UE->ho_context != NULL) { nr_ho_source_cu_t *source_ctx = UE->ho_context->source; - bool from_source_du = source_ctx && source_ctx->du->assoc_id == assoc_id; + bool from_source_du = source_ctx && source_ctx->cell->assoc_id == assoc_id; if (from_source_du) { // we received release request from the source DU, but HO is still // ongoing; free the UE, and remove the HO context. - LOG_W(NR_RRC, "UE %d: release request from source DU ID %ld during HO, marking HO as complete\n", UE->rrc_ue_id, source_ctx->du->setup_req->gNB_DU_id); - RETURN_IF_INVALID_ASSOC_ID(source_ctx->du->assoc_id); + LOG_W(NR_RRC, + "UE %d: release request from source cell PCI %d during HO, marking HO as complete\n", + UE->rrc_ue_id, + source_ctx->cell->info.pci); + RETURN_IF_INVALID_ASSOC_ID(source_ctx->cell->assoc_id); f1ap_ue_context_rel_cmd_t cmd = { .gNB_CU_ue_id = UE->rrc_ue_id, .gNB_DU_ue_id = source_ctx->du_ue_id, @@ -2747,6 +2819,10 @@ static void rrc_CU_process_ue_context_modification_response(MessageDef *msg_p, i nr_ho_source_cu_t *source_ctx = UE->ho_context->source; DevAssert(source_ctx->old_rnti == UE->rnti); nr_rrc_apply_target_context(UE); + /* Update cell association after handover */ + if (!nr_rrc_update_cell_assoc_after_ho(rrc, UE)) { + return; + } } } @@ -2768,7 +2844,9 @@ static void rrc_CU_process_ue_modification_required(MessageDef *msg_p, instance_ } gNB_RRC_UE_t *UE = &ue_context_p->ue_context; - if (UE->ho_context && UE->ho_context->source && UE->ho_context->source->du && UE->ho_context->source->du->assoc_id == assoc_id) { + bool on_source_du = UE->ho_context && UE->ho_context->source && UE->ho_context->source->cell + && UE->ho_context->source->cell->assoc_id == assoc_id; + if (on_source_du) { LOG_W(NR_RRC, "UE %d: UE Context Modification Required during handover, ignoring message\n", UE->rrc_ue_id); return; } @@ -3606,14 +3684,14 @@ static int rrc_fill_f1_drb_to_setup(const gNB_RRC_INST *rrc, const gNB_RRC_UE_t * - Handover preparation information (if provided) * @param rrc RRC instance * @param ue UE context - * @param du Target DU container containing cell and setup information + * @param cell Target cell container containing cell information * @param ho_prep_info Optional handover preparation information */ void rrc_f1_ue_context_setup_for_target_du(const gNB_RRC_INST *rrc, gNB_RRC_UE_t *ue, - const nr_rrc_du_container_t *du, + const nr_rrc_cell_container_t *cell, const byte_array_t *ho_prep_info) { - DevAssert(du != NULL); + DevAssert(cell != NULL); /* Handle two scenarios: * 1. Handover (ho_prep_info provided): Prepare and copy handover preparation information * for the target DU. The target DU will assign a new gNB-DU UE ID in the response. @@ -3635,7 +3713,7 @@ void rrc_f1_ue_context_setup_for_target_du(const gNB_RRC_INST *rrc, du_ue_id = ue_data.secondary_ue; opt_du_ue_id = &du_ue_id; } - LOG_I(NR_RRC, "Triggering UE Context Setup for UE %d on DU %d\n", ue->rrc_ue_id, du->assoc_id); + LOG_I(NR_RRC, "Triggering UE Context Setup for UE %d on DU %d\n", ue->rrc_ue_id, cell->assoc_id); int nb_srb = 2; f1ap_srb_to_setup_t *srbs = calloc_or_fail(nb_srb, sizeof(*srbs)); f1ap_drb_to_setup_t *drbs = calloc_or_fail(MAX_DRBS_PER_UE, sizeof(*drbs)); @@ -3649,12 +3727,12 @@ void rrc_f1_ue_context_setup_for_target_du(const gNB_RRC_INST *rrc, /* Update measurement config for target DU */ free_MeasConfig(ue->measConfig); - ue->measConfig = nr_rrc_get_measconfig(rrc, du->setup_req->cell[0].info.nr_cellid); + ue->measConfig = nr_rrc_get_measconfig(rrc, cell->info.cell_id); byte_array_t *meas_config = calloc_or_fail(1, sizeof(*meas_config)); meas_config->buf = calloc_or_fail(1, NR_RRC_BUF_SIZE); meas_config->len = do_NR_MeasConfig(ue->measConfig, meas_config->buf, NR_RRC_BUF_SIZE); /* Fill common fields */ - f1ap_ue_context_setup_req_t req = rrc_fill_f1_ue_context_setup(ue, du, opt_du_ue_id); + f1ap_ue_context_setup_req_t req = rrc_fill_f1_ue_context_setup(ue, cell, opt_du_ue_id); /* Fill target DU specific fields */ req.srbs_len = 2; req.srbs = srbs; @@ -3663,8 +3741,8 @@ void rrc_f1_ue_context_setup_for_target_du(const gNB_RRC_INST *rrc, req.cu_to_du_rrc_info.meas_config = meas_config; req.cu_to_du_rrc_info.ho_prep_info = hpi; - RETURN_IF_INVALID_ASSOC_ID(du->assoc_id); - rrc->mac_rrc.ue_context_setup_request(du->assoc_id, &req); + RETURN_IF_INVALID_ASSOC_ID(cell->assoc_id); + rrc->mac_rrc.ue_context_setup_request(cell->assoc_id, &req); free_ue_context_setup_req(&req); } @@ -3689,7 +3767,8 @@ void rrc_f1_ue_context_setup_from_e1_response(const gNB_RRC_INST *rrc, AssertFatal(!NODE_IS_DU(rrc->node_type), "illegal node type DU!\n"); - nr_rrc_du_container_t *du = get_du_for_ue((gNB_RRC_INST *)rrc, ue_p->rrc_ue_id); + nr_rrc_cell_container_t *cell = rrc_get_pcell_for_ue((gNB_RRC_INST *)rrc, ue_p); + DevAssert(cell != NULL); int nb_srb = 1; f1ap_srb_to_setup_t *srbs = calloc_or_fail(nb_srb, sizeof(*srbs)); @@ -3706,7 +3785,7 @@ void rrc_f1_ue_context_setup_from_e1_response(const gNB_RRC_INST *rrc, const uint32_t du_ue_id = ue_data.secondary_ue; /* Fill common fields */ - f1ap_ue_context_setup_req_t ue_context_setup_req = rrc_fill_f1_ue_context_setup(ue_p, du, &du_ue_id); + f1ap_ue_context_setup_req_t ue_context_setup_req = rrc_fill_f1_ue_context_setup(ue_p, cell, &du_ue_id); /* Fill bearers information for serving DU */ ue_context_setup_req.srbs_len = nb_srb; ue_context_setup_req.srbs = srbs; diff --git a/openair2/RRC/NR/rrc_gNB_NGAP.c b/openair2/RRC/NR/rrc_gNB_NGAP.c index acb556d8f5..515091a459 100644 --- a/openair2/RRC/NR/rrc_gNB_NGAP.c +++ b/openair2/RRC/NR/rrc_gNB_NGAP.c @@ -92,6 +92,7 @@ #include "rrc_gNB_du.h" #include "rrc_cell_management.h" #include "common/utils/alg/find.h" +#include "common/utils/nr/nr_common.h" #ifdef E2_AGENT #include "openair2/E2AP/RAN_FUNCTION/O-RAN/ran_func_rc_extern.h" @@ -255,8 +256,9 @@ void rrc_gNB_send_NGAP_NAS_FIRST_REQ(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE, NR_RRC /* UE is connected to only one PLMN at a time: store this as the serving PLMN */ UE->serving_plmn = req->plmn; - // Cell ID (NR CGI) - req->nr_cell_id = UE->nr_cellid; + nr_rrc_cell_container_t *cell = rrc_get_pcell_for_ue(rrc, UE); + DevAssert(cell); + req->nr_cell_id = cell->info.cell_id; // PLMN (NR CGI and TAI) plmn_id_t *p = &req->plmn; LOG_I(NGAP, "Selected PLMN in the NG Initial UE Message: MCC=%03d MNC=%0*d\n", p->mcc, p->mnc_digit_length, p->mnc); @@ -748,8 +750,10 @@ void rrc_gNB_send_NGAP_UPLINK_NAS(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE, const NR_ NGAP_UPLINK_NAS(msg_p).nas_pdu.buf = buf; /* Fill PLMN and location info: use serving PLMN of the UE */ NGAP_UPLINK_NAS(msg_p).plmn = UE->serving_plmn; - NGAP_UPLINK_NAS(msg_p).nr_cell_id = rrc->nr_cellid; - NGAP_UPLINK_NAS(msg_p).tac = rrc->configuration.tac; + nr_rrc_cell_container_t *cell = rrc_get_pcell_for_ue(rrc, UE); + DevAssert(cell); + NGAP_UPLINK_NAS(msg_p).nr_cell_id = cell->info.cell_id; + NGAP_UPLINK_NAS(msg_p).tac = cell->info.tac != 0 ? cell->info.tac : rrc->configuration.tac; itti_send_msg_to_task(TASK_NGAP, rrc->module_id, msg_p); } @@ -1151,6 +1155,7 @@ int rrc_gNB_process_Handover_Request(gNB_RRC_INST *rrc, instance_t instance, nga // allocate context for target UE->ho_context = alloc_ho_ctx(HO_CTX_TARGET); + UE->ho_context->target->cell = cell; UE->ho_context->target->ho_trigger = nr_rrc_trigger_n2_ho_target; // Store IDs in UE context @@ -1173,7 +1178,7 @@ int rrc_gNB_process_Handover_Request(gNB_RRC_INST *rrc, instance_t instance, nga memset(UE->kgnb, 0, SECURITY_KEY_LENGTH); // Derive KgNB* const nr_rrc_cell_info_t *cell_info = &cell->info; - uint32_t ssb_arfcn = get_ssb_arfcn(du); + uint32_t ssb_arfcn = get_ssb_arfcn(cell); nr_derive_key_ng_ran_star(cell_info->pci, ssb_arfcn, UE->nh, UE->kgnb); UE->as_security_active = true; // Activate SRBs @@ -1458,24 +1463,28 @@ void rrc_gNB_send_NGAP_HANDOVER_REQUEST_ACKNOWLEDGE(gNB_RRC_INST *rrc, gNB_RRC_U void rrc_gNB_send_NGAP_HANDOVER_NOTIFY(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE) { LOG_I(NR_RRC, "Triggering NGAP Handover Notify\n"); - nr_rrc_du_container_t *du = get_du_by_cell_id(rrc, rrc->nr_cellid); + nr_rrc_cell_container_t *cell = rrc_get_pcell_for_ue(rrc, UE); + if (cell == NULL) { + LOG_E(NR_RRC, "Failed to send Handover Notify: no PCell found for UE %d\n", UE->rrc_ue_id); + return; + } + nr_rrc_du_container_t *du = get_du_for_ue(rrc, UE->rrc_ue_id); if (du == NULL) { - LOG_E(NR_RRC, "Failed to send Handover Notify: no DU found with NR Cell ID=%lu\n", rrc->nr_cellid); + LOG_E(NR_RRC, "Failed to send Handover Notify: no DU found for UE %d\n", UE->rrc_ue_id); return; } - MessageDef *msg_p = itti_alloc_new_message(TASK_RRC_GNB, 0, NGAP_HANDOVER_NOTIFY); ngap_handover_notify_t *ho_notify = &NGAP_HANDOVER_NOTIFY(msg_p); memset(ho_notify, 0, sizeof(*ho_notify)); ho_notify->gNB_ue_ngap_id = UE->rrc_ue_id; ho_notify->amf_ue_ngap_id = UE->amf_ue_ngap_id; - ho_notify->user_info.nrCellIdentity = rrc->nr_cellid; + ho_notify->user_info.nrCellIdentity = cell->info.cell_id; target_ran_node_id_t *target_ng_ran = &ho_notify->user_info.target_ng_ran; - target_ng_ran->tac = *du->setup_req->cell->info.tac; + target_ng_ran->tac = cell->info.tac; target_ng_ran->targetgNBId = rrc->node_id; - target_ng_ran->plmn_identity = du->setup_req->cell->info.plmn; + target_ng_ran->plmn_identity = cell->info.plmn; itti_send_msg_to_task(TASK_NGAP, rrc->module_id, msg_p); } @@ -1612,7 +1621,23 @@ int rrc_gNB_process_PAGING_IND(MessageDef *msg_p, instance_t instance) if (NODE_IS_CU(RC.nrrrc[instance]->node_type)) { MessageDef *m = itti_alloc_new_message(TASK_RRC_GNB, 0, F1AP_PAGING_IND); F1AP_PAGING_IND(m).plmn = *req_plmn; - F1AP_PAGING_IND (m).nr_cellid = RC.nrrrc[j]->nr_cellid; + // Construct 5G-S-TMSI-Part1 from paging identity to find UE + const fiveg_s_tmsi_t *s_tmsi = &msg->ue_paging_identity.s_tmsi; + uint64_t s_tmsi_part1 = nr_construct_5g_s_tmsi_part1(s_tmsi->amf_set_id, s_tmsi->amf_pointer, s_tmsi->m_tmsi); + // Try to find UE by 5G-S-TMSI-Part1 + rrc_gNB_ue_context_t *ue_context_p = rrc_gNB_ue_context_5g_s_tmsi_exist(RC.nrrrc[instance], s_tmsi_part1); + if (ue_context_p == NULL) { + LOG_W(NR_RRC, "UE not found by paging identity\n"); + return -1; + } + // UE found: use its associated cell + gNB_RRC_UE_t *UE = &ue_context_p->ue_context; + nr_rrc_cell_container_t *cell = rrc_get_pcell_for_ue(RC.nrrrc[instance], UE); + if (cell == NULL) { + LOG_W(NR_RRC, "UE %d (rnti %04x) found by paging identity but has no valid cell\n", UE->rrc_ue_id, UE->rnti); + return -1; + } + F1AP_PAGING_IND(m).nr_cellid = cell->info.cell_id; F1AP_PAGING_IND(m).ueidentityindexvalue = (uint16_t)(msg->ue_paging_identity.s_tmsi.m_tmsi % 1024); F1AP_PAGING_IND(m).fiveg_s_tmsi = msg->ue_paging_identity.s_tmsi.m_tmsi; F1AP_PAGING_IND(m).paging_drx = msg->paging_drx; diff --git a/openair2/RRC/NR/rrc_gNB_cuup.c b/openair2/RRC/NR/rrc_gNB_cuup.c index 1e3df71822..8292699a5c 100644 --- a/openair2/RRC/NR/rrc_gNB_cuup.c +++ b/openair2/RRC/NR/rrc_gNB_cuup.c @@ -43,6 +43,7 @@ #include "tree.h" #include "e1ap_interface_management.h" #include "rrc_gNB_NGAP.h" +#include "rrc_cell_management.h" static int cuup_compare(const nr_rrc_cuup_container_t *a, const nr_rrc_cuup_container_t *b) { diff --git a/openair2/RRC/NR/rrc_gNB_du.c b/openair2/RRC/NR/rrc_gNB_du.c index 6024079936..5b1be47d5e 100644 --- a/openair2/RRC/NR/rrc_gNB_du.c +++ b/openair2/RRC/NR/rrc_gNB_du.c @@ -79,10 +79,15 @@ static int ssb_arfcn_mtc(const NR_MeasurementTimingConfiguration_t *mtc) return mtlist->list.array[0]->frequencyAndTiming->carrierFreq; } -int get_ssb_arfcn(const struct nr_rrc_du_container_t *du) +/** @brief Find first cell owned by this DU (assoc_id) and return its SSB ARFCN + * @note Current assumption is that each DU serves only one cell*/ +int get_ssb_arfcn(const struct nr_rrc_cell_container_t *cell) { - DevAssert(du != NULL && du->mtc != NULL); - return ssb_arfcn_mtc(du->mtc); + DevAssert(cell != NULL); + if (cell->mtc != NULL) + return ssb_arfcn_mtc(cell->mtc); + AssertFatal(false, "no mtc found for cell %ld\n", cell->info.cell_id); + return 0; } static bool rrc_gNB_plmn_matches(const gNB_RRC_INST *rrc, const f1ap_served_cell_info_t *info) @@ -172,22 +177,6 @@ static neighbour_cell_configuration_t *get_cell_neighbour_list(const gNB_RRC_INS return (neighbour_cell_configuration_t *)it; } -const struct f1ap_served_cell_info_t *get_cell_information_by_phycellId(int phyCellId) -{ - gNB_RRC_INST *rrc = RC.nrrrc[0]; - nr_rrc_du_container_t *it = NULL; - RB_FOREACH (it, rrc_du_tree, &rrc->dus) { - for (int cellIdx = 0; cellIdx < it->setup_req->num_cells_available; cellIdx++) { - const f1ap_served_cell_info_t *cell_info = &(it->setup_req->cell[cellIdx].info); - if (cell_info->nr_pci == phyCellId) { - LOG_D(NR_RRC, "HO LOG: Found cell with phyCellId %d\n", phyCellId); - return cell_info; - } - } - } - return NULL; -} - static void is_intra_frequency_neighbour(void *ssb_arfcn, void *neighbour_cell) { uint32_t *ssb_arfcn_ptr = (uint32_t *)ssb_arfcn; @@ -201,12 +190,9 @@ static void is_intra_frequency_neighbour(void *ssb_arfcn, void *neighbour_cell) /** * @brief Labels neighbour cells if they are intra frequency to prepare meas config only for intra frequency ho * @param[in] rrc Pointer to RRC instance - * @param[in] du Pointer to DU container * @param[in] cell Pointer to cell container */ -static void label_intra_frequency_neighbours(gNB_RRC_INST *rrc, - const nr_rrc_du_container_t *du, - const nr_rrc_cell_container_t *cell) +static void label_intra_frequency_neighbours(gNB_RRC_INST *rrc, const nr_rrc_cell_container_t *cell) { if (!rrc->neighbour_cell_configuration) return; @@ -215,7 +201,7 @@ static void label_intra_frequency_neighbours(gNB_RRC_INST *rrc, if (!neighbour_cell_config) return; - uint32_t ssb_arfcn = get_ssb_arfcn(du); + uint32_t ssb_arfcn = get_ssb_arfcn(cell); LOG_D(NR_RRC, "Cell %lu (PCI %d, SSB ARFCN %u) has neighbour cell configuration, labeling intra-frequency neighbours\n", cell->info.cell_id, @@ -422,18 +408,11 @@ void rrc_gNB_process_f1_setup_req(f1ap_setup_req_t *req, sctp_assoc_t assoc_id) // DU is accepted, add it to tree and add cell to DU's cell array nr_rrc_du_container_t *du = calloc_or_fail(1, sizeof(*du)); - du->mtc = new->mtc; // TODO: remove in upcoming commit (kept for backward compatibility) du->assoc_id = assoc_id; du->gNB_DU_id = req->gNB_DU_id; du->gNB_DU_name = strdup(req->gNB_DU_name ? req->gNB_DU_name : "N/A"); memcpy(du->rrc_ver, req->rrc_ver, sizeof(du->rrc_ver)); - du->setup_req = calloc(1,sizeof(*du->setup_req)); - AssertFatal(du->setup_req, "out of memory\n"); - // Copy F1AP message - *du->setup_req = cp_f1ap_setup_request(req); - // MIB can be null and configured later via DU Configuration Update - du->mib = mib; - du->sib1 = sib1; + // Initialize cell array for this DU seq_arr_init(&du->cells, sizeof(nr_rrc_cell_container_t *)); nr_rrc_du_container_t *du_collision = rrc_add_du(rrc, du); @@ -471,7 +450,7 @@ void rrc_gNB_process_f1_setup_req(f1ap_setup_req_t *req, sctp_assoc_t assoc_id) } if (new->mib != NULL &&new->sib1 != NULL) - label_intra_frequency_neighbours(rrc, du, new); + label_intra_frequency_neighbours(rrc, new); f1ap_setup_resp_t resp = {.transaction_id = req->transaction_id, .num_cells_to_activate = 1, @@ -501,6 +480,9 @@ static int invalidate_du_connections(gNB_RRC_INST *rrc, sctp_assoc_t assoc_id) if (UE->ho_context != NULL) LOG_W(NR_RRC, "DU disconnected while handover for UE %d active\n", ue_id); + // Remove SCells from this DU for this UE + rrc_remove_ue_scells_from_du(UE, assoc_id); + f1_ue_data_t ue_data = cu_get_f1_ue_data(ue_id); if (ue_data.du_assoc_id != assoc_id) continue; /* this UE is on another DU */ @@ -689,8 +671,9 @@ void rrc_gNB_process_f1_du_configuration_update(f1ap_gnb_du_configuration_update LOG_I(NR_RRC, "update system information of DU %ld\n", du->gNB_DU_id); } } - if (du->mib != NULL && du->sib1 != NULL && cell != NULL) { - label_intra_frequency_neighbours(rrc, du, cell); + + if (cell && cell->mib != NULL && cell->sib1 != NULL && cell->assoc_id == du->assoc_id) { + label_intra_frequency_neighbours(rrc, cell); } } @@ -723,15 +706,6 @@ void rrc_CU_process_f1_lost_connection(gNB_RRC_INST *rrc, f1ap_lost_connection_t return; } - f1ap_setup_req_t *req = du->setup_req; - LOG_I(NR_RRC, "releasing DU ID %ld (%s) on assoc_id %d\n", req->gNB_DU_id, req->gNB_DU_name, assoc_id); - ASN_STRUCT_FREE(asn_DEF_NR_MIB, du->mib); - ASN_STRUCT_FREE(asn_DEF_NR_SIB1, du->sib1); - ASN_STRUCT_FREE(asn_DEF_NR_MeasurementTimingConfiguration, du->mtc); - if (du->setup_req) - free_f1ap_setup_request(du->setup_req); - free(du->setup_req); - // Clean up DU and associated cell resources rrc_cleanup_du(rrc, du); @@ -749,46 +723,48 @@ nr_rrc_du_container_t *get_du_for_ue(gNB_RRC_INST *rrc, uint32_t ue_id) return get_du_by_assoc_id(rrc, ue_data.du_assoc_id); } -/* \brief find DU by cell ID. Note: currently the CU is limited to one cell per - * DU, hence here, DU == cell. Modify this to look up a specific cell. */ -nr_rrc_du_container_t *get_du_by_cell_id(gNB_RRC_INST *rrc, uint64_t cell_id) -{ - nr_rrc_du_container_t *du = NULL; - RB_FOREACH(du, rrc_du_tree, &rrc->dus) { - if (cell_id == du->setup_req->cell[0].info.nr_cellid) - return du; - } - return NULL; -} - void dump_du_info(const gNB_RRC_INST *rrc, FILE *f) { fprintf(f, "%ld connected DUs \n", rrc->num_dus); - int i = 1; + int du_idx = 1; nr_rrc_du_container_t *du = NULL; - /* cast is necessary to eliminate warning "discards ‘const’ qualifier" */ + /* cast is necessary to eliminate warning "discards 'const' qualifier" */ RB_FOREACH(du, rrc_du_tree, &((gNB_RRC_INST *)rrc)->dus) { - const f1ap_setup_req_t *sr = du->setup_req; - fprintf(f, "[%d] DU ID %ld (%s) ", i++, sr->gNB_DU_id, sr->gNB_DU_name); + fprintf(f, "[%d] DU ID %ld (%s) ", du_idx++, du->gNB_DU_id, du->gNB_DU_name); if (du->assoc_id == -1) { fprintf(f, "integrated DU-CU"); } else { fprintf(f, "assoc_id %d", du->assoc_id); } - const f1ap_served_cell_info_t *info = &sr->cell[0].info; - fprintf(f, ": nrCellID %ld, PCI %d, SSB ARFCN %d\n", info->nr_cellid, info->nr_pci, get_ssb_arfcn(du)); - if (info->mode == F1AP_MODE_TDD) { - const f1ap_nr_frequency_info_t *fi = &info->tdd.freqinfo; - const f1ap_transmission_bandwidth_t *tb = &info->tdd.tbw; - fprintf(f, " TDD: band %d ARFCN %d SCS %d (kHz) PRB %d\n", fi->band, fi->arfcn, 15 * (1 << tb->scs), tb->nrb); + /* Print all cells for this DU */ + int cell_count = seq_arr_size(&du->cells); + + if (cell_count == 0) { + fprintf(f, ": no cells\n"); } else { - const f1ap_nr_frequency_info_t *dfi = &info->fdd.dl_freqinfo; - const f1ap_transmission_bandwidth_t *dtb = &info->fdd.dl_tbw; - fprintf(f, " FDD: DL band %d ARFCN %d SCS %d (kHz) PRB %d\n", dfi->band, dfi->arfcn, 15 * (1 << dtb->scs), dtb->nrb); - const f1ap_nr_frequency_info_t *ufi = &info->fdd.ul_freqinfo; - const f1ap_transmission_bandwidth_t *utb = &info->fdd.ul_tbw; - fprintf(f, " UL band %d ARFCN %d SCS %d (kHz) PRB %d\n", ufi->band, ufi->arfcn, 15 * (1 << utb->scs), utb->nrb); + fprintf(f, ": %d cell%s\n", cell_count, cell_count > 1 ? "s" : ""); + int cell_idx = 1; + FOR_EACH_SEQ_ARR (nr_rrc_cell_container_t **, cell_ptr, &du->cells) { + nr_rrc_cell_container_t *cell = *cell_ptr; + const char *mode = (cell->info.mode == NR_MODE_TDD) ? "TDD" : "FDD"; + fprintf(f, + " [%d] nrCellID %lu, PCI %d, Mode %s, SSB ARFCN %d\n", + cell_idx++, + cell->info.cell_id, + cell->info.pci, + mode, + ssb_arfcn_mtc(cell->mtc)); + if (cell->info.mode == NR_MODE_TDD) { + const nr_rrc_freq_info_t *tdd = &cell->info.tdd.dlul; + fprintf(f, " TDD: band %d ARFCN %d SCS %d (kHz) PRB %d\n", tdd->band, tdd->arfcn, 15 * (1 << tdd->scs), tdd->nrb); + } else { + const nr_rrc_freq_info_t *dl = &cell->info.fdd.dl; + const nr_rrc_freq_info_t *ul = &cell->info.fdd.ul; + fprintf(f, " DL: band %d ARFCN %d SCS %d (kHz) PRB %d\n", dl->band, dl->arfcn, 15 * (1 << dl->scs), dl->nrb); + fprintf(f, " UL: band %d ARFCN %d SCS %d (kHz) PRB %d\n", ul->band, ul->arfcn, 15 * (1 << ul->scs), ul->nrb); + } + } } } } diff --git a/openair2/RRC/NR/rrc_gNB_du.h b/openair2/RRC/NR/rrc_gNB_du.h index 4206096aa2..ce033e91e8 100644 --- a/openair2/RRC/NR/rrc_gNB_du.h +++ b/openair2/RRC/NR/rrc_gNB_du.h @@ -30,6 +30,7 @@ struct f1ap_setup_req_s; struct f1ap_lost_connection_t; struct gNB_RRC_INST_s; struct nr_rrc_du_container_t; +struct nr_rrc_cell_container_t; struct f1ap_gnb_du_configuration_update_s; struct f1ap_served_cell_info_t; @@ -38,14 +39,12 @@ void rrc_CU_process_f1_lost_connection(struct gNB_RRC_INST_s *rrc, struct f1ap_l void rrc_gNB_process_f1_du_configuration_update(struct f1ap_gnb_du_configuration_update_s *conf_up, sctp_assoc_t assoc_id); struct nr_rrc_du_container_t *get_du_for_ue(struct gNB_RRC_INST_s *rrc, uint32_t ue_id); -const struct f1ap_served_cell_info_t *get_cell_information_by_phycellId(int phyCellId); -struct nr_rrc_du_container_t *get_du_by_cell_id(struct gNB_RRC_INST_s *rrc, uint64_t cell_id); void dump_du_info(const struct gNB_RRC_INST_s *rrc, FILE *f); int get_dl_band(const struct f1ap_served_cell_info_t *cell_info); int get_ssb_scs(const struct f1ap_served_cell_info_t *cell_info); -int get_ssb_arfcn(const struct nr_rrc_du_container_t *du); +int get_ssb_arfcn(const struct nr_rrc_cell_container_t *cell); struct nr_rrc_du_container_t *find_target_du(struct gNB_RRC_INST_s *rrc, sctp_assoc_t source_assoc_id); diff --git a/openair2/RRC/NR/rrc_gNB_mobility.c b/openair2/RRC/NR/rrc_gNB_mobility.c index a2230c1146..d18937fbc5 100644 --- a/openair2/RRC/NR/rrc_gNB_mobility.c +++ b/openair2/RRC/NR/rrc_gNB_mobility.c @@ -27,6 +27,7 @@ #include "nr_rrc_proto.h" #include "rrc_gNB_du.h" +#include "rrc_cell_management.h" #include "rrc_gNB_radio_bearers.h" #include "rrc_gNB_UE_context.h" #include "openair2/LAYER2/NR_MAC_COMMON/nr_mac.h" @@ -37,6 +38,7 @@ #include "openair3/SECU/key_nas_deriver.h" #include "openair2/RRC/NR/rrc_gNB_NGAP.h" #include "NR_DL-DCCH-MessageType.h" +#include "rrc_cell_management.h" #ifdef E2_AGENT #include "openair2/E2AP/RAN_FUNCTION/O-RAN/ran_func_rc_extern.h" @@ -80,15 +82,12 @@ void nr_rrc_apply_target_context(gNB_RRC_UE_t *UE) /* update F1 data: secondary UE association and DU association */ ue_data.secondary_ue = target_ctx->du_ue_id; - ue_data.du_assoc_id = target_ctx->du->assoc_id; + ue_data.du_assoc_id = target_ctx->cell->assoc_id; bool success = cu_update_f1_ue_data(UE->rrc_ue_id, &ue_data); DevAssert(success); /* update UE RNTI */ UE->rnti = target_ctx->new_rnti; - - /* update UE NR cell ID */ - UE->nr_cellid = target_ctx->du->setup_req->cell[0].info.nr_cellid; } /* \brief Initiate a handover of UE to a specific target cell handled by this @@ -101,8 +100,7 @@ void nr_rrc_apply_target_context(gNB_RRC_UE_t *UE) * \param ho_ctxt contextual data for the type of handover (F1, N2, Xn) */ static void nr_initiate_handover(const gNB_RRC_INST *rrc, gNB_RRC_UE_t *ue, - const nr_rrc_du_container_t *source_du, - const nr_rrc_du_container_t *target_du, + const nr_rrc_cell_container_t *source_cell, byte_array_t *ho_prep_info, ho_req_ack_t ack, ho_success_t success, @@ -111,10 +109,10 @@ static void nr_initiate_handover(const gNB_RRC_INST *rrc, { DevAssert(rrc != NULL); DevAssert(ue != NULL); - DevAssert(target_du != NULL); - // source_du might be NULL -> inter-CU handover + // source_cell might be NULL -> inter-CU handover DevAssert(ho_prep_info->buf != NULL && ho_prep_info->len > 0); DevAssert(ue->ho_context); + DevAssert(ue->ho_context->target != NULL); // if any reconfiguration is ongoing, abort handover request for (int i = 0; i < NR_RRC_TRANSACTION_IDENTIFIER_NUMBER; ++i) { @@ -125,7 +123,9 @@ static void nr_initiate_handover(const gNB_RRC_INST *rrc, } nr_handover_context_t *ho_ctx = ue->ho_context; - ho_ctx->target->du = target_du; + const nr_rrc_cell_container_t *target_cell = ho_ctx->target->cell; + DevAssert(target_cell != NULL); + // we will know target->{du_ue_id,new_rnti} once we have UE ctxt setup // response ho_ctx->target->ho_req_ack = ack; @@ -133,14 +133,14 @@ static void nr_initiate_handover(const gNB_RRC_INST *rrc, ho_ctx->target->ho_failure = failure; const f1_ue_data_t ue_data = cu_get_f1_ue_data(ue->rrc_ue_id); - if (source_du != NULL) { - DevAssert(source_du->assoc_id == ue_data.du_assoc_id); - // we also have the source DU (F1 handover), store meta info + if (source_cell != NULL) { + DevAssert(source_cell->assoc_id == ue_data.du_assoc_id); ho_ctx->source->ho_cancel = cancel; - ho_ctx->source->du = source_du; + ho_ctx->source->cell = source_cell; ho_ctx->source->du_ue_id = ue_data.secondary_ue; ho_ctx->source->old_rnti = ue->rnti; + DevAssert(source_cell->assoc_id == ue_data.du_assoc_id); // Save the GTP-U tunnel info for source DU before process UE context setup request/response // since tunnel info will be updated by calling store_du_f1u_tunnel() in rrc_CU_process_ue_context_setup_response() @@ -153,14 +153,14 @@ static void nr_initiate_handover(const gNB_RRC_INST *rrc, } LOG_A(NR_RRC, - "Handover triggered for UE %u/RNTI %04x towards DU %ld/assoc_id %d/PCI %d\n", + "Handover triggered for UE %u/RNTI %04x towards cell %ld/assoc_id %d/PCI %d\n", ue->rrc_ue_id, ue->rnti, - target_du->setup_req->gNB_DU_id, - target_du->assoc_id, - target_du->setup_req->cell[0].info.nr_pci); + target_cell->info.cell_id, + target_cell->assoc_id, + target_cell->info.pci); - rrc_f1_ue_context_setup_for_target_du(rrc, ue, target_du, ho_prep_info); + rrc_f1_ue_context_setup_for_target_du(rrc, ue, target_cell, ho_prep_info); } typedef struct deliver_ue_ctxt_modification_data_t { @@ -249,15 +249,19 @@ static void nr_rrc_f1_ho_complete(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE) DevAssert(UE->ho_context != NULL); nr_ho_source_cu_t *source_ctx = UE->ho_context->source; DevAssert(source_ctx != NULL); - RETURN_IF_INVALID_ASSOC_ID(source_ctx->du->assoc_id); + RETURN_IF_INVALID_ASSOC_ID(source_ctx->cell->assoc_id); f1ap_ue_context_rel_cmd_t cmd = { .gNB_CU_ue_id = UE->rrc_ue_id, .gNB_DU_ue_id = source_ctx->du_ue_id, .cause = F1AP_CAUSE_RADIO_NETWORK, .cause_value = 5, // 5 = F1AP_CauseRadioNetwork_interaction_with_other_procedure }; - rrc->mac_rrc.ue_context_release_command(source_ctx->du->assoc_id, &cmd); - LOG_I(NR_RRC, "UE %d Handover: trigger release on DU assoc_id %d\n", UE->rrc_ue_id, source_ctx->du->assoc_id); + rrc->mac_rrc.ue_context_release_command(source_ctx->cell->assoc_id, &cmd); + LOG_I(NR_RRC, + "UE %d Handover: trigger release on cell PCI %d/assoc_id %d\n", + UE->rrc_ue_id, + source_ctx->cell->info.pci, + source_ctx->cell->assoc_id); } static void nr_rrc_cancel_f1_ho(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE) @@ -271,10 +275,13 @@ static void nr_rrc_cancel_f1_ho(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE) .cause = F1AP_CAUSE_RADIO_NETWORK, // better .cause_value = 5, // 5 = F1AP_CauseRadioNetwork_interaction_with_other_procedure }; - rrc->mac_rrc.ue_context_release_command(target_ctx->du->assoc_id, &cmd); + rrc->mac_rrc.ue_context_release_command(target_ctx->cell->assoc_id, &cmd); } -void nr_rrc_trigger_f1_ho(gNB_RRC_INST *rrc, gNB_RRC_UE_t *ue, nr_rrc_du_container_t *source_du, nr_rrc_du_container_t *target_du) +void nr_rrc_trigger_f1_ho(gNB_RRC_INST *rrc, + gNB_RRC_UE_t *ue, + const nr_rrc_cell_container_t *source_cell, + const nr_rrc_cell_container_t *target_cell) { DevAssert(rrc != NULL); DevAssert(ue != NULL); @@ -288,6 +295,7 @@ void nr_rrc_trigger_f1_ho(gNB_RRC_INST *rrc, gNB_RRC_UE_t *ue, nr_rrc_du_contain return; } ue->ho_context = alloc_ho_ctx(HO_CTX_BOTH); + ue->ho_context->target->cell = target_cell; // corresponds to a "handover request", 38.300 Sec 9.3.2.3 // see also 38.413 Sec 9.3.1.29 for information on source-CU to target-CU @@ -299,7 +307,7 @@ void nr_rrc_trigger_f1_ho(gNB_RRC_INST *rrc, gNB_RRC_UE_t *ue, nr_rrc_du_contain ho_success_t success = nr_rrc_f1_ho_complete; ho_cancel_t cancel = nr_rrc_cancel_f1_ho; byte_array_t hpi = {.buf = buf, .len = size}; - nr_initiate_handover(rrc, ue, source_du, target_du, &hpi, ack, success, cancel, NULL); + nr_initiate_handover(rrc, ue, source_cell, &hpi, ack, success, cancel, NULL); } void nr_rrc_finalize_ho(gNB_RRC_UE_t *ue) @@ -324,6 +332,11 @@ void nr_HO_F1_trigger_telnet(gNB_RRC_INST *rrc, uint32_t rrc_ue_id) LOG_E(NR_RRC, "cannot get source gNB-DU with assoc_id %d for UE %u\n", ue_data.du_assoc_id, ue->rrc_ue_id); return; } + nr_rrc_cell_container_t *source_cell = rrc_get_pcell_for_ue(rrc, ue); + if (source_cell == NULL) { + LOG_E(NR_RRC, "cannot get source cell for UE %u\n", ue->rrc_ue_id); + return; + } nr_rrc_du_container_t *target_du = find_target_du(rrc, source_du->assoc_id); if (target_du == NULL) { @@ -331,7 +344,19 @@ void nr_HO_F1_trigger_telnet(gNB_RRC_INST *rrc, uint32_t rrc_ue_id) return; } - nr_rrc_trigger_f1_ho(rrc, ue, source_du, target_du); + // For target cell, get the first cell from target DU + // (in future, this could be selected based on measurement) + nr_rrc_cell_container_t *target_cell = NULL; + FOR_EACH_SEQ_ARR (nr_rrc_cell_container_t **, cell_ptr, &target_du->cells) { + target_cell = *cell_ptr; + break; // Get first cell + } + if (target_cell == NULL) { + LOG_E(NR_RRC, "cannot get target cell for UE %u\n", ue->rrc_ue_id); + return; + } + + nr_rrc_trigger_f1_ho(rrc, ue, source_cell, target_cell); } /** @brief Generate the HandoverPreparationInformation to be carried @@ -395,6 +420,74 @@ static byte_array_t rrc_gNB_encode_HandoverCommand(gNB_RRC_UE_t *UE, gNB_RRC_INS return out; } +/** @brief Update cell association after handover: set PCell on target DU + * @param[in] rrc RRC instance + * @param[in] UE UE context + * @return true on success, false on failure + * + * Generic for all handover types (F1, N2, Xn). Behaviour depends on whether + * ho_context->source is set on this CU: + * - Source present (intra-CU): F1 HO, remove all serving cells belonging to + * the source DU, then add target cell as PCell. + * - Source absent (inter-CU): N2 or Xn handover (target CU created new UE + * on Handover Request). This UE context has no serving cells yet; add + * target cell as first PCell. */ +bool nr_rrc_update_cell_assoc_after_ho(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE) +{ + DevAssert(UE->ho_context); + DevAssert(UE->ho_context->target); + nr_ho_target_cu_t *target_ctx = UE->ho_context->target; + + /* Pre-check: ensure F1 UE data matches target cell before mutating state. */ + if (!cu_exists_f1_ue_data(UE->rrc_ue_id)) { + LOG_E(NR_RRC, "UE %d handover: no F1 UE data, cannot add PCell\n", UE->rrc_ue_id); + return false; + } + f1_ue_data_t ue_data = cu_get_f1_ue_data(UE->rrc_ue_id); + if (ue_data.du_assoc_id != target_ctx->cell->assoc_id) { + LOG_E(NR_RRC, + "UE %d handover: target cell assoc_id %d does not match F1 du_assoc_id %d\n", + UE->rrc_ue_id, + target_ctx->cell->assoc_id, + ue_data.du_assoc_id); + return false; + } + + if (UE->ho_context->source) { + /* F1 HO (intra-CU): migrate serving cells from source DU to target cell */ + nr_ho_source_cu_t *source_ctx = UE->ho_context->source; + + // Remove SCells that belong to the source DU during handover (cell group changes) + // This also removes PCell if it was on the source DU + rrc_remove_ue_scells_from_du(UE, source_ctx->cell->assoc_id); + + // Update PCell in serving_cells + ue_serving_cell_t *added = rrc_add_ue_serving_cell(UE, target_ctx->cell, RRC_PCELL_INDEX); + if (added == NULL) { + LOG_E(NR_RRC, + "UE %d handover: failed to add PCell (cell %ld) to serving cells\n", + UE->rrc_ue_id, + target_ctx->cell->info.cell_id); + return false; + } + } else { + /* Inter-CU (N2/Xn target): this UE context has no serving cells yet */ + const ue_serving_cell_t *existing_pcell = ue_get_pcell_entry(UE); + AssertFatal(existing_pcell == NULL, "UE context should not have PCell yet (inter-CU target)\n"); + + ue_serving_cell_t *added = rrc_add_ue_serving_cell(UE, target_ctx->cell, RRC_PCELL_INDEX); + if (added == NULL) { + LOG_E(NR_RRC, + "UE %d handover: failed to add PCell (cell %ld) to serving cells\n", + UE->rrc_ue_id, + target_ctx->cell->info.cell_id); + return false; + } + } + + return true; +} + /** @brief This callback is used by the target gNB * to trigger the Handover Request Acknowledge towards the AMF */ static void nr_rrc_n2_ho_acknowledge(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE) @@ -407,13 +500,23 @@ static void nr_rrc_n2_ho_acknowledge(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE) AssertFatal(previous_data.secondary_ue == -1, "there was already a DU present\n"); nr_rrc_apply_target_context(UE); + /* Update cell association after handover */ + nr_ho_target_cu_t *target = UE->ho_context->target; + if (!nr_rrc_update_cell_assoc_after_ho(rrc, UE)) { + ngap_handover_failure_t fail = {.amf_ue_ngap_id = UE->amf_ue_ngap_id, + .cause.type = NGAP_CAUSE_RADIO_NETWORK, + .cause.value = NGAP_CAUSE_RADIO_NETWORK_HO_FAILURE_IN_TARGET_5GC_NGRAN_NODE_OR_TARGET_SYSTEM}; + target->ho_failure(rrc, UE->rrc_ue_id, &fail); + return; + } + byte_array_t hoCommand = rrc_gNB_encode_HandoverCommand(UE, rrc); if (hoCommand.len < 0) { LOG_E(NR_RRC, "ASN1 message encoding failed: failed to generate Handover Command Message\n"); ngap_handover_failure_t fail = {.amf_ue_ngap_id = UE->amf_ue_ngap_id, .cause.type = NGAP_CAUSE_RADIO_NETWORK, .cause.value = NGAP_CAUSE_RADIO_NETWORK_HO_FAILURE_IN_TARGET_5GC_NGRAN_NODE_OR_TARGET_SYSTEM}; - UE->ho_context->target->ho_failure(rrc, UE->rrc_ue_id, &fail); + target->ho_failure(rrc, UE->rrc_ue_id, &fail); return; } else { LOG_D(NR_RRC, "HO LOG: Handover Command for UE %u Encoded (%ld bytes)\n", UE->rrc_ue_id, hoCommand.len); @@ -463,8 +566,7 @@ void nr_rrc_trigger_n2_ho_target(gNB_RRC_INST *rrc, gNB_RRC_UE_t *ue) ho_success_t success = nr_rrc_n2_ho_complete; ho_failure_t failure = nr_rrc_n2_ho_failure; - const nr_rrc_du_container_t *target_du = get_du_for_ue(rrc, ue->rrc_ue_id); - nr_initiate_handover(rrc, ue, NULL, target_du, &ue->ho_context->target->ue_ho_prep_info, ack, success, NULL, failure); + nr_initiate_handover(rrc, ue, NULL, &ue->ho_context->target->ue_ho_prep_info, ack, success, NULL, failure); FREE_AND_ZERO_BYTE_ARRAY(ue->ho_context->target->ue_ho_prep_info); NR_UE_NR_Capability_t *ue_cap = get_ue_nr_capability(ue->rnti, ue->ue_cap_buffer.buf, ue->ue_cap_buffer.len); @@ -495,7 +597,13 @@ void nr_rrc_trigger_n2_ho(gNB_RRC_INST *rrc, } ue->ho_context = alloc_ho_ctx(HO_CTX_SOURCE); - ue->ho_context->source->du = get_du_for_ue(rrc, ue->rrc_ue_id); + nr_rrc_cell_container_t *source_cell = rrc_get_pcell_for_ue(rrc, ue); + if (source_cell == NULL) { + LOG_E(NR_RRC, "No source cell found for UE %d\n", ue->rrc_ue_id); + free_byte_array(hoPrepInfo); + return; + } + ue->ho_context->source->cell = source_cell; ue->ho_context->source->ho_status_transfer = rrc_gNB_send_NGAP_ul_ran_status_transfer; ue->ho_context->source->ho_cancel = nr_rrc_n2_ho_cancel; @@ -514,42 +622,51 @@ void nr_HO_N2_trigger_telnet(gNB_RRC_INST *rrc, uint32_t neighbour_pci, uint32_t return; } gNB_RRC_UE_t *UE = &ue_context_p->ue_context; - - struct nr_rrc_du_container_t *du = get_du_for_ue(rrc, rrc_ue_id); + nr_rrc_du_container_t *du = get_du_for_ue(rrc, UE->rrc_ue_id); if (du == NULL) { LOG_E(NR_RRC, "N2 HO trigger failed for UE %d: Unknown DU\n", rrc_ue_id); return; } - uint16_t scell_pci = du->setup_req->cell[0].info.nr_pci; + nr_rrc_cell_container_t *pcell = rrc_get_pcell_for_ue(rrc, UE); + if (pcell == NULL) { + LOG_E(NR_RRC, "N2 HO trigger failed for UE %d: Unknown cell\n", rrc_ue_id); + return; + } + uint16_t scell_pci = pcell->info.pci; // Simulate handover on the same cell (testing purposes) if (neighbour_pci == scell_pci) { LOG_I(NR_RRC, "UE %d: trigger handover on the same cell PCI=%d\n", rrc_ue_id, neighbour_pci); nr_neighbour_cell_t neighbourConfig = { .isIntraFrequencyNeighbour = true, - .gNB_ID = du->setup_req->gNB_DU_id, - .nrcell_id = du->setup_req->cell[0].info.nr_cellid, - .physicalCellId = du->setup_req->cell[0].info.nr_pci, - .plmn = du->setup_req->cell[0].info.plmn, - .subcarrierSpacing = du->setup_req->cell[0].info.tdd.tbw.scs, + .gNB_ID = du->gNB_DU_id, + .nrcell_id = pcell->info.cell_id, + .physicalCellId = pcell->info.pci, + .plmn = pcell->info.plmn, + .subcarrierSpacing = pcell->info.mode == NR_MODE_TDD ? pcell->info.tdd.dlul.scs : pcell->info.fdd.dl.scs, }; nr_rrc_trigger_n2_ho(rrc, UE, neighbour_pci, &neighbourConfig); return; } - const f1ap_served_cell_info_t *scell_du = get_cell_information_by_phycellId(scell_pci); - DevAssert(scell_du); LOG_I(NR_RRC, "UE %d: triggered N2 HO, source PCI=%d to target PCI=%d\n", rrc_ue_id, scell_pci, neighbour_pci); - const neighbour_cell_configuration_t *cell = get_neighbour_cell_config(rrc, scell_du->nr_cellid); - const nr_neighbour_cell_t *neighbour = get_neighbour_cell_by_pci(cell, neighbour_pci); + const neighbour_cell_configuration_t *cell = get_neighbour_cell_config(rrc, pcell->info.cell_id); + if (cell == NULL) { + LOG_E(NR_RRC, + "N2 HO trigger failed for UE %d: could not find neighbour cell with Cell ID=%ld\n", + rrc_ue_id, + pcell->info.cell_id); + return; + } + const nr_neighbour_cell_t *neighbour = get_neighbour_cell_by_pci(cell, neighbour_pci); if (neighbour == NULL) { LOG_E(NR_RRC, "N2 HO trigger failed for UE %d: could not find neighbour cell with PCI=%d\n", rrc_ue_id, neighbour_pci); return; } - nr_rrc_trigger_n2_ho(rrc, UE, scell_du->nr_pci, neighbour); + nr_rrc_trigger_n2_ho(rrc, UE, pcell->info.pci, neighbour); } // This function detects if there are at least two different ssbFrequency values, and if so, returns meas_timing_config; diff --git a/openair2/RRC/NR/rrc_gNB_mobility.h b/openair2/RRC/NR/rrc_gNB_mobility.h index 456730060e..d4a51efeb9 100644 --- a/openair2/RRC/NR/rrc_gNB_mobility.h +++ b/openair2/RRC/NR/rrc_gNB_mobility.h @@ -28,7 +28,7 @@ /* forward declarations */ typedef struct gNB_RRC_INST_s gNB_RRC_INST; typedef struct gNB_RRC_UE_s gNB_RRC_UE_t; -typedef struct nr_rrc_du_container_t nr_rrc_du_container_t; +typedef struct nr_rrc_cell_container_t nr_rrc_cell_container_t; typedef struct NR_CellGroupConfig NR_CellGroupConfig_t; @@ -40,8 +40,8 @@ typedef int (*ho_status_transfer_t)(gNB_RRC_INST *rrc, const e1_pdcp_status_info_t *pdcp_status); typedef struct nr_ho_source_cu { - /// pointer to the (source) DU structure - const nr_rrc_du_container_t *du; + /// pointer to the (source) cell container + const nr_rrc_cell_container_t *cell; /// (source) DU UE ID; in F1, the DU UE ID will change to the new (target) DU /// UE ID during handover, and the CU needs to keep track of this in case of /// reestablishment @@ -67,8 +67,8 @@ typedef void (*ho_failure_t)(gNB_RRC_INST *rrc, uint32_t gnb_ue_id, ngap_handove typedef void (*ho_trigger_t)(gNB_RRC_INST *rrc, gNB_RRC_UE_t *ue); typedef struct nr_ho_target_cu { - /// pointer to the (target) DU structure - const nr_rrc_du_container_t *du; + /// pointer to the (target) cell container + const nr_rrc_cell_container_t *cell; /// (target) DU UE ID; the (source) DU UE ID will change to the new (target) /// DU UE ID after sending a reconfiguration, which is later than when /// receiving this ID @@ -95,7 +95,10 @@ typedef struct nr_handover_context_s { typedef enum { HO_CTX_BOTH, HO_CTX_SOURCE, HO_CTX_TARGET } ho_ctx_type_t; nr_handover_context_t *alloc_ho_ctx(ho_ctx_type_t type); -void nr_rrc_trigger_f1_ho(gNB_RRC_INST *rrc, gNB_RRC_UE_t *ue, nr_rrc_du_container_t *source_du, nr_rrc_du_container_t *target_du); +void nr_rrc_trigger_f1_ho(gNB_RRC_INST *rrc, + gNB_RRC_UE_t *ue, + const nr_rrc_cell_container_t *source_cell, + const nr_rrc_cell_container_t *target_cell); void nr_rrc_finalize_ho(gNB_RRC_UE_t *ue); void nr_rrc_n2_ho_failure(gNB_RRC_INST *rrc, uint32_t gnb_ue_id, ngap_handover_failure_t *msg); @@ -112,4 +115,6 @@ byte_array_t *get_meas_timing_config(const NR_MeasurementTimingConfiguration_t * void nr_rrc_apply_target_context(gNB_RRC_UE_t *UE); +bool nr_rrc_update_cell_assoc_after_ho(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE); + #endif /* RRC_GNB_MOBILITY_H_ */ diff --git a/openair2/RRC/NR/rrc_gNB_nsa.c b/openair2/RRC/NR/rrc_gNB_nsa.c index 88f06338d7..18513d14c5 100644 --- a/openair2/RRC/NR/rrc_gNB_nsa.c +++ b/openair2/RRC/NR/rrc_gNB_nsa.c @@ -62,6 +62,7 @@ #include "openair3/SECU/key_nas_deriver.h" #include "openair2/SDAP/nr_sdap/nr_sdap_entity.h" #include "rrc_gNB_du.h" +#include "rrc_cell_management.h" #include "rlc.h" #include "s1ap_messages_types.h" #include "tree.h" @@ -368,20 +369,37 @@ void rrc_add_nsa_user(gNB_RRC_INST *rrc, x2ap_ENDC_sgnb_addition_req_t *m, sctp_ *ue_agg_mbr_ul = 1000000000; byte_array_t *cg_configinfo = malloc_or_fail(sizeof(*cg_configinfo)); *cg_configinfo = cgci; + + f1_ue_data_t ue_data = cu_get_f1_ue_data(UE->rrc_ue_id); + RETURN_IF_INVALID_ASSOC_ID(ue_data.du_assoc_id); + nr_rrc_du_container_t *du = get_du_by_assoc_id(rrc, ue_data.du_assoc_id); + if (!du) { + LOG_E(NR_RRC, "UE %d: no valid cell for UE context setup (DU assoc_id %d not found)\n", UE->rrc_ue_id, ue_data.du_assoc_id); + return; + } + // Get the first cell from the DU (NSA assumption: 1 cell per DU) + if (seq_arr_size(&du->cells) == 0) { + LOG_E(NR_RRC, "UE %d: no cells available in DU (assoc_id %d)\n", UE->rrc_ue_id, ue_data.du_assoc_id); + return; + } + nr_rrc_cell_container_t *ue_cell = *(nr_rrc_cell_container_t **)seq_arr_at(&du->cells, 0); + // Add the cell as PCell + if (rrc_add_ue_serving_cell(UE, ue_cell, RRC_PCELL_INDEX) == NULL) { + LOG_E(NR_RRC, "UE %d: failed to add PCell (cell %ld) for UE context setup\n", UE->rrc_ue_id, ue_cell->info.cell_id); + return; + } f1ap_ue_context_setup_req_t req = { .gNB_CU_ue_id = UE->rrc_ue_id, .plmn.mcc = rrc->configuration.plmn[0].mcc, .plmn.mnc = rrc->configuration.plmn[0].mnc, .plmn.mnc_digit_length = rrc->configuration.plmn[0].mnc_digit_length, - .nr_cellid = rrc->nr_cellid, - .servCellIndex = 0, + .nr_cellid = ue_cell->info.cell_id, + .servCellIndex = RRC_PCELL_INDEX, .drbs_len = 1, .drbs = drb, .cu_to_du_rrc_info.cg_configinfo = cg_configinfo, .gnb_du_ue_agg_mbr_ul = ue_agg_mbr_ul, }; - f1_ue_data_t ue_data = cu_get_f1_ue_data(UE->rrc_ue_id); - RETURN_IF_INVALID_ASSOC_ID(ue_data.du_assoc_id); rrc->mac_rrc.ue_context_setup_request(ue_data.du_assoc_id, &req); free_ue_context_setup_req(&req); } From bd2fc0d237ff12373a7016d6137bebdf6ac97003 Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Thu, 5 Feb 2026 16:23:51 +0100 Subject: [PATCH 14/17] RRC multi-cell: enable multiple cell in F1 Setup Remove the single-cell limit in F1 Setup Request processing and refactor to loop-based cell handling. Move cell validation (PLMN check, neighbor configuration validation, system info extraction), cell creation, SIB encoding, and neighbor labeling inside a loop over all available cells. Update F1 Setup Response to include all cells instead of hardcoding to 1. Add warning log when more than one cell is received. Add proper cleanup with free_f1ap_setup_response() on error paths. Impact: - F1 Setup now accepts and processes multiple cells from a single DU, enabling multi-cell deployments - All cells are validated, created, and included in the setup response --- openair2/RRC/NR/rrc_gNB_du.c | 209 ++++++++++++++++++----------------- 1 file changed, 107 insertions(+), 102 deletions(-) diff --git a/openair2/RRC/NR/rrc_gNB_du.c b/openair2/RRC/NR/rrc_gNB_du.c index 5b1be47d5e..f50c7a2c6e 100644 --- a/openair2/RRC/NR/rrc_gNB_du.c +++ b/openair2/RRC/NR/rrc_gNB_du.c @@ -324,30 +324,9 @@ void rrc_gNB_process_f1_setup_req(f1ap_setup_req_t *req, sctp_assoc_t assoc_id) f1ap_setup_failure_t fail = {.transaction_id = F1AP_get_next_transaction_identifier(0, 0)}; // check: - // - it is one cell // - PLMN and Cell ID matches // - no previous DU with the same ID // else reject - if (req->num_cells_available != 1) { - LOG_E(NR_RRC, "can only handle on DU cell, but gNB_DU %ld has %d\n", req->gNB_DU_id, req->num_cells_available); - fail.cause = F1AP_CauseRadioNetwork_gNB_CU_Cell_Capacity_Exceeded; - rrc->mac_rrc.f1_setup_failure(assoc_id, &fail); - return; - } - f1ap_served_cell_info_t *cell_info = &req->cell[0].info; - if (!rrc_gNB_plmn_matches(rrc, cell_info)) { - LOG_E(NR_RRC, - "PLMN mismatch: CU %03d.%0*d, DU %03d%0*d\n", - rrc->configuration.plmn[0].mcc, - rrc->configuration.plmn[0].mnc_digit_length, - rrc->configuration.plmn[0].mnc, - cell_info->plmn.mcc, - cell_info->plmn.mnc_digit_length, - cell_info->plmn.mnc); - fail.cause = F1AP_CauseRadioNetwork_plmn_not_served_by_the_gNB_CU; - rrc->mac_rrc.f1_setup_failure(assoc_id, &fail); - return; - } nr_rrc_du_container_t *it = NULL; RB_FOREACH(it, rrc_du_tree, &rrc->dus) { if (it->gNB_DU_id == req->gNB_DU_id) { @@ -361,50 +340,7 @@ void rrc_gNB_process_f1_setup_req(f1ap_setup_req_t *req, sctp_assoc_t assoc_id) return; } } - - if (rrc->neighbour_cell_configuration && !valid_du_in_neighbour_configs(rrc->neighbour_cell_configuration, cell_info)) { - LOG_E(NR_RRC, "problem with DU %ld in neighbor configuration, rejecting DU\n", req->gNB_DU_id); - f1ap_setup_failure_t fail = {.cause = F1AP_CauseMisc_unspecified}; - rrc->mac_rrc.f1_setup_failure(assoc_id, &fail); - return; - } - - const f1ap_gnb_du_system_info_t *sys_info = req->cell[0].sys_info; - NR_MIB_t *mib = NULL; - NR_SIB1_t *sib1 = NULL; - - if (sys_info != NULL && sys_info->mib != NULL && !(sys_info->sib1 == NULL && IS_SA_MODE(get_softmodem_params()))) { - if (!extract_sys_info(sys_info, &mib, &sib1)) { - LOG_W(NR_RRC, "rejecting DU ID %ld\n", req->gNB_DU_id); - fail.cause = F1AP_CauseProtocol_semantic_error; - rrc->mac_rrc.f1_setup_failure(assoc_id, &fail); - return; - } - } - LOG_I(NR_RRC, "Accepting DU %ld (%s), sending F1 Setup Response\n", req->gNB_DU_id, req->gNB_DU_name); - LOG_I(NR_RRC, "DU uses RRC version %u.%u.%u\n", req->rrc_ver[0], req->rrc_ver[1], req->rrc_ver[2]); - - /* Create cell and add to global tree */ - nr_rrc_cell_container_t *new = calloc_or_fail(1, sizeof(*new)); - new->assoc_id = assoc_id; - cp_f1_served_cell_info_to_cell(new, cell_info); - new->mib = mib; - new->sib1 = sib1; - // Add cell to DU's cell array - nr_rrc_cell_container_t *collision = rrc_add_cell(rrc, new); - if (collision != NULL) { - nr_rrc_du_container_t *existing_du = get_du_by_assoc_id(rrc, collision->assoc_id); - const char *du_name = existing_du ? existing_du->gNB_DU_name : "unknown"; - LOG_E(NR_RRC, - "Cell ID %lu already exists in DU %s (assoc_id %d), rejecting requesting gNB-DU\n", - new->info.cell_id, - du_name, - collision->assoc_id); - rrc_free_cell_container(new); - fail.cause = F1AP_CauseRadioNetwork_cell_not_available; - rrc->mac_rrc.f1_setup_failure(assoc_id, &fail); - return; - } + LOG_I(NR_RRC, "Accepting DU %ld (%s) (RRC version %u.%u.%u)\n", req->gNB_DU_id, req->gNB_DU_name, req->rrc_ver[0], req->rrc_ver[1], req->rrc_ver[2]); // DU is accepted, add it to tree and add cell to DU's cell array nr_rrc_du_container_t *du = calloc_or_fail(1, sizeof(*du)); @@ -418,48 +354,117 @@ void rrc_gNB_process_f1_setup_req(f1ap_setup_req_t *req, sctp_assoc_t assoc_id) nr_rrc_du_container_t *du_collision = rrc_add_du(rrc, du); AssertFatal(du_collision == NULL, "rrc_add_du should succeed for new DU (assoc_id %d)", assoc_id); - nr_rrc_cell_container_t *added = rrc_add_cell_to_du(&du->cells, new); - AssertFatal(added != NULL, "Failed to add cell %ld to DU %ld\n", new->info.cell_id, du->gNB_DU_id); - LOG_I(NR_RRC, "DU %ld: Added cell %ld\n", du->gNB_DU_id, new->info.cell_id); - - served_cells_to_activate_t cell = { - .plmn = cell_info->plmn, - .nr_cellid = cell_info->nr_cellid, - .nrpci = cell_info->nr_pci, - .num_SI = 0, - }; - - // Encode CU SIBs and configure setup response with sysinfo - seq_arr_t *sibs = rrc->SIBs; - if (sibs) { - for (int i = 0; i < sibs->size; i++) { - nr_SIBs_t *sib = (nr_SIBs_t *)seq_arr_at(sibs, i); - switch (sib->SIB_type) { - case 2: { - NR_SSB_MTC_t *ssbmtc = get_ssb_mtc(new->mtc); - sib->SIB_size = do_SIB2_NR(&sib->SIB_buffer, ssbmtc); - cell.SI_msg[cell.num_SI].SI_container = sib->SIB_buffer; - cell.SI_msg[cell.num_SI].SI_container_length = sib->SIB_size; - cell.SI_msg[cell.num_SI].SI_type = sib->SIB_type; - cell.num_SI++; - } break; - default: - AssertFatal(false, "SIB%d not handled yet\n", sib->SIB_type); - } - } - } - - if (new->mib != NULL &&new->sib1 != NULL) - label_intra_frequency_neighbours(rrc, new); - + // Build F1 Setup Response f1ap_setup_resp_t resp = {.transaction_id = req->transaction_id, - .num_cells_to_activate = 1, - .cells_to_activate = calloc_or_fail(1, sizeof(*resp.cells_to_activate))}; - resp.cells_to_activate[0] = cell; + .num_cells_to_activate = req->num_cells_available, + .cells_to_activate = calloc_or_fail(req->num_cells_available, sizeof(*resp.cells_to_activate))}; int num = read_version(TO_STRING(NR_RRC_VERSION), &resp.rrc_ver[0], &resp.rrc_ver[1], &resp.rrc_ver[2]); AssertFatal(num == 3, "could not read RRC version string %s\n", TO_STRING(NR_RRC_VERSION)); if (rrc->node_name != NULL) resp.gNB_CU_name = strdup(rrc->node_name); + + if (req->num_cells_available > 1) { + LOG_W(NR_RRC, "Received F1 Setup Request with %u cells, only one cell is supported\n", req->num_cells_available); + } + for (int i = 0; i < req->num_cells_available; i++) { + f1ap_served_cell_info_t *cell_info = &req->cell[i].info; + if (!rrc_gNB_plmn_matches(rrc, cell_info)) { + LOG_E(NR_RRC, + "PLMN mismatch: CU %03d.%0*d, DU %03d%0*d\n", + rrc->configuration.plmn[0].mcc, + rrc->configuration.plmn[0].mnc_digit_length, + rrc->configuration.plmn[0].mnc, + cell_info->plmn.mcc, + cell_info->plmn.mnc_digit_length, + cell_info->plmn.mnc); + fail.cause = F1AP_CauseRadioNetwork_plmn_not_served_by_the_gNB_CU; + rrc->mac_rrc.f1_setup_failure(assoc_id, &fail); + free_f1ap_setup_response(&resp); + return; + } + + if (rrc->neighbour_cell_configuration && !valid_du_in_neighbour_configs(rrc->neighbour_cell_configuration, cell_info)) { + LOG_E(NR_RRC, "problem with DU %ld in neighbor configuration, rejecting DU\n", req->gNB_DU_id); + f1ap_setup_failure_t fail = {.cause = F1AP_CauseMisc_unspecified}; + rrc->mac_rrc.f1_setup_failure(assoc_id, &fail); + free_f1ap_setup_response(&resp); + return; + } + + const f1ap_gnb_du_system_info_t *sys_info = req->cell[i].sys_info; + NR_MIB_t *mib = NULL; + NR_SIB1_t *sib1 = NULL; + if (sys_info != NULL && sys_info->mib != NULL && !(sys_info->sib1 == NULL && IS_SA_MODE(get_softmodem_params()))) { + if (!extract_sys_info(sys_info, &mib, &sib1)) { + LOG_W(NR_RRC, "rejecting DU ID %ld\n", req->gNB_DU_id); + fail.cause = F1AP_CauseProtocol_semantic_error; + rrc->mac_rrc.f1_setup_failure(assoc_id, &fail); + free_f1ap_setup_response(&resp); + return; + } + } + + /* Create cell and add to global tree */ + nr_rrc_cell_container_t *new = calloc_or_fail(1, sizeof(*new)); + new->assoc_id = assoc_id; + cp_f1_served_cell_info_to_cell(new, cell_info); + new->mib = mib; + new->sib1 = sib1; + // Add cell to DU's cell array + nr_rrc_cell_container_t *collision = rrc_add_cell(rrc, new); + if (collision != NULL) { + nr_rrc_du_container_t *existing_du = get_du_by_assoc_id(rrc, collision->assoc_id); + const char *du_name = existing_du ? existing_du->gNB_DU_name : "unknown"; + LOG_E(NR_RRC, + "Cell ID %lu already exists in DU %s (assoc_id %d), rejecting requesting gNB-DU\n", + new->info.cell_id, + du_name, + collision->assoc_id); + rrc_free_cell_container(new); + fail.cause = F1AP_CauseRadioNetwork_cell_not_available; + rrc->mac_rrc.f1_setup_failure(assoc_id, &fail); + free_f1ap_setup_response(&resp); + return; + } + + nr_rrc_cell_container_t *added = rrc_add_cell_to_du(&du->cells, new); + AssertFatal(added != NULL, "Failed to add cell %ld to DU %ld\n", new->info.cell_id, du->gNB_DU_id); + LOG_I(NR_RRC, "DU %ld: Added cell %ld\n", du->gNB_DU_id, new->info.cell_id); + + served_cells_to_activate_t cell = { + .plmn = cell_info->plmn, + .nr_cellid = cell_info->nr_cellid, + .nrpci = cell_info->nr_pci, + .num_SI = 0, + }; + + // Encode CU SIBs and configure setup response with sysinfo + seq_arr_t *sibs = rrc->SIBs; + if (sibs) { + for (int i = 0; i < sibs->size; i++) { + nr_SIBs_t *sib = (nr_SIBs_t *)seq_arr_at(sibs, i); + switch (sib->SIB_type) { + case 2: { + NR_SSB_MTC_t *ssbmtc = get_ssb_mtc(new->mtc); + sib->SIB_size = do_SIB2_NR(&sib->SIB_buffer, ssbmtc); + cell.SI_msg[cell.num_SI].SI_container = sib->SIB_buffer; + cell.SI_msg[cell.num_SI].SI_container_length = sib->SIB_size; + cell.SI_msg[cell.num_SI].SI_type = sib->SIB_type; + cell.num_SI++; + } break; + default: + AssertFatal(false, "SIB%d not handled yet\n", sib->SIB_type); + } + } + } + + if (new->mib != NULL &&new->sib1 != NULL) + label_intra_frequency_neighbours(rrc, new); + + resp.cells_to_activate[i] = cell; + } + + LOG_I(NR_RRC, "DU %ld (%s): sending F1 Setup Response\n", req->gNB_DU_id, req->gNB_DU_name); rrc->mac_rrc.f1_setup_response(assoc_id, &resp); free_f1ap_setup_response(&resp); From 9bfe62b85b26788cee21960e1cfd9a4c09c5e910 Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Tue, 6 Jan 2026 11:08:28 +0100 Subject: [PATCH 15/17] RRC multi-cell (finalise): make internal helper functions static After introduction of new cell management design, these helper functions are now only used internally within their respective modules and do not need to be part of the public API. They are made static and their declarations are removed from public headers. Changes: - f1ap_interface_management.c: Make free_f1ap_cell() and copy_f1ap_served_cell_info() static - f1ap_interface_management.h: Remove declarations for the above functions - rrc_gNB_du.c: Make get_dl_band() and get_ssb_scs() static - rrc_gNB_du.h: Remove declarations for get_dl_band() and get_ssb_scs() --- openair2/F1AP/lib/f1ap_interface_management.c | 4 ++-- openair2/F1AP/lib/f1ap_interface_management.h | 2 -- openair2/RRC/NR/rrc_gNB_du.c | 4 ++-- openair2/RRC/NR/rrc_gNB_du.h | 2 -- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/openair2/F1AP/lib/f1ap_interface_management.c b/openair2/F1AP/lib/f1ap_interface_management.c index 38de5e4dcc..55b4972d4b 100644 --- a/openair2/F1AP/lib/f1ap_interface_management.c +++ b/openair2/F1AP/lib/f1ap_interface_management.c @@ -423,7 +423,7 @@ static F1AP_ProtocolExtensionContainer_11023P34_t *write_slice_info(int num_ssi, /** * @brief F1AP Setup Request memory management */ -void free_f1ap_cell(const f1ap_served_cell_info_t *info, const f1ap_gnb_du_system_info_t *sys_info) +static void free_f1ap_cell(const f1ap_served_cell_info_t *info, const f1ap_gnb_du_system_info_t *sys_info) { if (sys_info) { free(sys_info->mib); @@ -886,7 +886,7 @@ bool decode_f1ap_setup_request(const F1AP_F1AP_PDU_t *pdu, f1ap_setup_req_t *out return true; } -f1ap_served_cell_info_t copy_f1ap_served_cell_info(const f1ap_served_cell_info_t *src) +static f1ap_served_cell_info_t copy_f1ap_served_cell_info(const f1ap_served_cell_info_t *src) { f1ap_served_cell_info_t dst = { .plmn = src->plmn, diff --git a/openair2/F1AP/lib/f1ap_interface_management.h b/openair2/F1AP/lib/f1ap_interface_management.h index 56f6579f2f..942a9932ad 100644 --- a/openair2/F1AP/lib/f1ap_interface_management.h +++ b/openair2/F1AP/lib/f1ap_interface_management.h @@ -46,8 +46,6 @@ bool decode_f1ap_setup_request(const struct F1AP_F1AP_PDU *pdu, f1ap_setup_req_t f1ap_setup_req_t cp_f1ap_setup_request(const f1ap_setup_req_t *msg); bool eq_f1ap_setup_request(const f1ap_setup_req_t *a, const f1ap_setup_req_t *b); void free_f1ap_setup_request(const f1ap_setup_req_t *msg); -f1ap_served_cell_info_t copy_f1ap_served_cell_info(const f1ap_served_cell_info_t *src); -void free_f1ap_cell(const f1ap_served_cell_info_t *info, const f1ap_gnb_du_system_info_t *sys_info); /* F1 Setup Response */ struct F1AP_F1AP_PDU *encode_f1ap_setup_response(const f1ap_setup_resp_t *msg); diff --git a/openair2/RRC/NR/rrc_gNB_du.c b/openair2/RRC/NR/rrc_gNB_du.c index f50c7a2c6e..f2e66257f6 100644 --- a/openair2/RRC/NR/rrc_gNB_du.c +++ b/openair2/RRC/NR/rrc_gNB_du.c @@ -53,12 +53,12 @@ #include "utils.h" #include "xer_encoder.h" -int get_dl_band(const struct f1ap_served_cell_info_t *cell_info) +static int get_dl_band(const struct f1ap_served_cell_info_t *cell_info) { return cell_info->mode == F1AP_MODE_TDD ? cell_info->tdd.freqinfo.band : cell_info->fdd.dl_freqinfo.band; } -int get_ssb_scs(const struct f1ap_served_cell_info_t *cell_info) +static int get_ssb_scs(const struct f1ap_served_cell_info_t *cell_info) { return cell_info->mode == F1AP_MODE_TDD ? cell_info->tdd.tbw.scs : cell_info->fdd.dl_tbw.scs; } diff --git a/openair2/RRC/NR/rrc_gNB_du.h b/openair2/RRC/NR/rrc_gNB_du.h index ce033e91e8..8942af9af5 100644 --- a/openair2/RRC/NR/rrc_gNB_du.h +++ b/openair2/RRC/NR/rrc_gNB_du.h @@ -42,8 +42,6 @@ struct nr_rrc_du_container_t *get_du_for_ue(struct gNB_RRC_INST_s *rrc, uint32_t void dump_du_info(const struct gNB_RRC_INST_s *rrc, FILE *f); -int get_dl_band(const struct f1ap_served_cell_info_t *cell_info); -int get_ssb_scs(const struct f1ap_served_cell_info_t *cell_info); int get_ssb_arfcn(const struct nr_rrc_cell_container_t *cell); struct nr_rrc_du_container_t *find_target_du(struct gNB_RRC_INST_s *rrc, sctp_assoc_t source_assoc_id); From e75399707c6a9e8d36310111afd52e224115e97d Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Tue, 6 Jan 2026 11:11:30 +0100 Subject: [PATCH 16/17] RRC multi-cell (finalise): add informative comments and logging This commit adds documentation comments and logging to improve code readability and debugging capabilities for the multi-cell architecture. Changes: - gnb_config.c: Add logging when configuring DU with cell ID and PCI - rrc_gNB_du.c: Add documentation comment for ssb_arfcn_mtc() - rrc_gNB_du.c: Add documentation comment for dump_du_info() --- openair2/GNB_APP/gnb_config.c | 1 + openair2/RRC/NR/rrc_gNB_du.c | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/openair2/GNB_APP/gnb_config.c b/openair2/GNB_APP/gnb_config.c index 338f9ad8a5..b84f927f82 100644 --- a/openair2/GNB_APP/gnb_config.c +++ b/openair2/GNB_APP/gnb_config.c @@ -1684,6 +1684,7 @@ void RCconfig_nr_macrlc(configmodule_interface_t *cfg) seq_arr_t *du_SIBs = RC.nrmac[0]->common_channels[0].du_SIBs; f1ap_setup_req_t *req = RC_read_F1Setup(gnb_du_id, name, &info, scc, mib, sib1, du_SIBs); AssertFatal(req != NULL, "could not read F1 Setup information\n"); + LOG_I(GNB_APP, "Configured DU: cell ID %lu, PCI %d\n", info.nr_cellid, info.nr_pci); RC.nrmac[0]->f1_config.setup_req = req; RC.nrmac[0]->f1_config.gnb_id = gnb_id; diff --git a/openair2/RRC/NR/rrc_gNB_du.c b/openair2/RRC/NR/rrc_gNB_du.c index f2e66257f6..10d2e4131c 100644 --- a/openair2/RRC/NR/rrc_gNB_du.c +++ b/openair2/RRC/NR/rrc_gNB_du.c @@ -72,6 +72,9 @@ static NR_SSB_MTC_t *get_ssb_mtc(const NR_MeasurementTimingConfiguration_t *mtc) return ssb_mtc; } +/** @brief Return the frequency of the SS block of the cell for which this message is included, + * or of other SS blocks within the same carrier. MeasurementTimingConfiguration message is + * used to convey assistance information for measurement timing between nodes (e.g F1 setup) */ static int ssb_arfcn_mtc(const NR_MeasurementTimingConfiguration_t *mtc) { /* format has been verified when accepting MeasurementTimingConfiguration */ @@ -728,6 +731,11 @@ nr_rrc_du_container_t *get_du_for_ue(gNB_RRC_INST *rrc, uint32_t ue_id) return get_du_by_assoc_id(rrc, ue_data.du_assoc_id); } +/** @brief Dump DU and cell information to file + * @param[in] rrc RRC instance + * @param[in] f File handle to write to + * @note Complexity: O(d + N_CELL) = O(N_CELL) where d = number of DUs, N_CELL = total number of cells across all DUs + * @note Called periodically (every 1 second) for stats logging */ void dump_du_info(const gNB_RRC_INST *rrc, FILE *f) { fprintf(f, "%ld connected DUs \n", rrc->num_dus); From 337cc4bbe734d66618631a0d4c66b2f23e4fc0b7 Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Tue, 11 Nov 2025 13:26:57 +0100 Subject: [PATCH 17/17] Doc: update RRC documentation with new multi-cell architecture Expand RRC developer and usage documentation to match the current multi-cell design with separate DU and cell management. - rrc-dev.md: Document DU and cell data structures and key functions (get_du_by_assoc_id, get_cell_by_cell_id, rrc_add_cell_to_du, etc.); add DU and cell lifecycle (F1 Setup, config update, disconnection) and UE cell association (serving cells, PCell/SCells) with sequence diagrams; add handover and cell association update flow; update UE context description. - rrc-usage.md: Update DU/cell list output format and description (e.g. integrated DU-CU) and multi-cell per DU wording. --- doc/RRC/rrc-dev.md | 252 ++++++++++++++++++++++++++++++++++++++++++- doc/RRC/rrc-usage.md | 18 ++-- 2 files changed, 258 insertions(+), 12 deletions(-) diff --git a/doc/RRC/rrc-dev.md b/doc/RRC/rrc-dev.md index f5c4956c9d..2299394e0c 100644 --- a/doc/RRC/rrc-dev.md +++ b/doc/RRC/rrc-dev.md @@ -390,13 +390,255 @@ sequenceDiagram ## Structures -### Cells +### DUs and Cells -OAI 5G RRC does not actually handle multiple cells as of now, but multiple DUs, -each being limited to one cell. +OAI 5G RRC is enabling support for multiple DUs, with each DU potentially +serving multiple cells (though currently each DU is limited to one cell in practice). +The architecture separates DU management from cell management to enable future +multi-cell per DU capability. -Cell-related data is stored in `nr_rrc_du_container_t`, and kept in a tree -indexed by the SCTP association ID. +#### DU Management + +DU-related data is stored in `nr_rrc_du_container_t`, and kept in a red-black +tree indexed by the unique SCTP association ID (`assoc_id`). Each DU container stores: +- DU identity and name +- SCTP association ID for F1 interface communication +- RRC version information +- Sequential array of cell pointers (`cells`) - stores pointers to cells belonging to this DU (seq_arr_t) + +Key Functions: +- `get_du_by_assoc_id()` - Lookup DU by SCTP association ID (O(log d) tree lookup) +- `get_du_for_ue()` - Get DU associated with a specific UE +- `find_target_du()` - Find a target DU for handover operations + +#### Cell Management + +Cell-related data is stored in `nr_rrc_cell_container_t`. Cells are stored in two data structures: + +1. Global cell tree (`rrc->cells`): Red-black tree indexed by `cell_id`, containing all cells from all DUs. Used for efficient O(log N_CELL) lookups across all DUs. PCI reuse across the network is allowed and this is reflected in the tree. +2. DU cell array (`du->cells`): Sequential array (seq_arr_t) of cell pointers, storing only cells belonging to that specific DU. Used for DU-specific operations. PCI must be unique within a DU. + +Each cell container stores: +- Cell identity (NR Cell ID) and PCI (Physical Cell ID) +- Link to serving DU via the unique `assoc_id` +- Cell-specific information (PLMN, TAC, frequency, mode TDD/FDD) +- MIB, SIB1, and MeasurementTimingConfiguration messages + +Key Functions: +- `get_cell_by_cell_id()` - Lookup cell by NR Cell ID using global cell tree +- `rrc_get_cell_for_du()` - Lookup cell by cell_id within a specific DU's cell array +- `rrc_get_cell_by_pci_for_du()` - Lookup cell by PCI within DU's cells array +- `rrc_add_cell_to_du()` - Add cell to DU's sequential array +- `rrc_free_cell_container()` - Free cell container and associated ASN.1 structures + +Architecture Notes: +- The `assoc_id` field (in cell and DUs containers) links cells to their serving DUs +- When a DU connects via F1 Setup, cells are added to both the global tree and the DU's array +- When a DU disconnects, cells are removed from both structures +- Global tree enables efficient cross-DU cell lookups (O(log N_CELL)) +- DU array enables efficient per-DU cell iteration (O(k) where k=cells per DU) +- Each DU maintains a sequential array of cell pointers + +##### DU and Cell Lifecycle + +The following diagram shows the lifecycle of DUs and their associated cells, including the main F1AP messages and internal operations: + +```mermaid +sequenceDiagram + participant CellTree as Cell Tree + participant DUTree as DU Tree + participant CU as CU-CP + participant DU as gNB-DU + participant UE as UE + + Note over DU,CellTree: DU Connection & Cell Registration + DU->>CU: F1AP F1 Setup Request(DU ID, Cell Info, MIB/SIB1) + Note over CU: Validate: PLMN match with CU configuration + alt PLMN mismatch + CU->>DU: F1AP F1 Setup Failure (PLMN not served) + end + Note over CU: Validate: DU ID uniqueness (RB_FOREACH gNB_DU_id in DU tree) + alt DU ID already exists + CU->>DU: F1AP F1 Setup Failure (Unspecified) + end + Note over CU: Validate: Neighbour cell configuration (if configured) + alt Neighbour config invalid + CU->>DU: F1AP F1 Setup Failure (Unspecified) + end + Note over CU: Extract MIB/SIB1 from system info (if present) + alt System info extraction fails + CU->>DU: F1AP F1 Setup Failure (Semantic error) + end + alt All Validations Success + CU->>CU: Create cell container (nr_rrc_cell_container_t), set assoc_id, copy cell info, set MIB/SIB1 + CU->>CellTree: rrc_add_cell(rrc, new) - RB_INSERT into global tree, increment rrc->num_cells + alt Duplicate cell_id (collision) + CU->>DU: F1AP F1 Setup Failure (Cell not available) + else Cell added to tree + CU->>CU: Create DU container (nr_rrc_du_container_t) + CU->>CU: seq_arr_init(&du->cells) - Initialize DU's cell array + CU->>DUTree: rrc_add_du(rrc, du) - RB_INSERT(du), increment rrc->num_dus + CU->>CU: rrc_add_cell_to_du(&du->cells, new) - Add cell to DU's array + Note over CU,UE: Cell available for UE association + CU->>CU: Encode CU SIBs (if configured) + CU->>DU: F1AP F1 Setup Response(Cells to Activate, CU SIBs) + Note over CU,CellTree: DU and cell now active + end + end + + UE->>CU: RRC Setup Request + Note over CU: rrc_handle_RRCSetupRequest() + CU->>CU: get_cell_by_cell_id(&rrc->cells) - Use global tree + alt Cell not found + CU->>UE: RRC Reject + end + Note over CU: UE Cell Association + CU->>CU: rrc_add_ue_serving_cell(UE, cell, RRC_PCELL_INDEX) + + Note over DU,CellTree: Optional: Cell Configuration Update + opt DU sends configuration update + DU->>CU: F1AP DU Configuration Update(Add/Modify/Delete cells) + Note over CU: get_du_by_assoc_id(assoc_id) + loop For each cell to add + CU->>CU: get_cell_by_cell_id(&rrc->cells) - Check cell_id uniqueness globally + alt Duplicate cell_id found + Note over CU: Reject and return + end + CU->>CU: rrc_get_cell_by_pci_for_du(&du->cells) - Check PCI unique within DU + alt Duplicate PCI in DU + Note over CU: Reject and return + end + end + loop Cell Modification + CU->>CU: get_cell_by_cell_id(cells, old_nr_cellid) - Find cell by old cell_id + CU->>CU: update_cell_info(rrc, old_nci, new_ci) - Update in place + Note over CU: If cell_id changes: RB_REMOVE then re-insert after update + Note over CU: Free old MTC if new measurement timing config provided + Note over CU: If sys_info present: extract MIB/SIB1 and set on cell + end + CU->>DU: F1AP DU Configuration Update Acknowledge + end + + Note over DU,CellTree: DU Disconnection & Cell Cleanup + DU-->>CU: F1AP Lost Connection(SCTP connection lost) + Note over CU: rrc_CU_process_f1_lost_connection() + CU->>DUTree: RB_FIND(du) - Find DU by assoc_id using temporary struct + alt DU not found + Note over CU: Log warning and return + end + Note over CU: rrc_cleanup_du() then invalidate_du_connections() + CU->>CU: Iterate cells in DU's array (last to first) + loop For each cell in DU's array + CU->>CU: seq_arr_erase(&du->cells, cell_ptr) - Remove from DU's array + CU->>CellTree: rrc_rm_cell(): RB_REMOVE(cell), decrement num_cells, rrc_free_cell_container() + end + CU->>DUTree: rrc_rm_du(): RB_REMOVE(du) - Remove DU from tree + CU->>CU: Decrement rrc->num_dus counter + CU->>CU: seq_arr_free(&du->cells) - Free DU's cell array + CU->>CU: rrc_free_du_container() - Free DU container + Note over CU: invalidate_du_connections() + loop For each UE: + CU->>CU: rrc_remove_ue_scells_from_du() - Remove SCells from disconnected DU + alt UE belongs to disconnected DU + CU->>CU: Set du_assoc_id = 0 (mark DU offline) + CU->>CU: Trigger NGAP UE Context Release Request + end + end + Note over CU,CellTree: DU and all cells removed +``` + +Key Functions: +- `rrc_gNB_process_f1_setup_req()` - Handles F1 Setup Request, creates DU and cell containers. + Validates PLMN match, DU ID uniqueness, and cell_id/PCI uniqueness before creating containers. +- `rrc_gNB_process_f1_du_configuration_update()` - Handles cell configuration updates. + Currently supports cell modification (MIB/SIB1 updates) and validates cell additions, but cell addition + and deletion are not yet fully implemented. +- `rrc_CU_process_f1_lost_connection()` - Handles DU disconnection. Calls `rrc_cleanup_du()` to + remove all cells and the DU from their trees and free resources, then + `invalidate_du_connections()` to clean up UE associations (e.g. trigger NGAP UE Context Release Request for UEs on that DU). + +#### UE Cell Association Management + +The RRC maintains a per-UE association with serving cells, tracking which cells +a UE is currently using. This replaces the previous single-cell assumption and +enables proper multi-cell support where each UE can have multiple serving cells +(one PCell and up to 31 SCells). The servCellIndex (TS 38.331) is tracked per-UE +in the `ue_serving_cell_t` structure in the UE context (the same cell can have +different servCellIndex values for different UEs). + +Data Structures: +- `ue_serving_cell_t`: Stores serving cell information (nci, servCellIndex, assoc_id) +- `gNB_RRC_UE_t.serving_cells`: Dynamic array (seq_arr_t) of serving cell entries. PCell is always at index 0. + +Key Functions: +- `rrc_add_ue_serving_cell()` - Adds a new serving cell to UE's serving_cells array. +- `rrc_get_ue_serving_cell_by_id()` - Retrieves serving cell entry by servCellIndex. +- `ue_get_pcell_entry()` - Returns the PCell serving cell entry (first element in serving_cells). +- `rrc_remove_ue_scells_from_du()` - Removes all serving cells belonging to a specific DU via assoc_id (e.g. during handover or DU disconnection). + +##### Handover and Cell Association Updates + +During handover, the UE's serving cell list is updated so the PCell reflects the target cell and source-DU cells are removed. + +###### F1 handover (inter-DU, same CU-CP) + +Cell association is updated when the **source** DU sends F1AP UE Context Modification Response (after it has sent the RRC Reconfiguration to the UE). The CU-CP is a single RRC instance; both source DU and target DU are under the same CU. + +```mermaid +sequenceDiagram + participant SourceDU as Source DU + participant CU as CU-CP + + Note over CU,CU: F1 handover: cell association update on Context Modification Response + SourceDU->>CU: F1AP UE Context Modification Response + Note over CU: rrc_CU_process_ue_context_modification_response() + Note over CU: Check: ho_context && source && target (F1 HO) + + alt F1 handover in progress + CU->>CU: nr_rrc_apply_target_context(UE) + Note over CU: F1 UE data: du_assoc_id = target DU, secondary_ue = target DU UE ID, RNTI = target RNTI + CU->>CU: nr_rrc_update_cell_assoc_after_ho(rrc, UE) + Note over CU: F1 branch (ho_context->source present): + CU->>CU: rrc_remove_ue_scells_from_du(UE, source_ctx->cell->assoc_id) + Note over CU: Remove all serving cells (incl. PCell) belonging to source DU + CU->>CU: rrc_add_ue_serving_cell(UE, target_ctx->cell, RRC_PCELL_INDEX) + Note over CU: Handover complete: PCell = target cell, source DU cells removed + end +``` + +###### N2 handover (inter-gNB: source CU vs target CU) + +Cell association is updated only on the target CU-CP, when the target DU sends F1AP UE Context Setup Response. The UE context was created for handover, so there is no existing serving cell to remove. The flow is triggered inside the handover request acknowledge callback. + +- Target CU-CP: Receives HANDOVER REQUEST, sets up bearer and F1 UE context on target DU; when the target DU sends F1AP UE Context Setup Response, the target CU runs the cell-association update and then sends HANDOVER REQUEST ACKNOWLEDGE. + +```mermaid +sequenceDiagram + participant TargetDU as Target DU + participant TargetCU as Target CU-CP + + Note over TargetCU: N2 handover: cell association update + Note over TargetCU: F1AP UE Context Setup Resp from target DU (in nr_rrc_n2_ho_acknowledge) + TargetDU->>TargetCU: F1AP UE Context Setup Response + Note over TargetCU: rrc_CU_process_ue_context_setup_response() then callback ho_req_ack() + Note over TargetCU: nr_rrc_n2_ho_acknowledge(rrc, UE) + + TargetCU->>TargetCU: nr_rrc_apply_target_context(UE) + Note over TargetCU: F1 UE data: du_assoc_id = target DU, secondary_ue = target DU UE ID, RNTI = target RNTI + TargetCU->>TargetCU: nr_rrc_update_cell_assoc_after_ho(rrc, UE) + Note over TargetCU: N2 branch (ho_context->source NULL): no SCells to remove + TargetCU->>TargetCU: rrc_add_ue_serving_cell(UE, target_ctx->cell, RRC_PCELL_INDEX) + Note over TargetCU: Then: encode Handover Command, send NGAP HANDOVER REQUEST ACKNOWLEDGE +``` + +## UE Context + +UE context information is stored in `gNB_RRC_UE_t`, which includes: +- Serving cells tracking: Dynamic array of serving cells (PCell + SCells) +- Security context: Keys, algorithms, and security state +- Radio bearers: SRB and DRB configurations +- PDU sessions: Active PDU session information +- Handover context: Temporary data during handover procedures ### CU-UPs diff --git a/doc/RRC/rrc-usage.md b/doc/RRC/rrc-usage.md index bae31e864a..f9188de05a 100644 --- a/doc/RRC/rrc-usage.md +++ b/doc/RRC/rrc-usage.md @@ -41,15 +41,19 @@ For each DU, it prints: ``` 1 connected DUs -[1] DU ID 3584 (gNB-OAI-DU) assoc_id 8: nrCellID 12345678, PCI 0, SSB ARFCN 641280 - TDD: band 78 ARFCN 640008 SCS 30 (kHz) PRB 106 +[1] DU ID 3584 (gNB-OAI) integrated DU-CU: 1 cell + [1] nrCellID 12345678, PCI 0, Mode TDD, SSB ARFCN 641280 + TDD: band 78 ARFCN 640008 SCS 30 (kHz) PRB 106 ``` -i.e., an index (`[1]`), the DU ID and it's name, the SCTP association ID -(`assoc_id 8`, cf. UE information), and DU specific information for the cell -(cell ID, physical cell identity/PCI, the SSB frequency in ARFCN notation, the -band and Point A ARFCN, subcarrier spacing/SCS, and the number of resource -blocks/PRB). Only one cell per DU is supported. +The output shows: +- Total number of connected DUs +- For each DU: an index (`[1]`), the DU ID and name, connection type (either `integrated DU-CU` for monolithic deployments or `assoc_id X` for split deployments), and the number of cells served by this DU +- For each cell served by the DU: a cell index (`[1]`), the NR Cell ID, Physical Cell ID (PCI), operating mode (TDD or FDD), and SSB ARFCN +- Cell-specific frequency information: for TDD mode, the band, Point A ARFCN, subcarrier spacing (SCS), and number of resource blocks (PRB); for FDD mode, separate DL and UL frequency information + +The RRC enables the support of multiple cells per DU (though currently each DU +typically serves one cell in practice). As of now, it does not print information about connected CU-UPs or AMFs.