# Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Implements raw HID interface on Linux using SysFS and device files.""" from __future__ import absolute_import import os import struct import six from . import base, errors REPORT_DESCRIPTOR_KEY_MASK = 0xfc LONG_ITEM_ENCODING = 0xfe OUTPUT_ITEM = 0x90 INPUT_ITEM = 0x80 COLLECTION_ITEM = 0xa0 REPORT_COUNT = 0x94 REPORT_SIZE = 0x74 USAGE_PAGE = 0x04 USAGE = 0x08 def GetValueLength(rd, pos): """Get value length for a key in rd. For a key at position pos in the Report Descriptor rd, return the length of the associated value. This supports both short and long format values. Args: rd: Report Descriptor pos: The position of the key in rd. Returns: (key_size, data_len) where key_size is the number of bytes occupied by the key and data_len is the length of the value associated by the key. """ key = six.indexbytes(rd, pos) if key == LONG_ITEM_ENCODING: # If the key is tagged as a long item (0xfe), then the format is # [key (1 byte)] [data len (1 byte)] [item tag (1 byte)] [data (n # bytes)]. # Thus, the entire key record is 3 bytes long. if pos + 1 < len(rd): return (3, rd[pos + 1]) else: raise errors.HidError('Malformed report descriptor') else: # If the key is tagged as a short item, then the item tag and data len are # packed into one byte. The format is thus: # [tag (high 4 bits)] [type (2 bits)] [size code (2 bits)] [data (n bytes)]. # The size code specifies 1,2, or 4 bytes (0x03 means 4 bytes). code = key & 0x03 if code <= 0x02: return (1, code) elif code == 0x03: return (1, 4) raise errors.HidError('Cannot happen') def ReadLsbBytes(rd, offset, value_size): """Reads value_size bytes from rd at offset, least signifcant byte first.""" encoding = None if value_size == 1: encoding = '= pos + 1 + value_length: report_count = ReadLsbBytes(rd, pos + 1, value_length) elif key & REPORT_DESCRIPTOR_KEY_MASK == REPORT_SIZE: if len(rd) >= pos + 1 + value_length: report_size = ReadLsbBytes(rd, pos + 1, value_length) elif key & REPORT_DESCRIPTOR_KEY_MASK == USAGE_PAGE: if len(rd) >= pos + 1 + value_length: usage_page = ReadLsbBytes(rd, pos + 1, value_length) elif key & REPORT_DESCRIPTOR_KEY_MASK == USAGE: if len(rd) >= pos + 1 + value_length: usage = ReadLsbBytes(rd, pos + 1, value_length) pos += value_length + key_size return desc def ParseUevent(uevent, desc): lines = uevent.split(b'\n') for line in lines: line = line.strip() if not line: continue k, v = line.split(b'=') if k == b'HID_NAME': desc.product_string = v.decode('utf8') elif k == b'HID_ID': _, vid, pid = v.split(b':') desc.vendor_id = int(vid, 16) desc.product_id = int(pid, 16) class LinuxHidDevice(base.HidDevice): """Implementation of HID device for linux. Implementation of HID device interface for linux that uses block devices to interact with the device and sysfs to enumerate/discover device metadata. """ @staticmethod def Enumerate(): for hidraw in os.listdir('/sys/class/hidraw'): rd_path = ( os.path.join( '/sys/class/hidraw', hidraw, 'device/report_descriptor')) uevent_path = os.path.join('/sys/class/hidraw', hidraw, 'device/uevent') rd_file = open(rd_path, 'rb') uevent_file = open(uevent_path, 'rb') desc = base.DeviceDescriptor() desc.path = os.path.join('/dev/', hidraw) ParseReportDescriptor(rd_file.read(), desc) ParseUevent(uevent_file.read(), desc) rd_file.close() uevent_file.close() yield desc.ToPublicDict() def __init__(self, path): base.HidDevice.__init__(self, path) self.dev = os.open(path, os.O_RDWR) self.desc = base.DeviceDescriptor() self.desc.path = path rd_file = open(os.path.join('/sys/class/hidraw', os.path.basename(path), 'device/report_descriptor'), 'rb') ParseReportDescriptor(rd_file.read(), self.desc) rd_file.close() def GetInReportDataLength(self): """See base class.""" return self.desc.internal_max_in_report_len def GetOutReportDataLength(self): """See base class.""" return self.desc.internal_max_out_report_len def Write(self, packet): """See base class.""" out = bytes(bytearray([0] + packet)) # Prepend the zero-byte (report ID) os.write(self.dev, out) def Read(self): """See base class.""" raw_in = os.read(self.dev, self.GetInReportDataLength()) decoded_in = list(six.iterbytes(raw_in)) return decoded_in