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_control_endpoint(self, endpoint_number: int, in_request: USBControlRequest, data: bytes, blocking: bool=True):
 97        """
 98        Sends a collection of USB data in response to a IN control request by the host.
 99
100        Args:
101            endpoint_number  : The number of the IN endpoint on which data should be sent.
102            in_request       : The control request being responded to.
103            data             : The data to be sent.
104            blocking         : If true, this function should wait for the transfer to complete.
105        """
106        # Truncate data to requested length and forward to `send_on_endpoint()` for backends
107        # that do not need to support this method.
108        return self.send_on_endpoint(endpoint_number, data[:in_request.length], blocking)
109
110
111    def send_on_endpoint(self, endpoint_number: int, data: bytes, blocking: bool=True):
112        """
113        Sends a collection of USB data on a given endpoint.
114
115        Args:
116            endpoint_number : The number of the IN endpoint on which data should be sent.
117            data : The data to be sent.
118            blocking : If true, this function should wait for the transfer to complete.
119        """
120        raise NotImplementedError
121
122
123    def ack_status_stage(self, direction: USBDirection=USBDirection.OUT, endpoint_number:int =0, blocking: bool=False):
124        """
125        Handles the status stage of a correctly completed control request,
126        by priming the appropriate endpoint to handle the status phase.
127
128        Args:
129            direction : Determines if we're ACK'ing an IN or OUT vendor request.
130                       (This should match the direction of the DATA stage.)
131            endpoint_number : The endpoint number on which the control request
132                              occurred.
133            blocking : True if we should wait for the ACK to be fully issued
134                       before returning.
135        """
136
137
138    def stall_endpoint(self, endpoint_number:int, direction: USBDirection=USBDirection.OUT):
139        """
140        Stalls the provided endpoint, as defined in the USB spec.
141
142        Args:
143            endpoint_number : The number of the endpoint to be stalled.
144        """
145        raise NotImplementedError
146
147
148    def clear_halt(self, endpoint_number:int, direction: USBDirection):
149        """ Clears a halt condition on the provided non-control endpoint.
150
151        Args:
152            endpoint_number : The endpoint number
153            direction       : The endpoint direction; or OUT if not provided.
154        """
155        # FIXME do nothing as only the moondancer backend supports this for now
156        # raise NotImplementedError
157        pass
158
159
160    def service_irqs(self):
161        """
162        Core routine of the Facedancer execution/event loop. Continuously monitors the
163        Facedancer's execution status, and reacts as events occur.
164        """
165        raise NotImplementedError
166
167
168    def validate_configuration(self, configuration: USBConfiguration):
169        """
170        Check if this backend is able to support this configuration.
171        Raises an exception if it is not.
172
173        Args:
174            configuration : The configuration to validate.
175        """
176        if configuration is None:
177            return
178
179        # Currently, endpoints are only set up in the configured() method, and
180        # cannot be changed on the fly by SET_INTERFACE requests.
181        #
182        # Therefore, no backends are able to support configurations which
183        # re-use endpoint addresses between alternate interface settings.
184        used_addresses = set()
185        for interface in configuration.get_interfaces():
186            for endpoint in interface.get_endpoints():
187                address = endpoint.get_identifier()
188                if address in used_addresses:
189                    raise Exception(
190                        f"This configuration cannot currently be supported, "
191                        f"because it re-uses endpoint address 0x{address:02X} "
192                        f"between multiple interface definitions.")
193                used_addresses.add(address)

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.