Initial commit for ngap in enb

master
David Rupprecht 4 years ago committed by David Rupprecht
parent 3284143b39
commit c117b563c9

@ -0,0 +1,31 @@
/**
*
* \section COPYRIGHT
*
* Copyright 2013-2021 Software Radio Systems Limited
*
* By using this file, you agree to the terms and conditions set
* forth in the LICENSE file which can be found at the top level of
* the distribution.
*
*/
#ifndef SRSRAN_NGAP_UTILS_H
#define SRSRAN_NGAP_UTILS_H
#include "asn1_utils.h"
#include "ngap.h"
/************************
* Forward declarations
***********************/
namespace asn1 {
namespace ngap_nr {
struct rrcestablishment_cause_opts;
struct cause_radio_network_opts;
using rrcestablishment_cause_e = enumerated<rrcestablishment_cause_opts, true, 1>;
using cause_radio_network_e = enumerated<cause_radio_network_opts, true, 2>;
} // namespace ngap
} // namespace asn1
#endif // SRSRAN_NGAP_UTILS_H

@ -159,18 +159,6 @@ class rrc_interface_pdcp_nr
public:
virtual void write_pdu(uint16_t rnti, uint32_t lcid, srsran::unique_byte_buffer_t pdu) = 0;
};
class rrc_interface_ngap_nr
{
public:
};
/*****************************
* NGAP INTERFACES
****************************/
class ngap_interface_rrc_nr
{
public:
};
class phy_interface_stack_nr
{

@ -0,0 +1,60 @@
/**
*
* \section COPYRIGHT
*
* Copyright 2013-2021 Software Radio Systems Limited
*
* By using this file, you agree to the terms and conditions set
* forth in the LICENSE file which can be found at the top level of
* the distribution.
*
*/
#ifndef SRSRAN_GNB_NGAP_INTERFACES_H
#define SRSRAN_GNB_NGAP_INTERFACES_H
#include "srsran/asn1/ngap_utils.h"
namespace srsenb {
struct ngap_args_t {
uint32_t gnb_id; // 20-bit id (lsb bits)
uint8_t cell_id; // 8-bit cell id
uint16_t tac; // 16-bit tac
uint16_t mcc; // BCD-coded with 0xF filler
uint16_t mnc; // BCD-coded with 0xF filler
std::string amf_addr;
std::string gtp_bind_addr;
std::string gtp_advertise_addr;
std::string ngc_bind_addr;
std::string gnb_name;
};
// NGAP interface for RRC
class ngap_interface_rrc_nr
{
public:
virtual void initial_ue(uint16_t rnti,
uint32_t gnb_cc_idx,
asn1::ngap_nr::rrcestablishment_cause_e cause,
srsran::unique_byte_buffer_t pdu) = 0;
virtual void initial_ue(uint16_t rnti,
uint32_t gnb_cc_idx,
asn1::ngap_nr::rrcestablishment_cause_e cause,
srsran::unique_byte_buffer_t pdu,
uint32_t m_tmsi,
uint8_t mmec) = 0;
virtual void write_pdu(uint16_t rnti, srsran::unique_byte_buffer_t pdu) = 0;
virtual bool user_exists(uint16_t rnti) = 0;
virtual void user_mod(uint16_t old_rnti, uint16_t new_rnti) = 0;
virtual bool user_release(uint16_t rnti, asn1::ngap_nr::cause_radio_network_e cause_radio) = 0;
virtual bool is_amf_connected() = 0;
/// TS 36.413, 8.3.1 - Initial Context Setup
virtual void ue_ctxt_setup_complete(uint16_t rnti) = 0;
};
} // namespace srsenb
#endif // SRSRAN_GNB_NGAP_INTERFACES_H

@ -0,0 +1,24 @@
/**
*
* \section COPYRIGHT
*
* Copyright 2013-2021 Software Radio Systems Limited
*
* By using this file, you agree to the terms and conditions set
* forth in the LICENSE file which can be found at the top level of
* the distribution.
*
*/
#ifndef SRSRAN_GNB_RRC_NR_INTERFACES_H
#define SRSRAN_GNB_RRC_NR_INTERFACES_H
namespace srsenb {
class rrc_interface_ngap_nr
{
public:
};
} // namespace srsenb
#endif // SRSRAN_GNB_RRC_NR_INTERFACES_H

@ -24,6 +24,8 @@
#include "srsran/common/threads.h"
#include "srsran/common/timeout.h"
#include "srsran/interfaces/gnb_interfaces.h"
#include "srsran/interfaces/gnb_ngap_interfaces.h"
#include "srsran/interfaces/gnb_rrc_nr_interfaces.h"
#include <map>
#include <queue>

@ -0,0 +1,134 @@
/**
*
* \section COPYRIGHT
*
* Copyright 2013-2021 Software Radio Systems Limited
*
* By using this file, you agree to the terms and conditions set
* forth in the LICENSE file which can be found at the top level of
* the distribution.
*
*/
#ifndef SRSENB_NGAP_H
#define SRSENB_NGAP_H
#include "srsenb/hdr/common/common_enb.h"
#include "srsran/adt/circular_map.h"
#include "srsran/asn1/asn1_utils.h"
#include "srsran/asn1/ngap.h"
#include "srsran/common/bcd_helpers.h"
#include "srsran/common/buffer_pool.h"
#include "srsran/common/common.h"
#include "srsran/common/network_utils.h"
#include "srsran/common/stack_procedure.h"
#include "srsran/common/standard_streams.h"
#include "srsran/common/task_scheduler.h"
#include "srsran/common/threads.h"
#include "srsran/interfaces/gnb_ngap_interfaces.h"
#include "srsran/interfaces/gnb_rrc_nr_interfaces.h"
#include "srsran/srslog/srslog.h"
namespace srsenb {
class ngap : public ngap_interface_rrc_nr
{
public:
ngap(srsran::task_sched_handle task_sched_,
srslog::basic_logger& logger,
srsran::socket_manager_itf* rx_socket_handler);
int init(ngap_args_t args_, rrc_interface_ngap_nr* rrc_);
void stop();
// RRC NR interface
void initial_ue(uint16_t rnti,
uint32_t gnb_cc_idx,
asn1::ngap_nr::rrcestablishment_cause_e cause,
srsran::unique_byte_buffer_t pdu){};
void initial_ue(uint16_t rnti,
uint32_t gnb_cc_idx,
asn1::ngap_nr::rrcestablishment_cause_e cause,
srsran::unique_byte_buffer_t pdu,
uint32_t m_tmsi,
uint8_t mmec){};
void write_pdu(uint16_t rnti, srsran::unique_byte_buffer_t pdu){};
bool user_exists(uint16_t rnti) { return true; };
void user_mod(uint16_t old_rnti, uint16_t new_rnti){};
bool user_release(uint16_t rnti, asn1::ngap_nr::cause_radio_network_e cause_radio) { return true; };
bool is_amf_connected();
/// TS 36.413, 8.3.1 - Initial Context Setup
void ue_ctxt_setup_complete(uint16_t rnti){};
// Stack interface
bool
handle_amf_rx_msg(srsran::unique_byte_buffer_t pdu, const sockaddr_in& from, const sctp_sndrcvinfo& sri, int flags);
private:
static const int AMF_PORT = 38412;
static const int ADDR_FAMILY = AF_INET;
static const int SOCK_TYPE = SOCK_STREAM;
static const int PROTO = IPPROTO_SCTP;
static const int PPID = 60;
static const int NONUE_STREAM_ID = 0;
// args
rrc_interface_ngap_nr* rrc = nullptr;
ngap_args_t args;
srslog::basic_logger& logger;
srsran::task_sched_handle task_sched;
srsran::task_queue_handle amf_task_queue;
srsran::socket_manager_itf* rx_socket_handler;
srsran::unique_socket amf_socket;
struct sockaddr_in amf_addr = {}; // AMF address
bool amf_connected = false;
bool running = false;
uint32_t next_enb_ue_ngap_id = 1; // Next ENB-side UE identifier
uint16_t next_ue_stream_id = 1; // Next UE SCTP stream identifier
srsran::unique_timer amf_connect_timer, ngsetup_timeout;
// Protocol IEs sent with every UL NGAP message
asn1::ngap_nr::tai_s tai;
asn1::ngap_nr::nr_cgi_s nr_cgi;
asn1::ngap_nr::ng_setup_resp_s ngsetupresponse;
// procedures
class ng_setup_proc_t
{
public:
struct ngsetupresult {
bool success = false;
enum class cause_t { timeout, failure } cause;
};
explicit ng_setup_proc_t(ngap* ngap_) : ngap_ptr(ngap_) {}
srsran::proc_outcome_t init();
srsran::proc_outcome_t step() { return srsran::proc_outcome_t::yield; }
srsran::proc_outcome_t react(const ngsetupresult& event);
void then(const srsran::proc_state_t& result) const;
const char* name() const { return "AMF Connection"; }
private:
srsran::proc_outcome_t start_amf_connection();
ngap* ngap_ptr = nullptr;
};
void build_tai_cgi();
bool connect_amf();
bool setup_ng();
bool sctp_send_ngap_pdu(const asn1::ngap_nr::ngap_pdu_c& tx_pdu, uint32_t rnti, const char* procedure_name);
bool handle_ngap_rx_pdu(srsran::byte_buffer_t* pdu);
bool handle_successfuloutcome(const asn1::ngap_nr::successful_outcome_s& msg);
bool handle_ngsetupresponse(const asn1::ngap_nr::ng_setup_resp_s& msg);
srsran::proc_t<ng_setup_proc_t> ngsetup_proc;
std::string get_cause(const asn1::ngap_nr::cause_c& c);
void log_ngap_msg(const asn1::ngap_nr::ngap_pdu_c& msg, srsran::const_span<uint8_t> sdu, bool is_rx);
};
} // namespace srsenb
#endif

@ -9,5 +9,5 @@
set(SOURCES gtpu.cc pdcp.cc rlc.cc s1ap.cc)
add_library(srsenb_upper STATIC ${SOURCES})
set(SOURCES pdcp_nr.cc rlc_nr.cc sdap.cc)
set(SOURCES pdcp_nr.cc rlc_nr.cc sdap.cc ngap.cc)
add_library(srsgnb_upper STATIC ${SOURCES})

@ -0,0 +1,405 @@
/**
*
* \section COPYRIGHT
*
* Copyright 2013-2021 Software Radio Systems Limited
*
* By using this file, you agree to the terms and conditions set
* forth in the LICENSE file which can be found at the top level of
* the distribution.
*
*/
#include "srsenb/hdr/stack/upper/ngap.h"
#define procError(fmt, ...) ngap_ptr->logger.error("Proc \"%s\" - " fmt, name(), ##__VA_ARGS__)
#define procWarning(fmt, ...) ngap_ptr->logger.warning("Proc \"%s\" - " fmt, name(), ##__VA_ARGS__)
#define procInfo(fmt, ...) ngap_ptr->logger.info("Proc \"%s\" - " fmt, name(), ##__VA_ARGS__)
using namespace asn1::ngap_nr;
namespace srsenb {
/*********************************************************
* AMF Connection
*********************************************************/
srsran::proc_outcome_t ngap::ng_setup_proc_t::init()
{
procInfo("Starting new AMF connection.");
return start_amf_connection();
}
srsran::proc_outcome_t ngap::ng_setup_proc_t::start_amf_connection()
{
if (not ngap_ptr->running) {
procInfo("NGAP is not running anymore.");
return srsran::proc_outcome_t::error;
}
if (ngap_ptr->amf_connected) {
procInfo("gNB NGAP is already connected to AMF");
return srsran::proc_outcome_t::success;
}
if (not ngap_ptr->connect_amf()) {
procInfo("Could not connect to AMF");
return srsran::proc_outcome_t::error;
}
if (not ngap_ptr->setup_ng()) {
procError("NG setup failed. Exiting...");
srsran::console("NG setup failed\n");
ngap_ptr->running = false;
return srsran::proc_outcome_t::error;
}
ngap_ptr->ngsetup_timeout.run();
procInfo("NGSetupRequest sent. Waiting for response...");
return srsran::proc_outcome_t::yield;
}
srsran::proc_outcome_t ngap::ng_setup_proc_t::react(const srsenb::ngap::ng_setup_proc_t::ngsetupresult& event)
{
if (ngap_ptr->ngsetup_timeout.is_running()) {
ngap_ptr->ngsetup_timeout.stop();
}
if (event.success) {
procInfo("NGSetup procedure completed successfully");
return srsran::proc_outcome_t::success;
}
procError("NGSetup failed.");
srsran::console("NGsetup failed\n");
return srsran::proc_outcome_t::error;
}
void ngap::ng_setup_proc_t::then(const srsran::proc_state_t& result) const
{
if (result.is_error()) {
procInfo("Failed to initiate NG connection. Attempting reconnection in %d seconds",
ngap_ptr->amf_connect_timer.duration() / 1000);
srsran::console("Failed to initiate NG connection. Attempting reconnection in %d seconds\n",
ngap_ptr->amf_connect_timer.duration() / 1000);
ngap_ptr->rx_socket_handler->remove_socket(ngap_ptr->amf_socket.get_socket());
ngap_ptr->amf_socket.close();
procInfo("NGAP socket closed.");
ngap_ptr->amf_connect_timer.run();
// Try again with in 10 seconds
}
}
/*********************************************************
* NGAP class
*********************************************************/
ngap::ngap(srsran::task_sched_handle task_sched_,
srslog::basic_logger& logger,
srsran::socket_manager_itf* rx_socket_handler_) :
ngsetup_proc(this), logger(logger), task_sched(task_sched_), rx_socket_handler(rx_socket_handler_)
{
amf_task_queue = task_sched.make_task_queue();
}
int ngap::init(ngap_args_t args_, rrc_interface_ngap_nr* rrc_)
{
rrc = rrc_;
args = args_;
build_tai_cgi();
// Setup AMF reconnection timer
amf_connect_timer = task_sched.get_unique_timer();
auto amf_connect_run = [this](uint32_t tid) {
if (ngsetup_proc.is_busy()) {
logger.error("Failed to initiate NGSetup procedure.");
}
ngsetup_proc.launch();
};
amf_connect_timer.set(10000, amf_connect_run);
// Setup NGSetup timeout
ngsetup_timeout = task_sched.get_unique_timer();
uint32_t ngsetup_timeout_val = 5000;
ngsetup_timeout.set(ngsetup_timeout_val, [this](uint32_t tid) {
ng_setup_proc_t::ngsetupresult res;
res.success = false;
res.cause = ng_setup_proc_t::ngsetupresult::cause_t::timeout;
ngsetup_proc.trigger(res);
});
running = true;
// starting AMF connection
if (not ngsetup_proc.launch()) {
logger.error("Failed to initiate NGSetup procedure.");
}
return SRSRAN_SUCCESS;
}
void ngap::stop()
{
running = false;
amf_socket.close();
}
bool ngap::is_amf_connected()
{
return amf_connected;
}
// Generate common NGAP protocol IEs from config args
void ngap::build_tai_cgi()
{
uint32_t plmn;
// TAI
srsran::s1ap_mccmnc_to_plmn(args.mcc, args.mnc, &plmn);
tai.plmn_id.from_number(plmn);
tai.tac.from_number(args.tac);
// nr_cgi
nr_cgi.plmn_id.from_number(plmn);
// TODO Check how to build nr cell id
nr_cgi.nrcell_id.from_number((uint32_t)(args.gnb_id << 8) | args.cell_id);
}
/*******************************************************************************
/* NGAP message handlers
********************************************************************************/
bool ngap::handle_amf_rx_msg(srsran::unique_byte_buffer_t pdu,
const sockaddr_in& from,
const sctp_sndrcvinfo& sri,
int flags)
{
// Handle Notification Case
if (flags & MSG_NOTIFICATION) {
// Received notification
union sctp_notification* notification = (union sctp_notification*)pdu->msg;
logger.debug("SCTP Notification %d", notification->sn_header.sn_type);
if (notification->sn_header.sn_type == SCTP_SHUTDOWN_EVENT) {
logger.info("SCTP Association Shutdown. Association: %d", sri.sinfo_assoc_id);
srsran::console("SCTP Association Shutdown. Association: %d\n", sri.sinfo_assoc_id);
rx_socket_handler->remove_socket(amf_socket.get_socket());
amf_socket.close();
} else if (notification->sn_header.sn_type == SCTP_PEER_ADDR_CHANGE &&
notification->sn_paddr_change.spc_state == SCTP_ADDR_UNREACHABLE) {
logger.info("SCTP peer addres unreachable. Association: %d", sri.sinfo_assoc_id);
srsran::console("SCTP peer address unreachable. Association: %d\n", sri.sinfo_assoc_id);
rx_socket_handler->remove_socket(amf_socket.get_socket());
amf_socket.close();
}
} else if (pdu->N_bytes == 0) {
logger.error("SCTP return 0 bytes. Closing socket");
amf_socket.close();
}
// Restart MME connection procedure if we lost connection
if (not amf_socket.is_open()) {
amf_connected = false;
if (ngsetup_proc.is_busy()) {
logger.error("Failed to initiate MME connection procedure, as it is already running.");
return false;
}
ngsetup_proc.launch();
return false;
}
handle_ngap_rx_pdu(pdu.get());
return true;
}
bool ngap::handle_ngap_rx_pdu(srsran::byte_buffer_t* pdu)
{
// Save message to PCAP
// if (pcap != nullptr) {
// pcap->write_ngap(pdu->msg, pdu->N_bytes);
// }
ngap_pdu_c rx_pdu;
asn1::cbit_ref bref(pdu->msg, pdu->N_bytes);
if (rx_pdu.unpack(bref) != asn1::SRSASN_SUCCESS) {
logger.error(pdu->msg, pdu->N_bytes, "Failed to unpack received PDU");
cause_c cause;
cause.set_protocol().value = cause_protocol_opts::transfer_syntax_error;
// send_error_indication(cause);
return false;
}
// log_ngap_msg(rx_pdu, srsran::make_span(*pdu), true);
switch (rx_pdu.type().value) {
// case ngap_pdu_c::types_opts::init_msg:
// return handle_initiatingmessage(rx_pdu.init_msg());
case ngap_pdu_c::types_opts::successful_outcome:
return handle_successfuloutcome(rx_pdu.successful_outcome());
// case ngap_pdu_c::types_opts::unsuccessful_outcome:
// return handle_unsuccessfuloutcome(rx_pdu.unsuccessful_outcome());
default:
logger.error("Unhandled PDU type %d", rx_pdu.type().value);
return false;
}
return true;
}
bool ngap::handle_successfuloutcome(const successful_outcome_s& msg)
{
switch (msg.value.type().value) {
case ngap_elem_procs_o::successful_outcome_c::types_opts::ng_setup_resp:
return handle_ngsetupresponse(msg.value.ng_setup_resp());
default:
logger.error("Unhandled successful outcome message: %s", msg.value.type().to_string());
}
return true;
}
bool ngap::handle_ngsetupresponse(const asn1::ngap_nr::ng_setup_resp_s& msg)
{
ngsetupresponse = msg;
amf_connected = true;
ng_setup_proc_t::ngsetupresult res;
res.success = true;
ngsetup_proc.trigger(res);
return true;
}
/*******************************************************************************
/* NGAP connection helpers
********************************************************************************/
bool ngap::connect_amf()
{
using namespace srsran::net_utils;
logger.info("Connecting to AMF %s:%d", args.amf_addr.c_str(), int(AMF_PORT));
// Init SCTP socket and bind it
if (not sctp_init_client(&amf_socket, socket_type::seqpacket, args.ngc_bind_addr.c_str())) {
return false;
}
logger.info("SCTP socket opened. fd=%d", amf_socket.fd());
// Connect to the AMF address
if (not amf_socket.connect_to(args.amf_addr.c_str(), AMF_PORT, &amf_addr)) {
return false;
}
logger.info("SCTP socket connected with AMF. fd=%d", amf_socket.fd());
// Assign a handler to rx AMF packets
auto rx_callback =
[this](srsran::unique_byte_buffer_t pdu, const sockaddr_in& from, const sctp_sndrcvinfo& sri, int flags) {
// Defer the handling of AMF packet to eNB stack main thread
handle_amf_rx_msg(std::move(pdu), from, sri, flags);
};
rx_socket_handler->add_socket_handler(amf_socket.fd(),
srsran::make_sctp_sdu_handler(logger, amf_task_queue, rx_callback));
logger.info("SCTP socket established with AMF");
return true;
}
bool ngap::setup_ng()
{
uint32_t tmp32;
uint16_t tmp16;
uint32_t plmn;
srsran::s1ap_mccmnc_to_plmn(args.mcc, args.mnc, &plmn);
plmn = htonl(plmn);
ngap_pdu_c pdu;
pdu.set_init_msg().load_info_obj(ASN1_NGAP_NR_ID_NG_SETUP);
ng_setup_request_ies_container& container = pdu.init_msg().value.ng_setup_request().protocol_ies;
global_gnb_id_s& global_gnb_id = container.global_ran_node_id.value.set_global_gnb_id();
global_gnb_id.plmn_id = tai.plmn_id;
// TODO: when ASN1 is fixed
// global_gnb_id.gnb_id.set_gnb_id().from_number(args.gnb_id);
// container.ran_node_name_present = true;
// container.ran_node_name.value.from_string(args.gnb_name);
asn1::bounded_bitstring<22, 32, false, true>& gnb_str = global_gnb_id.gnb_id.set_gnb_id();
gnb_str.resize(32);
uint8_t buffer[4];
asn1::bit_ref bref(&buffer[0], sizeof(buffer));
bref.pack(args.gnb_id, 8);
memcpy(gnb_str.data(), &buffer[0], bref.distance_bytes());
// .from_number(args.gnb_id);
container.ran_node_name_present = true;
if (args.gnb_name.length() >= 150) {
args.gnb_name.resize(150);
}
// container.ran_node_name.value.from_string(args.enb_name);
container.ran_node_name.value.resize(args.gnb_name.size());
memcpy(&container.ran_node_name.value[0], &args.gnb_name[0], args.gnb_name.size());
container.supported_ta_list.value.resize(1);
container.supported_ta_list.value[0].tac = tai.tac;
container.supported_ta_list.value[0].broadcast_plmn_list.resize(1);
container.supported_ta_list.value[0].broadcast_plmn_list[0].plmn_id = tai.plmn_id;
container.supported_ta_list.value[0].broadcast_plmn_list[0].tai_slice_support_list.resize(1);
container.supported_ta_list.value[0].broadcast_plmn_list[0].tai_slice_support_list[0].s_nssai.sst.from_number(1);
container.default_paging_drx.value.value = asn1::ngap_nr::paging_drx_opts::v256; // Todo: add to args, config file
return sctp_send_ngap_pdu(pdu, 0, "ngSetupRequest");
}
/*******************************************************************************
/* General helpers
********************************************************************************/
bool ngap::sctp_send_ngap_pdu(const asn1::ngap_nr::ngap_pdu_c& tx_pdu, uint32_t rnti, const char* procedure_name)
{
if (not amf_connected and rnti != SRSRAN_INVALID_RNTI) {
logger.error("Aborting %s for rnti=0x%x. Cause: AMF is not connected.", procedure_name, rnti);
return false;
}
srsran::unique_byte_buffer_t buf = srsran::make_byte_buffer();
if (buf == nullptr) {
logger.error("Fatal Error: Couldn't allocate buffer for %s.", procedure_name);
return false;
}
asn1::bit_ref bref(buf->msg, buf->get_tailroom());
if (tx_pdu.pack(bref) != asn1::SRSASN_SUCCESS) {
logger.error("Failed to pack TX PDU %s", procedure_name);
return false;
}
buf->N_bytes = bref.distance_bytes();
// TODO: when we got pcap support
// Save message to PCAP
// if (pcap != nullptr) {
// pcap->write_s1ap(buf->msg, buf->N_bytes);
// }
if (rnti != SRSRAN_INVALID_RNTI) {
logger.info(buf->msg, buf->N_bytes, "Tx S1AP SDU, %s, rnti=0x%x", procedure_name, rnti);
} else {
logger.info(buf->msg, buf->N_bytes, "Tx S1AP SDU, %s", procedure_name);
}
// TODO: when user list is ready
// uint16_t streamid = rnti == SRSRAN_INVALID_RNTI ? NONUE_STREAM_ID : users.find_ue_rnti(rnti)->stream_id;
uint16_t streamid = 0;
ssize_t n_sent = sctp_sendmsg(amf_socket.fd(),
buf->msg,
buf->N_bytes,
(struct sockaddr*)&amf_addr,
sizeof(struct sockaddr_in),
htonl(PPID),
0,
streamid,
0,
0);
if (n_sent == -1) {
if (rnti != SRSRAN_INVALID_RNTI) {
logger.error("Error: Failure at Tx S1AP SDU, %s, rnti=0x%x", procedure_name, rnti);
} else {
logger.error("Error: Failure at Tx S1AP SDU, %s", procedure_name);
}
return false;
}
return true;
}
} // namespace srsenb
Loading…
Cancel
Save