Part 1: Setup Interface#

The setup interface is the first component of a visual interface in MyoGestic. It is responsible for:

  • Configuring VI-specific parameters (ports, executable paths, etc.).

  • Launching and stopping the external VI process.

  • Managing bi-directional communication (outgoing predictions, incoming kinematics) via UDP or another protocol.

  • Providing custom signals and data buffers for the Online protocol.

Step 1: Create the UI#

Design a Qt .ui file with at least a groupBox containing a start/stop button. Convert it with pyside6-uic.

Note

Copy an existing UI file from myogestic/gui/widgets/visual_interfaces/virtual_hand_interface/ui/ as a starting point.

Step 2: Understand the Base Class#

Your setup interface must inherit from:

SetupInterfaceTemplate(*args, **kwargs)

Base class for the setup interface of a visual interface.

The base class provides:

You must implement these abstract methods:

Optionally override:

  • get_custom_save_data() – Return extra data to include when saving recordings (e.g., predicted hand positions).

Step 3: Implement the Setup Interface#

Below is a minimal self-contained example, followed by references to the full VHI implementation.

Step 3.1: Minimal Setup Interface#

This shows the skeleton of a setup interface. In practice, you would add UDP socket management, process launching, and data streaming.

import numpy as np
from PySide6.QtCore import QByteArray
from PySide6.QtGui import QCloseEvent

from myogestic.gui.widgets.templates.visual_interface import SetupInterfaceTemplate


class MyInterface_SetupInterface(SetupInterfaceTemplate):
    """Minimal setup interface example."""

    def __init__(self, main_window, name: str = "MyInterface"):
        # Import your generated UI class:
        # from myogestic.gui.widgets.visual_interfaces.my_interface.ui import Ui_MySetup
        # super().__init__(main_window, name, ui=Ui_MySetup())
        #
        # For this example we pass None (won't run without a real UI):
        pass  # Replace with super().__init__(...)

    def initialize_ui_logic(self) -> None:
        """Wire up buttons, labels, and other UI elements."""
        pass

    def start_interface(self) -> None:
        """Launch the external VI process and start UDP communication."""
        pass

    def stop_interface(self) -> None:
        """Stop the VI process and close sockets."""
        pass

    def interface_was_killed(self) -> None:
        """Handle unexpected VI termination (e.g., user closed window)."""
        pass

    def close_event(self, event: QCloseEvent) -> None:
        """Clean up on application exit."""
        pass

    def connect_custom_signals(self) -> None:
        """Connect data streaming signals (called when online starts)."""
        pass

    def disconnect_custom_signals(self) -> None:
        """Disconnect data streaming signals (called when online stops)."""
        pass

    def clear_custom_signal_buffers(self) -> None:
        """Reset internal data buffers."""
        pass

    def get_custom_save_data(self) -> dict:
        """Return extra data to include when saving recordings."""
        return {}

Step 3.2: VHI Reference – Class Definition and Helpers#

The VHI setup interface launches a Unity executable, manages UDP communication on multiple ports, and streams kinematics at 32 Hz.

Imports#
 1import ast
 2import platform
 3import subprocess
 4import sys
 5import time
 6from pathlib import Path
 7
 8import numpy as np
 9from PySide6.QtCore import QByteArray, QMetaObject, QProcess, QTimer, Qt, Signal
10from PySide6.QtGui import QCloseEvent
11from PySide6.QtNetwork import QHostAddress, QUdpSocket
12from PySide6.QtWidgets import QCheckBox, QPushButton, QWidget
13
14from myogestic.gui.widgets.logger import LoggerLevel
15from myogestic.gui.widgets.templates.visual_interface import SetupInterfaceTemplate
16from myogestic.gui.widgets.visual_interfaces.virtual_hand_interface.ui import (
17    Ui_SetupVirtualHandInterface
18)
19from myogestic.utils.constants import MYOGESTIC_UDP_PORT
20
21# Stylesheets
22NOT_CONNECTED_STYLESHEET = "background-color: red; border-radius: 5px;"
23CONNECTED_STYLESHEET = "background-color: green; border-radius: 5px;"
24
25# Constants
26STREAMING_FREQUENCY = 32
27TIME_BETWEEN_MESSAGES = 1 / STREAMING_FREQUENCY
28
29SOCKET_IP = "127.0.0.1"
30STATUS_REQUEST = "status"
31STATUS_RESPONSE = "active"
32
33
34# Ports
35# on this port the VHI listens for incoming messages from MyoGestic
36VHI__UDP_PORT = 1236
37
38# on this port the VHI sends the currently displayed predicted hand after having applied linear interpolation
39VHI_PREDICTION__UDP_PORT = 1234
40
Constructor#
65    def __init__(self, main_window, name="VirtualHandInterface"):
66        super().__init__(main_window, name, ui=Ui_SetupVirtualHandInterface())
67
68        self._unity_process = QProcess()
69        executable = self._get_unity_executable().resolve()  # Get absolute path
70        self._unity_process.setProgram(str(executable))
71
72        # On macOS, set working directory to the app's MacOS folder for Unity to find resources
73        if platform.system() == "Darwin":
74            self._unity_process.setWorkingDirectory(str(executable.parent))
75
76        self._unity_process.started.connect(
77            lambda: self._main_window.toggle_selected_visual_interface(self.name)
78        )
79        self._unity_process.finished.connect(self.interface_was_killed)
80        self._unity_process.finished.connect(
81            lambda: self._main_window.toggle_selected_visual_interface(self.name)
82        )
83        self._unity_process.errorOccurred.connect(self._on_process_error)
84        self._unity_process.readyReadStandardError.connect(self._on_stderr)
85
86        self._setup_timers()
87
88        self._last_message_time = time.time()
89        self._is_connected: bool = False
90        self._streaming__udp_socket: QUdpSocket | None = None
91
92        # Custom Stuff
93        self._predicted_hand__udp_socket: QUdpSocket | None = None
94        self._predicted_hand_recording__buffer: list[(float, np.ndarray)] = []
95
96        # Initialize Virtual Hand Interface UI
97        self.initialize_ui_logic()

