Custom Touchscreen Interaction

Note

The touchscreen must be enabled and configured before using it in tasks. See Screen Integration for hardware setup and settings.

Every time a touch event passes the debounce filter, the trigger method of TouchTriggerBase is called. By default it writes the touch coordinates to the box camera feed. You can override this behavior to implement task-specific touch responses.


Creating a custom TouchTrigger

Create a file named touch_trigger inside your project’s code directory and define a class named TouchTrigger that inherits from TouchTriggerBase. The system will automatically detect it and use it instead of the default base class.

from village.custom_classes.touch_trigger_base import TouchTriggerBase


class TouchTrigger(TouchTriggerBase):

    def __init__(self) -> None:
        super().__init__()

    def trigger(self, x: int, y: int, timestamp: float) -> None:
        """Called on every touch event that passes the debounce interval.

        Args:
            x (int): Touch x position in screen pixels.
            y (int): Touch y position in screen pixels.
            timestamp (float): Monotonic timestamp of the event (seconds).

        Available via self.task:
        - self.task.cam_box      — box camera (write_text, areas, position, …)
        - self.task.bpod         — Bpod controller (send_softcode_to_bpod, …)
        - any attribute defined in the task class
        """
        self.task.cam_box.write_text(f"Touch: {x}, {y}")

Example — checking a target area and sending a softcode to Bpod

A common pattern in touchscreen tasks is to define a circular target on the stimulus screen and determine whether the animal’s touch landed inside or outside it. The result is then sent to Bpod as a softcode so that the state machine can branch accordingly.

In the task, define the target position and tolerance as instance attributes so the TouchTrigger can read them via self.task:

class MyTask(Task):

    def start_trial(self):
        # Position and radius of the target on the stimulus screen (pixels)
        self.target_x: int = 640
        self.target_y: int = 360
        self.tolerance_px: int = 80
        self.touch_correct: bool = False

        # ... build and run the state machine ...

In touch_trigger.py, check the distance to the target and send the appropriate softcode:

from village.custom_classes.touch_trigger_base import TouchTriggerBase


class TouchTrigger(TouchTriggerBase):

    def __init__(self) -> None:
        super().__init__()

    def trigger(self, x: int, y: int, timestamp: float) -> None:
        task = self.task

        # Compute distance from touch to the current target
        dx = x - getattr(task, "target_x", 0)
        dy = y - getattr(task, "target_y", 0)
        distance = (dx**2 + dy**2) ** 0.5
        tolerance = getattr(task, "tolerance_px", 80)

        if distance <= tolerance:
            task.touch_correct = True
            task.cam_box.write_text(f"Correct touch: {x}, {y}")
            task.bpod.send_softcode_to_bpod(1)   # softcode 1 → correct
        else:
            task.touch_correct = False
            task.cam_box.write_text(f"Wrong touch: {x}, {y}")
            task.bpod.send_softcode_to_bpod(2)   # softcode 2 → incorrect

The Bpod state machine receives the softcode and can use it to branch:

sma.add_state(
    state_name="WaitForTouch",
    state_timer=5,
    state_change_conditions={
        "Tup": "Timeout",
        "SoftCode1": "Reward",
        "SoftCode2": "Punish",
    },
    output_actions=[],
)

Note

trigger runs in the touch reader thread, not in the task thread. Keep the method fast and avoid blocking calls. Writing to cam_box and sending a softcode are both non-blocking and safe to call from here.