Skip to content

USB HID Report - USB Busy #5461

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

Closed
JaggerJo opened this issue Oct 13, 2021 · 16 comments
Closed

USB HID Report - USB Busy #5461

JaggerJo opened this issue Oct 13, 2021 · 16 comments
Labels

Comments

@JaggerJo
Copy link

JaggerJo commented Oct 13, 2021

CircuitPython version

Adafruit CircuitPython 7.0.0 on 2021-09-20; Raspberry Pi Pico with rp2040

Code/REPL

# boot.py

import time
import usb_hid

print("booting...")

def find_device(devices, *, usage_page, usage):
    """Search through the provided list of devices to find the one with the matching usage_page and
    usage."""
    if hasattr(devices, "send_report"):
        devices = [devices]
    for device in devices:
        if (
            device.usage_page == usage_page
            and device.usage == usage
            and hasattr(device, "send_report")
        ):
            return device
    raise ValueError("Could not find matching HID device.")

class Mouse:
    """Send USB HID mouse reports."""

    LEFT_BUTTON = 1
    """Left mouse button."""
    RIGHT_BUTTON = 2
    """Right mouse button."""
    MIDDLE_BUTTON = 4
    """Middle mouse button."""

    def __init__(self, devices):
        """Create a Mouse object that will send USB mouse HID reports.
        Devices can be a list of devices that includes a keyboard device or a keyboard device
        itself. A device is any object that implements ``send_report()``, ``usage_page`` and
        ``usage``.
        """
        self._mouse_device = find_device(devices, usage_page=0x1, usage=0x02)
        #print(dir(devices))
        # Reuse this bytearray to send mouse reports.
        # report[0] buttons pressed (LEFT, MIDDLE, RIGHT)
        # report[1] x1 movement
        # report[2] x2 movement
        # report[3] y1 movement
        # report[4] y2 movement
        # report[5] wheel movement
        self.report = bytearray(6)

        # Do a no-op to test if HID device is ready.
        # If not, wait a bit and try once more.
        try:
            self._send_no_move()
        except OSError:
            time.sleep(1)
            self._send_no_move()

    def press(self, buttons):
        """Press the given mouse buttons.
        :param buttons: a bitwise-or'd combination of ``LEFT_BUTTON``,
            ``MIDDLE_BUTTON``, and ``RIGHT_BUTTON``.
        Examples::
            # Press the left button.
            m.press(Mouse.LEFT_BUTTON)
            # Press the left and right buttons simultaneously.
            m.press(Mouse.LEFT_BUTTON | Mouse.RIGHT_BUTTON)
        """
        self.report[0] |= buttons
        self._send_no_move()

    def release(self, buttons):
        """Release the given mouse buttons.
        :param buttons: a bitwise-or'd combination of ``LEFT_BUTTON``,
            ``MIDDLE_BUTTON``, and ``RIGHT_BUTTON``.
        """
        self.report[0] &= ~buttons
        self._send_no_move()

    def release_all(self):
        """Release all the mouse buttons."""
        self.report[0] = 0
        self._send_no_move()

    def click(self, buttons):
        """Press and release the given mouse buttons.
        :param buttons: a bitwise-or'd combination of ``LEFT_BUTTON``,
            ``MIDDLE_BUTTON``, and ``RIGHT_BUTTON``.
        Examples::
            # Click the left button.
            m.click(Mouse.LEFT_BUTTON)
            # Double-click the left button.
            m.click(Mouse.LEFT_BUTTON)
            m.click(Mouse.LEFT_BUTTON)
        """
        self.press(buttons)
        self.release(buttons)

    def move(self, x=0, y=0, wheel=0):
        """Move the mouse and turn the wheel as directed.
        :param x: Set pointer on x axis. 32767 = 100% to the right
        :param y: Set pointer on y axis. 32767 = 100% to the bottom
        :param wheel: Rotate the wheel this amount. Negative is toward the user, positive
            is away from the user. The scrolling effect depends on the host.
        Examples::
            # Move 100 to the left. Do not move up and down. Do not roll the scroll wheel.
            m.move(1000, 3000, 0)
            # Same, with keyword arguments.
            m.move(x=1000, y=3000, wheel=0)
            # Roll the mouse wheel away from the user.
            m.move(wheel=1)
        """

        # Wheel
        while wheel != 0:
            partial_wheel = self._limit(wheel)
            print(wheel)
            self.report[5] = partial_wheel & 0xFF
            self._mouse_device.send_report(self.report)
            wheel -= partial_wheel

        # Coordinates
        x = self._limit_coord(x)
        y = self._limit_coord(y)
        # HID reports use little endian
        x1, x2 = (x & 0xFFFFFFFF).to_bytes(2, 'little')
        y1, y2 = (y & 0xFFFFFFFF).to_bytes(2, 'little')
        #print(x1)
        #print(x2)
        #print(y1)
        #print(y2)
        self.report[1] = x1
        self.report[2] = x2
        self.report[3] = y1
        self.report[4] = y2
        self._mouse_device.send_report(self.report)

    def _send_no_move(self):
        """Send a button-only report."""
        self.report[1] = 0
        self.report[2] = 0
        self.report[3] = 0
        self.report[4] = 0
        self._mouse_device.send_report(self.report)

    @staticmethod
    def _limit(dist):
        return min(127, max(-127, dist))

    @staticmethod
    def _limit_coord(coord):
        return min(32767, max(0, coord))



