Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #64 - First implementation for libusb1-based hotplug #160

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

roberthartung
Copy link

No description provided.

@walac
Copy link
Member

walac commented Nov 7, 2016

@roberthartung sorry, I didn't forget your PR, crazy weeks...

@ijager
Copy link

ijager commented Jan 23, 2017

Do you have any idea when you are going to look at this? I would love to use hotplugging.

@walac
Copy link
Member

walac commented Jan 23, 2017

@ijager I lost my test hardware, I am working on a solution for that, that's why I stalled more serious patches for the project.

@roberthartung
Copy link
Author

@ijager You can add my repo as an upstream in the meantime and cherry pick the commit!

@roberthartung
Copy link
Author

I just uploaded a bugfix for user_data in the hotplug handle

@hetsch
Copy link

hetsch commented Sep 19, 2017

Oh yes, please consider merging this pull request!

@braineo
Copy link

braineo commented Feb 15, 2018

cool this works. Maybe maintainers can consider merging this PR or discuss what issues are remaining to fix?

@roberthartung
Copy link
Author

@walac Any news?

@walac
Copy link
Member

walac commented Apr 30, 2018

Any news?

I am not the maintainer of PyUSB anymore, 303 @SimplicityGuy

@roberthartung
Copy link
Author

Fixed another bug where userdata was pointing to the wrong object, if a dict was passed directly to the register function.

@slide
Copy link

slide commented Jan 18, 2019

Is there an example on how to use this in practice?

@rautesamtr
Copy link

The following example seems to work but also causes an exception

import usb.hotplug

def on_plug(dev, event, user_data):
    print(dev, event, user_data)

handle = usb.hotplug.register_callback(0x01 | 0x02, 0, -1, -1, -1, on_plug, None)

for event in usb.hotplug.loop():
    print(event)
Traceback (most recent call last):
  File "_ctypes/callbacks.c", line 257, in 'converting callback result'
TypeError: an integer is required (got type NoneType)
Exception ignored in: <bound method _HotplugHandle.callback of <usb.backend.libusb1._HotplugHandle object at 0x7f676c5ab0d0>>

@mcuee
Copy link
Member

mcuee commented Nov 25, 2019

Please help to rebase. Thanks.

@jonasmalacofilho
Copy link
Member

Conflict was trivial. Solved it through the web UI, sorry for the resulting ugly merge commit.

Still needs to be tested, and possibly a fix for the issue mentioned by @raetiacorvus.

@roberthartung
Copy link
Author

roberthartung commented Nov 29, 2019

I will try to rebase and test tomorrow! I will force push to get rid of the merge commit as well.

@om26er
Copy link

om26er commented Dec 23, 2019

Can one of the maintainers kindly take a look at the pull request and provide feedback. This is quite a useful feature that other libraries like https://github.com/python-escpos/python-escpos use to actively monitor connected usb printers.

