Part 1: Setup Interface#

The setup interface is the first component of a visual interface in MyoGestic.

It is responsible for configuring the parameters, initializing the system, and managing the communication between MyoGestic and the visual interface.

Step 1: Create the setup UI#

You will need an UI that will allow the user to start and stop the interface. This UI can be as simple as a button or as complex as you need.

Note

To start copy the UI files in myogestic > gui > widgets > visual_interfaces > ui and adapt them with the functionality you need.

You need to modify them with QT-Designer and convert them using UIC to a python file.

Step 2: Understand what is needed in the setup interface#

Note

A setup interface is a class that inherits from

Please read the documentation of the class and make a mental note of what you have to provide (e.g. signals, methods, attributes) and what you have to implement (e.g. start_interface, stop_interface, toggle_interface).

Step 3: Implement a setup interface (Example Virtual Hand Interface)#

This example focuses on implementing and adding the setup interface for the Virtual Hand Interface using the VirtualHandInterface_SetupInterface class from setup_interface.py. We explain how it is constructed and registered into MyoGestic via CONFIG_REGISTRY in config.py.

Steps: 1. Define a custom SetupInterface class. 2. Manage the interface’s state and initialization. 3. Handle communication with MyoGestic’s runtime. 4. Handle custom data signals and data processing. 5. Register this class in MyoGestic’s configuration registry.

Step 3.1: Define the Setup Interface Class#

This step implements the VirtualHandInterface_SetupInterface class, which initializes the Virtual Hand Interface, manages its state, and ensures communication with MyoGestic’s runtime.

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
Class Definition#
41
42class VirtualHandInterface_SetupInterface(SetupInterfaceTemplate):
43    """
44    Setup interface for the Virtual Hand Interface.
45
46    This class is responsible for setting up the Virtual Hand Interface.
47
48    Attributes
49    ----------
50    predicted_hand__signal : Signal
51        Signal that emits the predicted hand data.
52
53    Parameters
54    ----------
55    main_window : QMainWindow
56        The main window of the application.
57    name : str
58        The name of the interface. Default is "VirtualHandInterface".
59
60        .. important:: The name of the interface must be unique.
61    """
62
63    predicted_hand__signal = Signal(np.ndarray)
64
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
Helper Functions#
 99    @staticmethod
100    def _get_unity_executable() -> Path:
101        """Get the path to the Unity executable based on the platform."""
102        base_dirs = [
103            Path("dist") if not hasattr(sys, "_MEIPASS") else Path(sys._MEIPASS, "dist"),
104            Path("myogestic", "dist") if not hasattr(sys, "_MEIPASS") else Path(sys._MEIPASS, "dist"),
105        ]
106
107        unity_executable_paths = {
108            "Windows": "windows/Virtual Hand Interface.exe",
109            "Darwin": "macOS/Virtual Hand Interface.app/Contents/MacOS/Virtual Hand Interface",
110        "Linux": "linux/VirtualHandInterface.x86_64",
111    }
112
113        for base_dir in base_dirs:
114            executable = base_dir / unity_executable_paths.get(platform.system(), "")
115            if executable.exists():
116                return executable
117
118        raise FileNotFoundError(f"Unity executable not found for platform {platform.system()}.")
169    def _setup_timers(self):
170        """Setup the timers for the Virtual Hand Interface."""
171        self._status_request__timer = QTimer(self)
172        self._status_request__timer.setInterval(2000)
173        self._status_request__timer.timeout.connect(self.write_status_message)
174
175        self._status_request_timeout__timer = QTimer(self)
176        self._status_request_timeout__timer.setSingleShot(True)
177        self._status_request_timeout__timer.setInterval(1000)
178        self._status_request_timeout__timer.timeout.connect(self._update_status)
UI Setup#
180    def initialize_ui_logic(self):
181        """Initialize the UI logic for the Virtual Hand Interface."""
182        self._main_window.ui.visualInterfacesVerticalLayout.addWidget(self.ui.groupBox)
183
184        self._toggle_virtual_hand_interface__push_button: QPushButton = (
185            self.ui.toggleVirtualHandInterfacePushButton
186        )
187        self._toggle_virtual_hand_interface__push_button.clicked.connect(
188            self.toggle_virtual_hand_interface
189        )
190        self._virtual_hand_interface__status_widget: QWidget = (
191            self.ui.virtualHandInterfaceStatusWidget
192        )
193
194        self._virtual_hand_interface__status_widget.setStyleSheet(
195            NOT_CONNECTED_STYLESHEET
196        )
197        self._virtual_hand_interface__status_widget.setToolTip(
198            "Connection status: Red = Not connected, Green = Connected"
199        )
200
201        self._use_external_virtual_hand_interface__check_box: QCheckBox = (
202            self.ui.useExternalVirtualHandInterfaceCheckBox
203        )
204        self._use_external_virtual_hand_interface__check_box.setToolTip(
205            "Enable this to use an externally running Virtual Hand Interface instead of launching the built-in one"
206        )

