Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # -*- coding: utf-8 -*- |
| 3 | # |
Eric Jeong | dfe5b86 | 2021-11-11 11:39:33 -0800 | [diff] [blame] | 4 | # Copyright 2016 The Android Open Source Project |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 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 |
Scott Randolph | 4ce965a | 2017-05-08 15:52:34 -0700 | [diff] [blame] | 21 | on an Android Auto device. It uses port forwarding via ADB to communicate with the Android |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 22 | device. |
| 23 | |
| 24 | Example Usage: |
| 25 | |
Enrico Granata | 5e06712 | 2017-03-29 12:05:30 -0700 | [diff] [blame] | 26 | import vhal_consts_2_0 as c |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 27 | from vhal_emulator import Vhal |
| 28 | |
| 29 | # Create an instance of vhal class. Need to pass the vhal_types constants. |
Enrico Granata | 5e06712 | 2017-03-29 12:05:30 -0700 | [diff] [blame] | 30 | v = Vhal(c.vhal_types_2_0) |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 31 | |
| 32 | # Get the property config (if desired) |
Scott Randolph | 4ce965a | 2017-05-08 15:52:34 -0700 | [diff] [blame] | 33 | v.getConfig(c.VEHICLEPROPERTY_HVAC_TEMPERATURE_SET) |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 34 | |
| 35 | # Get the response message to getConfig() |
| 36 | reply = v.rxMsg() |
Enrico Granata | 66d5a97 | 2017-04-05 16:48:08 -0700 | [diff] [blame] | 37 | print(reply) |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 38 | |
| 39 | # Set left temperature to 70 degrees |
Scott Randolph | 4ce965a | 2017-05-08 15:52:34 -0700 | [diff] [blame] | 40 | v.setProperty(c.VEHICLEPROPERTY_HVAC_TEMPERATURE_SET, c.VEHICLEAREAZONE_ROW_1_LEFT, 70) |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 41 | |
| 42 | # Get the response message to setProperty() |
| 43 | reply = v.rxMsg() |
Enrico Granata | 66d5a97 | 2017-04-05 16:48:08 -0700 | [diff] [blame] | 44 | print(reply) |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 45 | |
| 46 | # Get the left temperature value |
Scott Randolph | 4ce965a | 2017-05-08 15:52:34 -0700 | [diff] [blame] | 47 | v.getProperty(c.VEHICLEPROPERTY_HVAC_TEMPERATURE_SET, c.VEHICLEAREAZONE_ROW_1_LEFT) |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 48 | |
| 49 | # Get the response message to getProperty() |
| 50 | reply = v.rxMsg() |
Enrico Granata | 66d5a97 | 2017-04-05 16:48:08 -0700 | [diff] [blame] | 51 | print(reply) |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 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 | |
Scott Randolph | 18baf5f | 2017-06-06 15:54:27 -0700 | [diff] [blame] | 60 | # Define a simple thread that receives messages from a vhal object (v) and prints them |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 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 | |
Enrico Granata | 66d5a97 | 2017-04-05 16:48:08 -0700 | [diff] [blame] | 75 | from __future__ import print_function |
| 76 | |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 77 | # Suppress .pyc files |
| 78 | import sys |
| 79 | sys.dont_write_bytecode = True |
| 80 | |
| 81 | import socket |
| 82 | import struct |
| 83 | import subprocess |
| 84 | |
Enrico Granata | 66d5a97 | 2017-04-05 16:48:08 -0700 | [diff] [blame] | 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 |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 88 | # protoc -I=proto --python_out=proto proto/VehicleHalProto.proto |
| 89 | import VehicleHalProto_pb2 |
| 90 | |
Enrico Granata | 5e06712 | 2017-03-29 12:05:30 -0700 | [diff] [blame] | 91 | # If container is a dictionary, retrieve the value for key item; |
| 92 | # Otherwise, get the attribute named item out of container |
| 93 | def 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 |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 103 | |
| 104 | class 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 |
Scott Randolph | 95fb30b | 2017-05-11 17:32:26 -0700 | [diff] [blame] | 121 | self.sock.sendall(msgHdr) |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 122 | # Then send the protobuf |
Scott Randolph | 95fb30b | 2017-05-11 17:32:26 -0700 | [diff] [blame] | 123 | self.sock.sendall(msgStr) |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 124 | |
| 125 | ### Public Functions |
| 126 | def printHex(self, data): |
| 127 | """ |
| 128 | For debugging, print the protobuf message string in hex. |
| 129 | """ |
Enrico Granata | 66d5a97 | 2017-04-05 16:48:08 -0700 | [diff] [blame] | 130 | print("len = ", len(data), "str = ", ":".join("{:02x}".format(ord(d)) for d in data)) |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 131 | |
Enrico Granata | 9b6111d | 2017-06-29 11:36:17 -0700 | [diff] [blame] | 132 | def openSocket(self, device=None): |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 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 |
Enrico Granata | 9b6111d | 2017-06-29 11:36:17 -0700 | [diff] [blame] | 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)) |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 145 | # Open the socket and connect |
| 146 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
Enrico Granata | 9b6111d | 2017-06-29 11:36:17 -0700 | [diff] [blame] | 147 | self.sock.connect(('localhost', localPortNumber)) |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 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 |
Scott Randolph | 95fb30b | 2017-05-11 17:32:26 -0700 | [diff] [blame] | 167 | else: |
| 168 | print("Ignored message fragment") |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 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 | """ |
Scott Randolph | 02f7a32 | 2017-05-08 17:41:54 -0700 | [diff] [blame] | 182 | Sends a getConfigAll message to the host. This will return all configs available. |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 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 | """ |
Scott Randolph | 02f7a32 | 2017-05-08 17:41:54 -0700 | [diff] [blame] | 201 | Sends a getPropertyAll message to the host. This will return all properties available. |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 202 | """ |
| 203 | cmd = VehicleHalProto_pb2.EmulatorMessage() |
| 204 | cmd.msg_type = VehicleHalProto_pb2.GET_PROPERTY_ALL_CMD |
| 205 | self._txCmd(cmd) |
| 206 | |
Steve Paik | e6a9a4d | 2018-01-25 16:34:22 -0800 | [diff] [blame] | 207 | def setProperty(self, prop, area_id, value, status=VehicleHalProto_pb2.AVAILABLE): |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 208 | """ |
Steve Paik | e6a9a4d | 2018-01-25 16:34:22 -0800 | [diff] [blame] | 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. |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 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 |
Steve Paik | e6a9a4d | 2018-01-25 16:34:22 -0800 | [diff] [blame] | 221 | propValue.status = status; |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 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) |
Steve Paik | e6a9a4d | 2018-01-25 16:34:22 -0800 | [diff] [blame] | 243 | elif valType in self._types.TYPE_MIXED: |
Enrico Granata | 5e06712 | 2017-03-29 12:05:30 -0700 | [diff] [blame] | 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) |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 254 | else: |
| 255 | raise ValueError('value type not recognized:', valType) |
| 256 | return |
| 257 | self._txCmd(cmd) |
| 258 | |
Enrico Granata | 9b6111d | 2017-06-29 11:36:17 -0700 | [diff] [blame] | 259 | def __init__(self, types, device=None): |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 260 | # Save the list of types constants |
| 261 | self._types = types |
| 262 | # Open the socket |
Enrico Granata | 9b6111d | 2017-06-29 11:36:17 -0700 | [diff] [blame] | 263 | self.openSocket(device) |
Steve Paik | 7861f4e | 2017-02-22 15:02:41 -0800 | [diff] [blame] | 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 |