Added basic vhal property simulator

Support user actions (HVAC) and driving information (speed,
odometer, rpm, gear) simulation

Bug: 65132450
Test: Step 1: ./vhal_prop_simulator -s deviceid --timeout <num_in_seconds>
--gpx <gpxFile> Step 2: Open Kitchen Sink app on device to see if value
changes

Change-Id: I483a59b3fba1f04a5f498b642a3d0038684b3368
diff --git a/tools/emulator/driving_info_generator.py b/tools/emulator/driving_info_generator.py
new file mode 100644
index 0000000..6d30dbd
--- /dev/null
+++ b/tools/emulator/driving_info_generator.py
@@ -0,0 +1,201 @@
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the 'License');
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an 'AS IS' BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import datetime
+import time
+
+from xml.dom import minidom
+
+import vhal_consts_2_0 as c
+
+# interval of generating driving information
+SAMPLE_INTERVAL_SECONDS = 0.5
+
+RPM_LOW = 1000
+RPM_HIGH = 3000
+
+REVERSE_DURATION_SECONDS = 10
+PARK_DURATION_SECONDS = 10
+
+# roughly 5 miles/hour
+REVERSE_SPEED_METERS_PER_SECOND = 2.3
+
+UTC_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
+
+
+def speed2Gear(speed):
+    """
+        Get the current gear based on speed of vehicle. The conversion may not be strictly real but
+        are close enough to a normal vehicle. Assume the vehicle is moving forward.
+    """
+    if speed < 4.4:
+        # 0 - 10 mph
+        return c.VEHICLEGEAR_GEAR_1
+    elif speed < 11.2:
+        # 10 - 25 mph
+        return c.VEHICLEGEAR_GEAR_2
+    elif speed < 20.1:
+        # 25 - 45 mph
+        return c.VEHICLEGEAR_GEAR_3
+    elif speed < 26.8:
+        # 45 - 60 mph
+        return c.VEHICLEGEAR_GEAR_4
+    else:
+        # > 60 mph
+        return c.VEHICLEGEAR_GEAR_5
+
+class GpxFrame(object):
+    """
+        A class representing a track point from GPX file
+    """
+    def __init__(self, trkptDom):
+        timeElements = trkptDom.getElementsByTagName('time')
+        if timeElements:
+            # time value in GPX is in UTC format: YYYY-MM-DDTHH:MM:SS, need to parse it
+            self.datetime = datetime.datetime.strptime(timeElements[0].firstChild.nodeValue,
+                                                          UTC_TIME_FORMAT)
+        speedElements = trkptDom.getElementsByTagName('speed')
+        if speedElements:
+            self.speedInMps = float(speedElements[0].firstChild.nodeValue)
+
+class DrivingInfoGenerator(object):
+    """
+        A class that generates driving information like speed, odometer, rpm, etc. It is taking a
+        GPX file which describes a real route that consists of a sequence of location data. And then
+        derive driving information from those location data.
+
+        Assume it is a car with automatic transmission, so that current gear does not
+        necessarily match selected gear.
+    """
+
+    def __init__(self, gpxFile):
+        self.gpxDom = minidom.parse(gpxFile)
+        # Speed of vehicle (meter / second)
+        self.speedInMps = 0
+        # Fixed RPM with average value during driving
+        self.rpm = RPM_LOW
+        # Odometer (kilometer)
+        self.odometerInKm = 0
+        # Gear selection
+        self.selectedGear = c.VEHICLEGEAR_GEAR_PARK
+        # Current gear
+        self.currentGear = c.VEHICLEGEAR_GEAR_PARK
+        # Timestamp while driving on route defined in GPX file
+        self.datetime = 0
+
+    def _generateFrame(self, listener):
+        """
+            Handle newly generated vehicle property with listener
+        """
+        listener.handle(c.VEHICLEPROPERTY_PERF_VEHICLE_SPEED, 0, self.speedInMps, "PERF_VEHICLE_SPEED")
+        listener.handle(c.VEHICLEPROPERTY_ENGINE_RPM, 0, self.rpm, "ENGINE_RPM")
+        listener.handle(c.VEHICLEPROPERTY_PERF_ODOMETER, 0, self.odometerInKm, "PERF_ODOMETER")
+        listener.handle(c.VEHICLEPROPERTY_CURRENT_GEAR, 0, self.currentGear, "CURRENT_GEAR")
+
+    def _generateFromGpxFrame(self, gpxFrame, listener):
+        """
+            Generate a sequence of vehicle property frames from current track point to the next one.
+            The frequency of frames are pre-defined.
+
+            Some assumptions here:
+                - Two track points are very close to each other (e.g. 1 second driving distance)
+                - It is a straight line between two track point
+                - Speed is changing linearly between two track point
+
+            Given the info:
+                timestamp1 : speed1
+                timestamp2 : speed2
+
+            Vehicle properties in each frame are derived like this:
+                - Speed is calculated based on linear model
+                - Odometer is calculated based on speed and time
+                - RPM will be set to a low value if not accelerating, otherwise set to a high value
+                - Current gear will be set according to speed
+        """
+
+        duration = (gpxFrame.datetime - self.datetime).total_seconds()
+        speedIncrement = (gpxFrame.speedInMps - self.speedInMps) / duration * SAMPLE_INTERVAL_SECONDS
+        self.rpm = RPM_HIGH if speedIncrement > 0 else RPM_LOW
+
+        timeElapsed = 0
+        while timeElapsed < duration:
+            self._generateFrame(listener)
+            if timeElapsed + SAMPLE_INTERVAL_SECONDS < duration:
+                self.odometerInKm += (self.speedInMps + speedIncrement / 2.0) * SAMPLE_INTERVAL_SECONDS / 1000
+                self.speedInMps += speedIncrement
+                time.sleep(SAMPLE_INTERVAL_SECONDS)
+            else:
+                timeLeft = duration - timeElapsed
+                self.odometerInKm += (self.speedInMps + gpxFrame.speedInMps) / 2.0 * timeLeft / 1000
+                self.speedInMps = gpxFrame.speedInMps
+                time.sleep(timeLeft)
+
+            self.currentGear = speed2Gear(self.speedInMps)
+            timeElapsed += SAMPLE_INTERVAL_SECONDS
+
+        self.datetime = gpxFrame.datetime
+
+    def _generateInReverseMode(self, duration, listener):
+        print "Vehicle is reversing"
+        listener.handle(c.VEHICLEPROPERTY_GEAR_SELECTION, 0, c.VEHICLEGEAR_GEAR_REVERSE,
+                        "GEAR_SELECTION")
+        listener.handle(c.VEHICLEPROPERTY_CURRENT_GEAR, 0, c.VEHICLEGEAR_GEAR_REVERSE,
+                        "CURRENT_GEAR")
+        self.rpm = RPM_LOW
+        self.speedInMps = REVERSE_SPEED_METERS_PER_SECOND
+        curTime = 0
+        while curTime < duration:
+            self._generateFrame(listener)
+            self.odometerInKm += self.speedInMps * SAMPLE_INTERVAL_SECONDS / 1000
+            curTime += SAMPLE_INTERVAL_SECONDS
+            time.sleep(SAMPLE_INTERVAL_SECONDS)
+        # After reverse is done, set speed to 0
+        self.speedInMps = .0
+
+    def _generateInParkMode(self, duration, listener):
+        print "Vehicle is parked"
+        listener.handle(c.VEHICLEPROPERTY_GEAR_SELECTION, 0, c.VEHICLEGEAR_GEAR_PARK,
+                        "GEAR_SELECTION")
+        listener.handle(c.VEHICLEPROPERTY_CURRENT_GEAR, 0, c.VEHICLEGEAR_GEAR_PARK,
+                        "CURRENT_GEAR")
+        # Assume in park mode, engine is still on
+        self.rpm = RPM_LOW
+        self.speedInMps = .0
+        curTime = 0
+        while curTime < duration:
+            self._generateFrame(listener)
+            curTime += SAMPLE_INTERVAL_SECONDS
+            time.sleep(SAMPLE_INTERVAL_SECONDS)
+
+    def generate(self, listener):
+        # First, car is parked (probably in garage)
+        self._generateInParkMode(PARK_DURATION_SECONDS, listener)
+        # Second, car will reverse (out of garage)
+        self._generateInReverseMode(REVERSE_DURATION_SECONDS, listener)
+
+        trk = self.gpxDom.getElementsByTagName('trk')[0]
+        trkseg = trk.getElementsByTagName('trkseg')[0]
+        trkpts = trkseg.getElementsByTagName('trkpt')
+
+        print "Vehicle start moving forward"
+        listener.handle(c.VEHICLEPROPERTY_GEAR_SELECTION, 0, c.VEHICLEGEAR_GEAR_DRIVE,
+                        "GEAR_SELECTION")
+
+        firstGpxFrame = GpxFrame(trkpts[0])
+        self.speedInMps = firstGpxFrame.speedInMps
+        self.datetime = firstGpxFrame.datetime
+
+        for i in xrange(1, len(trkpts)):
+            self._generateFromGpxFrame(GpxFrame(trkpts[i]), listener)
diff --git a/tools/emulator/user_action_generator.py b/tools/emulator/user_action_generator.py
new file mode 100644
index 0000000..3d08e17
--- /dev/null
+++ b/tools/emulator/user_action_generator.py
@@ -0,0 +1,219 @@
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the 'License');
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an 'AS IS' BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import time
+
+import vhal_consts_2_0 as c
+
+# A list of user actions as a subset of VHAL properties to simulate
+userActions = [
+    c.VEHICLEPROPERTY_HVAC_POWER_ON,
+    c.VEHICLEPROPERTY_HVAC_TEMPERATURE_SET,
+    c.VEHICLEPROPERTY_HVAC_FAN_SPEED,
+    c.VEHICLEPROPERTY_HVAC_FAN_DIRECTION,
+    c.VEHICLEPROPERTY_HVAC_AUTO_ON,
+    c.VEHICLEPROPERTY_HVAC_RECIRC_ON,
+    c.VEHICLEPROPERTY_HVAC_AC_ON,
+    c.VEHICLEPROPERTY_HVAC_DEFROSTER
+]
+
+propDesc = {
+    c.VEHICLEPROPERTY_HVAC_POWER_ON: 'HVAC_POWER_ON',
+    c.VEHICLEPROPERTY_HVAC_TEMPERATURE_SET: 'HVAC_TEMPERATURE_SET',
+    c.VEHICLEPROPERTY_HVAC_FAN_SPEED: 'HVAC_FAN_SPEED',
+    c.VEHICLEPROPERTY_HVAC_FAN_DIRECTION: 'HVAC_FAN_DIRECTION',
+    c.VEHICLEPROPERTY_HVAC_AUTO_ON: 'HVAC_AUTO_ON',
+    c.VEHICLEPROPERTY_HVAC_RECIRC_ON: 'HVAC_AUTO_RECIRC_ON',
+    c.VEHICLEPROPERTY_HVAC_AC_ON: 'HVAC_AC_ON',
+    c.VEHICLEPROPERTY_HVAC_DEFROSTER: 'HVAC_DEFROSTER'
+}
+
+SHORT_SLEEP_TIME_SEC = 2
+LONG_SLEEP_TIME_SEC = 30
+
+MIN_TEMPERATURE_C = 22
+MAX_TEMPERATURE_C = 28
+
+MIN_FAN_SPEED = 1
+MAX_FAN_SPEED = 5
+
+class ActionPropConfig(object):
+    """
+        A configuration class that parse the vhal property config message and hold the
+        configuration values
+    """
+    def __init__(self, vhalConfig):
+        self.supportedAreas = self._getSupportedAreas(vhalConfig)
+        self.areaConfigs = self._getAreaConfigs(vhalConfig, self.supportedAreas,
+                                                vhalConfig.value_type)
+
+    def _getSupportedAreas(self, vhalConfig):
+        supportedAreas = []
+        areas = vhalConfig.supported_areas
+        while areas != 0:
+            area = areas & (areas - 1)
+            area ^= areas
+            areas &= (areas - 1)
+            supportedAreas.append(area)
+
+        return supportedAreas
+
+    def _getAreaConfigs(self, vhalConfig, supportedAreas, valueType):
+        areaConfigs = {}
+        configs = vhalConfig.area_configs
+        if not configs:
+            return None
+
+        if len(configs) == 1:
+            for area in supportedAreas:
+                areaConfigs[area] = AreaConfig(configs[0], valueType)
+        else:
+            for config in configs:
+                areaConfigs[config.area_id] = AreaConfig(config, valueType)
+
+        return areaConfigs
+
+class AreaConfig(object):
+    """
+        A configuration class is representing an area config of a vhal property.
+    """
+    def __init__(self, vhalAreaConfig, valueType):
+        """
+            The class is initialized by parsing the vhal area config object
+        """
+        if valueType == c.VEHICLEPROPERTYTYPE_INT32:
+            self.min = vhalAreaConfig.min_int32_value
+            self.max = vhalAreaConfig.max_int32_value
+        elif valueType == c.VEHICLEPROPERTYTYPE_INT64:
+            self.min = vhalAreaConfig.min_int64_value
+            self.max = vhalAreaConfig.max_int64_value
+        elif valueType == c.VEHICLEPROPERTYTYPE_FLOAT:
+            self.min = vhalAreaConfig.min_float_value
+            self.max = vhalAreaConfig.max_float_value
+        else:
+            self.min = None
+            self.max = None
+
+class UserActionGenerator(object):
+    """
+        A class generate user action related vhal properties in a deterministic algorithm based on
+        pre-fetched vhal configuration
+    """
+    def __init__(self, vhal):
+        self.configs = self._getConfig(vhal)
+
+
+    def _getConfig(self, vhal):
+        """
+            Get vhal configuration for properties that need to be simulated
+        """
+        vhalConfig = {}
+        for actionProp in userActions:
+            vhal.getConfig(actionProp)
+            vhalConfig[actionProp] = ActionPropConfig(vhal.rxMsg().config[0])
+        return vhalConfig
+
+    def _adjustContinuousProperty(self, prop, begin, end, listener):
+        """
+            The method generate continuous property value from "begin" value to "end" value
+            (exclusive).
+        """
+        config = self.configs[prop]
+        for area in config.supportedAreas:
+            areaConfig = config.areaConfigs[area]
+            if begin < end:
+                begin = areaConfig.min if begin < areaConfig.min else begin
+                end = areaConfig.max if end > areaConfig.max else end
+            else:
+                begin = areaConfig.max if begin > areaConfig.max else begin
+                end = areaConfig.min if end < areaConfig.min else end
+
+            for value in self._range(begin, end):
+                listener.handle(prop, area, value, propDesc[prop])
+                time.sleep(0.2)
+
+    def _setProperty(self, prop, value, listener):
+        """
+            This method generates single property value (e.g. boolean or integer value)
+        """
+        config = self.configs[prop]
+        for area in config.supportedAreas:
+            listener.handle(prop, area, value, propDesc[prop])
+            time.sleep(1)
+
+    def _range(self, begin, end):
+        if begin < end:
+            i = begin
+            while i < end:
+                yield i
+                i += 1
+        else:
+            i = begin
+            while i > end:
+                yield i
+                i += -1
+
+    def generate(self, listener):
+        """
+            Main method that simulate user in-car actions such as HVAC
+        """
+        listener.handle(c.VEHICLEPROPERTY_HVAC_POWER_ON, c.VEHICLEAREAZONE_ROW_1,
+                        1, 'HVAC_POWER_ON')
+        time.sleep(SHORT_SLEEP_TIME_SEC)
+
+        while True:
+
+            self._setProperty(c.VEHICLEPROPERTY_HVAC_AC_ON, 1, listener)
+            time.sleep(SHORT_SLEEP_TIME_SEC)
+
+            self._setProperty(c.VEHICLEPROPERTY_HVAC_FAN_DIRECTION,
+                              c.VEHICLEHVACFANDIRECTION_FACE, listener)
+            time.sleep(SHORT_SLEEP_TIME_SEC)
+
+            self._adjustContinuousProperty(c.VEHICLEPROPERTY_HVAC_TEMPERATURE_SET,
+                                           MAX_TEMPERATURE_C, MIN_TEMPERATURE_C, listener)
+            time.sleep(SHORT_SLEEP_TIME_SEC)
+
+            self._adjustContinuousProperty(c.VEHICLEPROPERTY_HVAC_FAN_SPEED,
+                                           MIN_FAN_SPEED, MAX_FAN_SPEED, listener)
+            time.sleep(LONG_SLEEP_TIME_SEC)
+
+            self._setProperty(c.VEHICLEPROPERTY_HVAC_AUTO_ON, 1, listener)
+            time.sleep(LONG_SLEEP_TIME_SEC)
+
+            self._setProperty(c.VEHICLEPROPERTY_HVAC_AUTO_ON, 0, listener)
+            time.sleep(SHORT_SLEEP_TIME_SEC)
+
+            self._setProperty(c.VEHICLEPROPERTY_HVAC_FAN_DIRECTION,
+                              c.VEHICLEHVACFANDIRECTION_FACE_AND_FLOOR, listener)
+            time.sleep(LONG_SLEEP_TIME_SEC)
+
+            self._setProperty(c.VEHICLEPROPERTY_HVAC_DEFROSTER, 1, listener)
+            time.sleep(SHORT_SLEEP_TIME_SEC)
+
+            self._setProperty(c.VEHICLEPROPERTY_HVAC_FAN_DIRECTION,
+                              c.VEHICLEHVACFANDIRECTION_DEFROST, listener)
+            time.sleep(SHORT_SLEEP_TIME_SEC)
+
+            self._adjustContinuousProperty(c.VEHICLEPROPERTY_HVAC_TEMPERATURE_SET,
+                                           MIN_TEMPERATURE_C, MAX_TEMPERATURE_C, listener)
+            time.sleep(SHORT_SLEEP_TIME_SEC)
+
+            self._adjustContinuousProperty(c.VEHICLEPROPERTY_HVAC_FAN_SPEED,
+                                           MAX_FAN_SPEED, MIN_FAN_SPEED, listener)
+            time.sleep(LONG_SLEEP_TIME_SEC)
+
+            self._setProperty(c.VEHICLEPROPERTY_HVAC_AC_ON, 0, listener)
+            time.sleep(SHORT_SLEEP_TIME_SEC)
diff --git a/tools/emulator/vhal_prop_simulator.py b/tools/emulator/vhal_prop_simulator.py
new file mode 100755
index 0000000..4b1b1a8
--- /dev/null
+++ b/tools/emulator/vhal_prop_simulator.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import argparse
+import time
+
+from threading import Thread
+
+import driving_info_generator
+import user_action_generator
+import vhal_consts_2_0 as c
+
+from vhal_emulator import Vhal
+
+DEFAULT_TIMEOUT_SEC = 60 * 60 # 1 hour
+
+class VhalPropSimulator(object):
+    """
+        A class simulates vhal properties by calling each generator in a separate thread. It is
+        itself a listener passed to each generator to handle vhal event
+    """
+
+    def __init__(self, device, gpxFile,):
+        self.vhal = Vhal(c.vhal_types_2_0, device)
+        self.gpxFile = gpxFile
+
+    def handle(self, prop, area_id, value, desc=None):
+        """
+            handle generated VHAL property by injecting through vhal emulator.
+        """
+        print "Generated property %s with value: %s" % (desc, value)
+        self.vhal.setProperty(prop, area_id, value)
+
+    def _startGeneratorThread(self, generator):
+        thread = Thread(target=generator.generate, args=(self,))
+        thread.daemon = True
+        thread.start()
+
+    def run(self, timeout):
+        self._startGeneratorThread(user_action_generator.UserActionGenerator(self.vhal))
+        self._startGeneratorThread(driving_info_generator.DrivingInfoGenerator(self.gpxFile))
+        time.sleep(float(timeout))
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(description='Vhal Property Simulator')
+    parser.add_argument('-s', action='store', dest='deviceid', default=None)
+    parser.add_argument('--timeout', action='store', dest='timeout', default=DEFAULT_TIMEOUT_SEC)
+    parser.add_argument('--gpx', action='store', dest='gpxFile', default=None)
+    args = parser.parse_args()
+
+    simulator = VhalPropSimulator(device=args.deviceid, gpxFile=args.gpxFile)
+    simulator.run(args.timeout)