Skip to content

Commit e034a4d

Browse files
author
Michał Fąferek
committed
[#19] feat: add GET /components/{component_id}/data/{topic_name} endpoint
Add endpoint to read specific topic data from a component. Fix topic path construction to use namespace_path instead of fqn for correct topic discovery.
1 parent 44ac9f9 commit e034a4d

File tree

7 files changed

+544
-15
lines changed

7 files changed

+544
-15
lines changed

postman/collections/ros2-medkit-gateway.postman_collection.json

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
"name": "Component Data",
8989
"item": [
9090
{
91-
"name": "GET Component Data",
91+
"name": "GET Component Data (All Topics)",
9292
"request": {
9393
"method": "GET",
9494
"header": [],
@@ -106,6 +106,69 @@
106106
"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."
107107
},
108108
"response": []
109+
},
110+
{
111+
"name": "GET Component Topic Data (Temperature)",
112+
"request": {
113+
"method": "GET",
114+
"header": [],
115+
"url": {
116+
"raw": "{{base_url}}/components/temp_sensor/data/temperature",
117+
"host": [
118+
"{{base_url}}"
119+
],
120+
"path": [
121+
"components",
122+
"temp_sensor",
123+
"data",
124+
"temperature"
125+
]
126+
},
127+
"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."
128+
},
129+
"response": []
130+
},
131+
{
132+
"name": "GET Component Topic Data (RPM)",
133+
"request": {
134+
"method": "GET",
135+
"header": [],
136+
"url": {
137+
"raw": "{{base_url}}/components/rpm_sensor/data/rpm",
138+
"host": [
139+
"{{base_url}}"
140+
],
141+
"path": [
142+
"components",
143+
"rpm_sensor",
144+
"data",
145+
"rpm"
146+
]
147+
},
148+
"description": "Read RPM topic data from the rpm_sensor component."
149+
},
150+
"response": []
151+
},
152+
{
153+
"name": "GET Component Topic Data (Pressure)",
154+
"request": {
155+
"method": "GET",
156+
"header": [],
157+
"url": {
158+
"raw": "{{base_url}}/components/pressure_sensor/data/pressure",
159+
"host": [
160+
"{{base_url}}"
161+
],
162+
"path": [
163+
"components",
164+
"pressure_sensor",
165+
"data",
166+
"pressure"
167+
]
168+
},
169+
"description": "Read brake pressure topic data from the pressure_sensor component."
170+
},
171+
"response": []
109172
}
110173
]
111174
}

src/ros2_medkit_gateway/README.md

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ The ROS 2 Medkit Gateway exposes ROS 2 system information and data through a RES
2525
### Component Data Endpoints
2626

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

2930
### API Reference
3031

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

199200
**Performance Considerations:**
200-
- Topic sampling is currently **sequential** (one topic at a time)
201-
- Response time scales linearly: `(number of topics) × (timeout per topic)`
202-
- Example: A component with 10 topics could take up to 30 seconds (10 × 3s)
203-
- For components with many topics, consider querying specific topics individually
204-
- **Future improvement**: Parallel topic sampling will be implemented to reduce latency
201+
- Topic sampling uses **parallel execution** with configurable concurrency
202+
- Default: up to 10 topics sampled in parallel (configurable via `max_parallel_topic_samples`)
203+
- Response time scales with batch count: `ceil(topics / batch_size) × timeout`
204+
- 3-second timeout per topic to accommodate slow-publishing topics
205+
206+
#### GET /components/{component_id}/data/{topic_name}
207+
208+
Read data from a specific topic within a component.
209+
210+
**Example:**
211+
```bash
212+
curl http://localhost:8080/components/temp_sensor/data/temperature
213+
```
214+
215+
**Response (200 OK):**
216+
```json
217+
{
218+
"topic": "/powertrain/engine/temperature",
219+
"timestamp": 1732377600000000000,
220+
"data": {
221+
"temperature": 85.5,
222+
"variance": 0.0
223+
}
224+
}
225+
```
226+
227+
**Example (Error - Topic Not Found):**
228+
```bash
229+
curl http://localhost:8080/components/temp_sensor/data/nonexistent
230+
```
231+
232+
**Response (404 Not Found):**
233+
```json
234+
{
235+
"error": "Topic not found or not publishing",
236+
"component_id": "temp_sensor",
237+
"topic_name": "nonexistent"
238+
}
239+
```
240+
241+
**Example (Error - Component Not Found):**
242+
```bash
243+
curl http://localhost:8080/components/nonexistent/data/temperature
244+
```
245+
246+
**Response (404 Not Found):**
247+
```json
248+
{
249+
"error": "Component not found",
250+
"component_id": "nonexistent"
251+
}
252+
```
253+
254+
**Example (Error - Invalid Topic Name):**
255+
```bash
256+
curl http://localhost:8080/components/temp_sensor/data/invalid-name
257+
```
258+
259+
**Response (400 Bad Request):**
260+
```json
261+
{
262+
"error": "Invalid topic name",
263+
"details": "Entity ID contains invalid character: '-'. Only alphanumeric and underscore are allowed",
264+
"topic_name": "invalid-name"
265+
}
266+
```
267+
268+
**URL Parameters:**
269+
- `component_id` - Component identifier (e.g., `temp_sensor`, `rpm_sensor`)
270+
- `topic_name` - Topic name within the component (e.g., `temperature`, `rpm`)
271+
272+
**Response Fields:**
273+
- `topic` - Full topic path (e.g., `/powertrain/engine/temperature`)
274+
- `timestamp` - Unix timestamp (nanoseconds since epoch) when data was sampled
275+
- `data` - Topic message data as JSON object
276+
277+
**Validation:**
278+
- Both `component_id` and `topic_name` follow ROS 2 naming conventions
279+
- Allowed characters: alphanumeric (a-z, A-Z, 0-9), underscore (_)
280+
- Hyphens, special characters, and escape sequences are rejected
281+
282+
**Use Cases:**
283+
- Read specific sensor value (e.g., just temperature, not all engine data)
284+
- Lower latency than reading all component data
285+
- Targeted monitoring of specific metrics
205286

