# Copyright (c) 2014 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 logging
import os

from autotest_lib.client.common_lib import error


SYS_GPIO_PATH = '/sys/class/gpio/'
SYS_PINMUX_PATH = '/sys/kernel/debug/omap_mux/'
OMAP_MUX_GPIO_MODE = 'OMAP_MUX_MODE7'

MAX_VARIABLE_ATTENUATION = 95

# Index of GPIO banks. Each GPIO bank is 32-bit long.
GPIO_BANK0 = 0
GPIO_BANK1 = 1
GPIO_BANK2 = 2


class GpioPin(object):
    """Contains relevant details about a GPIO pin."""
    def __init__(self, bank, bit, pinmux_file, pin_name):
        """Construct a GPIO pin object.

        @param bank: int GPIO bank number (from 0-2 on BeagleBone White).
        @param bit: int bit offset in bank (from 0-31 on BeagleBone White).
        @param pinmux_file: string name of pinmux file.  This file is used to
                set the mode of a pin.  For instance, some pins are part of
                UART interfaces in addition to being GPIO capable.
        @param pin_name: string name of pin for debugging.

        """
        self.offset = str(bank * 32 + bit)
        self.pinmux_file = os.path.join(SYS_PINMUX_PATH, pinmux_file)
        self.pin_name = pin_name
        self.value_file = os.path.join(SYS_GPIO_PATH, 'gpio' + self.offset,
                                       'value')
        self.export_file = os.path.join(SYS_GPIO_PATH, 'export')
        self.unexport_file = os.path.join(SYS_GPIO_PATH, 'unexport')
        self.direction_file = os.path.join(SYS_GPIO_PATH, 'gpio' + self.offset,
                                           'direction')

# Variable attenuators are controlled by turning GPIOs on and off.  GPIOs
# are arranged in 3 banks on the BeagleBone White, 32 pins to a bank.  We
# pick groups of 8 pins such that the pins are physically near to each other
# to form the inputs to a given variable attenuator.  These inputs spell
# a binary word, which corresponds to the generated attenuation in dB.  For
# instance, turning on bits 0, 3, and 5 in a group:
#
#      attenuation = (1 << 0) + (1 << 3) + (1 << 5) = 0x25 = 37 dB
#
# Bits are listed in ascending order in a group (bit 0 first).  There are
# four groups of bits, one group per attenuator.
#
# Note that there is also a fixed amount of loss generated by the attenuator
# that we account for in the constant for the fixed loss along the path
# for a given antenna.
#
# On hosts with 4 attenuators, these are arranged so that attenuators 0/1
# control the main/aux antennas of a radio, and 2/3 control the main/aux
# lines of a second radio.  For hosts with only two attenuators, there
# should also be only a single phy.
#
# These mappings are specific to:
#  hardware: BeagleBone board (revision A3)
#  operating system: Angstrom Linux v2011.11-core (Core edition)
#  image version:
#      Angstrom-Cloud9-IDE-eglibc-ipk-v2011.11-core-beaglebone-2011.11.16
VARIABLE_ATTENUATORS = {
        0: [GpioPin(GPIO_BANK1, 31, 'gpmc_csn2', 'GPIO1_31'),
            GpioPin(GPIO_BANK1, 30, 'gpmc_csn1', 'GPIO1_30'),
            GpioPin(GPIO_BANK1, 5,  'gpmc_ad5',  'GPIO1_5'),
            GpioPin(GPIO_BANK1, 4,  'gpmc_ad4',  'GPIO1_4'),
            GpioPin(GPIO_BANK1, 1,  'gpmc_ad1',  'GPIO1_1'),
            GpioPin(GPIO_BANK1, 0,  'gpmc_ad0',  'GPIO1_0'),
            GpioPin(GPIO_BANK1, 29, 'gpmc_csn0', 'GPIO1_29'),
            GpioPin(GPIO_BANK2, 22, 'lcd_vsync', 'GPIO2_22'),
           ],
        1: [GpioPin(GPIO_BANK1, 6,  'gpmc_ad6',      'GPIO1_6'),
            GpioPin(GPIO_BANK1, 2,  'gpmc_ad2',      'GPIO1_2'),
            GpioPin(GPIO_BANK1, 3,  'gpmc_ad3',      'GPIO1_3'),
            GpioPin(GPIO_BANK2, 2,  'gpmc_advn_ale', 'TIMER4'),
            GpioPin(GPIO_BANK2, 3,  'gpmc_oen_ren',  'TIMER7'),
            GpioPin(GPIO_BANK2, 5,  'gpmc_ben0_cle', 'TIMER5'),
            GpioPin(GPIO_BANK2, 4,  'gpmc_wen',      'TIMER6'),
            GpioPin(GPIO_BANK1, 13, 'gpmc_ad13',     'GPIO1_13'),
           ],
        2: [GpioPin(GPIO_BANK1, 12, 'gpmc_ad12',  'GPIO1_12'),
            GpioPin(GPIO_BANK0, 23, 'gpmc_ad9',   'EHRPWM2B'),
            GpioPin(GPIO_BANK0, 26, 'gpmc_ad10',  'GPIO0_26'),
            GpioPin(GPIO_BANK1, 15, 'gpmc_ad15',  'GPIO1_15'),
            GpioPin(GPIO_BANK1, 14, 'gpmc_ad14',  'GPIO1_14'),
            GpioPin(GPIO_BANK0, 27, 'gpmc_ad11',  'GPIO0_27'),
            GpioPin(GPIO_BANK2, 1,  'mcasp0_fsr', 'GPIO2_1'),
            GpioPin(GPIO_BANK0, 22, 'gpmc_ad11',  'EHRPWM2A'),
           ],
        3: [GpioPin(GPIO_BANK2, 24, 'lcd_pclk',       'GPIO2_24'),
            GpioPin(GPIO_BANK2, 23, 'lcd_hsync',      'GPIO2_23'),
            GpioPin(GPIO_BANK2, 25, 'lcd_ac_bias_en', 'GPIO2_25'),
            GpioPin(GPIO_BANK0, 10, 'lcd_data14',     'UART5_CTSN'),
            GpioPin(GPIO_BANK0, 11, 'lcd_data15',     'UART5_RTSN'),
            GpioPin(GPIO_BANK0, 9,  'lcd_data13',     'UART4_RTSN'),
            GpioPin(GPIO_BANK2, 17, 'lcd_data11',     'UART3_RTSN'),
            GpioPin(GPIO_BANK0, 8,  'lcd_data12',     'UART4_CTSN'),
           ],
}


