Skip to content

Commit 03b1dde

Browse files
committed
Added get_action_status tool
1 parent a339dfc commit 03b1dde

File tree

1 file changed

+187
-12
lines changed

1 file changed

+187
-12
lines changed

server.py

Lines changed: 187 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ def inspect_all_topics() -> dict:
496496
def subscribe_once(
497497
topic: str = "",
498498
msg_type: str = "",
499-
timeout: Optional[float] = None,
499+
timeout: float = None,
500500
queue_length: Optional[int] = None,
501501
throttle_rate_ms: Optional[int] = None,
502502
) -> dict:
@@ -2112,7 +2112,12 @@ def get_action_details(action_type: str) -> dict:
21122112
if not action_type or not action_type.strip():
21132113
return {"error": "Action type cannot be empty"}
21142114

2115-
result = {"action_type": action_type, "goal": {}, "result": {}, "feedback": {}}
2115+
result = {
2116+
"action_type": action_type,
2117+
"goal": {},
2118+
"result": {},
2119+
"feedback": {}
2120+
}
21162121

21172122
# Get goal, result, and feedback details in a single WebSocket context
21182123
with ws_manager:
@@ -2126,16 +2131,35 @@ def get_action_details(action_type: str) -> dict:
21262131
}
21272132

21282133
goal_response = ws_manager.request(goal_message)
2129-
if goal_response and "values" in goal_response:
2134+
if goal_response and isinstance(goal_response, dict) and "values" in goal_response and "error" not in goal_response:
21302135
typedefs = goal_response["values"].get("typedefs", [])
21312136
if typedefs:
21322137
for typedef in typedefs:
21332138
field_names = typedef.get("fieldnames", [])
21342139
field_types = typedef.get("fieldtypes", [])
2140+
field_array_len = typedef.get("fieldarraylen", [])
2141+
examples = typedef.get("examples", [])
2142+
const_names = typedef.get("constnames", [])
2143+
const_values = typedef.get("constvalues", [])
2144+
21352145
fields = {}
2136-
for name, ftype in zip(field_names, field_types):
2146+
field_details = {}
2147+
for i, (name, ftype) in enumerate(zip(field_names, field_types)):
21372148
fields[name] = ftype
2138-
result["goal"] = {"fields": fields, "field_count": len(fields)}
2149+
field_details[name] = {
2150+
"type": ftype,
2151+
"array_length": field_array_len[i] if i < len(field_array_len) else -1,
2152+
"example": examples[i] if i < len(examples) else None
2153+
}
2154+
2155+
result["goal"] = {
2156+
"fields": fields,
2157+
"field_count": len(fields),
2158+
"field_details": field_details,
2159+
"message_type": typedef.get("type", ""),
2160+
"examples": examples,
2161+
"constants": dict(zip(const_names, const_values)) if const_names else {}
2162+
}
21392163

21402164
# Get result details using action-specific service
21412165
result_message = {
@@ -2147,16 +2171,35 @@ def get_action_details(action_type: str) -> dict:
21472171
}
21482172

21492173
result_response = ws_manager.request(result_message)
2150-
if result_response and "values" in result_response:
2174+
if result_response and isinstance(result_response, dict) and "values" in result_response and "error" not in result_response:
21512175
typedefs = result_response["values"].get("typedefs", [])
21522176
if typedefs:
21532177
for typedef in typedefs:
21542178
field_names = typedef.get("fieldnames", [])
21552179
field_types = typedef.get("fieldtypes", [])
2180+
field_array_len = typedef.get("fieldarraylen", [])
2181+
examples = typedef.get("examples", [])
2182+
const_names = typedef.get("constnames", [])
2183+
const_values = typedef.get("constvalues", [])
2184+
21562185
fields = {}
2157-
for name, ftype in zip(field_names, field_types):
2186+
field_details = {}
2187+
for i, (name, ftype) in enumerate(zip(field_names, field_types)):
21582188
fields[name] = ftype
2159-
result["result"] = {"fields": fields, "field_count": len(fields)}
2189+
field_details[name] = {
2190+
"type": ftype,
2191+
"array_length": field_array_len[i] if i < len(field_array_len) else -1,
2192+
"example": examples[i] if i < len(examples) else None
2193+
}
2194+
2195+
result["result"] = {
2196+
"fields": fields,
2197+
"field_count": len(fields),
2198+
"field_details": field_details,
2199+
"message_type": typedef.get("type", ""),
2200+
"examples": examples,
2201+
"constants": dict(zip(const_names, const_values)) if const_names else {}
2202+
}
21602203

