############################################################################
#                                                                          #
# Copyright (c)2008, 2009, Digi International (Digi). All Rights Reserved. #
#                                                                          #
# Permission to use, copy, modify, and distribute this software and its    #
# documentation, without fee and without a signed licensing agreement, is  #
# hereby granted, provided that the software is used on Digi products only #
# and that the software contain this copyright notice,  and the following  #
# two paragraphs appear in all copies, modifications, and distributions as #
# well. Contact Product Management, Digi International, Inc., 11001 Bren   #
# Road East, Minnetonka, MN, +1 952-912-3444, for commercial licensing     #
# opportunities for non-Digi products.                                     #
#                                                                          #
# DIGI SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED   #
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A          #
# PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, #
# PROVIDED HEREUNDER IS PROVIDED "AS IS" AND WITHOUT WARRANTY OF ANY KIND. #
# DIGI HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,         #
# ENHANCEMENTS, OR MODIFICATIONS.                                          #
#                                                                          #
# IN NO EVENT SHALL DIGI BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,      #
# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,   #
# ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF   #
# DIGI HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.                #
#                                                                          #
############################################################################

"""\
A Dia Driver for the XBee local Analog and Digital IO on the Coordinator
"""

import digihw
import time

from channels.channel import PERM_GET, PERM_SET, PERM_REFRESH, \
    OPT_AUTOTIMESTAMP
from channels.channel_source_device_property import ChannelSourceDeviceProperty
from common.types.boolean import Boolean, STYLE_ONOFF
from common.digi_device_info import get_platform_name
from devices.device_base import DeviceBase
from devices.xbee.xbee_devices.xbee_base import XBeeBase
from devices.xbee.common.prodid import MOD_XB_802154, MOD_XB_ZNET25, MOD_XB_ZB, \
    parse_dd
from devices.xbee.xbee_config_blocks.xbee_config_block_ddo import \
    XBeeConfigBlockDDO
from devices.xbee.xbee_device_manager.xbee_device_manager_event_specs import \
    XBeeDeviceManagerRunningEventSpec

from devices.xbee.common.io_sample import parse_is
from samples.sample import Sample
from settings.settings_base import SettingsBase, Setting

