# This file documents Guardian's use of the Ezca channel access
# object. In particular it is meant to document exactly how the Ezca
# object is used; how it's intialized, how it's methods are called,
# what return values it is expecting, etc.  Also a bit of the intended
# use of the object.
#
# Ezca's original purpose was as a wrapper of the EPICS pyepics Device
# class, for managing interaction with groups of common prefixed EPICS
# channels.
#
# http://cars9.uchicago.edu/software/python/pyepics3/devices.html
#
# Guardian user code would use the interface to read values from and
# write values to EPICS channels on the network, and the Ezca object
# (and ultimately the underlying pyepics.Device) would manage the
# connections to those channels.
#
# Currently guardian expects an 'ezca' module to exist, for it to
# provide an Ezca class, and for the Ezca class to provide certain
# methods and properties with certain return values.  However, the
# guardian user code is in no way required to use the interface or to
# even make any reference to it.  The entire module can in fact
# therefore be a no-op.  But it's important that at least guardian
# gets what it expects out of it, even if none of it is used.
#
# That said, any use of guardian may want to provide an Ezca module
# that is actually useful in the user code for the task at hand.
#
#
# MODULE
# ======
#
# The ezca module should provide the following objects, which are all
# imported by guardian:
#
#   from ezca import Ezca, EzcaError, EzcaConnectError
#
# PVS
# ===
#
# 'pv' objects are container objects that hold state of some process
# variable in use by the system.  In EPICS these hold state of some
# active channel access connection.  The LIGO Ezca object provides
# access to the underlying pvs via a dict-like get/setitem interface
# to the object:
#
# >>> prefix = 'T1:'
# >>> ezca = Ezca(prefix):
# >>> a = ezca['CHANNEL_A']
# >>> ezca['CHANNEL_B'] = 4
#
# In EPICS Ezca each pv maintains the state of a particular EPICS
# channel while guardian is active.  They are instantiated in the
# object when they are first accessed.
#
# Guardian assumes that Ezca has a 'pvs' property, assumed to be a
# dictionary of individual pv objects keyed by the names used to
# access them.  The only real requirement is that the 'pvs' property
# be a container object that supports the len() method.
#
# If a setpoint "snapshot" is requested (see SETPOINTS below), the
# guardian will actually iterate over the 'pvs' object to look at the
# value of each pv.  In that case each individual pv objects must have
# the following properties:
#
# pv.pvname:     string name of the pv
# pv.char_value: string representation of the value of the channel
#
#
# CONNECTIONS
# ===========
#
# Guardian wants to check the connection status of all pvs before
# executing state code.  If any of pvs have lost connection to their
# underlying channel, guardian will go into a connection error
# (CERROR) state and will forgo executing any state code until all
# connections have been reestablished.  Guardian does this by calling
# the ezca.check_connections() method.  This method should return an
# list of pv objects (see above) that are currently having connections
# issues.  Guardian will keep running check_connections() every cycle,
# not executing any state code, until it returns an empty list.
#
#  dead_channel_list = ezca.check_connections()
#
#
# SETPOINTS
# =========
#
# Like the connection checking described above, Guardian has the
# ability to provide information from setpoint checks performed by the
# Ezca object.  A setpoint is any value written by Ezca (usually via
# the constituent pvs).
#
# If the system.ca_channels variable is defined, guardian will pass it
# directly to the ezca.init_setpoints() method.  The typical usage
# would be for system.ca_channels to be a list of
# (channel_name, initial_setpoint_value) tuples.
#
#   ezca.init_setpoints(system.ca_channels)
#
# Setpoints should be stored in a ezca.setpoints property, which
# should be a dictionary keyed on channel name as PVS above, with
# (pvname, setpoint_value) tuples as values.
#
# If the system.ca_monitor variable is set to True, once a cycle
# guardian will execute the ezca.check_setpoints() method, which is
# expected to return a list of setpoint status tuples for channels
# whose value does not equal their setpoint.
#
#   modified_channel_list = ezca.check_setpoints()
#
# where modified_channel_list is a list of tuples:
#
#   (channel_name, setpoint_value, current_value, difference)
#
# Guardian can also request that the ezca object write all current
# setpoint values to a file to disk.  This is done with the following
# method:
#
#   ezca.setpoint_snap(path_to_snap_file)
#
#
# EXCEPTIONS
# ==========
#
# If the ezca object (or any user code for that matter) throws an
# EzcaError exception, a guardian ERROR error status will be reported.
#
# If the ezca object (or any user code for that matter) throws an
# EzcaConnectError exception, a guardian CERROR connection error
# status will be reported (see CONNECTIONS above).

