22import inspect
33import logging
44import time
5+ import threading
56from typing import Dict , Optional , Type
67
78from Backend .robot_controllers .base_robot_controller import BaseRobotController
8- from Backend .config_backend import AVAILABLE_ROBOTS
9+ from Backend .config_backend import AVAILABLE_ROBOTS , ALLOWED_COMMANDS
910
1011logger = logging .getLogger ("cobot_backend" )
1112
@@ -32,10 +33,11 @@ def _load_controllers() -> Dict[str, Type[BaseRobotController]]:
3233class MessageHandler :
3334 """Processes incoming commands and dispatches them to the robot controller."""
3435
35- ALLOWED_COMMANDS = ["ping" , "get_status" , "execute_sequence" ]
36-
37- def __init__ (self ):
36+ def __init__ (self ) -> None :
3837 self .robot : Optional [BaseRobotController ] = None
38+ self ._script_stop_event : threading .Event = threading .Event ()
39+ self ._script_thread : Optional [threading .Thread ] = None
40+
3941 for robot_type , cls in CONTROLLERS .items ():
4042 logger .debug ("Controller available: '%s' -> %s" , robot_type , cls .__name__ )
4143
@@ -54,13 +56,18 @@ def process_message(self, message: dict) -> dict:
5456 command = message .get ("command" , "" )
5557 data = message .get ("data" , {})
5658
57- if command not in self . ALLOWED_COMMANDS :
59+ if command not in ALLOWED_COMMANDS :
5860 return self ._unknown_command (command )
5961
6062 commands = {
6163 "ping" : self ._answer_ping ,
6264 "get_status" : self ._send_status ,
6365 "execute_sequence" : lambda : self ._execute_sequence (data ),
66+ "save_script" : lambda : self ._save_script (data ),
67+ "run_script" : lambda : self ._run_script (data ),
68+ "stop_script" : self ._stop_script ,
69+ "get_script_status" : self ._get_script_status ,
70+ "delete_script" : lambda : self ._delete_script (data ),
6471 }
6572
6673 return commands [command ]()
@@ -69,6 +76,8 @@ def process_message(self, message: dict) -> dict:
6976 logger .exception ("Error processing message: %s" , message )
7077 return self ._formatted_response ("error" , {"error message" : str (e )})
7178
79+
80+
7281 def _ensure_robot_ready (self , robot_type : str ) -> dict :
7382 self ._current_robot_type = robot_type
7483 expected_class = CONTROLLERS [robot_type ]
@@ -187,7 +196,7 @@ def _process_command(self, command: dict, robot: BaseRobotController) -> dict:
187196 offset = None
188197 if target .get ("type" ) == "offset_from_pose" :
189198 raw = target .get ("offset" , {})
190- offset = [raw [ "x_mm" ], raw [ "y_mm" ], raw [ "z_mm" ] ]
199+ offset = [raw . get ( "x_mm" , 0.0 ), raw . get ( "y_mm" , 0.0 ), raw . get ( "z_mm" , 0.0 ) ]
191200
192201 if motion_type == "moveJ" :
193202 return robot .move_joint (pose , speed , offset )
@@ -230,4 +239,114 @@ def _process_command(self, command: dict, robot: BaseRobotController) -> dict:
230239 return self ._ensure_robot_ready (self ._current_robot_type )
231240
232241 else :
233- return {"success" : False , "message" : f"Unknown action: { action } " }
242+ return {"success" : False , "message" : f"Unknown action: { action } " }
243+
244+ def _save_script (self , data : dict ) -> dict :
245+ robot_type = data .get ("robot" )
246+ script_name = data .get ("script_name" )
247+ commands = data .get ("commands" , [])
248+
249+ if not robot_type or robot_type not in CONTROLLERS :
250+ return self ._formatted_response ("rejected" , {"message" : f"Unsupported robot type: { robot_type } " })
251+ if not script_name or not isinstance (commands , list ):
252+ return self ._formatted_response ("rejected" , {"message" : "Missing script_name or commands" })
253+
254+ result = self ._ensure_robot_ready (robot_type )
255+ if not result ["success" ]:
256+ return self ._formatted_response ("rejected" ,
257+ {"message" : f"Could not connect to { robot_type } : { result ['message' ]} " })
258+
259+ result = self .robot .save_script (script_name , commands )
260+ if not result ["success" ]:
261+ return self ._formatted_response ("error" , {"message" : result ["message" ]})
262+
263+ logger .info ("Script '%s' saved for robot '%s' with %d command(s)" , script_name , robot_type , len (commands ))
264+ return self ._formatted_response ("success" , {"message" : f"Script '{ script_name } ' saved" })
265+
266+ def _run_script (self , data : dict ) -> dict :
267+ robot_type = data .get ("robot" )
268+ script_name = data .get ("script_name" )
269+ loop = data .get ("loop" , 1 )
270+
271+ if not robot_type or robot_type not in CONTROLLERS :
272+ return self ._formatted_response ("rejected" , {"message" : f"Unsupported robot type: { robot_type } " })
273+
274+ result = self ._ensure_robot_ready (robot_type )
275+ if not result ["success" ]:
276+ return self ._formatted_response ("rejected" ,
277+ {"message" : f"Could not connect to { robot_type } : { result ['message' ]} " })
278+
279+ script = self .robot .get_script (script_name )
280+ if script is None :
281+ return self ._formatted_response ("rejected" , {"message" : f"Unknown script: '{ script_name } '" })
282+
283+ if self ._script_thread and self ._script_thread .is_alive ():
284+ return self ._formatted_response ("rejected" , {"message" : "A script is already running" })
285+
286+ validation_errors = [
287+ error
288+ for cmd in script
289+ if (error := self ._validate_command (cmd , self .robot )) is not None
290+ ]
291+ if validation_errors :
292+ logger .warning ("Script '%s' rejected: %s" , script_name , validation_errors )
293+ return self ._formatted_response ("rejected" , {"reasons" : validation_errors })
294+
295+ self ._script_stop_event .clear ()
296+ self ._script_thread = threading .Thread (
297+ target = self ._run_script_loop ,
298+ args = (script_name , loop , script ),
299+ daemon = True ,
300+ name = "thread_script_loop"
301+ )
302+ self ._script_thread .start ()
303+ logger .info ("Script '%s' started for robot '%s', loop=%d" , script_name , robot_type , loop )
304+ return self ._formatted_response ("success" , {"message" : f"Script '{ script_name } ' started" })
305+
306+ def _stop_script (self ) -> dict :
307+ """Signal the running script loop to stop after the current command."""
308+ self ._script_stop_event .set ()
309+ logger .info ("Stop signal sent to script loop" )
310+ return self ._formatted_response ("success" , {"message" : "Stop signal sent" })
311+
312+ def _run_script_loop (self , script_name : str , loop : int , commands : list ) -> None :
313+ """Execute script commands in a loop. Checks stop event between commands."""
314+ iteration = 0
315+ while not self ._script_stop_event .is_set ():
316+ logger .info ("Script '%s' — iteration %d" , script_name , iteration + 1 )
317+ for cmd in commands :
318+ if self ._script_stop_event .is_set ():
319+ break
320+ result = self ._process_command (cmd , self .robot )
321+ if not result ["success" ]:
322+ logger .error ("Script '%s' aborted at command: %s" , script_name , result ["message" ])
323+ return
324+
325+ iteration += 1
326+ if loop != - 1 and iteration >= loop :
327+ break
328+
329+ logger .info ("Script '%s' finished after %d iteration(s)" , script_name , iteration )
330+
331+ def _get_script_status (self ) -> dict :
332+ is_running = self ._script_thread is not None and self ._script_thread .is_alive ()
333+ return self ._formatted_response ("success" , {"is_running" : is_running })
334+
335+ def _delete_script (self , data : dict ) -> dict :
336+ robot_type = data .get ("robot" )
337+ script_name = data .get ("script_name" )
338+
339+ if not robot_type or robot_type not in CONTROLLERS :
340+ return self ._formatted_response ("rejected" , {"message" : f"Unsupported robot type: { robot_type } " })
341+
342+ result = self ._ensure_robot_ready (robot_type )
343+ if not result ["success" ]:
344+ return self ._formatted_response ("rejected" ,
345+ {"message" : f"Could not connect to { robot_type } : { result ['message' ]} " })
346+
347+ result = self .robot .delete_script (script_name )
348+ if not result ["success" ]:
349+ return self ._formatted_response ("error" , {"message" : result ["message" ]})
350+
351+ logger .info ("Script '%s' deleted" , script_name )
352+ return self ._formatted_response ("success" , {"message" : f"Script '{ script_name } ' deleted" })
0 commit comments