Skip to content

Commit 8979d65

Browse files
mfaferek93Michał Fąferek
andauthored
58/operations api (#59)
* feat(gateway): add SOVD Operations API for ROS2 service calls - Add OperationManager for executing ROS2 service calls via CLI - Extend DiscoveryManager with native service/action discovery - Add POST /api/v1/components/{id}/operations/{name} endpoint - Include operations in component discovery response - Parse Python repr format from ros2 service call output - Add 8 integration tests (test_31-38) for operations API - Update Postman collection with Operations folder * feat(gateway): add async action operations with native status tracking Add support for ROS2 action operations (async long-running tasks): - POST /components/{id}/operations/{op} with goal: send action goal - GET .../status?goal_id=X: get goal status via native subscription - DELETE ...?goal_id=X: cancel running action goal Key features: - Goal sending via ros2 action send_goal CLI (3s timeout) - Native subscription to /_action/status for real-time updates - Goal tracking with status transitions (executing->succeeded/canceled) - Demo long_calibration_action node (Fibonacci-based) - Integration tests for all action endpoints (test_39-44) * docs: update architecture diagram with Operations API - Add OperationManager class for service/action execution - Add ServiceInfo and ActionInfo models - Update DiscoveryManager with service/action discovery methods - Update Component with services/actions fields - Add operations REST endpoints to RESTServer description - Update relationships diagram --------- Co-authored-by: Michał Fąferek <michal.faferek@42dot.ai>
1 parent 53a98c2 commit 8979d65

19 files changed

+2831
-38
lines changed

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

Lines changed: 290 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
"components"
7777
]
7878
},
79-
"description": "List all discovered components across all areas. Returns component metadata including id, namespace, fqn, type, and parent area."
79+
"description": "List all discovered components across all areas. Returns component metadata including id, namespace, fqn, type, parent area, and available operations (services and actions)."
8080
},
8181
"response": []
8282
},
@@ -249,6 +249,294 @@
249249
"response": []
250250
}
251251
]
252+
},
253+
{
254+
"name": "Operations",
255+
"item": [
256+
{
257+
"name": "Sync Operations (Services)",
258+
"item": [
259+
{
260+
"name": "GET List Component Operations",
261+
"request": {
262+
"method": "GET",
263+
"header": [],
264+
"url": {
265+
"raw": "{{base_url}}/components",
266+
"host": [
267+
"{{base_url}}"
268+
],
269+
"path": [
270+
"components"
271+
]
272+
},
273+
"description": "List all components with their available operations. Each component includes an 'operations' array showing available services (kind: 'service') and actions (kind: 'action') with their types and paths."
274+
},
275+
"response": []
276+
},
277+
{
278+
"name": "POST Call Calibrate Service",
279+
"request": {
280+
"method": "POST",
281+
"header": [
282+
{
283+
"key": "Content-Type",
284+
"value": "application/json"
285+
}
286+
],
287+
"body": {
288+
"mode": "raw",
289+
"raw": "{}"
290+
},
291+
"url": {
292+
"raw": "{{base_url}}/components/calibration/operations/calibrate",
293+
"host": [
294+
"{{base_url}}"
295+
],
296+
"path": [
297+
"components",
298+
"calibration",
299+
"operations",
300+
"calibrate"
301+
]
302+
},
303+
"description": "Call a ROS 2 service operation on a component. This example calls the std_srvs/srv/Trigger calibrate service. The service type is auto-discovered from the component's registered services. Response includes success status and service response data."
304+
},
305+
"response": []
306+
},
307+
{
308+
"name": "POST Call Service with Type Override",
309+
"request": {
310+
"method": "POST",
311+
"header": [
312+
{
313+
"key": "Content-Type",
314+
"value": "application/json"
315+
}
316+
],
317+
"body": {
318+
"mode": "raw",
319+
"raw": "{\n \"type\": \"std_srvs/srv/Trigger\",\n \"request\": {}\n}"
320+
},
321+
"url": {
322+
"raw": "{{base_url}}/components/calibration/operations/calibrate",
323+
"host": [
324+
"{{base_url}}"
325+
],
326+
"path": [
327+
"components",
328+
"calibration",
329+
"operations",
330+
"calibrate"
331+
]
332+
},
333+
"description": "Call a ROS 2 service with explicit type override. Use 'type' field to specify the service type (e.g., 'std_srvs/srv/Trigger') and 'request' field for the service request data. Useful when the service type cannot be auto-discovered."
334+
},
335+
"response": []
336+
},
337+
{
338+
"name": "POST Call Service (Error - Not Found)",
339+
"request": {
340+
"method": "POST",
341+
"header": [
342+
{
343+
"key": "Content-Type",
344+
"value": "application/json"
345+
}
346+
],
347+
"body": {
348+
"mode": "raw",
349+
"raw": "{}"
350+
},
351+
"url": {
352+
"raw": "{{base_url}}/components/calibration/operations/nonexistent",
353+
"host": [
354+
"{{base_url}}"
355+
],
356+
"path": [
357+
"components",
358+
"calibration",
359+
"operations",
360+
"nonexistent"
361+
]
362+
},
363+
"description": "Example of calling a non-existent operation. Returns 500 error with 'Service not found' message."
364+
},
365+
"response": []
366+
}
367+
]
368+
},
369+
{
370+
"name": "Async Operations (Actions)",
371+
"item": [
372+
{
373+
"name": "POST Send Action Goal (Long Calibration)",
374+
"request": {
375+
"method": "POST",
376+
"header": [
377+
{
378+
"key": "Content-Type",
379+
"value": "application/json"
380+
}
381+
],
382+
"body": {
383+
"mode": "raw",
384+
"raw": "{\n \"goal\": {\n \"order\": 10\n }\n}"
385+
},
386+
"url": {
387+
"raw": "{{base_url}}/components/long_calibration/operations/long_calibration",
388+
"host": [
389+
"{{base_url}}"
390+
],
391+
"path": [
392+
"components",
393+
"long_calibration",
394+
"operations",
395+
"long_calibration"
396+
]
397+
},
398+
"description": "Start an async action operation. Sends a goal to the action server and returns immediately with goal_id. Response includes goal_id, goal_status (executing/succeeded), kind (action), and component info. Takes ~3-4 seconds due to CLI discovery. The action continues running in the background."
399+
},
400+
"response": []
401+
},
402+
{
403+
"name": "POST Send Short Action Goal (Fast)",
404+
"request": {
405+
"method": "POST",
406+
"header": [
407+
{
408+
"key": "Content-Type",
409+
"value": "application/json"
410+
}
411+
],
412+
"body": {
413+
"mode": "raw",
414+
"raw": "{\n \"goal\": {\n \"order\": 3\n }\n}"
415+
},
416+
"url": {
417+
"raw": "{{base_url}}/components/long_calibration/operations/long_calibration",
418+
"host": [
419+
"{{base_url}}"
420+
],
421+
"path": [
422+
"components",
423+
"long_calibration",
424+
"operations",
425+
"long_calibration"
426+
]
427+
},
428+
"description": "Send a short action goal (order=3 completes in ~1.5s). If the action completes within 3 seconds, goal_status will be 'succeeded' immediately."
429+
},
430+
"response": []
431+
},
432+
{
433+
"name": "GET Action Status (Latest)",
434+
"request": {
435+
"method": "GET",
436+
"header": [],
437+
"url": {
438+
"raw": "{{base_url}}/components/long_calibration/operations/long_calibration/status",
439+
"host": [
440+
"{{base_url}}"
441+
],
442+
"path": [
443+
"components",
444+
"long_calibration",
445+
"operations",
446+
"long_calibration",
447+
"status"
448+
]
449+
},
450+
"description": "Get the status of the most recent action goal. No goal_id needed - the gateway tracks goals internally and returns the latest one. Returns goal_id, status (accepted/executing/succeeded/canceled/aborted), action_path, and action_type."
451+
},
452+
"response": []
453+
},
454+
{
455+
"name": "GET Action Status (Specific Goal)",
456+
"request": {
457+
"method": "GET",
458+
"header": [],
459+
"url": {
460+
"raw": "{{base_url}}/components/long_calibration/operations/long_calibration/status?goal_id={{goal_id}}",
461+
"host": [
462+
"{{base_url}}"
463+
],
464+
"path": [
465+
"components",
466+
"long_calibration",
467+
"operations",
468+
"long_calibration",
469+
"status"
470+
],
471+
"query": [
472+
{
473+
"key": "goal_id",
474+
"value": "{{goal_id}}"
475+
}
476+
]
477+
},
478+
"description": "Get the status of a specific action goal by goal_id. Use this when you have multiple concurrent goals and need to track a specific one."
479+
},
480+
"response": []
481+
},
482+
{
483+
"name": "GET Action Status (All Goals)",
484+
"request": {
485+
"method": "GET",
486+
"header": [],
487+
"url": {
488+
"raw": "{{base_url}}/components/long_calibration/operations/long_calibration/status?all=true",
489+
"host": [
490+
"{{base_url}}"
491+
],
492+
"path": [
493+
"components",
494+
"long_calibration",
495+
"operations",
496+
"long_calibration",
497+
"status"
498+
],
499+
"query": [
500+
{
501+
"key": "all",
502+
"value": "true"
503+
}
504+
]
505+
},
506+
"description": "Get the status of all tracked goals for this action. Returns an array of goals sorted by most recent first, with count."
507+
},
508+
"response": []
509+
},
510+
{
511+
"name": "DELETE Cancel Action Goal",
512+
"request": {
513+
"method": "DELETE",
514+
"header": [],
515+
"url": {
516+
"raw": "{{base_url}}/components/long_calibration/operations/long_calibration?goal_id={{goal_id}}",
517+
"host": [
518+
"{{base_url}}"
519+
],
520+
"path": [
521+
"components",
522+
"long_calibration",
523+
"operations",
524+
"long_calibration"
525+
],
526+
"query": [
527+
{
528+
"key": "goal_id",
529+
"value": "{{goal_id}}"
530+
}
531+
]
532+
},
533+
"description": "Cancel a running action goal. Sends cancel request to the action server. Returns confirmation with status 'canceling'. Use GET /status to verify the goal was canceled (status becomes 'canceled')."
534+
},
535+
"response": []
536+
}
537+
]
538+
}
539+
]
252540
}
253541
]
254-
}
542+
}

