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

New mouse peripheral and tests #94

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions examples/crazy-mouse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
#
# This file is part of FaceDancer.
#
""" USB mouse device example, makes the mouse go crazy on screen """

import asyncio

from facedancer import main
from facedancer.devices.mouse import USBMouseDevice

device = USBMouseDevice()


async def crazy_mouse():
"""Makes the mouse oscillate"""
while 1:
device.set_x(-10)
await asyncio.sleep(0.1)
device.set_x(10)
await asyncio.sleep(0.1)


main(device, crazy_mouse())
168 changes: 168 additions & 0 deletions facedancer/devices/mouse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""
Create a basic mouse device with three buttons and two axis
"""

from . import default_main
from .. import *
from ..classes.hid.descriptor import *
from ..classes.hid.usage import *


@use_inner_classes_automatically
class USBMouseDevice(USBDevice):
"""Simple USB mouse device."""

name: str = "USB Mouse Device"
product_string: str = "Non-suspicious Mouse"

# Local mouse state
_x: int = 0
_y: int = 0
_wheel: int = 0
_trigger: bool = False
_secondary: bool = False
_tertiary: bool = False

class MouseConfiguration(USBConfiguration):
"""Primary configuration : act as a mouse"""

max_power: int = 100
self_powered: bool = False
supports_remote_wakeup: bool = True

class MouseInterface(USBInterface):
"""Core HID interface for our mouse"""

name: str = "Generic USB mouse interface"
class_number: int = 3 # Human Interface Device class number

class MouseEventEndpoint(USBEndpoint):
"""Interrupt IN endpoint for guaranteed max latency"""

number: int = 1
direction: USBDirection = USBDirection.IN
transfer_type: USBTransferType = USBTransferType.INTERRUPT
interval: int = 10

class MouseHIDDescriptor(USBClassDescriptor):
"""Container for the mouse HID report descriptors"""

number: int = USBDescriptorTypeNumber.HID

# raw descriptor fields
bLength: bytes = b"\x09"
bHIDDescriptorType: bytes = b"\x21" # HID descriptor type
bcdHID: bytes = b"\x11\x01" # HID 1.11
bCountryCode: bytes = b"\x00"
bNumDescriptors: bytes = b"\x01"
bDescriptorType: bytes = b"\x22" # Report descriptor type
wDescriptorLength: bytes = (
b"\x3e\x00" # 62 -- TODO should be computed automatically
)

raw: bytes = (
bLength
+ bHIDDescriptorType
+ bcdHID
+ bCountryCode
+ bNumDescriptors
+ bDescriptorType
+ wDescriptorLength
)

class MouseReportDescriptor(HIDReportDescriptor):
"""Defines the mouse report descriptor :
* X/Y axis
* three buttons (trigger/primary, secondary, tertiary)
"""

fields: tuple = (
USAGE_PAGE(HIDUsagePage.GENERIC_DESKTOP),
USAGE(HIDGenericDesktopUsage.MOUSE),
COLLECTION(HIDCollection.APPLICATION),
USAGE(HIDGenericDesktopUsage.POINTER),
COLLECTION(HIDCollection.PHYSICAL),
USAGE_PAGE(HIDUsagePage.BUTTONS),
USAGE_MINIMUM(0x01), # see HID 1.11
USAGE_MAXIMUM(0x03),
LOGICAL_MINIMUM(0x0),
LOGICAL_MAXIMUM(0x01),
REPORT_SIZE(1),
REPORT_COUNT(3),
INPUT(variable=True, relative=False),
REPORT_SIZE(5),
REPORT_COUNT(1),
INPUT(variable=True, constant=True),
USAGE_PAGE(HIDUsagePage.GENERIC_DESKTOP),
USAGE(HIDGenericDesktopUsage.X),
USAGE(HIDGenericDesktopUsage.Y),
LOGICAL_MINIMUM(0x81), # -127
LOGICAL_MAXIMUM(0x7F), # 127
REPORT_SIZE(8),
REPORT_COUNT(2),
INPUT(variable=True, relative=True),
USAGE(HIDGenericDesktopUsage.WHEEL),
LOGICAL_MINIMUM(0x81), # -127
LOGICAL_MAXIMUM(0x7F), # 127
REPORT_SIZE(8),
REPORT_COUNT(1),
INPUT(variable=True, relative=True),
END_COLLECTION(),
END_COLLECTION(),
)

@class_request_handler(number=USBStandardRequests.GET_INTERFACE)
@to_this_interface
def handle_get_interface_request(self, request):
# Silently stall GET_INTERFACE class requests.
request.stall()

def set_x(self, x: int):
"""Set X axis translation"""
self._x = x

def set_y(self, y: int):
"""Set Y axis translation"""
self._y = y

def set_wheel(self, rotation: int):
"""Set rotation"""
self._wheel = rotation

def set_trigger(self, down: bool):
"""Set down to True to trigger primary button"""
self._trigger = down

def set_secondary(self, down: bool):
"""Set down to True to trigger secondary button"""
self._secondary = down

def set_tertiary(self, down: bool):
"""Set down to True to trigger tertiary button"""
self._tertiary = down

def _get_buttons_state(self):
"""Create buttons report from current state"""
state = 0x00

if self._trigger:
state |= 1 << 0
if self._secondary:
state |= 1 << 1
if self._tertiary:
state |= 1 << 2

return state

def handle_data_requested(self, endpoint: USBEndpoint):
"""Provide data once per host request."""
endpoint.send(
self._get_buttons_state().to_bytes(1, "little")
+ self._x.to_bytes(1, "little", signed=True)
+ self._y.to_bytes(1, "little", signed=True)
+ self._wheel.to_bytes(1, "little", signed=True)
)


if __name__ == "__main__":
default_main(USBMouseDevice)
104 changes: 104 additions & 0 deletions test/loopback_fullspeed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
#
#
"""
Create a basic mouse device with three buttons and two axis
Copy link
Member

