Browse Source

commit first working code, with descriptor parsers/emitters

main
Kate Temkin 5 years ago
parent
commit
708f838dc1
11 changed files with 823 additions and 173 deletions
  1. +1
    -1
      setup.py
  2. +0
    -163
      usb-protocol/types/descriptors/standard.py
  3. +0
    -0
      usb_protocol/__init__.py
  4. +94
    -0
      usb_protocol/emitters/__init__.py
  5. +68
    -0
      usb_protocol/emitters/descriptor.py
  6. +0
    -0
      usb_protocol/emitters/descriptors/__init__.py
  7. +171
    -0
      usb_protocol/emitters/descriptors/standard.py
  8. +225
    -0
      usb_protocol/types/__init__.py
  9. +46
    -9
      usb_protocol/types/descriptor.py
  10. +0
    -0
      usb_protocol/types/descriptors/__init__.py
  11. +218
    -0
      usb_protocol/types/descriptors/standard.py

+ 1
- 1
setup.py View File

@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
setup(

# Vitals
name='usb-protocol',
name='usb_protocol',
license='BSD',
url='https://github.com/usb-tool/luna',
author='Katherine J. Temkin',


+ 0
- 163
usb-protocol/types/descriptors/standard.py View File

@@ -1,163 +0,0 @@
#
# This file is part of usb-protocol.
#
""" Structures describing standard USB descriptors. """

import unittest

import construct
from construct import this

from ..descriptor import DescriptorField, DescriptorNumber, DescriptorFormat

DeviceDescriptor = DescriptorFormat(
"bLength" / DescriptorField("Length"),
"bDescriptorType" / DescriptorNumber(1),
"bcdUSB" / DescriptorField("USB Version"),
"bDeviceClass" / DescriptorField("Class"),
"bDeviceSubclass" / DescriptorField("Subclass"),
"bDeviceProtocol" / DescriptorField("Protocol"),
"bMaxPacketSize0" / DescriptorField("EP0 Max Pkt Size"),
"idVendor" / DescriptorField("Vendor ID"),
"idProduct" / DescriptorField("Product ID"),
"bcdDevice" / DescriptorField("Device Version"),
"iManufacturer" / DescriptorField("Manufacturer Str"),
"iProduct" / DescriptorField("Product Str"),
"iSerialNumber" / DescriptorField("Serial Number"),
"bNumConfigurations" / DescriptorField("Configuration Count"),
)


ConfigurationDescriptor = DescriptorFormat(
"bLength" / DescriptorField("Length"),
"bDescriptorType" / DescriptorNumber(2),
"wTotalLength" / DescriptorField("Length including subordinates"),
"bNumInterfaces" / DescriptorField("Interface count"),
"bConfigurationValue" / DescriptorField("Configuration number"),
"iConfiguration" / DescriptorField("Description string"),
"bmAttributes" / DescriptorField("Attributes"),
"bMaxPower" / DescriptorField("Max power consumption"),
)


StringDescriptor = DescriptorFormat(
"bLength" / DescriptorField("Length"),
"bDescriptorType" / DescriptorNumber(3),
"bString" / construct.PaddedString(this.bLength - 2, "utf_16_le")
)


InterfaceDescriptor = DescriptorFormat(
"bLength" / DescriptorField("Length"),
"bDescriptorType" / DescriptorNumber(4),
"bInterfaceNumber" / DescriptorField("Interface number"),
"bAlternateSetting" / DescriptorField("Alternate setting"),
"bNumEndpoints" / DescriptorField("Endpoints included"),
"bInterfaceClass" / DescriptorField("Class"),
"bInterfaceSubclass" / DescriptorField("Subclass"),
"bInterfaceProtocol" / DescriptorField("Protocol"),
"iInterface" / DescriptorField("String index"),
)


EndpointDescriptor = DescriptorFormat(
"bLength" / DescriptorField("Length"),
"bDescriptorType" / DescriptorNumber(5),
"bEndpointAddress" / DescriptorField("Endpoint Address"),
"bmAttributes" / DescriptorField("Attributes"),
"wMaxPacketSize" / DescriptorField("Maximum Packet Size"),
"bInterval" / DescriptorField("Polling interval"),
)


DeviceQualifierDescriptor = DescriptorFormat(
"bLength" / DescriptorField("Length"),
"bDescriptorType" / DescriptorNumber(6),
"bcdUSB" / DescriptorField("USB Version"),
"bDeviceClass" / DescriptorField("Class"),
"bDeviceSubclass" / DescriptorField("Subclass"),
"bDeviceProtocol" / DescriptorField("Protocol"),
"bMaxPacketSize0" / DescriptorField("EP0 Max Pkt Size"),
"bNumConfigurations" / DescriptorField("Configuration Count"),
"_bReserved" / construct.Optional(construct.Const(b"\0"))
)


class DescriptorParserCases(unittest.TestCase):

def test_string_descriptor(self):

string_descriptor = bytes([
40, # Length
3, # Type
ord('G'), 0x00,
ord('r'), 0x00,
ord('e'), 0x00,
ord('a'), 0x00,
ord('t'), 0x00,
ord(' '), 0x00,
ord('S'), 0x00,
ord('c'), 0x00,
ord('o'), 0x00,
ord('t'), 0x00,
ord('t'), 0x00,
ord(' '), 0x00,
ord('G'), 0x00,
ord('a'), 0x00,
ord('d'), 0x00,
ord('g'), 0x00,
ord('e'), 0x00,
ord('t'), 0x00,
ord('s'), 0x00,
])

# Parse the relevant string...
parsed = StringDescriptor.parse(string_descriptor)

# ... and check the desriptor's fields.
self.assertEqual(parsed.bLength, 40)
self.assertEqual(parsed.bDescriptorType, 3)
self.assertEqual(parsed.bString, "Great Scott Gadgets")


def test_device_descriptor(self):

device_descriptor = [
0x12, # Length
0x01, # Type
0x00, 0x02, # USB version
0xFF, # class
0xFF, # subclass
0xFF, # protocol
64, # ep0 max packet size
0xd0, 0x16, # VID
0x3b, 0x0f, # PID
0x00, 0x00, # device rev
0x01, # manufacturer string
0x02, # product string
0x03, # serial number
0x01 # number of configurations
]

# Parse the relevant string...
parsed = DeviceDescriptor.parse(device_descriptor)

# ... and check the desriptor's fields.
self.assertEqual(parsed.bLength, 18)
self.assertEqual(parsed.bDescriptorType, 1)
self.assertEqual(parsed.bcdUSB, 0x0200)
self.assertEqual(parsed.bDeviceClass, 0xFF)
self.assertEqual(parsed.bDeviceSubclass, 0xFF)
self.assertEqual(parsed.bDeviceProtocol, 0xFF)
self.assertEqual(parsed.bMaxPacketSize0, 64)
self.assertEqual(parsed.idVendor, 0x16d0)
self.assertEqual(parsed.idProduct, 0x0f3b)
self.assertEqual(parsed.bcdDevice, 0x0000)
self.assertEqual(parsed.iManufacturer, 1)
self.assertEqual(parsed.iProduct, 2)
self.assertEqual(parsed.iSerialNumber, 3)
self.assertEqual(parsed.bNumConfigurations, 1)


if __name__ == "__main__":
unittest.main()

usb-protocol/__init__.py → usb_protocol/__init__.py View File


+ 94
- 0
usb_protocol/emitters/__init__.py View File

@@ -0,0 +1,94 @@
#
# This file is part of usb-protocol.
#
""" Helpers for creating easy emitters. """

import unittest
import construct

class ConstructEmitter:
""" Class that creates a simple emitter based on a construct struct.

For example, if we have a construct format that looks like the following:
MyStruct = struct(
"a" / Int8
"b" / Int8
)

We could create emit an object like follows:
emitter = ConstructEmitter(MyStruct)
emitter.a = 0xab
emitter.b = 0xcd
my_bytes = emitter.emit() # "\xab\xcd"
"""

def __init__(self, struct):
"""
Parmeters:
construct_format -- The format for which to create an emitter.
"""
self.__dict__['format'] = struct
self.__dict__['fields'] = {}


def _format_contains_field(self, field_name):
""" Returns True iff the given format has a field with the provided name.

Parameters:
format_object -- The Construct format to work with. This includes e.g. most descriptor types.
field_name -- The field name to query.
"""
return any(f.name == field_name for f in self.format.subcons)


def __setattr__(self, name, value):
""" Hook that we used to set our fields. """

# If the field starts with a '_', don't handle it, as it's an internal field.
if name.startswith('_'):
super().__setattr__(name, value)
return

if not self._format_contains_field(name):
raise AttributeError(f"emitter specification contains no field {name}")

self.fields[name] = value


def emit(self):
""" Emits the stream of bytes associated with this object. """

try:
return self.format.build(self.fields)
except KeyError as e:
raise KeyError(f"missing necessary field: {e}")



class ConstructEmitterTest(unittest.TestCase):

def test_simple_emitter(self):

test_struct = construct.Struct(
"a" / construct.Int8ul,
"b" / construct.Int8ul
)

emitter = ConstructEmitter(test_struct)
emitter.a = 0xab
emitter.b = 0xcd

self.assertEqual(emitter.emit(), b"\xab\xcd")


def emitter_for_format(construct_format):
""" Creates a factory method for the relevant construct format. """

def _factory():
return ConstructEmitter(construct_format)

return _factory


if __name__ == "__main__":
unittest.main()

+ 68
- 0
usb_protocol/emitters/descriptor.py View File

@@ -0,0 +1,68 @@
#
# This file is part of usb-protocol.
#


from . import ConstructEmitter
from collections import defaultdict

class ComplexDescriptorEmitter(ConstructEmitter):
""" Base class for emitting complex descriptors, which contain nested subordinates. """

# Base classes should override this.
DESCRIPTOR_FORMAT = None

def __init__(self):

# Always create a basic ConstructEmitter from the given format.
super().__init__(self.DESCRIPTOR_FORMAT)

# Store a list of subordinate descriptors, and a count of
# subordinate descriptor types.
self._subordinates = []
self._type_counts = {}


def add_subordinate_descriptor(self, subordinate):
""" Adds a subordinate descriptor to the relevant descriptor.

Parameter:
subordinate -- The subordinate descriptor to add; can be an emitter,
or a bytes-like object.
"""

if hasattr(subordinate, 'emit'):
subordinate = subordinate.emit()
else:
subordinate = bytes(subordinate)

# The second byte of a given descriptor is always its type number.
# Count this descriptor type...
subordinate_type = subordinate[1]

try:
self._type_counts[subordinate_type] += 1
except KeyError:
self._type_counts[subordinate_type] = 1

# ... and add the relevant bytes to our list of subordinates.
self._subordinates.append(subordinate)


def emit(self, include_subordinates=True):
""" Emit our descriptor.

Parameters:
include_subordinates -- If true or not provided, any subordinate descriptors will be included.
"""

result = bytearray()

# Add our basic descriptor...
result.extend(super().emit())

# ... and if descired, add our subordinates...
for sub in self._subordinates:
result.extend(sub)

return bytes(result)

usb-protocol/types/descriptors/__init__.py → usb_protocol/emitters/descriptors/__init__.py View File


+ 171
- 0
usb_protocol/emitters/descriptors/standard.py View File

@@ -0,0 +1,171 @@
#
# This file is part of usb_protocol.
#
""" Convenience emitters for simple, standard descriptors. """

import unittest
import functools

from contextlib import contextmanager

from .. import emitter_for_format
from ..descriptor import ComplexDescriptorEmitter

from ...types.descriptors.standard import \
DeviceDescriptor, StringDescriptor, EndpointDescriptor, DeviceQualifierDescriptor, \
ConfigurationDescriptor, InterfaceDescriptor, StandardDescriptorNumbers


# Create our basic emitters...
DeviceDescriptorEmitter = emitter_for_format(DeviceDescriptor)
StringDescriptorEmitter = emitter_for_format(StringDescriptor)
EndpointDescriptorEmitter = emitter_for_format(EndpointDescriptor)
DeviceQualifierDescriptor = emitter_for_format(DeviceQualifierDescriptor)

# ... convenience functions ...
def get_string_descriptor(string):
""" Generates a string descriptor for the relevant string. """

emitter = StringDescriptorEmitter()
emitter.bString = string
return emitter.emit()

# ... and complex emitters.

class InterfaceDescriptorEmitter(ComplexDescriptorEmitter):
""" Emitter that creates an InterfaceDescriptor. """

DESCRIPTOR_FORMAT = InterfaceDescriptor

@contextmanager
def EndpointDescriptor(self):
""" Context manager that allows addition of a subordinate endpoint descriptor.

It can be used with a `with` statement; and yields an EndpointDesriptorEmitter
that can be populated:

with interface.EndpointDescriptor() as d:
d.bEndpointAddress = 0x01
d.bmAttributes = 0x80
d.wMaxPacketSize = 64
d.bInterval = 0

This adds the relevant descriptor, automatically.
"""

descriptor = EndpointDescriptorEmitter()
yield descriptor

self.add_subordinate_descriptor(descriptor)


def emit(self, include_subordinates=True):

# Count our endpoints, and then call our parent emitter.
self.bNumEndpoints = self._type_counts[StandardDescriptorNumbers.ENDPOINT]
return super().emit(include_subordinates=include_subordinates)



class ConfigurationDescriptorEmitter(ComplexDescriptorEmitter):
""" Emitter that creates a configuration descriptor. """

DESCRIPTOR_FORMAT = ConfigurationDescriptor

@contextmanager
def InterfaceDescriptor(self):
""" Context manager that allows addition of a subordinate interface descriptor.

It can be used with a `with` statement; and yields an InterfaceDescriptorEmitter
that can be populated:

with interface.InterfaceDescriptor() as d:
d.bInterfaceNumber = 0x01
[snip]

This adds the relevant descriptor, automatically. Note that populating derived
fields such as bNumEndpoints aren't necessary; they'll be populated automatically.
"""
descriptor = InterfaceDescriptorEmitter()
yield descriptor

self.add_subordinate_descriptor(descriptor)


def emit(self, include_subordinates=True):

# Count our interfaces...
self.bNumInterfaces = self._type_counts[StandardDescriptorNumbers.INTERFACE]

# ... and figure out our total length.
subordinate_length = sum(len(sub) for sub in self._subordinates)
self.wTotalLength = subordinate_length + self.DESCRIPTOR_FORMAT.sizeof()

# Finally, emit our whole descriptor.
return super().emit(include_subordinates=include_subordinates)


class EmitterTests(unittest.TestCase):

def test_string_emitter(self):
emitter = StringDescriptorEmitter()
emitter.bString = "Hello"

self.assertEqual(emitter.emit(), b"\x0C\x03H\0e\0l\0l\0o\0")


def test_string_emitter_function(self):
self.assertEqual(get_string_descriptor("Hello"), b"\x0C\x03H\0e\0l\0l\0o\0")


def test_configuration_emitter(self):
descriptor = bytes([

# config descriptor
12, # length
2, # type
25, 00, # total length
1, # num interfaces
1, # configuration number
0, # config string
0x80, # attributes
250, # max power

# interface descriptor
9, # length
4, # type
0, # number
0, # alternate
1, # num endpoints
0xff, # class
0xff, # subclass
0xff, # protocol
0, # string

# endpoint descriptor
7, # length
5, # type
0x01, # address
2, # attributes
64, 0, # max packet size
255, # interval
])


# Create a trivial configuration descriptor...
emitter = ConfigurationDescriptorEmitter()

with emitter.InterfaceDescriptor() as interface:
interface.bInterfaceNumber = 0

with interface.EndpointDescriptor() as endpoint:
endpoint.bEndpointAddress = 1


# ... and validate that it maches our reference descriptor.
binary = emitter.emit()
self.assertEqual(len(binary), len(descriptor))


if __name__ == "__main__":
unittest.main()

usb-protocol/types/__init__.py → usb_protocol/types/__init__.py View File

@@ -414,3 +414,228 @@ LANGUAGE_NAMES = {
0xf8ff: "HID (Vendor Defined 3)",
0xfcff: "HID (Vendor Defined 4)",
}


class LanguageIDs(IntEnum):
AFRIKAANS = 0X0436
ALBANIAN = 0X041C
ARABIC_SAUDI_ARABIA = 0X0401
ARABIC_IRAQ = 0X0801
ARABIC_EGYPT = 0X0C01
ARABIC_LIBYA = 0X1001
ARABIC_ALGERIA = 0X1401
ARABIC_MOROCCO = 0X1801
ARABIC_TUNISIA = 0X1C01
ARABIC_OMAN = 0X2001
ARABIC_YEMEN = 0X2401
ARABIC_SYRIA = 0X2801
ARABIC_JORDAN = 0X2C01
ARABIC_LEBANON = 0X3001
ARABIC_KUWAIT = 0X3401
ARABIC_UAE = 0X3801
ARABIC_BAHRAIN = 0X3C01
ARABIC_QATAR = 0X4001
ARMENIAN = 0X042B
ASSAMESE = 0X044D
AZERI_LATIN = 0X042C
AZERI_CYRILLIC = 0X082C
BASQUE = 0X042D
BELARUSSIAN = 0X0423
BENGALI = 0X0445
BULGARIAN = 0X0402
BURMESE = 0X0455
CATALAN = 0X0403
CHINESE_TAIWAN = 0X0404
CHINESE_PRC = 0X0804
CHINESE_HONG_KONG = 0X0C04
CHINESE_SINGAPORE = 0X1004
CHINESE_MACAU_SAR = 0X1404
CROATIAN = 0X041A
CZECH = 0X0405
DANISH = 0X0406
DUTCH_NETHERLANDS = 0X0413
DUTCH_BELGIUM = 0X0813
ENGLISH_US = 0X0409
ENGLISH_UNITED_KINGDOM = 0X0809
ENGLISH_AUSTRALIAN = 0X0C09
ENGLISH_CANADIAN = 0X1009
ENGLISH_NEW_ZEALAND = 0X1409
ENGLISH_IRELAND = 0X1809
ENGLISH_SOUTH_AFRICA = 0X1C09
ENGLISH_JAMAICA = 0X2009
ENGLISH_CARIBBEAN = 0X2409
ENGLISH_BELIZE = 0X2809
ENGLISH_TRINIDAD = 0X2C09
ENGLISH_ZIMBABWE = 0X3009
ENGLISH_PHILIPPINES = 0X3409
ESTONIAN = 0X0425
FAEROESE = 0X0438
FARSI = 0X0429
FINNISH = 0X040B
FRENCH_STANDARD = 0X040C
FRENCH_BELGIAN = 0X080C
FRENCH_CANADIAN = 0X0C0C
FRENCH_SWITZERLAND = 0X100C
FRENCH_LUXEMBOURG = 0X140C
FRENCH_MONACO = 0X180C
GEORGIAN = 0X0437
GERMAN_STANDARD = 0X0407
GERMAN_SWITZERLAND = 0X0807
GERMAN_AUSTRIA = 0X0C07
GERMAN_LUXEMBOURG = 0X1007
GERMAN_LIECHTENSTEIN = 0X1407
GREEK = 0X0408
GUJARATI = 0X0447
HEBREW = 0X040D
HINDI = 0X0439
HUNGARIAN = 0X040E
ICELANDIC = 0X040F
INDONESIAN = 0X0421
ITALIAN_STANDARD = 0X0410
ITALIAN_SWITZERLAND = 0X0810
JAPANESE = 0X0411
KANNADA = 0X044B
KASHMIRI_INDIA = 0X0860
KAZAKH = 0X043F
KONKANI = 0X0457
KOREAN = 0X0412
KOREAN_JOHAB = 0X0812
LATVIAN = 0X0426
LITHUANIAN = 0X0427
LITHUANIAN_CLASSIC = 0X0827
MACEDONIAN = 0X042F
MALAY_MALAYSIAN = 0X043E
MALAY_BRUNEI_DARUSSALAM = 0X083E
MALAYALAM = 0X044C
MANIPURI = 0X0458
MARATHI = 0X044E
NEPALI_INDIA = 0X0861
NORWEGIAN_BOKMAL = 0X0414
NORWEGIAN_NYNORSK = 0X0814
ORIYA = 0X0448
POLISH = 0X0415
PORTUGUESE_BRAZIL = 0X0416
PORTUGUESE_STANDARD = 0X0816
PUNJABI = 0X0446
ROMANIAN = 0X0418
RUSSIAN = 0X0419
SANSKRIT = 0X044F
SERBIAN_CYRILLIC = 0X0C1A
SERBIAN_LATIN = 0X081A
SINDHI = 0X0459
SLOVAK = 0X041B
SLOVENIAN = 0X0424
SPANISH_TRADITIONAL_SORT = 0X040A
SPANISH_MEXICAN = 0X080A
SPANISH_MODERN_SORT = 0X0C0A
SPANISH_GUATEMALA = 0X100A
SPANISH_COSTA_RICA = 0X140A
SPANISH_PANAMA = 0X180A
SPANISH_DOMINICAN_REPUBLIC = 0X1C0A
SPANISH_VENEZUELA = 0X200A
SPANISH_COLOMBIA = 0X240A
SPANISH_PERU = 0X280A
SPANISH_ARGENTINA = 0X2C0A
SPANISH_ECUADOR = 0X300A
SPANISH_CHILE = 0X340A
SPANISH_URUGUAY = 0X380A
SPANISH_PARAGUAY = 0X3C0A
SPANISH_BOLIVIA = 0X400A
SPANISH_EL_SALVADOR = 0X440A
SPANISH_HONDURAS = 0X480A
SPANISH_NICARAGUA = 0X4C0A
SPANISH_PUERTO_RICO = 0X500A
SUTU = 0X0430
SWAHILI_KENYA = 0X0441
SWEDISH = 0X041D
SWEDISH_FINLAND = 0X081D
TAMIL = 0X0449
TATAR_TATARSTAN = 0X0444
TELUGU = 0X044A
THAI = 0X041E
TURKISH = 0X041F
UKRAINIAN = 0X0422
URDU_PAKISTAN = 0X0420
URDU_INDIA = 0X0820
UZBEK_LATIN = 0X0443
UZBEK_CYRILLIC = 0X0843
VIETNAMESE = 0X042A
HID_USAGE_DATA_DESCRIPTOR = 0X04FF
HID_VENDOR_DEFINED_1 = 0XF0FF
HID_VENDOR_DEFINED_2 = 0XF4FF
HID_VENDOR_DEFINED_3 = 0XF8FF
HID_VENDOR_DEFINED_4 = 0XFCFF


class DescriptorTypes(IntEnum):
DEVICE = 1
CONFIGURATION = 2
STRING = 3
INTERFACE = 4
ENDPOINT = 5
DEVICE_QUALIFIER = 6
OTHER_SPEED_CONFIGURATION = 7
INTERFACE_POWER = 8
HID = 33
REPORT = 34


class USBSynchronizationType(IntEnum):
NONE = 0x00
ASYNC = 0x01
ADAPTIVE = 0x02
SYNCHRONOUS = 0x03


class USBUsageType(IntEnum):
DATA = 0
FEEDBACK = 1
IMPLICIT_FEEDBACK = 2


class USBStandardRequests(IntEnum):
GET_STATUS = 0
CLEAR_FEATURE = 1
SET_FEATURE = 3
SET_ADDRESS = 5
GET_DESCRIPTOR = 6
SET_DESCRIPTOR = 7
GET_CONFIGURATION = 8
SET_CONFIGURATION = 9
GET_INTERFACE = 10
SET_INTERFACE = 11
SYNCH_FRAME = 12


class USBTransferType(IntEnum):
CONTROL = 0
ISOCHRONOUS = 1
BULK = 2
INTERRUPT = 3


class USBSynchronizationType(IntEnum):
NONE = 0x00
ASYNC = 0x01
ADAPTIVE = 0x02
SYNCHRONOUS = 0x03


class USBUsageType(IntEnum):
DATA = 0
FEEDBACK = 1
IMPLICIT_FEEDBACK = 2


class USBStandardRequests(IntEnum):
GET_STATUS = 0
CLEAR_FEATURE = 1
SET_FEATURE = 3
SET_ADDRESS = 5
GET_DESCRIPTOR = 6
SET_DESCRIPTOR = 7
GET_CONFIGURATION = 8
SET_CONFIGURATION = 9
GET_INTERFACE = 10
SET_INTERFACE = 11
SYNCH_FRAME = 12

usb-protocol/types/descriptor.py → usb_protocol/types/descriptor.py View File

@@ -3,6 +3,7 @@
#
""" Type elements for defining USB descriptors. """

import unittest
import construct

class DescriptorFormat(construct.Struct):
@@ -76,6 +77,28 @@ class DescriptorNumber(construct.Const):
return self.number


class BCDFieldAdapter(construct.Adapter):
""" Construct adapter that dynamically parses BCD fields. """

def _decode(self, obj, context, path):
hex_string = f"{obj:04x}"
return float(f"{hex_string[0:2]}.{hex_string[2:]}")


def _encode(self, obj, context, path):

# Ensure the data is parseable.
if (obj * 100) % 1:
raise AssertionError("BCD fields must be in the format XX.YY")

# Break the object down into its component parts...
integer = int(obj)
percent = int((obj * 100) % 100)

# ... and squish them into an integer.
return int(f"{integer:02}{percent:02}", 16)



class DescriptorField(construct.Subconstruct):
"""
@@ -90,12 +113,12 @@ class DescriptorField(construct.Subconstruct):
# FIXME: these are really primitive views of these types;
# we should extend these to get implicit parsing wherever possible
USB_TYPES = {
'b' : construct.Optional(construct.Int8ul),
'bcd' : construct.Optional(construct.Int16ul), # TODO: Create a BCD parser for this
'i' : construct.Optional(construct.Int8ul),
'id' : construct.Optional(construct.Int16ul),
'bm' : construct.Optional(construct.Int8ul),
'w' : construct.Optional(construct.Int16ul),
'b' : construct.Int8ul,
'bcd' : BCDFieldAdapter(construct.Int16ul), # TODO: Create a BCD parser for this
'i' : construct.Int8ul,
'id' : construct.Int16ul,
'bm' : construct.Int8ul,
'w' : construct.Int16ul,
}

@staticmethod
@@ -128,12 +151,26 @@ class DescriptorField(construct.Subconstruct):
raise ValueError("field names must be formatted per the USB standard!")


def __init__(self, description=""):
def __init__(self, description="", default=None):
self.description = description
self.default = default


def __rtruediv__(self, field_name):
field_type = self._get_type_for_name(field_name)

# wew does construct make this look weird
return (field_name / field_type) * self.description
# Build our subconstruct. Construct makes this look super weird,
# but this is actually "we have a field with <field_name> of type <field_type>".
# In long form, we'll call it "description".
subconstruct = (field_name / field_type) * self.description

if self.default is not None:
return construct.Default(subconstruct, self.default)
else:
return subconstruct


# Convenience type that gets a descriptor's own length.
DescriptorLength = \
construct.Rebuild(construct.Int8ul, construct.len_(construct.this)) \
* "Descriptor Length"

+ 0
- 0
usb_protocol/types/descriptors/__init__.py View File


+ 218
- 0
usb_protocol/types/descriptors/standard.py View File

@@ -0,0 +1,218 @@
#
# This file is part of usb-protocol.
#
""" Structures describing standard USB descriptors. """

import unittest
from enum import IntEnum

import construct
from construct import this, Default

from .. import LanguageIDs
from ..descriptor import \
DescriptorField, DescriptorNumber, DescriptorFormat, \
BCDFieldAdapter, DescriptorLength


class StandardDescriptorNumbers(IntEnum):
""" Numbers of our standard descriptors. """

DEVICE = 1
CONFIGURATION = 2
STRING = 3
INTERFACE = 4
ENDPOINT = 5
DEVICE_QUALIFIER = 6
OTHER_SPEED_DESCRIPTOR = 7
INTERFACE_POWER = 8


DeviceDescriptor = DescriptorFormat(
"bLength" / DescriptorLength,
"bDescriptorType" / DescriptorNumber(StandardDescriptorNumbers.DEVICE),
"bcdUSB" / DescriptorField("USB Version", default=2.0),
"bDeviceClass" / DescriptorField("Class", default=0),
"bDeviceSubclass" / DescriptorField("Subclass", default=0),
"bDeviceProtocol" / DescriptorField("Protocol", default=0),
"bMaxPacketSize0" / DescriptorField("EP0 Max Pkt Size", default=64),
"idVendor" / DescriptorField("Vendor ID"),
"idProduct" / DescriptorField("Product ID"),
"bcdDevice" / DescriptorField("Device Version", default=0),
"iManufacturer" / DescriptorField("Manufacturer Str", default=0),
"iProduct" / DescriptorField("Product Str", default=0),
"iSerialNumber" / DescriptorField("Serial Number", default=0),
"bNumConfigurations" / DescriptorField("Configuration Count"),
)


ConfigurationDescriptor = DescriptorFormat(
"bLength" / DescriptorLength,
"bDescriptorType" / DescriptorNumber(StandardDescriptorNumbers.CONFIGURATION),
"wTotalLength" / DescriptorField("Length including subordinates"),
"bNumInterfaces" / DescriptorField("Interface count"),
"bConfigurationValue" / DescriptorField("Configuration number", default=1),
"iConfiguration" / DescriptorField("Description string", default=0),
"bmAttributes" / DescriptorField("Attributes", default=0x80),
"bMaxPower" / DescriptorField("Max power consumption", default=250),
)

# Field that automatically reflects a string descriptor's length.
StringDescriptorLength = construct.Rebuild(construct.Int8ul, construct.len_(this.bString) * 2 + 2)

StringDescriptor = DescriptorFormat(
"bLength" / StringDescriptorLength,
"bDescriptorType" / DescriptorNumber(StandardDescriptorNumbers.STRING),
"bString" / construct.GreedyString("utf_16_le")
)


StringLanguageDescriptorLength = \
construct.Rebuild(construct.Int8ul, construct.len_(this.wLANGID) * 2 + 2)

StringLanguageDescriptor = DescriptorFormat(
"bLength" / StringLanguageDescriptorLength,
"bDescriptorType" / DescriptorNumber(StandardDescriptorNumbers.STRING),
"wLANGID" / construct.GreedyRange(construct.Int16ul)
)


InterfaceDescriptor = DescriptorFormat(
"bLength" / construct.Const(9, construct.Int8ul),
"bDescriptorType" / DescriptorNumber(StandardDescriptorNumbers.INTERFACE),
"bInterfaceNumber" / DescriptorField("Interface number"),
"bAlternateSetting" / DescriptorField("Alternate setting", default=0),
"bNumEndpoints" / DescriptorField("Endpoints included"),
"bInterfaceClass" / DescriptorField("Class", default=0xff),
"bInterfaceSubclass" / DescriptorField("Subclass", default=0xff),
"bInterfaceProtocol" / DescriptorField("Protocol", default=0xff),
"iInterface" / DescriptorField("String index", default=0),
)


EndpointDescriptor = DescriptorFormat(
"bLength" / construct.Const(7, construct.Int8ul),
"bDescriptorType" / DescriptorNumber(StandardDescriptorNumbers.ENDPOINT),
"bEndpointAddress" / DescriptorField("Endpoint Address"),
"bmAttributes" / DescriptorField("Attributes", default=2),
"wMaxPacketSize" / DescriptorField("Maximum Packet Size", default=64),
"bInterval" / DescriptorField("Polling interval", default=255),
)


DeviceQualifierDescriptor = DescriptorFormat(
"bLength" / DescriptorLength,
"bDescriptorType" / DescriptorNumber(StandardDescriptorNumbers.DEVICE_QUALIFIER),
"bcdUSB" / DescriptorField("USB Version"),
"bDeviceClass" / DescriptorField("Class"),
"bDeviceSubclass" / DescriptorField("Subclass"),
"bDeviceProtocol" / DescriptorField("Protocol"),
"bMaxPacketSize0" / DescriptorField("EP0 Max Pkt Size"),
"bNumConfigurations" / DescriptorField("Configuration Count"),
"_bReserved" / construct.Optional(construct.Const(b"\0"))
)


class DescriptorParserCases(unittest.TestCase):

STRING_DESCRIPTOR = bytes([
40, # Length
3, # Type
ord('G'), 0x00,
ord('r'), 0x00,
ord('e'), 0x00,
ord('a'), 0x00,
ord('t'), 0x00,
ord(' '), 0x00,
ord('S'), 0x00,
ord('c'), 0x00,
ord('o'), 0x00,
ord('t'), 0x00,
ord('t'), 0x00,
ord(' '), 0x00,
ord('G'), 0x00,
ord('a'), 0x00,
ord('d'), 0x00,
ord('g'), 0x00,
ord('e'), 0x00,
ord('t'), 0x00,
ord('s'), 0x00,
])


def test_string_descriptor_parse(self):

# Parse the relevant string...
parsed = StringDescriptor.parse(self.STRING_DESCRIPTOR)

# ... and check the desriptor's fields.
self.assertEqual(parsed.bLength, 40)
self.assertEqual(parsed.bDescriptorType, 3)
self.assertEqual(parsed.bString, "Great Scott Gadgets")


def test_string_descriptor_build(self):
data = StringDescriptor.build({
'bString': "Great Scott Gadgets"
})

self.assertEqual(data, self.STRING_DESCRIPTOR)


def test_string_language_descriptor_build(self):
data = StringLanguageDescriptor.build({
'wLANGID': (LanguageIDs.ENGLISH_US,)
})

self.assertEqual(data, b"\x04\x03\x09\x04")


def test_device_descriptor(self):

device_descriptor = [
0x12, # Length
0x01, # Type
0x00, 0x02, # USB version
0xFF, # class
0xFF, # subclass
0xFF, # protocol
64, # ep0 max packet size
0xd0, 0x16, # VID
0x3b, 0x0f, # PID
0x00, 0x00, # device rev
0x01, # manufacturer string
0x02, # product string
0x03, # serial number
0x01 # number of configurations
]

# Parse the relevant string...
parsed = DeviceDescriptor.parse(device_descriptor)

# ... and check the desriptor's fields.
self.assertEqual(parsed.bLength, 18)
self.assertEqual(parsed.bDescriptorType, 1)
self.assertEqual(parsed.bcdUSB, 2.0)
self.assertEqual(parsed.bDeviceClass, 0xFF)
self.assertEqual(parsed.bDeviceSubclass, 0xFF)
self.assertEqual(parsed.bDeviceProtocol, 0xFF)
self.assertEqual(parsed.bMaxPacketSize0, 64)
self.assertEqual(parsed.idVendor, 0x16d0)
self.assertEqual(parsed.idProduct, 0x0f3b)
self.assertEqual(parsed.bcdDevice, 0)
self.assertEqual(parsed.iManufacturer, 1)
self.assertEqual(parsed.iProduct, 2)
self.assertEqual(parsed.iSerialNumber, 3)
self.assertEqual(parsed.bNumConfigurations, 1)


def test_bcd_constructor(self):

emitter = BCDFieldAdapter(construct.Int16ul)
result = emitter.build(1.4)

self.assertEqual(result, b"\x40\x01")


if __name__ == "__main__":
unittest.main()

Loading…
Cancel
Save