src/ros2_medkit_gateway/CMakeLists.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ find_package(nlohmann_json REQUIRED)
2828
find_package(yaml_cpp_vendor REQUIRED)
2929
find_package(yaml-cpp REQUIRED)
3030
find_package(ament_index_cpp REQUIRED)
31+
find_package(action_msgs REQUIRED)
32+
find_package(rclcpp_action REQUIRED)
33+
find_package(example_interfaces REQUIRED)
3134

3235
# Find cpp-httplib using pkg-config
3336
find_package(PkgConfig REQUIRED)
@@ -47,6 +50,7 @@ add_library(gateway_lib STATIC
4750
src/data_access_manager.cpp
4851
src/type_introspection.cpp
4952
src/native_topic_sampler.cpp
53+
src/operation_manager.cpp
5054
)
5155

5256
ament_target_dependencies(gateway_lib
@@ -55,6 +59,7 @@ ament_target_dependencies(gateway_lib
5559
std_srvs
5660
rcl_interfaces
5761
ament_index_cpp
62+
action_msgs
5863
)
5964

6065
target_link_libraries(gateway_lib
@@ -219,6 +224,18 @@ if(BUILD_TESTING)
219224
install(TARGETS demo_calibration_service
220225
DESTINATION lib/${PROJECT_NAME}
221226
)
227+
228+
add_executable(demo_long_calibration_action
229+
test/demo_nodes/long_calibration_action.cpp
230+
)
231+
ament_target_dependencies(demo_long_calibration_action
232+
rclcpp
233+
rclcpp_action
234+
example_interfaces
235+
)
236+
install(TARGETS demo_long_calibration_action
237+
DESTINATION lib/${PROJECT_NAME}
238+
)
222239
endif()
223240

224241
ament_package()

0 commit comments

Comments
 (0)