@@ -575,6 +582,16 @@ def libusb_fill_iso_transfer(_libusb_transfer_p, dev_handle, endpoint, buffer, l
#int libusb_handle_events(libusb_context *ctx);
lib.libusb_handle_events.argtypes = [c_void_p]

try:
lib.libusb_hotplug_register_callback.argtypes = [c_void_p, c_int, c_int, c_int, c_int, c_int, _libusb_hotplug_callback_fn, py_object, POINTER(_libusb_hotplug_callback_handle)]
except AttributeError:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really doing a review, just putting a comment: A mention about why exception handling was needed would be helpful

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea. :-D

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the code: I am not sure, the code existing code above mine also catched the exception - sometimes. Too bad I didn't document this. But I guess I just did the same way as the existing code.
Might have something to do with POINTER c_void_p? not sure.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the other places it is used to deal with older versions libusb 1.0 that lack certain APIs.¹

All hotplug APIs are marked with

Since version 1.0.16, LIBUSB_API_VERSION >= 0x01000102

so the try-catch blocks are appropriate here.


¹ Though in some of these existing places it might not be necessary after all (e.g. libusb_get_port_number).

@jonasmalacofilho
Copy link
Member

@om26er

It's on my queue, but there a few bugs ahead of it still.

I'm also hoping that @roberthartung can take a first look at the possible issue pointed out by @raetiacorvus, since right now he knows a lot more about USB hotplug than me.

@roberthartung
Copy link
Author

Force-pushed with latest rebase.

@roberthartung
Copy link
Author

@jonasmalacofilho @raetiacorvus You code is just missing a line of code, it should be:

import usb.hotplug

def on_plug(dev, event, user_data):
    print(dev, event, user_data)
    return 0

handle = usb.hotplug.register_callback(0x01 | 0x02, 0, -1, -1, -1, on_plug, None)

for event in usb.hotplug.loop():
    print(event)

The callback function requires a return value ( see Documentation ):

bool whether this callback is finished processing events. returning 1 will cause this callback to be deregistered

@jonasmalacofilho jonasmalacofilho added this to the PyUSB 1.1.0 milestone Jan 10, 2020
@jonasmalacofilho
Copy link
Member

Quick update: I started to look over this pull request, but there are some things I still don't quite understand (so no review yet).

I'm also thinking about whether the public API should be that tied to libusb1. Sure, libusb1 is the only backend that matters (in the foreseeable future); it's also hard to build a general API based on a single real world implementation. But maybe we could avoid depending/exposing/directly manipulating libusb1 constants, and also be a bit closer to find when it comes to specifying vendor/product/class filters. Again, not sure about this...

@RyanHope
Copy link

This need to become a higher priority.

@roberthartung
Copy link
Author

Quick update: I started to look over this pull request, but there are some things I still don't quite understand (so no review yet).

I'm also thinking about whether the public API should be that tied to libusb1. Sure, libusb1 is the only backend that matters (in the foreseeable future); it's also hard to build a general API based on a single real world implementation. But maybe we could avoid depending/exposing/directly manipulating libusb1 constants, and also be a bit closer to find when it comes to specifying vendor/product/class filters. Again, not sure about this...

Hm yeah it makes sense I guess. I will try to implement some abstracted API or at least provide generalized constants!

@giusebar
Copy link

giusebar commented Dec 17, 2020

Has this been abandoned? would be a really useful feature

@mcuee mcuee modified the milestones: PyUSB 1.2.0, pyusb 1.3.0 Jun 15, 2021
Copy link
Member

@jonasmalacofilho jonasmalacofilho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my previous comment still applies.

I'm also thinking about whether the public API should be that tied to libusb1. Sure, libusb1 is the only backend that matters (in the foreseeable future); it's also hard to build a general API based on a single real world implementation. But maybe we could avoid depending/exposing/directly manipulating libusb1 constants, and also be a bit closer to find when it comes to specifying vendor/product/class filters.

While this appears to be a functional implementation, we have the problem that the advertised goal of PyUSB still is "an API rich, backend neutral Python USB module easy to use." This is a bit weird in a time where, essentially, only libusb1 is maintained, but it may still be useful if new backends become available.

I honestly don't know how I feel about dropping the "backend neutral" requirement, nor do I think I should get to decide about that alone. For the time being, I think it's better to keep as much neutrality as possible, which in this case would mean at least exploring an API that leaks a lot less of the underlying libusb1 backend.

@williamkapke
Copy link

I extracted the changes from this PR in to a monkey patch. Seems to work great - but happy to hear about any troubles I may find doing this. Hope to see this PR get resolved someday! Thanks.

Patch

import usb
from usb.core import Device
from usb.backend.libusb1 import (
    CFUNCTYPE, POINTER, c_int, c_void_p, _libusb_device_handle, c_uint, py_object,
    _objfinalizer, byref, _Device
)

# # # # # #  PATCH pyusb for hotplug # # # # # #
# Extracted from: https://github.com/pyusb/pyusb/pull/160
backend = usb.backend.libusb1.get_backend()

_libusb_hotplug_callback_handle = c_int
_libusb_hotplug_callback_fn = CFUNCTYPE(c_int, c_void_p, _libusb_device_handle, c_uint, py_object)
backend.lib.libusb_hotplug_register_callback.argtypes = [
    c_void_p, c_int, c_int, c_int, c_int, c_int,
    _libusb_hotplug_callback_fn, py_object,
    POINTER(_libusb_hotplug_callback_handle)
]
backend.lib.libusb_hotplug_deregister_callback.argtypes = [
    c_void_p, _libusb_hotplug_callback_handle
]


class _HotplugHandle(_objfinalizer.AutoFinalizedObject):
    def __init__(self, backend, events, flags, vendor_id, product_id, dev_class, user_callback, user_data):
        self.user_callback = user_callback
        self.__callback = _libusb_hotplug_callback_fn(self.callback)
        self.__backend = backend
        # If an object is passed directly and never stored anywhere,
        # the pointer will remain and user data might be wrong!
        # therefore we have to keep a refernce here
        self.__user_data = py_object(user_data)
        handle = _libusb_hotplug_callback_handle()
        backend.lib.libusb_hotplug_register_callback(backend.ctx, events, flags, vendor_id, product_id, dev_class,
                                                     self.__callback, self.__user_data, byref(handle))

    def callback(self, ctx, dev, evnt, user_data):
        dev = Device(_Device(dev), self.__backend)
        return self.user_callback(dev, evnt, user_data)


def register_callback(callback, *,
                      user_data=None,
                      events=0x01 | 0x02,  # arrive and exit
                      flags=0,  # do not emumerate on register
                      vendor_id=-1,  # match any
                      product_id=-1,  # match any
                      dev_class=-1  # match any
                      ):
    return _HotplugHandle(backend, events, flags, vendor_id, product_id, dev_class, callback, user_data)


def deregister_callback(handle):
    backend.lib.libusb_hotplug_deregister_callback(backend.ctx, handle.handle)


def loop():
    while True:
        yield backend.lib.libusb_handle_events(backend.ctx)

Use

def on_plug(dev, event, user_data):
    print(dev, event, user_data)
    return 0

handle = register_callback(on_plug)

for event in loop():
    print(event)

@NastuzziSamy
Copy link

Thank you a lot @williamkapke, works like a sharm !

@ChangmingTian
Copy link

I extracted the changes from this PR in to a monkey patch. Seems to work great - but happy to hear about any troubles I may find doing this. Hope to see this PR get resolved someday! Thanks.

Patch

import usb
from usb.core import Device
from usb.backend.libusb1 import (
    CFUNCTYPE, POINTER, c_int, c_void_p, _libusb_device_handle, c_uint, py_object,
    _objfinalizer, byref, _Device
)

# # # # # #  PATCH pyusb for hotplug # # # # # #
# Extracted from: https://github.com/pyusb/pyusb/pull/160
backend = usb.backend.libusb1.get_backend()

_libusb_hotplug_callback_handle = c_int
_libusb_hotplug_callback_fn = CFUNCTYPE(c_int, c_void_p, _libusb_device_handle, c_uint, py_object)
backend.lib.libusb_hotplug_register_callback.argtypes = [
    c_void_p, c_int, c_int, c_int, c_int, c_int,
    _libusb_hotplug_callback_fn, py_object,
    POINTER(_libusb_hotplug_callback_handle)
]
backend.lib.libusb_hotplug_deregister_callback.argtypes = [
    c_void_p, _libusb_hotplug_callback_handle
]


class _HotplugHandle(_objfinalizer.AutoFinalizedObject):
    def __init__(self, backend, events, flags, vendor_id, product_id, dev_class, user_callback, user_data):
        self.user_callback = user_callback
        self.__callback = _libusb_hotplug_callback_fn(self.callback)
        self.__backend = backend
        # If an object is passed directly and never stored anywhere,
        # the pointer will remain and user data might be wrong!
        # therefore we have to keep a refernce here
        self.__user_data = py_object(user_data)
        handle = _libusb_hotplug_callback_handle()
        backend.lib.libusb_hotplug_register_callback(backend.ctx, events, flags, vendor_id, product_id, dev_class,
                                                     self.__callback, self.__user_data, byref(handle))

    def callback(self, ctx, dev, evnt, user_data):
        dev = Device(_Device(dev), self.__backend)
        return self.user_callback(dev, evnt, user_data)


def register_callback(callback, *,
                      user_data=None,
                      events=0x01 | 0x02,  # arrive and exit
                      flags=0,  # do not emumerate on register
                      vendor_id=-1,  # match any
                      product_id=-1,  # match any
                      dev_class=-1  # match any
                      ):
    return _HotplugHandle(backend, events, flags, vendor_id, product_id, dev_class, callback, user_data)


def deregister_callback(handle):
    backend.lib.libusb_hotplug_deregister_callback(backend.ctx, handle.handle)


def loop():
    while True:
        yield backend.lib.libusb_handle_events(backend.ctx)

Use

def on_plug(dev, event, user_data):
    print(dev, event, user_data)
    return 0

handle = register_callback(on_plug)

for event in loop():
    print(event)
  1. handle = _libusb_hotplug_callback_handle() need change to self.handle = libusb_hotplug_callback_handle()
  2. Must quite on_plug before call dev.ctrl_transfer API,Or get resource busy error

@mcuee
Copy link
Member

mcuee commented Apr 7, 2024

FYI: there is a pending but working PR for libusb Windows hotplug implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet