blob: dad052eb28c40981aacd3440ba7460380c1e6f53 [file] [log] [blame]
Dennis Kempin1a8a5be2013-06-18 11:00:02 -07001# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
Dennis Kempin037675e2013-06-14 14:12:39 -07002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4#
Dennis Kempin17766a62013-06-17 14:09:33 -07005""" This module manages the platform properties in mttools/platforms """
Dennis Kempin1a8a5be2013-06-18 11:00:02 -07006import json
Dennis Kempin037675e2013-06-14 14:12:39 -07007import os
8import re
Dennis Kempin1a8a5be2013-06-18 11:00:02 -07009import sys
Dennis Kempin037675e2013-06-14 14:12:39 -070010from xorg_conf import XorgInputClassParser
Dennis Kempind0b722a2014-04-15 11:54:48 -070011from cros_remote import CrOSRemote
Dennis Kempin037675e2013-06-14 14:12:39 -070012
13# path to current script directory
14script_dir = os.path.dirname(os.path.realpath(__file__))
Dennis Kempin17766a62013-06-17 14:09:33 -070015platforms_dir = os.path.realpath(os.path.join(script_dir, '..', 'platforms'))
16xorg_conf_path = os.path.realpath(os.path.join(script_dir, '..',
17 '..', 'xorg-conf'))
Dennis Kempin037675e2013-06-14 14:12:39 -070018
Dennis Kempind0b722a2014-04-15 11:54:48 -070019props_template = """\
20{
21 "gestures": {
22 },
23 "xorg": {
24 "file": "%s",
25 "identifiers": %s
26 },
27 "ignore": [
28 ]
29}"""
30
Dennis Kempin037675e2013-06-14 14:12:39 -070031class PlatformProperties(object):
Dennis Kempin17766a62013-06-17 14:09:33 -070032 """ A class containing hardware and xorg properties for a platform.
33
34 The class can be created from an activity log or by providing
35 the name of the platform. Information will then be read from the
36 'platforms_dir' directory.
Dennis Kempin037675e2013-06-14 14:12:39 -070037 """
Dennis Kempin1a8a5be2013-06-18 11:00:02 -070038 def __init__(self, platform=None, log=None):
39 self.required_axis = []
40 self.has_axis = []
41 self.device_class = "touchpad"
Dennis Kempin037675e2013-06-14 14:12:39 -070042 if platform:
43 basename = os.path.join(platforms_dir, platform)
44 self.name = platform
Dennis Kempin17766a62013-06-17 14:09:33 -070045 self.hwprops_file = basename + '.hwprops'
46 self.props_file = basename + '.props'
Dennis Kempin037675e2013-06-14 14:12:39 -070047 self.xorg_parser = XorgInputClassParser()
48 self._ParseHWProperties(open(self.hwprops_file).read())
49 self._ParseProperties(open(self.props_file).read())
50 self._UpdateDimensions()
Dennis Kempin1a8a5be2013-06-18 11:00:02 -070051 elif log:
Dennis Kempin17766a62013-06-17 14:09:33 -070052 self.name = ''
Dennis Kempin1a8a5be2013-06-18 11:00:02 -070053 if log.evdev:
54 self._ParseEvdevLog(log.evdev)
Dennis Kempincd7caba2014-04-16 13:37:18 -070055 self._ParseActivityLog(log.activity)
56
Dennis Kempin037675e2013-06-14 14:12:39 -070057
58 def _ParseActivityLog(self, activity_data):
Dennis Kempin17766a62013-06-17 14:09:33 -070059 """ Parse property information from an activity log """
Dennis Kempin037675e2013-06-14 14:12:39 -070060 activity = json.loads(activity_data)
Dennis Kempin17766a62013-06-17 14:09:33 -070061 self.properties = activity['properties']
Dennis Kempin037675e2013-06-14 14:12:39 -070062
Dennis Kempin17766a62013-06-17 14:09:33 -070063 hwprops = activity['hardwareProperties']
64 self.x_min = int(hwprops['left'])
65 self.x_max = int(hwprops['right'])
66 self.x_res = int(hwprops['xResolution'])
67 self.y_min = int(hwprops['top'])
68 self.y_max = int(hwprops['bottom'])
69 self.y_res = int(hwprops['yResolution'])
Dennis Kempin037675e2013-06-14 14:12:39 -070070
Dennis Kempin1a8a5be2013-06-18 11:00:02 -070071 def _ParseEvdevLog(self, evdev_data):
Dennis Kempin143ef162014-04-09 13:46:50 -070072 # Look for embedded hwproperties in header. Format:
73 # absinfo: axis min max 0 0 res
74 abs_regex = 5 * ' ([0-9]+)'
75 xregex = re.compile('# absinfo: 53' + abs_regex)
76 xmatch = xregex.search(evdev_data);
77 self.x_min = int(xmatch.group(1))
78 self.x_max = int(xmatch.group(2))
79 self.x_res = int(xmatch.group(5))
80
81 yregex = re.compile('# absinfo: 54' + abs_regex)
82 ymatch = yregex.search(evdev_data);
83 self.y_min = int(ymatch.group(1))
84 self.y_max = int(ymatch.group(2))
85 self.y_res = int(ymatch.group(5))
86
87 axis_regex = re.compile('# absinfo: ([0-9]+)')
88 for match in axis_regex.finditer(evdev_data):
89 self.has_axis.append(int(match.group(1)))
90
91 # look for axes used in the log itself.
Dennis Kempin1a8a5be2013-06-18 11:00:02 -070092 # The format of ABS (0003) reports is:
93 # timestamp 0003 axis value
94 report_regex = re.compile(' 0003 ([0-9a-f]{4}) ([0-9a-f]+)')
95 for match in report_regex.finditer(evdev_data):
96 axis = int(match.group(1), 16)
97 if axis not in self.required_axis:
98 self.required_axis.append(axis)
99
Dennis Kempin143ef162014-04-09 13:46:50 -0700100
Dennis Kempin037675e2013-06-14 14:12:39 -0700101 def _ParseHWProperties(self, data):
Dennis Kempin17766a62013-06-17 14:09:33 -0700102 """ Parse x and y dimensions and resolution from hwprops file """
Dennis Kempin143ef162014-04-09 13:46:50 -0700103 abs_regex = 5 * ' ([0-9\-]+)'
104 xregex = re.compile('A: 35' + abs_regex)
Dennis Kempin037675e2013-06-14 14:12:39 -0700105 xmatch = xregex.search(data);
106 self.x_min = int(xmatch.group(1))
107 self.x_max = int(xmatch.group(2))
108 self.x_res = int(xmatch.group(5))
109
Dennis Kempin143ef162014-04-09 13:46:50 -0700110 yregex = re.compile('A: 36' + abs_regex)
Dennis Kempin037675e2013-06-14 14:12:39 -0700111 ymatch = yregex.search(data);
112 self.y_min = int(ymatch.group(1))
113 self.y_max = int(ymatch.group(2))
114 self.y_res = int(ymatch.group(5))
115
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700116 axis_regex = re.compile('A: ([0-9a-f]+)')
117 for match in axis_regex.finditer(data):
118 self.has_axis.append(int(match.group(1), 16))
119
Dennis Kempin037675e2013-06-14 14:12:39 -0700120 def _ParseProperties(self, data):
Dennis Kempin17766a62013-06-17 14:09:33 -0700121 """ Parse properties from file and inject xorg properties. """
Dennis Kempin037675e2013-06-14 14:12:39 -0700122 self.properties = {}
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700123 self.ignore_properties = []
Dennis Kempin037675e2013-06-14 14:12:39 -0700124 data = json.loads(data)
125
Dennis Kempin17766a62013-06-17 14:09:33 -0700126 if 'gestures' in data:
127 self.properties.update(data['gestures'])
Dennis Kempin037675e2013-06-14 14:12:39 -0700128
Dennis Kempin17766a62013-06-17 14:09:33 -0700129 if 'device_class' in data:
130 self.device_class = data['device_class']
Dennis Kempin037675e2013-06-14 14:12:39 -0700131
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700132 if 'ignore' in data:
133 self.ignore_properties.extend(data['ignore'])
134
Dennis Kempin17766a62013-06-17 14:09:33 -0700135 if xorg_conf_path and 'xorg' in data and 'file' in data['xorg']:
136 filename = os.path.join(xorg_conf_path, data['xorg']['file'])
Dennis Kempin037675e2013-06-14 14:12:39 -0700137 input_classes = self.xorg_parser.Parse(file=filename)
Dennis Kempin17766a62013-06-17 14:09:33 -0700138 if 'identifier' in data['xorg']:
139 properties = input_classes[data['xorg']['identifier']]
Dennis Kempin037675e2013-06-14 14:12:39 -0700140 self.properties.update(properties)
Dennis Kempin17766a62013-06-17 14:09:33 -0700141 if 'identifiers' in data['xorg']:
142 for identifier in data['xorg']['identifiers']:
Dennis Kempin037675e2013-06-14 14:12:39 -0700143 properties = input_classes[identifier]
144 self.properties.update(properties)
145
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700146 for prop in self.ignore_properties:
147 if prop in self.properties:
148 del self.properties[prop]
149
Dennis Kempin037675e2013-06-14 14:12:39 -0700150 def _UpdateDimensions(self):
Dennis Kempin17766a62013-06-17 14:09:33 -0700151 """ Update x/y min/max with xorg properties.
152
153 CMT allows hardware properties to be overwritten by xorg properties.
154 Do the same in this class.
Dennis Kempin037675e2013-06-14 14:12:39 -0700155 """
Dennis Kempin17766a62013-06-17 14:09:33 -0700156 if 'Active Area Left' in self.properties:
157 self.x_min = int(self.properties['Active Area Left'])
158 if 'Active Area Right' in self.properties:
159 self.x_max = int(self.properties['Active Area Right'])
Dennis Kempin17766a62013-06-17 14:09:33 -0700160 if 'Active Area Top' in self.properties:
161 self.y_min = int(self.properties['Active Area Top'])
162 if 'Active Area Bottom' in self.properties:
163 self.y_max = int(self.properties['Active Area Bottom'])
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700164
165 if 'Horizontal Resolution' in self.properties:
166 self.x_res = int(self.properties['Horizontal Resolution'])
Dennis Kempin17766a62013-06-17 14:09:33 -0700167 if 'Vertical Resolution' in self.properties:
168 self.y_res = int(self.properties['Vertical Resolution'])
Dennis Kempin037675e2013-06-14 14:12:39 -0700169
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700170 if 'SemiMT Non Linear Area Left' in self.properties:
171 self.x_min = int(self.properties['SemiMT Non Linear Area Left'])
172 if 'SemiMT Non Linear Area Right' in self.properties:
173 self.x_max = int(self.properties['SemiMT Non Linear Area Right'])
174 if 'SemiMT Non Linear Area Top' in self.properties:
175 self.y_min = int(self.properties['SemiMT Non Linear Area Top'])
176 if 'SemiMT Non Linear Area Bottom' in self.properties:
177 self.y_max = int(self.properties['SemiMT Non Linear Area Bottom'])
178
179
180 def Match(self, other, loose, debug=False):
Dennis Kempin17766a62013-06-17 14:09:33 -0700181 """ Compare properties and return similarity.
182
183 Compare these properties to another PlatformProperties instance.
184 The return value is a score between 1. 0 meaning there is a big mismatch
185 and 1 meaning the properties match completely.
186 Only a selected range of properties are compared in order to
187 prevent property adjustments to cause platforms to be mismatched.
Dennis Kempin037675e2013-06-14 14:12:39 -0700188 """
189 scores = []
190 def compare(a, b, what):
191 value = abs(float(a) - float(b))
192 if value > 0:
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700193 value = min(1, value / max(abs(float(a)), abs(float(b))))
Dennis Kempin037675e2013-06-14 14:12:39 -0700194 scores.append(1-value)
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700195 if debug:
196 print "%s: %s == %s" % (what, str(a), str(b))
Dennis Kempin037675e2013-06-14 14:12:39 -0700197 def compare_attr(what):
198 compare(getattr(self, what), getattr(other, what), what)
199 def compare_prop(what):
200 if what not in self.properties or what not in other.properties:
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700201 scores.append(0)
202 else:
203 compare(self.properties[what], other.properties[what], what)
204 def check_axis(required, available):
205 for axis in required:
206 if axis not in available:
207 scores.append(0)
208 return
209
Dennis Kempin17766a62013-06-17 14:09:33 -0700210 compare_attr('x_min')
211 compare_attr('x_max')
212 compare_attr('x_res')
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700213 compare_attr('y_min')
214 compare_attr('y_max')
215 compare_attr('y_res')
216 if not loose:
217 compare_prop('Pressure Calibration Offset')
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700218
219 if self.required_axis:
220 if debug:
221 print "axis:", self.required_axis, "in", other.has_axis
222 check_axis(self.required_axis, other.has_axis)
223
224 if other.required_axis:
225 if debug:
226 print "axis:", other.required_axis, "in", self.has_axis
227 check_axis(other.required_axis, self.has_axis)
228
Dennis Kempin037675e2013-06-14 14:12:39 -0700229 return reduce(lambda x,y: (x * y), scores)
230
231class PlatformDatabase(object):
Dennis Kempin17766a62013-06-17 14:09:33 -0700232 """ Class for managing platforms.
233
234 This class reads all available platforms from the platforms_dir and allows
235 to search for matching platforms to an activity log file.
Dennis Kempin037675e2013-06-14 14:12:39 -0700236 """
237 def __init__(self):
238 platform_files = [ f for f in os.listdir(platforms_dir)
Dennis Kempin17766a62013-06-17 14:09:33 -0700239 if f.endswith('.hwprops') ]
Dennis Kempin037675e2013-06-14 14:12:39 -0700240 self.platforms = {}
241 for file in platform_files:
Dennis Kempin17766a62013-06-17 14:09:33 -0700242 name = file.replace('.hwprops', '')
Dennis Kempin037675e2013-06-14 14:12:39 -0700243 self.platforms[name] = PlatformProperties(platform=name)
244
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700245 def FindMatching(self, log, loose=True, debug=False):
Dennis Kempin17766a62013-06-17 14:09:33 -0700246 """ Find platform matching activity_data.
247
248 Returns the PlatformProperties instance of the platform matching
249 the activity log data. This method might terminate the program in
250 case no match can be made, or the match is ambiguous.
Dennis Kempin037675e2013-06-14 14:12:39 -0700251 """
252 result = None
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700253 properties = PlatformProperties(log=log)
Dennis Kempin037675e2013-06-14 14:12:39 -0700254 for name, platform in self.platforms.items():
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700255 if debug:
256 print "#" * 10, name
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700257 score = platform.Match(properties, loose, debug)
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700258 if debug:
259 print name, "score =", score
Dennis Kempin037675e2013-06-14 14:12:39 -0700260 if score > 0.9:
261 if result:
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700262 if loose:
263 if debug:
264 print "-" * 10, "Multiple matches. Try strict"
265 return self.FindMatching(log, False, debug)
Dennis Kempin17766a62013-06-17 14:09:33 -0700266 print ('multiple matching platforms:', result.name,
267 'and', platform.name)
Dennis Kempin19e972b2013-06-20 13:21:38 -0700268 return None
Dennis Kempin037675e2013-06-14 14:12:39 -0700269 result = platform
270 if not result:
Dennis Kempin17766a62013-06-17 14:09:33 -0700271 print 'cannot find matching platform'
Dennis Kempin19e972b2013-06-20 13:21:38 -0700272 return None
Dennis Kempin037675e2013-06-14 14:12:39 -0700273 return result
Dennis Kempind0b722a2014-04-15 11:54:48 -0700274
275 @staticmethod
276 def RegisterPlatformFromDevice(ip):
277 # get list of multitouch devices
278 remote = CrOSRemote(ip)
279 devices = remote.ReadCommand(
280 "/opt/google/input/inputcontrol -t multitouch --names",
281 verbose=True)
282 if devices is False:
283 return None
284
285 # Each line has the format:
286 # id: Device Name
287 # devices[*][0] will have the id
288 # devices[*][1] will have the name
289 devices = devices.splitlines()
290 devices = [l.split(":", 1) for l in devices]
291
292 # select one device from list
293 idx = UserSelection([d[1] for d in devices],
294 "Which device would you like to register?")
295 device_id = devices[idx][0]
296
297 # read hardware properties
298 hwprops = remote.ReadCommand(
299 "/opt/google/input/inputcontrol --id %s --hwprops" % device_id,
300 verbose=True)
301 if not hwprops:
302 print "Please update your device to latest canary or:"
303 print " emerge-${BOARD} inputcontrol"
304 print " cros deploy $DEVICE_IP inputcontrol"
305 return None
306
Dennis Kempincd7caba2014-04-16 13:37:18 -0700307 xorg_files = [
308 "/etc/X11/xorg.conf.d/60-touchpad-cmt-*.conf",
309 "/etc/X11/xorg.conf.d/50-touchpad-cmt-*.conf",
310 "/etc/X11/xorg.conf.d/40-touchpad-cmt.conf"
311 ]
312
313 for pattern in xorg_files:
314 # find filename of xorg configuration file
315 xorg_file = remote.ReadCommand("ls " + pattern, verbose=False)
316 if not xorg_file:
317 continue
318 xorg_file = xorg_file.strip()
319
320 # extract identifiers
Dennis Kempind0b722a2014-04-15 11:54:48 -0700321 print "Selecting Xorg identifiers from", xorg_file
322 conf = remote.Read(xorg_file)
Dennis Kempincd7caba2014-04-16 13:37:18 -0700323 all_ids = []
324 for match in re.finditer("Identifier\s+\"([a-zA-Z0-9-_ ]+)\"", conf):
325 all_ids.append(match.group(1))
Dennis Kempind0b722a2014-04-15 11:54:48 -0700326
Dennis Kempincd7caba2014-04-16 13:37:18 -0700327 # ask user to select
328 idxs = UserSelection(all_ids,
329 "Which xorg identifiers apply to this device?",
Dennis Kempind0b722a2014-04-15 11:54:48 -0700330 allow_multi=True, allow_zero=True)
Dennis Kempincd7caba2014-04-16 13:37:18 -0700331 ids = [all_ids[i] for i in idxs]
332 if ids:
333 break
Dennis Kempind0b722a2014-04-15 11:54:48 -0700334
Dennis Kempind0b722a2014-04-15 11:54:48 -0700335 if not ids:
Dennis Kempincd7caba2014-04-16 13:37:18 -0700336 print "Please configure the platform properties manually"
337 xorg_file = "todo: add correct xorg conf file"
338 ids = ["todo: add correct xorg identifier"]
Dennis Kempind0b722a2014-04-15 11:54:48 -0700339
340 ids_string = "[" + ", ".join(["\"%s\"" % id for id in ids]) + "]"
341 xorg_file = os.path.basename(xorg_file)
342
343 sys.stdout.write("Please name this platform: ")
344 sys.stdout.flush()
345 platform_name = sys.stdin.readline().strip()
346
347 # write platform info to files
348 hwprops_file = os.path.join(platforms_dir, platform_name + ".hwprops")
349 props_file = os.path.join(platforms_dir, platform_name + ".props")
350
351 open(hwprops_file, "w").write(hwprops)
352 open(props_file, "w").write(props_template % (xorg_file, ids_string))
353
354 print "Created files: "
355 print " ", hwprops_file
356 print " ", props_file
357
358 return platform_name
359
360
361def UserSelection(list, msg, allow_multi=False, allow_zero=False):
362 idx = [0]
Dennis Kempincd7caba2014-04-16 13:37:18 -0700363 if len(list) == 1 and not allow_zero:
Dennis Kempind0b722a2014-04-15 11:54:48 -0700364 return [0] if allow_multi else 0
365
366 # repeat until user made a valid selection
367 while True:
368
369 # get user input
370 print msg
371 if allow_zero:
372 print " 0: None"
373 for i, item in enumerate(list):
374 print " ", str(i + 1) + ":", item
375 if allow_multi:
376 print "(Separate multiple selections with spaces)"
377 sys.stdout.write('> ')
378 sys.stdout.flush()
379 selection = sys.stdin.readline()
380
381 # validates single input value
382 def CheckSelection(selection):
383 try:
384 idx = int(selection) - 1
385 if allow_zero and idx == -1:
386 return True
387 if idx < 0 or idx >= len(list):
388 print 'Number out of range'
389 return False
390 except:
391 print 'Not a number'
392 return False
393 return True
394
395 if allow_multi:
396 # validate list of values
397 valid = True
398 selections = selection.split(" ")
399 for selection in selections:
400 if not CheckSelection(selection):
401 valid = False
402 break
403 if valid:
404 selections = [int(s) - 1 for s in selections]
405 if -1 in selections:
406 return []
407 return selections
408 else:
409 if CheckSelection(selection):
410 return int(selection) - 1