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
7import collections
8import glob
9import os
10import re
11import tempfile
12
13import common_util
14
15from firmware_constants import AXIS
16
17
18# Define AbsAxis class with axis attributes: min, max, and resolution
19AbsAxis = collections.namedtuple('AbsAxis', ['min', 'max', 'resolution'])
20
21
22class 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