# https://stackoverflow.com/questions/36750287/two-byte-report-count-for-hid-report-descriptor
absolute_mouse = usb_hid.Device(
    report_descriptor=bytes(
        # Absolute mouse
        (0x05, 0x01)  # Usage Page (Generic Desktop)
        + (0x09, 0x02)  # Usage (Mouse)
        + (0xA1, 0x01)  # Collection (Application)
        + (0x09, 0x01)  # Usage (Pointer)
        + (0xA1, 0x00)  # Collection (Physical)
        + (0x85, 0xFF)  # Report ID  [11 is SET at RUNTIME]
        # Buttons
        + (0x05, 0x09)  # Usage Page (Button)
        + (0x19, 0x01)  # Usage Minimum (0x01)
        + (0x29, 0x05)  # Usage Maximum (0x05)
        + (0x15, 0x00)  # Logical Minimum (0)
        + (0x25, 0x01)  # Logical Maximum (1)
        + (0x95, 0x05)  # Report Count (5)
        + (0x75, 0x01)  # Report Size (1)
        + (0x81, 0x02)  # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
        + (0x75, 0x03)  # Report Size (3)
        + (0x95, 0x01)  # Report Count (1)
        + (0x81, 0x03)  # Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
        # Movement
        + (0x05, 0x01)  # Usage Page (Generic Desktop Ctrls)
        + (0x09, 0x30)  # Usage (X)
        + (0x09, 0x31)  # Usage (Y)
        + (0x15, 0x00)  # LOGICAL_MINIMUM (0)       ; Note: 0x15 = 1 Byte; 0x16 = 2 Byte; 0x17 = 4 Byte
        + (0x26, 0xFF, 0x7F)  # LOGICAL_MAXIMUM (32767)   ; Note: 0x25 = 1 Byte, 0x26 = 2 Byte; 0x27 = 4 Byte Report
        # + (0x35, 0x00)        # Physical Minimum (0)
        # + (0x46, 0xff, 0x7f)  # Physical Maximum (32767)
        + (0x75, 0x10)  # REPORT_SIZE (16)
        + (0x95, 0x02)  # REPORT_COUNT (2)
        + (0x81, 0x02)  # Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
        # Wheel
        + (0x09, 0x38)  # Usage (Wheel)
        + (0x15, 0x81)  # Logical Minimum (-127)
        + (0x25, 0x7F)  # Logical Maximum (127)
        # + (0x35, 0x81)   # Physical Minimum (same as logical)
        # + (0x45, 0x7f)   # Physical Maximum (same as logical)
        + (0x75, 0x08)  # Report Size (8)
        + (0x95, 0x01)  # Report Count (1)
        + (0x81, 0x06)  # Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
        + (0xC0,)  # End Collection
        + (0xC0,)  # End Collection
    ),
    usage_page=1,
    usage=2,
    report_ids=(11,),
    in_report_lengths=(6,),
    out_report_lengths=(0,)
)

# usb_hid.enable((usb_hid.Device.KEYBOARD, absolute_mouse))
usb_hid.enable((absolute_mouse,))  # Make sure to have double brackets and comma\

mouse = Mouse(usb_hid.devices)
# Note: Values are NOT pixels! 32767 = 100% (to right or to bottom)
mouse.move(x=20000, y=2000)

