1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 """Touch device module provides some touch device related attributes.""" 6 7 import collections 8 import glob 9 import os 10 import re 11 import tempfile 12 13 import common_util 14 15 from firmware_constants import AXIS 16 17 18 # Define AbsAxis class with axis attributes: min, max, and resolution 19 AbsAxis = collections.namedtuple('AbsAxis', ['min', 'max', 'resolution']) 20 21 22 class TouchDevice: 23 """A class about touch device properties.""" 24 def __init__(self, device_node=None, is_touchscreen=False, 25 device_description_file=None): 26 """If the device_description_file is provided (i.e., not None), it is 27 used to create a mocked device for the purpose of replaying or 28 unit tests. 29 """ 30 self.device_node = (device_node if device_node else 31 self.get_device_node(is_touchscreen=is_touchscreen)) 32 self.device_description = self._get_device_description( 33 device_description_file) 34 self.axis_x, self.axis_y = self.parse_abs_axes() 35 self.axes = {AXIS.X: self.axis_x, AXIS.Y: self.axis_y} 36 37 @staticmethod 38 def xinput_helper(cmd): 39 """A helper of xinput.sh to execute a command. 40 41 This is a method copied from factory/py/test/utils.py 42 """ 43 dummy_script = '. /opt/google/input/xinput.sh\n%s' 44 with tempfile.NamedTemporaryFile(prefix='cros_touch_xinput.') as f: 45 f.write(dummy_script % cmd) 46 f.flush() 47 return common_util.simple_system_output('sh %s' % f.name) 48 49 @staticmethod 50 def get_device_node(is_touchscreen=False): 51 """Get the touch device node through xinput. 52 53 Touchscreens have a different device name, so this 54 chooses between them. Otherwise they are the same. 55 56 A device id is a simple integer say 12 extracted from a string like 57 Atmel maXTouch Touchscreen id=12 [floating slave] 58 59 A device node is extracted from "xinput list-props device_id" and 60 looks like 61 Device Node (250): "/dev/input/event8" 62 In this example, the device node is /dev/input/event8 63 """ 64 device_id = TouchDevice.xinput_helper( 65 'list_touchscreens' if is_touchscreen else 'list_touchpads') 66 if device_id: 67 device_node = TouchDevice.xinput_helper( 68 'device_get_prop %s "Device Node"' % device_id).strip('"') 69 else: 70 device_node = None 71 return device_node 72 73 def exists(self): 74 """Indicate whether this device exists or not. 75 76 Note that the device description is derived either from the provided 77 device description file or from the system device node. 78 """ 79 return bool(self.device_description) 80 81 def get_dimensions_in_mm(self): 82 """Get the width and height in mm of the device.""" 83 (left, right, top, bottom, 84 resolution_x, resolution_y) = self.get_resolutions() 85 width = float((right - left)) / resolution_x 86 height = float((bottom - top)) / resolution_y 87 return (width, height) 88 89 def get_resolutions(self): 90 """Get the resolutions in x and y axis of the device.""" 91 return (self.axis_x.resolution, self.axis_y.resolution) 92 93 def get_edges(self): 94 """Get the left, right, top, and bottom edges of the device.""" 95 return (self.axis_x.min, self.axis_x.max, 96 self.axis_y.min, self.axis_y.max) 97 98 def save_device_description_file(self, filepath, board): 99 """Save the device description file in the specified filepath.""" 100 if self.device_description: 101 # Replace the device name with the board name to reduce the risk 102 # of leaking the touch device name which may be confidential. 103 # Take the touchpad on link as an example: 104 # N: Atmel-maXTouch-Touchpad would be replaced with 105 # N: link-touch-device 106 name = 'N: %s-touch-device\n' % board 107 try: 108 with open(filepath, 'w') as fo: 109 for line in self.device_description.splitlines(): 110 fo.write(name if line.startswith('N:') else line + '\n') 111 return True 112 except Exception as e: 113 msg = 'Error: %s in getting device description from %s' 114 print msg % (e, self.device_node) 115 return False 116 117 def _get_device_description(self, device_description_file): 118 """Get the device description either from the specified device 119 description file or from the system device node. 120 """ 121 if device_description_file: 122 # Get the device description from the device description file. 123 try: 124 with open(device_description_file) as dd: 125 return dd.read() 126 except Exception as e: 127 msg = 'Error: %s in opening the device description file: %s' 128 print msg % (e, device_description_file) 129 elif self.device_node: 130 # Get the device description from the device node. 131 cmd = 'evemu-describe %s' % self.device_node 132 try: 133 return common_util.simple_system_output(cmd) 134 except Exception as e: 135 msg = 'Error: %s in getting the device description from %s' 136 print msg % (e, self.device_node) 137 return None 138 139 def parse_abs_axes(self): 140 """Prase to get information about min, max, and resolution of 141 ABS_X and ABS_Y 142 143 Example of ABS_X: 144 A: 00 0 1280 0 0 12 145 Example of ABS_y: 146 A: 01 0 1280 0 0 12 147 """ 148 pattern = 'A:\s*%s\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)' 149 pattern_x = pattern % '00' 150 pattern_y = pattern % '01' 151 axis_x = axis_y = None 152 if self.device_description: 153 for line in self.device_description.splitlines(): 154 if not axis_x: 155 result = re.search(pattern_x, line, re.I) 156 if result: 157 min_x = int(result.group(1)) 158 max_x = int(result.group(2)) 159 resolution_x = int(result.group(5)) 160 axis_x = AbsAxis(min_x, max_x, resolution_x) 161 if not axis_y: 162 result = re.search(pattern_y, line, re.I) 163 if result: 164 min_y = int(result.group(1)) 165 max_y = int(result.group(2)) 166 resolution_y = int(result.group(5)) 167 axis_y = AbsAxis(min_y, max_y, resolution_y) 168 return (axis_x, axis_y) 169 170 def pixel_to_mm(self, (pixel_x, pixel_y)): 171 """Convert the point coordinate from pixel to mm.""" 172 mm_x = float(pixel_x - self.axis_x.min) / self.axis_x.resolution 173 mm_y = float(pixel_y - self.axis_y.min) / self.axis_y.resolution 174 return (mm_x, mm_y) 175 176 def pixel_to_mm_single_axis(self, value_pixel, axis): 177 """Convert the coordinate from pixel to mm.""" 178 value_mm = float(value_pixel - axis.min) / axis.resolution 179 return value_mm 180 181 def pixel_to_mm_single_axis_by_name(self, value_pixel, axis_name): 182 """Convert the coordinate from pixel to mm.""" 183 return self.pixel_to_mm_single_axis(value_pixel, self.axes[axis_name]) 184 185 def get_dimensions(self): 186 """Get the vendor-specified dimensions of the touch device.""" 187 return (self.axis_x.max - self.axis_x.min, 188 self.axis_y.max - self.axis_y.min) 189 190 def get_display_geometry(self, screen_size, display_ratio): 191 """Get a preferred display geometry when running the test.""" 192 display_ratio = 0.8 193 dev_width, dev_height = self.get_dimensions() 194 screen_width, screen_height = screen_size 195 196 if 1.0 * screen_width / screen_height <= 1.0 * dev_width / dev_height: 197 disp_width = int(screen_width * display_ratio) 198 disp_height = int(disp_width * dev_height / dev_width) 199 disp_offset_x = 0 200 disp_offset_y = screen_height - disp_height 201 else: 202 disp_height = int(screen_height * display_ratio) 203 disp_width = int(disp_height * dev_width / dev_height) 204 disp_offset_x = 0 205 disp_offset_y = screen_height - disp_height 206 207 return (disp_width, disp_height, disp_offset_x, disp_offset_y) 208 209 def _touch_input_name_re_str(self): 210 pattern_str = ('touchpad', 'trackpad') 211 return '(?:%s)' % '|'.join(pattern_str) 212 213 def get_touch_input_dir(self): 214 """Get touch device input directory.""" 215 input_root_dir = '/sys/class/input' 216 input_dirs = glob.glob(os.path.join(input_root_dir, 'input*')) 217 re_pattern = re.compile(self._touch_input_name_re_str(), re.I) 218 for input_dir in input_dirs: 219 filename = os.path.join(input_dir, 'name') 220 if os.path.isfile(filename): 221 with open(filename) as f: 222 for line in f: 223 if re_pattern.search(line) is not None: 224 return input_dir 225 return None 226 227 def get_firmware_version(self): 228 """Probe the firmware version.""" 229 input_dir = self.get_touch_input_dir() 230 device_dir = 'device' 231 232 # Get the re search pattern for firmware_version file name 233 fw_list = ('firmware', 'fw') 234 ver_list = ('version', 'id') 235 sep_list = ('_', '-') 236 re_str = '%s%s%s' % ('(?:%s)' % '|'.join(fw_list), 237 '(?:%s)' % '|'.join(sep_list), 238 '(?:%s)' % '|'.join(ver_list)) 239 re_pattern = re.compile(re_str, re.I) 240 241 if input_dir is not None: 242 device_dir = os.path.join(input_dir, 'device', '*') 243 for f in glob.glob(device_dir): 244 if os.path.isfile(f) and re_pattern.search(f): 245 with open (f) as f: 246 for line in f: 247 return line.strip('\n') 248 return 'unknown' 249