callbox: Add ability to configure tx/rx power

This adds APIs to the CallboxManager to configure uplink/downlink power
independently i.e. without reconfiguring the entire callbox. It also
adds support for setting the callbox downlink power using a float as
opposed to only using the predefined values.

The predefined Rx power levels are updated to use the default ChromeOS
RSRP values defined in cellular_capability_3gpp.cc rather than the
Android values that it was previously using.

BUG=b:247788033
TEST=build_dockerimage $chroot_path $sysroot_path --service cros-callbox --build_type local

Change-Id: I061a6301edeadf752e6bb69aeec558575c0f42b8
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/3924035
Reviewed-by: Nagi Marupaka <nmarupaka@google.com>
Tested-by: Jason Stanko <jstanko@google.com>
Commit-Queue: Jason Stanko <jstanko@google.com>
Reviewed-by: Madhav <madhavadas@google.com>
diff --git a/src/chromiumos/test/callbox/docker/cellular/callbox_utils/cmw500_cellular_simulator.py b/src/chromiumos/test/callbox/docker/cellular/callbox_utils/cmw500_cellular_simulator.py
index c71927e..69fd2ec 100644
--- a/src/chromiumos/test/callbox/docker/cellular/callbox_utils/cmw500_cellular_simulator.py
+++ b/src/chromiumos/test/callbox/docker/cellular/callbox_utils/cmw500_cellular_simulator.py
@@ -2,13 +2,17 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import time
-import logging
+# TODO(b/254347891): unify formatting and ignore specific lints in callbox libraries
+# pylint: skip-file
 
-from .. import cellular_simulator as cc
+import logging
+import time
+
 from . import cmw500
+from .. import cellular_simulator as cc
 from ..simulation_utils import LteSimulation
 