print("done!")

Behavior

Adafruit CircuitPython 7.0.0 on 2021-09-20; Raspberry Pi Pico with rp2040
Board ID:raspberry_pi_pico
boot.py output:
booting...
Traceback (most recent call last):
File "boot.py", line 209, in
File "boot.py", line 55, in init
File "boot.py", line 142, in _send_no_move
OSError: USB busy

Description

Copied the code from https://gist.github.com/bitboy85/cdcd0e7e04082db414b5f1d23ab09005 and slightly changed it. It appears like the usb_hid.Device function has changed and now takes different arguments.

Additional information

The goal is to send absolute mouse HID events with a raspberry pi pico.

There might be something wrong with the Report, can't figure it out tho.

@JaggerJo JaggerJo added the bug label Oct 13, 2021
@ThomasAtBBTF
Copy link

Here is a post that I found some times ago:
https://developer.apple.com/forums/thread/652700
This is also about absolute mouse positioning.
I also fiddled around with the HID descriptor but in my case the mouse movement under IOS always was relative regardless if I used relative or absolute in the descriptor.
But I will try again with the descriptor from bitboy85 mentioned above.
I will let report back.

@JaggerJo
Copy link
Author

@ThomasAtBBTF Thanks!

Why would you say is the thread related? Is the USB Busy error sent by the host?

@dhalbert
Copy link
Collaborator

The Report ID is no longer set at runtime. that line should be:

        + (0x85, 0x0B)  # Report ID  (11 decimal)

Do not put the Mouse class in boot.py. It will not be available when code.py runs. You need it to be in something you import from code.py directly or indirectly.

@JaggerJo
Copy link
Author

@dhalbert Thanks, just tried it but I get the same error as before (USB Busy).

The Mouse class is only in boot.py for testing purposes. I only want to see the mouse move one time right now.

@dhalbert
Copy link
Collaborator

dhalbert commented Oct 13, 2021

@JaggerJo The devices are not set up until just before code.py runs. USB is not turned on at all in boot.py.

This is explained in https://learn.adafruit.com/customizing-usb-devices-in-circuitpython/usb-setup-timing .

@JaggerJo
Copy link
Author

@dhalbert works. Thank you!

@ThomasAtBBTF
Copy link

Could you please describe a little bit in more detail what "works" ?
Are you simulating a mouse over USB or Bluetooth?
To what host do you connect your Mouse? Windows, Linux, MacOS, IOS?

@JaggerJo
Copy link
Author

Sure, I'm simulating a Mouse over USB.

Tried absolute positioning on a linux host.

Did not test it on windows/macOS yet.

@ThomasAtBBTF
Copy link

I also got "it working" on a ItsyBitsy nRF52840 as Bluetooth HID device.

But only on MAC OS !
(Did not test linux, but JaggerJo confirmed that it is working there)

Under Windows and under IOS absolute HID mouse reports do not seem to be accepted.

I contacted someone at Apple with a feature request to make it working also under IOS.

@dhalbert
Copy link
Collaborator

I tested some "Digitizer"-usage absolute positioning HID devices a long time ago, considering whether to include them in the standard devices we provided. I could not find a descriptor that would work on all three major operating systems, so I omitted it. At that time I did include a gamepad, but even it did not work on all three. That's why I dropped it when I added custom HID device capability.

@JaggerJo
Copy link
Author

Just tested it on MacOS (Big Sur) and absolute mouse positioning works fine.

@hdo
Copy link

hdo commented Nov 26, 2021

Just tested it on MacOS (Big Sur) and absolute mouse positioning works fine.

Do you mind posting your code example?
For some reason i can't get it running with 7.0 and PICO.

Thanks

@JaggerJo
Copy link
Author

JaggerJo commented Nov 27, 2021

This should move you mouse to an absolute position once.

# boot.py

import time
import usb_hid

