Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/ros2_medkit_gateway/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,10 @@ if(BUILD_TESTING)
ament_add_gtest(test_plugin_manager test/test_plugin_manager.cpp)
target_link_libraries(test_plugin_manager gateway_lib)

# Plugin entity routing tests
ament_add_gtest(test_plugin_entity_routing test/test_plugin_entity_routing.cpp)
target_link_libraries(test_plugin_entity_routing gateway_lib)

# Plugin HTTP types tests
ament_add_gtest(test_plugin_http_types test/test_plugin_http_types.cpp)
target_link_libraries(test_plugin_http_types gateway_lib)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ struct EntityInfo {
std::string peer_url; ///< Base URL of the peer (e.g., "http://localhost:8081")
std::string peer_name; ///< Peer name for metadata (e.g., "subsystem_b")

// Plugin routing fields
bool is_plugin{false}; ///< True if entity is owned by a plugin
std::string plugin_name; ///< Plugin name (empty if not plugin-owned)

/**
* @brief Get SovdEntityType equivalent
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class UpdateProvider;
class IntrospectionProvider;
class LogProvider;
class ScriptProvider;
class DataProvider;
class FaultProvider;
class OperationProvider;

/**
* @brief Result of loading a gateway plugin.
Expand Down Expand Up @@ -63,6 +66,16 @@ struct GatewayPluginLoadResult {
/// Lifetime tied to plugin - do not use after plugin is destroyed.
ScriptProvider * script_provider = nullptr;

/// Non-owning pointer to DataProvider interface (null if not provided).
/// Unlike LogProvider/ScriptProvider, multiple plugins can each provide data for different entities.
DataProvider * data_provider = nullptr;

/// Non-owning pointer to OperationProvider interface (null if not provided).
OperationProvider * operation_provider = nullptr;

/// Non-owning pointer to FaultProvider interface (null if not provided).
FaultProvider * fault_provider = nullptr;

/// Get the dlopen handle (for dlsym queries by PluginManager)
void * dl_handle() const {
return handle_;
Expand All @@ -85,6 +98,9 @@ struct GatewayPluginLoadResult {
* extern "C" IntrospectionProvider* get_introspection_provider(GatewayPlugin* plugin);
* extern "C" LogProvider* get_log_provider(GatewayPlugin* plugin);
* extern "C" ScriptProvider* get_script_provider(GatewayPlugin* plugin);
* extern "C" DataProvider* get_data_provider(GatewayPlugin* plugin);
* extern "C" OperationProvider* get_operation_provider(GatewayPlugin* plugin);
* extern "C" FaultProvider* get_fault_provider(GatewayPlugin* plugin);
*
* Path requirements: must be absolute, have .so extension, and resolve to a real file.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,22 @@
#include "ros2_medkit_gateway/plugins/plugin_context.hpp"
#include "ros2_medkit_gateway/plugins/plugin_loader.hpp"
#include "ros2_medkit_gateway/plugins/plugin_types.hpp"
#include "ros2_medkit_gateway/providers/data_provider.hpp"
#include "ros2_medkit_gateway/providers/fault_provider.hpp"
#include "ros2_medkit_gateway/providers/introspection_provider.hpp"
#include "ros2_medkit_gateway/providers/log_provider.hpp"
#include "ros2_medkit_gateway/providers/operation_provider.hpp"
#include "ros2_medkit_gateway/providers/script_provider.hpp"
#include "ros2_medkit_gateway/providers/update_provider.hpp"
#include "ros2_medkit_gateway/resource_sampler.hpp"
#include "ros2_medkit_gateway/subscription_transport.hpp"

#include <memory>
#include <nlohmann/json.hpp>
#include <optional>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <vector>

namespace httplib {
Expand Down Expand Up @@ -165,6 +170,31 @@ class PluginManager {
return context_;
}

// ---- Entity ownership (per-entity provider routing) ----

/// Register entity ownership for a plugin.
/// Called after IntrospectionProvider::introspect() returns new entities.
/// Maps entity IDs to the plugin that created them, enabling per-entity
/// provider routing in handlers.
void register_entity_ownership(const std::string & plugin_name, const std::vector<std::string> & entity_ids);

/// Get DataProvider for a specific entity (if plugin-owned)
/// @return Non-owning pointer, or nullptr if entity is not plugin-owned
/// or owning plugin doesn't implement DataProvider
DataProvider * get_data_provider_for_entity(const std::string & entity_id) const;

/// Get OperationProvider for a specific entity (if plugin-owned)
/// @return Non-owning pointer, or nullptr if entity is not plugin-owned
/// or owning plugin doesn't implement OperationProvider
OperationProvider * get_operation_provider_for_entity(const std::string & entity_id) const;

/// Get FaultProvider for a specific entity (if plugin-owned)
FaultProvider * get_fault_provider_for_entity(const std::string & entity_id) const;

/// Check if an entity is owned by a plugin
/// @return Plugin name if owned, nullopt otherwise
std::optional<std::string> get_entity_owner(const std::string & entity_id) const;

// ---- Info ----
bool has_plugins() const;
std::vector<std::string> plugin_names() const;
Expand All @@ -179,6 +209,9 @@ class PluginManager {
IntrospectionProvider * introspection_provider = nullptr;
LogProvider * log_provider = nullptr;
ScriptProvider * script_provider = nullptr;
DataProvider * data_provider = nullptr;
OperationProvider * operation_provider = nullptr;
FaultProvider * fault_provider = nullptr;
};

/// Disable a plugin after a lifecycle error (nulls providers, resets plugin).
Expand All @@ -192,6 +225,8 @@ class PluginManager {
ScriptProvider * first_script_provider_ = nullptr;
ResourceSamplerRegistry * sampler_registry_ = nullptr;
TransportRegistry * transport_registry_ = nullptr;
/// Entity ID -> plugin name mapping (populated from IntrospectionProvider results)
std::unordered_map<std::string, std::string> entity_ownership_;
bool shutdown_called_ = false;
mutable std::shared_mutex plugins_mutex_;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2026 bburda
//
// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
//
// 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.

#pragma once

#include <nlohmann/json.hpp>
#include <string>
#include <tl/expected.hpp>

namespace ros2_medkit_gateway {

enum class DataProviderError {
EntityNotFound,
ResourceNotFound,
ReadOnly,
WriteOnly,
TransportError,
Timeout,
InvalidValue,
Internal
};

struct DataProviderErrorInfo {
DataProviderError code;
std::string message;
int http_status{500}; ///< Suggested HTTP status code
};

/**
* @brief Provider interface for entity data resources
*
* Typed provider interface for plugins that serve SOVD data resources
* (GET /{entity_type}/{id}/data, GET /{entity_type}/{id}/data/{name}).
* Unlike LogProvider/ScriptProvider (singletons), multiple DataProvider
* plugins can coexist - each handles its own set of entities.
*
* Entity ownership is determined by IntrospectionProvider: entities
* created by a plugin's introspect() are routed to that plugin's
* DataProvider.
*
* @par Thread safety
* All methods may be called from multiple HTTP handler threads concurrently.
* Implementations must provide their own synchronization.
*
* @see GatewayPlugin for the base class all plugins must also implement
* @see OperationProvider for the operations counterpart
*/
Comment on lines +40 to +58
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new provider interfaces are part of the public plugin API (including the required dlsym export names in plugin_loader). The PR currently doesn’t update any developer-facing docs describing how to implement/export DataProvider/OperationProvider (and expected JSON schemas / error mapping). Please add documentation in the appropriate docs section (e.g., docs/design or a plugin author guide) and reference it here.

Copilot uses AI. Check for mistakes.
class DataProvider {
public:
virtual ~DataProvider() = default;

/// List available data resources for an entity
/// @param entity_id SOVD entity ID (e.g., "openbsw_demo_ecu")
/// @return JSON with {"items": [...]} array of data resource descriptors
virtual tl::expected<nlohmann::json, DataProviderErrorInfo> list_data(const std::string & entity_id) = 0;

/// Read a specific data resource
/// @param entity_id SOVD entity ID
/// @param resource_name Data resource name (e.g., "hardcoded_data")
/// @return JSON response body for the data resource
virtual tl::expected<nlohmann::json, DataProviderErrorInfo> read_data(const std::string & entity_id,
const std::string & resource_name) = 0;

/// Write a data resource value
/// @param entity_id SOVD entity ID
/// @param resource_name Data resource name
/// @param value JSON value to write
/// @return JSON response body confirming the write
virtual tl::expected<nlohmann::json, DataProviderErrorInfo>
write_data(const std::string & entity_id, const std::string & resource_name, const nlohmann::json & value) = 0;
};

} // namespace ros2_medkit_gateway
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2026 bburda
//
// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
//
// 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.

