Agent.py 12.4 KB
VERSION = "0.3"

#from __future__ import absolute_import

import time
import datetime
import os


"""TODO:

- 1 log par agent + lastlog (30 dernières lignes)
- 1 table par agent
- table agents_log avec log minimum pour affichage dans dashboard, et ordre chrono intéressant pour suivi activité : nom agent, timestamp, message
- table agent_<agent-name>_vars : nom variable, value, desc

"""


# from django.core.exceptions import ObjectDoesNotExist
# from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.conf import settings as djangosettings

import utils.Logger as L

from config.configpyros import ConfigPyros

"""
import observation_manager
import observation_manager.tasks
import scheduler
import scheduler.tasks as sched_task
import monitoring.tasks
import alert_manager.tasks
"""
# from common.models import *
from common.models import Config, Log, PlcDeviceStatus
from common.models import AgentsSurvey
from dashboard.views import get_sunelev
from devices.TelescopeRemoteControlDefault import TelescopeRemoteControlDefault

"""
from devices.CameraNIR import NIRCameraController
from devices.CameraVIS import VISCameraController
from devices.Dome import DomeController
from devices.PLC import PLCController
from devices.Telescope import TelescopeController
from majordome.MajordomeDecorators import *
from utils.JDManipulator import *
"""


from threading import Thread

DEBUG_FILE = False
log = L.setupLogger("MajordomeTaskLogger", "Majordome")

"""
    Task to handle the execution of the program

    check the environment status in database
    check the devices status (telescope / cameras)
    check if the last schedule made has to be planned
    launch schedule's sequences
"""



