diff --git a/CONTRIBUTORS b/CONTRIBUTORS index c7a5e46..c7f9d4d 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -21,3 +21,7 @@ We are grateful for every contribution, big or small, that helped shape this pro - Filippo Olimpieri <126257005+Ninjabippo1205@users.noreply.github.com> - Callback mechanism for dApp status reporting to the E2 Agent + +- Davide Villa (NVIDIA Corporation) + - JSON encoder: camelCase wire format with nested-JSON protocolData + - Subscription manager: per-subscription metadata, C API subscription getters diff --git a/VERSION b/VERSION index bbdeab6..1750564 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.5 +0.0.6 diff --git a/include/libe3/c_api.h b/include/libe3/c_api.h index 12fe97e..d652e4a 100644 --- a/include/libe3/c_api.h +++ b/include/libe3/c_api.h @@ -289,6 +289,47 @@ uint32_t* e3_agent_get_dapp_subscriptions(e3_agent_handle_t* agent, uint32_t dap */ uint32_t* e3_agent_get_ran_function_subscribers(e3_agent_handle_t* agent, uint32_t ran_function_id, size_t* out_len); +/** + * @brief Get the reporting periodicity a dApp requested for a RAN function. + * + * @return periodicity in microseconds, 0 if not set or subscription not found + */ +uint32_t e3_agent_get_subscription_periodicity( + e3_agent_handle_t* agent, + uint32_t dapp_id, + uint32_t ran_function_id +); + +/** + * @brief Get the telemetry IDs a dApp requested for a RAN function. + * + * Caller must free the returned array with e3_agent_free_uint32_array(). + * + * @param out_len receives the number of elements (set to 0 on error/empty) + * @return newly-allocated array, or NULL if empty/error + */ +uint32_t* e3_agent_get_subscription_telemetry_ids( + e3_agent_handle_t* agent, + uint32_t dapp_id, + uint32_t ran_function_id, + size_t* out_len +); + +/** + * @brief Get the control IDs a dApp requested for a RAN function. + * + * Caller must free the returned array with e3_agent_free_uint32_array(). + * + * @param out_len receives the number of elements (set to 0 on error/empty) + * @return newly-allocated array, or NULL if empty/error + */ +uint32_t* e3_agent_get_subscription_control_ids( + e3_agent_handle_t* agent, + uint32_t dapp_id, + uint32_t ran_function_id, + size_t* out_len +); + /** * @brief Free an array previously returned by the e3_agent_get_* helpers. */ diff --git a/include/libe3/e3_agent.hpp b/include/libe3/e3_agent.hpp index b09af9f..120b98d 100644 --- a/include/libe3/e3_agent.hpp +++ b/include/libe3/e3_agent.hpp @@ -306,6 +306,33 @@ class E3Agent { uint32_t ran_function_id ) const; + /** + * @brief Get the reporting periodicity a dApp requested for a RAN function + * + * @return periodicity in microseconds, 0 if not set or subscription not found + */ + uint32_t get_subscription_periodicity( + uint32_t dapp_id, uint32_t ran_function_id + ) const; + + /** + * @brief Get the telemetry IDs a dApp requested for a RAN function + * + * @return vector of telemetry IDs (empty if none requested or subscription not found) + */ + std::vector get_subscription_telemetry_ids( + uint32_t dapp_id, uint32_t ran_function_id + ) const; + + /** + * @brief Get the control IDs a dApp requested for a RAN function + * + * @return vector of control IDs (empty if none requested or subscription not found) + */ + std::vector get_subscription_control_ids( + uint32_t dapp_id, uint32_t ran_function_id + ) const; + // ========================================================================= // Configuration // ========================================================================= diff --git a/include/libe3/subscription_manager.hpp b/include/libe3/subscription_manager.hpp index 1a15ef5..993d618 100644 --- a/include/libe3/subscription_manager.hpp +++ b/include/libe3/subscription_manager.hpp @@ -22,6 +22,15 @@ namespace libe3 { +/** + * @brief Per-subscription metadata stored by the SubscriptionManager. + */ +struct SubscriptionDetails { + std::vector telemetry_ids; + std::vector control_ids; + uint32_t periodicity_us{0}; ///< 0 = use SM default +}; + /** * @brief Callback type for SM lifecycle events * @@ -116,13 +125,21 @@ class SubscriptionManager { * * @param dapp_id dApp identifier * @param ran_function_id RAN function identifier (0-255) + * @param telemetry_ids Telemetry IDs the dApp wants + * @param control_ids Control IDs the dApp wants + * @param periodicity_us Reporting periodicity in microseconds (0 = SM default) * @return std::pair containing: * - ErrorCode::SUCCESS on success, or error code on failure * - The assigned subscription ID (valid only if ErrorCode::SUCCESS) * @return ErrorCode::DAPP_NOT_REGISTERED if dApp not registered * @return ErrorCode::SUBSCRIPTION_EXISTS if already subscribed */ - std::pair add_subscription(uint32_t dapp_id, uint32_t ran_function_id); + std::pair add_subscription( + uint32_t dapp_id, + uint32_t ran_function_id, + const std::vector& telemetry_ids = {}, + const std::vector& control_ids = {}, + uint32_t periodicity_us = 0); /** * @brief Remove a subscription between dApp and RAN function @@ -159,6 +176,13 @@ class SubscriptionManager { */ std::vector get_subscribed_dapps(uint32_t ran_function_id) const; + /** + * @brief Get subscription details for a specific dApp + RAN function pair + * + * @return nullptr if no such subscription exists + */ + const SubscriptionDetails* get_subscription_details(uint32_t dapp_id, uint32_t ran_function_id) const; + /** * @brief Get count of subscribers for a RAN function */ @@ -215,6 +239,9 @@ class SubscriptionManager { // (dApp ID, RAN function ID) -> Subscription ID reverse mapping std::unordered_map subscription_id_reverse_; + + // (dApp ID, RAN function ID) -> per-subscription metadata + std::unordered_map subscription_details_; // Callback for SM lifecycle events SmLifecycleCallback sm_lifecycle_callback_; diff --git a/src/c_api.cpp b/src/c_api.cpp index 02548af..079fdbd 100644 --- a/src/c_api.cpp +++ b/src/c_api.cpp @@ -300,6 +300,37 @@ e3_error_t e3_agent_set_dapp_report_handler( return copy_vector_u32_to_c(v, out_len); } + uint32_t e3_agent_get_subscription_periodicity( + e3_agent_handle_t* agent, + uint32_t dapp_id, + uint32_t ran_function_id + ) { + if (!agent || !agent->agent) return 0; + return agent->agent->get_subscription_periodicity(dapp_id, ran_function_id); + } + + uint32_t* e3_agent_get_subscription_telemetry_ids( + e3_agent_handle_t* agent, + uint32_t dapp_id, + uint32_t ran_function_id, + size_t* out_len + ) { + if (!agent || !agent->agent) { if (out_len) *out_len = 0; return nullptr; } + auto v = agent->agent->get_subscription_telemetry_ids(dapp_id, ran_function_id); + return copy_vector_u32_to_c(v, out_len); + } + + uint32_t* e3_agent_get_subscription_control_ids( + e3_agent_handle_t* agent, + uint32_t dapp_id, + uint32_t ran_function_id, + size_t* out_len + ) { + if (!agent || !agent->agent) { if (out_len) *out_len = 0; return nullptr; } + auto v = agent->agent->get_subscription_control_ids(dapp_id, ran_function_id); + return copy_vector_u32_to_c(v, out_len); + } + void e3_agent_free_uint32_array(uint32_t* arr) { free(arr); } diff --git a/src/core/e3_agent.cpp b/src/core/e3_agent.cpp index 03213d0..d13722c 100644 --- a/src/core/e3_agent.cpp +++ b/src/core/e3_agent.cpp @@ -271,6 +271,33 @@ std::vector E3Agent::get_ran_function_subscribers(uint32_t ran_functio return impl_->interface->subscription_manager().get_subscribed_dapps(ran_function_id); } +uint32_t E3Agent::get_subscription_periodicity(uint32_t dapp_id, uint32_t ran_function_id) const { + if (!impl_->interface) { + return 0; + } + const auto* details = impl_->interface->subscription_manager() + .get_subscription_details(dapp_id, ran_function_id); + return details ? details->periodicity_us : 0; +} + +std::vector E3Agent::get_subscription_telemetry_ids(uint32_t dapp_id, uint32_t ran_function_id) const { + if (!impl_->interface) { + return {}; + } + const auto* details = impl_->interface->subscription_manager() + .get_subscription_details(dapp_id, ran_function_id); + return details ? details->telemetry_ids : std::vector{}; +} + +std::vector E3Agent::get_subscription_control_ids(uint32_t dapp_id, uint32_t ran_function_id) const { + if (!impl_->interface) { + return {}; + } + const auto* details = impl_->interface->subscription_manager() + .get_subscription_details(dapp_id, ran_function_id); + return details ? details->control_ids : std::vector{}; +} + // ========================================================================= // Configuration // ========================================================================= diff --git a/src/core/e3_interface.cpp b/src/core/e3_interface.cpp index 7f3847e..d8e00dc 100644 --- a/src/core/e3_interface.cpp +++ b/src/core/e3_interface.cpp @@ -626,9 +626,13 @@ void E3Interface::handle_subscription_request(const SubscriptionRequest& request if (!subscription_manager_->is_dapp_registered(request.dapp_identifier)) { E3_LOG_ERROR(LOG_TAG) << "dApp " << request.dapp_identifier << " not registered"; } else { + uint32_t period = request.periodicity.value_or(0); auto [result, sub_id] = subscription_manager_->add_subscription( request.dapp_identifier, - request.ran_function_identifier + request.ran_function_identifier, + request.telemetry_identifier_list, + request.control_identifier_list, + period ); subscription_id = sub_id; @@ -681,13 +685,17 @@ void E3Interface::handle_subscription_delete(const SubscriptionDelete& del, uint << error_code_to_string(result); } - // Create and queue ack response - Pdu response_pdu(PduType::MESSAGE_ACK); - MessageAck ack; + // Create and queue response + Pdu response_pdu(PduType::SUBSCRIPTION_RESPONSE); + SubscriptionResponse resp; response_pdu.message_id = generate_message_id(); - ack.request_id = request_message_id; - ack.response_code = response_code; - response_pdu.choice = ack; + resp.request_id = request_message_id; + resp.dapp_identifier = del.dapp_identifier; + resp.response_code = response_code; + if (response_code == ResponseCode::POSITIVE) { + resp.subscription_id = del.subscription_id; + } + response_pdu.choice = resp; queue_outbound(std::move(response_pdu)); } diff --git a/src/core/subscription_manager.cpp b/src/core/subscription_manager.cpp index 7aa1616..5ee9117 100644 --- a/src/core/subscription_manager.cpp +++ b/src/core/subscription_manager.cpp @@ -109,13 +109,14 @@ ErrorCode SubscriptionManager::unregister_dapp(uint32_t dapp_id) { affected_ran_functions.emplace_back(ran_func, false); // should_start = false } - // Remove subscription ID mapping + // Remove subscription ID mapping and details uint64_t sub_key = make_sub_key(dapp_id, ran_func); auto sub_id_it = subscription_id_reverse_.find(sub_key); if (sub_id_it != subscription_id_reverse_.end()) { subscription_ids_.erase(sub_id_it->second); subscription_id_reverse_.erase(sub_id_it); } + subscription_details_.erase(sub_key); } dapp_subscriptions_.erase(sub_it); } @@ -155,7 +156,12 @@ std::vector SubscriptionManager::get_registered_dapps() const { // Subscription Management // ========================================================================= -std::pair SubscriptionManager::add_subscription(uint32_t dapp_id, uint32_t ran_function_id) { +std::pair SubscriptionManager::add_subscription( + uint32_t dapp_id, + uint32_t ran_function_id, + const std::vector& telemetry_ids, + const std::vector& control_ids, + uint32_t periodicity_us) { std::unique_lock lock(mutex_); // Check if dApp is registered @@ -190,11 +196,22 @@ std::pair SubscriptionManager::add_subscription(uint32_t da // Add subscription ID mappings subscription_ids_[subscription_id] = {dapp_id, ran_function_id}; - subscription_id_reverse_[make_sub_key(dapp_id, ran_function_id)] = subscription_id; + uint64_t sub_key = make_sub_key(dapp_id, ran_function_id); + subscription_id_reverse_[sub_key] = subscription_id; + + // Store per-subscription metadata + SubscriptionDetails details; + details.telemetry_ids = telemetry_ids; + details.control_ids = control_ids; + details.periodicity_us = periodicity_us; + subscription_details_[sub_key] = std::move(details); E3_LOG_INFO(LOG_TAG) << "Subscription added: dApp " << dapp_id << " -> RAN function " << ran_function_id - << " (subscription_id=" << subscription_id << ")"; + << " (subscription_id=" << subscription_id + << ", periodicity_us=" << periodicity_us + << ", telemetry_ids=" << telemetry_ids.size() + << ", control_ids=" << control_ids.size() << ")"; // Notify about SM lifecycle change if this is the first subscriber lock.unlock(); @@ -226,13 +243,14 @@ ErrorCode SubscriptionManager::remove_subscription(uint32_t dapp_id, uint32_t ra subscribers.erase(dapp_id); bool still_has_subscribers = !subscribers.empty(); - // Remove subscription ID mappings + // Remove subscription ID mappings and details uint64_t sub_key = make_sub_key(dapp_id, ran_function_id); auto sub_id_it = subscription_id_reverse_.find(sub_key); if (sub_id_it != subscription_id_reverse_.end()) { subscription_ids_.erase(sub_id_it->second); subscription_id_reverse_.erase(sub_id_it); } + subscription_details_.erase(sub_key); E3_LOG_INFO(LOG_TAG) << "Subscription removed: dApp " << dapp_id << " -> RAN function " << ran_function_id; @@ -277,10 +295,11 @@ ErrorCode SubscriptionManager::remove_subscription_by_id(uint32_t dapp_id, uint3 subscribers.erase(dapp_id); bool still_has_subscribers = !subscribers.empty(); - // Remove subscription ID mappings + // Remove subscription ID mappings and details uint64_t sub_key = make_sub_key(dapp_id, ran_function_id); subscription_id_reverse_.erase(sub_key); subscription_ids_.erase(sub_it); + subscription_details_.erase(sub_key); E3_LOG_INFO(LOG_TAG) << "Subscription " << subscription_id << " removed: dApp " << dapp_id << " -> RAN function " << ran_function_id; @@ -324,6 +343,17 @@ std::vector SubscriptionManager::get_subscribed_dapps(uint32_t ran_fun return std::vector(it->second.begin(), it->second.end()); } +const SubscriptionDetails* SubscriptionManager::get_subscription_details( + uint32_t dapp_id, uint32_t ran_function_id) const { + std::shared_lock lock(mutex_); + + auto it = subscription_details_.find(make_sub_key(dapp_id, ran_function_id)); + if (it == subscription_details_.end()) { + return nullptr; + } + return &it->second; +} + size_t SubscriptionManager::get_subscriber_count(uint32_t ran_function_id) const { std::shared_lock lock(mutex_); @@ -381,6 +411,9 @@ void SubscriptionManager::clear() { registered_dapps_.clear(); dapp_subscriptions_.clear(); ran_function_subscribers_.clear(); + subscription_ids_.clear(); + subscription_id_reverse_.clear(); + subscription_details_.clear(); E3_LOG_INFO(LOG_TAG) << "All registrations and subscriptions cleared"; } diff --git a/src/encoder/json_encoder.cpp b/src/encoder/json_encoder.cpp index 96d9c17..7ae6eb0 100644 --- a/src/encoder/json_encoder.cpp +++ b/src/encoder/json_encoder.cpp @@ -16,6 +16,12 @@ namespace { constexpr const char* LOG_TAG = "JsonEnc"; +std::string to_camel_case(const char* pascal) { + std::string s(pascal); + if (!s.empty()) s[0] = static_cast(std::tolower(static_cast(s[0]))); + return s; +} + } // anonymous namespace @@ -23,19 +29,19 @@ constexpr const char* LOG_TAG = "JsonEnc"; // Helper methods for type conversions // ============================================================================ -PduType JsonE3Encoder::string_to_pdu_type(const std::string& s) const { - if (s == "SetupRequest") return PduType::SETUP_REQUEST; - if (s == "SetupResponse") return PduType::SETUP_RESPONSE; - if (s == "SubscriptionRequest") return PduType::SUBSCRIPTION_REQUEST; - if (s == "SubscriptionDelete") return PduType::SUBSCRIPTION_DELETE; - if (s == "SubscriptionResponse") return PduType::SUBSCRIPTION_RESPONSE; - if (s == "IndicationMessage") return PduType::INDICATION_MESSAGE; - if (s == "DAppControlAction") return PduType::DAPP_CONTROL_ACTION; - if (s == "DAppReport") return PduType::DAPP_REPORT; - if (s == "XAppControlAction") return PduType::XAPP_CONTROL_ACTION; - if (s == "ReleaseMessage") return PduType::RELEASE_MESSAGE; - if (s == "MessageAck") return PduType::MESSAGE_ACK; - return PduType::SETUP_REQUEST; // Default +std::optional JsonE3Encoder::string_to_pdu_type(const std::string& s) const { + if (s == "setupRequest") return PduType::SETUP_REQUEST; + if (s == "setupResponse") return PduType::SETUP_RESPONSE; + if (s == "subscriptionRequest") return PduType::SUBSCRIPTION_REQUEST; + if (s == "subscriptionDelete") return PduType::SUBSCRIPTION_DELETE; + if (s == "subscriptionResponse") return PduType::SUBSCRIPTION_RESPONSE; + if (s == "indicationMessage") return PduType::INDICATION_MESSAGE; + if (s == "dAppControlAction") return PduType::DAPP_CONTROL_ACTION; + if (s == "dAppReport") return PduType::DAPP_REPORT; + if (s == "xAppControlAction") return PduType::XAPP_CONTROL_ACTION; + if (s == "releaseMessage") return PduType::RELEASE_MESSAGE; + if (s == "messageAck") return PduType::MESSAGE_ACK; + return std::nullopt; } ErrorCode JsonE3Encoder::string_to_error_code(const std::string& s) const { @@ -74,9 +80,9 @@ std::vector JsonE3Encoder::hex_to_binary(const std::string& hex) { nlohmann::json JsonE3Encoder::encode_setup_request(const SetupRequest& req) const { nlohmann::json j; - j["e3ap_protocol_version"] = req.e3ap_protocol_version; - j["dapp_name"] = req.dapp_name; - j["dapp_version"] = req.dapp_version; + j["e3apProtocolVersion"] = req.e3ap_protocol_version; + j["dAppName"] = req.dapp_name; + j["dAppVersion"] = req.dapp_version; j["vendor"] = req.vendor; return j; @@ -84,97 +90,100 @@ nlohmann::json JsonE3Encoder::encode_setup_request(const SetupRequest& req) cons nlohmann::json JsonE3Encoder::encode_setup_response(const SetupResponse& resp) const { nlohmann::json j; - j["request_id"] = resp.request_id; - j["response_code"] = (resp.response_code == ResponseCode::POSITIVE) ? "positive" : "negative"; + j["requestId"] = resp.request_id; + j["responseCode"] = (resp.response_code == ResponseCode::POSITIVE) ? "positive" : "negative"; if (resp.e3ap_protocol_version.has_value()) { - j["e3ap_protocol_version"] = resp.e3ap_protocol_version.value(); + j["e3apProtocolVersion"] = resp.e3ap_protocol_version.value(); } if (resp.dapp_identifier.has_value()) { - j["dapp_identifier"] = resp.dapp_identifier.value(); + j["dAppIdentifier"] = resp.dapp_identifier.value(); } - j["ran_identifier"] = resp.ran_identifier; + j["ranIdentifier"] = resp.ran_identifier; if (!resp.ran_function_list.empty()) { nlohmann::json ran_funcs = nlohmann::json::array(); for (const auto& func : resp.ran_function_list) { nlohmann::json func_obj; - func_obj["ran_function_identifier"] = func.ran_function_identifier; - func_obj["telemetry_identifier_list"] = func.telemetry_identifier_list; - func_obj["control_identifier_list"] = func.control_identifier_list; - func_obj["ran_function_data"] = binary_to_hex(func.ran_function_data); + func_obj["ranFunctionIdentifier"] = func.ran_function_identifier; + func_obj["telemetryIdentifierList"] = func.telemetry_identifier_list; + func_obj["controlIdentifierList"] = func.control_identifier_list; + func_obj["ranFunctionData"] = binary_to_hex(func.ran_function_data); ran_funcs.push_back(func_obj); } - j["ran_function_list"] = ran_funcs; + j["ranFunctionList"] = ran_funcs; } return j; } nlohmann::json JsonE3Encoder::encode_subscription_request(const SubscriptionRequest& req) const { nlohmann::json j; - j["dapp_identifier"] = req.dapp_identifier; - j["ran_function_identifier"] = req.ran_function_identifier; - j["telemetry_identifier_list"] = req.telemetry_identifier_list; - j["control_identifier_list"] = req.control_identifier_list; + j["dAppIdentifier"] = req.dapp_identifier; + j["ranFunctionIdentifier"] = req.ran_function_identifier; + j["telemetryIdentifierList"] = req.telemetry_identifier_list; + j["controlIdentifierList"] = req.control_identifier_list; if (req.subscription_time.has_value()) { - j["subscription_time"] = req.subscription_time.value(); + j["subscriptionTime"] = req.subscription_time.value(); + } + if (req.periodicity.has_value()) { + j["periodicity"] = req.periodicity.value(); } return j; } nlohmann::json JsonE3Encoder::encode_subscription_delete(const SubscriptionDelete& del) const { nlohmann::json j; - j["dapp_identifier"] = del.dapp_identifier; - j["subscription_id"] = del.subscription_id; + j["dAppIdentifier"] = del.dapp_identifier; + j["subscriptionId"] = del.subscription_id; return j; } nlohmann::json JsonE3Encoder::encode_subscription_response(const SubscriptionResponse& resp) const { nlohmann::json j; - j["request_id"] = resp.request_id; - j["dapp_identifier"] = resp.dapp_identifier; - j["response_code"] = (resp.response_code == ResponseCode::POSITIVE) ? "positive" : "negative"; + j["requestId"] = resp.request_id; + j["dAppIdentifier"] = resp.dapp_identifier; + j["responseCode"] = (resp.response_code == ResponseCode::POSITIVE) ? "positive" : "negative"; if (resp.subscription_id.has_value()) { - j["subscription_id"] = resp.subscription_id.value(); + j["subscriptionId"] = resp.subscription_id.value(); } return j; } nlohmann::json JsonE3Encoder::encode_indication_message(const IndicationMessage& msg) const { nlohmann::json j; - j["dapp_identifier"] = msg.dapp_identifier; - j["ran_function_identifier"] = msg.ran_function_identifier; - j["protocol_data"] = binary_to_hex(msg.protocol_data); + j["dAppIdentifier"] = msg.dapp_identifier; + j["ranFunctionIdentifier"] = msg.ran_function_identifier; + j["protocolData"] = nlohmann::json::parse(msg.protocol_data); return j; } nlohmann::json JsonE3Encoder::encode_dapp_control_action(const DAppControlAction& action) const { nlohmann::json j; - j["dapp_identifier"] = action.dapp_identifier; - j["ran_function_identifier"] = action.ran_function_identifier; - j["control_identifier"] = action.control_identifier; - j["action_data"] = binary_to_hex(action.action_data); + j["dAppIdentifier"] = action.dapp_identifier; + j["ranFunctionIdentifier"] = action.ran_function_identifier; + j["controlIdentifier"] = action.control_identifier; + j["actionData"] = binary_to_hex(action.action_data); return j; } nlohmann::json JsonE3Encoder::encode_dapp_report(const DAppReport& report) const { nlohmann::json j; - j["dapp_identifier"] = report.dapp_identifier; - j["ran_function_identifier"] = report.ran_function_identifier; - j["report_data"] = binary_to_hex(report.report_data); + j["dAppIdentifier"] = report.dapp_identifier; + j["ranFunctionIdentifier"] = report.ran_function_identifier; + j["reportData"] = binary_to_hex(report.report_data); return j; } nlohmann::json JsonE3Encoder::encode_xapp_control_action(const XAppControlAction& action) const { nlohmann::json j; - j["dapp_identifier"] = action.dapp_identifier; - j["ran_function_identifier"] = action.ran_function_identifier; - j["xapp_control_data"] = binary_to_hex(action.xapp_control_data); + j["dAppIdentifier"] = action.dapp_identifier; + j["ranFunctionIdentifier"] = action.ran_function_identifier; + j["xAppControlData"] = binary_to_hex(action.xapp_control_data); return j; } nlohmann::json JsonE3Encoder::encode_message_ack(const MessageAck& ack) const { nlohmann::json j; - j["request_id"] = ack.request_id; - j["response_code"] = (ack.response_code == ResponseCode::POSITIVE) ? "positive" : "negative"; + j["requestId"] = ack.request_id; + j["responseCode"] = (ack.response_code == ResponseCode::POSITIVE) ? "positive" : "negative"; return j; } @@ -184,9 +193,9 @@ nlohmann::json JsonE3Encoder::encode_message_ack(const MessageAck& ack) const { SetupRequest JsonE3Encoder::decode_setup_request(const nlohmann::json& j) const { SetupRequest req; - req.e3ap_protocol_version = j.value("e3ap_protocol_version", ""); - req.dapp_name = j.value("dapp_name", ""); - req.dapp_version = j.value("dapp_version", ""); + req.e3ap_protocol_version = j.value("e3apProtocolVersion", ""); + req.dapp_name = j.value("dAppName", ""); + req.dapp_version = j.value("dAppVersion", ""); req.vendor = j.value("vendor", ""); return req; @@ -194,25 +203,25 @@ SetupRequest JsonE3Encoder::decode_setup_request(const nlohmann::json& j) const SetupResponse JsonE3Encoder::decode_setup_response(const nlohmann::json& j) const { SetupResponse resp; - resp.request_id = j.value("request_id", 0u); + resp.request_id = j.value("requestId", 0u); - std::string response_code_str = j.value("response_code", "negative"); + std::string response_code_str = j.value("responseCode", "negative"); resp.response_code = (response_code_str == "positive") ? ResponseCode::POSITIVE : ResponseCode::NEGATIVE; - if (j.contains("e3ap_protocol_version")) { - resp.e3ap_protocol_version = j["e3ap_protocol_version"].get(); + if (j.contains("e3apProtocolVersion")) { + resp.e3ap_protocol_version = j["e3apProtocolVersion"].get(); } - if (j.contains("dapp_identifier")) { - resp.dapp_identifier = j["dapp_identifier"].get(); + if (j.contains("dAppIdentifier")) { + resp.dapp_identifier = j["dAppIdentifier"].get(); } - resp.ran_identifier = j.value("ran_identifier", ""); - if (j.contains("ran_function_list")) { - for (const auto& func_obj : j["ran_function_list"]) { + resp.ran_identifier = j.value("ranIdentifier", ""); + if (j.contains("ranFunctionList")) { + for (const auto& func_obj : j["ranFunctionList"]) { RanFunctionDef func; - func.ran_function_identifier = func_obj.value("ran_function_identifier", 0u); - func.telemetry_identifier_list = func_obj.value("telemetry_identifier_list", std::vector{}); - func.control_identifier_list = func_obj.value("control_identifier_list", std::vector{}); - func.ran_function_data = hex_to_binary(func_obj.value("ran_function_data", "")); + func.ran_function_identifier = func_obj.value("ranFunctionIdentifier", 0u); + func.telemetry_identifier_list = func_obj.value("telemetryIdentifierList", std::vector{}); + func.control_identifier_list = func_obj.value("controlIdentifierList", std::vector{}); + func.ran_function_data = hex_to_binary(func_obj.value("ranFunctionData", "")); resp.ran_function_list.push_back(func); } } @@ -221,72 +230,76 @@ SetupResponse JsonE3Encoder::decode_setup_response(const nlohmann::json& j) cons SubscriptionRequest JsonE3Encoder::decode_subscription_request(const nlohmann::json& j) const { SubscriptionRequest req; - req.dapp_identifier = j.value("dapp_identifier", 0u); - req.ran_function_identifier = j.value("ran_function_identifier", 0u); - req.telemetry_identifier_list = j.value("telemetry_identifier_list", std::vector{}); - req.control_identifier_list = j.value("control_identifier_list", std::vector{}); - if (j.contains("subscription_time")) { - req.subscription_time = j["subscription_time"].get(); + req.dapp_identifier = j.value("dAppIdentifier", 0u); + req.ran_function_identifier = j.value("ranFunctionIdentifier", 0u); + req.telemetry_identifier_list = j.value("telemetryIdentifierList", std::vector{}); + req.control_identifier_list = j.value("controlIdentifierList", std::vector{}); + if (j.contains("subscriptionTime")) { + req.subscription_time = j["subscriptionTime"].get(); + } + if (j.contains("periodicity")) { + req.periodicity = j["periodicity"].get(); } return req; } SubscriptionDelete JsonE3Encoder::decode_subscription_delete(const nlohmann::json& j) const { SubscriptionDelete del; - del.dapp_identifier = j.value("dapp_identifier", 0u); - del.subscription_id = j.value("subscription_id", 0u); + del.dapp_identifier = j.value("dAppIdentifier", 0u); + del.subscription_id = j.value("subscriptionId", 0u); return del; } SubscriptionResponse JsonE3Encoder::decode_subscription_response(const nlohmann::json& j) const { SubscriptionResponse resp; - resp.request_id = j.value("request_id", 0u); - resp.dapp_identifier = j.value("dapp_identifier", 0u); - std::string response_code_str = j.value("response_code", "negative"); + resp.request_id = j.value("requestId", 0u); + resp.dapp_identifier = j.value("dAppIdentifier", 0u); + std::string response_code_str = j.value("responseCode", "negative"); resp.response_code = (response_code_str == "positive") ? ResponseCode::POSITIVE : ResponseCode::NEGATIVE; - if (j.contains("subscription_id")) { - resp.subscription_id = j["subscription_id"].get(); + if (j.contains("subscriptionId")) { + resp.subscription_id = j["subscriptionId"].get(); } return resp; } IndicationMessage JsonE3Encoder::decode_indication_message(const nlohmann::json& j) const { IndicationMessage msg; - msg.dapp_identifier = j.value("dapp_identifier", 0u); - msg.ran_function_identifier = j.value("ran_function_identifier", 0u); - msg.protocol_data = hex_to_binary(j.value("protocol_data", "")); + msg.dapp_identifier = j.value("dAppIdentifier", 0u); + msg.ran_function_identifier = j.value("ranFunctionIdentifier", 0u); + std::string dumped = j["protocolData"].dump(); + msg.protocol_data.assign(dumped.begin(), dumped.end()); return msg; } DAppControlAction JsonE3Encoder::decode_dapp_control_action(const nlohmann::json& j) const { DAppControlAction action; - action.dapp_identifier = j.value("dapp_identifier", 0u); - action.ran_function_identifier = j.value("ran_function_identifier", 0u); - action.control_identifier = j.value("control_identifier", 0u); - action.action_data = hex_to_binary(j.value("action_data", "")); + action.dapp_identifier = j.value("dAppIdentifier", 0u); + action.ran_function_identifier = j.value("ranFunctionIdentifier", 0u); + action.control_identifier = j.value("controlIdentifier", 0u); + action.action_data = hex_to_binary(j.value("actionData", "")); return action; } DAppReport JsonE3Encoder::decode_dapp_report(const nlohmann::json& j) const { DAppReport report; - report.dapp_identifier = j.value("dapp_identifier", 0u); - report.ran_function_identifier = j.value("ran_function_identifier", 0u); - report.report_data = hex_to_binary(j.value("report_data", "")); + report.dapp_identifier = j.value("dAppIdentifier", 0u); + report.ran_function_identifier = j.value("ranFunctionIdentifier", 0u); + report.report_data = hex_to_binary(j.value("reportData", "")); return report; } XAppControlAction JsonE3Encoder::decode_xapp_control_action(const nlohmann::json& j) const { XAppControlAction action; - action.dapp_identifier = j.value("dapp_identifier", 0u); - action.ran_function_identifier = j.value("ran_function_identifier", 0u); - action.xapp_control_data = hex_to_binary(j.value("xapp_control_data", "")); + action.dapp_identifier = j.value("dAppIdentifier", 0u); + action.ran_function_identifier = j.value("ranFunctionIdentifier", 0u); + action.xapp_control_data = hex_to_binary(j.value("xAppControlData", "")); return action; } MessageAck JsonE3Encoder::decode_message_ack(const nlohmann::json& j) const { MessageAck ack; - ack.request_id = j.value("request_id", 0u); - std::string response_code_str = j.value("response_code", "negative"); + ack.request_id = j.value("requestId", 0u); + std::string response_code_str = j.value("responseCode", "negative"); ack.response_code = (response_code_str == "positive") ? ResponseCode::POSITIVE : ResponseCode::NEGATIVE; return ack; } @@ -294,13 +307,13 @@ MessageAck JsonE3Encoder::decode_message_ack(const nlohmann::json& j) const { // ReleaseMessage encode/decode nlohmann::json JsonE3Encoder::encode_release_message(const ReleaseMessage& msg) const { nlohmann::json j; - j["dapp_identifier"] = msg.dapp_identifier; + j["dAppIdentifier"] = msg.dapp_identifier; return j; } ReleaseMessage JsonE3Encoder::decode_release_message(const nlohmann::json& j) const { ReleaseMessage msg; - msg.dapp_identifier = j.value("dapp_identifier", 0u); + msg.dapp_identifier = j.value("dAppIdentifier", 0u); return msg; } @@ -311,52 +324,51 @@ ReleaseMessage JsonE3Encoder::decode_release_message(const nlohmann::json& j) co EncodeResult JsonE3Encoder::encode(const Pdu& pdu) { try { nlohmann::json root; - root["pdu_type"] = pdu_type_to_string(pdu.type); - root["message_id"] = pdu.message_id; + root["type"] = to_camel_case(pdu_type_to_string(pdu.type)); + root["id"] = pdu.message_id; root["timestamp"] = pdu.timestamp; - // Encode the data based on PDU type - nlohmann::json data; - std::visit([this, &data](auto&& arg) { + // Encode payload fields directly into root (flat format) + std::visit([this, &root](auto&& arg) { using T = std::decay_t; + nlohmann::json fields; if constexpr (std::is_same_v) { - data = encode_setup_request(arg); + fields = encode_setup_request(arg); } else if constexpr (std::is_same_v) { - data = encode_setup_response(arg); + fields = encode_setup_response(arg); } else if constexpr (std::is_same_v) { - data = encode_subscription_request(arg); + fields = encode_subscription_request(arg); } else if constexpr (std::is_same_v) { - data = encode_subscription_delete(arg); + fields = encode_subscription_delete(arg); } else if constexpr (std::is_same_v) { - data = encode_subscription_response(arg); + fields = encode_subscription_response(arg); } else if constexpr (std::is_same_v) { - data = encode_indication_message(arg); + fields = encode_indication_message(arg); } else if constexpr (std::is_same_v) { - data = encode_dapp_control_action(arg); + fields = encode_dapp_control_action(arg); } else if constexpr (std::is_same_v) { - data = encode_dapp_report(arg); + fields = encode_dapp_report(arg); } else if constexpr (std::is_same_v) { - data = encode_xapp_control_action(arg); + fields = encode_xapp_control_action(arg); } else if constexpr (std::is_same_v) { - data = encode_release_message(arg); + fields = encode_release_message(arg); } else if constexpr (std::is_same_v) { - data = encode_message_ack(arg); + fields = encode_message_ack(arg); } + root.update(fields); }, pdu.choice); - root["data"] = data; - std::string json_str = root.dump(); EncodedMessage msg; msg.buffer.assign(json_str.begin(), json_str.end()); @@ -393,53 +405,55 @@ EncodeResult JsonE3Encoder::decode(const uint8_t* data, size_t size) { Pdu pdu; // Get PDU type - std::string pdu_type_str = root.value("pdu_type", ""); - pdu.type = string_to_pdu_type(pdu_type_str); - pdu.message_id = root.value("message_id", 0u); + std::string pdu_type_str = root.value("type", ""); + auto pdu_type = string_to_pdu_type(pdu_type_str); + if (!pdu_type) { + E3_LOG_ERROR(LOG_TAG) << "Unrecognized PDU type: " << pdu_type_str; + return tl::unexpected(ErrorCode::DECODE_FAILED); + } + pdu.type = *pdu_type; + pdu.message_id = root.value("id", 0u); pdu.timestamp = root.value("timestamp", 0ull); - // Get data object - if (!root.contains("data")) { - E3_LOG_ERROR(LOG_TAG) << "Missing 'data' field in JSON"; + if (root.contains("data")) { + E3_LOG_ERROR(LOG_TAG) << "Nested \"data\" wrapper is not supported; use flat format"; return tl::unexpected(ErrorCode::DECODE_FAILED); } - const nlohmann::json& j = root["data"]; - // Decode based on PDU type switch (pdu.type) { case PduType::SETUP_REQUEST: - pdu.choice = decode_setup_request(j); + pdu.choice = decode_setup_request(root); break; case PduType::SETUP_RESPONSE: - pdu.choice = decode_setup_response(j); + pdu.choice = decode_setup_response(root); break; case PduType::SUBSCRIPTION_REQUEST: - pdu.choice = decode_subscription_request(j); + pdu.choice = decode_subscription_request(root); break; case PduType::SUBSCRIPTION_DELETE: - pdu.choice = decode_subscription_delete(j); + pdu.choice = decode_subscription_delete(root); break; case PduType::SUBSCRIPTION_RESPONSE: - pdu.choice = decode_subscription_response(j); + pdu.choice = decode_subscription_response(root); break; case PduType::INDICATION_MESSAGE: - pdu.choice = decode_indication_message(j); + pdu.choice = decode_indication_message(root); break; case PduType::DAPP_CONTROL_ACTION: - pdu.choice = decode_dapp_control_action(j); + pdu.choice = decode_dapp_control_action(root); break; case PduType::DAPP_REPORT: - pdu.choice = decode_dapp_report(j); + pdu.choice = decode_dapp_report(root); break; case PduType::XAPP_CONTROL_ACTION: - pdu.choice = decode_xapp_control_action(j); + pdu.choice = decode_xapp_control_action(root); break; case PduType::RELEASE_MESSAGE: - pdu.choice = decode_release_message(j); + pdu.choice = decode_release_message(root); break; case PduType::MESSAGE_ACK: - pdu.choice = decode_message_ack(j); + pdu.choice = decode_message_ack(root); break; default: E3_LOG_ERROR(LOG_TAG) << "Unknown PDU type: " << pdu_type_str; diff --git a/src/encoder/json_encoder.hpp b/src/encoder/json_encoder.hpp index 0b3233a..73a12a8 100644 --- a/src/encoder/json_encoder.hpp +++ b/src/encoder/json_encoder.hpp @@ -17,6 +17,13 @@ namespace libe3 { * @brief JSON encoder for E3AP PDUs * * Provides JSON encoding/decoding for development and debugging. + * + * JSON wire format uses camelCase keys (e.g. "dAppName", "ranFunctionIdentifier") + * and camelCase PDU type strings (e.g. "setupRequest", "indicationMessage"). + * PascalCase PDU types are rejected by the decoder. + * + * Envelope format is flat: payload fields sit alongside "type", "id", and + * "timestamp" at root level. Messages with a "data" wrapper are rejected. */ class JsonE3Encoder : public E3Encoder { public: @@ -60,7 +67,7 @@ class JsonE3Encoder : public E3Encoder { static std::vector hex_to_binary(const std::string& hex); // Helper methods for type conversions - PduType string_to_pdu_type(const std::string& s) const; + std::optional string_to_pdu_type(const std::string& s) const; ErrorCode string_to_error_code(const std::string& s) const; }; diff --git a/tests/test_json_encoder.cpp b/tests/test_json_encoder.cpp index c657480..2452f52 100644 --- a/tests/test_json_encoder.cpp +++ b/tests/test_json_encoder.cpp @@ -8,6 +8,7 @@ #include "test_framework.hpp" #include "libe3/e3_encoder.hpp" #include "libe3/types.hpp" +#include using namespace libe3; @@ -34,7 +35,7 @@ TEST(JsonEncoder_encode_setup_request) { // Verify JSON contains expected fields std::string json(encoded->buffer.begin(), encoded->buffer.end()); - ASSERT_TRUE(json.find("SetupRequest") != std::string::npos); + ASSERT_TRUE(json.find("setupRequest") != std::string::npos); ASSERT_TRUE(json.find("TestDApp") != std::string::npos); ASSERT_TRUE(json.find("TestVendor") != std::string::npos); } @@ -78,7 +79,7 @@ TEST(JsonEncoder_encode_setup_response) { ASSERT_TRUE(encoded.has_value()); std::string json(encoded->buffer.begin(), encoded->buffer.end()); - ASSERT_TRUE(json.find("SetupResponse") != std::string::npos); + ASSERT_TRUE(json.find("setupResponse") != std::string::npos); } TEST(JsonEncoder_encode_decode_subscription_request) { @@ -150,7 +151,8 @@ TEST(JsonEncoder_encode_decode_indication_message) { IndicationMessage msg; msg.dapp_identifier = 77; msg.ran_function_identifier = 55; - msg.protocol_data = {0x01, 0x02, 0x03, 0x04, 0xAB, 0xCD}; + std::string payload = R"({"rnti":42069,"mcs":9,"snr":12.5})"; + msg.protocol_data.assign(payload.begin(), payload.end()); original.choice = msg; auto encoded = encoder->encode(original); @@ -162,8 +164,9 @@ TEST(JsonEncoder_encode_decode_indication_message) { auto& restored = std::get((*decoded).choice); ASSERT_EQ(restored.dapp_identifier, 77u); ASSERT_EQ(restored.ran_function_identifier, 55u); - ASSERT_EQ(restored.protocol_data.size(), 6u); - ASSERT_EQ(restored.protocol_data[4], 0xAB); + auto restored_json = nlohmann::json::parse(restored.protocol_data); + ASSERT_EQ(restored_json["rnti"].get(), 42069); + ASSERT_EQ(restored_json["mcs"].get(), 9); } TEST(JsonEncoder_encode_decode_control_action) { @@ -260,11 +263,12 @@ TEST(JsonEncoder_roundtrip_large_data) { Pdu original(PduType::INDICATION_MESSAGE); IndicationMessage msg; msg.dapp_identifier = 1; - // 1KB of data - msg.protocol_data.resize(1024); - for (size_t i = 0; i < msg.protocol_data.size(); ++i) { - msg.protocol_data[i] = static_cast(i & 0xFF); + nlohmann::json large; + for (int i = 0; i < 256; ++i) { + large["k" + std::to_string(i)] = i; } + std::string payload = large.dump(); + msg.protocol_data.assign(payload.begin(), payload.end()); original.choice = msg; auto encoded = encoder->encode(original); @@ -274,10 +278,47 @@ TEST(JsonEncoder_roundtrip_large_data) { ASSERT_TRUE(decoded.has_value()); auto& restored = std::get((*decoded).choice); - ASSERT_EQ(restored.protocol_data.size(), 1024u); - for (size_t i = 0; i < restored.protocol_data.size(); ++i) { - ASSERT_EQ(restored.protocol_data[i], static_cast(i & 0xFF)); - } + auto restored_json = nlohmann::json::parse(restored.protocol_data); + ASSERT_EQ(restored_json.size(), 256u); + ASSERT_EQ(restored_json["k0"].get(), 0); + ASSERT_EQ(restored_json["k255"].get(), 255); +} + +TEST(JsonEncoder_reject_nested_data_wrapper) { + auto encoder = create_encoder(); + + std::string nested_json = R"({ + "type": "setupRequest", + "id": 99, + "timestamp": 0, + "data": { + "e3apProtocolVersion": "2.0.0", + "dAppName": "NestedDApp", + "dAppVersion": "3.0.0", + "vendor": "NestedVendor" + } + })"; + + std::vector buf(nested_json.begin(), nested_json.end()); + auto result = encoder->decode(buf.data(), buf.size()); + ASSERT_FALSE(result.has_value()); +} + +TEST(JsonEncoder_reject_pascal_case_pdu_type) { + auto encoder = create_encoder(); + + std::string pascal_json = R"({ + "type": "SetupRequest", + "id": 1, + "timestamp": 0, + "dAppName": "Test", + "dAppVersion": "1.0", + "vendor": "V" + })"; + + std::vector buf(pascal_json.begin(), pascal_json.end()); + auto result = encoder->decode(buf.data(), buf.size()); + ASSERT_FALSE(result.has_value()); } int main() { diff --git a/tests/test_subscription_manager.cpp b/tests/test_subscription_manager.cpp index 03e8b3c..b1be65f 100644 --- a/tests/test_subscription_manager.cpp +++ b/tests/test_subscription_manager.cpp @@ -278,6 +278,65 @@ TEST(SubscriptionManager_clear) { ASSERT_EQ(mgr.subscription_count(), 0u); } +TEST(SubscriptionManager_subscription_details_stored) { + SubscriptionManager mgr; + auto [reg_result, dapp_id] = mgr.register_dapp(); + + std::vector tids = {10, 20, 30}; + std::vector cids = {7, 8}; + auto [result, sub_id] = mgr.add_subscription(dapp_id, 200, tids, cids, 5000); + ASSERT_TRUE(result == ErrorCode::SUCCESS); + + const auto* details = mgr.get_subscription_details(dapp_id, 200); + ASSERT_TRUE(details != nullptr); + ASSERT_EQ(details->periodicity_us, 5000u); + ASSERT_EQ(details->telemetry_ids.size(), 3u); + ASSERT_EQ(details->telemetry_ids[0], 10u); + ASSERT_EQ(details->telemetry_ids[1], 20u); + ASSERT_EQ(details->telemetry_ids[2], 30u); + ASSERT_EQ(details->control_ids.size(), 2u); + ASSERT_EQ(details->control_ids[0], 7u); + ASSERT_EQ(details->control_ids[1], 8u); +} + +TEST(SubscriptionManager_subscription_details_defaults) { + SubscriptionManager mgr; + auto [reg_result, dapp_id] = mgr.register_dapp(); + + auto [result, sub_id] = mgr.add_subscription(dapp_id, 200); + ASSERT_TRUE(result == ErrorCode::SUCCESS); + + const auto* details = mgr.get_subscription_details(dapp_id, 200); + ASSERT_TRUE(details != nullptr); + ASSERT_EQ(details->periodicity_us, 0u); + ASSERT_TRUE(details->telemetry_ids.empty()); + ASSERT_TRUE(details->control_ids.empty()); +} + +TEST(SubscriptionManager_subscription_details_removed_on_delete) { + SubscriptionManager mgr; + auto [reg_result, dapp_id] = mgr.register_dapp(); + + std::vector tids = {1}; + mgr.add_subscription(dapp_id, 200, tids, {}, 1000); + mgr.remove_subscription(dapp_id, 200); + + const auto* details = mgr.get_subscription_details(dapp_id, 200); + ASSERT_TRUE(details == nullptr); +} + +TEST(SubscriptionManager_subscription_details_removed_on_unregister) { + SubscriptionManager mgr; + auto [reg_result, dapp_id] = mgr.register_dapp(); + + std::vector tids = {5, 6}; + mgr.add_subscription(dapp_id, 300, tids, {}, 2000); + mgr.unregister_dapp(dapp_id); + + const auto* details = mgr.get_subscription_details(dapp_id, 300); + ASSERT_TRUE(details == nullptr); +} + int main() { return RUN_ALL_TESTS(); }