Skip to content
Merged
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
65 changes: 64 additions & 1 deletion postman/collections/ros2-medkit-gateway.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
"name": "Component Data",
"item": [
{
"name": "GET Component Data",
"name": "GET Component Data (All Topics)",
"request": {
"method": "GET",
"header": [],
Expand All @@ -106,6 +106,69 @@
"description": "Read all topic data from a specific component. Returns array of topic samples with metadata (topic, timestamp, data). Change 'temp_sensor' to other component IDs like 'rpm_sensor', 'pressure_sensor', etc."
},
"response": []
},
{
"name": "GET Component Topic Data (Temperature)",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/components/temp_sensor/data/temperature",
"host": [
"{{base_url}}"
],
"path": [
"components",
"temp_sensor",
"data",
"temperature"
]
},
"description": "Read specific topic data from a component. Returns single topic sample with topic path, timestamp, and data. The topic_name is relative to the component's namespace."
},
"response": []
},
{
"name": "GET Component Topic Data (RPM)",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/components/rpm_sensor/data/rpm",
"host": [
"{{base_url}}"
],
"path": [
"components",
"rpm_sensor",
"data",
"rpm"
]
},
"description": "Read RPM topic data from the rpm_sensor component."
},
"response": []
},
{
"name": "GET Component Topic Data (Pressure)",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/components/pressure_sensor/data/pressure",
"host": [
"{{base_url}}"
],
"path": [
"components",
"pressure_sensor",
"data",
"pressure"
]
},
"description": "Read brake pressure topic data from the pressure_sensor component."
},
"response": []
}
]
}
Expand Down
91 changes: 86 additions & 5 deletions src/ros2_medkit_gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The ROS 2 Medkit Gateway exposes ROS 2 system information and data through a RES
### Component Data Endpoints

- `GET /components/{component_id}/data` - Read all topic data from a component
- `GET /components/{component_id}/data/{topic_name}` - Read specific topic data from a component

### API Reference

Expand Down Expand Up @@ -197,11 +198,91 @@ curl http://localhost:8080/components/nonexistent/data
- Data logging - Periodic sampling of component data

**Performance Considerations:**
- Topic sampling is currently **sequential** (one topic at a time)
- Response time scales linearly: `(number of topics) × (timeout per topic)`
- Example: A component with 10 topics could take up to 30 seconds (10 × 3s)
- For components with many topics, consider querying specific topics individually
- **Future improvement**: Parallel topic sampling will be implemented to reduce latency
- Topic sampling uses **parallel execution** with configurable concurrency
- Default: up to 10 topics sampled in parallel (configurable via `max_parallel_topic_samples`)
- Response time scales with batch count: `ceil(topics / batch_size) × timeout`
- 3-second timeout per topic to accommodate slow-publishing topics

#### GET /components/{component_id}/data/{topic_name}

Read data from a specific topic within a component.

**Example:**
```bash
curl http://localhost:8080/components/temp_sensor/data/temperature
```

**Response (200 OK):**
```json
{
"topic": "/powertrain/engine/temperature",
"timestamp": 1732377600000000000,
"data": {
"temperature": 85.5,
"variance": 0.0
}
}
```

**Example (Error - Topic Not Found):**
```bash
curl http://localhost:8080/components/temp_sensor/data/nonexistent
```

**Response (404 Not Found):**
```json
{
"error": "Topic not found or not publishing",
"component_id": "temp_sensor",
"topic_name": "nonexistent"
}
```

**Example (Error - Component Not Found):**
```bash
curl http://localhost:8080/components/nonexistent/data/temperature
```

**Response (404 Not Found):**
```json
{
"error": "Component not found",
"component_id": "nonexistent"
}
```

**Example (Error - Invalid Topic Name):**
```bash
curl http://localhost:8080/components/temp_sensor/data/invalid-name
```

**Response (400 Bad Request):**
```json
{
"error": "Invalid topic name",
"details": "Entity ID contains invalid character: '-'. Only alphanumeric and underscore are allowed",
"topic_name": "invalid-name"
}
```

**URL Parameters:**
- `component_id` - Component identifier (e.g., `temp_sensor`, `rpm_sensor`)
- `topic_name` - Topic name within the component (e.g., `temperature`, `rpm`)