# https://stackoverflow.com/questions/36750287/two-byte-report-count-for-hid-report-descriptor
absolute_mouse = usb_hid.Device(
    report_descriptor=bytes(
        # Absolute mouse
        (0x05, 0x01)  # Usage Page (Generic Desktop)
        + (0x09, 0x02)  # Usage (Mouse)
        + (0xA1, 0x01)  # Collection (Application)
        + (0x09, 0x01)  # Usage (Pointer)
        + (0xA1, 0x00)  # Collection (Physical)
        + (0x85, 0x0B)  # Report ID  [11 is SET at RUNTIME]
        # Buttons
        + (0x05, 0x09)  # Usage Page (Button)
        + (0x19, 0x01)  # Usage Minimum (0x01)
        + (0x29, 0x05)  # Usage Maximum (0x05)
        + (0x15, 0x00)  # Logical Minimum (0)
        + (0x25, 0x01)  # Logical Maximum (1)
        + (0x95, 0x05)  # Report Count (5)
        + (0x75, 0x01)  # Report Size (1)
        + (0x81, 0x02)  # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
        + (0x75, 0x03)  # Report Size (3)
        + (0x95, 0x01)  # Report Count (1)
        + (0x81, 0x03)  # Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
        # Movement
        + (0x05, 0x01)  # Usage Page (Generic Desktop Ctrls)
        + (0x09, 0x30)  # Usage (X)
        + (0x09, 0x31)  # Usage (Y)
        + (0x15, 0x00)  # LOGICAL_MINIMUM (0)       ; Note: 0x15 = 1 Byte; 0x16 = 2 Byte; 0x17 = 4 Byte
        + (0x26, 0xFF, 0x7F)  # LOGICAL_MAXIMUM (32767)   ; Note: 0x25 = 1 Byte, 0x26 = 2 Byte; 0x27 = 4 Byte Report
        # + (0x35, 0x00)        # Physical Minimum (0)
        # + (0x46, 0xff, 0x7f)  # Physical Maximum (32767)
        + (0x75, 0x10)  # REPORT_SIZE (16)
        + (0x95, 0x02)  # REPORT_COUNT (2)
        + (0x81, 0x02)  # Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
        # Wheel
        + (0x09, 0x38)  # Usage (Wheel)
        + (0x15, 0x81)  # Logical Minimum (-127)
        + (0x25, 0x7F)  # Logical Maximum (127)
        # + (0x35, 0x81)   # Physical Minimum (same as logical)
        # + (0x45, 0x7f)   # Physical Maximum (same as logical)
        + (0x75, 0x08)  # Report Size (8)
        + (0x95, 0x01)  # Report Count (1)
        + (0x81, 0x06)  # Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
        + (0xC0,)  # End Collection
        + (0xC0,)  # End Collection
    ),
    usage_page=1,
    usage=2,
    report_ids=(11,),
    in_report_lengths=(6,),
    out_report_lengths=(0,)
)

# usb_hid.enable((usb_hid.Device.KEYBOARD, absolute_mouse))
usb_hid.enable((absolute_mouse,))  # Make sure to have double brackets and comma\
import usb_hid
import time

def find_device(devices, *, usage_page, usage):
    """Search through the provided list of devices to find the one with the matching usage_page and
    usage."""
    if hasattr(devices, "send_report"):
        devices = [devices]
    for device in devices:
        if (
            device.usage_page == usage_page
            and device.usage == usage
            and hasattr(device, "send_report")
        ):
            return device
    raise ValueError("Could not find matching HID device.")

