Device: Touch Screen - fdechaumont/micecraft GitHub Wiki

The touchscreen is a device that can display anything and handle touch events by animals. Images can be displayed in grid layouts, at specific XY coordinates or moving.

** PHOTO **

[!NOTE] This device is design for both Mice and Rats thanks to different operational modes.

  • Documentation
  • Examples
  • Blueprint
  • Manage Alarms
  • Auto-Reconnect

Events Handling

When an animal (or user) touches the screen, the TouchScreen fires a DeviceEvent. The event.description string indicates the type of touch:

  • Grid-based image
    • exact string: "symbol touched" + data
    • data: (image_name, image_id, grid_x, grid_y, touch_x, touch_y)
  • XY-based image
    • exact string: "symbol xy touched" + data
    • data: (image_name, image_id, image_center_x, image_center_y, touch_x, touch_y)
  • Missed touch
    • exact string: "missed" + data
    • data: (touch_x, touch_y)
  • Device Error
    • exact string: "traceback"

Where image_id is the id of the image in the device memory, grid_x and grid_y are the grid coordinates of the image, image_center_x and image_center_y are the coordinates of the center of the image touched, and touch_x and touch_y are the coordinates of the touch.

Image Configuration

  • Grid-based image
    • method: setImage(id, x, y)
    • places a predefined image ID at a specific grid coordinate (x, y)
  • XY based image
    • method: setXYImage(name, id, centerX, centerY, rotation, scale)
    • places an image on precise centered pixel coordinates (centerX, centerY) with rotation and scaling options
  • Remove all images (black screen)
    • method: clear()
    • remove all displayed images
  • Remove grid-based image
    • method: removeImage(x, y)
    • removes the image at the specified grid coordinate
  • Remove XY-based image
    • removeXYImage(name)
    • removes a specific XY image by its unique name

[!TIP] The screen size is 1920 x 1080 pixels.

Blueprints:

[!NOTE] Will be released on publication

Example:

In this example, we create a touchscreen, and we listen to its events.

[!IMPORTANT] You need to check the COM port where the device is connected.

from micecraft.soft.device_event.DeviceEvent import DeviceEvent
from micecraft.devices.touchscreen.TouchScreen import TouchScreen


# 1. Define a listener for touch events
def my_listener(event: DeviceEvent) -> None:
    if "symbol xy touched" in event.description:
        img_name, img_id, center_x, center_y, touch_x, touch_y = event.data
        name_info = f"Touched {img_name} image ID: {img_id}"
        center_info = f"Image center: ({center_x}, {center_y})"
        touch_info = f"Touch point: ({touch_x}, {touch_y})"
        print(f"{name_info}, {center_info}, {touch_info}")


if __name__ == "__main__":

    # 2. Initialize the TouchScreen and add the listener
    ts = TouchScreen(comPort="COM39")
    ts.addDeviceListener(my_listener)

    # 3. Clear screen
    ts.clear()

    # 4. Display images on left and right
    center_x = 1920 / 2
    center_y = 750
    half_size = 400

    ts.setXYImage(
        name="example_left_image",
        id=0,
        centerX=center_x - half_size,
        centerY=center_y,
        rotation=0,
        scale=1,
    )

    ts.setXYImage(
        name="example_right_image",
        id=1,
        centerX=center_x + half_size,
        centerY=center_y,
        rotation=0,
        scale=1,
    )

    # 5. Shutdown the TouchScreen after testing
    input("Press enter to stop the test")
    ts.shutdown()

Features

Configuration & Modes

  • setConfig(nbCols, nbRows, imageSize): Defines the grid layout and the size of the images displayed within the grid.
  • setMouseMode() / setRatMode(): Configures animal mode of the touchscreen.
  • setNormalMode(): Disables any special rotation of touch coordinates.
  • setTransparency(transparency): Sets the transparency level (0.0 to 1.0) for displayed images.
  • ping(): Sends a heartbeat to the device to check connectivity.
  • showCalibration(bool): Toggles the visibility of calibration points and pointers on the hardware device. addDeviceListener(listener): Registers a callback function that receives DeviceEvent objects when the screen is touched or a system event occurs.

[!TIP] If the TouchScreen display red lines on the screen, they are calibrations lines. To remove them, use the showCalibration method with False as input.

Standalone example code

To run a an example of the TouchScreen, you need to setup it in a standalone QT app. Full code here:

import sys
import time
import random
import threading
import traceback

from PyQt6 import QtCore
from PyQt6.QtGui import QPainter, QPaintEvent
from PyQt6.QtWidgets import QWidget, QApplication, QPushButton, QVBoxLayout

from micecraft.soft.device_event.DeviceEvent import DeviceEvent
from micecraft.devices.touchscreen.TouchScreen import TouchScreen


