blob: 11d22c3b11d3a4bdb662de88d6a4461894f21923 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2020 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import array
import fcntl
import hidtools.hidraw
import sys
import time
# Definitions from USI HID descriptor spec v1.0
FIELD_BITS = {
# Get/Set features:
'color': 8,
'width': 8,
# Diagnostic command feature:
'preamble': 3,
'index': 3,
'command_id': 6,
'command_data': 16,
'crc': 5
}
# Get/Set feature options
STYLES = ['ink', 'pencil', 'highlighter', 'marker', 'brush', 'none']
ALL_BUTTONS = ['unimplemented', 'barrel', 'secondary', 'eraser', 'disabled']
REAL_BUTTONS = ALL_BUTTONS[1:-1]
ASSIGNABLE_BUTTONS = ALL_BUTTONS[1:]
# Diagnostic command constants
DEFAULT_PREAMBLE = 0x4
CRC_POLYNOMIAL = 0b111001
CRC_POLYNOMIAL_LENGTH = 6
# These usages only appear in a single feature report as defined
# by the USI HID descriptor spec v1.0, so we can use them to
# look for the feature reports.
UNIQUE_REPORT_USAGES = {
'color': [
0x5c, # Preferred color
0x5d # Preferred color is locked
],
'width': [
0x5e, # Preferred line width
0x5f # Preferred line width is locked
],
'style': [
0x70, # Preferred line style
0x71, # Preferred line style is locked
0x72, # Ink
0x73, # Pencil
0x74, # Highlighter
0x75, # Chisel Marker
0x76, # Brush
0x77 # No preferred line style
],
'buttons': [
0xa4, # Switch unimplemented
0x44, # Barrel switch
0x5a, # Secondary barrel switch
0x45, # Eraser
0xa3 # Switch disabled
],
'firmware': [
0x90, # Transducer software info
0x91, # Transducer vendor ID
0x92, # Transducer product ID
0x2a # Software version
],
'usi_version': [0x2b], # Protocol version
'diagnostic': [0x80], # Digitizer Diagnostic
'index': [0xa6] # Transducer index selector
}
# ioctl definitions from <ioctl.h>
_IOC_NRBITS = 8
_IOC_TYPEBITS = 8
_IOC_SIZEBITS = 14
_IOC_NRSHIFT = 0
_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS
_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS
_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS
_IOC_WRITE = 1
_IOC_READ = 2
def _IOC(dir, type, nr, size):
return ((dir << _IOC_DIRSHIFT) | (ord(type) << _IOC_TYPESHIFT) |
(nr << _IOC_NRSHIFT) | (size << _IOC_SIZESHIFT))
def HIDIOCSFEATURE(size):
""" set report feature """
return _IOC(_IOC_WRITE | _IOC_READ, 'H', 0x06, size)
def HIDIOCGFEATURE(size):
""" get report feature """
return _IOC(_IOC_WRITE | _IOC_READ, 'H', 0x07, size)
class FeatureNotFoundError(ValueError):
pass
def log(message, end='\n'):
print(message, end=end, file=sys.stderr)
sys.stderr.flush()
def field_max(field_name):
return 2**FIELD_BITS[field_name] - 1
def check_in_range(field_name, value):
min_val = 0
max_val = field_max(field_name)
if value < 0 or value > max_val:
raise ValueError('{} must be between {} and {}, inclusive'.format(
field_name, min_val, max_val))
def find_feature_report_id(rdesc, feature_name):
"""Find the correct report ID for the feature.
Search the report descriptor for a usage unique to the desired feature
report.
"""
report_id = None
usage_found = False
for item in rdesc.rdesc_items:
if item.item == 'Report ID':
report_id = item.value
elif item.item == 'Usage' and item.value in UNIQUE_REPORT_USAGES[
feature_name]:
usage_found = True
elif item.item == 'Feature':
if usage_found and report_id is not None:
return report_id
elif item.item == 'Input' or item.item == 'Output':
usage_found = False
raise FeatureNotFoundError(
'Feature report not found for {}'.format(feature_name))
def set_stylus_index(device, stylus_index):
"""Send a report to set the stylus index.
Tell the controller which stylus index the next 'get feature' command should
return information for.
"""
try:
report_id = find_feature_report_id(device.report_descriptor, 'index')
except FeatureNotFoundError as e:
if stylus_index != 1:
raise e
# Some devices only support one stylus and don't have a feature to set
# the stylus index, so continue with a warning if the desired stylus
# index is 1.
log('WARNING: {}. Proceeding regardless.'.format(str(e)))
return
buf = array.array('B', [report_id, stylus_index])
fcntl.ioctl(device.device.fileno(), HIDIOCSFEATURE(len(buf)), buf)
def get_ioctl(device, buf):
"""Send a 'get feature' ioctl to the device and return the results.
Repeatedly poll until the device gets the information from the stylus,
so the user can run this before pairing the stylus.
"""
while fcntl.ioctl(device.device.fileno(), HIDIOCGFEATURE(len(buf)),
buf) == 0:
log('.', end='')
time.sleep(0.1)
log('')
return (buf)
def get_feature(device, stylus_index, feature_name):
report_id = find_feature_report_id(device.report_descriptor, feature_name)
set_stylus_index(device, stylus_index)
buf = array.array('B', [0] * 64)
buf[0] = report_id
return get_ioctl(device, buf)
def calculate_crc(data):
"""Calculate the cyclic redundancy check.
Calculate the cyclic redundancy check for diagnostic commands according to
the USI spec.
"""
data_len = (
FIELD_BITS['command_data'] +
FIELD_BITS['command_id'] +
FIELD_BITS['index'])
crc_polynomial = CRC_POLYNOMIAL
msb_mask = 1 << (data_len - 1)
crc_polynomial = crc_polynomial << (data_len - CRC_POLYNOMIAL_LENGTH)
for _ in range(data_len):
if data & msb_mask:
data = data ^ crc_polynomial
data = data << 1
return data >> (data_len - (CRC_POLYNOMIAL_LENGTH - 1))
def diagnostic(device, args):
check_in_range('command_id', args.command_id)
check_in_range('command_data', args.data)
check_in_range('index', args.index)
check_in_range('preamble', args.preamble)
command = args.data
command = args.command_id | (command << FIELD_BITS['command_id'])
command = args.index | (command << FIELD_BITS['index'])
if args.crc is None:
crc = calculate_crc(command)
else:
crc = args.crc
check_in_range('crc', args.crc)
full_command = crc
full_command = command | (
full_command << (FIELD_BITS['command_data'] +
FIELD_BITS['command_id'] +
FIELD_BITS['index']))
full_command = args.preamble | (full_command << FIELD_BITS['preamble'])
command_bytes = full_command.to_bytes(8, byteorder='little')
report_id = find_feature_report_id(device.report_descriptor, 'diagnostic')
buf = array.array('B', [report_id] + list(command_bytes))
# Get a feature from the stylus to make sure it is paired.
get_feature(device, args.index, 'color')
fcntl.ioctl(device.device.fileno(), HIDIOCSFEATURE(len(buf)), buf)
return get_ioctl(device, buf)
def format_bytes(buf):
"""Format bytes for printing.
Given a little-endian list of bytes, format them for printing as a single
hex value.
"""
ret = '0x'
for byte in reversed(buf):
ret += '{:02x}'.format(byte)
return ret
def print_diagnostic(buf):
error_bit = (buf[3] & 0x40) >> 6
error_code = buf[3] & 0x3f
print('full response: {}'.format(format_bytes(buf[1:])))
print('error information available? {}'.format(error_bit))
print('error code: 0x{:02x}'.format(error_code))
print('command response: {}'.format(format_bytes(buf[1:3])))
def print_feature(buf, feature_name):
# The first two elements of buf hold the report ID and stylus index,
# which do not need to be printed
if feature_name == 'color':
print('transducer color: {}'.format(buf[2]))
elif feature_name == 'width':
print('transducer width: {}'.format(buf[2]))
elif feature_name == 'style':
style = buf[2]
style_index = style - 1
if 0 <= style_index < len(STYLES):
style_name = STYLES[style_index]
else:
style_name = str(style)
print('transducer style: {}'.format(style_name))
elif feature_name == 'buttons':
for real_button_index in range(len(REAL_BUTTONS)):
real_button_name = REAL_BUTTONS[real_button_index]
virtual_button = buf[2 + real_button_index]
virtual_button_index = virtual_button - 1
if 0 <= virtual_button_index < len(ALL_BUTTONS):
virtual_button_name = ALL_BUTTONS[virtual_button_index]
else:
virtual_button_name = str(virtual_button)
print("'{}' maps to '{}'".format(real_button_name,
virtual_button_name))
elif feature_name == 'firmware':
print('vendor ID: {}'.format(format_bytes(buf[2:4])))
print('product ID: {}'.format(format_bytes(buf[4:12])))
print('firmware version: {}.{}'.format(buf[12], buf[13]))
elif feature_name == 'usi_version':
print('USI version: {}.{}'.format(buf[2], buf[3]))
def set_feature(device, stylus_index, feature_name, value):
buf = get_feature(device, stylus_index, feature_name)
buf[2] = value
fcntl.ioctl(device.device.fileno(), HIDIOCSFEATURE(len(buf)), buf)
def set_number_feature(device, stylus_index, feature_name, value):
try:
check_in_range(feature_name, value)
except ValueError as e:
log('ERROR: {}. Proceeding with setting other features.'.format(str(e)))
return
set_feature(device, stylus_index, feature_name, value)
def set_style_feature(device, stylus_index, style_name):
style = STYLES.index(style_name) + 1
set_feature(device, stylus_index, 'style', style)
def set_buttons_feature(device, stylus_index, mappings):
buf = get_feature(device, stylus_index, 'buttons')
base_offset = 2
for real_button_name, virtual_button_name in mappings:
offset = base_offset + REAL_BUTTONS.index(real_button_name)
virtual_button = ALL_BUTTONS.index(virtual_button_name) + 1
buf[offset] = virtual_button
fcntl.ioctl(device.device.fileno(), HIDIOCSFEATURE(len(buf)), buf)
def set_features(device, args):
if args.color is not None:
set_number_feature(device, args.index, 'color', args.color)
if args.width is not None:
set_number_feature(device, args.index, 'width', args.width)
if args.style is not None:
set_style_feature(device, args.index, args.style)
if args.buttons is not None:
set_buttons_feature(device, args.index, args.buttons)
def parse_arguments():
parser = argparse.ArgumentParser(
description='USI test tool',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(
'-p',
'--path',
default='/dev/hidraw0',
help='the path of the USI device')
subparsers = parser.add_subparsers(dest='command')
descriptor_help = 'print the report descriptor in human-readable format'
subparsers.add_parser(
'descriptor', help=descriptor_help, description=descriptor_help)
data_help = 'print incoming data reports in human-readable format'
subparsers.add_parser('data', help=data_help, description=data_help)
parser_get = subparsers.add_parser(
'get',
help='read a feature from a stylus',
description='Send a HID feature request to the device and print the '
'response. Will wait for the specified stylus index to pair first.')
parser_get.add_argument(
'feature',
choices=[
'color', 'width', 'style', 'buttons', 'firmware', 'usi_version'
],
help='which feature to read')
parser_get.add_argument(
'-i',
'--index',
default=1,
type=int,
help='which stylus to read values for, by index '
'(default: %(default)s)')
parser_set = subparsers.add_parser(
'set',
help='change stylus features',
description='Set a stylus feature via HID feature report. Will wait '
'for the specified stylus index to pair first.')
parser_set.add_argument(
'-i',
'--index',
default=1,
type=int,
help='which stylus to set values for, by index (default: %(default)s)')
parser_set.add_argument(
'-c',
'--color',
type=int,
metavar='[{},{}]'.format(0, field_max('color')),
help='set the stylus color to the specified value '
'in the range ({},{})'.format(0, field_max('color')))
parser_set.add_argument(
'-w',
'--width',
type=int,
metavar='[{},{}]'.format(0, field_max('width')),
help='set the stylus width to the specified value '
'in the range ({},{})'.format(0, field_max('width')))
parser_set.add_argument(
'-s',
'--style',
choices=STYLES,
help='set the stylus style to the specified value')
parser_set.add_argument(
'-b',
'--button',
dest='buttons',
nargs=2,
action='append',
metavar=('{%s}' % ','.join(REAL_BUTTONS),
'{%s}' % ','.join(ASSIGNABLE_BUTTONS)),
help='Remap a button to emulate another. '
'For example: \'usi-test set -b barrel eraser\' will set the '
'barrel button to act as an eraser button. You may set multiple '
'buttons by setting this option multiple times.')
parser_diag = subparsers.add_parser(
'diagnostic',
help='send a diagnostic command',
description='Send the specified diagnostic command to a stylus and '
'read the response. You may set the preamble and cyclic redundancy '
'check manually, but by default the tool will send the preamble and '
'CRC defined by the USI spec.')
parser_diag.add_argument(
'command_id', type=int, help='which diagnostic command to send')
parser_diag.add_argument(
'data', type=int, help='the data to send with the diagnostic command')
parser_diag.add_argument(
'-i',
'--index',
default=1,
type=int,
help='which stylus to send command to, by index (default: %(default)s)')
parser_diag.add_argument(
'-c',
'--crc',
type=int,
help='the custom cyclic redundancy check for the diagnostic command. '
'By default this tool will send the appropriate crc calculated by the '
'algorithm defined in the USI spec.')
parser_diag.add_argument(
'-p',
'--preamble',
type=int,
default=DEFAULT_PREAMBLE,
help='the preamble for the diagnostic command. '
'By default this tool will send the preamble defined in the USI spec')
args = parser.parse_args()
return args
def main():
args = parse_arguments()
fd = open(args.path, 'rb+')
device = hidtools.hidraw.HidrawDevice(fd)
if args.command == 'descriptor':
device.dump(sys.stdout)
elif args.command == 'data':
while True:
device.dump(sys.stdout)
device.read_events()
elif args.command == 'get':
buf = get_feature(device, args.index, args.feature)
print_feature(buf, args.feature)
elif args.command == 'set':
set_features(device, args)
elif args.command == 'diagnostic':
buf = diagnostic(device, args)
print_diagnostic(buf)
if __name__ == '__main__':
main()