<gh_stars>0
import logging
import threading
import time
try:
    from queue import Queue, Empty
except ImportError:
    # Python2 compatibility
    from Queue import Queue, Empty

from . import utils
from .command import SCPICommand, SCPICommandList
from .interface import SCPIInterfaceTCP, SCPIInterfaceUDP, SCPIInterfaceSerial

class SCPIDevice():
    """``SCPIDevice`` is the main class for the SCPI device. It contains a 
    list with all valid commands. Each command requires a callback function to 
    be set. It will be called when ``SCPIDevice.execute(cmd_string)`` is 
    and a matching command could be found."""

    def __init__(self, *args, **kwargs):
        """Instantiate the SCPIDevice.
        
        You can create commands from a dictonary by using 
        ``SCPIDevice(cmd_dict=cmd_dict)``.
        The dictionary's keys are the SCPI strings and the values represent 
        the function callbacks. 
        """
        self._command_list = SCPICommandList()
        self._command_history = list()
        self._alarm_state = False
        self._alarm_trace = list()
        self._interface_list = list()
        self._interface_type_list = list()
        self._is_running = threading.Event()
        self._thread = None
        self._watchdog_running = threading.Event()

        if "cmd_dict" in kwargs:
            for cmd_string in kwargs["cmd_dict"]:
                self.add_command(
                    cmd_string,
                    kwargs["cmd_dict"][cmd_string],
                )

    def get_command_list(self):
        """Return a list of command objects."""
        return self._command_list

    def get_command_history(self):
        """Return a list which contains all succesfully executed commands."""
        return self._command_history

    def add_command(self, scpi_string, action, name="", description=""):
        """Add a command string and an associated action."""
        new_cmd = SCPICommand(
            scpi_string=scpi_string,
            action=action,
            name=name,
            description=description,
        )
        self._command_list.append(new_cmd)

    def list_commands(self):
        """Return a list of command strings which were added to the device."""
        commands = list()
        for cmd in self._command_list:
            commands.append(str(cmd))
        return commands

    def set_alarm(self, message):
        """Set an alarm with the content of ``message``."""
        alarm_string = message
        self._alarm_state = True
        self._alarm_trace.append(alarm_string)
        logging.info(alarm_string)

    def get_alarm(self, clear_alarm_when_empty=True):
        """Return the most recent alarm and remove it from the alarm trace. If 
        ``clear_alarm_when_empty`` is True, the alarm status will be cleared 
        if all alarms got consumed."""
        if self._alarm_trace:
            alarm = self._alarm_trace.pop()
            if alarm is None and clear_alarm_when_empty:
                self._alarm_state = False
            return alarm
        else:
            return None

    def clear_alarm(self, clear_history=True):
        """Confirm an alarm. If ``clear_history`` is True, the alarm history 
        will also be cleared. Attention: All previous alarms will be lost in 
        that case."""
        self._alarm_state = False
        if clear_history:
            self._alarm_trace = list()

    def create_interface(self, type, *args, **kwargs):
        """Create a communication interface. The actual instantiation will be 
        done when the interface is actually needed. The application programmer 
        needs to make sure that assigning the same resource more than 1 time 
        is avoided.

        Currently, these types are supported:
        - TCP
        - UDP
        - Serial (not yet implemented)
        """
        type = type.lower()
        # The interfaces will only be defined here. The instantiation will 
        # happen later when ``run()`` is called. This insures that after 
        # multiple ``run()`` calls new interfaces are instantiated.
        interface_type = (type, args, kwargs)
        self._interface_type_list.append(interface_type)

    def _instantiate_interface(self, type, *args, **kwargs):
        if type == "tcp":
            interface = SCPIInterfaceTCP(*args, **kwargs)
        elif type == "udp":
            interface = SCPIInterfaceUDP(*args, **kwargs)
        elif type == "serial":
            interface = SCPIInterfaceSerial(*args, **kwargs)
        return interface

    def execute(self, command_string):
        """Search a matching command and execute it. If exceptions arise 
        during execution, they are catched and an alarm is set.
        
        Todo: 
        - Implement multiple commands in one line, e.g. MEAS?;MEAS:CURR?\n
        - Implement parallelism in execution tasks
        """
        executed = False
        result = None
        result_string = None
        reason = "No reason."
        command_string = utils.sanitize(command_string)
        cmd = self._command_list.get_command(command_string, 
            match_parameters=False)
        if cmd is not None:
            if self._command_list.get_command(command_string, 
                    match_parameters=True) is not None:
                fn_name = cmd.get_action_name()
                try:
                    result = cmd.execute(command_string)
                    if result is not None:
                        result_string = str(result)
                        if not result_string.endswith("\n"):
                            result_string = result_string + "\n"
                    cmd_hist_string = "{cs!r} => {fn} => {res!r}".format(
                        cs=command_string, fn=fn_name, res=result_string)
                    self._command_history.append(cmd_hist_string)
                    executed = True
                except Exception as e:
                    reason = (
                        "Exception during execution of function {!r}: {}."
                        .format(fn_name, e))
            else:
                reason = "Parameter mismatch."
        else:
            reason = "No match found."
        if not executed:
            self.set_alarm("Could not execute command {c!r}. {r}"
                .format(c=command_string, r=reason))

        return result_string

    def start(self):
        """Instantiate a thread and run the ``run()`` routine."""
        if self._thread is None:
            self._thread = threading.Thread(
                name="SCPIDevice",
                target=self.run)
            self._thread.start()
            return True
        else:
            return False

    def _get_data_from_queue(self):
        """Get data from the queue which will be written by the interface
        data handlers. This "timeout => continue" loop is necessary because 
        otherwise, Queue.get() would block KeyboardInterrupts."""
        data_recv = None
        while self._is_running.is_set():
            try:
                data_recv = self._recv_queue.get(timeout=1)
                break
            except Empty:
                continue
        return data_recv

    def run(self):
        """Start listening on the previously by ``create_interface()`` defined 
        interfaces and execute commands when a message is received. This 
        function will run until ``stop()`` is called.

        TODO:
        - Implement parallel execution tasks
        """
        self._recv_queue = Queue() # TODO: Maxsize
        self._thread_list = list()
        self._interface_list = list()

        if not self._interface_type_list:
            logging.error("Cannot run: No interfaces were specified.")
            return

        self.start_watchdog()

        # Instantiate the interfaces.
        for interface_type in self._interface_type_list:
            type = interface_type[0]
            args = interface_type[1]
            kwargs = interface_type[2]
            try:
                interface = self._instantiate_interface(type, *args, **kwargs)
                self._interface_list.append(interface)
            except Exception as e:
                logging.error("Could not instantiate interface '{}'. "
                    "Exception: {}".format(interface_type, e))
        logging.debug("Instantiated {} interfaces.".format(
            len(self._interface_list)))

        # Create threads for each interface's data handler.
        for interface in self._interface_list:
            args = (self._recv_queue,)
            t = threading.Thread(
                target=interface.data_handler, name=str(interface), args=args)
            self._thread_list.append(t)
            t.start()

        # As long as we did not receive a stop command, we try to get the data 
        # from the receive queue and execute a command.
        self._is_running.set()
        while self._is_running.is_set():
            data_recv = self._get_data_from_queue()

            # Execute the received command string and return the result (if 
            # any).
            if data_recv is not None:
                interface = data_recv[0]
                command_string = data_recv[1]
                result = self.execute(command_string)
                if result is not None:
                    try:
                        interface.write(str(result))
                    except Exception as e:
                        logging.info("Could not send data to {}. Exception: "
                            "{}.".format(interface, e))

        # Do not forget to clean-up.
        self.stop_watchdog()
        logging.debug("'run()' has finished.")

    def stop(self, timeout=None):
        self._is_running.clear()
        for interface in self._interface_list:
            interface.stop()
        for thread in self._thread_list:
            logging.debug("Waiting for Thread '{}' to finish."
                .format(thread.name))
            thread.join(timeout=timeout)
        self._thread.join(timeout=timeout)
        self._thread = None
        logging.debug("All data handlers have finished.")

    def start_watchdog(self):
        self._watchdog_thread = threading.Thread(
            target=self._watchdog_handler, name="Watchdog")
        self._watchdog_thread.daemon = False
        self._watchdog_running.set()
        self._watchdog_thread.start()

    def stop_watchdog(self):
        self._watchdog_running.clear()
        self._watchdog_thread.join()

    def _watchdog_handler(self):
        """The watchdog should periodically check for deadlocks or other 
        inconsistencies. 
        
        TODO: Implement an intelligent watchdog.
        Ideas:
        - restart data_handler thread
        """
        iterations = 0
        while self._watchdog_running.is_set():
            if iterations < 9:
                iterations += 1
            else:
                logging.debug("{}: Watchdog alive. Alarms: {}."
                    .format(time.time(), len(self._alarm_trace)))
                alive_threads = 0
                for t in self._thread_list:
                    if t.is_alive():
                        alive_threads += 1
                    else:
                        logging.warning("Watchdog: Thread {!r} is not alive"
                            .format(t))
                if alive_threads == len(self._thread_list):
                    logging.debug("Watchdog: All threads alive.")
                iterations = 0
            time.sleep(1)
        logging.info("Watchdog has stopped.")

