Initial commit

This commit is contained in:
wagiminator
2022-09-11 11:42:08 +02:00
parent 54d0f15ce8
commit 03e8a184a2
166 changed files with 86898 additions and 2 deletions

View File

@@ -0,0 +1,165 @@
"""Base class for all HID transport mechanisms."""
from logging import getLogger
from . import toolinfo
class HidTool(object):
"""
Holds transport and DAP properties of a CMSIS-DAP debugger.
Used to select the debugger to use if multiple debuggers are connected.
"""
# pylint: disable=too-many-instance-attributes, too-many-arguments
# These are primary keys used to identify the debugger.
def __init__(self, vendor_id, product_id, serial_number, product_string="", manufacturer_string=""):
self.logger = getLogger(__name__)
self.interface_number = -1
self.vendor_id = vendor_id
self.product_id = product_id
self.serial_number = serial_number
self.product_string = product_string
self.manufacturer_string = manufacturer_string
self.firmware_version = ""
self.device_vendor_id = ""
self.device_name = ""
self.packet_size = 64
def set_packet_size(self, packet_size):
"""
Sets the packet size
:param packet_size: bytes per packet
"""
self.packet_size = packet_size
def set_product_string(self, product_string):
"""
Sets the product string
:param product_string: product name string
"""
self.product_string = product_string
class HidTransportBase(object):
"""Base class for HID transports"""
def __init__(self):
self.logger = getLogger(__name__)
self.devices = []
self.device = None
self.detect_devices()
self.connected = False
def __del__(self):
# Make sure we always disconnect the HID connection
self.disconnect()
def detect_devices(self):
"""Raise error as this method needs to be overridden."""
raise NotImplementedError("method needs to be defined by sub-class")
def get_matching_tools(self, serial_number_substring='', product=None):
"""
Returns a list of tools matching the given serial_number_substring and product.
:param serial_number_substring: can be an empty string or a subset of a serial number. Not case sensitive
This function will do matching of the last part of the devices serial numbers to
the serial_number_substring. Examples:
'123' will match "MCHP3252000000043123" but not "MCP32520001230000000"
'' will match any serial number
:param product: product type to connect to. If None any tool matching the serial_number_substring
will be returned
:return: List of matching tools
"""
# Support systems which use None as the standard for a unspecified USB serial
if serial_number_substring is None:
serial_number_substring = ""
# Making serial_number_substring case insensitive
serial_number_substring = serial_number_substring.lower()
# Support tool shortnames
toolname_in_product_string = toolinfo.tool_shortname_to_product_string_name(product)
if toolname_in_product_string is not None:
# Making product name case insensitive
toolname_in_product_string = toolname_in_product_string.lower()
matching_devices = []
for device in self.devices:
if toolname_in_product_string is None or device.product_string.lower().startswith(
toolname_in_product_string):
if device.serial_number.lower().endswith(serial_number_substring):
matching_devices.append(device)
return matching_devices
def connect(self, serial_number=None, product=None):
"""
Makes a HID connection to a debugger
:param serial_number: instance serial number to connect to
:param product: product type to connect to
:return: True if successfully connected to a tool, False if not
"""
if self.connected:
return True
device_count = len(self.devices)
self.logger.debug("{:d} devices available".format(device_count))
if device_count == 0:
self.logger.error("No CMSIS-DAP devices found.")
return False
matching_devices = self.get_matching_tools(serial_number_substring=serial_number, product=product)
number_of_matching_devices = len(matching_devices)
# Did we find exactly 1 tool?
if number_of_matching_devices != 1:
log_str = "Found {:d} daps matching the filter serial = \"{}\" and product = \"{}\""
self.logger.debug(log_str.format(number_of_matching_devices, serial_number, product))
if number_of_matching_devices > 1:
self.logger.error("Too many products found. Please specify one of:")
for device in self.devices:
self.logger.error(" > {:s} {:s}".format(device.product_string,
device.serial_number))
return False
# Everything is peachy, connect to the tool
self.device = matching_devices[0]
self.hid_connect(self.device)
self.logger.debug("Connected OK")
self.connected = True
packet_size = toolinfo.get_default_report_size(self.device.product_id)
self.device.set_packet_size(packet_size)
self.hid_info()
return True
def disconnect(self):
"""Release the HID connection"""
if self.connected:
self.hid_disconnect()
self.connected = False
def hid_connect(self, device):
"""Raise error as this method needs to be overridden."""
raise NotImplementedError("method needs to be defined by sub-class")
def hid_info(self):
"""Raise error as this method needs to be overridden."""
raise NotImplementedError("method needs to be defined by sub-class")
def hid_disconnect(self):
"""Raise error as this method needs to be overridden."""
raise NotImplementedError("method needs to be defined by sub-class")
def get_report_size(self):
"""
Get the packet size in bytes
:return: bytes per packet/report
"""
return self.device.packet_size

View File

