Home | History | Annotate | Download | only in utils
      1 #!/usr/bin/python
      2 # Copyright 2016 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 
      7 '''
      8 This script provides tools to map BattOrs to phones.
      9 
     10 Phones are identified by the following string:
     11 
     12 "Phone serial number" - Serial number of the phone. This can be
     13 obtained via 'adb devices' or 'usb-devices', and is not expected
     14 to change for a given phone.
     15 
     16 BattOrs are identified by the following two strings:
     17 
     18 "BattOr serial number" - Serial number of the BattOr. This can be
     19 obtained via 'usb-devices', and is not expected to change for
     20 a given BattOr.
     21 
     22 "BattOr path" - The path of the form '/dev/ttyUSB*' that is used
     23 to communicate with the BattOr (the battor_agent binary takes
     24 this BattOr path as a parameter). The BattOr path is frequently
     25 reassigned by the OS, most often when the device is disconnected
     26 and then reconnected. Thus, the BattOr path cannot be expected
     27 to be stable.
     28 
     29 In a typical application, the user will require the BattOr path
     30 for the BattOr that is plugged into a given phone. For instance,
     31 the user will be running tracing on a particular phone, and will
     32 need to know which BattOr path to use to communicate with the BattOr
     33 to get the corresponding power trace.
     34 
     35 Getting this mapping requires two steps: (1) determining the
     36 mapping between phone serial numbers and BattOr serial numbers, and
     37 (2) getting the BattOr path corresponding to a given BattOr serial
     38 number.
     39 
     40 For step (1), we generate a JSON file giving this mapping. This
     41 JSON file consists of a list of items of the following form:
     42 [{'phone': <phone serial 1>, 'battor': <battor serial 1>},
     43 {'phone': <phone serial 2>, 'battor': <battor serial 2>}, ...]
     44 
     45 The default way to generate this JSON file is using the function
     46 GenerateSerialMapFile, which generates a mapping based on assuming
     47 that the system has two identical USB hubs connected to it, and
     48 the phone plugged into physical port number 1 on one hub corresponds
     49 to the BattOr plugged into physical port number 1 on the other hub,
     50 and similarly with physical port numbers 2, 3, etc. This generates
     51 the map file based on the structure at the time GenerateSerialMapFile called.
     52 Note that after the map file is generated, port numbers are no longer used;
     53 the user could move around the devices in the ports without affecting
     54 which phone goes with which BattOr. (Thus, if the user wanted to update the
     55 mapping to match the new port connections, the user would have to
     56 re-generate this file.)
     57 
     58 The script update_mapping.py will do this updating from the command line.
     59 
     60 If the user wanted to specify a custom mapping, the user could instead
     61 create the JSON file manually. (In this case, hubs would not be necessary
     62 and the physical ports connected would be irrelevant.)
     63 
     64 Step (2) is conducted through the function GetBattOrPathFromPhoneSerial,
     65 which takes a serial number mapping generated via step (1) and a phone
     66 serial number, then gets the corresponding BattOr serial number from the
     67 map and determines its BattOr path (e.g. /dev/ttyUSB0). Since BattOr paths
     68 can change if devices are connected and disconnected (even if connected
     69 or disconnected via the same port) this function should be called to
     70 determine the BattOr path every time before connecting to the BattOr.
     71 
     72 Note that if there is only one BattOr connected to the system, then
     73 GetBattOrPathFromPhoneSerial will always return that BattOr and will ignore
     74 the mapping file. Thus, if the user never has more than one BattOr connected
     75 to the system, the user will not need to generate mapping files.
     76 '''
     77 
     78 
     79 import json
     80 import collections
     81 
     82 from battor import battor_error
     83 from devil.utils import find_usb_devices
     84 from devil.utils import usb_hubs
     85 
     86 
     87 def GetBattOrList(device_tree_map):
     88   return [x for x in find_usb_devices.GetTTYList()
     89           if IsBattOr(x, device_tree_map)]
     90 
     91 
     92 def IsBattOr(tty_string, device_tree_map):
     93   (bus, device) = find_usb_devices.GetBusDeviceFromTTY(tty_string)
     94   node = device_tree_map[bus].FindDeviceNumber(device)
     95   return '0403:6001' in node.desc
     96 
     97 
     98 def GetBattOrSerialNumbers(device_tree_map):
     99   for x in find_usb_devices.GetTTYList():
    100     if IsBattOr(x, device_tree_map):
    101       (bus, device) = find_usb_devices.GetBusDeviceFromTTY(x)
    102       devnode = device_tree_map[bus].FindDeviceNumber(device)
    103       yield devnode.serial
    104 
    105 
    106 def ReadSerialMapFile(filename):
    107   """Reads JSON file giving phone-to-battor serial number map.
    108 
    109   Parses a JSON file consisting of a list of items of the following form:
    110   [{'phone': <phone serial 1>, 'battor': <battor serial 1>},
    111   {'phone': <phone serial 2>, 'battor': <battor serial 2>}, ...]
    112 
    113   indicating which phone serial numbers should be matched with
    114   which BattOr serial numbers. Returns dictionary of the form:
    115 
    116   {<phone serial 1>: <BattOr serial 1>,
    117    <phone serial 2>: <BattOr serial 2>}
    118 
    119   Args:
    120       filename: Name of file to read.
    121   """
    122   result = {}
    123   with open(filename, 'r') as infile:
    124     in_dict = json.load(infile)
    125   for x in in_dict:
    126     result[x['phone']] = x['battor']
    127   return result
    128 
    129 def WriteSerialMapFile(filename, serial_map):
    130   """Writes a map of phone serial numbers to BattOr serial numbers to file.
    131 
    132   Writes a JSON file consisting of a list of items of the following form:
    133   [{'phone': <phone serial 1>, 'battor': <battor serial 1>},
    134   {'phone': <phone serial 2>, 'battor': <battor serial 2>}, ...]
    135 
    136   indicating which phone serial numbers should be matched with
    137   which BattOr serial numbers. Mapping is based on the physical port numbers
    138   of the hubs that the BattOrs and phones are connected to.
    139 
    140   Args:
    141       filename: Name of file to write.
    142       serial_map: Serial map {phone: battor}
    143   """
    144   result = []
    145   for (phone, battor) in serial_map.iteritems():
    146     result.append({'phone': phone, 'battor': battor})
    147   with open(filename, 'w') as outfile:
    148     json.dump(result, outfile)
    149 
    150 def GenerateSerialMap(hub_types=None):
    151   """Generates a map of phone serial numbers to BattOr serial numbers.
    152 
    153   Generates a dict of:
    154   {<phone serial 1>: <battor serial 1>,
    155    <phone serial 2>: <battor serial 2>}
    156   indicating which phone serial numbers should be matched with
    157   which BattOr serial numbers. Mapping is based on the physical port numbers
    158   of the hubs that the BattOrs and phones are connected to.
    159 
    160   Args:
    161       hub_types: List of hub types to check for. If not specified, checks
    162       for all defined hub types. (see usb_hubs.py for details)
    163   """
    164   if hub_types:
    165     hub_types = [usb_hubs.GetHubType(x) for x in hub_types]
    166   else:
    167     hub_types = usb_hubs.ALL_HUBS
    168 
    169   devtree = find_usb_devices.GetBusNumberToDeviceTreeMap()
    170 
    171   # List of serial numbers in the system that represent BattOrs.
    172   battor_serials = list(GetBattOrSerialNumbers(devtree))
    173 
    174   # If there's only one BattOr in the system, then a serial number ma
    175   # is not necessary.
    176   if len(battor_serials) == 1:
    177     return {}
    178 
    179   # List of dictionaries, one for each hub, that maps the physical
    180   # port number to the serial number of that hub. For instance, in a 2
    181   # hub system, this could return [{1:'ab', 2:'cd'}, {1:'jkl', 2:'xyz'}]
    182   # where 'ab' and 'cd' are the phone serial numbers and 'jkl' and 'xyz'
    183   # are the BattOr serial numbers.
    184   port_to_serial = find_usb_devices.GetAllPhysicalPortToSerialMaps(
    185       hub_types, device_tree_map=devtree)
    186 
    187   class serials(object):
    188     def __init__(self):
    189       self.phone = None
    190       self.battor = None
    191 
    192   # Map of {physical port number: [phone serial #, BattOr serial #]. This
    193   # map is populated by executing the code below. For instance, in the above
    194   # example, after the code below is executed, port_to_devices would equal
    195   # {1: ['ab', 'jkl'], 2: ['cd', 'xyz']}
    196   port_to_devices = collections.defaultdict(serials)
    197   for hub in port_to_serial:
    198     for (port, serial) in hub.iteritems():
    199       if serial in battor_serials:
    200         if port_to_devices[port].battor is not None:
    201           raise battor_error.BattOrError('Multiple BattOrs on same port number')
    202         else:
    203           port_to_devices[port].battor = serial
    204       else:
    205         if port_to_devices[port].phone is not None:
    206           raise battor_error.BattOrError('Multiple phones on same port number')
    207         else:
    208           port_to_devices[port].phone = serial
    209 
    210   # Turn the port_to_devices map into a map of the form
    211   # {phone serial number: BattOr serial number}.
    212   result = {}
    213   for pair in port_to_devices.values():
    214     if pair.phone is None:
    215       continue
    216     if pair.battor is None:
    217       raise battor_error.BattOrError(
    218           'Phone detected with no corresponding BattOr')
    219     result[pair.phone] = pair.battor
    220   return result
    221 
    222 def GenerateSerialMapFile(filename, hub_types=None):
    223   """Generates a serial map file and writes it."""
    224   WriteSerialMapFile(filename, GenerateSerialMap(hub_types))
    225 
    226 def _PhoneToPathMap(serial, serial_map, devtree):
    227   """Maps phone serial number to TTY path, assuming serial map is provided."""
    228   try:
    229     battor_serial = serial_map[serial]
    230   except KeyError:
    231     raise battor_error.BattOrError('Serial number not found in serial map.')
    232   for tree in devtree.values():
    233     for node in tree.AllNodes():
    234       if isinstance(node, find_usb_devices.USBDeviceNode):
    235         if node.serial == battor_serial:
    236           bus_device_to_tty = find_usb_devices.GetBusDeviceToTTYMap()
    237           bus_device = (node.bus_num, node.device_num)
    238           try:
    239             return bus_device_to_tty[bus_device]
    240           except KeyError:
    241             raise battor_error.BattOrError(
    242                 'Device with given serial number not a BattOr '
    243                 '(does not have TTY path)')
    244 
    245 
    246 def GetBattOrPathFromPhoneSerial(serial, serial_map=None,
    247                                  serial_map_file=None):
    248   """Gets the TTY path (e.g. '/dev/ttyUSB0')  to communicate with the BattOr.
    249 
    250   (1) If serial_map is given, it is treated as a dictionary mapping
    251   phone serial numbers to BattOr serial numbers. This function will get the
    252   TTY path for the given BattOr serial number.
    253 
    254   (2) If serial_map_file is given, it is treated as the name of a
    255   phone-to-BattOr mapping file (generated with GenerateSerialMapFile)
    256   and this will be loaded and used as the dict to map port numbers to
    257   BattOr serial numbers.
    258 
    259   You can only give one of serial_map and serial_map_file.
    260 
    261   Args:
    262     serial: Serial number of phone connected on the same physical port that
    263     the BattOr is connected to.
    264     serial_map: Map of phone serial numbers to BattOr serial numbers, given
    265     as a dictionary.
    266     serial_map_file: Map of phone serial numbers to BattOr serial numbers,
    267     given as a file.
    268     hub_types: List of hub types to check for. Used only if serial_map_file
    269     is None.
    270 
    271   Returns:
    272     Device string used to communicate with device.
    273 
    274   Raises:
    275     ValueError: If serial number is not given.
    276     BattOrError: If BattOr not found or unexpected USB topology.
    277   """
    278   # If there's only one BattOr connected to the system, just use that one.
    279   # This allows for use on, e.g., a developer's workstation with no hubs.
    280   devtree = find_usb_devices.GetBusNumberToDeviceTreeMap()
    281   all_battors = GetBattOrList(devtree)
    282   if len(all_battors) == 1:
    283     return '/dev/' + all_battors[0]
    284 
    285   if not serial:
    286     raise battor_error.BattOrError(
    287         'Two or more BattOrs connected, no serial provided')
    288 
    289   if serial_map and serial_map_file:
    290     raise ValueError('Cannot specify both serial_map and serial_map_file')
    291 
    292   if serial_map_file:
    293     serial_map = ReadSerialMapFile(serial_map_file)
    294 
    295   tty_string = _PhoneToPathMap(serial, serial_map, devtree)
    296 
    297   if not tty_string:
    298     raise battor_error.BattOrError(
    299         'No device with given serial number detected.')
    300 
    301   if IsBattOr(tty_string, devtree):
    302     return '/dev/' + tty_string
    303   else:
    304     raise battor_error.BattOrError(
    305         'Device with given serial number is not a BattOr.')
    306 
    307 if __name__ == '__main__':
    308   # Main function for testing purposes
    309   print GenerateSerialMap()
    310