How to write a new Facedancer Backend

Facedancer board backends can be found in the facedancer/backends/ directory.

To create a new backend, follow these steps:

1. Derive a new backend class

All Facedancer board backends inherit from the FacedancerApp and FacedancerBackend classes. Begin by deriving your new backend class from these base classes, as shown below:

from facedancer.core           import FacedancerApp
from facedancer.backends.base  import FacedancerBackend

class MydancerBackend(FacedancerApp, FacedancerBackend):

    app_name = "Mydancer"

2. Implement backend callback methods

Your new backend must implement the required callback methods defined in the FacedancerBackend class. These methods contain the functionality specific to your Facedancer board:

  1from typing    import List
  2from ..        import *
  3
  4
  5class FacedancerBackend:
  6    def __init__(self, device: USBDevice=None, verbose: int=0, quirks: List[str]=[]):
  7        """
  8        Initializes the backend.
  9
 10        Args:
 11            device  :  The device that will act as our Facedancer.   (Optional)
 12            verbose : The verbosity level of the given application. (Optional)
 13            quirks  :  List of USB platform quirks.                  (Optional)
 14        """
 15        raise NotImplementedError
 16
 17
 18    @classmethod
 19    def appropriate_for_environment(cls, backend_name: str) -> bool:
 20        """
 21        Determines if the current environment seems appropriate
 22        for using this backend.
 23
 24        Args:
 25            backend_name : Backend name being requested. (Optional)
 26        """
 27        raise NotImplementedError
 28
 29
 30    def get_version(self):
 31        """
 32        Returns information about the active Facedancer version.
 33        """
 34        raise NotImplementedError
 35
 36
 37    def connect(self, usb_device: USBDevice, max_packet_size_ep0: int=64, device_speed: DeviceSpeed=DeviceSpeed.FULL):
 38        """
 39        Prepares backend to connect to the target host and emulate
 40        a given device.
 41
 42        Args:
 43            usb_device : The USBDevice object that represents the emulated device.
 44            max_packet_size_ep0 : Max packet size for control endpoint.
 45            device_speed : Requested usb speed for the Facedancer board.
 46        """
 47        raise NotImplementedError
 48
 49
 50    def disconnect(self):
 51        """ Disconnects Facedancer from the target host. """
 52        raise NotImplementedError
 53
 54
 55    def reset(self):
 56        """
 57        Triggers the Facedancer to handle its side of a bus reset.
 58        """
 59        raise NotImplementedError
 60
 61
 62    def set_address(self, address: int, defer: bool=False):
 63        """
 64        Sets the device address of the Facedancer. Usually only used during
 65        initial configuration.
 66
 67        Args:
 68            address : The address the Facedancer should assume.
 69            defer   : True iff the set_address request should wait for an active transaction to
 70                      finish.
 71        """
 72        raise NotImplementedError
 73
 74
 75    def configured(self, configuration: USBConfiguration):
 76        """
 77        Callback that's issued when a USBDevice is configured, e.g. by the
 78        SET_CONFIGURATION request. Allows us to apply the new configuration.
 79
 80        Args:
 81            configuration : The USBConfiguration object applied by the SET_CONFIG request.
 82        """
 83        raise NotImplementedError
 84
 85
 86    def read_from_endpoint(self, endpoint_number: int) -> bytes:
 87        """
 88        Reads a block of data from the given endpoint.
 89
 90        Args:
 91            endpoint_number : The number of the OUT endpoint on which data is to be rx'd.
 92        """
 93        raise NotImplementedError
 94
 95
 96    def send_on_endpoint(self, endpoint_number: int, data: bytes, blocking: bool=True):
 97        """
 98        Sends a collection of USB data on a given endpoint.
 99
100        Args:
101            endpoint_number : The number of the IN endpoint on which data should be sent.
102            data : The data to be sent.
103            blocking : If true, this function should wait for the transfer to complete.
104        """
105        raise NotImplementedError
106
107
108    def ack_status_stage(self, direction: USBDirection=USBDirection.OUT, endpoint_number:int =0, blocking: bool=False):
109        """
110        Handles the status stage of a correctly completed control request,
111        by priming the appropriate endpoint to handle the status phase.
112
113        Args:
114            direction : Determines if we're ACK'ing an IN or OUT vendor request.
115                       (This should match the direction of the DATA stage.)
116            endpoint_number : The endpoint number on which the control request
117                              occurred.
118            blocking : True if we should wait for the ACK to be fully issued
119                       before returning.
120        """
121
122
123    def stall_endpoint(self, endpoint_number:int, direction: USBDirection=USBDirection.OUT):
124        """
125        Stalls the provided endpoint, as defined in the USB spec.
126
127        Args:
128            endpoint_number : The number of the endpoint to be stalled.
129        """
130        raise NotImplementedError
131
132
133    def service_irqs(self):
134        """
135        Core routine of the Facedancer execution/event loop. Continuously monitors the
136        Facedancer's execution status, and reacts as events occur.
137        """
138        raise NotImplementedError

3. Implement the backend event loop

Facedancer uses a polling approach to service events originating from the Facedancer board.

The actual events that need to be serviced will be specific to your Facedancer board but will generally include at least the following:

  • Receiving a setup packet.

  • Receiving data on an endpoint.

  • Receiving NAK events (e.g. host requested data from an IN endpoint)

Facedancer will take care of scheduling execution of the service_irqs() callback but it is up to you to dispatch any events generated by your board to the corresponding methods of the Facedancer USBDevice object obtained in the FacedancerBackend.connect() callback.

That said, most backend implementations will follow a pattern similiar to the pseudo-code below:

class MydancerBackend(FacedancerApp, FacedancerBackend):

    ...

    def service_irqs(self):
        """
        Core routine of the Facedancer execution/event loop. Continuously monitors the
        Moondancer's execution status, and reacts as events occur.
        """

        # obtain latest events and handle them
        for event in self.mydancer.get_events():
            match event:
                case USB_RECEIVE_SETUP:
                    self.usb_device.create_request(event.data)
                case USB_RECEIVE_PACKET:
                    self.usb_device.handle_data_available(event.endpoint_number, event.data)
                case USB_EP_IN_NAK:
                    self.usb_device.handle_nak(event.endpoint_number)

Additionally, referencing the service_irqs methods of the other backend implementations can provide valuable insights into handling events specific to your implementation.