Choose a reason for hiding this comment

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

This comment should probably be changed to fit the device :)

"""

from facedancer.devices import default_main
from facedancer import *


@use_inner_classes_automatically
class USBLoopback(USBDevice):
"""Loopback on EP1"""

name: str = "USB EP1 Loopback"
product_string: str = "Loopback device"
max_packet_size_ep0: int = 64
device_speed : DeviceSpeed = DeviceSpeed.FULL

EP_MAX_SIZE = 64
buffer = [None, None, None, None, None]

class USBLoopbackConfiguration(USBConfiguration):
"""Primary configuration : act as a mouse"""

max_power: int = 100
self_powered: bool = False
supports_remote_wakeup: bool = True
ep_in_ready: bool = False

class USBLoopbackInterface(USBInterface):
"""Core interface"""

name: str = "Loopback device"
class_number: int = 0xff # Vendor class

class USBLoopbackOUT1(USBEndpoint):
"""Interrupt IN endpoint for guaranteed max latency"""

number: int = 1
direction: USBDirection = USBDirection.OUT
transfer_type: USBTransferType = USBTransferType.BULK
max_packet_size: int = 64

class USBLoopbackIN1(USBEndpoint):
"""Interrupt IN endpoint for guaranteed max latency"""

number: int = 1
direction: USBDirection = USBDirection.IN
transfer_type: USBTransferType = USBTransferType.BULK
max_packet_size: int = 64

class USBLoopbackOUT2(USBEndpoint):
"""Interrupt IN endpoint for guaranteed max latency"""

number: int = 2
direction: USBDirection = USBDirection.OUT
transfer_type: USBTransferType = USBTransferType.BULK
max_packet_size: int = 64

class USBLoopbackIN2(USBEndpoint):
"""Interrupt IN endpoint for guaranteed max latency"""

number: int = 2
direction: USBDirection = USBDirection.IN
transfer_type: USBTransferType = USBTransferType.BULK
max_packet_size: int = 64

class USBLoopbackOUT3(USBEndpoint):
"""Interrupt IN endpoint for guaranteed max latency"""

number: int = 3
direction: USBDirection = USBDirection.OUT
transfer_type: USBTransferType = USBTransferType.BULK
max_packet_size: int = 64

class USBLoopbackIN3(USBEndpoint):
"""Interrupt IN endpoint for guaranteed max latency"""

number: int = 3
direction: USBDirection = USBDirection.IN
transfer_type: USBTransferType = USBTransferType.BULK
max_packet_size: int = 64

@class_request_handler(number=USBStandardRequests.GET_INTERFACE)
@to_this_interface
def handle_get_interface_request(self, request):
# Silently stall GET_INTERFACE class requests.
request.stall()

def handle_data_received(self, ep, data):
print(f"received {len(data)} bytes on {ep}")
self.buffer[ep.number] = data

def handle_data_requested(self, ep):
"""Provide data once per host request."""
if self.buffer[ep.number] is not None:
self.send(ep.number, self.buffer[ep.number])
self.buffer[ep.number] = None


if __name__ == "__main__":
default_main(USBLoopback)
Loading