@@ -0,0 +1,56 @@
"""
Factory for HID transport connections.
Currently supports only Cython/HIDAPI
"""
import platform
from logging import getLogger
from ..pyedbglib_errors import PyedbglibNotSupportedError
def hid_transport(library="hidapi"):
"""
Dispatch a transport layer for the OS in question
The transport layer is typically used to connect to a tool and then it is passed in as a parameter when creating
protocol objects. An example where the transport layer is used to create an instance of the housekeepingprotocol
for communication with the nEDBG debugger::
from pyedbglib.hidtransport.hidtransportfactory import hid_transport
transport = hid_transport()
connect_status = False
try:
connect_status = transport.connect(serial_number='', product='nedbg')
except IOError as error:
print("Unable to connect to USB device ({})".format(error))
if not connect_status:
print("Unable to connect to USB device")
housekeeper = housekeepingprotocol.Jtagice3HousekeepingProtocol(transport)
:param library: Transport library to use, currently only 'hidapi' is supported which will use the libusb hidapi
:type library: string
:returns: Instance of transport layer object
:rtype: class:cyhidapi:CyHidApiTransport
"""
logger = getLogger(__name__)
operating_system = platform.system().lower()
logger.debug("HID transport using library '{:s}' on OS '{:s}'".format(library, operating_system))
# HID API is the primary transport
if library == 'hidapi':
hid_api_supported_os = ['windows', 'darwin', 'linux', 'linux2']
if operating_system in hid_api_supported_os:
from .cyhidapi import CyHidApiTransport
return CyHidApiTransport()
msg = "System '{0:s}' not implemented for library '{1:s}'".format(operating_system, library)
logger.error(msg)
raise PyedbglibNotSupportedError(msg)
# Other transports may include cmsis-dap DLL, atusbhid (dll or so) etc
msg = "Transport library '{0}' not implemented.".format(library)
logger.error(msg)
raise PyedbglibNotSupportedError(msg)

View File

@@ -0,0 +1,94 @@
"""Gathering of all known Microchip CMSIS-DAP debuggers and default EP sizes"""
from logging import getLogger
# List of known useful HID/CMSIS-DAP tools
# 3G tools:
USB_TOOL_DEVICE_PRODUCT_ID_JTAGICE3 = 0x2140
USB_TOOL_DEVICE_PRODUCT_ID_ATMELICE = 0x2141
USB_TOOL_DEVICE_PRODUCT_ID_POWERDEBUGGER = 0x2144
USB_TOOL_DEVICE_PRODUCT_ID_EDBG_A = 0x2111
USB_TOOL_DEVICE_PRODUCT_ID_ZERO = 0x2157
USB_TOOL_DEVICE_PRODUCT_ID_MASS_STORAGE = 0x2169
USB_TOOL_DEVICE_PRODUCT_ID_PUBLIC_EDBG_C = 0x216A
USB_TOOL_DEVICE_PRODUCT_ID_KRAKEN = 0x2170
# 4G tools:
USB_TOOL_DEVICE_PRODUCT_ID_MEDBG = 0x2145
# 5G tools:
USB_TOOL_DEVICE_PRODUCT_ID_NEDBG_HID_MSD_DGI_CDC = 0x2175
USB_TOOL_DEVICE_PRODUCT_ID_PICKIT4_HID_CDC = 0x2177
USB_TOOL_DEVICE_PRODUCT_ID_SNAP_HID_CDC = 0x2180
# The Product String Names are used to identify the tool based on the USB
# device product strings (i.e. these names are usually just a subset of the
# actual product strings)
TOOL_SHORTNAME_TO_USB_PRODUCT_STRING = {
'atmelice': "Atmel-ICE",
'powerdebugger': "Power Debugger",
'pickit4': "MPLAB PICkit 4",
'snap': "MPLAB Snap",
'nedbg': "nEDBG",
'jtagice3': "JTAGICE3",
'medbg': "mEDBG",
'edbg': "EDBG",
}
def get_default_report_size(pid):
"""
Retrieve default EP report size based on known PIDs
:param pid: product ID
:return: packet size
"""
logger = getLogger(__name__)
hid_tools = [
# 3G
{'pid': USB_TOOL_DEVICE_PRODUCT_ID_JTAGICE3, 'default_report_size': 512},
{'pid': USB_TOOL_DEVICE_PRODUCT_ID_ATMELICE, 'default_report_size': 512},
{'pid': USB_TOOL_DEVICE_PRODUCT_ID_POWERDEBUGGER, 'default_report_size': 512},
{'pid': USB_TOOL_DEVICE_PRODUCT_ID_EDBG_A, 'default_report_size': 512},
# 4G
{'pid': USB_TOOL_DEVICE_PRODUCT_ID_MEDBG, 'default_report_size': 64},
# 5G
{'pid': USB_TOOL_DEVICE_PRODUCT_ID_NEDBG_HID_MSD_DGI_CDC, 'default_report_size': 64},
{'pid': USB_TOOL_DEVICE_PRODUCT_ID_PICKIT4_HID_CDC, 'default_report_size': 64},
{'pid': USB_TOOL_DEVICE_PRODUCT_ID_SNAP_HID_CDC, 'default_report_size': 64}]
logger.debug("Looking up report size for pid 0x{:04X}".format(pid))
for tool in hid_tools:
if tool['pid'] == pid:
logger.debug("Default report size is {:d}".format(tool['default_report_size']))
return tool['default_report_size']
logger.debug("PID not found! Reverting to 64b.")
return 64
def tool_shortname_to_product_string_name(shortname):
"""
Mapping for common short names of tools to product string name
The intention is that this function is always run on the tool name and that the conversion
only happens if the name is a known shortname. If the shortname is not known of if the name
provided is already a valid Product string name then the provided shortname parameter will
just be returned unchanged. So if the name already is a correct Product string name it is
still safe to run this conversion funtion on it.
:param shortname: shortname typically used by atbackend (powerdebugger, atmelice etc.)
:return: String to look for in USB product strings to identify the tool
"""
logger = getLogger(__name__)
if shortname is None:
logger.debug("Tool shortname is None")
# This is also valid as the user might have provided no tool name, but the conversion function
# should still be valid
return shortname
shortname_lower = shortname.lower()
if shortname_lower not in TOOL_SHORTNAME_TO_USB_PRODUCT_STRING:
logger.debug("%s is not a known tool shortname", shortname)
# ...but it could be a valid Product string name already so no reason to report an error
return shortname
return TOOL_SHORTNAME_TO_USB_PRODUCT_STRING[shortname_lower]