# Copyright 2016 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 math
import time

from autotest_lib.client.common_lib import error
from autotest_lib.server.cros.faft.firmware_test import FirmwareTest
from autotest_lib.server.cros.servo import pd_console


class firmware_PDVbusRequest(FirmwareTest):
    """
    Servo based USB PD VBUS level test. This test is written to use both
    the DUT and Plankton test board. It requires that the DUT support
    dualrole (SRC or SNK) operation. VBUS change requests occur in two
    methods. First, with the DUT in SNK mode, it uses the pd console command
    'pd 0/1 dev V' command where V is the desired voltage 5/12/20. The 2nd
    test initiates the VBUS change by using special Plankton feature to
    send new SRC CAP message. This causes the DUT to request a new VBUS
    voltage mathcing what's in the SRC CAP message.

    Pass critera is all voltage transitions are successful.

    """
    version = 1

    PD_SETTLE_DELAY = 4
    USBC_SINK_VOLTAGE = 5
    USBC_MAX_VOLTAGE = 20
    VBUS_TOLERANCE = 0.12

    VOLTAGE_SEQUENCE = [5, 12, 20, 12, 5, 20, 5, 5, 12, 12, 20]

    def _compare_vbus(self, expected_vbus_voltage):
        """Check VBUS using plankton

        @param expected_vbus_voltage: nominal VBUS level (in volts)

        @returns: a tuple containing pass/fail indication and logging string
        """
        # Get Vbus voltage and current
        vbus_voltage = self.plankton.vbus_voltage
        vbus_current = self.plankton.vbus_current
        # Compute voltage tolerance range
        tolerance = self.VBUS_TOLERANCE * expected_vbus_voltage
        voltage_difference = math.fabs(expected_vbus_voltage - vbus_voltage)
        result_str = 'Target = %02dV:\tAct = %.2f\tDelta = %.2f' % \
                     (expected_vbus_voltage, vbus_voltage, voltage_difference)
        # Verify that measured Vbus voltage is within expected range
        voltage_difference = math.fabs(expected_vbus_voltage - vbus_voltage)
        if voltage_difference > tolerance:
            result = 'FAIL'
        else:
            result = 'PASS'
        return result, result_str

    def initialize(self, host, cmdline_args):
        super(firmware_PDVbusRequest, self).initialize(host, cmdline_args)
        # Only run in normal mode
        self.switcher.setup_mode('normal')
        self.usbpd.send_command('chan 0')

    def cleanup(self):
        self.usbpd.send_command('chan 0xffffffff')
        super(firmware_PDVbusRequest, self).cleanup()

    def run_once(self):
        """Exectue VBUS request test.

        """

        # create objects for pd utilities
        pd_dut_utils = pd_console.PDConsoleUtils(self.usbpd)
        pd_plankton_utils = pd_console.PDConsoleUtils(self.plankton)

        # Make sure PD support exists in the UART console
        if pd_dut_utils.verify_pd_console() == False:
            raise error.TestFail("pd command not present on console!")

        # Type C connection (PD contract) should exist at this point
        dut_state = pd_dut_utils.query_pd_connection()
        logging.info('DUT PD connection state: %r', dut_state)
        if dut_state['connect'] == False:
            raise error.TestFail("pd connection not found")
        if dut_state['role'] != pd_dut_utils.SNK_CONNECT:
            # DUT needs to be in SINK Mode, attempt to force change
            pd_dut_utils.set_pd_dualrole('snk')
            time.sleep(self.PD_SETTLE_DELAY)
            if pd_dut_utils.get_pd_state(dut_state['port']) != pd_dut_utils.SNK_CONNECT:
                raise error.TestFail("DUT not able to connect in SINK mode")

        # Plankton must be set to 20V SRC mode in order for the DUT
        # to be able to request all 3 possible voltage levels (5, 12, 20).
        # The DUT must be in SNK mode for the pd <port> dev <voltage>
        # command to have an effect.
        self.plankton.charge(self.USBC_MAX_VOLTAGE)
        time.sleep(self.PD_SETTLE_DELAY)
        logging.info('Start of DUT initiated tests')
        dut_failures = []
        for v in self.VOLTAGE_SEQUENCE:
            # Build 'pd <port> dev <voltage> command
            cmd = 'pd %d dev %d' % (dut_state['port'], v)
            pd_dut_utils.send_pd_command(cmd)
            time.sleep(self.PD_SETTLE_DELAY)
            result, result_str = self._compare_vbus(v)
            logging.info('%s, %s', result_str, result)
            if result == 'FAIL':
                dut_failures.append(result_str)

        # Make sure Plankton is set back to 20VSRC so DUT will accept all options
        cmd = 'pd %d dev %d' % (dut_state['port'], self.USBC_MAX_VOLTAGE)
        time.sleep(self.PD_SETTLE_DELAY)
        # The next group of tests need DUT to connect in SNK and SRC modes
        pd_dut_utils.set_pd_dualrole('on')

        plankton_failures = []
        logging.info('Start Plankton initiated tests')
        for voltage in self.plankton.get_charging_voltages():
            logging.info('********* %r *********', voltage)
            # Set charging voltage
            self.plankton.charge(voltage)
            # Wait for new PD contract to be established
            time.sleep(self.PD_SETTLE_DELAY)
            # Get current Plankton PD state
            plankton_state = pd_plankton_utils.get_pd_state(0)
            expected_vbus_voltage = self.plankton.charging_voltage
            # If Plankton is sink, then Vbus_exp = 5v
            if plankton_state == pd_plankton_utils.SNK_CONNECT:
                expected_vbus_voltage = self.USBC_SINK_VOLTAGE
            result, result_str = self._compare_vbus(expected_vbus_voltage)
            logging.info('%s, %s', result_str, result)
            if result == 'FAIL':
                plankton_failures.append(result_str)

        if dut_failures:
            logging.error('DUT voltage request failures')
            for fail in dut_failures:
                logging.error('%s', fail)

        if plankton_failures:
            logging.error('Plankton voltage source cap failures')
            for fail in plankton_failures:
                logging.error('%s', fail)

        if dut_failures or plankton_failures:
            if dut_failures and plankton_failures:
                test = 'DUT and Plankton'
                number = len(dut_failures) + len(plankton_failures)
            elif dut_failures:
                test = 'DUT'
                number = len(dut_failures)
            else:
                test = 'Plankton'
                number = len(plankton_failures)
            raise error.TestFail('%s failed %d times' % (test, number))