From 7138789eea6e2bb74e45d3ad0766df6b754509e4 Mon Sep 17 00:00:00 2001 From: Etienne Pallier Date: Thu, 16 Jun 2022 02:25:48 +0200 Subject: [PATCH] new Agent with 3 states and better exceptions... --- src/core/pyros_django/agent/Agent.py | 492 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------------------------------------------- src/core/pyros_django/agent/AgentDevice.py | 7 ++++++- src/core/pyros_django/agent/doc/Agent_activity_diag.pu | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------- src/core/pyros_django/common/models.py | 26 ++++++++++++++++---------- 4 files changed, 537 insertions(+), 136 deletions(-) diff --git a/src/core/pyros_django/agent/Agent.py b/src/core/pyros_django/agent/Agent.py index dafd33a..18ffc99 100755 --- a/src/core/pyros_django/agent/Agent.py +++ b/src/core/pyros_django/agent/Agent.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -VERSION = "0.5" +VERSION = "1.0" #DEBUG=True #DEBUG=False @@ -227,12 +227,46 @@ class StoppableThreadEvenWhenSleeping(threading.Thread): ''' +### +# ================================================================= +# EXCEPTIONS classes +# ================================================================= +### -""" -================================================================= - class Agent -================================================================= -""" +class UnknownCmdException(Exception): + ''' Raised when an Agent (specific) cmd is NOT known by the agent ''' + # pass + def __init__(self, cmd_name): + self.cmd_name = cmd_name + def __str__(self): + return f"The Agent command '{self.cmd_name}' is unknown by the agent" + #return f"({type(self).__name__}): Device Generic command has no implementation in the controller" + +class AgentCmdUnimplementedException(Exception): + ''' Raised when an Agent Specific cmd is known by the agent but not implemented ''' + # pass + def __init__(self, cmd_name): + self.cmd_name = cmd_name + def __str__(self): + return f"The Agent (General or Specific) command '{self.cmd_name}' is known by the agent but not implemented" + #return f"({type(self).__name__}): Device Generic command has no implementation in the controller" + +class AgentCmdBadArgsException(Exception): + ''' Raised when an Agent cmd has bad or missing argument(s) ''' + # pass + def __init__(self, cmd_name): + self.cmd_name = cmd_name + def __str__(self): + return f"The Agent command '{self.cmd_name}' has bad or missing argument(s)" + #return f"({type(self).__name__}): Device Generic command has no implementation in the controller" + + + +### +# ================================================================= +# class Agent +# ================================================================= +### class Agent: """ @@ -258,7 +292,7 @@ class Agent: DEBUG_MODE = False WITH_SIMULATOR = False TEST_MODE = False - + # Default LOG level is INFO #PYROS_DEFAULT_GLOBAL_LOG_LEVEL = LogPyros.LOG_LEVEL_INFO # INFO @@ -285,9 +319,11 @@ class Agent: # Default scenario to be executed #TEST_COMMANDS = iter([ TEST_COMMANDS_LIST = [ - "set_state:active", - "set_state:idle", + "Agent set_state ATTENTIVE", + "Agent set_state ROUTINE", + "Agent set_state IDLE", + ''' # specific0 not_executed_because_idle "specific0", @@ -333,6 +369,7 @@ class Agent: # and not even be added to the database command table "set_state:active", "specific10" + ''' ] #TEST_COMMANDS = iter(TEST_COMMANDS_LIST) @@ -367,9 +404,22 @@ class Agent: ###STATUS_SPECIFIC_PROCESS = "IN_SPECIFIC_PROCESS" STATUS_EXIT = "EXITING" - # Modes - MODE_ACTIVE = "ACTIVE" + + ''' + MODES + + In all modes, the Agent listens to commands sent to him and executes Agent level GENERAL ones. + + - MODE_IDLE : "idle" mode, does nothing, only executes Agent level GENERAL commands (DO_RESTART, DO_EXIT, DO_ABORT, DO_FLUSH...) + - MODE_ROUTINE : idem IDLE + executes routine process (before & after) + - MODE_ATTENTIVE : idem ROUTINE + executes Agent level SPECIFIC commands (commands specific to this agent, that only this agent understands and can execute) + + Default mode is MODE_ATTENTIVE (most active mode) + + ''' MODE_IDLE = "IDLE" + MODE_ROUTINE = "ROUTINE" + MODE_ATTENTIVE = "ATTENTIVE" ''' Moved to more central file : config.config_base PYROS_DJANGO_BASE_DIR = Path("src/core/pyros_django") # pathlib @@ -414,6 +464,8 @@ class Agent: # new obsconfig init for agent: ##def __init__(self, RUN_IN_THREAD=True): def __init__(self): + # Agent is by default in mode ATTENTIVE (most active mode) + self.mode = self.MODE_ATTENTIVE log.addHandler(handler_filebyagent(logging.INFO, self.__class__.__name__)) log.debug("start Agent init") obs_config_file_path = os.environ["PATH_TO_OBSCONF_FILE"] @@ -440,7 +492,7 @@ class Agent: self.TEST_COMMANDS = iter(self.TEST_COMMANDS_LIST) ##self.RUN_IN_THREAD = RUN_IN_THREAD self._set_status(self.STATUS_LAUNCH) - self._set_idle() + ####self._set_idle() # Create 1st survey if none #tmp = AgentSurvey.objects.filter(name=self.name) @@ -756,26 +808,16 @@ class Agent: # TEST MODE ONLY # IF in test mode but with REAL devices (no SIMULATOR), delete all dangerous commands from the test commands list scenario: - if self.TEST_MODE: - log.debug("\n!!! In TEST mode !!! => preparing to run a scenario of test commands") - log.debug("- Current test commands list scenario is:\n" + str(self.TEST_COMMANDS_LIST)) - if not self.WITH_SIMULATOR: - log.debug("\n!!! In TEST but no SIMULATOR mode (using REAL device) !!! => removing dangerous commands for real devices... :") - TEST_COMMANDS_LIST_copy = self.TEST_COMMANDS_LIST.copy() - for cmd in TEST_COMMANDS_LIST_copy: - cmd_key = cmd.split()[1] - if ("set_" in cmd_key) or ("do_start" in cmd_key) or cmd_key in ["do_init", "do_goto", "do_open", "do_close"]: - self.TEST_COMMANDS_LIST.remove(cmd) - log.debug("- NEW test commands list scenario is:\n" + self.TEST_COMMANDS_LIST, '\n') - - self._DO_EXIT = False - self._DO_RESTART = True + if self.TEST_MODE: self.TEST_prepare() + + #self._DO_EXIT = False + self.DO_RESTART_LOOP = True # Will loop again only if _DO_RESTART is True - while self._DO_RESTART: - self._DO_RESTART = False + while self.DO_RESTART_LOOP: + #self._DO_RESTART = False self.start_time = time.time() - log.debug("on est ici: " + os.getcwd()) + #log.debug("on est ici: " + os.getcwd()) self._load_config() @@ -794,12 +836,12 @@ class Agent: AgentCmd.delete_commands_with_running_status_for_agent(self.name) self._iter_num = 1 - self._DO_MAIN_LOOP = True + self.DO_MAIN_LOOP = True # Main loop - while self._DO_MAIN_LOOP: + while self.DO_MAIN_LOOP: try: self._main_loop(nb_iter,FOR_REAL) - if not self._DO_MAIN_LOOP: break + #if not self.DO_MAIN_LOOP: break except KeyboardInterrupt: # CTRL-C # In case of CTRL-C, kill the current thread (process) before dying (in error) log.info("CTRL-C Interrupted, I kill the current thread (process) before exiting (if exists)") @@ -814,15 +856,25 @@ class Agent: #def _kill_running_device_cmd_if_exists(self, abort_cmd_sender): def do_things_before_exit(self, abort_cmd_sender): - pass + #pass + log.info("Before exiting, Here are (if exists) the current (still) pending commands (time ordered) :") + commands = AgentCmd.get_pending_and_running_commands_for_agent(self.name) + AgentCmd.show_commands(commands, True) + print("TODO: flush commands !!!") + #if self.TEST_MODE and self.TEST_WITH_FINAL_TEST and self.TEST_COMMANDS_DEST == "myself": self.simulator_test_results() + if self.TEST_MODE and self.TEST_WITH_FINAL_TEST: + self._TEST_test_results() + #self._DO_EXIT=True + #exit(0) + def _main_loop(self, nb_iter:int=None, FOR_REAL:bool=True): self._main_loop_start(nb_iter) - if not self._DO_MAIN_LOOP: return + #if not self.DO_MAIN_LOOP: return - self._load_config() # only if changed + self._reload_config_if_changed() # only if changed # better to do this in a subclass #self.show_config() @@ -833,20 +885,34 @@ class Agent: #self.printd("====== START COMMMANDS PROCESSING ======") + # SIMU + #self.send_cmd_to("AgentScheduler", "do_replan") + # ROUTINE process # To be overriden to be specified by subclass - self._routine_process_before() + if not self.IS_IDLE(): self._routine_process_before() #self.printd("I am IDLE, so I bypass the routine_process (do not send any new command)") # Processing the next pending command if exists - self._command_process_if_exists() + # If Agent general level command like (DO_RESTART, DO_EXIT, DO_ABORT) + # => will set DO_MAIN_LOOP=False and/or DO_RESTART_LOOP + + print() + print() + log.info("*"*10 + " NEXT COMMAND PROCESSING (START) " + "*"*10 + '\n') + self._process_next_command_if_exists() + log.info("*"*10 + " NEXT COMMAND PROCESSING (END) " + "*"*10 + "\n") + + if not self.DO_MAIN_LOOP: return + ''' # if restart, exit this loop to restart from beginning - if self._DO_RESTART or self._DO_EXIT: - self._DO_MAIN_LOOP = False + if self.DO_RESTART or self.DO_EXIT or self.DO_ABORT: + self.DO_MAIN_LOOP = False return + ''' - self._routine_process_after() + if not self.IS_IDLE(): self._routine_process_after() #self.printd("====== END COMMMANDS PROCESSING ======") @@ -858,6 +924,8 @@ class Agent: self._iter_num += 1 + + def _main_loop_start(self, nb_iter:int): for i in range(3): print() @@ -872,7 +940,7 @@ class Agent: # Bad number of iterations or nb iterations reached => exit if nb_iter <= 0 or nb_iter < self._iter_num: log.info(f"Exit because number of iterations asked ({nb_iter}) has been reached") - self._DO_MAIN_LOOP = False + self.DO_MAIN_LOOP = False return # Wait a random number of sec before starting iteration @@ -889,14 +957,15 @@ class Agent: #self._kill_running_device_cmd_if_exists(self.name) self.do_things_before_exit(self.name) #if self.TEST_MODE and self.TEST_WITH_FINAL_TEST: self._TEST_test_results() - self._DO_MAIN_LOOP = False + self.DO_MAIN_LOOP = False return self._set_status(self.STATUS_MAIN_LOOP) self.show_mode_and_status() - def process_other_level_cmd(self): + # To be overriden by subclass (AgentDevice) + def process_device_level_cmd(self): pass ''' log.info("(DEVICE LEVEL CMD)") @@ -914,12 +983,12 @@ class Agent: - def _command_process_if_exists(self): + def _process_next_command_if_exists(self): ''' Processing the next pending command if exists ''' - print() - print() - log.info("*"*10 + " NEXT COMMAND PROCESSING (START) " + "*"*10 + '\n') + #print() + #print() + #log.info("*"*10 + " NEXT COMMAND PROCESSING (START) " + "*"*10 + '\n') # Purge commands (every N iterations, delete old commands) @@ -933,49 +1002,101 @@ class Agent: cmd = self._get_next_valid_and_not_running_command() #self._set_status(self.STATUS_GENERAL_PROCESS) #if cmd: self.command_process(cmd) - if cmd: - log.info('-'*6) - log.info('-'*6 + " RECEIVED NEW COMMAND TO PROCESS: ") - log.info('-'*6 + str(cmd)) - log.info('-'*6) - - # CASE 1 - AGENT LEVEL command - # => I process it directly without asking my DC - # => Simple action, short execution time, so I execute it directly (in current main thread, not in parallel) - if self._is_agent_level_cmd(cmd): - log.info("(AGENT LEVEL CMD)") - try: - self._exec_agent_cmd(cmd) - except AttributeError as e: - #self.log_e(f"EXCEPTION: Agent level specific command '{cmd.name}' unknown (not implemented as a function) :", e) - #self.log_e("Thus => I ignore this command...") - log.e(f"EXCEPTION: Agent level specific command '{cmd.name}' unknown (not implemented as a function) :", e) - log.e("Thus => I ignore this command...") - cmd.set_result("ERROR: INVALID AGENT LEVEL SPECIFIC COMMAND") - cmd.set_as_pending() - cmd.set_as_skipped() - - # CASE 2 - DEVICE LEVEL command - # => I delegate it to my DC - # => Execute it only if I am active and no currently running another device level cmd - # => Long execution time, so I will execute it in parallel (in a new thread or process) - #elif self.is_device_level_cmd(cmd): - elif self.is_other_level_cmd(cmd): - self.process_other_level_cmd(cmd) - - # CASE 3 - INVALID COMMAND - else: - #raise Exception("INVALID COMMAND: " + cmd.name) - log.warning("******************************************************") - log.warning("*************** ERROR: INVALID COMMAND (UNKNOWN) ***************") - log.warning("******************************************************") - log.warning("Thus => I ignore this command...") - cmd.set_result("ERROR: INVALID COMMAND") - cmd.set_as_skipped() + + # SIMU + #cmd = "do_replan" + #self.send_cmd_to("AgentScheduler", "do_replan") + + # No new command => nothing to do + if not cmd: return - print() - log.info("*"*10 + " NEXT COMMAND PROCESSING (END) " + "*"*10 + "\n") + log.info('-'*6) + log.info('-'*6 + " RECEIVED NEW COMMAND TO PROCESS: ") + log.info('-'*6 + str(cmd)) + log.info('-'*6) + cmd.set_read_time() + + # CASE 1 - AGENT GENERAL command + # (DO_RESTART, DO_EXIST, DO_ABORT, DO_ABORT_COMMAND, DO_FLUSH_COMMANDS, ...) + # This processing can set DO_MAIN_LOOP and DO_RESTART_LOOP to False + if self._is_agent_general_cmd(cmd): + try: + self._process_agent_general_cmd(cmd) + except AgentCmdUnimplementedException as e: + print(e) + cmd.set_as_skipped("ERROR: Unimplemented Agent General command") + self._cleanup_before_exist() + raise + #except ValueError as e: + except AgentCmdBadArgsException as e: + print(e) + cmd.set_as_skipped("ERROR: Bad Argument(s)") + self._cleanup_before_exit() + raise + #raise AgentCmdBadArgsException(cmd.name) + # Must I stop or restart ? + if cmd.name in ('DO_RESTART','DO_EXIT','OO_ABORT') : + self.DO_MAIN_LOOP = False + if cmd.name == 'OO_ABORT': + self._abort_current_running_cmd_if_exists() + else : + self._cleanup_before_exit() + if cmd.name != 'OO_RESTART': + self.DO_RESTART_LOOP = False + return + + # CASE 2 - AGENT SPECIFIC command + # => Simple action, short execution time, so I execute it directly (in current main thread, not in parallel) + if self._is_agent_specific_cmd(cmd): + #log.info("(AGENT LEVEL CMD)") + if not self._IS_ATTENTIVE(): + cmd.set_as_skipped("Skipped because I am not ATTENTIVE") + return + try: + self._process_agent_specific_cmd(cmd) + #self._exec_agent_cmd(cmd) + #except AttributeError as e: + except AgentCmdUnimplementedException as e: + print(e) + #self.log_e(f"EXCEPTION: Agent level specific command '{cmd.name}' unknown (not implemented as a function) :", e) + #self.log_e("Thus => I ignore this command...") + log.e(f"EXCEPTION: Agent specific command '{cmd.name}' unknown (not implemented as a function) :", e) + #log.e("Thus => I ignore this command...") + #cmd.set_result("ERROR: UNIMPLEMENTED AGENT SPECIFIC COMMAND", False) + #cmd.set_as_pending() + cmd.set_as_skipped("ERROR: UNIMPLEMENTED AGENT SPECIFIC COMMAND") + self._cleanup_before_exit() + raise AgentCmdUnimplementedException(cmd.name) + return + + # CASE 3 - OTHER level command (DEVsICE level, only for AgentDevice) + if self.is_device_level_cmd(cmd): + self.process_device_level_cmd(cmd) + return + + # CASE 4 - INVALID COMMAND + #raise Exception("INVALID COMMAND: " + cmd.name) + log.warning("******************************************************") + log.warning("*************** ERROR: INVALID COMMAND (UNKNOWN) ***************") + log.warning("******************************************************") + #log.warning("Thus => I ignore this command...") + #cmd.set_result("ERROR: INVALID COMMAND") + cmd.set_as_skipped("ERROR: INVALID AGENT COMMAND") + self._cleanup_before_exit() + raise UnknownCmdException(cmd.name) + + #print() + #log.info("*"*10 + " NEXT COMMAND PROCESSING (END) " + "*"*10 + "\n") + + + + + def _abort_current_running_cmd_if_exists(self): + pass + + def _cleanup_before_exit(self): + self.do_things_before_exit() def print_TEST_MODE(self): @@ -1041,7 +1162,7 @@ class Agent: # To be overridden by subclasses def routine_process_after_body(self): - if self.TEST_MODE: self._test_routine_process() + if self.TEST_MODE: self._TEST_test_routine_process() """ def purge_commands(self): @@ -1096,17 +1217,18 @@ class Agent: log.info(f"[NEW MODE {mode}]") self.mode = mode - def _is_active(self): - return self.mode == self.MODE_ACTIVE - def _is_idle(self): - return not self._is_active() + # Test mode + def IS_IDLE(self): return self.mode == self.MODE_IDLE + def IS_ROUTINE(self): return self.mode == self.MODE_ROUTINE + def IS_ATTENTIVE(self): return self.mode == self.MODE_ATTENTIVE - def _set_active(self): - self._set_mode(self.MODE_ACTIVE) - log.info("set active mode") - def _set_idle(self): + def set_idle(self): self._set_mode(self.MODE_IDLE) + def set_routine(self): + self._set_mode(self.MODE_ROUTINE) + def set_attentive(self): + self._set_mode(self.MODE_ATTENTIVE) def show_mode_and_status(self): log.info(f"CURRENT MODE is {self.mode} (status is {self.status})") @@ -1149,7 +1271,8 @@ class Agent: def _set_mode_from_config(self, agent_name): # all agent are active ? - mode = self.MODE_ACTIVE + #mode = self.MODE_ACTIVE + mode = self.MODE_ATTENTIVE self._set_mode(mode) return True # old config @@ -1179,6 +1302,9 @@ class Agent: log.debug("Initializing...") self._set_status(self.STATUS_INIT) + def _reload_config_if_changed(self): + self._load_config() + def _load_config(self): """ TODO: @@ -1325,13 +1451,14 @@ class Agent: real_agent_name = self._get_real_agent_name(to_agent) except KeyError as e: ''' - # real_agent_name = self._get_real_agent_name(to_agent) - # if not real_agent_name: + real_agent_name = self._get_real_agent_name(to_agent) + if not real_agent_name: + return AgentCmd.create(self.name, to_agent, cmd_name, cmd_args) # log.e("UNKNOWN AgentDevice ALIAS", to_agent) # #self.log_e("Exception raised", e) # log.e(f"=> Thus, I do not send this command '{cmd_name}'") # return None - return AgentCmd.create(self.name, to_agent, cmd_name, cmd_args) + return AgentCmd.create(self.name, real_agent_name, cmd_name, cmd_args) ''' return AgentCmd( sender=self.name, @@ -1518,6 +1645,128 @@ class Agent: + def _process_agent_general_cmd(self, cmd:AgentCmd): + # GENERAL command (related to any agent) + + #self.print(f"Starting execution of an AGENT LEVEL cmd {cmd}...") + log.info(f"Starting execution of an AGENT GENERAL cmd...") + + # Update read time to say that the command has been READ + #cmd.set_read_time() + cmd.set_as_running() + + log.info("(Agent level GENERAL CMD)") + _,cmd_name,cmd_args = cmd.get_full_name_parts() + #cmd_name, cmd_args = cmd.tokenize() + #if cmd.name == "set_state:active": + #elif cmd.name == "set_state:idle": + + # Default result (null) + result = None + + #if cmd_name in ("do_abort", "do_exit", "do_restart_init"): + if cmd_name in ("do_abort", "do_exit", "do_restart"): + #self.printd("Current pending commands are:") + #Command.show_commands(self._pending_commands) + log.info("Stopping/Aborting current executing command if exists:") + #self._kill_running_device_cmd_if_exists(cmd.sender) + ''' + self.do_things_before_exit(cmd.sender) + if cmd_name == "do_restart_init": + log.info("restart_init received: Restarting from init()") + self._DO_RESTART=True + elif cmd.name == "do_exit": + self._DO_EXIT=True + cmd.set_result('SHOULD BE DONE NOW') + ''' + result = "STOP or RESTART" + + elif cmd_name == "set_state": + #if not cmd_args: raise ValueError() + if not cmd_args: raise AgentCmdBadArgsException(cmd.name) + state = cmd_args[0] + if state == "IDLE": self.set_idle() + elif state == "ROUTINE": self.set_routine() + elif state == "ATTENTIVE": self.set_attentive() + else: raise AgentCmdBadArgsException(cmd.name) + #cmd.set_result("I am now " + state) + result = "I am now " + state + #time.sleep(1) + #self.waitfor(1) + + #elif cmd_name in ("do_flush_commands"): + elif cmd_name == "do_flush_commands": + "flush_commands received: Delete all pending commands" + AgentCmd.delete_pending_commands_for_agent(self.name) + #cmd.set_result('DONE') + result = "FLUSH DONE" + + elif cmd_name == "do_eval": + #if not cmd_args: raise ValueError() + if not cmd_args: raise AgentCmdBadArgsException(cmd.name) + #cmd.set_result(eval(cmd_args)) + result = eval(cmd_args) + + else: + raise UnknownCmdException(cmd.name) + ''' + name = cmd.name + args = None + if " " in name: name,args = name.split() + if name == "do_eval": + if args==None: raise(ValueError) + cmd.set_result(eval(args)) + ''' + + cmd.set_as_processed(result) + log.info("...Agent level GENERAL cmd has been executed") + + ''' + # If cmd is "do_exit", kill myself (without any question, this is an order soldier !) + # This "do_exit" should normally kill any current thread (to be checked...) + if cmd.name == "do_exit": + log.info("Before exiting, Here are (if exists) the current (still) pending commands (time ordered) :") + commands = AgentCmd.get_pending_and_running_commands_for_agent(self.name) + AgentCmd.show_commands(commands, True) + #if self.TEST_MODE and self.TEST_WITH_FINAL_TEST and self.TEST_COMMANDS_DEST == "myself": self.simulator_test_results() + if self.TEST_MODE and self.TEST_WITH_FINAL_TEST: + self._TEST_test_results() + #self._DO_EXIT=True + #exit(0) + ''' + + + + + def _process_agent_specific_cmd(self, cmd:AgentCmd): + + #self.print(f"Starting execution of an AGENT LEVEL cmd {cmd}...") + log.info(f"Starting execution of an AGENT LEVEL cmd...") + + # Update read time to say that the command has been READ + #cmd.set_read_time(False) + cmd.set_as_running() + + log.info("(Agent SPECIFIC cmd)") + # Execute method self."cmd.name"() + # This can raise an exception (caught by this method caller) + self.exec_cmd_from_its_name(cmd) + ''' + try: + except AttributeError as e: + self.printd(f"EXCEPTION: Agent level specific command '{cmd.name}' unknown (not implemented as a function) :", e) + self.printd("Thus => I ignore this command...") + cmd.set_result("ERROR: INVALID AGENT LEVEL SPECIFIC COMMAND") + cmd.set_as_pending() + cmd.set_as_skipped() + return + ''' + #cmd.set_result("Agent level SPECIFIC cmd done") + cmd.set_as_processed("Agent SPECIFIC cmd done") + log.info("...Agent SPECIFIC cmd has been executed") + + + ''' def do_log(self): @@ -1531,13 +1780,23 @@ class Agent: def exec_cmd_from_its_name(self, cmd:AgentCmd): func = cmd.name - if cmd.args: - return getattr(self, func)(*cmd.args) - else: - return getattr(self, func)() + try: + if cmd.args: + return getattr(self, func)(*cmd.args) + else: + return getattr(self, func)() + except AttributeError as e: + # TODO: AgentCmdBadArgsException si la fonction existe dans le code de l'agent + print("I know this specific command but it is not yet implemented : ", func) + print(e) + raise AgentCmdUnimplementedException(cmd.name) def _is_agent_level_cmd(self, cmd:AgentCmd): - return cmd.is_agent_general_cmd() or self._is_agent_specific_cmd(cmd) + return self._is_agent_general_cmd(cmd) or self._is_agent_specific_cmd(cmd) + + def _is_agent_general_cmd(self, cmd:AgentCmd): + return cmd.is_agent_general_cmd() + ''' def _exec_agent_cmd(self, cmd:Command): @@ -1578,8 +1837,7 @@ class Agent: # To be overriden by subclass (AgentDevice...) # @abstract - #def is_device_level_cmd(self, cmd): - def is_other_level_cmd(self, cmd): + def is_device_level_cmd(self, cmd): return False ''' @@ -1641,7 +1899,7 @@ class Agent: #self._nb_test_cmds += 1 """ - def _test_routine_process(self): + def _TEST_test_routine_process(self): """ TEST MODE ONLY """ @@ -1713,7 +1971,7 @@ class Agent: self._cmdts = self._TEST_get_next_command_to_send() # No more command to send (from simulator) => return and EXIT if self._cmdts is None: - self._DO_MAIN_LOOP = False + self.DO_MAIN_LOOP = False return # Send cmd (= set as pending and save) log.info("***") @@ -1787,6 +2045,18 @@ class Agent: nb_asserted2 = self.TEST_test_results_main(commands) self._TEST_test_results_end(nb_asserted) + def TEST_prepare(self): + log.debug("\n!!! In TEST mode !!! => preparing to run a scenario of test commands") + log.debug("- Current test commands list scenario is:\n" + str(self.TEST_COMMANDS_LIST)) + if not self.WITH_SIMULATOR: + log.debug("\n!!! In TEST but no SIMULATOR mode (using REAL device) !!! => removing dangerous commands for real devices... :") + TEST_COMMANDS_LIST_copy = self.TEST_COMMANDS_LIST.copy() + for cmd in TEST_COMMANDS_LIST_copy: + cmd_key = cmd.split()[1] + if ("set_" in cmd_key) or ("do_start" in cmd_key) or cmd_key in ["do_init", "do_goto", "do_open", "do_close"]: + self.TEST_COMMANDS_LIST.remove(cmd) + log.debug("- NEW test commands list scenario is:\n" + self.TEST_COMMANDS_LIST, '\n') + # Can be overriden by subclass (AgentDevice) def TEST_test_results_other(self, commands): pass @@ -1889,7 +2159,7 @@ def extract_parameters(): #def build_agent(Agent_type:Agent, name="GenericAgent", RUN_IN_THREAD=True): #def build_agent(Agent_type:Agent, RUN_IN_THREAD=True): -def build_agent(Agent_type:Agent): +def build_agent(Agent_type:Agent) -> Agent : #DEBUG_MODE, WITH_SIM, TEST_MODE, VERBOSE_MODE, configfile = extract_parameters() DEBUG_MODE, TEST_MODE, VERBOSE_MODE = extract_parameters() log.debug("debug mode is" + os.getenv("PYROS_DEBUG")) diff --git a/src/core/pyros_django/agent/AgentDevice.py b/src/core/pyros_django/agent/AgentDevice.py index 13544da..bdaea73 100755 --- a/src/core/pyros_django/agent/AgentDevice.py +++ b/src/core/pyros_django/agent/AgentDevice.py @@ -295,14 +295,19 @@ class AgentDevice(Agent): ================================================================================================ """ + ''' # @override superclass (Agent) method def is_other_level_cmd(self, cmd: AgentCmd): return self.is_device_level_cmd(cmd) + ''' + # I delegate this command to my DC + # => Execute it only if I am active and no currently running another device level cmd + # => Long execution time, so I will execute it in parallel (in a new thread or process) def is_device_level_cmd(self, cmd: AgentCmd): return self._device_ctrl.is_valid_cmd(DeviceCmd(cmd.full_name)) - def process_other_level_cmd(self): + def process_device_level_cmd(self): log.info("(DEVICE LEVEL CMD)") try: self.exec_device_cmd_if_possible(cmd) diff --git a/src/core/pyros_django/agent/doc/Agent_activity_diag.pu b/src/core/pyros_django/agent/doc/Agent_activity_diag.pu index 7ba181a..28456d7 100644 --- a/src/core/pyros_django/agent/doc/Agent_activity_diag.pu +++ b/src/core/pyros_django/agent/doc/Agent_activity_diag.pu @@ -1,7 +1,7 @@ @startuml -' --- Agent & AgentDevice ACTIVIY DIAGRAM (plantUML) --- +' --- Agent ACTIVIY DIAGRAM (plantUML) --- ' NEW syntax => http://plantuml.com/fr/activity-diagram-beta ' OLD syntax => http://plantuml.com/fr/activity-diagram-legacy @@ -25,24 +25,143 @@ title -__**Agent & AgentDevice run() function : Activity Diagram**__ -Version 19-11-2019 +__**Agent Activity Diagram**__ +Version 10-06-2022 end title '(E. Pallier) + + '|Agent| start -:DO_EXIT = False -DO_RESTART = True; +:STATUS = ATTENTIVE; +note right + Possible statuses : IDLE, ROUTINE, ATTENTIVE +end note + +:DO_RESTART = True; + +' *** RESTART loop *** +' while (DO_RESTART ?) is (yes) +repeat -while (DO_RESTART ?) is (yes) :load_config(); :init(); :DO_MAIN_LOOP = True; + + ' *** MAIN loop *** + 'while (DO_MAIN_LOOP ?) is (yes) + repeat + + :reload_config_if_changed(); + + :log_agent_status(); + note right + Log this agent status in DB + end note + + if (STATUS is 'IDLE' ?) then (yes) + else (no)) + #green:process_routine_before(); + endif + + :CMD = get_next_cmd_if_exists(); + if (CMD ?) then + + ' AGENT GENERAL level + if (is_agent_general_level(CMD) ?) then (yes) + + partition AGENT_GENERAL_level { + #green:process_agent_level_general_cmd(CMD); + + ' I must stop or restart + if (CMD is DO_ABORT || DO_EXIT || DO_RESTART ?) then (yes) + + :DO_MAIN_LOOP = False; + + if (CMD is DO_ABORT ?) then (yes) + :abort_current_running_cmd_if_exists(); + else (no) + :cleanup_before_exit(); + endif + + if (CMD is DO_RESTART ?) then (yes) + else (no) + :DO_RESTART = False; + endif + + ->// ==> GOTO "DO_MAIN_LOOP ?"//; + + break + + ' I don't have to stop + else (no) + ' end of "I must stop or restart ?" + ' ->NOK; + ' :Alert "toto"; + endif + } + + + + ' AGENT SPECIFIC level + else (no) + partition AGENT_SPECIFIC_level { + if (STATUS is 'ATTENTIVE' ?) then (yes) + if (is_agent_specific_level(CMD) ?) then (yes) + #green:process_agent_level_specific_cmd(CMD); + if (KO?) then (yes) + #pink:cleanup_before_exit() + EXC UnimplementedAgentLevelSpecificCmdExc; + kill + else (no) + endif + else (no) + #pink:cleanup_before_exit() + EXC UnknownCmdException; + kill + endif + else (no) + endif + } + + ' Agent level Command processing + endif + + ' No command to process + else (no) + + ' Command processing + endif + + if (STATUS is 'IDLE' ?) then (yes) + else (no) + #green:process_routine_after(); + endif + + + ' DO_MAIN_LOOP loop + 'endwhile (no) + repeat while (DO_MAIN_LOOP ?) is (yes) not (no) + ' ->//merged step//; + +' DO_RESTART loop +' endwhile (no) +repeat while (DO_RESTART ?) is (yes) not (no) + +stop + + + + + +/' while (DO_MAIN_LOOP ?) is (yes) + partition main_loop() { + :reload_config(); note right only if changed @@ -53,7 +172,10 @@ while (DO_RESTART ?) is (yes) Log this agent status in DB end note - :routine_process(); + :routine_process_before(); + note right + Routine process at "loop start" + end note :COMMAND = get_next_valid_command(); if (COMMAND ?) then @@ -82,8 +204,8 @@ while (DO_RESTART ?) is (yes) :DO_MAIN_LOOP = False; endif - } + endwhile (no) endwhile (no) @@ -91,9 +213,7 @@ stop (A) -/' - exec_agent_cmd() -'/ +' exec_agent_cmd() if (is_agent_specific_cmd(COMMAND) ?) then (yes) :exec_cmd_from_its_name(COMMAND); else (no) @@ -105,9 +225,7 @@ detach (D) -/' - exec_device_cmd_if_possible() -'/ +' exec_device_cmd_if_possible() if (I am IDLE ?) then (yes) :SKIP command (ignored); else if (another device cmd running ?) then (yes) @@ -130,4 +248,6 @@ else (no) } endif +'/ + @enduml diff --git a/src/core/pyros_django/common/models.py b/src/core/pyros_django/common/models.py index 833d025..b735983 100644 --- a/src/core/pyros_django/common/models.py +++ b/src/core/pyros_django/common/models.py @@ -1,6 +1,7 @@ ##from __future__ import unicode_literals # Stdlib imports +from numpy import False_ from src.device_controller.abstract_component.device_controller import DeviceCmd from enum import Enum from datetime import datetime, timedelta, date @@ -488,7 +489,7 @@ class AgentCmd(models.Model): # TODO: maybe à mettre au format json (key:value) result = models.CharField(max_length=400, blank=True) - # on creation: (AUTO) Automatically set at table line creation (line created by the sender) + # - on creation: (AUTO) Automatically set at table line creation (line created by the sender) s_deposit_time = models.DateTimeField( blank=True, null=True, auto_now_add=True) # - on reading: @@ -810,18 +811,18 @@ class AgentCmd(models.Model): self.refresh_from_db() return self.result - def set_result(self, result: str): + def set_result(self, result: str, do_save:bool=True): self.result = result - self.save() + if do_save: self.save() - def set_read_time(self): + def set_read_time(self, do_save:bool=True): self.r_read_time = datetime.utcnow().astimezone() # Optimization: update only 1 field - self.save(update_fields=["r_read_time"]) + if do_save: self.save(update_fields=["r_read_time"]) - def set_as_processed(self): + def set_as_processed(self, result:str=None): printd(f"- Set command {self.name} as processed") - self.set_state_to(self.CMD_STATUS_CODES.CMD_EXECUTED) + self.set_state_to(self.CMD_STATUS_CODES.CMD_EXECUTED, result=result) # print(self) """ self.state = self.CMD_STATUS_CODES.CMD_EXECUTED @@ -839,8 +840,8 @@ class AgentCmd(models.Model): def set_as_pending(self): self.set_state_to(self.CMD_STATUS_CODES.CMD_PENDING) - def set_as_skipped(self): - self.set_state_to(self.CMD_STATUS_CODES.CMD_SKIPPED) + def set_as_skipped(self, result:str=None): + self.set_state_to(self.CMD_STATUS_CODES.CMD_SKIPPED, result=result) def set_as_killed_by(self, author_agent: str): printd(f"- Set command {self.name} as killed") @@ -855,8 +856,13 @@ class AgentCmd(models.Model): self.set_state_to(self.CMD_STATUS_CODES.CMD_EXECUTED) ''' - def set_state_to(self, status: str, author_agent_name: str = None): + def set_state_to(self, status: str, author_agent_name: str = None, result:str=None): + ''' + If comment is present it will be set in the "result" field + ''' now_time = datetime.utcnow().astimezone() + if result: + self.set_result(result, False) if status in (self.CMD_STATUS_CODES.CMD_RUNNING, self.CMD_STATUS_CODES.CMD_SKIPPED, self.CMD_STATUS_CODES.CMD_OUTOFDATE): assert self.is_pending() if status == self.CMD_STATUS_CODES.CMD_RUNNING: -- libgit2 0.21.2