+
 CMW_TM_MAPPING = {
         LteSimulation.TransmissionMode.TM1: cmw500.TransmissionModes.TM1,
         LteSimulation.TransmissionMode.TM2: cmw500.TransmissionModes.TM2,
@@ -256,7 +260,8 @@
             self.log.warning('Open loop supports-50dBm to 23 dBm. '
                              'Setting it to max power 23 dBm')
             input_power = 23
-        bts.uplink_power_control = input_power
+        # open loop power only supports integers
+        bts.uplink_power_control = round(input_power)
         bts.tpc_power_control = cmw500.TpcPowerControl.CLOSED_LOOP
         bts.tpc_closed_loop_target_power = input_power
 
@@ -599,4 +604,3 @@
         self.cmw.wait_for_attached_state()
         self.cmw.set_sms(sms_message)
         self.cmw.send_sms()
-
diff --git a/src/chromiumos/test/callbox/docker/cellular/cellular_simulator.py b/src/chromiumos/test/callbox/docker/cellular/cellular_simulator.py
index f6cdc5d..e0aefe1 100644
--- a/src/chromiumos/test/callbox/docker/cellular/cellular_simulator.py
+++ b/src/chromiumos/test/callbox/docker/cellular/cellular_simulator.py
@@ -2,6 +2,9 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+# TODO(b/254347891): unify formatting and ignore specific lints in callbox libraries
+# pylint: skip-file
+
 from . import simulation_utils as sims
 
 
@@ -71,10 +74,10 @@
             bts_index: the base station number.
         """
 
-        if config.output_power:
+        if config.output_power is not None:
             self.set_output_power(bts_index, config.output_power)
 
-        if config.input_power:
+        if config.input_power is not None:
             self.set_input_power(bts_index, config.input_power)
 
         if isinstance(config, sims.LteSimulation.LteSimulation.BtsConfig):
diff --git a/src/chromiumos/test/callbox/docker/cellular/proxyserver/flask_app.py b/src/chromiumos/test/callbox/docker/cellular/proxyserver/flask_app.py
index 1bbd042..68f9ce1 100644
--- a/src/chromiumos/test/callbox/docker/cellular/proxyserver/flask_app.py
+++ b/src/chromiumos/test/callbox/docker/cellular/proxyserver/flask_app.py
@@ -6,7 +6,7 @@
 
 # disable some lints to stay consistent with ACTS formatting
 # pylint: disable=bad-indentation, banned-string-format-function
-# pylint: disable=docstring-trailing-quotes, docstring-section-indent, bad-continuation
+# pylint: disable=docstring-trailing-quotes, docstring-section-indent
 
 import traceback
 import urllib
@@ -60,15 +60,41 @@
                     }, None)
 
         config.parameter_list = data['parameter_list']
+        config.simulation.parse_parameters(config.parameter_list)
         return 'OK'
 
     def begin_simulation(self, data):
         self._require_dict_keys(data, 'callbox')
         config = self._get_callbox_config(data['callbox'])
-        config.simulation.parse_parameters(config.parameter_list)
         config.simulation.start()
         return 'OK'
 
+    def set_uplink_tx_power(self, data):
+        self._require_dict_keys(data, 'callbox', LteSimulation.LteSimulation.PARAM_UL_PW)
+        config = self._get_callbox_config(data['callbox'])
+        parameters = [LteSimulation.LteSimulation.PARAM_UL_PW, data[LteSimulation.LteSimulation.PARAM_UL_PW]]
+        power = config.simulation.get_uplink_power_from_parameters(parameters)
+        config.simulation.set_uplink_tx_power(power)
+        return 'OK'
+
+    def set_downlink_rx_power(self, data):
+        self._require_dict_keys(data, 'callbox', LteSimulation.LteSimulation.PARAM_DL_PW)
+        config = self._get_callbox_config(data['callbox'])
+        parameters = [LteSimulation.LteSimulation.PARAM_DL_PW, data[LteSimulation.LteSimulation.PARAM_DL_PW]]
+        power = config.simulation.get_downlink_power_from_parameters(parameters)
+        config.simulation.set_downlink_rx_power(power)
+        return 'OK'
+
+    def query_uplink_tx_power(self, data):
+        self._require_dict_keys(data, 'callbox')
+        config = self._get_callbox_config(data['callbox'])
+        return { LteSimulation.LteSimulation.PARAM_UL_PW : config.simulation.get_uplink_tx_power()}
+
+    def query_downlink_rx_power(self, data):
+        self._require_dict_keys(data, 'callbox')
+        config = self._get_callbox_config(data['callbox'])
+        return { LteSimulation.LteSimulation.PARAM_DL_PW : config.simulation.get_downlink_rx_power()}
+
     def query_throughput(self, data):
         self._require_dict_keys(data, 'callbox')
         config = self._get_callbox_config(data['callbox'])
@@ -144,6 +170,10 @@
 
 path_lookup = {
         'config': callbox_manager.configure_callbox,
+        'config/power/downlink' : callbox_manager.set_downlink_rx_power,
+        'config/power/uplink' : callbox_manager.set_uplink_tx_power,
+        'config/fetch/power/downlink' : callbox_manager.query_downlink_rx_power,
+        'config/fetch/power/uplink' : callbox_manager.query_uplink_tx_power,
         'config/fetch/maxthroughput' : callbox_manager.query_throughput,
         'start': callbox_manager.begin_simulation,
         'sms': callbox_manager.send_sms,
diff --git a/src/chromiumos/test/callbox/docker/cellular/simulation_utils/BaseSimulation.py b/src/chromiumos/test/callbox/docker/cellular/simulation_utils/BaseSimulation.py
index 4fd41f6..0044c7f 100644
--- a/src/chromiumos/test/callbox/docker/cellular/simulation_utils/BaseSimulation.py
+++ b/src/chromiumos/test/callbox/docker/cellular/simulation_utils/BaseSimulation.py
@@ -2,10 +2,15 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import numpy as np
-import time
-from .. import cellular_simulator
+# TODO(b/254347891): unify formatting and ignore specific lints in callbox libraries
+# pylint: skip-file
+
 from enum import Enum
+import time
+
+import numpy as np
+
+from .. import cellular_simulator
 
 
 class BaseSimulation(object):
@@ -78,7 +83,7 @@
             values with the new ones for all the parameters different to None.
             """
             for attr, value in vars(new_config).items():
-                if value:
+                if value is not None:
                     setattr(self, attr, value)
 
     def __init__(self, simulator, log, dut, test_config, calibration_table):
@@ -387,6 +392,22 @@
         self.simulator.configure_bts(new_config)
         self.primary_config.incorporate(new_config)
 
+    def get_uplink_tx_power(self):
+        """ Returns the uplink tx power level
+
+        Returns:
+            calibrated tx power in dBm
+        """
+        return self.primary_config.input_power
+
+    def get_downlink_rx_power(self):
+        """ Returns the downlink tx power level
+
+        Returns:
+            calibrated rx power in dBm
+        """
+        return self.primary_config.output_power
+
     def get_uplink_power_from_parameters(self, parameters):
         """ Reads uplink power from a list of parameters. """
 
@@ -396,14 +417,11 @@
             if values[1] in self.UPLINK_SIGNAL_LEVEL_DICTIONARY:
                 return self.UPLINK_SIGNAL_LEVEL_DICTIONARY[values[1]]
             else:
-                try:
-                    if values[1][0] == 'n':
-                        # Treat the 'n' character as a negative sign
-                        return -int(values[1][1:])
-                    else:
-                        return int(values[1])
-                except ValueError:
-                    pass
+                if values[1][0] == 'n':
+                    # Treat the 'n' character as a negative sign
+                    return -float(values[1][1:])
+                else:
+                    return float(values[1])
 
         # If the method got to this point it is because PARAM_UL_PW was not
         # included in the test parameters or the provided value was invalid.
@@ -421,11 +439,15 @@
         values = self.consume_parameter(parameters, self.PARAM_DL_PW, 1)
 
         if values:
-            if values[1] not in self.DOWNLINK_SIGNAL_LEVEL_DICTIONARY:
-                raise ValueError("Invalid signal level value {}.".format(
-                        values[1]))
-            else:
+            if values[1] in self.DOWNLINK_SIGNAL_LEVEL_DICTIONARY:
                 return self.DOWNLINK_SIGNAL_LEVEL_DICTIONARY[values[1]]
+            else:
+                if values[1][0] == 'n':
+                    # Treat the 'n' character as a negative sign
+                    return -float(values[1][1:])
+                else:
+                    return float(values[1])
+
         else:
             # Use default value
             power = self.DOWNLINK_SIGNAL_LEVEL_DICTIONARY['excellent']
@@ -458,7 +480,7 @@
         # Try to use measured path loss value. If this was not set, it will
         # throw an TypeError exception
         try:
-            calibrated_power = round(power + self.dl_path_loss)
+            calibrated_power = round(power + self.dl_path_loss, 1)
             if calibrated_power > self.simulator.MAX_DL_POWER:
                 self.log.warning(
                         "Cannot achieve phone DL Rx power of {} dBm. Requested TX "
@@ -481,7 +503,7 @@
         except TypeError:
             self.log.info("Phone downlink received power set to {} (link is "
                           "uncalibrated).".format(round(power)))
-            return round(power)
+            return round(power, 1)
 
     def calibrated_uplink_tx_power(self, bts_config, signal_level):
         """ Calculates the power level at the instrument's input in order to
@@ -507,7 +529,7 @@
         # Try to use measured path loss value. If this was not set, it will
         # throw an TypeError exception
         try:
-            calibrated_power = round(power - self.ul_path_loss)
+            calibrated_power = round(power - self.ul_path_loss, 1)
             if calibrated_power < self.UL_MIN_POWER:
                 self.log.warning(
                         "Cannot achieve phone UL Tx power of {} dBm. Requested UL "
@@ -530,7 +552,7 @@
         except TypeError:
             self.log.info("Phone uplink transmitted power set to {} (link is "
                           "uncalibrated).".format(round(power)))
-            return round(power)
+            return round(power, 1)
 
     def calibrate(self, band):
         """ Calculates UL and DL path loss if it wasn't done before.
diff --git a/src/chromiumos/test/callbox/docker/cellular/simulation_utils/LteSimulation.py b/src/chromiumos/test/callbox/docker/cellular/simulation_utils/LteSimulation.py
index 36df63b..d877237 100644
--- a/src/chromiumos/test/callbox/docker/cellular/simulation_utils/LteSimulation.py
+++ b/src/chromiumos/test/callbox/docker/cellular/simulation_utils/LteSimulation.py
@@ -2,12 +2,15 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+# TODO(b/254347891): unify formatting and ignore specific lints in callbox libraries
+# pylint: skip-file
+
+from enum import Enum
 import math
 import time
-from enum import Enum
 
-from .BaseSimulation import BaseSimulation
 from . import BaseCellularDut
+from .BaseSimulation import BaseSimulation
 
 
 class TransmissionMode(Enum):
@@ -80,13 +83,12 @@
     # Units in which signal level is defined in DOWNLINK_SIGNAL_LEVEL_DICTIONARY
     DOWNLINK_SIGNAL_LEVEL_UNITS = "RSRP"
 
-    # RSRP signal levels thresholds (as reported by Android) in dBm/15KHz.
-    # Excellent is set to -75 since callbox B Tx power is limited to -30 dBm
+    # RSRP signal levels thresholds taken from cellular_capability_3gpp.cc
     DOWNLINK_SIGNAL_LEVEL_DICTIONARY = {
-            'excellent': -75,
-            'high': -110,
-            'medium': -115,
-            'weak': -120,
+            'excellent': -88,
+            'high': -98,
+            'medium': -108,
+            'weak': -118,
             'disconnected': -170
     }
 
@@ -829,25 +831,6 @@
         # Now that the band is set, calibrate the link if necessary
         self.load_pathloss_if_required()
 
-    def calibrated_downlink_rx_power(self, bts_config, rsrp):
-        """ LTE simulation overrides this method so that it can convert from
-        RSRP to total signal power transmitted from the basestation.
-
-        Args:
-            bts_config: the current configuration at the base station
-            rsrp: desired rsrp, contained in a key value pair
-        """
-
-        power = self.rsrp_to_signal_power(rsrp, bts_config)
-
-        self.log.info(
-                "Setting downlink signal level to {} RSRP ({} dBm)".format(
-                        rsrp, power))
-
-        # Use parent method to calculate signal level
-        return super(LteSimulation,
-                     self).calibrated_downlink_rx_power(bts_config, power)
-
     def downlink_calibration(self, rat=None, power_units_conversion_func=None):
         """ Computes downlink path loss and returns the calibration value.
 
@@ -864,8 +847,7 @@
         """
 
         return super().downlink_calibration(
-                rat='lteDbm',
-                power_units_conversion_func=self.rsrp_to_signal_power)
+                rat='lteDbm')
 
     def rsrp_to_signal_power(self, rsrp, bts_config):
         """ Converts rsrp to total band signal power