pythonwindowscallbackusbhotplugging

USB Hotplugging callbacks with python on Windows


Is it possible to write a python script such that a function is called whenever a USB device is added or removed on Windows?

libusb (and corresponding python modules such as libusb1) appears to be the most popular solution, but it lacks hotplugging callback registration support in Windows. A feature request has been open for this since 2015, and it's still not implemented.

I've seen some hacks query Windows' usb devices at some interval, do a diff of the current list of devices from the previous list of devices, and use this as an alternative. Due to the nature of my application, this hack would be a huge security risk and is not a valid solution. I need actual callback registration on usb hotplug events.

Though less ideal, I'm open to writing something in C or C++ and then writing a python binding to that code, if necessary.

Is it possible to have a python function called when a usb device is connected or disconnected on Windows?


Solution

  • Yes, you can use ctypes in python to register a callback for WM_DEVICECHANGE messages.

    I was successfully able to add register callback functions to usb hotplug events in both linux (using the libusb1 python module) and windows (using the ctypes python module). The bulk of the relevant windows code can be found here:

    Which is largely based on the code found at these links:

    import win32api, win32con, win32gui
    from ctypes import *
    
    #
    # Device change events (WM_DEVICECHANGE wParam)
    #
    DBT_DEVICEARRIVAL = 0x8000
    DBT_DEVICEQUERYREMOVE = 0x8001
    DBT_DEVICEQUERYREMOVEFAILED = 0x8002
    DBT_DEVICEMOVEPENDING = 0x8003
    DBT_DEVICEREMOVECOMPLETE = 0x8004
    DBT_DEVICETYPESSPECIFIC = 0x8005
    DBT_CONFIGCHANGED = 0x0018
    
    #
    # type of device in DEV_BROADCAST_HDR
    #
    DBT_DEVTYP_OEM = 0x00000000
    DBT_DEVTYP_DEVNODE = 0x00000001
    DBT_DEVTYP_VOLUME = 0x00000002
    DBT_DEVTYPE_PORT = 0x00000003
    DBT_DEVTYPE_NET = 0x00000004
    
    #
    # media types in DBT_DEVTYP_VOLUME
    #
    DBTF_MEDIA = 0x0001
    DBTF_NET = 0x0002
    
    WORD = c_ushort
    DWORD = c_ulong
    
    
    class DEV_BROADCAST_HDR(Structure):
        _fields_ = [
            ("dbch_size", DWORD),
            ("dbch_devicetype", DWORD),
            ("dbch_reserved", DWORD)
        ]
    
    
    class DEV_BROADCAST_VOLUME(Structure):
        _fields_ = [
            ("dbcv_size", DWORD),
            ("dbcv_devicetype", DWORD),
            ("dbcv_reserved", DWORD),
            ("dbcv_unitmask", DWORD),
            ("dbcv_flags", WORD)
        ]
    
    
    def drive_from_mask(mask):
        n_drive = 0
        while 1:
            if (mask & (2 ** n_drive)):
                return n_drive
            else:
                n_drive += 1
    
    
    class Notification:
        def __init__(self):
            message_map = {
                win32con.WM_DEVICECHANGE: self.onDeviceChange
            }
    
            wc = win32gui.WNDCLASS()
            hinst = wc.hInstance = win32api.GetModuleHandle(None)
            wc.lpszClassName = "DeviceChangeDemo"
            wc.style = win32con.CS_VREDRAW | win32con.CS_HREDRAW
            wc.hCursor = win32gui.LoadCursor(0, win32con.IDC_ARROW)
            wc.hbrBackground = win32con.COLOR_WINDOW
            wc.lpfnWndProc = message_map
            classAtom = win32gui.RegisterClass(wc)
            style = win32con.WS_OVERLAPPED | win32con.WS_SYSMENU
            self.hwnd = win32gui.CreateWindow(
                classAtom,
                "Device Change Demo",
                style,
                0, 0,
                win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT,
                0, 0,
                hinst, None
            )
    
        def onDeviceChange(self, hwnd, msg, wparam, lparam):
            #
            # WM_DEVICECHANGE:
            #  wParam - type of change: arrival, removal etc.
            #  lParam - what's changed?
            #    if it's a volume then...
            #  lParam - what's changed more exactly
            #
            dev_broadcast_hdr = DEV_BROADCAST_HDR.from_address(lparam)
    
            if wparam == DBT_DEVICEARRIVAL:
                print("Something's arrived")
    
                if dev_broadcast_hdr.dbch_devicetype == DBT_DEVTYP_VOLUME:
                    print("It's a volume!")
    
                    dev_broadcast_volume = DEV_BROADCAST_VOLUME.from_address(lparam)
                    if dev_broadcast_volume.dbcv_flags & DBTF_MEDIA:
                        print("with some media")
                        drive_letter = drive_from_mask(dev_broadcast_volume.dbcv_unitmask)
                        print("in drive", chr(ord("A") + drive_letter))
    
            return 1
    
    
    if __name__ == '__main__':
        w = Notification()
        win32gui.PumpMessages()