Skip to content

Commit 7bbbab9

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.
1 parent 5866028 commit 7bbbab9

File tree

5 files changed

+475
-7
lines changed

5 files changed

+475
-7
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

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
@@ -43,6 +43,7 @@ class RESTServer {
4343
void handle_list_components(const httplib::Request& req, httplib::Response& res);
4444
void handle_area_components(const httplib::Request& req, httplib::Response& res);
4545
void handle_component_data(const httplib::Request& req, httplib::Response& res);
46+
void handle_component_topic_data(const httplib::Request& req, httplib::Response& res);
4647

4748
// Helper methods
4849
bool validate_entity_id(const std::string& entity_id, std::string& error_message) const;

src/ros2_medkit_gateway/src/rest_server.cpp

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,15 @@ void RESTServer::setup_routes() {
5959
handle_area_components(req, res);
6060
});
6161

62-
// Component data
62+
// Component data (all topics)
6363
server_->Get(R"(/components/([^/]+)/data)", [this](const httplib::Request& req, httplib::Response& res) {
6464
handle_component_data(req, res);
6565
});
66+
67+
// Component topic data (specific topic)
68+
server_->Get(R"(/components/([^/]+)/data/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
69+
handle_component_topic_data(req, res);
70+
});
6671
}
6772

6873
void RESTServer::start() {
@@ -374,4 +379,121 @@ void RESTServer::handle_component_data(const httplib::Request& req, httplib::Res
374379
}
375380
}
376381

382+
void RESTServer::handle_component_topic_data(const httplib::Request& req, httplib::Response& res) {
383+
std::string component_id;
384+
std::string topic_name;
385+
try {
386+
// Extract component_id and topic_name from URL path
387+
if (req.matches.size() < 3) {
388+
res.status = 400;
389+
res.set_content(
390+
json{{"error", "Invalid request"}}.dump(2),
391+
"application/json"
392+
);
393+
return;
394+
}
395+
396+
component_id = req.matches[1];
397+
topic_name = req.matches[2];
398+
399+
// Validate component_id
400+
std::string validation_error;
401+
if (!validate_entity_id(component_id, validation_error)) {
402+
res.status = 400;
403+
res.set_content(
404+
json{
405+
{"error", "Invalid component ID"},
406+
{"details", validation_error},
407+
{"component_id", component_id}
408+
}.dump(2),
409+
"application/json"
410+
);
411+
return;
412+
}
413+
414+
// Validate topic_name
415+
if (!validate_entity_id(topic_name, validation_error)) {
416+
res.status = 400;
417+
res.set_content(
418+
json{
419+
{"error", "Invalid topic name"},
420+
{"details", validation_error},
421+
{"topic_name", topic_name}
422+
}.dump(2),
423+
"application/json"
424+
);
425+
return;
426+
}
427+
428+
const auto cache = node_->get_entity_cache();
429+
430+
// Find component in cache
431+
std::string component_namespace;
432+
bool component_found = false;
433+
434+
for (const auto& component : cache.components) {
435+
if (component.id == component_id) {
436+
component_namespace = component.namespace_path;
437+
component_found = true;
438+
break;
439+
}
440+
}
441+
442+
if (!component_found) {
443+
res.status = 404;
444+
res.set_content(
445+
json{
446+
{"error", "Component not found"},
447+
{"component_id", component_id}
448+
}.dump(2),
449+
"application/json"
450+
);
451+
return;
452+
}
453+
454+
// Construct full topic path: {namespace_path}/{topic_name}
455+
// Topics are relative to the component's namespace, not its FQN
456+
std::string full_topic_path = component_namespace + "/" + topic_name;
457+
458+
// Get topic data from DataAccessManager
459+
auto data_access_mgr = node_->get_data_access_manager();
460+
json topic_data = data_access_mgr->get_topic_sample(full_topic_path);
461+
462+
res.set_content(topic_data.dump(2), "application/json");
463+
} catch (const std::exception& e) {
464+
// Check if it's a "topic not found" type error
465+
std::string error_msg = e.what();
466+
if (error_msg.find("not available") != std::string::npos ||
467+
error_msg.find("timeout") != std::string::npos) {
468+
res.status = 404;
469+
res.set_content(
470+
json{
471+
{"error", "Topic not found or not publishing"},
472+
{"component_id", component_id},
473+
{"topic_name", topic_name}
474+
}.dump(2),
475+
"application/json"
476+
);
477+
} else {
478+
res.status = 500;
479+
res.set_content(
480+
json{
481+
{"error", "Failed to retrieve topic data"},
482+
{"details", e.what()},
483+
{"component_id", component_id},
484+
{"topic_name", topic_name}
485+
}.dump(2),
486+
"application/json"
487+
);
488+
}
489+
RCLCPP_ERROR(
490+
rclcpp::get_logger("rest_server"),
491+
"Error in handle_component_topic_data for component '%s', topic '%s': %s",
492+
component_id.c_str(),
493+
topic_name.c_str(),
494+
e.what()
495+
);
496+
}
497+
}
498+
377499
} // namespace ros2_medkit_gateway

0 commit comments

Comments
 (0)