############################################################
############################################################

# Variable holding version of library.  This is a string but it is
# expected to have a particular form of three integers separated by a
# period ('.') with 4, 8, and 8 bit values respectively.
#
# REQUIRED
# TYPE: str
# VALUE: three integers separated by '.', <4>.<8>.<8> bit
#        respectively.
__version__ = '0.0.0'


# Ezca object
#
# REQUIRED
# TYPE: object
class Ezca(object):
    # Guardian initializes in the worker subprocess only.  Ezca init
    # should accept the following keyword arguments:
    #
    #   'prefix': channel access prefix string as specified in the
    #   system module 'ca_prefix' variable.  It can be used or not
    #   used in the interface as appropriate.
    #
    #   'logger': standard python logging object that can be used to
    #   log activity in the object, such as channel read or writes.
    #   Using this provided object will prefix log writes with
    #   information about the state of the system, such as the time,
    #   node name, and current state.
    #
    def __init__(self, prefix='', logger=True):
        self._prefix = prefix
        self._log = logger

    # Channel prefix value.  This property should always return a
    # string, and if nothing else should return the 'prefix' string as
    # provided at initialization above.
    #
    # REQUIRED
    # TYPE: str
    @property
    def prefix(self):
        return self._prefix

    # Dictionary of pvs.  Keys are channel names, values are pv
    # objects with the following attributes:
    #
    #   pv.pvname: string
    #   pv.char_value: string of current value
    #
    # At very least len(ezca.pvs) must not fail.
    #
    # REQUIRED
    # TYPE: dict
    # VALUE: {channel_name: pv}
    @property
    def pvs(self):
        return {}

    # Method to check pv connection status.  Should return a list of
    # string channel names for channels with connection errors.
    #
    # REQUIRED
    # OUTPUT: list of str
    def check_connections(self):
        return []

    # Method to Initialize setpoints.  The value of system.ca_channels
    # will be passed directly to this method.
    #
    # CONDITIONAL: called if system.ca_channels is defined
    # INPUT: system.ca_channels defined
    def init_setpoints(self, table):
        pass

    # Dictionary of all current setpoints.  Keys are channel names
    # (see self.pvs), with (pvname, setpoint_value) tuples values.
    #
    # REQUIRED
    # TYPE: dict
    @property
    def setpoints(self):
        return {}

    # Method to request writing setpoint values to a file.
    #
    # CONDITIONAL: guardian system archive defined
    # INPUT: str path
    def setpoint_snap(self, snapfile):
        pass

    # Method to check current channel values against stored setpoints.
    # Should return a list of tuples of the form:
    #   (full_channel_name, setpoint_value, current_value, difference)
    #
    # CONDITIONAL: system.ca_monitor == True
    # INPUT: str path
    def check_setpoints(self):
        return []


# PV class provided as a convenience to show the properties expected
# in the ezca.pvs dictionary value objects.
class PV:
    # string name of pv
    @property
    def pvname(self):
        return ''

    # numeric/native value
    @property
    def value(self):
        return 0

    # string representation of value
    @property
    def char_value(self):
        return str(self.value)


# Exception that produces ERROR condition in guardian
class EzcaError(Exception):
    pass

# Exception that produces CERROR condition in guardian
class EzcaConnectError(Exception):
    pass