# This map represents the fixed loss overhead on a given antenna line.
# The map maps from:
#     attenuator hostname -> attenuator number -> frequency -> loss in dB.
HOST_TO_FIXED_ATTENUATIONS = {
        'chromeos1-grover-host1-attenuator': {
                0: {2437: 53, 5220: 56, 5765: 56},
                1: {2437: 54, 5220: 56, 5765: 59},
                2: {2437: 54, 5220: 57, 5765: 57},
                3: {2437: 54, 5220: 57, 5765: 59}},
        'chromeos1-grover-host2-attenuator': {
                0: {2437: 55, 5220: 59, 5765: 59},
                1: {2437: 53, 5220: 55, 5765: 55},
                2: {2437: 56, 5220: 60, 5765: 59},
                3: {2437: 56, 5220: 58, 5765: 58}},
        'chromeos1-grover-host3-attenuator': {
                0: {2437: 54, 5220: 59, 5765: 57},
                1: {2437: 54, 5220: 57, 5765: 57},
                2: {2437: 54, 5220: 58, 5765: 57},
                3: {2437: 54, 5220: 57, 5765: 57}},
        'chromeos1-grover-host4-attenuator': {
                0: {2437: 54, 5220: 58, 5765: 58},
                1: {2437: 54, 5220: 58, 5765: 58},
                2: {2437: 54, 5220: 58, 5765: 58},
                3: {2437: 54, 5220: 57, 5765: 57}},
        'chromeos1-grover-host5-attenuator': {
                0: {2437: 51, 5220: 59, 5765: 64},
                1: {2437: 52, 5220: 56, 5765: 57},
                2: {2437: 53, 5220: 57, 5765: 61},
                3: {2437: 52, 5220: 56, 5765: 57}},
        'chromeos1-grover-host6-attenuator': {
                0: {2437: 54, 5220: 56, 5765: 57},
                1: {2437: 54, 5220: 56, 5765: 58},
                2: {2437: 54, 5220: 56, 5765: 57},
                3: {2437: 54, 5220: 57, 5765: 58}},
        'chromeos1-grover-host7-attenuator': {
                0: {2437: 59, 5220: 61, 5765: 62},
                1: {2437: 59, 5220: 64, 5765: 66},
                2: {2437: 59, 5220: 61, 5765: 65},
                3: {2437: 58, 5220: 60, 5765: 63}},
        'chromeos1-grover-host8-attenuator': {
                0: {2437: 64, 5220: 64, 5765: 63},
                1: {2437: 65, 5220: 61, 5765: 63},
                2: {2437: 66, 5220: 67, 5765: 70},
                3: {2437: 68, 5220: 64, 5765: 65}},
        'chromeos1-grover-host9-attenuator': {
                0: {2437: 56, 5220: 63, 5765: 64},
                1: {2437: 59, 5220: 63, 5765: 66},
                2: {2437: 59, 5220: 65, 5765: 66},
                3: {2437: 57, 5220: 63, 5765: 63}},
        'chromeos1-grover-host10-attenuator': {
                0: {2437: 59, 5220: 64, 5765: 67},
                1: {2437: 66, 5220: 70, 5765: 64},
                2: {2437: 60, 5220: 67, 5765: 65},
                3: {2437: 65, 5220: 68, 5765: 61}},
        'chromeos1-grover-host11-attenuator': {
                0: {2437: 62, 5220: 62, 5765: 66},
                1: {2437: 57, 5220: 63, 5765: 65},
                2: {2437: 63, 5220: 63, 5765: 68},
                3: {2437: 56, 5220: 60, 5765: 64}},
        'chromeos1-grover-host12-attenuator': {
                0: {2437: 68, 5220: 66, 5765: 70},
                1: {2437: 56, 5220: 60, 5765: 63},
                2: {2437: 67, 5220: 64, 5765: 68},
                3: {2437: 57, 5220: 61, 5765: 64}},
        }


