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
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The `architecture.puml` file contains a PlantUML class diagram showing the relat
- Extracts the entity hierarchy from the ROS 2 graph

3. **RESTServer** - Provides the HTTP/REST API
- Serves endpoints: `/health`, `/`, `/areas`
- Serves endpoints: `/health`, `/`, `/areas`, `/components`, `/areas/{area_id}/components`
- Retrieves cached entities from the GatewayNode
- Runs on configurable host and port

Expand Down
5 changes: 5 additions & 0 deletions postman/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Includes below endpoints:
- ✅ GET `/` - Gateway info
- ✅ GET `/areas` - List all areas
- ✅ GET `/components` - List all components
- ✅ GET `/areas/{area_id}/components` - List components in specific area

## Quick Start

Expand Down Expand Up @@ -54,6 +55,10 @@ ros2 launch ros2_medkit_gateway demo_nodes.launch.py
9. Click **Send**
10. You should see components: `[{"id": "temp_sensor", "namespace": "/powertrain/engine", ...}, ...]`

11. Click **"GET Area Components"**
12. Click **Send**
13. You should see only powertrain components: `[{"id": "temp_sensor", "area": "powertrain", ...}, ...]`

## API Variables

The environment includes:
Expand Down
20 changes: 20 additions & 0 deletions postman/collections/ros2-medkit-gateway.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,26 @@
"description": "List all discovered components across all areas. Returns component metadata including id, namespace, fqn, type, and parent area."
},
"response": []
},
{
"name": "GET Area Components",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/areas/powertrain/components",
"host": [
"{{base_url}}"
],
"path": [
"areas",
"powertrain",
"components"
]
},
"description": "List components within a specific area. Returns 404 if area doesn't exist. Change 'powertrain' to other areas like 'chassis', 'body', etc."
},
"response": []
}
]
}
Expand Down
51 changes: 51 additions & 0 deletions src/ros2_medkit_gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The ROS 2 Medkit Gateway exposes ROS 2 system information and data through a RES
- `GET /` - Gateway status and version information
- `GET /areas` - List all discovered areas (powertrain, chassis, body, root)
- `GET /components` - List all discovered components across all areas
- `GET /areas/{area_id}/components` - List components within a specific area

### API Reference

Expand Down Expand Up @@ -84,6 +85,56 @@ curl http://localhost:8080/components
- `type` - Always "Component"
- `area` - Parent area this component belongs to

#### GET /areas/{area_id}/components

Lists all components within a specific area.

**Example (Success):**
```bash
curl http://localhost:8080/areas/powertrain/components
```

**Response (200 OK):**
```json
[
{
"id": "temp_sensor",
"namespace": "/powertrain/engine",
"fqn": "/powertrain/engine/temp_sensor",
"type": "Component",
"area": "powertrain"
},
{
"id": "rpm_sensor",
"namespace": "/powertrain/engine",
"fqn": "/powertrain/engine/rpm_sensor",
"type": "Component",
"area": "powertrain"
}
]
```

**Example (Error - Area Not Found):**
```bash
curl http://localhost:8080/areas/nonexistent/components
```

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

**URL Parameters:**
- `area_id` - Area identifier (e.g., `powertrain`, `chassis`, `body`)

**Use Cases:**
- Filter components by domain (only show powertrain components)
- Hierarchical navigation (select area → view its components)
- Area-specific health checks

## Quick Start

### Build
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class RESTServer {
void handle_root(const httplib::Request& req, httplib::Response& res);
void handle_list_areas(const httplib::Request& req, httplib::Response& res);
void handle_list_components(const httplib::Request& req, httplib::Response& res);
void handle_area_components(const httplib::Request& req, httplib::Response& res);

GatewayNode* node_;
std::string host_;
Expand Down
64 changes: 64 additions & 0 deletions src/ros2_medkit_gateway/src/rest_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ void RESTServer::setup_routes() {
server_->Get("/components", [this](const httplib::Request& req, httplib::Response& res) {
handle_list_components(req, res);
});

// Area components
server_->Get(R"(/areas/([^/]+)/components)", [this](const httplib::Request& req, httplib::Response& res) {
handle_area_components(req, res);
});
}

void RESTServer::start() {
Expand Down Expand Up @@ -160,4 +165,63 @@ void RESTServer::handle_list_components(const httplib::Request& req, httplib::Re
}
}

void RESTServer::handle_area_components(const httplib::Request& req, httplib::Response& res) {
try {
// Extract area_id from URL path
if (req.matches.size() < 2) {
res.status = 400;
res.set_content(
json{{"error", "Invalid request"}}.dump(2),
"application/json"
);
return;
}

std::string area_id = req.matches[1];
const auto cache = node_->get_entity_cache();

// Check if area exists
bool area_exists = false;
for (const auto& area : cache.areas) {
if (area.id == area_id) {
area_exists = true;
break;
}
}

if (!area_exists) {
res.status = 404;
res.set_content(
json{
{"error", "Area not found"},
{"area_id", area_id}
}.dump(2),
"application/json"
);
return;
}

// Filter components by area
json components_json = json::array();
for (const auto& component : cache.components) {
if (component.area == area_id) {
components_json.push_back(component.to_json());
}
}

res.set_content(components_json.dump(2), "application/json");
} catch (const std::exception& e) {
res.status = 500;
res.set_content(
json{{"error", "Internal server error"}}.dump(),
"application/json"
);
RCLCPP_ERROR(
rclcpp::get_logger("rest_server"),
"Error in handle_area_components: %s",
e.what()
);
}
}

} // namespace ros2_medkit_gateway
33 changes: 33 additions & 0 deletions src/ros2_medkit_gateway/test/test_integration.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,36 @@ def test_04_automotive_areas_discovery(self):
self.assertIn(expected, area_ids)

print(f'✓ All automotive areas discovered: {area_ids}')

def test_05_area_components_success(self):
"""Test GET /areas/{area_id}/components returns components for valid area."""
# Test powertrain area
components = self._get_json('/areas/powertrain/components')
self.assertIsInstance(components, list)
self.assertGreater(len(components), 0)

# All components should belong to powertrain area
for component in components:
self.assertEqual(component['area'], 'powertrain')
self.assertIn('id', component)
self.assertIn('namespace', component)

# Verify expected powertrain components
component_ids = [comp['id'] for comp in components]
self.assertIn('temp_sensor', component_ids)
self.assertIn('rpm_sensor', component_ids)

print(f'✓ Area components test passed: {len(components)} components in powertrain')

def test_06_area_components_nonexistent_error(self):
"""Test GET /areas/{area_id}/components returns 404 for nonexistent area."""
response = requests.get(f'{self.BASE_URL}/areas/nonexistent/components', timeout=5)
self.assertEqual(response.status_code, 404)

data = response.json()
self.assertIn('error', data)
self.assertEqual(data['error'], 'Area not found')
self.assertIn('area_id', data)
self.assertEqual(data['area_id'], 'nonexistent')

print('✓ Nonexistent area error test passed')
Loading