class XBeeLocalAIODIO(XBeeBase):
    """\
        This class extends one of our base classes and is intended as an
        example of a concrete, example implementation, but it is not itself
        meant to be included as part of our developer API. Please consult the
        base class documentation for the API and the source code for this file
        for an example implementation.

    """    
    #Mode constants for settings input
    AIO_MODE_CURRENTLOOP = "currentloop"
    AIO_MODE_TENV = "tenv"
    AIO_MODE_LIST = [AIO_MODE_CURRENTLOOP, AIO_MODE_TENV]
    
    DIO_DIR_IN = "in"
    DIO_DIR_OUT = "out"
    DIO_DIR_LIST = [DIO_DIR_IN, DIO_DIR_OUT]
    
    #Constants for calibration and CurrentLoop equation
    AIO_LOOP_R_OHMS = 51.3        
    AIO_CALIBRATION_06 = 511.5
    AIO_CALIBRATION_10 = 853.333
    
    #Constants for digihw configure_channel() call
    GPIO_MODE_CURRENTLOOP = 1
    GPIO_MODE_TENV = 2 
    GPIO_MODE_OUTPUT_HIGH = 3
    GPIO_MODE_OUTPUT_LOW = 4

    def __init__(self, name, core_services):
        self.__name = name
        self.__core = core_services

        ## Local State Variables:
        self.__xbee_manager = None        
        
        ## Calibration variables
        self.__scale = 0.0
        self.__calibration_method = None
        self.__last_calibration = 0 
        self.__platform = "digi"
        
        ## Scheduling variables
        self.__stop_event = False
        self.__eventid = 0

        # Settings
        #
        # xbee_device_manager: must be set to the name of an XBeeDeviceManager
        #                      instance.
        # sample_rate_ms: the sample rate of the XBee adapter.
        # channel1_mode: Operating input mode for pin 1 of the adapter.
        #                Must be a string value comprised of one of the following:
        #                    "TenV" - 0-10v input available on any channel.
        #                    "CurrentLoop" - 0-20 mA current loop available on
        #                                    any channel.
        # channel2_mode: Operating input mode for pin 2 of the adapter.
        #                See channel1_mode for valid setting information.
        # channel3_dir: Operating I/O mode for pin 1 of the adapter.
        #               Must be a string value comprised of one of the following:
        #                   "In" - pin is configured to be an input.
        #                   "Out" - pin is configured to be an output.
        # channel4_dir: Operating I/O mode for pin 2 of the adapter.
        #               See channel3_dir for valid setting information.  
        # channel3_source: If channel1_dir is configed as an output, this
        #                  option setting may be specified to a
        #                  "device.channel" channel name.  The Boolean value
        #                  of this channel will specify to logic state for
        #                  pin 1 on the adapter.
        # channel4_source: Configures output value source channel for pin 2
        #                  of the adapter.
        #                  See channel3_source setting information.

        settings_list = [
                         
            Setting(name='extended_address', type=str, default_value=''),
                         
            Setting(name='sample_rate_ms', type=int, required=True, 
                    default_value=60000, verify_function=lambda x: x >= 100),
                
            Setting(name='channel1_mode', type=str, 
                    default_value=self.AIO_MODE_TENV),
                    
            Setting(name='channel2_mode', type=str, 
                    default_value=self.AIO_MODE_TENV),
                    
            Setting(name='channel3_dir', type=str, required=True),
              
            Setting(name='channel3_source', type=str, default_value=''),
                    
            Setting(name='channel4_dir', type=str, required=True),
            
            Setting(name='channel4_source', type=str, default_value=''),
            
            Setting(name='calibration_rate_ms', type=float, 
                    default_value=900000), #15 min
        ]

        ## Channel Properties Definition:
        property_list = [
            ChannelSourceDeviceProperty(name="channel1_value", type=float,
                initial=Sample(timestamp=0, unit="V", value=0.0),
                refresh_cb = self.refresh_channels,
                perms_mask=PERM_GET|PERM_REFRESH, options=OPT_AUTOTIMESTAMP),
            ChannelSourceDeviceProperty(name="channel2_value", type=float,
                initial=Sample(timestamp=0, unit="V", value=0.0),
                refresh_cb = self.refresh_channels,
                perms_mask=PERM_GET|PERM_REFRESH, options=OPT_AUTOTIMESTAMP),
            ChannelSourceDeviceProperty(
                name='channel3_input', type=bool,
                initial=Sample(timestamp=0, value=False, unit='bool'),
                perms_mask=PERM_GET|PERM_REFRESH,
                refresh_cb = self.refresh_channels,
                options=OPT_AUTOTIMESTAMP),
            ChannelSourceDeviceProperty(
                name='channel4_input', type=bool,
                initial=Sample(timestamp=0, value=False, unit='bool'),
                perms_mask=PERM_GET|PERM_REFRESH,
                refresh_cb = self.refresh_channels,
                options=OPT_AUTOTIMESTAMP),
        ]
                                            
        ## Initialize the XBeeBase interface:
        XBeeBase.__init__(self, self.__name, self.__core,
                                settings_list, property_list)


    ## Functions which must be implemented to conform to the DeviceBase
    ## interface:

    def apply_settings(self):

        SettingsBase.merge_settings(self)
        accepted, rejected, not_found = SettingsBase.verify_settings(self)

        if len(rejected) or len(not_found):
            # there were problems with settings, terminate early:
            print ("XBeeLocalAIODIO: Settings rejected/not found: %s %s" 
                   %(rejected, not_found))
            return (accepted, rejected, not_found)
         
        accepted['channel1_mode'] = accepted['channel1_mode'].lower()
        accepted['channel2_mode'] = accepted['channel2_mode'].lower()
        
        accepted['channel3_dir'] = accepted['channel3_dir'].lower()
        accepted['channel4_dir'] = accepted['channel4_dir'].lower()
        
        if accepted['channel1_mode'] not in self.AIO_MODE_LIST:
            raise ValueError("channel1_mode:%s is not in %s" 
                             %(accepted['channel1_mode'], 
                               str(self.AIO_MODE_LIST)))  
        
        if accepted['channel2_mode'] not in self.AIO_MODE_LIST:
            raise ValueError("channel2_mode:%s is not in %s" 
                             %(accepted['channel2_mode'], 
                               str(self.AIO_MODE_LIST)))
            
        if accepted['channel3_dir'] not in self.DIO_DIR_LIST:
            raise ValueError("Channel3_dir:%s is not in %s" 
                             %(accepted['channel3_dir'],
                             str(self.DIO_MODE_LIST)))

        if accepted['channel4_dir'] not in self.DIO_DIR_LIST:
            raise ValueError("Channel4_dir:%s is not in %s" 
                             %(accepted['channel4_dir'],
                             str(self.DIO_MODE_LIST)))
        
        SettingsBase.commit_settings(self, accepted)

        return (accepted, rejected, not_found)

    def start(self):

        # Fetch the XBee Manager name from the Settings Manager:
        xbee_manager_name = self.get_setting("xbee_device_manager")
        dm = self.__core.get_service("device_driver_manager")
        self.__xbee_manager = dm.instance_get(xbee_manager_name)
        
        # Register ourselves with the XBee Device Manager instance:
        self.__xbee_manager.xbee_device_register(self)

        # Create a callback specification that calls back this driver when
        # our device has left the configuring state and has transitioned
        # to the running state:  
        
        xbdm_running_event_spec = XBeeDeviceManagerRunningEventSpec()
        xbdm_running_event_spec.cb_set(self.running_indication)
        self.__xbee_manager.xbee_device_event_spec_add(self, 
                                                       xbdm_running_event_spec)

        extended_address = None

        # Create a DDO configuration block for this device:
        xbee_ddo_cfg = XBeeConfigBlockDDO(extended_address)
        
        print "XBeeLocalAIODIO(%s): Configuring AIO lines" %self.__name
        
        for io_pin in range(2):
            xbee_ddo_cfg.add_parameter('D%s'%io_pin, 2)
            mode = self.get_setting('channel%s_mode' %(io_pin+1))          
            print ("XBeeLocalAIODIO(%s): Setting AIO pin: %s to mode: %s" 
                   %(self.__name, io_pin, mode))
            if mode == self.AIO_MODE_CURRENTLOOP:
                digihw.configure_channel(io_pin, self.GPIO_MODE_CURRENTLOOP)
            else:
                digihw.configure_channel(io_pin, self.GPIO_MODE_TENV)        
        
        ic = 0
        print "XBeeLocalAIODIO(%s): Configuring DIO lines" %self.__name        
        for io_pin in range(2, 4):            
            xbee_ddo_cfg.add_parameter('D%s'%io_pin, 3)
            direction = self.get_setting('channel%d_dir' % (io_pin+1))
            ic |= 1 << io_pin                        
            if direction == self.DIO_DIR_OUT:
                self.add_property(
                  ChannelSourceDeviceProperty(
                  name='channel%d_output' % (io_pin+1), type=bool,
                  initial=Sample(timestamp=0, value=False, unit='bool'),
                  perms_mask=(PERM_GET|PERM_SET),
                  options=OPT_AUTOTIMESTAMP,
                  set_cb=lambda sample, io=io_pin: self.set_output(sample, io))
                )

                # If set, subscribe to the channel that drives our output 
                # logic:
                source = self.get_setting('channel%d_source'% (io_pin+1))
                                
                if len(source):
                    cp.subscribe(source, lambda chan, io=io_pin: 
                                 self.update(chan, io))
                
        print ("XBeeLocalAIODIO(%s): Line detect change mask is: %s" 
               %(self.__name, hex(ic)))
        
        # Register this configuration block with the XBee Device Manager:
        self.__xbee_manager.xbee_device_config_block_add(self, xbee_ddo_cfg)

        # Indicate that we have no more configuration to add:
        self.__xbee_manager.xbee_device_configure(self)

        return True

    def stop(self):

        # Unregister ourselves with the XBee Device Manager instance:
        self.__xbee_manager.xbee_device_unregister(self)        
        self.__stop_event = True
        return True
          
    def make_request(self):
        """Self perpetuating function to manage calibration and sample 
        gathering"""
        if self.__stop_event:
            return ##We don't schedule another make_request, so should be last
        
        now = time.time()
        if ((now - self.__last_calibration) * 1000 >= 
            (self.get_setting('calibration_rate_ms'))):
          
            #print "XBeeLocalAIODIO(%s): Performing Calibration" %self.__name
            try:
                self.__calibrate()
                self.__last_calibration = now
            except Exception, e:
                print ("XBeeLocalAIODIO(%s): Error calibrating: %s"
                       %(self.__name, str(e)))
         
        try:
            resp = self.__xbee_manager.xbee_device_ddo_get_param(None, 'IS')                
            self.decode_sample(resp)
        except Exception, e:
            print ("XBeeLocalAIODIO(%s): Error retrieving/decoding sample: %s" 
                   %(self.__name, str(e)))
            
        delay = self.get_setting('sample_rate_ms') / 1000.0
        #print "Delaying for %.2f seconds" %delay
        self.__eventid = self.__xbee_manager.xbee_device_schedule_after(delay, 
                            self.make_request)
        
    def running_indication(self):
        # Our device is now running, load our initial state:
        print "XBeeLocalAIODIO(%s): running indication" %self.__name
        
        try:
            dd = self.__xbee_manager.xbee_device_ddo_get_param(None, 'DD', 
                                                               use_cache=True)
            module_id, product_id = parse_dd(dd)            
            self.__calibration_method = module_id
        except Exception, e:
            print ("XBeeLocalAIODIO(%s): Failure getting DD, using ZB family"
                    %(self.__name, str(e)))
            self.__calibration_method = MOD_XB_ZB 
        
        self.__platform = get_platform_name()
        
        self.make_request()

    def decode_sample(self, buf):        
        #print "XBeeLocalAIODIO(%s): Decoding sample" %self.__name
        io_sample = parse_is(buf)        
        for io_pin in range(2):            
            aio_name = 'AD%d' %(io_pin)
            #print "Decoding sample ", aio_name
            channel_name = 'channel%d_value' %(io_pin+1)
            mode = self.get_setting('channel%d_mode' %(io_pin+1))
          
            if aio_name not in io_sample:
                raise RuntimeError("XBeeLocalAIODIO(%s): Could not find " + 
                                   "AIO sample: %s in parsed_samples: %s"
                                   %(self.__name, aio_name, str(io_sample)))
          
            raw_value = io_sample[aio_name]
            scaled_value = float(raw_value) * self.__scale
            #print "Scaled value is: ", scaled_value
            
            ## NEED RIGHT FORUMULA
            if mode == self.AIO_MODE_CURRENTLOOP:
                mA = (scaled_value * 1200.0 / 1023) / self.AIO_LOOP_R_OHMS
                #print "Value is: ", mA
                self.property_set(channel_name, Sample(0, mA, 'mA'))
            else:
                mV = (scaled_value * 10.25) / 1023.0
                #print "Value is: ", mV         
                self.property_set(channel_name, Sample(0, mV, 'V'))
        
        for io_pin in range(2, 4):
            dio_name = 'DIO%d' %(io_pin)
            if dio_name not in io_sample:
                raise RuntimeError("XBeeLocalAIODIO(%s): Could not find " +
                                   "DIO sample: %s in parsed_samples: %s" 
                                   %(self.__name, dio_name, str(io_sample)))
                
            channel_name = 'channel%d_input' %(io_pin+1)
            self.property_set(channel_name, Sample(0, 
                                                   bool(io_sample[dio_name]), 
                                                   'bool'))

    def __calibrate(self):        
        
        if ((self.__calibration_method == MOD_XB_ZB) or
            (self.__calibration_method == MOD_XB_ZNET25)):
            
            digihw.configure_channel(0, self.GPIO_MODE_TENV)
            digihw.configure_channel(1, self.GPIO_MODE_TENV)
            time.sleep(.01)
            
            if self.__platform == 'digix3':
                digihw.gpio_set_value(0, 0) ##x3's method of going into calibration mode
            else:
                self.__xbee_manager.xbee_device_ddo_set_param(None, 'P2', 
                                                              4, apply=True)
                self.__xbee_manager.xbee_device_ddo_set_param(None, 'D4', 4, 
                                                              apply=True)
          
            resp = self.__xbee_manager.xbee_device_ddo_get_param(None, 'IS')
            sample = parse_is(resp)
            
            if self.__platform == 'digix3':
                pass ##Next block will configure channel 0 properly
            else:
                self.__xbee_manager.xbee_device_ddo_set_param(None, 'P2', 
                                                              5, apply=True)
                self.__xbee_manager.xbee_device_ddo_set_param(None, 'D4', 
                                                              5, apply=True)
            
            for io_pin in range(2):    
                mode = self.get_setting('channel%s_mode' %(io_pin+1))                
                if mode == self.AIO_MODE_CURRENTLOOP:
                    digihw.configure_channel(io_pin, 
                                             self.GPIO_MODE_CURRENTLOOP)
                else:
                    digihw.configure_channel(io_pin, self.GPIO_MODE_TENV)                    
            time.sleep(.01)
          
            calib1 = float(sample["AD1"])
            calib2 = float(sample["AD0"])            
          
            scale1 = self.AIO_CALIBRATION_06 / calib1
            scale2 = self.AIO_CALIBRATION_10 / calib2
            self.__scale = (scale1 + scale2) / 2.0
          
        elif self.__calibration_method == MOD_XB_802154:
          
            # Enable calibration voltage on channel 1
            self.__xbee_manager.xbee_device_ddo_set_param(None, 'D4', 4)
            time.sleep(0.010)

            # Read calibration sample
            result = self.__xbee_manager.xbee_device_ddo_get_param(None, 'IS')
            sample = parse_is(result)["AD1"]
            print ("XBeeLocalAIODIO(%s): Calibration sample is %d" 
                   %(self.__name, sample))

            # Return channel to operating mode
            self.__xbee_manager.xbee_device_ddo_set_param(None, 'D4', 5)
            time.sleep(0.010) 

            if sample == 0:
                print "Calibration error, samples are equal"
                return

            # Calculate linear scale and offset.
            # These apply to all analog channels.
            self.__scale = 1.25 / sample
        else:
            raise ValueError("Unknown Module type: %s" 
                             %self.__calibration_method)            
        
        #print "XBeeLocalAIODIO: Scale is %f" % (self.__scale)
        
    def refresh_channels(self):
        """Refreshes input channels of device when called.
        
        To do this, it cancels the current make_request, then schedules the 
        next make_request to occurr immediately.  This keeps all make_requests
        occuring in the same thread of execution.  
        """
        try:
            self.__xbee_manager.xbee_device_schedule_cancel(self.__eventid)
        except Exception, e:
            print ("Refresh: Failed to cancel make_request for reason: %s" 
                   %str(e))
        
        self.__xbee_manager.xbee_device_schedule_after(0.0, self.make_request)
         
    def set_output(self, sample, io_pin):
        new_val = False
        try:
            new_val = bool(sample.value)
        except Exception, e:
            print ("XBeeLocalAIODIO(%s): Error converting %s to bool" 
                   %(str(sample.value)))
            return

        ddo_val = self.DIO_MODE_OUTPUT_LOW
        if new_val:
            ddo_val = self.DIO_MODE_OUTPUT_HIGH

        digihw.configure_channel(io_pin, ddo_val)
        property = "channel%d_output" % (io_pin + 1)
        self.property_set(property, Sample(0, new_val, "bool")) 
    
    def update(self, channel, io_pin):
        sample = channel.get()
        self.set_output(sample, io_pin)
