Skip to content

Commit bf599f7

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 44ac9f9 commit bf599f7

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
@@ -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/rest_server.cpp

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

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

6974
void RESTServer::start() {
@@ -372,4 +377,121 @@ void RESTServer::handle_component_data(const httplib::Request& req, httplib::Res
372377
}
373378
}
374379

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

0 commit comments

Comments
 (0)