206287
## Quick Start
207288

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2025 mfaferek93
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#pragma once
16+
17+
#include <stdexcept>
18+
#include <string>
19+
20+
namespace ros2_medkit_gateway {
21+
22+
/// Exception thrown when a topic is not available or times out
23+
class TopicNotAvailableException : public std::runtime_error {
24+
public:
25+
explicit TopicNotAvailableException(const std::string& topic)
26+
: std::runtime_error("Topic not available: " + topic), topic_(topic) {}
27+
28+
const std::string& topic() const noexcept { return topic_; }
29+
30+
private:
31+
std::string topic_;
32+
};
33+
34+
/// Exception thrown when a required command (e.g., ros2 CLI) is not available
35+
class CommandNotAvailableException : public std::runtime_error {
36+
public:
37+
explicit CommandNotAvailableException(const std::string& command)
38+
: std::runtime_error("Command not available: " + command), command_(command) {}
39+
40+
const std::string& command() const noexcept { return command_; }
41+
42+
private:
43+
std::string command_;
44+
};
45+
46+
} // namespace ros2_medkit_gateway

src/ros2_medkit_gateway/include/ros2_medkit_gateway/rest_server.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class RESTServer {
4444
void handle_list_components(const httplib::Request& req, httplib::Response& res);
4545
void handle_area_components(const httplib::Request& req, httplib::Response& res);
4646
void handle_component_data(const httplib::Request& req, httplib::Response& res);
47+
void handle_component_topic_data(const httplib::Request& req, httplib::Response& res);
4748

4849
// Helper methods
4950
std::expected<void, std::string> validate_entity_id(const std::string& entity_id) const;

src/ros2_medkit_gateway/src/data_access_manager.cpp

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313
// limitations under the License.
1414

1515
#include "ros2_medkit_gateway/data_access_manager.hpp"
16+
1617
#include <algorithm>
1718
#include <chrono>
1819
#include <cmath>
1920
#include <future>
2021
#include <sstream>
2122

23+
#include "ros2_medkit_gateway/exceptions.hpp"
24+
2225
namespace ros2_medkit_gateway {
2326

2427
DataAccessManager::DataAccessManager(rclcpp::Node* node)
@@ -37,7 +40,7 @@ DataAccessManager::DataAccessManager(rclcpp::Node* node)
3740

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

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

7174
json data = output_parser_->parse_yaml(output);
7275

7376
// Check for empty/null parsed data
7477
if (data.is_null()) {
75-
throw std::runtime_error("Topic not available or timeout");
78+
throw TopicNotAvailableException(topic_name);
7679
}
7780

7881
json result = {
@@ -84,15 +87,17 @@ json DataAccessManager::get_topic_sample(
8487
};
8588

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

93-
throw std::runtime_error(
94-
"Failed to get sample from topic '" + topic_name + "': " + std::string(e.what())
95-
);
99+
// For other errors (CLI failures, parsing errors), wrap as TopicNotAvailableException
100+
throw TopicNotAvailableException(topic_name);
96101
}
97102
}
98103

0 commit comments

Comments
 (0)