1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright 2016 Google Inc.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#   http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18
19"""
20    This module provides a vhal class which sends and receives messages to the vehicle HAL module
21    on an Android Auto device.  It uses port forwarding via ADB to communicate with the Android
22    device.
23
24    Example Usage:
25
26        import vhal_consts_2_0 as c
27        from vhal_emulator import Vhal
28
29        # Create an instance of vhal class.  Need to pass the vhal_types constants.
30        v = Vhal(c.vhal_types_2_0)
31
32        # Get the property config (if desired)
33        v.getConfig(c.VEHICLEPROPERTY_HVAC_TEMPERATURE_SET)
34
35        # Get the response message to getConfig()
36        reply = v.rxMsg()
37        print(reply)
38
39        # Set left temperature to 70 degrees
40        v.setProperty(c.VEHICLEPROPERTY_HVAC_TEMPERATURE_SET, c.VEHICLEAREAZONE_ROW_1_LEFT, 70)
41
42        # Get the response message to setProperty()
43        reply = v.rxMsg()
44        print(reply)
45
46        # Get the left temperature value
47        v.getProperty(c.VEHICLEPROPERTY_HVAC_TEMPERATURE_SET, c.VEHICLEAREAZONE_ROW_1_LEFT)
48
49        # Get the response message to getProperty()
50        reply = v.rxMsg()
51        print(reply)
52
53    NOTE:  The rxMsg() is a blocking call, so it may be desirable to set up a separate RX thread
54            to handle any asynchronous messages coming from the device.
55
56    Example for creating RX thread (assumes vhal has already been instantiated):
57
58        from threading import Thread
59
60        # Define a simple thread that receives messages from a vhal object (v) and prints them
61        def rxThread(v):
62            while(1):
63                print v.rxMsg()
64
65        rx = Thread(target=rxThread, args=(v,))
66        rx.start()
67
68    Protocol Buffer:
69        This module relies on VehicleHalProto_pb2.py being in sync with the protobuf in the VHAL.
70        If the VehicleHalProto.proto file has changed, re-generate the python version using:
71
72            protoc -I=<proto_dir> --python_out=<out_dir> <proto_dir>/VehicleHalProto.proto
73"""
74
75from __future__ import print_function
76
77# Suppress .pyc files
78import sys
79sys.dont_write_bytecode = True
80
81import socket
82import struct
83import subprocess
84
85# Generate the protobuf file from hardware/interfaces/automotive/vehicle/2.0/default/impl/vhal_v2_0
86# It is recommended to use the protoc provided in: prebuilts/tools/common/m2/repository/com/google/protobuf/protoc/3.0.0
87# or a later version, in order to provide Python 3 compatibility
88#   protoc -I=proto --python_out=proto proto/VehicleHalProto.proto
89import VehicleHalProto_pb2
90
91# If container is a dictionary, retrieve the value for key item;
92# Otherwise, get the attribute named item out of container
93def getByAttributeOrKey(container, item, default=None):
94    if isinstance(container, dict):
95        try:
96            return container[item]
97        except KeyError as e:
98            return default
99    try:
100        return getattr(container, item)
101    except AttributeError as e:
102        return default
103
104class Vhal:
105    """
106        Dictionary of prop_id to value_type.  Used by setProperty() to properly format data.
107    """
108    _propToType = {}
109
110    ### Private Functions
111    def _txCmd(self, cmd):
112        """
113            Transmits a protobuf to Android Auto device.  Should not be called externally.
114        """
115        # Serialize the protobuf into a string
116        msgStr = cmd.SerializeToString()
117        msgLen = len(msgStr)
118        # Convert the message length into int32 byte array
119        msgHdr = struct.pack('!I', msgLen)
120        # Send the message length first
121        self.sock.sendall(msgHdr)
122        # Then send the protobuf
123        self.sock.sendall(msgStr)
124
125    ### Public Functions
126    def printHex(self, data):
127        """
128            For debugging, print the protobuf message string in hex.
129        """
130        print("len = ", len(data), "str = ", ":".join("{:02x}".format(ord(d)) for d in data))
131
132    def openSocket(self, device=None):
133        """
134            Connects to an Android Auto device running a Vehicle HAL with simulator.
135        """
136        # Hard-coded socket port needs to match the one in DefaultVehicleHal
137        remotePortNumber = 33452
138        extraArgs = '' if device is None else '-s %s' % device
139        adbCmd = 'adb %s forward tcp:0 tcp:%d' % (extraArgs, remotePortNumber)
140        adbResp = subprocess.check_output(adbCmd, shell=True)[0:-1]
141        localPortNumber = int(adbResp)
142        print('Connecting local port %s to remote port %s on %s' % (
143            localPortNumber, remotePortNumber,
144            'default device' if device is None else 'device %s' % device))
145        # Open the socket and connect
146        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
147        self.sock.connect(('localhost', localPortNumber))
148
149    def rxMsg(self):
150        """
151            Receive a message over the socket.  This function blocks if a message is not available.
152              May want to wrap this function inside of an rx thread to also collect asynchronous
153              messages generated by the device.
154        """
155        # Receive the message length (int32) first
156        b = self.sock.recv(4)
157        if (len(b) == 4):
158            msgLen, = struct.unpack('!I', b)
159            if (msgLen > 0):
160                # Receive the actual message
161                b = self.sock.recv(msgLen)
162                if (len(b) == msgLen):
163                    # Unpack the protobuf
164                    msg = VehicleHalProto_pb2.EmulatorMessage()
165                    msg.ParseFromString(b)
166                    return msg
167                else:
168                    print("Ignored message fragment")
169
170    def getConfig(self, prop):
171        """
172            Sends a getConfig message for the specified property.
173        """
174        cmd = VehicleHalProto_pb2.EmulatorMessage()
175        cmd.msg_type = VehicleHalProto_pb2.GET_CONFIG_CMD
176        propGet = cmd.prop.add()
177        propGet.prop = prop
178        self._txCmd(cmd)
179
180    def getConfigAll(self):
181        """
182            Sends a getConfigAll message to the host.  This will return all configs available.
183        """
184        cmd = VehicleHalProto_pb2.EmulatorMessage()
185        cmd.msg_type = VehicleHalProto_pb2.GET_CONFIG_ALL_CMD
186        self._txCmd(cmd)
187
188    def getProperty(self, prop, area_id):
189        """
190            Sends a getProperty command for the specified property ID and area ID.
191        """
192        cmd = VehicleHalProto_pb2.EmulatorMessage()
193        cmd.msg_type = VehicleHalProto_pb2.GET_PROPERTY_CMD
194        propGet = cmd.prop.add()
195        propGet.prop = prop
196        propGet.area_id = area_id
197        self._txCmd(cmd)
198
199    def getPropertyAll(self):
200        """
201            Sends a getPropertyAll message to the host.  This will return all properties available.
202        """
203        cmd = VehicleHalProto_pb2.EmulatorMessage()
204        cmd.msg_type = VehicleHalProto_pb2.GET_PROPERTY_ALL_CMD
205        self._txCmd(cmd)
206
207    def setProperty(self, prop, area_id, value, status=VehicleHalProto_pb2.AVAILABLE):
208        """
209            Sends a setProperty command for the specified property ID, area ID, value and status.
210              If Status is not specified, automatically send AVAILABLE as the default.
211              This function chooses the proper value field to populate based on the config for the
212              property.  It is the caller's responsibility to ensure the value data is the proper
213              type.
214        """
215        cmd = VehicleHalProto_pb2.EmulatorMessage()
216        cmd.msg_type = VehicleHalProto_pb2.SET_PROPERTY_CMD
217        propValue = cmd.value.add()
218        propValue.prop = prop
219        # Insert value into the proper area
220        propValue.area_id = area_id
221        propValue.status = status;
222        # Determine the value_type and populate the correct value field in protoBuf
223        try:
224            valType = self._propToType[prop]
225        except KeyError:
226            raise ValueError('propId is invalid:', prop)
227            return
228        propValue.value_type = valType
229        if valType in self._types.TYPE_STRING:
230            propValue.string_value = value
231        elif valType in self._types.TYPE_BYTES:
232            propValue.bytes_value = value
233        elif valType in self._types.TYPE_INT32:
234            propValue.int32_values.append(value)
235        elif valType in self._types.TYPE_INT64:
236            propValue.int64_values.append(value)
237        elif valType in self._types.TYPE_FLOAT:
238            propValue.float_values.append(value)
239        elif valType in self._types.TYPE_INT32S:
240            propValue.int32_values.extend(value)
241        elif valType in self._types.TYPE_FLOATS:
242            propValue.float_values.extend(value)
243        elif valType in self._types.TYPE_MIXED:
244            propValue.string_value = \
245                getByAttributeOrKey(value, 'string_value', '')
246            propValue.bytes_value = \
247                getByAttributeOrKey(value, 'bytes_value', '')
248            for newValue in getByAttributeOrKey(value, 'int32_values', []):
249                propValue.int32_values.append(newValue)
250            for newValue in getByAttributeOrKey(value, 'int64_values', []):
251                propValue.int64_values.append(newValue)
252            for newValue in getByAttributeOrKey(value, 'float_values', []):
253                propValue.float_values.append(newValue)
254        else:
255            raise ValueError('value type not recognized:', valType)
256            return
257        self._txCmd(cmd)
258
259    def __init__(self, types, device=None):
260        # Save the list of types constants
261        self._types = types
262        # Open the socket
263        self.openSocket(device)
264        # Get the list of configs
265        self.getConfigAll()
266        msg = self.rxMsg()
267        # Parse the list of configs to generate a dictionary of prop_id to type
268        for cfg in msg.config:
269            self._propToType[cfg.prop] = cfg.value_type
270