blob: 11d22c3b11d3a4bdb662de88d6a4461894f21923 [file] [log] [blame]
Sean O'Brien2c473012020-09-09 16:50:32 -07001#!/usr/bin/env python3
2# Copyright 2020 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import argparse
7import array
8import fcntl
9import hidtools.hidraw
10import sys
11import time
12
13# Definitions from USI HID descriptor spec v1.0
14FIELD_BITS = {
15 # Get/Set features:
16 'color': 8,
17 'width': 8,
18 # Diagnostic command feature:
19 'preamble': 3,
20 'index': 3,
21 'command_id': 6,
22 'command_data': 16,
23 'crc': 5
24}
25
26# Get/Set feature options
27STYLES = ['ink', 'pencil', 'highlighter', 'marker', 'brush', 'none']
28ALL_BUTTONS = ['unimplemented', 'barrel', 'secondary', 'eraser', 'disabled']
29REAL_BUTTONS = ALL_BUTTONS[1:-1]
30ASSIGNABLE_BUTTONS = ALL_BUTTONS[1:]
31
32# Diagnostic command constants
33DEFAULT_PREAMBLE = 0x4
34CRC_POLYNOMIAL = 0b111001
35CRC_POLYNOMIAL_LENGTH = 6
36
37# These usages only appear in a single feature report as defined
38# by the USI HID descriptor spec v1.0, so we can use them to
39# look for the feature reports.
40UNIQUE_REPORT_USAGES = {
41 'color': [
42 0x5c, # Preferred color
43 0x5d # Preferred color is locked
44 ],
45 'width': [
46 0x5e, # Preferred line width
47 0x5f # Preferred line width is locked
48 ],
49 'style': [
50 0x70, # Preferred line style
51 0x71, # Preferred line style is locked
52 0x72, # Ink
53 0x73, # Pencil
54 0x74, # Highlighter
55 0x75, # Chisel Marker
56 0x76, # Brush
57 0x77 # No preferred line style
58 ],
59 'buttons': [
60 0xa4, # Switch unimplemented
61 0x44, # Barrel switch
62 0x5a, # Secondary barrel switch
63 0x45, # Eraser
64 0xa3 # Switch disabled
65 ],
66 'firmware': [
67 0x90, # Transducer software info
68 0x91, # Transducer vendor ID
69 0x92, # Transducer product ID
70 0x2a # Software version
71 ],
72 'usi_version': [0x2b], # Protocol version
73 'diagnostic': [0x80], # Digitizer Diagnostic
74 'index': [0xa6] # Transducer index selector
75}
76
77# ioctl definitions from <ioctl.h>
78_IOC_NRBITS = 8
79_IOC_TYPEBITS = 8
80_IOC_SIZEBITS = 14
81
82_IOC_NRSHIFT = 0
83_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS
84_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS
85_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS
86
87_IOC_WRITE = 1
88_IOC_READ = 2
89
90
91def _IOC(dir, type, nr, size):
92 return ((dir << _IOC_DIRSHIFT) | (ord(type) << _IOC_TYPESHIFT) |
93 (nr << _IOC_NRSHIFT) | (size << _IOC_SIZESHIFT))
94
95
96def HIDIOCSFEATURE(size):
97 """ set report feature """
98 return _IOC(_IOC_WRITE | _IOC_READ, 'H', 0x06, size)
99
100
101def HIDIOCGFEATURE(size):
102 """ get report feature """
103 return _IOC(_IOC_WRITE | _IOC_READ, 'H', 0x07, size)
104
105
106class FeatureNotFoundError(ValueError):
107 pass
108
109
110def log(message, end='\n'):
111 print(message, end=end, file=sys.stderr)
112 sys.stderr.flush()
113
114
115def field_max(field_name):
116 return 2**FIELD_BITS[field_name] - 1
117
118
119def check_in_range(field_name, value):
120 min_val = 0
121 max_val = field_max(field_name)
122 if value < 0 or value > max_val:
123 raise ValueError('{} must be between {} and {}, inclusive'.format(
124 field_name, min_val, max_val))
125
126
127def find_feature_report_id(rdesc, feature_name):
128 """Find the correct report ID for the feature.
129
130 Search the report descriptor for a usage unique to the desired feature
131 report.
132 """
133 report_id = None
134 usage_found = False
135 for item in rdesc.rdesc_items:
136 if item.item == 'Report ID':
137 report_id = item.value
138 elif item.item == 'Usage' and item.value in UNIQUE_REPORT_USAGES[
139 feature_name]:
140 usage_found = True
141 elif item.item == 'Feature':
142 if usage_found and report_id is not None:
143 return report_id
144 elif item.item == 'Input' or item.item == 'Output':
145 usage_found = False
146
147 raise FeatureNotFoundError(
148 'Feature report not found for {}'.format(feature_name))
149
150
151def set_stylus_index(device, stylus_index):
152 """Send a report to set the stylus index.
153
154 Tell the controller which stylus index the next 'get feature' command should
155 return information for.
156 """
157 try:
158 report_id = find_feature_report_id(device.report_descriptor, 'index')
159 except FeatureNotFoundError as e:
160 if stylus_index != 1:
161 raise e
162 # Some devices only support one stylus and don't have a feature to set
163 # the stylus index, so continue with a warning if the desired stylus
164 # index is 1.
165 log('WARNING: {}. Proceeding regardless.'.format(str(e)))
166 return
167 buf = array.array('B', [report_id, stylus_index])
168 fcntl.ioctl(device.device.fileno(), HIDIOCSFEATURE(len(buf)), buf)
169
170
171def get_ioctl(device, buf):
172 """Send a 'get feature' ioctl to the device and return the results.
173
174 Repeatedly poll until the device gets the information from the stylus,
175 so the user can run this before pairing the stylus.
176 """
177 while fcntl.ioctl(device.device.fileno(), HIDIOCGFEATURE(len(buf)),
178 buf) == 0:
179 log('.', end='')
180 time.sleep(0.1)
181 log('')
182 return (buf)
183
184
185def get_feature(device, stylus_index, feature_name):
186 report_id = find_feature_report_id(device.report_descriptor, feature_name)
187 set_stylus_index(device, stylus_index)
188
189 buf = array.array('B', [0] * 64)
190 buf[0] = report_id
191
192 return get_ioctl(device, buf)
193
194
195def calculate_crc(data):
196 """Calculate the cyclic redundancy check.
197
198 Calculate the cyclic redundancy check for diagnostic commands according to
199 the USI spec.
200 """
201 data_len = (
202 FIELD_BITS['command_data'] +
203 FIELD_BITS['command_id'] +
204 FIELD_BITS['index'])
205 crc_polynomial = CRC_POLYNOMIAL
206 msb_mask = 1 << (data_len - 1)
207 crc_polynomial = crc_polynomial << (data_len - CRC_POLYNOMIAL_LENGTH)
208 for _ in range(data_len):
209 if data & msb_mask:
210 data = data ^ crc_polynomial
211 data = data << 1
212 return data >> (data_len - (CRC_POLYNOMIAL_LENGTH - 1))
213
214
215def diagnostic(device, args):
216 check_in_range('command_id', args.command_id)
217 check_in_range('command_data', args.data)
218 check_in_range('index', args.index)
219 check_in_range('preamble', args.preamble)
220
221 command = args.data
222 command = args.command_id | (command << FIELD_BITS['command_id'])
223 command = args.index | (command << FIELD_BITS['index'])
224
225 if args.crc is None:
226 crc = calculate_crc(command)
227 else:
228 crc = args.crc
229 check_in_range('crc', args.crc)
230
231 full_command = crc
232 full_command = command | (
233 full_command << (FIELD_BITS['command_data'] +
234 FIELD_BITS['command_id'] +
235 FIELD_BITS['index']))
236 full_command = args.preamble | (full_command << FIELD_BITS['preamble'])
237 command_bytes = full_command.to_bytes(8, byteorder='little')
238
239 report_id = find_feature_report_id(device.report_descriptor, 'diagnostic')
240 buf = array.array('B', [report_id] + list(command_bytes))
241
242 # Get a feature from the stylus to make sure it is paired.
243 get_feature(device, args.index, 'color')
244 fcntl.ioctl(device.device.fileno(), HIDIOCSFEATURE(len(buf)), buf)
245
246 return get_ioctl(device, buf)
247
248
249def format_bytes(buf):
250 """Format bytes for printing.
251
252 Given a little-endian list of bytes, format them for printing as a single
253 hex value.
254 """
255 ret = '0x'
256 for byte in reversed(buf):
257 ret += '{:02x}'.format(byte)
258 return ret
259
260
261def print_diagnostic(buf):
262 error_bit = (buf[3] & 0x40) >> 6
263 error_code = buf[3] & 0x3f
264 print('full response: {}'.format(format_bytes(buf[1:])))
265 print('error information available? {}'.format(error_bit))
266 print('error code: 0x{:02x}'.format(error_code))
267 print('command response: {}'.format(format_bytes(buf[1:3])))
268
269
270def print_feature(buf, feature_name):
271 # The first two elements of buf hold the report ID and stylus index,
272 # which do not need to be printed
273 if feature_name == 'color':
274 print('transducer color: {}'.format(buf[2]))
275 elif feature_name == 'width':
276 print('transducer width: {}'.format(buf[2]))
277 elif feature_name == 'style':
278 style = buf[2]
279 style_index = style - 1
280 if 0 <= style_index < len(STYLES):
281 style_name = STYLES[style_index]
282 else:
283 style_name = str(style)
284 print('transducer style: {}'.format(style_name))
285 elif feature_name == 'buttons':
286 for real_button_index in range(len(REAL_BUTTONS)):
287 real_button_name = REAL_BUTTONS[real_button_index]
288 virtual_button = buf[2 + real_button_index]
289 virtual_button_index = virtual_button - 1
290 if 0 <= virtual_button_index < len(ALL_BUTTONS):
291 virtual_button_name = ALL_BUTTONS[virtual_button_index]
292 else:
293 virtual_button_name = str(virtual_button)
294 print("'{}' maps to '{}'".format(real_button_name,
295 virtual_button_name))
296 elif feature_name == 'firmware':
297 print('vendor ID: {}'.format(format_bytes(buf[2:4])))
298 print('product ID: {}'.format(format_bytes(buf[4:12])))
299 print('firmware version: {}.{}'.format(buf[12], buf[13]))
300 elif feature_name == 'usi_version':
301 print('USI version: {}.{}'.format(buf[2], buf[3]))
302
303
304def set_feature(device, stylus_index, feature_name, value):
305 buf = get_feature(device, stylus_index, feature_name)
306 buf[2] = value
307 fcntl.ioctl(device.device.fileno(), HIDIOCSFEATURE(len(buf)), buf)
308
309
310def set_number_feature(device, stylus_index, feature_name, value):
311 try:
312 check_in_range(feature_name, value)
313 except ValueError as e:
314 log('ERROR: {}. Proceeding with setting other features.'.format(str(e)))
315 return
316 set_feature(device, stylus_index, feature_name, value)
317
318
319def set_style_feature(device, stylus_index, style_name):
320 style = STYLES.index(style_name) + 1
321 set_feature(device, stylus_index, 'style', style)
322
323
324def set_buttons_feature(device, stylus_index, mappings):
325 buf = get_feature(device, stylus_index, 'buttons')
326 base_offset = 2
327 for real_button_name, virtual_button_name in mappings:
328 offset = base_offset + REAL_BUTTONS.index(real_button_name)
329 virtual_button = ALL_BUTTONS.index(virtual_button_name) + 1
330 buf[offset] = virtual_button
331 fcntl.ioctl(device.device.fileno(), HIDIOCSFEATURE(len(buf)), buf)
332
333
334def set_features(device, args):
335 if args.color is not None:
336 set_number_feature(device, args.index, 'color', args.color)
337 if args.width is not None:
338 set_number_feature(device, args.index, 'width', args.width)
339 if args.style is not None:
340 set_style_feature(device, args.index, args.style)
341 if args.buttons is not None:
342 set_buttons_feature(device, args.index, args.buttons)
343
344
345def parse_arguments():
346 parser = argparse.ArgumentParser(
347 description='USI test tool',
348 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
349 parser.add_argument(
350 '-p',
351 '--path',
352 default='/dev/hidraw0',
353 help='the path of the USI device')
354 subparsers = parser.add_subparsers(dest='command')
355
356 descriptor_help = 'print the report descriptor in human-readable format'
357 subparsers.add_parser(
358 'descriptor', help=descriptor_help, description=descriptor_help)
359
360 data_help = 'print incoming data reports in human-readable format'
361 subparsers.add_parser('data', help=data_help, description=data_help)
362
363 parser_get = subparsers.add_parser(
364 'get',
365 help='read a feature from a stylus',
366 description='Send a HID feature request to the device and print the '
367 'response. Will wait for the specified stylus index to pair first.')
368 parser_get.add_argument(
369 'feature',
370 choices=[
371 'color', 'width', 'style', 'buttons', 'firmware', 'usi_version'
372 ],
373 help='which feature to read')
374 parser_get.add_argument(
375 '-i',
376 '--index',
377 default=1,
378 type=int,
379 help='which stylus to read values for, by index '
380 '(default: %(default)s)')
381
382 parser_set = subparsers.add_parser(
383 'set',
384 help='change stylus features',
385 description='Set a stylus feature via HID feature report. Will wait '
386 'for the specified stylus index to pair first.')
387 parser_set.add_argument(
388 '-i',
389 '--index',
390 default=1,
391 type=int,
392 help='which stylus to set values for, by index (default: %(default)s)')
393 parser_set.add_argument(
394 '-c',
395 '--color',
396 type=int,
397 metavar='[{},{}]'.format(0, field_max('color')),
398 help='set the stylus color to the specified value '
399 'in the range ({},{})'.format(0, field_max('color')))
400 parser_set.add_argument(
401 '-w',
402 '--width',
403 type=int,
404 metavar='[{},{}]'.format(0, field_max('width')),
405 help='set the stylus width to the specified value '
406 'in the range ({},{})'.format(0, field_max('width')))
407 parser_set.add_argument(
408 '-s',
409 '--style',
410 choices=STYLES,
411 help='set the stylus style to the specified value')
412 parser_set.add_argument(
413 '-b',
414 '--button',
415 dest='buttons',
416 nargs=2,
417 action='append',
418 metavar=('{%s}' % ','.join(REAL_BUTTONS),
419 '{%s}' % ','.join(ASSIGNABLE_BUTTONS)),
420 help='Remap a button to emulate another. '
421 'For example: \'usi-test set -b barrel eraser\' will set the '
422 'barrel button to act as an eraser button. You may set multiple '
423 'buttons by setting this option multiple times.')
424
425 parser_diag = subparsers.add_parser(
426 'diagnostic',
427 help='send a diagnostic command',
428 description='Send the specified diagnostic command to a stylus and '
429 'read the response. You may set the preamble and cyclic redundancy '
430 'check manually, but by default the tool will send the preamble and '
431 'CRC defined by the USI spec.')
432 parser_diag.add_argument(
433 'command_id', type=int, help='which diagnostic command to send')
434 parser_diag.add_argument(
435 'data', type=int, help='the data to send with the diagnostic command')
436 parser_diag.add_argument(
437 '-i',
438 '--index',
439 default=1,
440 type=int,
441 help='which stylus to send command to, by index (default: %(default)s)')
442 parser_diag.add_argument(
443 '-c',
444 '--crc',
445 type=int,
446 help='the custom cyclic redundancy check for the diagnostic command. '
447 'By default this tool will send the appropriate crc calculated by the '
448 'algorithm defined in the USI spec.')
449 parser_diag.add_argument(
450 '-p',
451 '--preamble',
452 type=int,
453 default=DEFAULT_PREAMBLE,
454 help='the preamble for the diagnostic command. '
455 'By default this tool will send the preamble defined in the USI spec')
456
457 args = parser.parse_args()
458
459 return args
460
461
462def main():
463 args = parse_arguments()
464
465 fd = open(args.path, 'rb+')
466 device = hidtools.hidraw.HidrawDevice(fd)
467
468 if args.command == 'descriptor':
469 device.dump(sys.stdout)
470 elif args.command == 'data':
471 while True:
472 device.dump(sys.stdout)
473 device.read_events()
474 elif args.command == 'get':
475 buf = get_feature(device, args.index, args.feature)
476 print_feature(buf, args.feature)
477 elif args.command == 'set':
478 set_features(device, args)
479 elif args.command == 'diagnostic':
480 buf = diagnostic(device, args)
481 print_diagnostic(buf)
482
483
484if __name__ == '__main__':
485 main()