#!/usr/bin/python
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import cellular_system_error
import cellular_logging
import os
import select
import socket
import traceback


class PrologixScpiDriver:
    """Wrapper for a Prologix TCP<->GPIB bridge.
    http://prologix.biz/gpib-ethernet-controller.html
    http://prologix.biz/index.php?dispatch=attachments.getfile&attachment_id=1

    Communication is over a plain TCP stream on port 1234.  Commands to
    the bridge are in-band, prefixed with ++.

    Notable instance variables include:

      self.auto: When 1, the bridge automatically addresses the target
        in listen mode.  When 0, we must issue a ++read after every
        query.  As of Aug '11, something between us and the Agilent 8960
        is wrong such that running in auto=0 mode leaves us hanging if
        we issue '*RST;*OPC?'
    """
    all_open_connections = {}

    def __init__(self, hostname, port=1234, gpib_address=14,
                 read_timeout_seconds=30, connect_timeout_seconds=5):
        """Constructs a wrapper for the Prologix TCP<->GPIB bridge :
        Arguments:
            hostname: hostname of prologix device
            port: port number
            gpib_address: initial GPIB device to connect to
            read_timeout_seconds: the read time out for the socket to the
                prologix box
            connect_timeout_seconds: the read time out for the socket to the
                prologix box
        """
        logger_name = 'prologix'
        s = 'IP:%s GPIB:%s: ' % (hostname, gpib_address)
        formatter_string = '%(asctime)s %(filename)s %(lineno)d ' + s + \
                           '- %(message)s'
        self.scpi_logger = cellular_logging.SetupCellularLogging(
            logger_name, formatter_string)

        self.connection_key = "%s:%s" % (hostname, port)
        self.connection_data = {self.connection_key: traceback.format_stack()}
        if self.connection_key in self.all_open_connections.keys():
            raise cellular_system_error.BadState(
              'IP network connection to '
              'prologix is already in use. : %s ' % self.all_open_connections)
        self.all_open_connections[self.connection_key] = self.connection_data
        self.socket = connect_to_port(hostname, port, connect_timeout_seconds)
        self.read_timeout_seconds = read_timeout_seconds
        self.socket.setblocking(0)
        self.SetAuto(1)
        self._AddCarrigeReturnsToResponses()
        self.SetGpibAddress(gpib_address)
        self.scpi_logger.debug('set read_timeout_seconds: %s ' %
                               self.read_timeout_seconds)

    def __del__(self):
        self.Close()

    def _AddCarrigeReturnsToResponses(self):
        """
        Have the prologix box add a line feed to each response.
        Some instruments may need this.
        """
        pass
        self.Send('++eot_enable 1')
        self.Send('++eot_char 10')

    def SetAuto(self, auto):
        """Controls Prologix read-after-write (aka 'auto') mode."""
        # Must be an int so we can send it as an arg to ++auto.
        self.auto = int(auto)
        self.Send('++auto %d' % self.auto)

    def Close(self):
        """Closes the socket."""
        try:
            self.scpi_logger.error('Closing prologix devices at : %s ' %
                                   self.connection_key)
            self.all_open_connections.pop(self.connection_key)
        except KeyError:
            self.scpi_logger.error('Closed %s more then once' %
                                   self.connection_key)
        try:
            self.socket.close()
        except AttributeError:  # Maybe we close before we finish building.
            pass

    def SetGpibAddress(self, gpib_address):
        max_tries = 10
        while max_tries > 0:
            max_tries -= 1
            self.Send('++addr %s' % gpib_address)
            read_back_value = self._DirectQuery('++addr')
            try:
                if int(read_back_value) == int(gpib_address):
                    break
            except ValueError:
                # If we read a string, don't raise, just try again.
                pass
            self.scpi_logger.error('Set gpib addr to: %s, read back: %s' %
                                   (gpib_address, read_back_value))
            self.scpi_logger.error('Setting the GPIB address failed. ' +
                                   'Trying again...')

    def Send(self, command):
        self.scpi_logger.info('] %s', command)
        try:
            self.socket.send(command + '\n')
        except Exception as e:
            self.scpi_logger.error('sending SCPI command %s failed. ' %
                                   command)
            self.scpi_logger.exception(e)
            raise SystemError('Sending SCPI command failed. '
                              'Did the instrument stopped talking?')

    def Reset(self):
        """Sends a standard SCPI reset and waits for it to complete."""
        # There is some misinteraction between the devices such that if we
        # send *RST and *OPC? and then manually query with ++read,
        # occasionally that ++read doesn't come back.  We currently depend
        # on self.Query to turn on Prologix auto mode to avoid this
        self.Send('*RST')
        self.Query('*OPC?')

    def Read(self):
        """Read a response from the bridge."""
        try:
            ready = select.select([self.socket], [], [],
                                  self.read_timeout_seconds)
        except Exception as e:
            self.scpi_logger.exception(e)
            s = 'Read from the instrument failed. Timeout:%s' % \
                self.read_timeout_seconds
            self.scpi_logger.error(s)
            raise SystemError(s)

        if ready[0]:
            response = self.socket.recv(4096)
            response = response.rstrip()
            self.scpi_logger.info('[ %s', response)
            return response
        else:
            self.Close()
            s = 'Connection to the prologix adapter worked.' \
                'But there was not data to read from the instrument.' \
                'Does that command return a result?' \
                'Bad GPIB port number, or timeout too short?'
        raise cellular_system_error.InstrumentTimeout(s)

    def Query(self, command):
        """Send a GPIB command and return the response."""
        #self.SetAuto(1) #maybe useful?

        s = list(self.scpi_logger.findCaller())
        s[0] = os.path.basename(s[0])

        s = list(self.scpi_logger.findCaller())
        s[0] = os.path.basename(s[0])
        self.scpi_logger.debug('caller :' + str(s) + command)

        self.Send(command)
        if not self.auto:
            self.Send('++read eoi')
        output = self.Read()
        #self.SetAuto(0) #maybe useful?
        return output

    def _DirectQuery(self, command):
        """Sends a query to the prologix (do not send ++read).

        Returns: response of the query.
        """
        self.Send(command)
        return self.Read()


def connect_to_port(hostname, port, connect_timeout_seconds):
    # Right out of the python documentation,
    #  http://docs.python.org/library/socket.html
    for res in socket.getaddrinfo(
                hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
        af, socktype, proto, _, sa = res
        try:
            s = socket.socket(af, socktype, proto)
        except socket.error as msg:
            raise cellular_system_error.SocketTimeout(
                'Failed to make a new socket object. ' + str(msg))
        try:
            s.settimeout(connect_timeout_seconds)
            s.connect(sa)
        except socket.error as msg:
            try:
                s.close()
            except Exception:
                pass  # Try to close it, but it may not have been created.
            temp_string_var = ' Could be bad IP address. Tried: %s : %s' % \
                              (hostname, port)
            raise cellular_system_error.SocketTimeout(str(msg) +
                                                      temp_string_var)
    return s