**Response Fields:**
- `topic` - Full topic path (e.g., `/powertrain/engine/temperature`)
- `timestamp` - Unix timestamp (nanoseconds since epoch) when data was sampled
- `data` - Topic message data as JSON object

**Validation:**
- Both `component_id` and `topic_name` follow ROS 2 naming conventions
- Allowed characters: alphanumeric (a-z, A-Z, 0-9), underscore (_)
- Hyphens, special characters, and escape sequences are rejected

**Use Cases:**
- Read specific sensor value (e.g., just temperature, not all engine data)
- Lower latency than reading all component data
- Targeted monitoring of specific metrics

## Quick Start

Expand Down
46 changes: 46 additions & 0 deletions src/ros2_medkit_gateway/include/ros2_medkit_gateway/exceptions.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2025 mfaferek93
//
// 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 <stdexcept>
#include <string>

namespace ros2_medkit_gateway {

/// Exception thrown when a topic is not available or times out
class TopicNotAvailableException : public std::runtime_error {
public:
explicit TopicNotAvailableException(const std::string& topic)
: std::runtime_error("Topic not available: " + topic), topic_(topic) {}

const std::string& topic() const noexcept { return topic_; }

private:
std::string topic_;
};

/// Exception thrown when a required command (e.g., ros2 CLI) is not available
class CommandNotAvailableException : public std::runtime_error {
public:
explicit CommandNotAvailableException(const std::string& command)
: std::runtime_error("Command not available: " + command), command_(command) {}

const std::string& command() const noexcept { return command_; }

private:
std::string command_;
};

} // namespace ros2_medkit_gateway
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class RESTServer {
void handle_list_components(const httplib::Request& req, httplib::Response& res);
void handle_area_components(const httplib::Request& req, httplib::Response& res);
void handle_component_data(const httplib::Request& req, httplib::Response& res);
void handle_component_topic_data(const httplib::Request& req, httplib::Response& res);

// Helper methods
std::expected<void, std::string> validate_entity_id(const std::string& entity_id) const;
Expand Down
17 changes: 11 additions & 6 deletions src/ros2_medkit_gateway/src/data_access_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@
// limitations under the License.

#include "ros2_medkit_gateway/data_access_manager.hpp"

#include <algorithm>
#include <chrono>
#include <cmath>
#include <future>
#include <sstream>

#include "ros2_medkit_gateway/exceptions.hpp"

namespace ros2_medkit_gateway {

DataAccessManager::DataAccessManager(rclcpp::Node* node)
Expand All @@ -37,7 +40,7 @@ DataAccessManager::DataAccessManager(rclcpp::Node* node)

if (!cli_wrapper_->is_command_available("ros2")) {
RCLCPP_ERROR(node_->get_logger(), "ROS 2 CLI not found!");
throw std::runtime_error("ros2 command not available");
throw CommandNotAvailableException("ros2");
}

RCLCPP_INFO(node_->get_logger(),
Expand Down Expand Up @@ -65,14 +68,14 @@ json DataAccessManager::get_topic_sample(
// Check for warning messages in raw output (before parsing)
// ROS 2 CLI prints warnings as text, not structured YAML
if (output.find("WARNING") != std::string::npos) {
throw std::runtime_error("Topic not available or timeout");
throw TopicNotAvailableException(topic_name);
}

json data = output_parser_->parse_yaml(output);

// Check for empty/null parsed data
if (data.is_null()) {
throw std::runtime_error("Topic not available or timeout");
throw TopicNotAvailableException(topic_name);
}

json result = {
Expand All @@ -84,15 +87,17 @@ json DataAccessManager::get_topic_sample(
};

return result;
} catch (const TopicNotAvailableException&) {
// Re-throw TopicNotAvailableException as-is for proper handling upstream
throw;
} catch (const std::exception& e) {
RCLCPP_ERROR(node_->get_logger(),
"Failed to get sample from topic '%s': %s",
topic_name.c_str(),
e.what());

throw std::runtime_error(
"Failed to get sample from topic '" + topic_name + "': " + std::string(e.what())
);
// For other errors (CLI failures, parsing errors), wrap as TopicNotAvailableException
throw TopicNotAvailableException(topic_name);
}
}

Expand Down
Loading