21612204
# Get feedback details using action-specific service
21622205
feedback_message = {
@@ -2168,16 +2211,35 @@ def get_action_details(action_type: str) -> dict:
21682211
}
21692212

21702213
feedback_response = ws_manager.request(feedback_message)
2171-
if feedback_response and "values" in feedback_response:
2214+
if feedback_response and isinstance(feedback_response, dict) and "values" in feedback_response and "error" not in feedback_response:
21722215
typedefs = feedback_response["values"].get("typedefs", [])
21732216
if typedefs:
21742217
for typedef in typedefs:
21752218
field_names = typedef.get("fieldnames", [])
21762219
field_types = typedef.get("fieldtypes", [])
2220+
field_array_len = typedef.get("fieldarraylen", [])
2221+
examples = typedef.get("examples", [])
2222+
const_names = typedef.get("constnames", [])
2223+
const_values = typedef.get("constvalues", [])
2224+
21772225
fields = {}
2178-
for name, ftype in zip(field_names, field_types):
2226+
field_details = {}
2227+
for i, (name, ftype) in enumerate(zip(field_names, field_types)):
21792228
fields[name] = ftype
2180-
result["feedback"] = {"fields": fields, "field_count": len(fields)}
2229+
field_details[name] = {
2230+
"type": ftype,
2231+
"array_length": field_array_len[i] if i < len(field_array_len) else -1,
2232+
"example": examples[i] if i < len(examples) else None
2233+
}
2234+
2235+
result["feedback"] = {
2236+
"fields": fields,
2237+
"field_count": len(fields),
2238+
"field_details": field_details,
2239+
"message_type": typedef.get("type", ""),
2240+
"examples": examples,
2241+
"constants": dict(zip(const_names, const_values)) if const_names else {}
2242+
}
21812243

21822244
# Check if we got any data
21832245
if not result["goal"] and not result["result"] and not result["feedback"]:
@@ -2186,6 +2248,119 @@ def get_action_details(action_type: str) -> dict:
21862248
return result
21872249

21882250