class Agent:
    # (EP) do this so that Majordome can be run from a thread, and called with thread.start():
    # class Majordome(Task, Thread):

    name = "Generic Agent"
    FOR_REAL = True
    mainloop_waittime = 3
    subloop_waittime = 2
    status = None
    mode = None
    config = None

    # Statuses
    STATUS_LAUNCH = "LAUNCHED"
    STATUS_INIT = "INITIALIZING"
    STATUS_MAIN_LOOP = "IN_MAIN_LOOP"
    STATUS_PROCESS_LOOP = "IN_PROCESS_LOOP"
    STATUS_EXIT = "EXITING"

    # Modes
    MODE_ACTIVE = "ACTIVE"
    MODE_IDLE = "IDLE"

    DEFAULT_CONFIG_FILE_NAME = "config_unit_simulunit1.xml"
    CONFIG_DIR = "config"

    _agents_survey = None


    def __init__(self, name:str=None, config_filename:str=None):
        self.set_mode(self.MODE_IDLE)
        self.set_status(self.STATUS_LAUNCH)
        self.name = name
        if not config_filename:
            #config_filename = '/PROJECTS/GFT/SOFT/PYROS_SOFT/CURRENT/config/config_unit_simulunit1.xml'
            config_filename = self.DEFAULT_CONFIG_FILE_NAME
        #config_file_path, _ = os.path.split(config_filename)
        if config_filename == os.path.basename(config_filename):
            config_filename = os.path.abspath(self.CONFIG_DIR + os.sep + config_filename)
        print("Config file used is", config_filename)
        #print("current path", os.getcwd())
        #print("this file path :", __file__)
        #print("config file path is", config_filename)
        # Instantiate an object for configuration
        #print("config file path is ", config_abs_filename)
        self.config = ConfigPyros(config_filename)
        if self.config.get_last_errno() != self.config.NO_ERROR:
            raise Exception(f"Bad config file name '{config_filename}', error {str(self.config.get_last_errno())}: {str(self.config.get_last_errmsg())}")
        tmp = AgentsSurvey.objects.filter(name=self.name)
        if len(tmp) == 0:
            self._agents_survey = AgentsSurvey.objects.create(name=self.name, validity_duration_sec=60, mode=self.mode, status=self.status)
            #self._agents_survey = AgentsSurvey(name=self.name, validity_duration_sec=60, mode=self.mode, status=self.status)
            #self._agents_survey.save()
        print("agent is", self._agents_survey)


    def __str__(self):
        return "I am agent " + self.name

    def run(self, FOR_REAL: bool = True):
        """
            FOR_REAL: set to False if you don't want Majordome to send commands to devices
        """

        self.FOR_REAL = FOR_REAL

        self.load_config()

        self.init()

        '''
        print()
        print(self)
        print("FOR REAL ?", self.FOR_REAL)
        print("DB3 used is:", djangosettings.DATABASES["default"]["NAME"])

        # SETUP
        try:
            self.config = get_object_or_404(Config, id=1)
            # By default, set mode to SCHEDULER (False = REMOTE, which should never be the default)
            self.config.global_mode = True
            self.config.save()
            # self.config = Config.objects.get(pk=1)
            # self.config = Config.objects.get()[0]
        except Exception as e:
            # except Config.ObjectDoesNotExist:
            print("Config read (or write) exception", str(e))
            return -1
        '''

        # Main loop
        while True:
            self.set_status(self.STATUS_MAIN_LOOP)

            """
            A chaque tour de boucle, remplir champ "iamalive" avec timestamp + durée validité (> temps iteration,  n minutes par défaut) 
            + nom agent dans table agents_survey (nom agent + mode + status + updated timestamp)
            """

            print()
            print("Starting main loop iteration...")
            self.show_mode_and_status()

            self.load_config()

            self.update_db_survey()

            self.read_db_commands()


            '''
            if self.config.majordome_state == "STOP":
                break
            if self.config.majordome_state == "RESTART":
                self.config.majordome_restarted = True
                self.closing_mode = self.config.majordome_state
                self.config.majordome_state = "RUNNING"
                self.config.save()
            '''

            # Sub-level loop (only if ACTIVE)
            if self.is_active():
                self.set_status(self.STATUS_PROCESS_LOOP)
                self.core_process()

            self.waitfor(self.mainloop_waittime)

            print("Ending main loop iteration...")

            #self.do_log(LOG_DEBUG, "Ending main loop iteration")



    def waitfor(self, nbsec):
            print(f"Now, waiting for {nbsec} seconds...")
            time.sleep(nbsec)

    def set_status(self, status:str):
        print(f"Switching from status {self.status} to status {status}")
        self.status = status

    def set_mode(self, mode:str):
        print(f"Switching from mode {self.mode} to mode {mode}")
        self.mode = mode

    def is_active(self):
        return self.mode == self.MODE_ACTIVE

    def set_active(self):
        self.set_mode(self.MODE_ACTIVE)

    def set_idle(self):
        self.mode = self.MODE_IDLE

    def show_mode_and_status(self):
        print(f"CURRENT MODE is {self.mode} (with status {self.status})")


    def die(self):
        self.set_status(self.STATUS_EXIT)

    """
    suspend/resume
    """
    def suspend(self):
        """
        TODO:
        Mode IDLE (doit rester à l'écoute d'un resume, 
        et doit continuer à alimenter les tables pour informer de son état via tables agents_logs, 
        et lire table agents_command pour reprendre via resume, 
        et update la table agents_survey pour donner son status "idle"
        """
        self.set_idle()
        return True

    def resume(self):
        """
        Quit suspend() mode
        """
        self.set_active()
        return True

    def set_mode_from_config(self,agent_alias):
        # --- Get the startmode of the AgentX
        modestr = self.config.get_paramvalue(agent_alias,'general','startmode')
        if self.config.get_last_errno() != self.config.NO_ERROR:
            raise Exception(f"error {str(self.config.get_last_errno())}: {str(self.config.get_last_errmsg())}")        
        if (modestr == None):
            return True
        # --- Set the mode according the startmode value
        mode = self.MODE_IDLE
        if modestr.upper() == 'RUN':
            mode = self.MODE_ACTIVE
        self.set_mode(mode)
        return True
        

    """
    =================================================================
    Generic methods that may be specialized (overriden) by subclasses
    =================================================================
    """

    def init(self):
        print("Initializing...")
        self.set_status(self.STATUS_INIT)

    def load_config(self):
        """
        TODO:
        only si date fichier xml changée => en RAM, un objet Config avec méthodes d'accès, appelle le parser de AK (classe Config.py indépendante)
        """
        print("Loading the config file...")
        #config_filename = 'c:/srv/develop/pyros/config/config_unit_simulunit1.xml'
        #config.set_configfile(config_filename)
        self.config.load()
        if self.config.get_last_errno() != self.config.NO_ERROR:
            raise Exception(f"error {str(self.config.get_last_errno())}: {str(self.config.get_last_errmsg())}")
        # --- display informations
        # --- Get all the assembly of this unit[0] (mount + channels)
        if self.config.is_config_contents_changed():
            print("--------- Components of the unit -----------")
            print("Configuration file is {}".format(self.config.get_configfile()))            
            alias = self.config.get_aliases('unit')[0]
            namevalue = self.config.get_paramvalue(alias,'unit','name')
            print("Unit alias is {}. Name is {}".format(alias,namevalue))
            unit_subtags = self.config.get_unit_subtags()
            for unit_subtag in unit_subtags:
                aliases = self.config.get_aliases(unit_subtag)
                for alias in aliases:
                    namevalue = self.config.get_paramvalue(alias,unit_subtag,'name')
                    print("Unit {} alias is {}. Name is {}".format(unit_subtag,alias,namevalue))
            print("------------------------------------------")
                
            #params = self.config.get_params(unit_alias)
            #for param in params:
            #    print("Unit component is {}".format(param))

        """
        # self.config = Config.objects.get(pk=1)
        try:
            self.config = get_object_or_404(Config, id=1)
            # By default, set mode to SCHEDULER (False = REMOTE, which should never be the default)
            # self.config.global_mode = True
            # self.config.save()
            # self.config = Config.objects.get(pk=1)
            # self.config = Config.objects.get()[0]
        except Exception as e:
            # except Config.ObjectDoesNotExist:
            # except Config.DoesNotExist:
            print("Config read (or write) exception", str(e))
            # return self.config
            # return -1
            return False
        """

    def update_db_survey(self):
        print("Updating the survey database table...")
        self._agents_survey = AgentsSurvey.objects.get(name=self.name)
        self._agents_survey.mode = self.mode
        self._agents_survey.status = self.status
        self._agents_survey.save()

    def read_db_commands(self):
        print("Looking for new commands from the database ...")

    def do_log(self):
        """
        log à 2 endroits ou 1 seul
        - in file
        - in db
        """
        print("Logging data...")


    def core_process(self):
        """
        Sublevel Loop (only if ACTIVE) :
        PLUS TARD, maybe :start_process_thread() dans un thread : ensuite, à chaque tour de boucle il regarde si c'est fini ou pas, et si fini recommence
        """
        assert(self.is_active())

        print("Starting the core process subloop...")

        self.waitfor(self.subloop_waittime)

        print("Ending core process subloop...")




    """ 
    ===================================
    OLD FUNCTIONS TO BE REMOVED 
    ===================================
    """

    def _plc_is_not_auto(self):
        if not self.plc_is_connected():
            return True
        # now, self.plc_status has been updated, so check it:
        return self.plc_status.plc_mode != "AUTO"


    def plc_is_auto(self):
        return not self._plc_is_not_auto()


    def plc_is_safe(self):
        if not self.plc_is_connected():
            return False
        # now, self.plc_status has been updated, so check it:
        return self.plc_status.is_safe


    def is_night(self):
        return get_sunelev() < -10