@@ -496,7 +496,7 @@ def inspect_all_topics() -> dict:
496496def 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)
24422617def 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