import logging
import threading
import time
try:
    from queue import Queue, Empty
except ImportError:
    # Python2 compatibility
    from Queue import Queue, Empty

from . import utils
from .command import SCPICommand, SCPICommandList
from .interface import SCPIInterfaceTCP, SCPIInterfaceUDP, SCPIInterfaceSerial

class SCPIDevice():
    """``SCPIDevice`` is the main class for the SCPI device. It contains a 
    list with all valid commands. Each command requires a callback function to 
    be set. It will be called when ``SCPIDevice.execute(cmd_string)`` is 
    and a matching command could be found."""

    def __init__(self, *args, **kwargs):
        """Instantiate the SCPIDevice.
        
        You can create commands from a dictonary by using 
        ``SCPIDevice(cmd_dict=cmd_dict)``.
        The dictionary's keys are the SCPI strings and the values represent 
        the function callbacks. 
        """
        self._command_list = SCPICommandList()
        self._command_history = list()
        self._alarm_state = False
        self._alarm_trace = list()
        self._interface_list = list()
        self._interface_type_list = list()
        self._is_running = threading.Event()
        self._thread = None
        self._watchdog_running = threading.Event()

        if "cmd_dict" in kwargs:
            for cmd_string in kwargs["cmd_dict"]:
                self.add_command(
                    cmd_string,
                    kwargs["cmd_dict"][cmd_string],
                )

    def get_command_list(self):
        """Return a list of command objects."""
        return self._command_list

    def get_command_history(self):
        """Return a list which contains all succesfully executed commands."""
        return self._command_history

    def add_command(self, scpi_string, action, name="", description=""):
        """Add a command string and an associated action."""
        new_cmd = SCPICommand(
            scpi_string=scpi_string,
            action=action,
            name=name,
            description=description,
        )
        self._command_list.append(new_cmd)

    def list_commands(self):
        """Return a list of command strings which were added to the device."""
        commands = list()
        for cmd in self._command_list:
            commands.append(str(cmd))
        return commands

    def set_alarm(self, message):
        """Set an alarm with the content of ``message``."""
        alarm_string = message
        self._alarm_state = True
        self._alarm_trace.append(alarm_string)
        logging.info(alarm_string)

    def get_alarm(self, clear_alarm_when_empty=True):
        """Return the most recent alarm and remove it from the alarm trace. If 
        ``clear_alarm_when_empty`` is True, the alarm status will be cleared 
        if all alarms got consumed."""
        if self._alarm_trace:
            alarm = self._alarm_trace.pop()
            if alarm is None and clear_alarm_when_empty:
                self._alarm_state = False
            return alarm
        else:
            return None

    def clear_alarm(self, clear_history=True):
        """Confirm an alarm. If ``clear_history`` is True, the alarm history 
        will also be cleared. Attention: All previous alarms will be lost in 
        that case."""
        self._alarm_state = False
        if clear_history:
            self._alarm_trace = list()

    def create_interface(self, type, *args, **kwargs):
        """Create a communication interface. The actual instantiation will be 
        done when the interface is actually needed. The application programmer 
        needs to make sure that assigning the same resource more than 1 time 
        is avoided.

        Currently, these types are supported:
        - TCP
        - UDP
        - Serial (not yet implemented)
        """
        type = type.lower()
        # The interfaces will only be defined here. The instantiation will 
        # happen later when ``run()`` is called. This insures that after 
        # multiple ``run()`` calls new interfaces are instantiated.
        interface_type = (type, args, kwargs)
        self._interface_type_list.append(interface_type)

    def _instantiate_interface(self, type, *args, **kwargs):
        if type == "tcp":
            interface = SCPIInterfaceTCP(*args, **kwargs)
        elif type == "udp":
            interface = SCPIInterfaceUDP(*args, **kwargs)
        elif type == "serial":
            interface = SCPIInterfaceSerial(*args, **kwargs)
        return interface

    def execute(self, command_string):
        """Search a matching command and execute it. If exceptions arise 
        during execution, they are catched and an alarm is set.
        
        Todo: 
        - Implement multiple commands in one line, e.g. MEAS?;MEAS:CURR?\n
        - Implement parallelism in execution tasks
        """
        executed = False
        result = None
        result_string = None
        reason = "No reason."
        command_string = utils.sanitize(command_string)
        cmd = self._command_list.get_command(command_string, 
            match_parameters=False)
        if cmd is not None:
            if self._command_list.get_command(command_string, 
                    match_parameters=True) is not None:
                fn_name = cmd.get_action_name()
                try:
                    result = cmd.execute(command_string)
                    if result is not None:
                        result_string = str(result)
                        if not result_string.endswith("\n"):
                            result_string = result_string + "\n"
                    cmd_hist_string = "{cs!r} => {fn} => {res!r}".format(
                        cs=command_string, fn=fn_name, res=result_string)
                    self._command_history.append(cmd_hist_string)
                    executed = True
                except Exception as e:
                    reason = (
                        "Exception during execution of function {!r}: {}."
                        .format(fn_name, e))
            else:
                reason = "Parameter mismatch."
        else:
            reason = "No match found."
        if not executed:
            self.set_alarm("Could not execute command {c!r}. {r}"
                .format(c=command_string, r=reason))

        return result_string

    def start(self):
        """Instantiate a thread and run the ``run()`` routine."""
        if self._thread is None:
            self._thread = threading.Thread(
                name="SCPIDevice",
                target=self.run)
            self._thread.start()
            return True
        else:
            return False

    def _get_data_from_queue(self):
        """Get data from the queue which will be written by the interface
        data handlers. This "timeout => continue" loop is necessary because 
        otherwise, Queue.get() would block KeyboardInterrupts."""
        data_recv = None
        while self._is_running.is_set():
            try:
                data_recv = self._recv_queue.get(timeout=1)
                break
            except Empty:
                continue
        return data_recv

    def run(self):
        """Start listening on the previously by ``create_interface()`` defined 
        interfaces and execute commands when a message is received. This 
        function will run until ``stop()`` is called.

        TODO:
        - Implement parallel execution tasks
        """
        self._recv_queue = Queue() # TODO: Maxsize
        self._thread_list = list()
        self._interface_list = list()

        if not self._interface_type_list:
            logging.error("Cannot run: No interfaces were specified.")
            return

        self.start_watchdog()

        # Instantiate the interfaces.
        for interface_type in self._interface_type_list:
            type = interface_type[0]
            args = interface_type[1]
            kwargs = interface_type[2]
            try:
                interface = self._instantiate_interface(type, *args, **kwargs)
                self._interface_list.append(interface)
            except Exception as e:
                logging.error("Could not instantiate interface '{}'. "
                    "Exception: {}".format(interface_type, e))
        logging.debug("Instantiated {} interfaces.".format(
            len(self._interface_list)))

        # Create threads for each interface's data handler.
        for interface in self._interface_list:
            args = (self._recv_queue,)
            t = threading.Thread(
                target=interface.data_handler, name=str(interface), args=args)
            self._thread_list.append(t)
            t.start()

        # As long as we did not receive a stop command, we try to get the data 
        # from the receive queue and execute a command.
        self._is_running.set()
        while self._is_running.is_set():
            data_recv = self._get_data_from_queue()

            # Execute the received command string and return the result (if 
            # any).
            if data_recv is not None:
                interface = data_recv[0]
                command_string = data_recv[1]
                result = self.execute(command_string)
                if result is not None:
                    try:
                        interface.write(str(result))
                    except Exception as e:
                        logging.info("Could not send data to {}. Exception: "
                            "{}.".format(interface, e))

        # Do not forget to clean-up.
        self.stop_watchdog()
        logging.debug("'run()' has finished.")

    def stop(self, timeout=None):
        self._is_running.clear()
        for interface in self._interface_list:
            interface.stop()
        for thread in self._thread_list:
            logging.debug("Waiting for Thread '{}' to finish."
                .format(thread.name))
            thread.join(timeout=timeout)
        self._thread.join(timeout=timeout)
        self._thread = None
        logging.debug("All data handlers have finished.")

    def start_watchdog(self):
        self._watchdog_thread = threading.Thread(
            target=self._watchdog_handler, name="Watchdog")
        self._watchdog_thread.daemon = False
        self._watchdog_running.set()
        self._watchdog_thread.start()

    def stop_watchdog(self):
        self._watchdog_running.clear()
        self._watchdog_thread.join()

    def _watchdog_handler(self):
        """The watchdog should periodically check for deadlocks or other 
        inconsistencies. 
        
        TODO: Implement an intelligent watchdog.
        Ideas:
        - restart data_handler thread
        """
        iterations = 0
        while self._watchdog_running.is_set():
            if iterations < 9:
                iterations += 1
            else:
                logging.debug("{}: Watchdog alive. Alarms: {}."
                    .format(time.time(), len(self._alarm_trace)))
                alive_threads = 0
                for t in self._thread_list:
                    if t.is_alive():
                        alive_threads += 1
                    else:
                        logging.warning("Watchdog: Thread {!r} is not alive"
                            .format(t))
                if alive_threads == len(self._thread_list):
                    logging.debug("Watchdog: All threads alive.")
                iterations = 0
            time.sleep(1)
        logging.info("Watchdog has stopped.")