class AttenuatorController(object):
    """Represents a BeagleBone controlling several variable attenuators.

    This device is used to vary the attenuation between a router and a client.
    This allows us to measure throughput as a function of signal strength and
    test some roaming situations.  The throughput vs signal strength tests
    are referred to rate vs range (RvR) tests in places.

    @see BeagleBone System Reference Manual (RevA3_1.0):
        http://beagleboard.org/static/beaglebone/a3/Docs/Hardware/BONE_SRM.pdf
    @see Texas Instrument's GPIO Driver Guide
        http://processors.wiki.ti.com/index.php/GPIO_Driver_Guide

    """

    @property
    def supported_attenuators(self):
        """@return iterable of int attenuators supported on this host."""
        return self._fixed_attenuations.keys()


    def __init__(self, host):
        """Construct a AttenuatorController.

        @param host: Host object representing the remote BeagleBone.

        """
        super(AttenuatorController, self).__init__()
        self._host = host
        hostname = host.hostname
        if hostname.find('.') > 0:
            hostname = hostname[0:hostname.find('.')]
        if hostname not in HOST_TO_FIXED_ATTENUATIONS.keys():
            raise error.TestError('Unexpected RvR host name %r.' % hostname)
        self._fixed_attenuations = HOST_TO_FIXED_ATTENUATIONS[hostname]
        logging.info('Configuring GPIO ports on attenuator host.')
        for attenuator in self.supported_attenuators:
            for gpio_pin in VARIABLE_ATTENUATORS[attenuator]:
                self._enable_gpio_pin(gpio_pin)
                self._setup_gpio_pin(gpio_pin)
        self.set_variable_attenuation(0)


    def _approximate_frequency(self, attenuator_num, freq):
        """Finds an approximate frequency to freq.

        In case freq is not present in self._fixed_attenuations, we use a value
        from a nearby channel as an approximation.

        @param attenuator_num: attenuator in question on the remote host.  Each
                attenuator has a different fixed path loss per frequency.
        @param freq: int frequency in MHz.
        @returns int approximate frequency from self._fixed_attenuations.

        """
        old_offset = None
        approx_freq = None
        for defined_freq in self._fixed_attenuations[attenuator_num].keys():
            new_offset = abs(defined_freq - freq)
            if old_offset is None or new_offset < old_offset:
                old_offset = new_offset
                approx_freq = defined_freq

        logging.debug('Approximating attenuation for frequency %d with '
                      'constants for frequency %d.', freq, approx_freq)
        return approx_freq


    def _enable_gpio_pin(self, gpio_pin):
        """Enable a pin's GPIO function.

        @param gpio_pin: GpioPin object.

        """
        self._host.run('echo 7 > %s' % gpio_pin.pinmux_file)
        # Example contents of pinmux sysfile:
        #  name: lcd_pclk.lcd_pclk (0x44e108e8/0x8e8 = 0x0000), b NA, t NA
        #  mode: OMAP_PIN_OUTPUT | OMAP_MUX_MODE0
        #  signals: lcd_pclk | NA | NA | NA | NA | NA | NA | NA
        desired_prefix = 'mode:'
        result = self._host.run('cat %s' % gpio_pin.pinmux_file)
        for line in result.stdout.splitlines():
            if not line.startswith(desired_prefix):
                continue
            line = line[len(desired_prefix):]
            modes = [mode.strip() for mode in line.split('|')]
            break
        else:
            raise error.TestError('Failed to parse pinmux file')

        if OMAP_MUX_GPIO_MODE not in modes:
            raise error.TestError('Error setting pin %s to GPIO mode' %
                                  gpio_pin.pin_name)


    def _setup_gpio_pin(self, gpio_pin, enable=True):
        """Export or unexport a GPIO pin.

        GPIO pins must be exported before becoming usable.

        @param gpio_pin: GpioPin object.
        @param enable: bool True to export this pin.

        """
        if enable:
            sysfile = gpio_pin.export_file
        else:
            sysfile = gpio_pin.unexport_file
        self._host.run('echo %s > %s' % (gpio_pin.offset, sysfile),
                       ignore_status=True)
        if enable:
            # Set it to output
            self._host.run('echo out > %s' % gpio_pin.direction_file)


    def close(self):
        """Close this BB host and turn off all variabel attenuation."""
        self.set_variable_attenuation(0)
        self._host.close()


    def set_total_attenuation(self, atten_db, frequency_mhz,
                              attenuator_num=None):
        """Set the total attenuation on one or all attenuators.

        @param atten_db: int level of attenuation in dB.  This must be
                higher than the fixed attenuation level of the affected
                attenuators.
        @param frequency_mhz: int frequency for which to calculate the
                total attenuation.  The fixed component of attenuation
                varies with frequency.
        @param attenuator_num: int attenuator to change, or None to
                set all variable attenuators.

        """
        affected_attenuators = self.supported_attenuators
        if attenuator_num is not None:
            affected_attenuators = [attenuator_num]
        for attenuator in affected_attenuators:
            freq_to_fixed_loss = self._fixed_attenuations[attenuator]
            approx_freq = self._approximate_frequency(attenuator,
                                                      frequency_mhz)
            variable_atten_db = atten_db - freq_to_fixed_loss[approx_freq]
            self.set_variable_attenuation(variable_atten_db,
                                          attenuator_num=attenuator)


    def set_variable_attenuation(self, atten_db, attenuator_num=None):
        """Set the variable attenuation on one or all attenuators.

        @param atten_db: int non-negative level of attenuation in dB.
        @param attenuator_num: int attenuator to change, or None to
                set all variable attenuators.

        """
        if atten_db > MAX_VARIABLE_ATTENUATION:
            raise error.TestError('Requested variable attenuation greater '
                                  'than maximum. (%d > %d)' %
                                  (atten_db, MAX_VARIABLE_ATTENUATION))

        if atten_db < 0:
            raise error.TestError('Only positive attenuations are supported. '
                                  '(requested %d)' % atten_db)

        affected_attenuators = self.supported_attenuators
        if attenuator_num is not None:
            affected_attenuators = [attenuator_num]
        for attenuator in affected_attenuators:
            bit_field = atten_db
            for gpio_pin in VARIABLE_ATTENUATORS[attenuator]:
                bit_value = bit_field & 1
                self._host.run('echo %d > %s' %
                               (bit_value, gpio_pin.value_file))
                bit_field = bit_field >> 1


    def get_minimal_total_attenuation(self):
        """Get attenuator's maximum fixed attenuation value.

        This is pulled from the current attenuator's lines and becomes the
        minimal total attenuation when stepping through attenuation levels.

        @return maximum starting attenuation value

        """
        max_atten = 0
        for atten_num in self._fixed_attenuations.iterkeys():
            atten_values = self._fixed_attenuations[atten_num].values()
            max_atten = max(max(atten_values), max_atten)
        return max_atten