class Mouse:
    """Send USB HID mouse reports."""

    LEFT_BUTTON = 1
    """Left mouse button."""
    RIGHT_BUTTON = 2
    """Right mouse button."""
    MIDDLE_BUTTON = 4
    """Middle mouse button."""

    def __init__(self, devices):
        """Create a Mouse object that will send USB mouse HID reports.
        Devices can be a list of devices that includes a keyboard device or a keyboard device
        itself. A device is any object that implements ``send_report()``, ``usage_page`` and
        ``usage``.
        """
        self._mouse_device = find_device(devices, usage_page=0x1, usage=0x02)
        #print(dir(devices))
        # Reuse this bytearray to send mouse reports.
        # report[0] buttons pressed (LEFT, MIDDLE, RIGHT)
        # report[1] x1 movement
        # report[2] x2 movement
        # report[3] y1 movement
        # report[4] y2 movement
        # report[5] wheel movement
        self.report = bytearray(6)

        # Do a no-op to test if HID device is ready.
        # If not, wait a bit and try once more.
        try:
            self._send_no_move()
        except OSError:
            time.sleep(1)
            self._send_no_move()

    def press(self, buttons):
        """Press the given mouse buttons.
        :param buttons: a bitwise-or'd combination of ``LEFT_BUTTON``,
            ``MIDDLE_BUTTON``, and ``RIGHT_BUTTON``.
        Examples::
            # Press the left button.
            m.press(Mouse.LEFT_BUTTON)
            # Press the left and right buttons simultaneously.
            m.press(Mouse.LEFT_BUTTON | Mouse.RIGHT_BUTTON)
        """
        self.report[0] |= buttons
        self._send_no_move()

    def release(self, buttons):
        """Release the given mouse buttons.
        :param buttons: a bitwise-or'd combination of ``LEFT_BUTTON``,
            ``MIDDLE_BUTTON``, and ``RIGHT_BUTTON``.
        """
        self.report[0] &= ~buttons
        self._send_no_move()

    def release_all(self):
        """Release all the mouse buttons."""
        self.report[0] = 0
        self._send_no_move()

    def click(self, buttons):
        """Press and release the given mouse buttons.
        :param buttons: a bitwise-or'd combination of ``LEFT_BUTTON``,
            ``MIDDLE_BUTTON``, and ``RIGHT_BUTTON``.
        Examples::
            # Click the left button.
            m.click(Mouse.LEFT_BUTTON)
            # Double-click the left button.
            m.click(Mouse.LEFT_BUTTON)
            m.click(Mouse.LEFT_BUTTON)
        """
        self.press(buttons)
        self.release(buttons)

    def move(self, x=0, y=0, wheel=0):
        """Move the mouse and turn the wheel as directed.
        :param x: Set pointer on x axis. 32767 = 100% to the right
        :param y: Set pointer on y axis. 32767 = 100% to the bottom
        :param wheel: Rotate the wheel this amount. Negative is toward the user, positive
            is away from the user. The scrolling effect depends on the host.
        Examples::
            # Move 100 to the left. Do not move up and down. Do not roll the scroll wheel.
            m.move(1000, 3000, 0)
            # Same, with keyword arguments.
            m.move(x=1000, y=3000, wheel=0)
            # Roll the mouse wheel away from the user.
            m.move(wheel=1)
        """

        # Wheel
        while wheel != 0:
            partial_wheel = self._limit(wheel)
            print(wheel)
            self.report[5] = partial_wheel & 0xFF
            self._mouse_device.send_report(self.report)
            wheel -= partial_wheel

        # Coordinates
        x = self._limit_coord(x)
        y = self._limit_coord(y)
        # HID reports use little endian
        x1, x2 = (x & 0xFFFFFFFF).to_bytes(2, 'little')
        y1, y2 = (y & 0xFFFFFFFF).to_bytes(2, 'little')
        #print(x1)
        #print(x2)
        #print(y1)
        #print(y2)
        self.report[1] = x1
        self.report[2] = x2
        self.report[3] = y1
        self.report[4] = y2
        self._mouse_device.send_report(self.report)

    def _send_no_move(self):
        """Send a button-only report."""
        self.report[1] = 0
        self.report[2] = 0
        self.report[3] = 0
        self.report[4] = 0
        self._mouse_device.send_report(self.report)

    @staticmethod
    def _limit(dist):
        return min(127, max(-127, dist))

    @staticmethod
    def _limit_coord(coord):
        return min(32767, max(0, coord))
    
    
mouse = Mouse(usb_hid.devices)
# Note: Values are NOT pixels! 32767 = 100% (to right or to bottom)
mouse.move(x=20000, y=2000)

@hdo
Copy link

hdo commented Nov 29, 2021

Thanks a lot. This works for me under Windows 10 :-)

This should move you mouse to an absolute position once.

# boot.py

import time

@gree303
Copy link

gree303 commented Apr 27, 2023

@JaggerJo + @hdo
would you mind sharing some details on how to implement your solution?
I'm having a hard time; tried all sort of solutions via Arduino to get absolute position without success on a Qt Py ESP32-S2 nor Nano 33 iot.

Does this work with CuircuitPython 8.x too?
If not; does it support 7.3?

It would help me a lot if you could just point out the procedure roughly.
Thank you very much

@tkomde
Copy link

tkomde commented Nov 18, 2023

As I am interested in this, I wrote a sample.
Confirmed with CircuitPython 8.2.7 and MacOS Sonoma 14.1.

https://github.com/jins-tkomoda/circuitpython-custom-hid-reportmap-sample

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

No branches or pull requests

6 participants