class VisualExperiment(QWidget):
    """This class implements a simple visual experiment using PyQt6
    and the TouchScreen device."""

    refresher = QtCore.pyqtSignal()

    def __init__(self, *args, **kwargs) -> None:
        """Initialize the visual experiment."""
        super().__init__(*args, **kwargs)
        self.shuttingDown = False

        print("Starting visual experiment...")

    def shutdown(self) -> None:
        """Shut down the visual experiment and release all resources."""
        print("Exiting...")
        self.shuttingDown = True
        self.touchscreen.shutdown()
        print("Done.")

    def on_refresh_data(self) -> None:
        """Refresh the visual experiment data."""
        self.update()

    def monitorGUI(self) -> None:
        """Monitor the GUI and emit refresh signals at regular intervals."""
        while self.shuttingDown == False:
            self.refresher.emit()
            time.sleep(0.1)

    def listener(self, event: DeviceEvent) -> None:
        """Listener for DeviceEvent from all devices."""
        if "symbol xy touched" in event.description:
            if not self.touch_enabled:
                print("Touch event received but touch is currently disabled.")
                return
            assert event.data is not None
            img_name, img_id, center_x, center_y, touch_x, touch_y = event.data
            name_info = f"Touched {img_name} image ID: {img_id}"
            center_info = f"Image center: ({center_x}, {center_y})"
            touch_info = f"Touch point: ({touch_x}, {touch_y})"
            print(f"{name_info}, {center_info}, {touch_info}")
            self.on_touch(True)
        elif "missed" in event.description:
            print("Touch event received but no image was touched.")
            self.on_touch(False)
        else:
            print(f"Received event: {event.description}")

    def start(self) -> None:
        """Initialize the user interface (UI) and start the visual
        experiment."""

        # App window
        # ----------------
        self.resize(400, 400)
        self.setWindowTitle("MiceCraft - Lever display test")

        # Layout for buttons
        layout = QVBoxLayout(self)

        self.btn_touch = QPushButton("Simulate Touch (Correct)", self)
        self.btn_touch.clicked.connect(self.simulate_touch)
        layout.addWidget(self.btn_touch)

        self.btn_miss = QPushButton("Simulate Miss", self)
        self.btn_miss.clicked.connect(self.simulate_miss)
        layout.addWidget(self.btn_miss)

        # Visual elements
        # ----------------

        self.touchscreen = TouchScreen("COM39")  # Create a touchscreen
        self.touchscreen.addDeviceListener(self.listener)

        self.touch_enabled = True

    def simulate_touch(self) -> None:
        """Simulate a successful touch event."""
        event = DeviceEvent(
            deviceType="touchscreen",
            deviceObject=self.touchscreen,
            description="symbol xy touched ",
            data=("simulate", 0, 0, 0, 0, 0),
        )
        self.listener(event)

    def simulate_miss(self) -> None:
        """Simulate a missed touch event."""
        event = DeviceEvent(
            deviceType="touchscreen",
            deviceObject=self.touchscreen,
            description="missed",
            data=None,
        )
        self.listener(event)

    def on_touch(self, result: bool) -> None:
        """Show the results on the touchscreen for 1 second then display a
        random image."""
        self.touch_enabled = False  # Disable touch until next display update

        if result:
            result_name = "example_correct_image"
            result_id = 0

        else:
            result_name = "example_wrong_image"
            result_id = 1

        self.touchscreen.clear()
        center_x = 1920 / 2
        center_y = 750

        self.touchscreen.setXYImage(
            name=result_name,
            id=result_id,
            centerX=center_x,
            centerY=center_y,
            rotation=0,
            scale=1,
        )

        time.sleep(1)  # Show result for 1 second

        self.random_display()
        self.touch_enabled = True  # Re-enable touch for next round

    def random_display(self) -> None:
        """Display an example image randomly on one side of the screen."""
        self.touchscreen.clear()
        side = random.choice(["left", "right"])

        image_name = f"example_{side}_image"
        image_id = 3

        center_x = 1920 / 2
        center_y = 750
        half_size = 400

        if side == "left":
            img_center_x = center_x - half_size
        else:
            img_center_x = center_x + half_size

        self.touchscreen.setXYImage(
            name=image_name,
            id=image_id,
            centerX=img_center_x,
            centerY=center_y,
            rotation=0,
            scale=1,
        )

    def paintEvent(self, event: QPaintEvent) -> None:
        """Override the paint event to draw custom visuals on the widget."""
        super().paintEvent(event)
        painter = QPainter()
        painter.begin(self)

        # here should be located your custom display code

        painter.end()


def excepthook(type_, value, traceback_) -> None:
    """Custom exception hook to print the traceback and exit properly."""
    traceback.print_exception(type_, value, traceback_)
    QtCore.qFatal("")


def shutdown_handler() -> None:
    """This function is called when the application is about to quit.
    It ensures that all programs and devices are properly shutdown."""
    visualExperiment.shutdown()


if __name__ == "__main__":

    sys.excepthook = excepthook

    app = QApplication(sys.argv)
    app.setQuitOnLastWindowClosed(True)
    app.aboutToQuit.connect(shutdown_handler)

    visualExperiment = VisualExperiment()
    visualExperiment.start()
    visualExperiment.show()

    sys.exit(app.exec())

This provides the following display:

** TODO: SCREENSHOT **

Note that until you connect a touchscreen, you will see a red dot blinking on the component complaining that there is no touchscreen connected.

** TODO: SCREENSHOT **