Step 3.2: Manage the Interface’s State and Initialization#

This step manages the state of the Virtual Hand Interface and initializes the interface’s parameters. It also ensures that the interface is correctly set up and ready for use.

Start Interface - This method starts the interface.#
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 - This method stops the interface.#
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()
Interface Was Killed - This method checks if the interface was killed (e.g. by the user closing the window).#
228    def interface_was_killed(self) -> None:
229        """Handle the case when the Virtual Hand Interface was killed."""
230        self._toggle_virtual_hand_interface__push_button.setChecked(False)
231        self._toggle_virtual_hand_interface__push_button.setText("Open")
232        self._use_external_virtual_hand_interface__check_box.setEnabled(True)
233        self._virtual_hand_interface__status_widget.setStyleSheet(
234            NOT_CONNECTED_STYLESHEET
235        )
236        self._is_connected = False
Update Status - This method updates the interface’s status. If the interface is not running, it will be set to “Stopped”.#
251    def _update_status(self) -> None:
252        """Update the status of the Virtual Hand Interface."""
253        self._is_connected = False
254        self._virtual_hand_interface__status_widget.setStyleSheet(
255            NOT_CONNECTED_STYLESHEET
256        )
Toggle Virtual Hand Interface - This method toggles the Virtual Hand Interface. If the interface is running, it will be stopped. If it is stopped, it will be started.#
258    def toggle_virtual_hand_interface(self):
259        """Toggle the Virtual Hand Interface."""
260        if self._toggle_virtual_hand_interface__push_button.isChecked():
261            print("Opening Virtual Hand Interface")
262            self.start_interface()
263            self._use_external_virtual_hand_interface__check_box.setEnabled(False)
264            self._toggle_virtual_hand_interface__push_button.setText("Close")
265        else:
266            print("Closing Virtual Hand Interface")
267            self.stop_interface()
268            self._use_external_virtual_hand_interface__check_box.setEnabled(True)
269            self._toggle_virtual_hand_interface__push_button.setText("Open")

Step 3.3: Handle Communication with MyoGestic#

This step manages the communication between the Virtual Hand Interface and MyoGestic’s runtime. It ensures that the interface is correctly set up and ready for use.