Step 3.3: VHI Reference – Lifecycle Methods#

start_interface – Launch Unity and open UDP sockets#
208    def start_interface(self):
209        """Start the Virtual Hand Interface."""
210        if not self._use_external_virtual_hand_interface__check_box.isChecked():
211            # Prepare macOS executable (chmod +x and remove quarantine)
212            executable = self._get_unity_executable().resolve()
213            self._prepare_macos_executable(executable)
214            self._unity_process.start()
215            self._unity_process.waitForStarted()
216        self._status_request__timer.start()
217        self.toggle_streaming()
stop_interface – Stop Unity and close sockets#
219    def stop_interface(self):
220        """Stop the Virtual Hand Interface."""
221        if not self._use_external_virtual_hand_interface__check_box.isChecked():
222            self._unity_process.kill()
223            self._unity_process.waitForFinished()
224        # In case the stop function would be called from outside the main thread we need to use invokeMethod
225        QMetaObject.invokeMethod(self._status_request__timer, "stop", Qt.QueuedConnection)
226        self.toggle_streaming()

Step 3.4: VHI Reference – Communication and Streaming#

toggle_streaming – Start/stop data streaming#
271    def toggle_streaming(self) -> None:
272        """Toggle the streaming of the Virtual Hand Interface."""
273        if self._toggle_virtual_hand_interface__push_button.isChecked():
274            self._streaming__udp_socket = QUdpSocket(self)
275            self._streaming__udp_socket.readyRead.connect(self.read_message)
276            self.outgoing_message_signal.connect(self.write_message)
277            self._streaming__udp_socket.bind(
278                QHostAddress(SOCKET_IP), MYOGESTIC_UDP_PORT
279            )
280
281            self._predicted_hand__udp_socket = QUdpSocket(self)
282            self._predicted_hand__udp_socket.bind(
283                QHostAddress(SOCKET_IP), VHI_PREDICTION__UDP_PORT
284            )
285            self._predicted_hand__udp_socket.readyRead.connect(self.read_predicted_hand)
286
287            self._last_message_time = time.time()
288        else:
289            try:
290                self._streaming__udp_socket.close()
291                self._predicted_hand__udp_socket.close()
292            except AttributeError:
293                pass
294            self._streaming__udp_socket = None
295            self._predicted_hand__udp_socket = None
296            self._is_connected = False
297            self._virtual_hand_interface__status_widget.setStyleSheet(
298                NOT_CONNECTED_STYLESHEET
299            )
read_message – Read UDP messages from the VI#
332    def read_message(self) -> None:
333        """Read a message from the Virtual Hand Interface."""
334        if self._toggle_virtual_hand_interface__push_button.isChecked():
335            while self._streaming__udp_socket.hasPendingDatagrams():
336                datagram, _, _ = self._streaming__udp_socket.readDatagram(
337                    self._streaming__udp_socket.pendingDatagramSize()
338                )
339
340                data = datagram.data().decode("utf-8")
341                if not data:
342                    return
343
344                try:
345                    if data == STATUS_RESPONSE:
346                        self._is_connected = True
347                        self._virtual_hand_interface__status_widget.setStyleSheet(
348                            CONNECTED_STYLESHEET
349                        )
350                        self._status_request_timeout__timer.stop()
351                        return
352
353                    self.incoming_message_signal.emit(np.array(ast.literal_eval(data)))
354                except (UnicodeDecodeError, SyntaxError):
355                    pass

Step 3.5: VHI Reference – Custom Signals and Data#

connect_custom_signals#
375    def connect_custom_signals(self) -> None:
376        """Connect custom signals for the Virtual Hand Interface."""
377        self.predicted_hand__signal.connect(self.online_predicted_hand_update)
get_custom_save_data – Extra recording data (predicted hand positions)#
383    def get_custom_save_data(self) -> dict:
384        """Get custom save data for the Virtual Hand Interface."""
385        return {
386            "predicted_hand": np.vstack(
387                [data for _, data in self._predicted_hand_recording__buffer],
388            ).T,
389            "predicted_hand_timings": np.array(
390                [time for time, _ in self._predicted_hand_recording__buffer],
391            ),
392        }

Step 3.6: Registration in CONFIG_REGISTRY#

Both the setup and recording interfaces are registered together. This is typically done in default_config or user_config.

from myogestic.utils.config import CONFIG_REGISTRY
from myogestic.gui.widgets.visual_interfaces.virtual_hand_interface import (
    VirtualHandInterface_SetupInterface,
    VirtualHandInterface_RecordingInterface,
)

CONFIG_REGISTRY.register_visual_interface(
    name="VHI",
    setup_interface_ui=VirtualHandInterface_SetupInterface,
    recording_interface_ui=VirtualHandInterface_RecordingInterface,
)

Gallery generated by Sphinx-Gallery