#pragma once

#include <nlohmann/json.hpp>
#include <string>
#include <tl/expected.hpp>

namespace ros2_medkit_gateway {

enum class FaultProviderError { EntityNotFound, FaultNotFound, TransportError, Timeout, Internal };

struct FaultProviderErrorInfo {
FaultProviderError code;
std::string message;
int http_status{500}; ///< Suggested HTTP status code
};

/**
* @brief Provider interface for entity fault/DTC resources
*
* Typed provider interface for plugins that serve SOVD faults
* (GET /{entity_type}/{id}/faults, GET /{entity_type}/{id}/faults/{code}).
* Per-entity routing like DataProvider.
*
* Plugins implementing this interface query their backend (e.g., UDS
* ReadDTCInformation 0x19) on demand and return faults in SOVD format.
*
* @par Thread safety
* All methods may be called concurrently. Implementations must synchronize.
*
* @see GatewayPlugin for the base class all plugins must also implement
* @see DataProvider for the data counterpart
*/
class FaultProvider {
public:
virtual ~FaultProvider() = default;

/// List faults for an entity
/// @param entity_id SOVD entity ID
/// @return JSON with {"items": [...]} array of fault descriptors
virtual tl::expected<nlohmann::json, FaultProviderErrorInfo> list_faults(const std::string & entity_id) = 0;

/// Get a specific fault with environment data
/// @param entity_id SOVD entity ID
/// @param fault_code Fault code (e.g., DTC identifier)
/// @return JSON response with fault detail + environment data
virtual tl::expected<nlohmann::json, FaultProviderErrorInfo> get_fault(const std::string & entity_id,
const std::string & fault_code) = 0;

/// Clear a fault
/// @param entity_id SOVD entity ID
/// @param fault_code Fault code to clear
/// @return JSON response confirming the clear
virtual tl::expected<nlohmann::json, FaultProviderErrorInfo> clear_fault(const std::string & entity_id,
const std::string & fault_code) = 0;
};

} // namespace ros2_medkit_gateway
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2026 bburda
//
// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
//
// 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.

