blob: 875b4b8393afc25de0ac3b04caec762d77a795fe [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'))
Chung-yih Wang35613f12014-04-25 13:57:23 +080016xorg_conf_project_path = 'src/platform/xorg-conf'
Dennis Kempin037675e2013-06-14 14:12:39 -070017
Dennis Kempind0b722a2014-04-15 11:54:48 -070018props_template = """\
19{
20 "gestures": {
21 },
22 "xorg": {
23 "file": "%s",
24 "identifiers": %s
25 },
26 "ignore": [
27 ]
28}"""
29
Dennis Kempin037675e2013-06-14 14:12:39 -070030class PlatformProperties(object):
Dennis Kempin17766a62013-06-17 14:09:33 -070031 """ A class containing hardware and xorg properties for a platform.
32
33 The class can be created from an activity log or by providing
34 the name of the platform. Information will then be read from the
35 'platforms_dir' directory.
Dennis Kempin037675e2013-06-14 14:12:39 -070036 """
Dennis Kempin1a8a5be2013-06-18 11:00:02 -070037 def __init__(self, platform=None, log=None):
38 self.required_axis = []
39 self.has_axis = []
40 self.device_class = "touchpad"
Dennis Kempin037675e2013-06-14 14:12:39 -070041 if platform:
42 basename = os.path.join(platforms_dir, platform)
43 self.name = platform
Dennis Kempin17766a62013-06-17 14:09:33 -070044 self.hwprops_file = basename + '.hwprops'
45 self.props_file = basename + '.props'
Dennis Kempin037675e2013-06-14 14:12:39 -070046 self.xorg_parser = XorgInputClassParser()
47 self._ParseHWProperties(open(self.hwprops_file).read())
48 self._ParseProperties(open(self.props_file).read())
49 self._UpdateDimensions()
Dennis Kempin1a8a5be2013-06-18 11:00:02 -070050 elif log:
Dennis Kempin17766a62013-06-17 14:09:33 -070051 self.name = ''
Dennis Kempin1a8a5be2013-06-18 11:00:02 -070052 if log.evdev:
53 self._ParseEvdevLog(log.evdev)
Dennis Kempincd7caba2014-04-16 13:37:18 -070054 self._ParseActivityLog(log.activity)
55
Dennis Kempin037675e2013-06-14 14:12:39 -070056
57 def _ParseActivityLog(self, activity_data):
Dennis Kempin17766a62013-06-17 14:09:33 -070058 """ Parse property information from an activity log """
Dennis Kempin037675e2013-06-14 14:12:39 -070059 activity = json.loads(activity_data)
Dennis Kempin17766a62013-06-17 14:09:33 -070060 self.properties = activity['properties']
Dennis Kempin037675e2013-06-14 14:12:39 -070061
Dennis Kempin17766a62013-06-17 14:09:33 -070062 hwprops = activity['hardwareProperties']
63 self.x_min = int(hwprops['left'])
64 self.x_max = int(hwprops['right'])
65 self.x_res = int(hwprops['xResolution'])
66 self.y_min = int(hwprops['top'])
67 self.y_max = int(hwprops['bottom'])
68 self.y_res = int(hwprops['yResolution'])
Dennis Kempin037675e2013-06-14 14:12:39 -070069
Dennis Kempin1a8a5be2013-06-18 11:00:02 -070070 def _ParseEvdevLog(self, evdev_data):
Dennis Kempin143ef162014-04-09 13:46:50 -070071 # Look for embedded hwproperties in header. Format:
72 # absinfo: axis min max 0 0 res
73 abs_regex = 5 * ' ([0-9]+)'
74 xregex = re.compile('# absinfo: 53' + abs_regex)
75 xmatch = xregex.search(evdev_data);
76 self.x_min = int(xmatch.group(1))
77 self.x_max = int(xmatch.group(2))
78 self.x_res = int(xmatch.group(5))
79
80 yregex = re.compile('# absinfo: 54' + abs_regex)
81 ymatch = yregex.search(evdev_data);
82 self.y_min = int(ymatch.group(1))
83 self.y_max = int(ymatch.group(2))
84 self.y_res = int(ymatch.group(5))
85
86 axis_regex = re.compile('# absinfo: ([0-9]+)')
87 for match in axis_regex.finditer(evdev_data):
88 self.has_axis.append(int(match.group(1)))
89
90 # look for axes used in the log itself.
Dennis Kempin1a8a5be2013-06-18 11:00:02 -070091 # The format of ABS (0003) reports is:
92 # timestamp 0003 axis value
93 report_regex = re.compile(' 0003 ([0-9a-f]{4}) ([0-9a-f]+)')
94 for match in report_regex.finditer(evdev_data):
95 axis = int(match.group(1), 16)
96 if axis not in self.required_axis:
97 self.required_axis.append(axis)
98
Dennis Kempin143ef162014-04-09 13:46:50 -070099
Dennis Kempin037675e2013-06-14 14:12:39 -0700100 def _ParseHWProperties(self, data):
Dennis Kempin17766a62013-06-17 14:09:33 -0700101 """ Parse x and y dimensions and resolution from hwprops file """
Dennis Kempin143ef162014-04-09 13:46:50 -0700102 abs_regex = 5 * ' ([0-9\-]+)'
103 xregex = re.compile('A: 35' + abs_regex)
Dennis Kempin037675e2013-06-14 14:12:39 -0700104 xmatch = xregex.search(data);
105 self.x_min = int(xmatch.group(1))
106 self.x_max = int(xmatch.group(2))
107 self.x_res = int(xmatch.group(5))
108
Dennis Kempin143ef162014-04-09 13:46:50 -0700109 yregex = re.compile('A: 36' + abs_regex)
Dennis Kempin037675e2013-06-14 14:12:39 -0700110 ymatch = yregex.search(data);
111 self.y_min = int(ymatch.group(1))
112 self.y_max = int(ymatch.group(2))
113 self.y_res = int(ymatch.group(5))
114
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700115 axis_regex = re.compile('A: ([0-9a-f]+)')
116 for match in axis_regex.finditer(data):
117 self.has_axis.append(int(match.group(1), 16))
118
Dennis Kempin037675e2013-06-14 14:12:39 -0700119 def _ParseProperties(self, data):
Dennis Kempin17766a62013-06-17 14:09:33 -0700120 """ Parse properties from file and inject xorg properties. """
Dennis Kempin037675e2013-06-14 14:12:39 -0700121 self.properties = {}
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700122 self.ignore_properties = []
Dennis Kempin037675e2013-06-14 14:12:39 -0700123 data = json.loads(data)
124
Dennis Kempin17766a62013-06-17 14:09:33 -0700125 if 'gestures' in data:
126 self.properties.update(data['gestures'])
Dennis Kempin037675e2013-06-14 14:12:39 -0700127
Dennis Kempin17766a62013-06-17 14:09:33 -0700128 if 'device_class' in data:
129 self.device_class = data['device_class']
Dennis Kempin037675e2013-06-14 14:12:39 -0700130
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700131 if 'ignore' in data:
132 self.ignore_properties.extend(data['ignore'])
133
Chung-yih Wang35613f12014-04-25 13:57:23 +0800134 # Sanity check: Make sure it is not used inside ebuild.
135 if os.environ.get('PN') and os.environ.get('S'):
136 raise Exception("Should not be executed inside ebuild")
137
138 # If run on a Chromebook device, access xorg-conf files from their normal
139 # installed location. If run from inside chroot, access xorg-conf files
140 # from the xorg-conf project repository.
141 src_root = os.environ.get('CROS_WORKON_SRCROOT')
142 if src_root:
143 xorg_conf_path = os.path.join(src_root, xorg_conf_project_path)
144 else:
145 xorg_conf_path = '/etc/X11/xorg.conf.d'
146
Dennis Kempin17766a62013-06-17 14:09:33 -0700147 if xorg_conf_path and 'xorg' in data and 'file' in data['xorg']:
148 filename = os.path.join(xorg_conf_path, data['xorg']['file'])
Dennis Kempin037675e2013-06-14 14:12:39 -0700149 input_classes = self.xorg_parser.Parse(file=filename)
Dennis Kempin17766a62013-06-17 14:09:33 -0700150 if 'identifier' in data['xorg']:
151 properties = input_classes[data['xorg']['identifier']]
Dennis Kempin037675e2013-06-14 14:12:39 -0700152 self.properties.update(properties)
Dennis Kempin17766a62013-06-17 14:09:33 -0700153 if 'identifiers' in data['xorg']:
154 for identifier in data['xorg']['identifiers']:
Dennis Kempin037675e2013-06-14 14:12:39 -0700155 properties = input_classes[identifier]
156 self.properties.update(properties)
157
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700158 for prop in self.ignore_properties:
159 if prop in self.properties:
160 del self.properties[prop]
161
Dennis Kempin037675e2013-06-14 14:12:39 -0700162 def _UpdateDimensions(self):
Dennis Kempin17766a62013-06-17 14:09:33 -0700163 """ Update x/y min/max with xorg properties.
164
165 CMT allows hardware properties to be overwritten by xorg properties.
166 Do the same in this class.
Dennis Kempin037675e2013-06-14 14:12:39 -0700167 """
Dennis Kempin17766a62013-06-17 14:09:33 -0700168 if 'Active Area Left' in self.properties:
169 self.x_min = int(self.properties['Active Area Left'])
170 if 'Active Area Right' in self.properties:
171 self.x_max = int(self.properties['Active Area Right'])
Dennis Kempin17766a62013-06-17 14:09:33 -0700172 if 'Active Area Top' in self.properties:
173 self.y_min = int(self.properties['Active Area Top'])
174 if 'Active Area Bottom' in self.properties:
175 self.y_max = int(self.properties['Active Area Bottom'])
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700176
177 if 'Horizontal Resolution' in self.properties:
178 self.x_res = int(self.properties['Horizontal Resolution'])
Dennis Kempin17766a62013-06-17 14:09:33 -0700179 if 'Vertical Resolution' in self.properties:
180 self.y_res = int(self.properties['Vertical Resolution'])
Dennis Kempin037675e2013-06-14 14:12:39 -0700181
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700182 if 'SemiMT Non Linear Area Left' in self.properties:
183 self.x_min = int(self.properties['SemiMT Non Linear Area Left'])
184 if 'SemiMT Non Linear Area Right' in self.properties:
185 self.x_max = int(self.properties['SemiMT Non Linear Area Right'])
186 if 'SemiMT Non Linear Area Top' in self.properties:
187 self.y_min = int(self.properties['SemiMT Non Linear Area Top'])
188 if 'SemiMT Non Linear Area Bottom' in self.properties:
189 self.y_max = int(self.properties['SemiMT Non Linear Area Bottom'])
190
191
192 def Match(self, other, loose, debug=False):
Dennis Kempin17766a62013-06-17 14:09:33 -0700193 """ Compare properties and return similarity.
194
195 Compare these properties to another PlatformProperties instance.
196 The return value is a score between 1. 0 meaning there is a big mismatch
197 and 1 meaning the properties match completely.
198 Only a selected range of properties are compared in order to
199 prevent property adjustments to cause platforms to be mismatched.
Dennis Kempin037675e2013-06-14 14:12:39 -0700200 """
201 scores = []
202 def compare(a, b, what):
203 value = abs(float(a) - float(b))
204 if value > 0:
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700205 value = min(1, value / max(abs(float(a)), abs(float(b))))
Dennis Kempin037675e2013-06-14 14:12:39 -0700206 scores.append(1-value)
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700207 if debug:
208 print "%s: %s == %s" % (what, str(a), str(b))
Dennis Kempin037675e2013-06-14 14:12:39 -0700209 def compare_attr(what):
210 compare(getattr(self, what), getattr(other, what), what)
211 def compare_prop(what):
212 if what not in self.properties or what not in other.properties:
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700213 scores.append(0)
214 else:
215 compare(self.properties[what], other.properties[what], what)
216 def check_axis(required, available):
217 for axis in required:
218 if axis not in available:
219 scores.append(0)
220 return
221
Dennis Kempin17766a62013-06-17 14:09:33 -0700222 compare_attr('x_min')
223 compare_attr('x_max')
224 compare_attr('x_res')
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700225 compare_attr('y_min')
226 compare_attr('y_max')
227 compare_attr('y_res')
228 if not loose:
229 compare_prop('Pressure Calibration Offset')
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700230
231 if self.required_axis:
232 if debug:
233 print "axis:", self.required_axis, "in", other.has_axis
234 check_axis(self.required_axis, other.has_axis)
235
236 if other.required_axis:
237 if debug:
238 print "axis:", other.required_axis, "in", self.has_axis
239 check_axis(other.required_axis, self.has_axis)
240
Dennis Kempin037675e2013-06-14 14:12:39 -0700241 return reduce(lambda x,y: (x * y), scores)
242
243class PlatformDatabase(object):
Dennis Kempin17766a62013-06-17 14:09:33 -0700244 """ Class for managing platforms.
245
246 This class reads all available platforms from the platforms_dir and allows
247 to search for matching platforms to an activity log file.
Dennis Kempin037675e2013-06-14 14:12:39 -0700248 """
249 def __init__(self):
250 platform_files = [ f for f in os.listdir(platforms_dir)
Dennis Kempin17766a62013-06-17 14:09:33 -0700251 if f.endswith('.hwprops') ]
Dennis Kempin037675e2013-06-14 14:12:39 -0700252 self.platforms = {}
253 for file in platform_files:
Dennis Kempin17766a62013-06-17 14:09:33 -0700254 name = file.replace('.hwprops', '')
Dennis Kempin037675e2013-06-14 14:12:39 -0700255 self.platforms[name] = PlatformProperties(platform=name)
256
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700257 def FindMatching(self, log, loose=True, debug=False):
Dennis Kempin17766a62013-06-17 14:09:33 -0700258 """ Find platform matching activity_data.
259
260 Returns the PlatformProperties instance of the platform matching
261 the activity log data. This method might terminate the program in
262 case no match can be made, or the match is ambiguous.
Dennis Kempin037675e2013-06-14 14:12:39 -0700263 """
264 result = None
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700265 properties = PlatformProperties(log=log)
Dennis Kempin037675e2013-06-14 14:12:39 -0700266 for name, platform in self.platforms.items():
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700267 if debug:
268 print "#" * 10, name
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700269 score = platform.Match(properties, loose, debug)
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700270 if debug:
271 print name, "score =", score
Dennis Kempin037675e2013-06-14 14:12:39 -0700272 if score > 0.9:
273 if result:
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700274 if loose:
275 if debug:
276 print "-" * 10, "Multiple matches. Try strict"
277 return self.FindMatching(log, False, debug)
Dennis Kempin17766a62013-06-17 14:09:33 -0700278 print ('multiple matching platforms:', result.name,
279 'and', platform.name)
Dennis Kempin19e972b2013-06-20 13:21:38 -0700280 return None
Dennis Kempin037675e2013-06-14 14:12:39 -0700281 result = platform
282 if not result:
Dennis Kempin17766a62013-06-17 14:09:33 -0700283 print 'cannot find matching platform'
Dennis Kempin19e972b2013-06-20 13:21:38 -0700284 return None
Dennis Kempin037675e2013-06-14 14:12:39 -0700285 return result
Dennis Kempind0b722a2014-04-15 11:54:48 -0700286
287 @staticmethod
288 def RegisterPlatformFromDevice(ip):
289 # get list of multitouch devices
290 remote = CrOSRemote(ip)
291 devices = remote.ReadCommand(
292 "/opt/google/input/inputcontrol -t multitouch --names",
293 verbose=True)
294 if devices is False:
295 return None
296
297 # Each line has the format:
298 # id: Device Name
299 # devices[*][0] will have the id
300 # devices[*][1] will have the name
301 devices = devices.splitlines()
302 devices = [l.split(":", 1) for l in devices]
303
304 # select one device from list
305 idx = UserSelection([d[1] for d in devices],
306 "Which device would you like to register?")
307 device_id = devices[idx][0]
308
309 # read hardware properties
310 hwprops = remote.ReadCommand(
311 "/opt/google/input/inputcontrol --id %s --hwprops" % device_id,
312 verbose=True)
313 if not hwprops:
314 print "Please update your device to latest canary or:"
315 print " emerge-${BOARD} inputcontrol"
316 print " cros deploy $DEVICE_IP inputcontrol"
317 return None
318
Dennis Kempincd7caba2014-04-16 13:37:18 -0700319 xorg_files = [
320 "/etc/X11/xorg.conf.d/60-touchpad-cmt-*.conf",
321 "/etc/X11/xorg.conf.d/50-touchpad-cmt-*.conf",
322 "/etc/X11/xorg.conf.d/40-touchpad-cmt.conf"
323 ]
324
325 for pattern in xorg_files:
326 # find filename of xorg configuration file
327 xorg_file = remote.ReadCommand("ls " + pattern, verbose=False)
328 if not xorg_file:
329 continue
330 xorg_file = xorg_file.strip()
331
332 # extract identifiers
Dennis Kempind0b722a2014-04-15 11:54:48 -0700333 print "Selecting Xorg identifiers from", xorg_file
334 conf = remote.Read(xorg_file)
Dennis Kempincd7caba2014-04-16 13:37:18 -0700335 all_ids = []
336 for match in re.finditer("Identifier\s+\"([a-zA-Z0-9-_ ]+)\"", conf):
337 all_ids.append(match.group(1))
Dennis Kempind0b722a2014-04-15 11:54:48 -0700338
Dennis Kempincd7caba2014-04-16 13:37:18 -0700339 # ask user to select
340 idxs = UserSelection(all_ids,
341 "Which xorg identifiers apply to this device?",
Dennis Kempind0b722a2014-04-15 11:54:48 -0700342 allow_multi=True, allow_zero=True)
Dennis Kempincd7caba2014-04-16 13:37:18 -0700343 ids = [all_ids[i] for i in idxs]
344 if ids:
345 break
Dennis Kempind0b722a2014-04-15 11:54:48 -0700346
Dennis Kempind0b722a2014-04-15 11:54:48 -0700347 if not ids:
Dennis Kempincd7caba2014-04-16 13:37:18 -0700348 print "Please configure the platform properties manually"
349 xorg_file = "todo: add correct xorg conf file"
350 ids = ["todo: add correct xorg identifier"]
Dennis Kempind0b722a2014-04-15 11:54:48 -0700351
352 ids_string = "[" + ", ".join(["\"%s\"" % id for id in ids]) + "]"
353 xorg_file = os.path.basename(xorg_file)
354
355 sys.stdout.write("Please name this platform: ")
356 sys.stdout.flush()
357 platform_name = sys.stdin.readline().strip()
358
359 # write platform info to files
360 hwprops_file = os.path.join(platforms_dir, platform_name + ".hwprops")
361 props_file = os.path.join(platforms_dir, platform_name + ".props")
362
363 open(hwprops_file, "w").write(hwprops)
364 open(props_file, "w").write(props_template % (xorg_file, ids_string))
365
366 print "Created files: "
367 print " ", hwprops_file
368 print " ", props_file
369
370 return platform_name
371
372
373def UserSelection(list, msg, allow_multi=False, allow_zero=False):
374 idx = [0]
Dennis Kempincd7caba2014-04-16 13:37:18 -0700375 if len(list) == 1 and not allow_zero:
Dennis Kempind0b722a2014-04-15 11:54:48 -0700376 return [0] if allow_multi else 0
377
378 # repeat until user made a valid selection
379 while True:
380
381 # get user input
382 print msg
383 if allow_zero:
384 print " 0: None"
385 for i, item in enumerate(list):
386 print " ", str(i + 1) + ":", item
387 if allow_multi:
388 print "(Separate multiple selections with spaces)"
389 sys.stdout.write('> ')
390 sys.stdout.flush()
391 selection = sys.stdin.readline()
392
393 # validates single input value
394 def CheckSelection(selection):
395 try:
396 idx = int(selection) - 1
397 if allow_zero and idx == -1:
398 return True
399 if idx < 0 or idx >= len(list):
400 print 'Number out of range'
401 return False
402 except:
403 print 'Not a number'
404 return False
405 return True
406
407 if allow_multi:
408 # validate list of values
409 valid = True
410 selections = selection.split(" ")
411 for selection in selections:
412 if not CheckSelection(selection):
413 valid = False
414 break
415 if valid:
416 selections = [int(s) - 1 for s in selections]
417 if -1 in selections:
418 return []
419 return selections
420 else:
421 if CheckSelection(selection):
422 return int(selection) - 1