blob: e505c6e2c256bc598a8495e15ef34e9615ad28df [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 self._ParseActivityLog(log.activity)
54 if log.evdev:
55 self._ParseEvdevLog(log.evdev)
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
Dennis Kempin17766a62013-06-17 14:09:33 -0700134 if xorg_conf_path and 'xorg' in data and 'file' in data['xorg']:
135 filename = os.path.join(xorg_conf_path, data['xorg']['file'])
Dennis Kempin037675e2013-06-14 14:12:39 -0700136 input_classes = self.xorg_parser.Parse(file=filename)
Dennis Kempin17766a62013-06-17 14:09:33 -0700137 if 'identifier' in data['xorg']:
138 properties = input_classes[data['xorg']['identifier']]
Dennis Kempin037675e2013-06-14 14:12:39 -0700139 self.properties.update(properties)
Dennis Kempin17766a62013-06-17 14:09:33 -0700140 if 'identifiers' in data['xorg']:
141 for identifier in data['xorg']['identifiers']:
Dennis Kempin037675e2013-06-14 14:12:39 -0700142 properties = input_classes[identifier]
143 self.properties.update(properties)
144
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700145 for prop in self.ignore_properties:
146 if prop in self.properties:
147 del self.properties[prop]
148
Dennis Kempin037675e2013-06-14 14:12:39 -0700149 def _UpdateDimensions(self):
Dennis Kempin17766a62013-06-17 14:09:33 -0700150 """ Update x/y min/max with xorg properties.
151
152 CMT allows hardware properties to be overwritten by xorg properties.
153 Do the same in this class.
Dennis Kempin037675e2013-06-14 14:12:39 -0700154 """
Dennis Kempin17766a62013-06-17 14:09:33 -0700155 if 'Active Area Left' in self.properties:
156 self.x_min = int(self.properties['Active Area Left'])
157 if 'Active Area Right' in self.properties:
158 self.x_max = int(self.properties['Active Area Right'])
Dennis Kempin17766a62013-06-17 14:09:33 -0700159 if 'Active Area Top' in self.properties:
160 self.y_min = int(self.properties['Active Area Top'])
161 if 'Active Area Bottom' in self.properties:
162 self.y_max = int(self.properties['Active Area Bottom'])
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700163
164 if 'Horizontal Resolution' in self.properties:
165 self.x_res = int(self.properties['Horizontal Resolution'])
Dennis Kempin17766a62013-06-17 14:09:33 -0700166 if 'Vertical Resolution' in self.properties:
167 self.y_res = int(self.properties['Vertical Resolution'])
Dennis Kempin037675e2013-06-14 14:12:39 -0700168
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700169 if 'SemiMT Non Linear Area Left' in self.properties:
170 self.x_min = int(self.properties['SemiMT Non Linear Area Left'])
171 if 'SemiMT Non Linear Area Right' in self.properties:
172 self.x_max = int(self.properties['SemiMT Non Linear Area Right'])
173 if 'SemiMT Non Linear Area Top' in self.properties:
174 self.y_min = int(self.properties['SemiMT Non Linear Area Top'])
175 if 'SemiMT Non Linear Area Bottom' in self.properties:
176 self.y_max = int(self.properties['SemiMT Non Linear Area Bottom'])
177
178
179 def Match(self, other, loose, debug=False):
Dennis Kempin17766a62013-06-17 14:09:33 -0700180 """ Compare properties and return similarity.
181
182 Compare these properties to another PlatformProperties instance.
183 The return value is a score between 1. 0 meaning there is a big mismatch
184 and 1 meaning the properties match completely.
185 Only a selected range of properties are compared in order to
186 prevent property adjustments to cause platforms to be mismatched.
Dennis Kempin037675e2013-06-14 14:12:39 -0700187 """
188 scores = []
189 def compare(a, b, what):
190 value = abs(float(a) - float(b))
191 if value > 0:
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700192 value = min(1, value / max(abs(float(a)), abs(float(b))))
Dennis Kempin037675e2013-06-14 14:12:39 -0700193 scores.append(1-value)
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700194 if debug:
195 print "%s: %s == %s" % (what, str(a), str(b))
Dennis Kempin037675e2013-06-14 14:12:39 -0700196 def compare_attr(what):
197 compare(getattr(self, what), getattr(other, what), what)
198 def compare_prop(what):
199 if what not in self.properties or what not in other.properties:
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700200 scores.append(0)
201 else:
202 compare(self.properties[what], other.properties[what], what)
203 def check_axis(required, available):
204 for axis in required:
205 if axis not in available:
206 scores.append(0)
207 return
208
Dennis Kempin17766a62013-06-17 14:09:33 -0700209 compare_attr('x_min')
210 compare_attr('x_max')
211 compare_attr('x_res')
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700212 compare_attr('y_min')
213 compare_attr('y_max')
214 compare_attr('y_res')
215 if not loose:
216 compare_prop('Pressure Calibration Offset')
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700217
218 if self.required_axis:
219 if debug:
220 print "axis:", self.required_axis, "in", other.has_axis
221 check_axis(self.required_axis, other.has_axis)
222
223 if other.required_axis:
224 if debug:
225 print "axis:", other.required_axis, "in", self.has_axis
226 check_axis(other.required_axis, self.has_axis)
227
Dennis Kempin037675e2013-06-14 14:12:39 -0700228 return reduce(lambda x,y: (x * y), scores)
229
230class PlatformDatabase(object):
Dennis Kempin17766a62013-06-17 14:09:33 -0700231 """ Class for managing platforms.
232
233 This class reads all available platforms from the platforms_dir and allows
234 to search for matching platforms to an activity log file.
Dennis Kempin037675e2013-06-14 14:12:39 -0700235 """
236 def __init__(self):
237 platform_files = [ f for f in os.listdir(platforms_dir)
Dennis Kempin17766a62013-06-17 14:09:33 -0700238 if f.endswith('.hwprops') ]
Dennis Kempin037675e2013-06-14 14:12:39 -0700239 self.platforms = {}
240 for file in platform_files:
Dennis Kempin17766a62013-06-17 14:09:33 -0700241 name = file.replace('.hwprops', '')
Dennis Kempin037675e2013-06-14 14:12:39 -0700242 self.platforms[name] = PlatformProperties(platform=name)
243
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700244 def FindMatching(self, log, loose=True, debug=False):
Dennis Kempin17766a62013-06-17 14:09:33 -0700245 """ Find platform matching activity_data.
246
247 Returns the PlatformProperties instance of the platform matching
248 the activity log data. This method might terminate the program in
249 case no match can be made, or the match is ambiguous.
Dennis Kempin037675e2013-06-14 14:12:39 -0700250 """
251 result = None
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700252 properties = PlatformProperties(log=log)
Dennis Kempin037675e2013-06-14 14:12:39 -0700253 for name, platform in self.platforms.items():
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700254 if debug:
255 print "#" * 10, name
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700256 score = platform.Match(properties, loose, debug)
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700257 if debug:
258 print name, "score =", score
Dennis Kempin037675e2013-06-14 14:12:39 -0700259 if score > 0.9:
260 if result:
Dennis Kempin55af9cc2013-06-20 15:07:21 -0700261 if loose:
262 if debug:
263 print "-" * 10, "Multiple matches. Try strict"
264 return self.FindMatching(log, False, debug)
Dennis Kempin17766a62013-06-17 14:09:33 -0700265 print ('multiple matching platforms:', result.name,
266 'and', platform.name)
Dennis Kempin19e972b2013-06-20 13:21:38 -0700267 return None
Dennis Kempin037675e2013-06-14 14:12:39 -0700268 result = platform
269 if not result:
Dennis Kempin17766a62013-06-17 14:09:33 -0700270 print 'cannot find matching platform'
Dennis Kempin19e972b2013-06-20 13:21:38 -0700271 return None
Dennis Kempin037675e2013-06-14 14:12:39 -0700272 return result
Dennis Kempind0b722a2014-04-15 11:54:48 -0700273
274 @staticmethod
275 def RegisterPlatformFromDevice(ip):
276 # get list of multitouch devices
277 remote = CrOSRemote(ip)
278 devices = remote.ReadCommand(
279 "/opt/google/input/inputcontrol -t multitouch --names",
280 verbose=True)
281 if devices is False:
282 return None
283
284 # Each line has the format:
285 # id: Device Name
286 # devices[*][0] will have the id
287 # devices[*][1] will have the name
288 devices = devices.splitlines()
289 devices = [l.split(":", 1) for l in devices]
290
291 # select one device from list
292 idx = UserSelection([d[1] for d in devices],
293 "Which device would you like to register?")
294 device_id = devices[idx][0]
295
296 # read hardware properties
297 hwprops = remote.ReadCommand(
298 "/opt/google/input/inputcontrol --id %s --hwprops" % device_id,
299 verbose=True)
300 if not hwprops:
301 print "Please update your device to latest canary or:"
302 print " emerge-${BOARD} inputcontrol"
303 print " cros deploy $DEVICE_IP inputcontrol"
304 return None
305
306 # Asks user to select xorg identifiers from a file
307 def SelectXorgIDs(xorg_file):
308 print "Selecting Xorg identifiers from", xorg_file
309 conf = remote.Read(xorg_file)
310 ids = []
311 for match in re.finditer("Identifier\s+\"([a-zA-Z0-9 ]+)\"", conf):
312 ids.append(match.group(1))
313
314 idxs = UserSelection(ids, "Which xorg identifiers apply to this device?",
315 allow_multi=True, allow_zero=True)
316 return [ids[i] for i in idxs]
317
318 # first ask user for xorg identifiers from platform specific file,
319 # then for identifiers from generic config file.
320 xorg_file = remote.ReadCommand(
321 "ls /etc/X11/xorg.conf.d/50-touchpad-cmt-*",
322 verbose=True).strip()
323 ids = SelectXorgIDs(xorg_file)
324 if not ids:
325 xorg_file = "/etc/X11/xorg.conf.d/40-touchpad-cmt.conf"
326 ids = SelectXorgIDs(xorg_file)
327 if not ids:
328 print "Please configure the platform properties manually"
329 xorg_file = "todo: add correct xorg conf file"
330 ids = ["todo: add correct xorg identifier"]
331
332 ids_string = "[" + ", ".join(["\"%s\"" % id for id in ids]) + "]"
333 xorg_file = os.path.basename(xorg_file)
334
335 sys.stdout.write("Please name this platform: ")
336 sys.stdout.flush()
337 platform_name = sys.stdin.readline().strip()
338
339 # write platform info to files
340 hwprops_file = os.path.join(platforms_dir, platform_name + ".hwprops")
341 props_file = os.path.join(platforms_dir, platform_name + ".props")
342
343 open(hwprops_file, "w").write(hwprops)
344 open(props_file, "w").write(props_template % (xorg_file, ids_string))
345
346 print "Created files: "
347 print " ", hwprops_file
348 print " ", props_file
349
350 return platform_name
351
352
353def UserSelection(list, msg, allow_multi=False, allow_zero=False):
354 idx = [0]
355 if len(list) == 1:
356 return [0] if allow_multi else 0
357
358 # repeat until user made a valid selection
359 while True:
360
361 # get user input
362 print msg
363 if allow_zero:
364 print " 0: None"
365 for i, item in enumerate(list):
366 print " ", str(i + 1) + ":", item
367 if allow_multi:
368 print "(Separate multiple selections with spaces)"
369 sys.stdout.write('> ')
370 sys.stdout.flush()
371 selection = sys.stdin.readline()
372
373 # validates single input value
374 def CheckSelection(selection):
375 try:
376 idx = int(selection) - 1
377 if allow_zero and idx == -1:
378 return True
379 if idx < 0 or idx >= len(list):
380 print 'Number out of range'
381 return False
382 except:
383 print 'Not a number'
384 return False
385 return True
386
387 if allow_multi:
388 # validate list of values
389 valid = True
390 selections = selection.split(" ")
391 for selection in selections:
392 if not CheckSelection(selection):
393 valid = False
394 break
395 if valid:
396 selections = [int(s) - 1 for s in selections]
397 if -1 in selections:
398 return []
399 return selections
400 else:
401 if CheckSelection(selection):
402 return int(selection) - 1