#pragma once

#include <nlohmann/json.hpp>
#include <string>
#include <tl/expected.hpp>

namespace ros2_medkit_gateway {

enum class OperationProviderError {
EntityNotFound,
OperationNotFound,
InvalidParameters,
TransportError,
Timeout,
Rejected,
Internal
};

struct OperationProviderErrorInfo {
OperationProviderError code;
std::string message;
int http_status{500}; ///< Suggested HTTP status code
};

/**
* @brief Provider interface for entity operations
*
* Typed provider interface for plugins that serve SOVD operations
* (GET /{entity_type}/{id}/operations, POST /{entity_type}/{id}/operations/{name}).
* Per-entity routing like DataProvider.
*
* @par Thread safety
* All methods may be called concurrently. Implementations must synchronize.
*
* @see GatewayPlugin for the base class all plugins must also implement
* @see DataProvider for the data counterpart
*/
class OperationProvider {
public:
virtual ~OperationProvider() = default;

/// List available operations for an entity
/// @param entity_id SOVD entity ID
/// @return JSON with {"items": [...]} array of operation descriptors
virtual tl::expected<nlohmann::json, OperationProviderErrorInfo> list_operations(const std::string & entity_id) = 0;

/// Execute an operation
/// @param entity_id SOVD entity ID
/// @param operation_name Operation name (e.g., "session_control")
/// @param parameters JSON parameters from request body
/// @return JSON response body with operation result
virtual tl::expected<nlohmann::json, OperationProviderErrorInfo>
execute_operation(const std::string & entity_id, const std::string & operation_name,
const nlohmann::json & parameters) = 0;
};

} // namespace ros2_medkit_gateway
14 changes: 14 additions & 0 deletions src/ros2_medkit_gateway/src/discovery/layers/plugin_layer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@ LayerOutput PluginLayer::discover() {
output.apps = std::move(result.new_entities.apps);
output.functions = std::move(result.new_entities.functions);

// Tag all plugin-created entities with source="plugin"
for (auto & area : output.areas) {
area.source = "plugin";
}
for (auto & comp : output.components) {
comp.source = "plugin";
}
for (auto & app : output.apps) {
app.source = "plugin";
}
for (auto & func : output.functions) {
func.source = "plugin";
}

// Validate entity IDs from plugin
validate_entities(output.areas, name_, logger_);
validate_entities(output.components, name_, logger_);
Expand Down
Loading
Loading