# Python2 compatibility ``SCPIDevice`` is the main class for the SCPI device. It contains a      list with all valid commands. Each command requires a callback function to      be set. It will be called when ``SCPIDevice.execute(cmd_string)`` is      and a matching command could be found. Instantiate the SCPIDevice.                  You can create commands from a dictonary by using          ``SCPIDevice(cmd_dict=cmd_dict)``.         The dictionary's keys are the SCPI strings and the values represent          the function callbacks. Return a list of command objects. Return a list which contains all succesfully executed commands. Add a command string and an associated action. Return a list of command strings which were added to the device. Set an alarm with the content of ``message``. Return the most recent alarm and remove it from the alarm trace. If          ``clear_alarm_when_empty`` is True, the alarm status will be cleared          if all alarms got consumed. Confirm an alarm. If ``clear_history`` is True, the alarm history          will also be cleared. Attention: All previous alarms will be lost in          that case. Create a communication interface. The actual instantiation will be          done when the interface is actually needed. The application programmer          needs to make sure that assigning the same resource more than 1 time          is avoided.          Currently, these types are supported:         - TCP         - UDP         - Serial (not yet implemented) # The interfaces will only be defined here. The instantiation will # happen later when ``run()`` is called. This insures that after # multiple ``run()`` calls new interfaces are instantiated. Search a matching command and execute it. If exceptions arise          during execution, they are catched and an alarm is set.                  Todo:          - Implement multiple commands in one line, e.g. MEAS?;MEAS:CURR?\n         - Implement parallelism in execution tasks Instantiate a thread and run the ``run()`` routine. Get data from the queue which will be written by the interface         data handlers. This "timeout => continue" loop is necessary because          otherwise, Queue.get() would block KeyboardInterrupts. Start listening on the previously by ``create_interface()`` defined          interfaces and execute commands when a message is received. This          function will run until ``stop()`` is called.          TODO:         - Implement parallel execution tasks # TODO: Maxsize # Instantiate the interfaces. # Create threads for each interface's data handler. # As long as we did not receive a stop command, we try to get the data # from the receive queue and execute a command. # Execute the received command string and return the result (if # any). # Do not forget to clean-up. The watchdog should periodically check for deadlocks or other          inconsistencies.                   TODO: Implement an intelligent watchdog.         Ideas:         - restart data_handler thread