2251+
@mcp.tool(
2252+
description=(
2253+
"Get action status for a specific action type. Works only with ROS 2.\n"
2254+
"Example:\n"
2255+
"get_action_status('action_tutorials_interfaces/action/Fibonacci')"
2256+
)
2257+
)
2258+
def get_action_status(action_type: str) -> dict:
2259+
"""
2260+
Get action status for a specific action type. Works only with ROS 2.
2261+
2262+
Args:
2263+
action_type (str): The action type (e.g., 'action_tutorials_interfaces/action/Fibonacci')
2264+
2265+
Returns:
2266+
dict: Contains action status information including active goals and their status.
2267+
"""
2268+
# Validate input
2269+
if not action_type or not action_type.strip():
2270+
return {"error": "Action type cannot be empty"}
2271+
2272+
# Extract action name from action type
2273+
# For example: 'action_tutorials_interfaces/action/Fibonacci' -> '/fibonacci'
2274+
action_name = f"/{action_type.split('/')[-1].lower()}"
2275+
2276+
# Try to get action status by subscribing to the status topic
2277+
status_topic = f"{action_name}/_action/status"
2278+
status_msg_type = "action_msgs/msg/GoalStatusArray"
2279+
2280+
try:
2281+
# Subscribe to action status topic
2282+
with ws_manager:
2283+
message = {
2284+
"op": "subscribe",
2285+
"topic": status_topic,
2286+
"type": status_msg_type,
2287+
"id": f"get_action_status_{action_name.replace('/', '_')}",
2288+
}
2289+
2290+
send_error = ws_manager.send(message)
2291+
if send_error:
2292+
return {
2293+
"action_type": action_type,
2294+
"action_name": action_name,
2295+
"success": False,
2296+
"error": f"Failed to subscribe to status topic: {send_error}",
2297+
}
2298+
2299+
# Wait for status message
2300+
response = ws_manager.receive(timeout=3.0)
2301+
if not response:
2302+
return {
2303+
"action_type": action_type,
2304+
"action_name": action_name,
2305+
"success": False,
2306+
"error": "No response from action status topic",
2307+
}
2308+
2309+
response_data = json.loads(response)
2310+
2311+
if response_data.get("op") == "status" and response_data.get("level") == "error":
2312+
return {"error": f"Action status error: {response_data.get('msg', 'Unknown error')}"}
2313+
2314+
if "msg" not in response_data or "status_list" not in response_data["msg"]:
2315+
return {
2316+
"action_type": action_type,
2317+
"action_name": action_name,
2318+
"success": True,
2319+
"active_goals": [],
2320+
"goal_count": 0,
2321+
"note": f"No active goals found for action {action_name}"
2322+
}
2323+
2324+
status_list = response_data["msg"]["status_list"]
2325+
status_map = {
2326+
0: "STATUS_UNKNOWN", 1: "STATUS_ACCEPTED", 2: "STATUS_EXECUTING",
2327+
3: "STATUS_CANCELING", 4: "STATUS_SUCCEEDED", 5: "STATUS_CANCELED", 6: "STATUS_ABORTED"
2328+
}
2329+
2330+
active_goals = []
2331+
for status_item in status_list:
2332+
goal_info = status_item.get("goal_info", {})
2333+
goal_id = goal_info.get("goal_id", {}).get("uuid", "unknown")
2334+
status = status_item.get("status", -1)
2335+
stamp = goal_info.get("stamp", {})
2336+
2337+
active_goals.append({
2338+
"goal_id": goal_id,
2339+
"status": status,
2340+
"status_text": status_map.get(status, "UNKNOWN"),
2341+
"timestamp": f"{stamp.get('sec', 0)}.{stamp.get('nanosec', 0)}"
2342+
})
2343+
2344+
return {
2345+
"action_type": action_type,
2346+
"action_name": action_name,
2347+
"success": True,
2348+
"active_goals": active_goals,
2349+
"goal_count": len(active_goals),
2350+
"note": f"Found {len(active_goals)} active goal(s) for action {action_name}"
2351+
}
2352+
2353+
except json.JSONDecodeError as e:
2354+
return {"error": f"Failed to parse status response: {str(e)}"}
2355+
except Exception as e:
2356+
return {
2357+
"action_type": action_type,
2358+
"action_name": action_name,
2359+
"success": False,
2360+
"error": f"Failed to get action status: {str(e)}",
2361+
}
2362+
2363+
21892364
@mcp.tool(
21902365
description=(
21912366
"Get comprehensive information about all actions including types and available actions. Works only with ROS 2.\n"
@@ -2441,7 +2616,7 @@ def cancel_action_goal(action_name: str, goal_id: str) -> dict:
24412616
)
24422617
def ping_robot(ip: str, port: int, ping_timeout: float = 2.0, port_timeout: float = 2.0) -> dict:
24432618
"""
2444-
Ping an IP address and check if a specific port is open. Works only with ROS 2.
2619+
Ping an IP address and check if a specific port is open.
24452620
24462621
Args:
24472622
ip (str): The IP address to ping (e.g., '192.168.1.100')

0 commit comments

Comments
 (0)