Toggle Streaming - This method toggles the streaming of data from MyoGestic to the Virtual Hand Interface.#
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 Predicted Hand - This method reads the predicted hand from MyoGestic.#
301    def read_predicted_hand(self) -> None:
302        """Read the predicted hand data from the Virtual Hand Interface."""
303        while self._predicted_hand__udp_socket.hasPendingDatagrams():
304            datagram, _, _ = self._predicted_hand__udp_socket.readDatagram(
305                self._predicted_hand__udp_socket.pendingDatagramSize()
306            )
307
308            data = datagram.data().decode("utf-8")
309            if not data:
310                return
311
312            self.predicted_hand__signal.emit(np.array(ast.literal_eval(data)))
Read/Write Message - This methods read and write messages from/to MyoGestic.#
330    def read_message(self) -> None:
331        """Read a message from the Virtual Hand Interface."""
332        if self._toggle_virtual_hand_interface__push_button.isChecked():
333            while self._streaming__udp_socket.hasPendingDatagrams():
334                datagram, _, _ = self._streaming__udp_socket.readDatagram(
335                    self._streaming__udp_socket.pendingDatagramSize()
336                )
337
338                data = datagram.data().decode("utf-8")
339                if not data:
340                    return
341
342                try:
343                    if data == STATUS_RESPONSE:
344                        self._is_connected = True
345                        self._virtual_hand_interface__status_widget.setStyleSheet(
346                            CONNECTED_STYLESHEET
347                        )
348                        self._status_request_timeout__timer.stop()
349                        return
350
351                    self.incoming_message_signal.emit(np.array(ast.literal_eval(data)))
352                except (UnicodeDecodeError, SyntaxError):
353                    pass
314    def write_message(self, message: QByteArray) -> None:
315        """Write a message to the Virtual Hand Interface."""
316        if self._is_connected and (
317            time.time() - self._last_message_time >= TIME_BETWEEN_MESSAGES
318        ):
319            self._last_message_time = time.time()
320            output_bytes = self._streaming__udp_socket.writeDatagram(
321                message, QHostAddress(SOCKET_IP), VHI__UDP_PORT
322            )
323
324            if output_bytes == -1:
325                self._main_window.logger.print(
326                    "Error in sending message to Virtual Hand Interface!",
327                    level=LoggerLevel.ERROR,
328                )
355    def write_status_message(self) -> None:
356        """Write a status message to the Virtual Hand Interface."""
357        if self._toggle_virtual_hand_interface__push_button.isChecked():
358            output_bytes = self._streaming__udp_socket.writeDatagram(
359                STATUS_REQUEST.encode("utf-8"),
360                QHostAddress(SOCKET_IP),
361                VHI__UDP_PORT,
362            )
363
364            if output_bytes == -1:
365                self._main_window.logger.print(
366                    "Error in sending status message to Virtual Hand Interface!",
367                    level=LoggerLevel.ERROR,
368                )
369                return
370
371            self._status_request_timeout__timer.start()

Step 3.4: Handle Custom Data Signals#

This step manages custom data signals and data processing for the Virtual Hand Interface. It ensures that the interface is correctly set up and ready for use.

Connect/Disconnect Custom Signals - This method connects/disconnects custom signals.#
373    def connect_custom_signals(self) -> None:
374        """Connect custom signals for the Virtual Hand Interface."""
375        self.predicted_hand__signal.connect(self.online_predicted_hand_update)
Online Predicted Hand Update - This method updates the predicted hand buffer with the current data.#
396    def online_predicted_hand_update(self, data: np.ndarray) -> None:
397        """Update the predicted hand data for the online protocol."""
398        if self._online_protocol.online_record_toggle_push_button.isChecked():
399            self._predicted_hand_recording__buffer.append(
400                (time.time() - self._online_protocol.recording_start_time, data)
401            )
Clear Custom Signal Buffers - This method clears the custom signal buffers.#
392    def clear_custom_signal_buffers(self) -> None:
393        """Clear custom signal buffers for the Virtual Hand Interface."""
394        self._predicted_hand_recording__buffer = []
Get Custom Save Data - This method will be called when saving the interface’s data. This should return a dictionary with the data that needs to be saved.#
381    def get_custom_save_data(self) -> dict:
382        """Get custom save data for the Virtual Hand Interface."""
383        return {
384            "predicted_hand": np.vstack(
385                [data for _, data in self._predicted_hand_recording__buffer],
386            ).T,
387            "predicted_hand_timings": np.array(
388                [time for time, _ in self._predicted_hand_recording__buffer],
389            ),
390        }

Step 3.5: Register the Setup Interface in Config#

This step integrates the VirtualHandInterface_SetupInterface class into the MyoGestic configuration. Once registered, the interface becomes available for use in the framework.

from myogestic.gui.widgets.visual_interfaces.virtual_hand_interface import VirtualHandInterface_SetupInterface
from myogestic.utils.config import CONFIG_REGISTRY


CONFIG_REGISTRY.register_visual_interface(
    name="VirtualHandInterface",  # Unique identifier for the visual interface.
    setup_interface_ui=VirtualHandInterface_SetupInterface,  # Associated setup class.
    recording_interface_ui=None,  # Placeholder (can be extended with a recording interface).
)

Gallery generated by Sphinx-Gallery